Pre-alpha documentation
Compose durable subgraphs and bounded loops.
Reuse shared-state graph templates with explicit return ports, scoped identities, crash-safe iterations, and pre-dispatch limits.
Start with the executable registry example
The example builds a review body, a three-iteration ceiling, explicit success and exhaustion paths, schemas, a reducer, and eight executable node bindings. It performs no provider or database I/O. Actual execution uses DurableAgentLoop and PostgreSQL; the database suite verifies recovery separately.
cargo run -p stateknot-runtime --example durable_graph_composition --locked
Use initial state {"count":0}. Read the
complete example
before adapting the abbreviated composition below.
let call = GraphSubgraphCall::bounded_loop(
NodeId::new("review")?,
SharedStateSubgraph::new(body)?,
NodeId::new("again")?,
3,
)?;
let composition = GraphComposition::compile(parent, [call])?; Make return and exhaustion paths explicit
Pure terminal nodes in an acyclic template are symbolic return ports, never executable nodes. The parent's call-site declares one matching route per port. Body nodes exchange typed state updates; they cannot produce child terminal outputs that might be discarded.
Selecting again enters the next iteration. On the final iteration
it follows the mandatory parent exhaustion route. Other ports exit early. GraphSubgraphCall::once executes one copy. Reusing a template at multiple call-sites requires globally
unique parent routes; bind them with with_return_routes.
State is visible after each barrier. A child wait suspends the containing run; durable resolution resumes its mapped successors. Parent and child share exact input/state/update/output schemas and reducer references. Ordinary BSP ordering and deduplicated ready sets still apply; this API does not add asynchronous cross-step joins.
Keep actual durable identities
Register only composition.graph(). Use
node_source(node_id) to select the original executable role, but
bind it to the composed graph and generated node ID. Resolve local decisions
through source.route_id(&local_route_id). Reducers also
receive composed IDs and must be role-independent or use the immutable
source map.
Pass the actual GraphNodeContext to durable invocation code unchanged.
Do not manufacture child checkpoints or replace activation identities. SHA-256
scope identities pin parent/template revisions, call-sites, iterations, bounds,
and return bindings; canonical ordinals preserve source order within each instance.
Keep old executable revisions until their in-flight runs drain.
Enforce limits before dispatch
The full expansion is capped at 1,024 executable nodes and a two-MiB canonical definition, with existing route and ready-set limits. Every iteration counts before allocation. Templates must be acyclic and fit their own step ceiling; parent parallelism cannot exceed a template's ceiling. There is no unbounded fallback.
The global maximum_supersteps budget stops new node starts, including
after restart at the limit. Lifecycle supervision records
runtime.graph.superstep_limit_reached only after trusted cumulative
usage is available. Missing evidence never becomes invented zero usage. A committed
failure handoff remains safe to retry.
No checkpoint wire change or database migration is needed. PostgreSQL tests cover pending-result reuse, early return, exhaustion, orphan takeover, changed-version rejection, and wait/resume. Read the complete contract and upgrade guide or the durable runtime guide.