interrupt to get the same treatment too!
This commit is contained in:
@@ -143,10 +143,48 @@ class InterruptNode(BaseModel):
|
||||
id: str
|
||||
type: Literal["interrupt"]
|
||||
kind: str
|
||||
request_map: dict[str, str] = Field(default_factory=dict)
|
||||
out_map: dict[str, str] = Field(default_factory=dict)
|
||||
request: list[InputBinding] = Field(default_factory=list)
|
||||
resume: list[OutputBinding] = Field(default_factory=list)
|
||||
outcomes: list[str] = Field(default_factory=lambda: ["submitted"])
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _coerce_deprecated_maps(cls, data: object) -> object:
|
||||
"""Normalize legacy interrupt maps into canonical parse-only bindings."""
|
||||
if not isinstance(data, Mapping):
|
||||
return data
|
||||
|
||||
old_fields = ("request_map", "out_map")
|
||||
has_canonical = "request" in data or "resume" in data
|
||||
present_old_fields = [field for field in old_fields if field in data]
|
||||
if has_canonical and present_old_fields:
|
||||
old_names = ", ".join(present_old_fields)
|
||||
raise ValueError(
|
||||
f"cannot mix canonical request/resume with deprecated fields: {old_names}"
|
||||
)
|
||||
|
||||
normalized = dict(data)
|
||||
request_bindings = list(normalized.pop("request", []))
|
||||
resume_bindings = list(normalized.pop("resume", []))
|
||||
|
||||
request_map = NodeUse._deprecated_mapping(
|
||||
normalized.pop("request_map", {}), field_name="request_map"
|
||||
)
|
||||
out_map = NodeUse._deprecated_mapping(
|
||||
normalized.pop("out_map", {}), field_name="out_map"
|
||||
)
|
||||
|
||||
request_bindings.extend(
|
||||
{"target": target, "path": path} for path, target in request_map.items()
|
||||
)
|
||||
resume_bindings.extend(
|
||||
{"source": source, "target": target} for source, target in out_map.items()
|
||||
)
|
||||
|
||||
normalized["request"] = request_bindings
|
||||
normalized["resume"] = resume_bindings
|
||||
return normalized
|
||||
|
||||
|
||||
Step = Annotated[
|
||||
NodeUse | ConditionNode | ForeachNode | JoinNode | InterruptNode,
|
||||
|
||||
@@ -5,13 +5,14 @@ from typing import Any
|
||||
|
||||
from wf_core.conditions import safe_resolve_path
|
||||
from wf_core.errors import WorkflowExecutionError
|
||||
from wf_core.models.steps import InterruptNode
|
||||
from wf_core.local_paths import LocalPathError, set_local_value
|
||||
from wf_core.models.steps import InputPathBinding, InputValueBinding, InterruptNode
|
||||
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
|
||||
from wf_core.runtime.ops.state import apply_output_bindings
|
||||
|
||||
|
||||
def build_interrupt_request(
|
||||
@@ -22,15 +23,25 @@ def build_interrupt_request(
|
||||
workflow_input: dict[str, Any],
|
||||
context: dict[str, Any],
|
||||
) -> InterruptRequest:
|
||||
payload = {
|
||||
payload_field: safe_resolve_path(
|
||||
source_path,
|
||||
state=state,
|
||||
workflow_input=workflow_input,
|
||||
context=context,
|
||||
)
|
||||
for source_path, payload_field in node.request_map.items()
|
||||
}
|
||||
payload: dict[str, Any] = {}
|
||||
for binding in node.request:
|
||||
if isinstance(binding, InputValueBinding):
|
||||
value = binding.value
|
||||
elif isinstance(binding, InputPathBinding):
|
||||
value = safe_resolve_path(
|
||||
str(binding.path),
|
||||
state=state,
|
||||
workflow_input=workflow_input,
|
||||
context=context,
|
||||
)
|
||||
else:
|
||||
raise WorkflowExecutionError(
|
||||
f"unsupported request binding for interrupt {node.id!r}"
|
||||
)
|
||||
try:
|
||||
set_local_value(payload, binding.target, value)
|
||||
except LocalPathError as exc:
|
||||
raise WorkflowExecutionError(str(exc)) from exc
|
||||
return InterruptRequest(
|
||||
id=f"interrupt:{node.id}",
|
||||
frame_id=frame_id,
|
||||
@@ -67,10 +78,10 @@ def resume_interrupt(
|
||||
f"interrupt node {step.id!r} does not declare resume outcome {resume_outcome!r}"
|
||||
)
|
||||
|
||||
state_changes = apply_mapped_state(
|
||||
state_changes = apply_output_bindings(
|
||||
workflow,
|
||||
step.resume,
|
||||
resume_payload,
|
||||
step.out_map,
|
||||
run.state,
|
||||
reducers=reducers,
|
||||
missing_field_message="interrupt resume payload is missing required field {field}",
|
||||
|
||||
@@ -165,31 +165,38 @@ def validate_interrupt_node(
|
||||
state_root_fields: set[str],
|
||||
input_root_fields: set[str],
|
||||
) -> None:
|
||||
for source_path, payload_field in node.request_map.items():
|
||||
if not payload_field:
|
||||
for binding_index, binding in enumerate(node.request):
|
||||
field_path = f"nodes[{index}].request[{binding_index}]"
|
||||
if not str(binding.target):
|
||||
report.add(
|
||||
ValidationIssueCode.INVALID_INTERRUPT_SOURCE,
|
||||
f"nodes[{index}].request_map[{source_path!r}]",
|
||||
field_path,
|
||||
"interrupt request payload field must not be empty",
|
||||
)
|
||||
if not is_valid_source_path(source_path, state_root_fields, input_root_fields):
|
||||
if isinstance(binding, InputPathBinding) and not is_valid_source_path(
|
||||
binding.path,
|
||||
state_root_fields,
|
||||
input_root_fields,
|
||||
allow_context=True,
|
||||
):
|
||||
report.add(
|
||||
ValidationIssueCode.INVALID_INTERRUPT_SOURCE,
|
||||
f"nodes[{index}].request_map[{source_path!r}]",
|
||||
"interrupt request source must start with input. or state. and reference a declared root field",
|
||||
field_path,
|
||||
"interrupt request source must start with input., state., or context. and reference a declared root field when applicable",
|
||||
)
|
||||
|
||||
for resume_field, destination_path in node.out_map.items():
|
||||
if not resume_field:
|
||||
for binding_index, binding in enumerate(node.resume):
|
||||
field_path = f"nodes[{index}].resume[{binding_index}]"
|
||||
if not str(binding.source):
|
||||
report.add(
|
||||
ValidationIssueCode.INVALID_INTERRUPT_DESTINATION,
|
||||
f"nodes[{index}].out_map[{resume_field!r}]",
|
||||
field_path,
|
||||
"interrupt resume field must not be empty",
|
||||
)
|
||||
if not is_valid_destination_path(destination_path):
|
||||
if not is_valid_destination_path(binding.target):
|
||||
report.add(
|
||||
ValidationIssueCode.INVALID_INTERRUPT_DESTINATION,
|
||||
f"nodes[{index}].out_map[{resume_field!r}]",
|
||||
field_path,
|
||||
"interrupt resume destination must start with state.",
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user