plan for durable run state storage idfk

This commit is contained in:
lda
2026-05-26 11:41:27 +07:00 Verified
parent 95ec73d518
commit d37077397a
6 changed files with 1532 additions and 138 deletions
-102
View File
@@ -1,102 +0,0 @@
# lda rambles
that was surprisingly tuff response from our first user. it made me think about how we handle @node atm.
atm only one way is allowed: path. If you want to inbuilt a value; please use the the graph input.
our first user doesnt understand this. AT ALL.
this calls to several problems.
## dict use, instead of list[InputMap]
why dict, when we can:
```python
class InputMap(BaseModel, Generic[TypeT]):
key: SupportToValue[TypeT]
input: Path
```
same thing
```python
class OutputMap(BaseModel, Generic[TypeT]):
output: Pathexpr
key (give me a better key name): SupportToValue[TypeT]
```
look at this support to value, or use another fitting name.
what it does is:
Path("state.foo") -> TypeT
Literal(TypeT) -> TypeT
two modes that we currently need to PATCH IN. we do not PATCH wf_core if there isnt a greater problem.
### default arguments, and literal()
expand on literal, in wf_authoring we can have some Field bs:
```python
@node(
in_map = [
InputMap(dont support this either, use g.use()),
]
)
def baz(spam: InputT = "literal input", egg: Input2T = Path(do not support this.)):
...
```
so that means wf_authoring.node shouldnt do any field augmentation.
```python
g.use(
baz,
in_map = [
now were talking
]
)
```
this SHOULD support both path and literal.
## Context, of some sort
langgraph has 3 ways of storage, if you read tests/rewrite
lets copy that over:
```
According to https://docs.langchain.com/oss/python/concepts/context, there are three types:
| type | mut | lifetime |
| --- | --- | --- |
|static runtime (context) | static | single run |
|dynamic runtime (state) | mut | single run |
|dynamic cross-convo (store) | mut | cross-conversation |
now what the hell is store
## store
store is used in langgraph-demo for debugging. but it can be used for more things.
it saves every turn. every graph nodes. I use InMemoryStore, you can use psql store!
This allows for picking the work up again after a while for example.
a lot more versatility there.
```
now we 100% has state. we kinda have store if you skim it (trace), but trace is only 50% store. and Context is patched in through input/state.
this can be done with support for async, because lowk store is going to give us superpowers.
## how this would aid adding async + foreach parallel
(probably async gather)
currently frame feels like magic + its core i dont understand. + the current serial mode impl is lowk smart.
its reasonable; it spawns how many child frames as how many loop in foreach container item. Serially, like how `for in` in python works.
complaints are only API improvements; not so much aid into adding these
## complaints come when user uses
not a lot from me rn.
+8 -3
View File
@@ -77,9 +77,14 @@ implementation state.
metadata and compatibility patches. Scope-root commits now apply to both the metadata and compatibility patches. Scope-root commits now apply to both the
root workflow and prepared native child scopes through the explicit root workflow and prepared native child scopes through the explicit
scope/lineage commit helper. scope/lineage commit helper.
- **Persistent run history**: add a run store before adding stable `run_id`, - **Durable run history and resume**: design is recorded in
`inspect_run`, or `read_run_trace(run_id, range)` APIs. Current traces are [2026-05-26 durable workflow runs](./superpowers/specs/2026-05-26-durable-workflow-runs-and-resume-design.md).
returned directly from immediate run responses. Add a validated `RunState` storage codec and dedicated run/checkpoint store.
Persist stopped snapshots for interrupted, completed, and failed executions;
replace process-local resume handles with stable `run_id` values; add compact
`inspect_run` and bounded `read_run_trace` APIs. Resuming an interrupted run
must revalidate its pinned dependency environment; ordinary live tool/source
failures remain failed runs, not implicit pauses.
- **Protocol-native long-running runs**: investigate MCP tasks/progress - **Protocol-native long-running runs**: investigate MCP tasks/progress
notifications for long-running workflow execution. Avoid inventing a custom notifications for long-running workflow execution. Avoid inventing a custom
"start" convention unless protocol-native behavior is insufficient. "start" convention unless protocol-native behavior is insufficient.
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
# Native Subgraphs Design # Native Subgraphs Design
Status: prepared-child execution and interrupt resume implemented; saved-artifact Status: prepared-child execution, saved-artifact resolution, and process-local
resolution scoped for platform implementation interrupt resume implemented; durable resume specified separately
Native subgraphs should make a workflow usable as a workflow step without Native subgraphs should make a workflow usable as a workflow step without
collapsing the child run into one opaque Python node call. The current collapsing the child run into one opaque Python node call. The current
@@ -10,10 +10,9 @@ compatibility wrappers, but they hide the child trace, child frames, and child
interrupt lifecycle from `wf_core`. interrupt lifecycle from `wf_core`.
This design defines the core runtime shape. The boundary model, prepared-child This design defines the core runtime shape. The boundary model, prepared-child
execution, and routed child interrupt resume are implemented. The remaining execution, routed child interrupt resume, and platform loading of immutable
saved-workflow work is platform preparation: load immutable child artifact saved-child artifact versions are implemented. Durable persisted resume is the
versions, resolve their runtime dependencies, and supply prepared children to remaining run-lifecycle concern and is specified separately.
the already-implemented core runtime.
## Goals ## Goals
@@ -407,15 +406,14 @@ must be keyed by the parent subgraph dependency/use site, not only by child
artifact id: one parent graph may intentionally invoke the same immutable artifact id: one parent graph may intentionally invoke the same immutable
child artifact twice against different accounts or capability bindings. child artifact twice against different accounts or capability bindings.
### Saved Child Interrupt Limitation ### Saved Child Interrupt Status
Native prepared children can interrupt and resume in core. The current Native prepared children can interrupt and resume in core. The workflow surface
workflow-surface `run_deployment` entrypoint is still a one-shot execution now exposes process-local `run_deployment` / `resume_run` support for saved
call, however, and does not expose persisted platform resume for a saved run. interrupting artifacts, including interrupts raised by saved descendants.
Therefore this slice preserves the existing unrunnable behavior for saved That support does not survive process restart yet. Durable run persistence is
artifacts containing interrupts, including interrupting descendant artifacts. specified separately in
The platform must report that diagnostic before execution rather than starting [`2026-05-26-durable-workflow-runs-and-resume-design.md`](2026-05-26-durable-workflow-runs-and-resume-design.md).
a run it cannot resume through its public surface.
## Implementation Slices ## Implementation Slices
@@ -454,18 +452,18 @@ a run it cannot resume through its public surface.
- Tests: child interrupt pauses parent, resume continues child, parent completes, - Tests: child interrupt pauses parent, resume continues child, parent completes,
wrong resume target fails clearly. wrong resume target fails clearly.
### Slice 3: Saved Workflow References ### Completed Slice 3: Saved Workflow References
- Structural saved-workflow references and conversion helpers already exist; - Structural saved-workflow references and conversion helpers identify exact
this slice is execution resolution, not a new identity shape. saved child versions without runtime string parsing.
- Add platform-level resolution for saved workflow artifacts. - Platform-level resolution prepares saved workflow artifacts before runtime.
- Apply the parent deployment binding environment transitively to each exact - The parent deployment binding environment applies transitively to each exact
saved child artifact version. saved child artifact version.
- Validate dependencies, missing artifacts, saved-child cycles, and existing - Dependencies, missing artifacts, and saved-child cycles validate before
unsupported interrupt diagnostics before execution. execution.
- Tests: saved child workflow runs through a deployment binding, nested saved - Tests cover saved child execution through deployment bindings, nested saved
child dependencies use the same binding environment, and missing, child dependencies, missing/cyclic diagnostics, and process-local
cyclic, or interrupting child artifacts report an unrunnable dependency. interrupt/resume for saved children.
### Slice 4: Optional Policy Expansion ### Slice 4: Optional Policy Expansion
@@ -498,7 +496,7 @@ resume are complete. Implement Slice 3 in the platform layer: prepare saved
non-interrupting child artifacts recursively under one deployment environment non-interrupting child artifacts recursively under one deployment environment
and pass those prepared dependencies into core execution. and pass those prepared dependencies into core execution.
Do not broaden saved interrupt support through `run_deployment` until the The process-local saved-interrupt execution path is complete. Next add durable
platform has a public persisted resume path. Do not delete wrapper-node run/checkpoint persistence so interrupted saved children survive process
helpers yet; they remain compatibility APIs while saved native execution restart. Do not delete wrapper-node helpers yet; they remain compatibility APIs
matures. while saved native execution matures.
@@ -0,0 +1,392 @@
# Durable Workflow Runs and Resume Design
Status: design approved for implementation planning
Durable workflow runs turn the current process-local `run_deployment` /
`resume_run` behavior into platform state. The runtime already exposes the
essential execution snapshot as `RunState`; the missing boundary is a durable
run repository and a strict persisted codec for that state.
This design intentionally implements safe stopped-run persistence first. It
does not promise replay of an in-flight external side effect or arbitrary
automatic retries for failed MCP tool calls.
## Goals
- Persist workflow runs that stop as `interrupted`, `completed`, or `failed`.
- Let an explicitly interrupted deployment resume after MCP server restart or
from another process using a durable `run_id`.
- Pin the artifact, deployment binding environment, and prepared dependency
environment used by a run when it starts.
- Revalidate pinned dependencies before a persisted interrupted run continues.
- Provide compact run inspection and bounded trace retrieval suitable for MCP
clients and a future dashboard.
- Keep run persistence outside `wf_core` and separate from auth/catalog storage.
- Leave room for finer-grained checkpoints without requiring them in v1.
## Non-Goals
- Do not pause automatically when an MCP connection or tool call fails.
- Do not implement automatic retry or timeout behavior in this slice.
- Do not checkpoint after every node or scheduler tick in v1.
- Do not add time travel, replay from arbitrary historical state, or state edits.
- Do not add LangGraph-style cross-run memory available to arbitrary nodes.
- Do not move artifact, deployment, auth, or catalog storage into one store.
## Current State
`wf_core.RunState` already owns the data necessary to continue a stopped run:
- workflow input, committed state, terminal outcome, and public output
- trace entries
- frames and ready-frame queue
- runtime scopes and lineage writes
- current execution cursor
- error information
- interrupt request and nested subgraph interrupt route
`wf_mcp.workflow_surface.handlers.ActiveWorkflowRun` currently retains the
remaining platform context only in memory:
- `WorkflowDeployment`
- `WorkflowArtifact`
- `RawWorkflowPlan`
- `RunState`
That class is the temporary seam to replace. It is not evidence that run state
belongs inside MCP: MCP is merely one front door for a workflow platform
concern.
## Relationship to LangGraph Persistence
LangGraph distinguishes execution checkpoints from a general memory store:
- A **checkpointer** stores execution snapshots so interrupted graphs can be
inspected and resumed.
- A **store** provides arbitrary memory shared across threads/runs.
This design needs the first concept only. A workflow run/checkpoint repository
is appropriate now; cross-run node-accessible memory is separate future work.
## Platform Ownership
The conceptual ownership remains:
```text
wf_core
workflow execution models and runtime
wf_artifacts
immutable workflow definitions and deployment contracts
wf_platform (future extraction target)
runs, checkpoints, schedules, run history, UI/admin policy
wf_mcp
MCP exposure of platform operations
```
The current package graph already has `wf_artifacts` depending on `wf_platform`
for capability/source refs. Placing typed run records in `wf_platform` while
they snapshot `WorkflowArtifact` and `WorkflowDeployment` would introduce a
cycle or weaken those fields into untyped dictionaries. V1 should therefore
place run models/store code under a focused `wf_artifacts.runs` subpackage.
Names and module boundaries must make later extraction straightforward if the
artifact/platform dependency direction is reorganized. Do not expand
`wf_mcp.storage.Store`, which owns MCP auth/catalog snapshots with a different
lifecycle.
## Domain Model
### Workflow Run
A `WorkflowRun` identifies one execution attempt of one pinned deployment
environment.
Required information:
```text
WorkflowRun
id: stable run id
deployment_id
artifact_id
artifact_version
status: interrupted | completed | failed
resume_readiness: ready | blocked | not_applicable
created_at
updated_at
latest_checkpoint_id
pinned_environment
diagnostics
```
`unrunnable` is not a run status. It is a pre-start response when deployment
dependency validation fails before an execution begins.
### Run Checkpoint
A `RunCheckpoint` is a stored stopped-state snapshot of a run.
```text
RunCheckpoint
id
run_id
sequence
reason: interrupted | completed | failed
created_at
persisted_run_state
```
V1 writes a checkpoint only when a public run operation stops:
- `run_deployment` returns an interrupted, completed, or failed execution.
- `resume_run` returns an interrupted, completed, or failed execution.
V1 does not guarantee recovery after a process crash while a node or external
tool call is in progress.
### Pinned Execution Environment
Runs must not reinterpret bindings when resumed. The persisted run pins:
- root artifact snapshot, in addition to its exact id/version
- deployment snapshot, including the binding selection used at start
- resolved saved child artifact snapshots, in addition to exact child versions
- dependency contract information needed to revalidate those bindings
A configuration edit after the run starts does not silently redirect a paused
run from one MCP account/source to another. If a future repair/migration flow
allows rebinding a stopped run, that must be explicit and auditable.
Snapshotting artifact contents is required by the current implementation:
`FileWorkflowArtifactStore` stores versioned paths but does not yet reject an
overwrite of an existing version. A durable run must not depend on a later
filesystem overwrite preserving the graph it originally executed. A future
store that enforces immutable artifact writes may still retain snapshots as an
audit record.
## Persisted Runtime Codec
`RunState.to_dict()` is useful for inspection but is not by itself a durable
storage contract. Nested dataclasses, enums, path types, reducer refs, workflow
refs, scopes, lineages, and interrupt routes must round-trip through a
validated persisted model or codec.
The storage codec must:
- serialize the full stopped `RunState` needed for resume
- restore equivalent enum/path/ref/dataclass values rather than loose dicts
- reject corrupted or unsupported stored state with a clear diagnostic
- be versioned or structured so later checkpoint migrations are possible
- contain JSON-compatible values only at the persistence boundary
Core should own the execution-state codec shape because it understands
`RunState`. The platform store should persist the codec output, not reconstruct
runtime internals itself.
## Lifecycle Semantics
### Start
```text
validate deployment and resolved dependencies
if blocking diagnostics:
return unrunnable response; do not create a run
pin resolved execution environment
execute workflow
persist WorkflowRun and stopped-state checkpoint
return run summary
```
Completed and failed runs are persisted as well as interrupted runs. This makes
inspection and debugging useful immediately without materially increasing
storage implementation complexity.
### Intentional Pause
Only a declared `InterruptNode` creates a resumable pause.
```text
InterruptNode reached
-> RunStatus.INTERRUPTED
-> persist interrupted checkpoint
-> resume_readiness = ready when pinned dependencies validate
```
Interrupts raised within a native saved child subgraph retain the typed
interrupt route already carried in `RunState`, so durable resume re-enters the
original child scope.
### Resume
```text
load run and latest interrupted checkpoint
verify run is interrupted and resumable
revalidate exact pinned dependency environment
if dependency validation fails:
keep run status interrupted
set resume_readiness = blocked
return dependency diagnostics without mutating execution state
else:
resume from restored RunState
persist next stopped-state checkpoint
```
When a missing or disabled pinned dependency returns, a later explicit
`validate_run`, `inspect_run`, or `resume_run` may report it ready again.
There is no background auto-resume.
### Failure
An ordinary runtime exception, MCP transport failure, or tool connection loss
while execution is running fails the run.
This is deliberately distinct from interruption. An external tool may have
completed a side effect before losing its response; automatically presenting
that condition as safely resumable could duplicate effects.
```text
tool/source failure during execution
-> status = failed
-> persist failed checkpoint and error details
-> no automatic resume
```
Recovery from such failures must later be declared in workflow semantics, such
as explicit outcome mapping or a carefully specified retry policy.
## Dependency Validation Semantics
Dependency status has three positions:
| Point | Result |
| --- | --- |
| Before start | `unrunnable`; no execution run is created |
| At a declared interrupt | `interrupted`; persisted and potentially resumable |
| Before resume when pinned dependency is broken | run remains `interrupted`, `resume_readiness = blocked`, diagnostics returned |
| During live execution | `failed`, unless future workflow logic explicitly models recovery |
Dependency diagnostics are platform/control-plane information, not graph
outcomes. They must not be forced through a user's declared `ok` / `error`
workflow outcome routes.
## Public Surface
The stable MCP workflow surface should eventually expose:
```text
wf.workflow.run_deployment
starts a run; always returns a stable run_id once execution starts
wf.workflow.resume_run
resumes one durable interrupted run after pinned dependency validation
wf.workflow.inspect_run
returns compact run status, readiness, terminal output/outcome,
interrupt summary, diagnostics, and trace_count
wf.workflow.read_run_trace
returns a bounded trace slice selected by range
```
The response style stays progressive:
- start/resume returns compact stopped-state status
- inspect returns detail needed for decision making
- trace is fetched only in bounded slices
## Store Interface Direction
Use a dedicated protocol rather than expanding MCP auth/catalog persistence:
```text
RunStore
save_run(run)
get_run(run_id)
save_checkpoint(checkpoint)
get_latest_checkpoint(run_id)
list_runs(query, cursor, limit)
read_checkpoints(run_id, cursor, limit)
```
The initial backend may be file-based under the configured application store
root:
```text
.wf_mcp_store/
auth/
catalog/
workflows/
deployments/
runs/
<run_id>/
run.json
checkpoints/
000001.json
000002.json
```
The protocol must allow later SQLite/Postgres storage without changing the MCP
surface or core runtime model.
## Retry and Timeout Fields
The codebase already carries `retry` and `timeout_seconds` fields in node/draft
models, plus a runtime `retry_count` context field. They are not currently an
implemented runtime policy.
For durable-run v1:
- treat these fields as declared-but-unsupported behavior
- do not interpret failed MCP calls as retryable
- do not add recovery checkpoints around external calls
- document clearly when a future slice implements retry/timeout semantics
A future retry design must distinguish known-not-executed failures from
unknown-side-effect failures, or require idempotency/author-provided policy.
## Testing Strategy
Implementation tests must cover:
- persisted `RunState` round-trip for root interrupts
- persisted `RunState` round-trip for native child-subgraph interrupts
- a completed run saved with output and terminal outcome
- a failed run saved with error details
- server/service recreation followed by successful `resume_run`
- pinned dependency disabled after pause: resume is blocked and state remains
interrupted and unmodified
- pinned dependency restored: later resume succeeds
- changed live configuration cannot silently redirect pinned source bindings
- transport failure during execution produces failed, not interrupted
- trace retrieval is ranged and reports total trace count
## Future Expansion
Once stopped-run persistence is stable:
1. Add checkpoint writes at explicit scheduler-safe boundaries if crash
recovery during long execution is needed.
2. Investigate MCP tasks/progress reporting for live long-running operations.
3. Design retry/timeout semantics explicitly rather than activating dormant
fields casually.
4. Consider replay/time-travel only after external side-effect semantics are
documented.
5. Add general cross-run memory separately if nodes need it; it is not a
replacement for checkpoints.
## Recommendation
Implement durable stopped-run snapshots first:
1. Add a validated persisted `RunState` codec.
2. Add `WorkflowRun`, `RunCheckpoint`, and `RunStore`.
3. Save interrupted, completed, and failed runs after start/resume returns.
4. Replace process-local `_active_runs` lookup with durable run retrieval.
5. Add compact `inspect_run` and bounded `read_run_trace`.
6. Revalidate pinned dependencies before resuming interrupted runs.
This delivers durable human-in-the-loop execution and run inspection while
preserving the correctness boundary that live external-call failures are not
safe implicit pauses.
+9 -5
View File
@@ -89,8 +89,10 @@ deployment surface, including interrupts raised inside saved child workflows.
This support is in-memory only: server restart, reload that replaces process This support is in-memory only: server restart, reload that replaces process
state, or another frontend process invalidates the `run_id`. state, or another frontend process invalidates the `run_id`.
Future run history should replace process-local `run_id` values with durable Durable run history is specified in
run records. The likely shape is: [`2026-05-26-durable-workflow-runs-and-resume-design.md`](superpowers/specs/2026-05-26-durable-workflow-runs-and-resume-design.md).
It should replace process-local `run_id` values with durable run records. The
planned surface is:
- `run_deployment` starts or completes a run and returns `run_id` - `run_deployment` starts or completes a run and returns `run_id`
- `inspect_run(run_id)` returns status, output, diagnostics, and trace metadata - `inspect_run(run_id)` returns status, output, diagnostics, and trace metadata
@@ -685,9 +687,11 @@ parent trace sees one node call; in the native examples child trace entries
remain in the parent run state. remain in the parent run state.
Persisted resume is still not implemented. In-memory resume works because the Persisted resume is still not implemented. In-memory resume works because the
server keeps the paused `RunState`; a durable run store will need to snapshot server keeps the paused `RunState`; the durable run design persists stopped
the root workflow, prepared child dependencies, deployment bindings, and trace snapshots for interrupted, completed, and failed runs and pins root workflow,
metadata before this can survive restart or move across processes. prepared child dependencies, deployment bindings, and trace metadata before a
paused run can survive restart or move across processes. Only declared
interrupts become resumable pauses; live tool/source failures remain failures.
Blocking dependency failures happen before workflow execution and are not normal Blocking dependency failures happen before workflow execution and are not normal
workflow outcomes. A missing source, disabled source, unresolved binding, or workflow outcomes. A missing source, disabled source, unresolved binding, or