pub struct StateGraph<S>{ /* private fields */ }Expand description
Builder for a state-machine graph parameterised over its state type S.
Nodes are Runnable<S, S> instances; each one consumes the current state
and returns the new full state. Three node-registration shapes coexist:
Self::add_node— full-state replace. The node owns the entire shape and returns the next state.Self::add_node_with— delta + bespoke merger closure. Best when the merge logic is graph-specific.Self::add_contributing_node— declarative per-field merge via theStateMergetrait. The state struct advertises its merge story (typically through#[derive(StateMerge)]and per-fieldAnnotated<T, R>wrappers); the node returns anOption-wrappedS::Contributionnaming exactly the slots it touched. Slots left asNonekeep the current value; slots set toSomemerge through the per-field reducer.
Implementations§
Source§impl<S> StateGraph<S>
impl<S> StateGraph<S>
Sourcepub fn with_checkpointer(self, checkpointer: Arc<dyn Checkpointer<S>>) -> Self
pub fn with_checkpointer(self, checkpointer: Arc<dyn Checkpointer<S>>) -> Self
Attach a checkpointer.
When set, the compiled graph writes a checkpoint after every node
invocation if the executing ExecutionContext carries a
thread_id. Use CompiledGraph::resume to continue from the
most recent checkpoint after a crash. Tune the write frequency
via Self::with_checkpoint_granularity.
Sourcepub const fn with_checkpoint_granularity(self, g: CheckpointGranularity) -> Self
pub const fn with_checkpoint_granularity(self, g: CheckpointGranularity) -> Self
Override how often the compiled graph writes a checkpoint
when a checkpointer is attached. Defaults to
CheckpointGranularity::PerNode.
Sourcepub fn add_node<R>(self, name: impl Into<String>, runnable: R) -> Selfwhere
R: Runnable<S, S> + 'static,
pub fn add_node<R>(self, name: impl Into<String>, runnable: R) -> Selfwhere
R: Runnable<S, S> + 'static,
Register a node. A second registration with the same name replaces the prior runnable (calls during construction are append-or-replace, not append-only).
Sourcepub fn add_node_with<R, U, F>(
self,
name: impl Into<String>,
runnable: R,
merger: F,
) -> Self
pub fn add_node_with<R, U, F>( self, name: impl Into<String>, runnable: R, merger: F, ) -> Self
Register a delta-style node. The inner runnable produces an
update of arbitrary type U; the merger combines it with the
inbound state into a fresh full state.
Use this when the natural shape of a node is “compute and
return what changed” rather than “thread the entire state
through”. The merger has access to both the snapshot of the
inbound state and the delta, so per-field
Reducer<T> calls (Append, MergeMap,
Max, …) plug in directly:
graph.add_node_with(
"plan",
planner_runnable,
|mut state: PlanState, update: PlannerOutput| {
state.log = Append::<String>::new()
.reduce(state.log, update.new_log_entries);
state.iterations += 1;
Ok(state)
},
);Existing Self::add_node (full-state replace) keeps working
unchanged — the two patterns coexist node-by-node.
Sourcepub fn add_contributing_node<R>(
self,
name: impl Into<String>,
runnable: R,
) -> Self
pub fn add_contributing_node<R>( self, name: impl Into<String>, runnable: R, ) -> Self
Register a contribution-style node whose output names
exactly the slots it touched, folded into the current state
through StateMerge::merge_contribution. The inner
runnable returns S::Contribution — an Option-wrapped
shape mirroring LangGraph’s TypedDict partial-return:
None slots keep the current value, Some slots merge
through the per-field reducer.
Use this when the state type owns its merge story
declaratively (via #[derive(StateMerge)] over fields wrapped
in Annotated<T, R>). Adding a new
state field never edits the graph builder — the per-field
reducer annotation does the work.
use entelix_graph::{Annotated, Append, Max, StateGraph, StateMerge};
use entelix_runnable::RunnableLambda;
#[derive(Clone, Default, StateMerge)]
struct AgentState {
log: Annotated<Vec<String>, Append<String>>,
score: Annotated<i32, Max<i32>>,
last_message: String,
}
// Node writes only `log` and `last_message`; `score`
// stays at whatever the upstream produced (the contribution
// shape carries `None` for it, which means "I didn't touch this").
let planner = RunnableLambda::new(|_state: AgentState, _ctx| async {
Ok(AgentStateContribution::default()
.with_log(vec!["planned".into()])
.with_last_message("scheduled".into()))
});
let graph = StateGraph::<AgentState>::new()
.add_contributing_node("planner", planner);Sourcepub fn add_edge(self, from: impl Into<String>, to: impl Into<String>) -> Self
pub fn add_edge(self, from: impl Into<String>, to: impl Into<String>) -> Self
Register a static from → to edge. Calling twice with the same
from replaces the previous target — single static next-hop per
node.
A node may not have both a static edge and a conditional edge; the
compile() step rejects that combination.
Sourcepub fn add_conditional_edges<F, K, V>(
self,
from: impl Into<String>,
selector: F,
mapping: impl IntoIterator<Item = (K, V)>,
) -> Self
pub fn add_conditional_edges<F, K, V>( self, from: impl Into<String>, selector: F, mapping: impl IntoIterator<Item = (K, V)>, ) -> Self
Register a conditional dispatch: after from runs, evaluate
selector(&state) and route to the node named by
mapping[selector_output]. Mapping targets may be node names or
END.
A second call with the same from replaces the prior conditional.
Mixing with add_edge on the same from is rejected at compile
time.
Sourcepub fn add_send_edges<F, I, T>(
self,
from: impl Into<String>,
targets: I,
selector: F,
join: impl Into<String>,
) -> Selfwhere
F: Fn(&S) -> Vec<(String, S)> + Send + Sync + 'static,
I: IntoIterator<Item = T>,
T: Into<String>,
S: StateMerge,
pub fn add_send_edges<F, I, T>(
self,
from: impl Into<String>,
targets: I,
selector: F,
join: impl Into<String>,
) -> Selfwhere
F: Fn(&S) -> Vec<(String, S)> + Send + Sync + 'static,
I: IntoIterator<Item = T>,
T: Into<String>,
S: StateMerge,
Register a parallel fan-out from from.
targets lists every node the selector may dispatch to —
statically declared so compile() can validate each name
resolves to a registered node and so leaf-validation knows
these nodes have a defined control path (the fan-out merges
their results back into the join node, no per-branch
outgoing edge is required).
After from runs, the runtime evaluates selector(&state)
to obtain a list of (target_node, branch_state) pairs.
Each branch is invoked in parallel; the resulting per-branch
states fold via reducer into a single S. Control then
flows to the join node, which sees the reduced state.
Selector outputs that name a node not in targets cause a
runtime Error::InvalidRequest — typo-resistant by
construction.
Mutually exclusive with Self::add_edge /
Self::add_conditional_edges on the same from — compile
rejects the combination. The join target must be registered
or END.
Sourcepub fn set_entry_point(self, name: impl Into<String>) -> Self
pub fn set_entry_point(self, name: impl Into<String>) -> Self
Mark the start node. Required at compile time.
Sourcepub fn add_finish_point(self, name: impl Into<String>) -> Self
pub fn add_finish_point(self, name: impl Into<String>) -> Self
Mark a node as terminal — running it halts the graph and returns the post-node state. A graph may have more than one finish point; any path that reaches one terminates.
Sourcepub const fn with_recursion_limit(self, n: usize) -> Self
pub const fn with_recursion_limit(self, n: usize) -> Self
Override the per-invocation recursion limit (F6 mitigation).
Sourcepub fn interrupt_before<I, T>(self, nodes: I) -> Self
pub fn interrupt_before<I, T>(self, nodes: I) -> Self
Mark nodes as HITL pause points evaluated before the
node runs. When control reaches a marked node the runtime
raises Error::Interrupted with
kind: InterruptionKind::ScheduledPause { phase: Before, node }
(the payload is Value::Null — every distinguishing
detail is on the typed kind) and (when a Checkpointer is
attached) persists a checkpoint pointing back at the same
node.
Resume via the existing Command machinery:
Command::Resumere-runs the marked node from the saved pre-state.Command::Update(s)re-runs the marked node froms.Command::GoTo(other)jumps tootherinstead.
Calling twice unions the supplied node sets.
Sourcepub fn interrupt_after<I, T>(self, nodes: I) -> Self
pub fn interrupt_after<I, T>(self, nodes: I) -> Self
Mark nodes as HITL pause points evaluated after the
node completes successfully. When such a node returns Ok
the runtime raises Error::Interrupted with
kind: InterruptionKind::ScheduledPause { phase: After, node }
and persists a checkpoint with the post-node state pointing
at the resolved next node — Command::Resume then continues
forward, skipping a re-run of the just-completed node.
Sourcepub fn node_count(&self) -> usize
pub fn node_count(&self) -> usize
Number of registered nodes.
Sourcepub fn edge_count(&self) -> usize
pub fn edge_count(&self) -> usize
Number of registered static edges.
Sourcepub fn conditional_edge_count(&self) -> usize
pub fn conditional_edge_count(&self) -> usize
Number of nodes with a conditional dispatch.
Sourcepub fn compile(self) -> Result<CompiledGraph<S>>
pub fn compile(self) -> Result<CompiledGraph<S>>
Validate and freeze the graph.
Returns Err(Error::Config(_)) for:
- Missing entry point.
- Entry point referencing an unregistered node.
- Static edge referencing an unregistered
fromorto. - Conditional edge
fromnot registered, or any mapping target that is neither a registered node norEND. - A node with both a static edge AND a conditional edge.
- No finish points registered.
- Finish point referencing an unregistered node.
- A non-finish-point node has no outgoing edge (static or conditional).
interrupt_before/interrupt_afterreferencing a node that is not registered.