Files
lda-wf/docs/historical/superpowers/plans/2026-05-24-lineage-state-runtime.md
T

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.