749 lines
22 KiB
Markdown
749 lines
22 KiB
Markdown
# Lineage State Runtime Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Add runtime scopes and lineages before native subgraphs so sibling branches/items can own isolated replayable state writes and later merge through barriers.
|
|
|
|
**Architecture:** Implements [`../specs/2026-05-24-lineage-state-runtime-design.md`](../specs/2026-05-24-lineage-state-runtime-design.md). Keep `RunState.state` as the committed root-scope compatibility state, add `RuntimeScope`, `LineageState`, and `StateWrite`, and migrate concurrent foreach from foreach-specific overlays to lineage-backed state views. A frame says where execution is; a scope says which workflow state root execution belongs to; a lineage says which pending writes that execution can see.
|
|
|
|
**Tech Stack:** Python 3.14, dataclasses, existing `StatePatch`, `StatePath`, `ReducerRef`, `ForeachBarrierState`, pytest, basedpyright, ruff.
|
|
|
|
---
|
|
|
|
## Current Implementation Status
|
|
|
|
This plan is being implemented incrementally. The full `RuntimeScope` /
|
|
`LineageState` storage model below is still future work, but the runtime now has
|
|
the compatibility subset needed before native subgraphs:
|
|
|
|
- `StateWrite` exists and records `incoming_value` for replay plus
|
|
`visible_value` for same-lineage reads.
|
|
- `StatePatch` stores ordered `writes` while preserving `changes` as the
|
|
trace/compatibility view.
|
|
- `LineageStateView` materializes committed state plus visible lineage writes.
|
|
- Concurrent foreach item overlays read `StateWrite.visible_value`.
|
|
- Foreach pending result metadata persists write records and `lineage_id`.
|
|
- `ExecutionFrame` and `RuntimeContext` carry `scope_id`, `lineage_id`, and
|
|
`parent_lineage_id`.
|
|
- Concurrent foreach child frames receive deterministic, opaque lineage ids,
|
|
including nested foreach frames.
|
|
- `RunState` has root scope/lineage storage, scope-aware state views, and
|
|
generic non-root node writes buffer into `RunState.lineages`.
|
|
- New concurrent foreach item writes are stored in `RunState.lineages`.
|
|
`ForeachBarrierState` now keeps scheduling/result metadata plus compatibility
|
|
patches for old serialized barrier data.
|
|
|
|
Direct commits now go through a scope-root commit decision: top-level frames
|
|
commit to root state, and prepared native-child root frames commit to their
|
|
child scope state. Descendant item/branch lineages still buffer writes until a
|
|
barrier or future gather commits them.
|
|
|
|
Remaining work should avoid jumping straight into a broad rewrite. Native
|
|
subgraph scaffolding and non-interrupting prepared-child execution are now
|
|
present (`SubgraphNode`, structural `WorkflowRef`, terminal workflow outcomes,
|
|
authoring helpers, and `PreparedSubgraph`). Child graphs execute through their
|
|
own scope/lineage and map output back at completion. Interrupt bubbling and
|
|
saved/deployed workflow resolution remain later work.
|
|
|
|
---
|
|
|
|
## File Structure
|
|
|
|
- Modify: `src/wf_core/run_state.py`
|
|
- Add `RuntimeScope`, `StateWrite`, `LineageState`.
|
|
- Add `ExecutionFrame.scope_id` and `ExecutionFrame.lineage_id`.
|
|
- Add `RunState.scopes` and `RunState.lineages`.
|
|
- Modify: `src/wf_core/runtime/ops/state.py`
|
|
- Change `StatePatch` from only path-value maps to ordered `StateWrite` records while preserving `changes` as a compatibility/trace view.
|
|
- Create: `src/wf_core/runtime/lineage.py`
|
|
- Own scope/lineage lookup, state view materialization, append writes, and conversion of completed lineage writes into barrier patches.
|
|
- Modify: `src/wf_core/runtime/ops/runs.py`
|
|
- Initialize root scope and root lineage.
|
|
- Modify: `src/wf_core/runtime/ops/nodes.py`
|
|
- Resolve node input from frame scope/lineage view and buffer non-root writes into lineage records.
|
|
- Modify: `src/wf_core/runtime/ops/foreach.py`
|
|
- Create concurrent item lineages and commit completed lineage writes through the barrier.
|
|
- Modify: `src/wf_core/runtime/foreach_state.py`
|
|
- Store completed lineage ids in pending item results; keep old patch metadata parse-compatible.
|
|
- Modify: `src/wf_core/runtime/ops/overlays.py`
|
|
- Reduce to a compatibility facade over lineage state views.
|
|
- Test: `tests/core/test_lineage_state.py`
|
|
- Unit tests for root scope/lineage, state views, write records, and non-root write buffering.
|
|
- Test: `tests/core/test_atomic_state_patches.py`
|
|
- Tests for ordered `StateWrite` records and compatibility `changes`.
|
|
- Test: `tests/core/test_concurrent_foreach.py`
|
|
- Regression tests for sibling isolation, same-item visibility, and deterministic barrier commits.
|
|
- Docs: `docs/wf_core_architecture.md`, `docs/current_roadmap.md`, `docs/superpowers/specs/2026-05-24-native-subgraphs-design.md`
|
|
- Document scope/lineage as the prerequisite for native subgraphs.
|
|
|
|
---
|
|
|
|
## Core Shape
|
|
|
|
Target runtime state types:
|
|
|
|
```python
|
|
@dataclass(slots=True)
|
|
class StateWrite:
|
|
"""One reducer-aware write record owned by a lineage or patch."""
|
|
|
|
path: StatePath
|
|
incoming_value: Any
|
|
visible_value: Any
|
|
reducer: ReducerRef
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class RuntimeScope:
|
|
"""Committed state root for one workflow activation."""
|
|
|
|
id: str
|
|
workflow_name: str
|
|
committed_state: dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class LineageState:
|
|
"""Pending ordered writes visible to frames in one lineage."""
|
|
|
|
id: str
|
|
scope_id: str
|
|
parent_id: str | None = None
|
|
writes: list[StateWrite] = field(default_factory=list)
|
|
```
|
|
|
|
Target patch shape:
|
|
|
|
```python
|
|
@dataclass(slots=True)
|
|
class StatePatch:
|
|
"""Validated state writes produced by one step before commit."""
|
|
|
|
writes: list[StateWrite] = dataclass_field(default_factory=list)
|
|
_staged_state: dict[str, Any] = dataclass_field(default_factory=dict, repr=False)
|
|
|
|
@property
|
|
def changes(self) -> dict[str, Any]:
|
|
return {str(write.path): write.incoming_value for write in self.writes}
|
|
|
|
@property
|
|
def visible_values(self) -> dict[str, Any]:
|
|
return {str(write.path): write.visible_value for write in self.writes}
|
|
```
|
|
|
|
Compatibility requirement: existing tests and callers that read `patch.changes`
|
|
should continue to work. New lineage code must use ordered `writes`, not
|
|
flattened final values.
|
|
|
|
---
|
|
|
|
## Task 1: Add Ordered StateWrite Records to StatePatch
|
|
|
|
Status: implemented as the compatibility shape. `StatePatch.changes` remains a
|
|
stored compatibility dict rather than a derived-only property for now.
|
|
|
|
**Files:**
|
|
|
|
- Modify: `src/wf_core/runtime/ops/state.py`
|
|
- Test: `tests/core/test_atomic_state_patches.py`
|
|
|
|
- [ ] **Step 1: Add failing tests**
|
|
|
|
Append:
|
|
|
|
```python
|
|
def test_output_patch_records_incoming_and_visible_values() -> None:
|
|
workflow = _workflow_with_state_field(
|
|
path="state.count",
|
|
schema={"type": "integer"},
|
|
reducer="wf.std.add",
|
|
)
|
|
state = {"count": 2}
|
|
|
|
patch = build_output_patch(
|
|
workflow,
|
|
[OutputBinding.model_validate({"source": "delta", "target": "state.count"})],
|
|
{"delta": 3},
|
|
state,
|
|
)
|
|
|
|
assert patch.changes["state.count"] == 3
|
|
assert patch.visible_values["state.count"] == 5
|
|
assert patch.writes[0].incoming_value == 3
|
|
assert patch.writes[0].visible_value == 5
|
|
|
|
|
|
def test_barrier_replays_incoming_values_not_lineage_visible_values() -> None:
|
|
workflow = _workflow_with_state_field(
|
|
path="state.number",
|
|
schema={"type": "integer"},
|
|
reducer="wf.std.add",
|
|
)
|
|
patch = build_barrier_patch(
|
|
workflow,
|
|
[
|
|
StatePatch(
|
|
writes=[
|
|
StateWrite(
|
|
path=StatePath(("number",)),
|
|
incoming_value=3,
|
|
visible_value=5,
|
|
reducer=ReducerRef(name="wf.std.add"),
|
|
)
|
|
]
|
|
),
|
|
StatePatch(
|
|
writes=[
|
|
StateWrite(
|
|
path=StatePath(("number",)),
|
|
incoming_value=1,
|
|
visible_value=3,
|
|
reducer=ReducerRef(name="wf.std.add"),
|
|
)
|
|
]
|
|
),
|
|
],
|
|
{"number": 2},
|
|
)
|
|
|
|
assert patch.changes["state.number"] == 6
|
|
assert patch.visible_values["state.number"] == 6
|
|
```
|
|
|
|
Add imports:
|
|
|
|
```python
|
|
from wf_core.models.reducers import ReducerRef
|
|
from wf_core.paths import StatePath
|
|
from wf_core.run_state import StateWrite
|
|
```
|
|
|
|
- [ ] **Step 2: Run expected failing tests**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run pytest tests/core/test_atomic_state_patches.py::test_output_patch_records_incoming_and_visible_values tests/core/test_atomic_state_patches.py::test_barrier_replays_incoming_values_not_lineage_visible_values -q
|
|
```
|
|
|
|
Expected: failure because `StateWrite`, `StatePatch.writes`, and `visible_values`
|
|
do not exist.
|
|
|
|
- [ ] **Step 3: Implement `StateWrite` and patch views**
|
|
|
|
In `src/wf_core/run_state.py`, add `StateWrite` near runtime dataclasses:
|
|
|
|
```python
|
|
@dataclass(slots=True)
|
|
class StateWrite:
|
|
path: StatePath
|
|
incoming_value: Any
|
|
visible_value: Any
|
|
reducer: ReducerRef
|
|
```
|
|
|
|
Import `ReducerRef` and `StatePath`.
|
|
|
|
In `src/wf_core/runtime/ops/state.py`, change `StatePatch` to store ordered
|
|
writes and expose `changes` / `visible_values` properties. Keep `_staged_state`.
|
|
|
|
- [ ] **Step 4: Build write records in `build_output_patch`**
|
|
|
|
When `prepare_state_value(...)` returns a merged value, also capture the reducer
|
|
used for the destination. If needed, extract reducer lookup from
|
|
`prepare_state_value(...)` into a helper:
|
|
|
|
```python
|
|
def reducer_for_state_path(
|
|
path: StatePath,
|
|
state_fields: Mapping[StatePath, StateFieldDecl],
|
|
) -> ReducerRef:
|
|
field = state_fields.get(path)
|
|
return field.reducer if field and field.reducer else ReducerRef(name="wf.std.replace")
|
|
```
|
|
|
|
Create:
|
|
|
|
```python
|
|
StateWrite(
|
|
path=destination_path,
|
|
incoming_value=value,
|
|
visible_value=merged_value,
|
|
reducer=reducer,
|
|
)
|
|
```
|
|
|
|
- [ ] **Step 5: Replay incoming values in `build_barrier_patch`**
|
|
|
|
Update `build_barrier_patch(...)` to iterate over `item_patch.writes`, not over
|
|
`item_patch.changes.items()`. Replay `write.incoming_value` against the staged
|
|
state. The resulting barrier patch should contain one `StateWrite` per final
|
|
destination with both incoming and visible values set to the final committed
|
|
aggregate value, because the barrier is the public commit point.
|
|
|
|
- [ ] **Step 6: Run atomic patch tests**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run pytest tests/core/test_atomic_state_patches.py -q
|
|
```
|
|
|
|
Expected: pass.
|
|
|
|
---
|
|
|
|
## Task 2: Add Runtime Scopes and Root Lineage
|
|
|
|
Status: partially implemented. Frames and runtime context carry `scope_id`,
|
|
`lineage_id`, and `parent_lineage_id`, but `RunState.scopes`,
|
|
`RunState.lineages`, `RuntimeScope`, and `LineageState` are not implemented yet.
|
|
This is deliberate; foreach still stores pending writes in barrier metadata.
|
|
|
|
**Files:**
|
|
|
|
- Modify: `src/wf_core/run_state.py`
|
|
- Modify: `src/wf_core/runtime/ops/runs.py`
|
|
- Test: `tests/core/test_lineage_state.py`
|
|
|
|
- [ ] **Step 1: Add root initialization test**
|
|
|
|
Create `tests/core/test_lineage_state.py` with a local minimal workflow helper
|
|
that builds a tiny core `Workflow`. Then add:
|
|
|
|
```python
|
|
def test_create_run_state_initializes_root_scope_and_lineage() -> None:
|
|
workflow = minimal_workflow()
|
|
|
|
run = create_run_state(workflow, {"value": "seed"})
|
|
|
|
assert run.scopes["root"].id == "root"
|
|
assert run.scopes["root"].workflow_name == workflow.name
|
|
assert run.scopes["root"].committed_state["value"] == "seed"
|
|
assert run.lineages["root"].id == "root"
|
|
assert run.lineages["root"].scope_id == "root"
|
|
assert run.lineages["root"].parent_id is None
|
|
assert run.lineages["root"].writes == []
|
|
assert run.frames["root"].scope_id == "root"
|
|
assert run.frames["root"].lineage_id == "root"
|
|
```
|
|
|
|
- [ ] **Step 2: Run expected failing test**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run pytest tests/core/test_lineage_state.py::test_create_run_state_initializes_root_scope_and_lineage -q
|
|
```
|
|
|
|
Expected: failure because `scopes`, `lineages`, `scope_id`, and `lineage_id`
|
|
do not exist.
|
|
|
|
- [ ] **Step 3: Add dataclasses and fields**
|
|
|
|
In `src/wf_core/run_state.py`, add `RuntimeScope` and `LineageState`. Add
|
|
`scope_id: str = "root"` and `lineage_id: str = "root"` to `ExecutionFrame`.
|
|
Add `scopes` and `lineages` to `RunState`.
|
|
|
|
- [ ] **Step 4: Initialize root scope and lineage**
|
|
|
|
In `src/wf_core/runtime/ops/runs.py`, initialize:
|
|
|
|
```python
|
|
run = RunState(
|
|
workflow_name=workflow.name,
|
|
status=RunStatus.PENDING,
|
|
workflow_input=dict(workflow_input),
|
|
state=state,
|
|
scopes={
|
|
"root": RuntimeScope(
|
|
id="root",
|
|
workflow_name=workflow.name,
|
|
committed_state=state,
|
|
)
|
|
},
|
|
lineages={"root": LineageState(id="root", scope_id="root")},
|
|
current_frame_id="root",
|
|
current_node_id=workflow.start,
|
|
)
|
|
```
|
|
|
|
The root scope may share the same dict object as `RunState.state` during this
|
|
migration.
|
|
|
|
- [ ] **Step 5: Run focused test**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run pytest tests/core/test_lineage_state.py -q
|
|
```
|
|
|
|
Expected: pass.
|
|
|
|
---
|
|
|
|
## Task 3: Add Lineage Runtime Helpers
|
|
|
|
Status: partially implemented. `LineageStateView` and
|
|
`lineage_writes_for_frame(run, frame)` exist in `src/wf_core/runtime/lineage.py`,
|
|
backed by current foreach metadata.
|
|
|
|
**Files:**
|
|
|
|
- Create: `src/wf_core/runtime/lineage.py`
|
|
- Test: `tests/core/test_lineage_state.py`
|
|
|
|
- [ ] **Step 1: Add helper tests**
|
|
|
|
Append:
|
|
|
|
```python
|
|
def test_lineage_state_view_applies_visible_values_only_for_reads() -> None:
|
|
workflow = minimal_workflow()
|
|
run = create_run_state(workflow, {"number": 2})
|
|
add_lineage(run, scope_id="root", lineage_id="branch", parent_id="root")
|
|
append_lineage_writes(
|
|
run,
|
|
scope_id="root",
|
|
lineage_id="branch",
|
|
writes=[
|
|
StateWrite(
|
|
path=StatePath(("number",)),
|
|
incoming_value=3,
|
|
visible_value=5,
|
|
reducer=ReducerRef(name="wf.std.add"),
|
|
)
|
|
],
|
|
)
|
|
|
|
view = lineage_state_view(run, scope_id="root", lineage_id="branch")
|
|
|
|
assert view["number"] == 5
|
|
assert run.state["number"] == 2
|
|
|
|
|
|
def test_lineage_write_patch_preserves_incoming_values_for_barrier_replay() -> None:
|
|
workflow = minimal_workflow()
|
|
run = create_run_state(workflow, {"number": 2})
|
|
add_lineage(run, scope_id="root", lineage_id="branch", parent_id="root")
|
|
append_lineage_writes(
|
|
run,
|
|
scope_id="root",
|
|
lineage_id="branch",
|
|
writes=[
|
|
StateWrite(
|
|
path=StatePath(("number",)),
|
|
incoming_value=3,
|
|
visible_value=5,
|
|
reducer=ReducerRef(name="wf.std.add"),
|
|
)
|
|
],
|
|
)
|
|
|
|
patch = lineage_patch(run, scope_id="root", lineage_id="branch")
|
|
|
|
assert patch.writes[0].incoming_value == 3
|
|
assert patch.writes[0].visible_value == 5
|
|
```
|
|
|
|
- [ ] **Step 2: Run expected failing tests**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run pytest tests/core/test_lineage_state.py -q
|
|
```
|
|
|
|
Expected: import failure for `wf_core.runtime.lineage`.
|
|
|
|
- [ ] **Step 3: Implement `runtime.lineage`**
|
|
|
|
Create helpers:
|
|
|
|
```python
|
|
def add_lineage(
|
|
run: RunState, *, scope_id: str, lineage_id: str, parent_id: str
|
|
) -> None: ...
|
|
|
|
def append_lineage_writes(
|
|
run: RunState,
|
|
*,
|
|
scope_id: str,
|
|
lineage_id: str,
|
|
writes: Sequence[StateWrite],
|
|
) -> None: ...
|
|
|
|
def lineage_patch(run: RunState, *, scope_id: str, lineage_id: str) -> StatePatch: ...
|
|
|
|
def lineage_state_view(
|
|
run: RunState, *, scope_id: str, lineage_id: str
|
|
) -> dict[str, Any]: ...
|
|
```
|
|
|
|
`lineage_state_view(...)` should deep-copy `run.scopes[scope_id].committed_state`
|
|
and apply `write.visible_value` from ancestor/current lineage writes in order.
|
|
`lineage_patch(...)` should return ordered writes with incoming values intact.
|
|
|
|
- [ ] **Step 4: Run focused tests**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run pytest tests/core/test_lineage_state.py -q
|
|
```
|
|
|
|
Expected: pass.
|
|
|
|
---
|
|
|
|
## Task 4: Route Node Reads and Non-Root Writes Through Lineage
|
|
|
|
**Files:**
|
|
|
|
- Modify: `src/wf_core/runtime/ops/overlays.py`
|
|
- Modify: `src/wf_core/runtime/ops/nodes.py`
|
|
- Test: `tests/core/test_lineage_state.py`
|
|
|
|
- [ ] **Step 1: Add non-root write buffering test**
|
|
|
|
Add a test with a one-node workflow that reads `state.value`, writes
|
|
`state.value`, and runs the frame with `lineage_id="child"`. Assert:
|
|
|
|
```python
|
|
assert result.state_changes == {}
|
|
assert run.state["value"] == "root"
|
|
assert run.lineages["child"].writes[0].incoming_value == "root-child"
|
|
assert lineage_state_view(run, scope_id="root", lineage_id="child")["value"] == "root-child"
|
|
```
|
|
|
|
- [ ] **Step 2: Run expected failing test**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run pytest tests/core/test_lineage_state.py::test_non_root_lineage_node_writes_are_buffered_not_committed -q
|
|
```
|
|
|
|
Expected: failure because node execution still commits or cannot read through
|
|
lineage.
|
|
|
|
- [ ] **Step 3: Update overlay facade**
|
|
|
|
`state_view_for_frame(run, frame)` should call:
|
|
|
|
```python
|
|
lineage_state_view(run, scope_id=frame.scope_id, lineage_id=frame.lineage_id)
|
|
```
|
|
|
|
For root scope/root lineage it may return `run.state` directly as an optimization.
|
|
|
|
- [ ] **Step 4: Update node finalization**
|
|
|
|
In `_finalize_node_execution(...)`:
|
|
|
|
- if `is_root_lineage_frame(frame)`, commit patch to `run.state`
|
|
- otherwise append `patch.writes` to the frame lineage and return empty committed
|
|
`state_changes`
|
|
|
|
- [ ] **Step 5: Run focused tests**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run pytest tests/core/test_lineage_state.py tests/core/test_atomic_state_patches.py -q
|
|
```
|
|
|
|
Expected: pass.
|
|
|
|
---
|
|
|
|
## Task 5: Migrate Concurrent Foreach to Lineages
|
|
|
|
Status: implemented for new concurrent foreach results. Concurrent foreach
|
|
child frames have lineage ids, nested item lineages are tested, item writes are
|
|
stored in `RunState.lineages`, and pending item results persist `lineage_id`.
|
|
`ForeachBarrierState.patch` remains as a compatibility fallback.
|
|
|
|
**Files:**
|
|
|
|
- Modify: `src/wf_core/runtime/ops/foreach.py`
|
|
- Modify: `src/wf_core/runtime/foreach_state.py`
|
|
- Test: `tests/core/test_concurrent_foreach.py`
|
|
- Test: `tests/core/test_foreach_barrier_state.py`
|
|
|
|
- [ ] **Step 1: Add item lineage regression**
|
|
|
|
Add:
|
|
|
|
```python
|
|
def test_concurrent_foreach_item_frames_use_distinct_lineages() -> None:
|
|
workflow = _workflow(mode="concurrent", concurrent={"max_active": 2})
|
|
run = execute_workflow(workflow, {"items": ["a", "b"]}, {"record": _record_handler})
|
|
|
|
item_frames = [frame for frame in run.frames.values() if frame.kind == "foreach_iteration"]
|
|
|
|
assert len(item_frames) == 2
|
|
assert item_frames[0].lineage_id != "root"
|
|
assert item_frames[1].lineage_id != "root"
|
|
assert item_frames[0].lineage_id != item_frames[1].lineage_id
|
|
```
|
|
|
|
- [ ] **Step 2: Add same-item read regression**
|
|
|
|
Add or keep a multi-step concurrent foreach test where item node 1 writes
|
|
`state.scratch`, item node 2 reads `state.scratch`, and siblings do not see each
|
|
other's scratch.
|
|
|
|
- [ ] **Step 3: Store lineage id on pending item result**
|
|
|
|
Add `lineage_id: str | None = None` to `PendingItemResult`, parse it from
|
|
metadata, and serialize it back. Keep old `patch` parse compatibility.
|
|
|
|
- [ ] **Step 4: Create item lineages on admission**
|
|
|
|
In `_admit_concurrent_children(...)`, before adding the child frame:
|
|
|
|
```python
|
|
child_lineage_id = child_id
|
|
add_lineage(
|
|
run,
|
|
scope_id=frame.scope_id,
|
|
lineage_id=child_lineage_id,
|
|
parent_id=frame.lineage_id,
|
|
)
|
|
```
|
|
|
|
Pass `scope_id=frame.scope_id` and `lineage_id=child_lineage_id` to the child
|
|
`ExecutionFrame`.
|
|
|
|
- [ ] **Step 5: Record completed lineage ids**
|
|
|
|
When a child completes, record `child.lineage_id` on the barrier pending result.
|
|
Do not copy flattened visible values into the barrier.
|
|
|
|
- [ ] **Step 6: Build barrier from lineage patches**
|
|
|
|
In `_finish_concurrent_foreach(...)`, construct `success_patches` from
|
|
`lineage_patch(run, scope_id=frame.scope_id, lineage_id=result.lineage_id)` for
|
|
new results. Keep existing `result.patch` fallback for old metadata.
|
|
|
|
- [ ] **Step 7: Run foreach tests**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run pytest tests/core/test_concurrent_foreach.py tests/core/test_concurrent_foreach_async.py tests/core/test_foreach_barrier_state.py -q
|
|
```
|
|
|
|
Expected: pass.
|
|
|
|
---
|
|
|
|
## Task 6: Remove Foreach-Specific Overlay Coupling
|
|
|
|
**Files:**
|
|
|
|
- Modify: `src/wf_core/runtime/ops/overlays.py`
|
|
- Modify: `src/wf_core/runtime/ops/nodes.py`
|
|
- Test: `tests/core`
|
|
|
|
- [ ] **Step 1: Remove foreach imports from node/overlay state path**
|
|
|
|
Ensure `ops/nodes.py` and `ops/overlays.py` do not import
|
|
`ForeachBarrierState` or `item_frame_owner`.
|
|
|
|
- [ ] **Step 2: Run core tests**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run pytest tests/core -q
|
|
```
|
|
|
|
Expected: pass.
|
|
|
|
---
|
|
|
|
## Task 7: Update Docs
|
|
|
|
**Files:**
|
|
|
|
- Modify: `docs/wf_core_architecture.md`
|
|
- Modify: `docs/current_roadmap.md`
|
|
- Modify: `docs/superpowers/specs/2026-05-24-native-subgraphs-design.md`
|
|
|
|
- [ ] **Step 1: Document scope/lineage**
|
|
|
|
Add a section explaining:
|
|
|
|
- scope is workflow state root
|
|
- frame is scheduler position
|
|
- lineage is pending write ownership
|
|
- concurrent foreach uses child lineages
|
|
- native subgraphs require child scopes
|
|
|
|
- [ ] **Step 2: Update native subgraph spec**
|
|
|
|
Ensure it says native subgraphs depend on child runtime scopes plus lineages,
|
|
not lineage alone.
|
|
|
|
- [ ] **Step 3: Run doc red-flag scan**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
rg -n "U[N]RESOLVED|I[N]COMPLETE|F[I]LL_ME|D[E]CIDE_ME" docs/wf_core_architecture.md docs/current_roadmap.md docs/superpowers/specs/2026-05-24-native-subgraphs-design.md
|
|
```
|
|
|
|
Expected: no output.
|
|
|
|
---
|
|
|
|
## Task 8: Full Verification
|
|
|
|
- [ ] **Step 1: Run tests**
|
|
|
|
```bash
|
|
uv run pytest -q
|
|
```
|
|
|
|
- [ ] **Step 2: Run type check**
|
|
|
|
```bash
|
|
uv run basedpyright --level error src tests examples
|
|
```
|
|
|
|
- [ ] **Step 3: Run lint**
|
|
|
|
```bash
|
|
uvx ruff check src tests examples
|
|
```
|
|
|
|
- [ ] **Step 4: Run format check**
|
|
|
|
```bash
|
|
uvx ruff format --check src tests examples
|
|
```
|
|
|
|
---
|
|
|
|
## Self-Review
|
|
|
|
Spec coverage:
|
|
|
|
- Scope is represented explicitly and is available for native subgraph state
|
|
roots.
|
|
- Lineage stores ordered replayable writes, not full state and not only visible
|
|
values.
|
|
- `StatePatch` preserves incoming values for gather/barrier replay.
|
|
- Same-lineage reads use visible values.
|
|
- Concurrent foreach is the first migration target.
|
|
|
|
Type consistency:
|
|
|
|
- `StateWrite.incoming_value` is replay/trace input.
|
|
- `StateWrite.visible_value` is same-lineage read value.
|
|
- `RuntimeScope.committed_state` is scope-local committed state.
|
|
- `ExecutionFrame.scope_id` and `lineage_id` select visibility.
|