137 lines
4.6 KiB
Python
137 lines
4.6 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping
|
|
from typing import Any
|
|
|
|
from wf_core.errors import WorkflowExecutionError
|
|
from wf_core.models.steps import InterruptNode
|
|
from wf_core.models.workflow import Workflow
|
|
from wf_core.run_state import (
|
|
FrameStatus,
|
|
InterruptRequest,
|
|
InterruptRoute,
|
|
RunState,
|
|
StepExecutionResult,
|
|
)
|
|
from wf_core.runtime.input_bindings import resolve_step_input_bindings
|
|
from wf_core.runtime.lineage import commit_foreach_aware_patch
|
|
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.overlays import state_view_for_frame
|
|
from wf_core.runtime.ops.schemas import validate_payload_against_schema
|
|
from wf_core.runtime.ops.state import build_output_patch
|
|
|
|
|
|
def build_interrupt_request(
|
|
node: InterruptNode,
|
|
*,
|
|
frame_id: str,
|
|
state: dict[str, Any],
|
|
workflow_input: dict[str, Any],
|
|
context: dict[str, Any],
|
|
public_frame_id: str | None = None,
|
|
public_node_id: str | None = None,
|
|
route: InterruptRoute | None = None,
|
|
) -> InterruptRequest:
|
|
payload = resolve_step_input_bindings(
|
|
node.request,
|
|
state=state,
|
|
workflow_input=workflow_input,
|
|
context=context,
|
|
label=f"interrupt {node.id!r} request",
|
|
)
|
|
validate_payload_against_schema(
|
|
node.request_schema,
|
|
payload,
|
|
f"interrupt request for {node.id}",
|
|
)
|
|
return InterruptRequest(
|
|
id=f"interrupt:{public_node_id or node.id}",
|
|
frame_id=public_frame_id or frame_id,
|
|
node_id=public_node_id or node.id,
|
|
kind=node.kind,
|
|
payload=payload,
|
|
route=route,
|
|
outcomes=list(node.outcomes),
|
|
request_schema=dict(node.request_schema),
|
|
resume_schema=dict(node.resume_schema),
|
|
typed=node.has_explicit_contract,
|
|
)
|
|
|
|
|
|
def resume_interrupt(
|
|
workflow: Workflow,
|
|
run: RunState,
|
|
*,
|
|
index: WorkflowIndex,
|
|
resume_payload: dict[str, Any],
|
|
resume_outcome: str,
|
|
reducers: Mapping[str, ReducerDefinition] | None = None,
|
|
) -> None:
|
|
if run.interrupt is None:
|
|
raise WorkflowExecutionError("run is interrupted but has no interrupt request")
|
|
|
|
route = run.interrupt.route
|
|
if route is not None:
|
|
frame = run.frames.get(route.frame_id)
|
|
if (
|
|
frame is None
|
|
or frame.scope_id != route.scope_id
|
|
or frame.lineage_id != route.lineage_id
|
|
or frame.node_id != route.node_id
|
|
or frame.status != FrameStatus.INTERRUPTED
|
|
):
|
|
raise WorkflowExecutionError("child interrupt route is no longer resumable")
|
|
run.current_frame_id = frame.id
|
|
run.sync_from_current_frame()
|
|
else:
|
|
if run.current_frame_id is None:
|
|
raise WorkflowExecutionError("interrupted run has no current frame")
|
|
if run.current_node_id is None:
|
|
raise WorkflowExecutionError("interrupted run has no current node")
|
|
frame = run.current_frame()
|
|
step = index.nodes_by_id[frame.node_id]
|
|
if not isinstance(step, InterruptNode):
|
|
raise WorkflowExecutionError(
|
|
f"interrupted run expected interrupt node, got {step.type!r}"
|
|
)
|
|
if resume_outcome not in step.outcomes:
|
|
raise WorkflowExecutionError(
|
|
f"interrupt node {step.id!r} does not declare resume outcome {resume_outcome!r}"
|
|
)
|
|
|
|
validate_payload_against_schema(
|
|
run.interrupt.resume_schema,
|
|
resume_payload,
|
|
f"interrupt resume for {step.id}",
|
|
)
|
|
|
|
patch = build_output_patch(
|
|
workflow,
|
|
step.resume,
|
|
resume_payload,
|
|
state_view_for_frame(run, frame),
|
|
reducers=reducers,
|
|
missing_field_message="interrupt resume payload is missing required field {field}",
|
|
)
|
|
# Foreach-aware routing: a serial item resume commits through the parent
|
|
# scope, a concurrent one buffers in the item lineage for barrier merge.
|
|
state_changes = commit_foreach_aware_patch(run, frame, patch)
|
|
next_node_id = index.next_node_id(frame.node_id, resume_outcome)
|
|
append_step_result_trace(
|
|
run,
|
|
frame_id=frame.id,
|
|
node_id=frame.node_id,
|
|
step_type=step.type,
|
|
next_node_id=next_node_id,
|
|
result=StepExecutionResult(
|
|
outcome=resume_outcome,
|
|
resolved_input=resume_payload,
|
|
output=resume_payload,
|
|
state_changes=state_changes,
|
|
),
|
|
)
|
|
run.interrupt = None
|
|
advance_frame(run, frame, outcome=resume_outcome, next_node_id=next_node_id)
|