first reducer sweep of failure

i will put second reducer sweep of logic error or second of success
This commit is contained in:
lda
2026-05-17 19:10:21 +07:00 Verified
parent 4339b8aa74
commit 23dc684c23
20 changed files with 364 additions and 43 deletions
+4
View File
@@ -46,8 +46,12 @@ def eval_condition(
return left != right
if condition.op == "gt":
return left > right
if condition.op == "ge":
return left >= right
if condition.op == "lt":
return left < right
if condition.op == "le":
return left <= right
raise WorkflowExecutionError(f"unsupported condition operator {condition.op!r}")
+1 -1
View File
@@ -45,7 +45,7 @@ class VariadicCondition(BaseModel):
class BinaryCondition(BaseModel):
"""Condition that compares two operands."""
op: Literal["eq", "ne", "gt", "lt"]
op: Literal["eq", "ne", "gt", "ge", "lt", "le"]
left: PathOperand | LiteralOperand
right: PathOperand | LiteralOperand
+13 -2
View File
@@ -6,6 +6,7 @@ from typing import Any
from wf_core.models.workflow import Workflow
from wf_core.runtime.ops.flow import finalize_run
from wf_core.runtime.ops.frames import collapse_completed_frames
from wf_core.runtime.ops.merges import ReducerDefinition
from wf_core.runtime.ops.nodes import AsyncNodeHandler, NodeHandler
from wf_core.runtime.ops.runs import create_run_state
from wf_core.run_state import RunState, RunStatus
@@ -19,13 +20,15 @@ def execute_workflow(
workflow: Workflow,
workflow_input: dict[str, Any],
registry: Mapping[str, NodeHandler],
*,
reducers: Mapping[str, ReducerDefinition] | None = None,
) -> RunState:
"""Create a run and execute a workflow synchronously until it stops."""
run = create_run_state(workflow, workflow_input)
try:
run = prepare_new_run(workflow, workflow_input)
return resume_workflow(workflow, run, registry)
return resume_workflow(workflow, run, registry, reducers=reducers)
except Exception as exc:
run.status = RunStatus.FAILED
run.error = str(exc)
@@ -36,13 +39,15 @@ async def execute_workflow_async(
workflow: Workflow,
workflow_input: dict[str, Any],
registry: Mapping[str, AsyncNodeHandler],
*,
reducers: Mapping[str, ReducerDefinition] | None = None,
) -> RunState:
"""Create a run and execute a workflow asynchronously until it stops."""
run = create_run_state(workflow, workflow_input)
try:
run = prepare_new_run(workflow, workflow_input)
return await resume_workflow_async(workflow, run, registry)
return await resume_workflow_async(workflow, run, registry, reducers=reducers)
except Exception as exc:
run.status = RunStatus.FAILED
run.error = str(exc)
@@ -56,6 +61,7 @@ def resume_workflow(
*,
resume_payload: dict[str, Any] | None = None,
resume_outcome: str = "submitted",
reducers: Mapping[str, ReducerDefinition] | None = None,
) -> RunState:
"""Resume a synchronous run from its current state."""
index = prepare_resume(
@@ -63,6 +69,7 @@ def resume_workflow(
run,
resume_payload=resume_payload,
resume_outcome=resume_outcome,
reducers=reducers,
)
if index is None:
if run.current_node_id == END:
@@ -78,6 +85,7 @@ def resume_workflow(
run,
registry,
index=index,
reducers=reducers,
)
if run.status == RunStatus.INTERRUPTED:
return run
@@ -92,6 +100,7 @@ async def resume_workflow_async(
*,
resume_payload: dict[str, Any] | None = None,
resume_outcome: str = "submitted",
reducers: Mapping[str, ReducerDefinition] | None = None,
) -> RunState:
"""Resume an async run from its current state."""
index = prepare_resume(
@@ -99,6 +108,7 @@ async def resume_workflow_async(
run,
resume_payload=resume_payload,
resume_outcome=resume_outcome,
reducers=reducers,
)
if index is None:
if run.current_node_id == END:
@@ -114,6 +124,7 @@ async def resume_workflow_async(
run,
registry,
index=index,
reducers=reducers,
)
if run.status == RunStatus.INTERRUPTED:
return run
+4
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from wf_core.conditions import safe_resolve_path
@@ -9,6 +10,7 @@ from wf_core.models.workflow import Workflow
from wf_core.run_state import InterruptRequest, RunState, StepExecutionResult
from wf_core.runtime.ops.flow import advance_frame, append_step_result_trace
from wf_core.runtime.ops.index import WorkflowIndex
from wf_core.runtime.ops.merges import ReducerDefinition
from wf_core.runtime.ops.state import apply_mapped_state
@@ -45,6 +47,7 @@ def resume_interrupt(
index: WorkflowIndex,
resume_payload: dict[str, Any],
resume_outcome: str,
reducers: Mapping[str, ReducerDefinition] | None = None,
) -> None:
if run.current_frame_id is None:
raise WorkflowExecutionError("interrupted run has no current frame")
@@ -69,6 +72,7 @@ def resume_interrupt(
resume_payload,
step.out_map,
run.state,
reducers=reducers,
missing_field_message="interrupt resume payload is missing required field {field}",
)
next_node_id = index.next_node_id(frame.node_id, resume_outcome)
+10 -3
View File
@@ -149,10 +149,17 @@ def apply_reducer(
current_value: Any,
incoming_value: Any,
destination_path: str,
reducers: Mapping[str, ReducerDefinition] = DEFAULT_REDUCER_DEFINITIONS,
reducers: Mapping[str, ReducerDefinition] | None = None,
) -> Any:
"""Apply one named pure reducer to a state write."""
definition = reducers.get(reducer.name)
"""Apply one named pure reducer to a state write.
Injected reducer definitions are additive over the built-ins so authoring
tests and local packages can provide custom reducers without re-registering
every `wf.std.*` reducer.
"""
definition = None if reducers is None else reducers.get(reducer.name)
if definition is None:
definition = DEFAULT_REDUCER_DEFINITIONS.get(reducer.name)
if definition is None:
raise WorkflowExecutionError(f"unknown reducer {reducer.name!r}")
return definition.apply(
+13 -1
View File
@@ -12,6 +12,7 @@ from wf_core.models.steps import NodeUse
from wf_core.models.workflow import Workflow
from wf_core.run_state import RunState, RuntimeContext, StepExecutionResult
from wf_core.runtime.ops.frames import frame_context_values
from wf_core.runtime.ops.merges import ReducerDefinition
from wf_core.runtime.ops.schemas import validate_payload_against_schema
from wf_core.runtime.ops.state import apply_output_map
@@ -65,6 +66,7 @@ def _finalize_node_execution(
node_def: NodeDef,
resolved_input: dict[str, Any],
raw_result: NodeResult | dict[str, Any],
reducers: Mapping[str, ReducerDefinition] | None = None,
) -> StepExecutionResult:
result = coerce_node_result(raw_result)
@@ -76,7 +78,13 @@ def _finalize_node_execution(
validate_payload_against_schema(
node_def.output_schema, result.output, f"node output for {node.id}"
)
state_changes = apply_output_map(workflow, node, result.output, run.state)
state_changes = apply_output_map(
workflow,
node,
result.output,
run.state,
reducers=reducers,
)
return StepExecutionResult(
outcome=result.outcome,
resolved_input=resolved_input,
@@ -91,6 +99,7 @@ def execute_node_use(
node: NodeUse,
node_def: NodeDef,
registry: Mapping[str, NodeHandler],
reducers: Mapping[str, ReducerDefinition] | None = None,
) -> StepExecutionResult:
handler = registry.get(node.node)
if handler is None:
@@ -112,6 +121,7 @@ def execute_node_use(
node_def=node_def,
resolved_input=resolved_input,
raw_result=raw_result,
reducers=reducers,
)
@@ -121,6 +131,7 @@ async def execute_node_use_async(
node: NodeUse,
node_def: NodeDef,
registry: Mapping[str, AsyncNodeHandler],
reducers: Mapping[str, ReducerDefinition] | None = None,
) -> StepExecutionResult:
handler = registry.get(node.node)
if handler is None:
@@ -146,6 +157,7 @@ async def execute_node_use_async(
node_def=node_def,
resolved_input=resolved_input,
raw_result=cast(NodeResult | dict[str, Any], raw_result),
reducers=reducers,
)
+22 -4
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from wf_core.errors import WorkflowExecutionError
@@ -13,7 +14,7 @@ from wf_core.paths import (
set_nested_value,
split_graph_path,
)
from wf_core.runtime.ops.merges import apply_reducer
from wf_core.runtime.ops.merges import ReducerDefinition, apply_reducer
def apply_output_map(
@@ -21,12 +22,14 @@ def apply_output_map(
node: NodeUse,
node_output: dict[str, Any],
state: dict[str, Any],
reducers: Mapping[str, ReducerDefinition] | None = None,
) -> dict[str, Any]:
return apply_mapped_state(
workflow,
node_output,
node.out_map,
state,
reducers=reducers,
missing_field_message=f"node {node.id!r} did not return required mapped field {{field}}",
)
@@ -37,6 +40,7 @@ def apply_mapped_state(
mapping: dict[str, str],
state: dict[str, Any],
*,
reducers: Mapping[str, ReducerDefinition] | None = None,
missing_field_message: str,
) -> dict[str, Any]:
if has_overlapping_paths(mapping.values()):
@@ -55,12 +59,23 @@ def apply_mapped_state(
patch[destination_path] = value
for destination_path, value in patch.items():
write_state_value(workflow, state, destination_path, value)
write_state_value(
workflow,
state,
destination_path,
value,
reducers=reducers,
)
return dict(patch)
def write_state_value(
workflow: Workflow, state: dict[str, Any], destination_path: str, value: Any
workflow: Workflow,
state: dict[str, Any],
destination_path: str,
value: Any,
*,
reducers: Mapping[str, ReducerDefinition] | None = None,
) -> None:
try:
root, parts = split_graph_path(destination_path)
@@ -74,7 +89,9 @@ def write_state_value(
declared_path = ".".join(parts)
declared_field = workflow.state_schema.fields.get(declared_path)
reducer = declared_field.reducer if declared_field else ReducerRef(name="wf.std.replace")
reducer = (
declared_field.reducer if declared_field else ReducerRef(name="wf.std.replace")
)
key_path = parts
current_value = get_nested_value(state, key_path)
merged_value = apply_reducer(
@@ -82,6 +99,7 @@ def write_state_value(
current_value=current_value,
incoming_value=value,
destination_path=destination_path,
reducers=reducers,
)
safe_set_nested_value(state, key_path, merged_value)
+4
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from wf_core.errors import WorkflowExecutionError
@@ -7,6 +8,7 @@ from wf_core.models.workflow import Workflow
from wf_core.runtime.ops.frames import collapse_completed_frames
from wf_core.runtime.ops.index import WorkflowIndex, build_workflow_index
from wf_core.runtime.ops.interrupts import resume_interrupt
from wf_core.runtime.ops.merges import ReducerDefinition
from wf_core.runtime.ops.runs import create_run_state
from wf_core.runtime.ops.schemas import validate_payload_against_schema
from wf_core.run_state import FrameStatus, RunState, RunStatus
@@ -29,6 +31,7 @@ def prepare_resume(
*,
resume_payload: dict[str, Any] | None,
resume_outcome: str,
reducers: Mapping[str, ReducerDefinition] | None = None,
) -> WorkflowIndex | None:
"""Validate and normalize a run state before resume execution."""
if run.workflow_name != workflow.name:
@@ -57,6 +60,7 @@ def prepare_resume(
index=index,
resume_payload=resume_payload,
resume_outcome=resume_outcome,
reducers=reducers,
)
collapse_completed_frames(run)
if run.current_node_id == END:
+17 -2
View File
@@ -20,6 +20,7 @@ from wf_core.runtime.ops.handlers import (
handle_join_step,
)
from wf_core.runtime.ops.index import WorkflowIndex
from wf_core.runtime.ops.merges import ReducerDefinition
from wf_core.runtime.ops.nodes import (
AsyncNodeHandler,
NodeHandler,
@@ -67,6 +68,7 @@ def step_workflow(
registry: Mapping[str, NodeHandler],
*,
index: WorkflowIndex | None = None,
reducers: Mapping[str, ReducerDefinition] | None = None,
) -> RunState:
"""Execute at most one synchronous workflow step."""
prepared = prepare_step(workflow, run, index)
@@ -77,7 +79,14 @@ def step_workflow(
if isinstance(step, NodeUse):
node_def = index.node_defs[step.node]
step_result = execute_node_use(workflow, run, step, node_def, registry)
step_result = execute_node_use(
workflow,
run,
step,
node_def,
registry,
reducers=reducers,
)
elif isinstance(step, ConditionNode):
step_result = handle_condition_step(run, step)
elif isinstance(step, JoinNode):
@@ -108,6 +117,7 @@ async def step_workflow_async(
registry: Mapping[str, AsyncNodeHandler],
*,
index: WorkflowIndex | None = None,
reducers: Mapping[str, ReducerDefinition] | None = None,
) -> RunState:
"""Execute at most one async workflow step."""
prepared = prepare_step(workflow, run, index)
@@ -119,7 +129,12 @@ async def step_workflow_async(
if isinstance(step, NodeUse):
node_def = index.node_defs[step.node]
step_result = await execute_node_use_async(
workflow, run, step, node_def, registry
workflow,
run,
step,
node_def,
registry,
reducers=reducers,
)
elif isinstance(step, ConditionNode):
step_result = handle_condition_step(run, step)