Skip to main content

StateGraph

Struct StateGraph 

Source
pub struct StateGraph<S>
where S: Clone + Send + Sync + 'static,
{ /* 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 the StateMerge trait. The state struct advertises its merge story (typically through #[derive(StateMerge)] and per-field Annotated<T, R> wrappers); the node returns an Option-wrapped S::Contribution naming exactly the slots it touched. Slots left as None keep the current value; slots set to Some merge through the per-field reducer.

Implementations§

Source§

impl<S> StateGraph<S>
where S: Clone + Send + Sync + 'static,

Source

pub fn new() -> Self

Empty graph.

Source

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.

Source

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.

Source

pub fn add_node<R>(self, name: impl Into<String>, runnable: R) -> Self
where 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).

Source

pub fn add_node_with<R, U, F>( self, name: impl Into<String>, runnable: R, merger: F, ) -> Self
where R: Runnable<S, U> + 'static, U: Send + Sync + 'static, F: Fn(S, U) -> Result<S> + Send + Sync + 'static,

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.

Source

pub fn add_contributing_node<R>( self, name: impl Into<String>, runnable: R, ) -> Self
where R: Runnable<S, S::Contribution> + 'static, S: StateMerge,

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);
Source

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.

Source

pub fn add_conditional_edges<F, K, V>( self, from: impl Into<String>, selector: F, mapping: impl IntoIterator<Item = (K, V)>, ) -> Self
where F: Fn(&S) -> String + Send + Sync + 'static, K: Into<String>, V: Into<String>,

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.

Source

pub fn add_send_edges<F, I, T>( self, from: impl Into<String>, targets: I, selector: F, join: impl Into<String>, ) -> Self
where 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 fromcompile rejects the combination. The join target must be registered or END.

Source

pub fn set_entry_point(self, name: impl Into<String>) -> Self

Mark the start node. Required at compile time.

Source

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.

Source

pub const fn with_recursion_limit(self, n: usize) -> Self

Override the per-invocation recursion limit (F6 mitigation).

Source

pub fn interrupt_before<I, T>(self, nodes: I) -> Self
where I: IntoIterator<Item = T>, T: Into<String>,

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::Resume re-runs the marked node from the saved pre-state.
  • Command::Update(s) re-runs the marked node from s.
  • Command::GoTo(other) jumps to other instead.

Calling twice unions the supplied node sets.

Source

pub fn interrupt_after<I, T>(self, nodes: I) -> Self
where I: IntoIterator<Item = T>, T: Into<String>,

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.

Source

pub fn node_count(&self) -> usize

Number of registered nodes.

Source

pub fn edge_count(&self) -> usize

Number of registered static edges.

Source

pub fn conditional_edge_count(&self) -> usize

Number of nodes with a conditional dispatch.

Source

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 from or to.
  • Conditional edge from not registered, or any mapping target that is neither a registered node nor END.
  • 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_after referencing a node that is not registered.

Trait Implementations§

Source§

impl<S> Debug for StateGraph<S>
where S: Clone + Send + Sync + 'static,

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<S> Default for StateGraph<S>
where S: Clone + Send + Sync + 'static,

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

§

impl<S> Freeze for StateGraph<S>

§

impl<S> !RefUnwindSafe for StateGraph<S>

§

impl<S> Send for StateGraph<S>

§

impl<S> Sync for StateGraph<S>

§

impl<S> Unpin for StateGraph<S>

§

impl<S> UnsafeUnpin for StateGraph<S>

§

impl<S> !UnwindSafe for StateGraph<S>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more