execution frame carry a lineage id

This commit is contained in:
lda
2026-05-24 22:51:12 +07:00 Verified
parent 4576127d09
commit e48656c165
7 changed files with 236 additions and 3 deletions
+20 -1
View File
@@ -71,6 +71,7 @@ class PendingItemResult:
index: int
frame_id: str
status: Literal["succeeded", "failed"]
lineage_id: str | None = None
patch: StatePatch = field(default_factory=StatePatch)
error: ItemErrorRecord | None = None
@@ -88,10 +89,13 @@ class PendingItemResult:
) from exc
patch_changes = raw.get("patch_changes", {})
patch_writes = raw.get("patch_writes")
lineage_id = raw.get("lineage_id")
if not isinstance(index, int) or index < 0:
raise WorkflowExecutionError("malformed pending foreach result index")
if not isinstance(frame_id, str):
raise WorkflowExecutionError("malformed pending foreach result frame id")
if lineage_id is not None and not isinstance(lineage_id, str):
raise WorkflowExecutionError("malformed pending foreach result lineage id")
if status not in {"succeeded", "failed"}:
raise WorkflowExecutionError("malformed pending foreach result status")
if not isinstance(patch_changes, dict):
@@ -103,6 +107,7 @@ class PendingItemResult:
index=index,
frame_id=frame_id,
status=status,
lineage_id=lineage_id,
patch=(
StatePatch(
writes=[_state_write_from_metadata(item) for item in patch_writes]
@@ -122,6 +127,7 @@ class PendingItemResult:
"index": self.index,
"frame_id": self.frame_id,
"status": self.status,
"lineage_id": self.lineage_id,
"patch_changes": dict(self.patch.changes),
"patch_writes": [
_state_write_to_metadata(write) for write in self.patch.writes
@@ -251,7 +257,12 @@ class ForeachBarrierState:
)
def add_success_patch(
self, *, index: int, frame_id: str, patch: StatePatch
self,
*,
index: int,
frame_id: str,
patch: StatePatch,
lineage_id: str | None = None,
) -> None:
"""Buffer or extend successful item patches by item index.
@@ -266,6 +277,7 @@ class ForeachBarrierState:
index=index,
frame_id=frame_id,
status="succeeded",
lineage_id=lineage_id,
patch=patch,
)
return
@@ -274,6 +286,13 @@ class ForeachBarrierState:
f"foreach item result for index {index!r} belongs to frame "
f"{existing.frame_id!r}, got {frame_id!r}"
)
if lineage_id is not None and existing.lineage_id not in {None, lineage_id}:
raise WorkflowExecutionError(
f"foreach item result for index {index!r} belongs to lineage "
f"{existing.lineage_id!r}, got {lineage_id!r}"
)
if existing.lineage_id is None:
existing.lineage_id = lineage_id
existing.patch.extend(patch)
def add_failure(self, *, error: ItemErrorRecord) -> None:
+17
View File
@@ -81,6 +81,7 @@ def _step_foreach_serial(
barrier.next_index = loop_index + 1
barrier.save_to_frame(frame, step.id)
child_id = f"{frame.id}:{step.id}:{loop_index}"
child_lineage_id = _child_lineage_id(frame, step, loop_index)
add_frame(
run,
ExecutionFrame(
@@ -89,6 +90,9 @@ def _step_foreach_serial(
node_id=loop_start,
status=FrameStatus.PENDING,
parent_frame_id=frame.id,
scope_id=frame.scope_id,
lineage_id=child_lineage_id,
parent_lineage_id=frame.lineage_id,
metadata=ForeachIterationMetadata(
foreach_node_id=step.id,
loop_index=loop_index,
@@ -243,6 +247,7 @@ def _admit_concurrent_children(
loop_index = barrier.next_index
item = iterable[loop_index]
child_id = f"{frame.id}:{step.id}:{loop_index}"
child_lineage_id = _child_lineage_id(frame, step, loop_index)
active_count = len(barrier.active_frame_ids)
barrier.next_index = loop_index + 1
barrier.start_child(child_id)
@@ -254,6 +259,9 @@ def _admit_concurrent_children(
node_id=loop_start,
status=FrameStatus.PENDING,
parent_frame_id=frame.id,
scope_id=frame.scope_id,
lineage_id=child_lineage_id,
parent_lineage_id=frame.lineage_id,
metadata=ForeachIterationMetadata(
foreach_node_id=step.id,
loop_index=loop_index,
@@ -344,3 +352,12 @@ def _finish_concurrent_foreach(
)
advance_frame(run, frame, outcome=outcome, next_node_id=next_node_id)
return run
def _child_lineage_id(frame: ExecutionFrame, step: ForeachNode, loop_index: int) -> str:
"""Return a deterministic opaque lineage id for one foreach child frame.
The readable shape is only for diagnostics. Runtime code should compare the
full id, not parse it; future structured lineage refs can replace this.
"""
return f"{frame.lineage_id}/{step.id}[{loop_index}]"
+3
View File
@@ -7,6 +7,9 @@ def frame_context_values(frame: ExecutionFrame) -> dict[str, object | None]:
context: dict[str, object | None] = {
"prior_outcome": frame.prior_outcome,
"activated_incoming_edge": frame.activated_incoming_edge,
"scope_id": frame.scope_id,
"lineage_id": frame.lineage_id,
"parent_lineage_id": frame.parent_lineage_id,
}
if frame.kind == "foreach_iteration":
loop_item = frame.metadata.get("loop_item")
+4
View File
@@ -79,6 +79,9 @@ def _resolve_node_execution(
context = RuntimeContext(
current_node_id=node.id,
frame_id=frame.id,
scope_id=frame.scope_id,
lineage_id=frame.lineage_id,
parent_lineage_id=frame.parent_lineage_id,
prior_outcome=frame.prior_outcome,
activated_incoming_edge=frame.activated_incoming_edge,
metadata=dict(frame.metadata),
@@ -127,6 +130,7 @@ def _finalize_node_execution(
index=item_index,
frame_id=frame.id,
patch=patch,
lineage_id=frame.lineage_id,
)
barrier.save_to_frame(parent_frame, foreach_node_id)
state_changes = {}