Files
lda-wf/src/wf_core/runtime/ops/handlers.py
T

109 lines
3.5 KiB
Python

from __future__ import annotations
from wf_core.conditions import eval_condition
from wf_core.errors import WorkflowExecutionError
from wf_core.models.steps import ConditionNode, InterruptNode
from wf_core.run_state import (
ROOT_SCOPE_ID,
ExecutionFrame,
FrameStatus,
InterruptRoute,
RunState,
RunStatus,
StepExecutionResult,
)
from wf_core.runtime.lineage import scope_input_for_frame
from wf_core.runtime.ops.flow import append_trace
from wf_core.runtime.ops.frames import frame_context_view
from wf_core.runtime.ops.interrupts import build_interrupt_request
from wf_core.runtime.ops.overlays import state_view_for_frame
def handle_condition_step(
run: RunState,
step: ConditionNode,
) -> StepExecutionResult:
frame = run.current_frame()
predicate = eval_condition(
step.check,
state_view_for_frame(run, frame),
scope_input_for_frame(run, frame),
# Structured context must match what validation blesses: conditions
# inside a foreach body can read context.foreach.* / loop aliases.
context=frame_context_view(run, frame).graph,
)
outcome = "true" if predicate else "false"
return StepExecutionResult(
outcome=outcome,
resolved_input={},
output={"predicate": predicate},
state_changes={},
)
def handle_interrupt_step(
run: RunState,
step: InterruptNode,
) -> RunState:
frame = run.current_frame()
public_frame = frame
route = None
if frame.scope_id != ROOT_SCOPE_ID:
public_frame = _owning_subgraph_frame(run, frame)
scope = run.scopes.get(frame.scope_id)
if scope is None or scope.workflow_ref is None:
raise WorkflowExecutionError(
f"child interrupt frame {frame.id!r} has no workflow scope"
)
route = InterruptRoute(
frame_id=frame.id,
node_id=frame.node_id,
scope_id=frame.scope_id,
lineage_id=frame.lineage_id,
parent_frame_id=public_frame.id,
workflow_ref=scope.workflow_ref,
)
interrupt_request = build_interrupt_request(
step,
frame_id=frame.id,
state=state_view_for_frame(run, frame),
workflow_input=scope_input_for_frame(run, frame),
context=dict(frame_context_view(run, frame).graph),
public_frame_id=public_frame.id,
public_node_id=public_frame.node_id,
route=route,
)
run.interrupt = interrupt_request
run.status = RunStatus.INTERRUPTED
frame.status = FrameStatus.INTERRUPTED
append_trace(
run,
frame_id=frame.id,
node_id=frame.node_id,
step_type=step.type,
resolved_input=interrupt_request.payload,
outcome="interrupt",
next_node_id=frame.node_id,
output=interrupt_request.payload,
state_changes={},
)
return run
def _owning_subgraph_frame(run: RunState, frame: ExecutionFrame) -> ExecutionFrame:
"""Return the graph-boundary frame that owns one child-scope interrupt."""
cursor = frame
while cursor.parent_frame_id is not None:
parent = run.frames.get(cursor.parent_frame_id)
if parent is None:
raise WorkflowExecutionError(
f"child interrupt frame {frame.id!r} references missing parent "
f"{cursor.parent_frame_id!r}"
)
if parent.scope_id != frame.scope_id:
return parent
cursor = parent
raise WorkflowExecutionError(
f"child interrupt frame {frame.id!r} has no parent subgraph"
)