Skip to main content

adk_graph/
graph.rs

1//! StateGraph builder for constructing graphs
2
3use crate::checkpoint::Checkpointer;
4use crate::deferred::DeferredNodeConfig;
5use crate::edge::{END, Edge, EdgeTarget, RouterFn, START};
6use crate::error::{GraphError, Result};
7use crate::node::{FunctionNode, Node, NodeContext, NodeOutput};
8use crate::state::{State, StateSchema};
9use std::collections::{HashMap, HashSet};
10use std::future::Future;
11use std::sync::Arc;
12
13/// Builder for constructing graphs
14pub struct StateGraph {
15    /// State schema
16    pub schema: StateSchema,
17    /// Registered nodes
18    pub nodes: HashMap<String, Arc<dyn Node>>,
19    /// Registered edges
20    pub edges: Vec<Edge>,
21    /// Fan-in (deferred) node configurations, keyed by node name.
22    pub deferred_configs: HashMap<String, DeferredNodeConfig>,
23}
24
25impl StateGraph {
26    /// Create a new graph with the given state schema
27    pub fn new(schema: StateSchema) -> Self {
28        Self { schema, nodes: HashMap::new(), edges: vec![], deferred_configs: HashMap::new() }
29    }
30
31    /// Create with a simple schema (just channel names, all overwrite)
32    pub fn with_channels(channels: &[&str]) -> Self {
33        Self::new(StateSchema::simple(channels))
34    }
35
36    /// Add a node to the graph
37    pub fn add_node<N: Node + 'static>(mut self, node: N) -> Self {
38        self.nodes.insert(node.name().to_string(), Arc::new(node));
39        self
40    }
41
42    /// Add a function as a node
43    pub fn add_node_fn<F, Fut>(self, name: &str, func: F) -> Self
44    where
45        F: Fn(NodeContext) -> Fut + Send + Sync + 'static,
46        Fut: Future<Output = Result<NodeOutput>> + Send + 'static,
47    {
48        self.add_node(FunctionNode::new(name, func))
49    }
50
51    /// Add a **fan-in** (deferred) function node.
52    ///
53    /// Unlike [`add_node_fn`](Self::add_node_fn), a deferred node does not run as
54    /// soon as one upstream edge completes — the scheduler holds it until **all**
55    /// upstream paths that can reach it have finished (or, with a configured
56    /// `fan_in_timeout`, until that deadline). This is what makes a fan-out /
57    /// fan-in pattern correct: several branches run in parallel and a single
58    /// aggregator node runs once, after they all complete.
59    ///
60    /// The [`DeferredNodeConfig`] selects how the upstream outputs are exposed
61    /// (e.g. [`MergeStrategy::Collect`](crate::deferred::MergeStrategy::Collect))
62    /// and an optional fan-in timeout.
63    ///
64    /// # Example
65    /// ```ignore
66    /// use adk_graph::{StateGraph, DeferredNodeConfig, MergeStrategy};
67    /// let graph = StateGraph::with_channels(&["x"])
68    ///     .add_node_fn("a", |_| async { Ok(Default::default()) })
69    ///     .add_node_fn("b", |_| async { Ok(Default::default()) })
70    ///     .add_deferred_node_fn("join", |_| async { Ok(Default::default()) },
71    ///         DeferredNodeConfig { merge_strategy: MergeStrategy::Collect, ..Default::default() })
72    ///     .add_edge("a", "join")
73    ///     .add_edge("b", "join");
74    /// ```
75    pub fn add_deferred_node_fn<F, Fut>(
76        mut self,
77        name: &str,
78        func: F,
79        config: DeferredNodeConfig,
80    ) -> Self
81    where
82        F: Fn(NodeContext) -> Fut + Send + Sync + 'static,
83        Fut: Future<Output = Result<NodeOutput>> + Send + 'static,
84    {
85        self.deferred_configs.insert(name.to_string(), config);
86        self.add_node(FunctionNode::new(name, func))
87    }
88
89    /// Mark an already-added node as a fan-in (deferred) node.
90    ///
91    /// Useful when the node was registered via [`add_node`](Self::add_node) with
92    /// a custom [`Node`] implementation.
93    pub fn mark_deferred(mut self, name: &str, config: DeferredNodeConfig) -> Self {
94        self.deferred_configs.insert(name.to_string(), config);
95        self
96    }
97
98    /// Add a direct edge from source to target
99    pub fn add_edge(mut self, source: &str, target: &str) -> Self {
100        let target = EdgeTarget::from(target);
101
102        if source == START {
103            // Find existing entry or create new one
104            let entry_idx = self.edges.iter().position(|e| matches!(e, Edge::Entry { .. }));
105
106            match entry_idx {
107                Some(idx) => {
108                    if let Edge::Entry { targets } = &mut self.edges[idx]
109                        && let EdgeTarget::Node(node) = &target
110                        && !targets.contains(node)
111                    {
112                        targets.push(node.clone());
113                    }
114                }
115                None => {
116                    if let EdgeTarget::Node(node) = target {
117                        self.edges.push(Edge::Entry { targets: vec![node] });
118                    }
119                }
120            }
121        } else {
122            self.edges.push(Edge::Direct { source: source.to_string(), target });
123        }
124
125        self
126    }
127
128    /// Add a conditional edge with a router function
129    pub fn add_conditional_edges<F, I>(mut self, source: &str, router: F, targets: I) -> Self
130    where
131        F: Fn(&State) -> String + Send + Sync + 'static,
132        I: IntoIterator<Item = (&'static str, &'static str)>,
133    {
134        let targets_map: HashMap<String, EdgeTarget> =
135            targets.into_iter().map(|(k, v)| (k.to_string(), EdgeTarget::from(v))).collect();
136
137        self.edges.push(Edge::Conditional {
138            source: source.to_string(),
139            router: Arc::new(router),
140            targets: targets_map,
141        });
142
143        self
144    }
145
146    /// Add a conditional edge with an Arc router (for pre-built routers)
147    pub fn add_conditional_edges_arc<I>(
148        mut self,
149        source: &str,
150        router: RouterFn,
151        targets: I,
152    ) -> Self
153    where
154        I: IntoIterator<Item = (&'static str, &'static str)>,
155    {
156        let targets_map: HashMap<String, EdgeTarget> =
157            targets.into_iter().map(|(k, v)| (k.to_string(), EdgeTarget::from(v))).collect();
158
159        self.edges.push(Edge::Conditional {
160            source: source.to_string(),
161            router,
162            targets: targets_map,
163        });
164
165        self
166    }
167
168    /// Compile the graph for execution
169    pub fn compile(mut self) -> Result<CompiledGraph> {
170        // A node with requirements on the graph that holds it states them now, so
171        // a mismatch cannot reach a run. `SubgraphNode` checks its channel map here.
172        for node in self.nodes.values() {
173            node.validate_against(&self.schema)?;
174        }
175        self.validate()?;
176        self.defer_unconditional_fan_in();
177
178        Ok(CompiledGraph {
179            schema: self.schema,
180            nodes: self.nodes,
181            edges: self.edges,
182            checkpointer: None,
183            interrupt_before: HashSet::new(),
184            interrupt_after: HashSet::new(),
185            recursion_limit: 100,
186            timeout_policies: HashMap::new(),
187            default_timeout: None,
188            default_retry: None,
189            error_handlers: HashMap::new(),
190            default_error_handler: None,
191            deferred_configs: self.deferred_configs,
192            max_concurrency: None,
193            retry_policies: HashMap::new(),
194            strict_channels: false,
195            retention: None,
196            #[cfg(feature = "node-cache")]
197            cache_policies: HashMap::new(),
198            #[cfg(feature = "node-cache")]
199            node_caches: HashMap::new(),
200        })
201    }
202
203    /// Mark any node reached by more than one unconditional edge as deferred.
204    ///
205    /// The frontier advances from whichever nodes finished in the last
206    /// super-step, so without this a join becomes eligible as soon as one
207    /// predecessor lands. On branches of unequal length it then runs once per
208    /// arriving predecessor, applying its updates repeatedly and reading a
209    /// half-built state.
210    ///
211    /// Only `Direct` and `Entry` edges count. A conditional predecessor may never
212    /// fire, and waiting for one that cannot arrive would deadlock the join. A
213    /// graph whose fan-in arrives through conditional edges therefore still needs
214    /// `mark_deferred` and a `fan_in_timeout`.
215    ///
216    /// An explicit configuration always wins, so a caller who wants the earlier
217    /// behaviour keeps it by configuring the node themselves.
218    fn defer_unconditional_fan_in(&mut self) {
219        let mut in_degree: HashMap<&str, usize> = HashMap::new();
220        for edge in &self.edges {
221            match edge {
222                Edge::Direct { target, .. } => {
223                    if let Some(name) = target.node_name() {
224                        *in_degree.entry(name).or_insert(0) += 1;
225                    }
226                }
227                Edge::Entry { targets } => {
228                    for target in targets {
229                        *in_degree.entry(target.as_str()).or_insert(0) += 1;
230                    }
231                }
232                // A conditional edge selects one target at run time, so its
233                // targets are not guaranteed arrivals.
234                Edge::Conditional { .. } => {}
235            }
236        }
237
238        let fan_ins: Vec<String> = in_degree
239            .into_iter()
240            .filter(|(name, degree)| *degree > 1 && self.nodes.contains_key(*name))
241            .map(|(name, _)| name.to_string())
242            .collect();
243
244        for name in fan_ins {
245            self.deferred_configs.entry(name).or_default();
246        }
247    }
248
249    /// Validate the graph structure
250    fn validate(&self) -> Result<()> {
251        // Check for entry point
252        let has_entry = self.edges.iter().any(|e| matches!(e, Edge::Entry { .. }));
253        if !has_entry {
254            return Err(GraphError::NoEntryPoint);
255        }
256
257        // Reject a node that cannot execute. A configuration whose backend is
258        // unavailable should fail here, not part-way through a run when earlier nodes
259        // may already have had side effects.
260        for node in self.nodes.values() {
261            node.validate()?;
262        }
263
264        // Check all node references exist
265        for edge in &self.edges {
266            match edge {
267                Edge::Direct { source, target } => {
268                    if source != START && !self.nodes.contains_key(source) {
269                        return Err(GraphError::NodeNotFound(source.clone()));
270                    }
271                    if let EdgeTarget::Node(name) = target
272                        && !self.nodes.contains_key(name)
273                    {
274                        return Err(GraphError::EdgeTargetNotFound(name.clone()));
275                    }
276                }
277                Edge::Conditional { source, targets, .. } => {
278                    if !self.nodes.contains_key(source) {
279                        return Err(GraphError::NodeNotFound(source.clone()));
280                    }
281                    for target in targets.values() {
282                        if let EdgeTarget::Node(name) = target
283                            && !self.nodes.contains_key(name)
284                        {
285                            return Err(GraphError::EdgeTargetNotFound(name.clone()));
286                        }
287                    }
288                }
289                Edge::Entry { targets } => {
290                    for target in targets {
291                        if !self.nodes.contains_key(target) {
292                            return Err(GraphError::EdgeTargetNotFound(target.clone()));
293                        }
294                    }
295                }
296            }
297        }
298
299        Ok(())
300    }
301}
302
303/// Turns a node failure into state and a route, instead of ending the run.
304///
305/// Called after the node's retry budget is spent. Returning a
306/// [`crate::node::NodeOutput`] lets the handler record what happened
307/// and name a recovery node with
308/// [`with_goto`](crate::node::NodeOutput::with_goto). Returning `Err` ends the
309/// run as before.
310pub type NodeErrorHandler =
311    Arc<dyn Fn(&str, &GraphError, &State) -> Result<crate::node::NodeOutput> + Send + Sync>;
312
313/// Policies a graph applies to every node that does not set its own.
314///
315/// Repeating the same retry or timeout on twenty nodes is easy to get wrong by
316/// omission. A default states it once; a per-node value always wins.
317///
318/// # Example
319///
320/// ```
321/// use adk_graph::graph::NodeDefaults;
322/// use adk_graph::retry::RetryPolicy;
323/// use adk_graph::timeout::TimeoutPolicy;
324/// use std::time::Duration;
325///
326/// let defaults = NodeDefaults::new().with_retry(RetryPolicy::new(3)).with_timeout(
327///     TimeoutPolicy { run_timeout: Some(Duration::from_secs(30)), ..Default::default() },
328/// );
329/// # let _ = defaults;
330/// ```
331#[derive(Clone, Default)]
332pub struct NodeDefaults {
333    /// Retry policy for a node with none of its own.
334    pub retry: Option<crate::retry::RetryPolicy>,
335    /// Timeout policy for a node with none of its own.
336    pub timeout: Option<crate::timeout::TimeoutPolicy>,
337    /// Failure handler for a node with none of its own.
338    pub error_handler: Option<NodeErrorHandler>,
339}
340
341impl std::fmt::Debug for NodeDefaults {
342    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
343        f.debug_struct("NodeDefaults")
344            .field("retry", &self.retry)
345            .field("timeout", &self.timeout)
346            .field("error_handler", &self.error_handler.as_ref().map(|_| "<handler>"))
347            .finish()
348    }
349}
350
351impl NodeDefaults {
352    /// An empty set of defaults, which changes nothing.
353    pub fn new() -> Self {
354        Self::default()
355    }
356
357    /// Applies this retry policy to every node that sets none.
358    pub fn with_retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
359        self.retry = Some(policy);
360        self
361    }
362
363    /// Applies this timeout policy to every node that sets none.
364    pub fn with_timeout(mut self, policy: crate::timeout::TimeoutPolicy) -> Self {
365        self.timeout = Some(policy);
366        self
367    }
368
369    /// Applies this failure handler to every node that sets none.
370    pub fn with_error_handler<F>(mut self, handler: F) -> Self
371    where
372        F: Fn(&str, &GraphError, &State) -> Result<crate::node::NodeOutput> + Send + Sync + 'static,
373    {
374        self.error_handler = Some(Arc::new(handler));
375        self
376    }
377}
378
379/// A compiled graph ready for execution
380pub struct CompiledGraph {
381    pub(crate) schema: StateSchema,
382    pub(crate) nodes: HashMap<String, Arc<dyn Node>>,
383    pub(crate) edges: Vec<Edge>,
384    pub(crate) checkpointer: Option<Arc<dyn Checkpointer>>,
385    pub(crate) interrupt_before: HashSet<String>,
386    pub(crate) interrupt_after: HashSet<String>,
387    pub(crate) recursion_limit: usize,
388    /// Per-node timeout policies, keyed by node name.
389    pub(crate) timeout_policies: HashMap<String, crate::timeout::TimeoutPolicy>,
390    /// Default timeout policy applied to all nodes without an explicit override.
391    pub(crate) default_timeout: Option<crate::timeout::TimeoutPolicy>,
392    /// Retry policy for every node that sets none of its own.
393    pub(crate) default_retry: Option<crate::retry::RetryPolicy>,
394    /// Per-node failure handlers, keyed by node name.
395    pub(crate) error_handlers: HashMap<String, NodeErrorHandler>,
396    /// Failure handler for every node that sets none of its own.
397    pub(crate) default_error_handler: Option<NodeErrorHandler>,
398    /// Deferred node configurations, keyed by node name.
399    pub(crate) deferred_configs: HashMap<String, crate::deferred::DeferredNodeConfig>,
400    /// Ceiling on how many nodes execute at once. `None` runs the whole frontier.
401    pub(crate) max_concurrency: Option<usize>,
402    /// Per-node retry policies, keyed by node name.
403    pub(crate) retry_policies: HashMap<String, crate::retry::RetryPolicy>,
404    /// Whether a node writing an undeclared channel fails the run.
405    pub(crate) strict_channels: bool,
406    /// How many checkpoints to keep per thread. `None` keeps every one.
407    pub(crate) retention: Option<crate::checkpoint::RetentionPolicy>,
408    /// Per-node cache policies, keyed by node name.
409    #[cfg(feature = "node-cache")]
410    pub(crate) cache_policies: HashMap<String, crate::cache::NodeCachePolicy>,
411    /// Per-node caches built from `cache_policies`, shared by every run of this
412    /// graph so a later run reuses what an earlier one stored.
413    #[cfg(feature = "node-cache")]
414    pub(crate) node_caches: HashMap<String, Arc<crate::cache::NodeCache>>,
415}
416
417impl CompiledGraph {
418    /// Configure checkpointing
419    pub fn with_checkpointer<C: Checkpointer + 'static>(mut self, checkpointer: C) -> Self {
420        self.checkpointer = Some(Arc::new(checkpointer));
421        self
422    }
423
424    /// Configure checkpointing with Arc
425    pub fn with_checkpointer_arc(mut self, checkpointer: Arc<dyn Checkpointer>) -> Self {
426        self.checkpointer = Some(checkpointer);
427        self
428    }
429
430    /// Configure interrupt before specific nodes
431    pub fn with_interrupt_before(mut self, nodes: &[&str]) -> Self {
432        self.interrupt_before = nodes.iter().map(|s| s.to_string()).collect();
433        self
434    }
435
436    /// Configure interrupt after specific nodes
437    pub fn with_interrupt_after(mut self, nodes: &[&str]) -> Self {
438        self.interrupt_after = nodes.iter().map(|s| s.to_string()).collect();
439        self
440    }
441
442    /// Set recursion limit for cycles
443    pub fn with_recursion_limit(mut self, limit: usize) -> Self {
444        self.recursion_limit = limit;
445        self
446    }
447
448    /// Cap how many nodes execute concurrently within one super-step.
449    ///
450    /// A wide fan-out otherwise dispatches its whole frontier at once, which can
451    /// exhaust a connection pool or trip a provider rate limit. Nodes beyond the
452    /// cap wait for a slot; the dispatch order is the frontier's, sorted, so it
453    /// does not depend on timing.
454    ///
455    /// Without this the frontier runs unbounded, which stays the default.
456    pub fn with_max_concurrency(mut self, limit: usize) -> Self {
457        self.max_concurrency = Some(limit.max(1));
458        self
459    }
460
461    /// Fail the run when a node writes a channel the schema does not declare.
462    ///
463    /// An undeclared channel otherwise takes the overwrite reducer, because that
464    /// is the fallback for a name the schema does not hold. A graph that declared
465    /// a list channel and then wrote a near-miss name keeps only the last value
466    /// and reports nothing. Enforcement turns that into
467    /// [`crate::error::GraphError::UndeclaredChannel`].
468    ///
469    /// A graph that declares no channels accepts any name even under
470    /// enforcement, because there is nothing to check against.
471    ///
472    /// Off by default: a graph may legitimately declare the channels a caller
473    /// reads and let its nodes pass other values between themselves.
474    ///
475    /// # Example
476    ///
477    /// ```
478    /// use adk_graph::edge::{END, START};
479    /// use adk_graph::graph::StateGraph;
480    /// use adk_graph::node::NodeOutput;
481    /// use serde_json::json;
482    ///
483    /// let graph = StateGraph::with_channels(&["total"])
484    ///     .add_node_fn("sum", |_ctx| async move {
485    ///         Ok(NodeOutput::new().with_update("total", json!(3)))
486    ///     })
487    ///     .add_edge(START, "sum")
488    ///     .add_edge("sum", END)
489    ///     .compile()
490    ///     .unwrap()
491    ///     .with_strict_channels();
492    /// # let _ = graph;
493    /// ```
494    pub fn with_strict_channels(mut self) -> Self {
495        self.strict_channels = true;
496        self
497    }
498
499    /// Discards old checkpoints as the run proceeds.
500    ///
501    /// A thread otherwise accumulates one checkpoint per super-step for as long as
502    /// it lives, which costs storage and slows a `list`. The newest is always kept,
503    /// because it is the one a resume loads.
504    ///
505    /// Off by default, so an existing thread keeps its whole history and time
506    /// travel can still reach every step.
507    ///
508    /// # Example
509    ///
510    /// ```
511    /// use adk_graph::checkpoint::{MemoryCheckpointer, RetentionPolicy};
512    /// use adk_graph::edge::{END, START};
513    /// use adk_graph::graph::StateGraph;
514    /// use adk_graph::node::NodeOutput;
515    ///
516    /// let graph = StateGraph::with_channels(&["value"])
517    ///     .add_node_fn("step", |_ctx| async move { Ok(NodeOutput::new()) })
518    ///     .add_edge(START, "step")
519    ///     .add_edge("step", END)
520    ///     .compile()?
521    ///     .with_checkpointer(MemoryCheckpointer::new())
522    ///     .with_checkpoint_retention(RetentionPolicy::keep_last(20));
523    /// # let _ = graph;
524    /// # Ok::<(), adk_graph::error::GraphError>(())
525    /// ```
526    pub fn with_checkpoint_retention(mut self, policy: crate::checkpoint::RetentionPolicy) -> Self {
527        self.retention = Some(policy);
528        self
529    }
530
531    /// Applies policies to every node that does not set its own.
532    ///
533    /// Repeating the same retry or timeout across twenty nodes is easy to get
534    /// wrong by omission. A per-node value always wins over the default.
535    ///
536    /// # Example
537    ///
538    /// ```
539    /// use adk_graph::edge::{END, START};
540    /// use adk_graph::graph::{NodeDefaults, StateGraph};
541    /// use adk_graph::node::NodeOutput;
542    /// use adk_graph::retry::RetryPolicy;
543    ///
544    /// let graph = StateGraph::with_channels(&["value"])
545    ///     .add_node_fn("fetch", |_ctx| async move { Ok(NodeOutput::new()) })
546    ///     .add_edge(START, "fetch")
547    ///     .add_edge("fetch", END)
548    ///     .compile()?
549    ///     // Every node retries three times, unless it says otherwise.
550    ///     .with_node_defaults(NodeDefaults::new().with_retry(RetryPolicy::new(3)))
551    ///     // And this one gets five.
552    ///     .with_node_retry("fetch", RetryPolicy::new(5));
553    /// # let _ = graph;
554    /// # Ok::<(), adk_graph::error::GraphError>(())
555    /// ```
556    pub fn with_node_defaults(mut self, defaults: NodeDefaults) -> Self {
557        if let Some(retry) = defaults.retry {
558            self.default_retry = Some(retry);
559        }
560        if let Some(timeout) = defaults.timeout {
561            self.default_timeout = Some(timeout);
562        }
563        if let Some(handler) = defaults.error_handler {
564            self.default_error_handler = Some(handler);
565        }
566        self
567    }
568
569    /// Handles one node's failure instead of ending the run.
570    ///
571    /// Called once the node's retry budget is spent. The handler receives the node
572    /// name, the error, and the state as it stands, and returns the updates to
573    /// apply — typically recording what failed and naming a recovery node with
574    /// [`NodeOutput::with_goto`](crate::node::NodeOutput::with_goto). Returning
575    /// `Err` ends the run.
576    ///
577    /// An interrupt is never routed here: a pause is not a failure.
578    pub fn with_node_error_handler<F>(mut self, node: &str, handler: F) -> Self
579    where
580        F: Fn(&str, &GraphError, &State) -> Result<crate::node::NodeOutput> + Send + Sync + 'static,
581    {
582        self.error_handlers.insert(node.to_string(), Arc::new(handler));
583        self
584    }
585
586    /// Whether this graph holds a checkpointer.
587    pub fn has_checkpointer(&self) -> bool {
588        self.checkpointer.is_some()
589    }
590
591    /// Whether this graph declares any static interrupt gate.
592    ///
593    /// A dynamic interrupt cannot be seen from the graph, because a node decides
594    /// at run time, so this reports only the declared gates.
595    pub fn can_pause(&self) -> bool {
596        !self.interrupt_before.is_empty() || !self.interrupt_after.is_empty()
597    }
598
599    /// The failure handler for a node, per-node first, then the graph default.
600    pub(crate) fn error_handler_for(&self, node: &str) -> Option<&NodeErrorHandler> {
601        self.error_handlers.get(node).or(self.default_error_handler.as_ref())
602    }
603
604    /// Attach a retry policy to one node.
605    ///
606    /// A node with no policy is attempted once, which is the behaviour of a graph
607    /// that configures none.
608    pub fn with_node_retry(mut self, node: &str, policy: crate::retry::RetryPolicy) -> Self {
609        self.retry_policies.insert(node.to_string(), policy);
610        self
611    }
612
613    /// A node by name, for a caller that needs to run one on its own.
614    pub fn node(&self, name: &str) -> Option<Arc<dyn Node>> {
615        self.nodes.get(name).cloned()
616    }
617
618    /// The declared state channel names, sorted.
619    pub fn state_channels(&self) -> Vec<String> {
620        let mut names: Vec<String> = self.schema.channels.keys().cloned().collect();
621        names.sort();
622        names
623    }
624
625    /// The retry policy for a node.
626    ///
627    /// The per-node policy wins; otherwise the graph's default applies. `None`
628    /// when neither is set, which means one attempt.
629    pub(crate) fn retry_policy_for(&self, node: &str) -> Option<&crate::retry::RetryPolicy> {
630        self.retry_policies.get(node).or(self.default_retry.as_ref())
631    }
632
633    /// Get the effective timeout policy for a node.
634    ///
635    /// Returns the per-node policy if one was configured via
636    /// `GraphAgentBuilder::node_timeout`, otherwise falls back to the
637    /// default timeout policy. Returns `None` if neither is set.
638    pub fn timeout_policy_for(&self, node_name: &str) -> Option<&crate::timeout::TimeoutPolicy> {
639        self.timeout_policies.get(node_name).or(self.default_timeout.as_ref())
640    }
641
642    /// Get entry nodes
643    pub fn get_entry_nodes(&self) -> Vec<String> {
644        for edge in &self.edges {
645            if let Edge::Entry { targets } = edge {
646                return targets.clone();
647            }
648        }
649        vec![]
650    }
651
652    /// Get next nodes after executing the given nodes
653    /// # Errors
654    ///
655    /// Returns [`GraphError::UnknownRouteTarget`] when a router answers with a
656    /// key that is not among the declared targets. A route to `END` is declared,
657    /// so it is not an error; a key nobody declared is, because the branch would
658    /// otherwise stop and the run would report success having skipped the work.
659    pub fn get_next_nodes(&self, executed: &[String], state: &State) -> Result<Vec<String>> {
660        let mut next = Vec::new();
661
662        for edge in &self.edges {
663            match edge {
664                Edge::Direct { source, target: EdgeTarget::Node(n) }
665                    if executed.contains(source) && !next.contains(n) =>
666                {
667                    next.push(n.clone());
668                }
669                Edge::Conditional { source, router, targets } if executed.contains(source) => {
670                    let route = router(state);
671                    match targets.get(&route) {
672                        Some(EdgeTarget::Node(n)) if !next.contains(n) => next.push(n.clone()),
673                        // Declared, and either already queued or the end of this branch.
674                        Some(_) => {}
675                        None => {
676                            return Err(GraphError::UnknownRouteTarget(format!(
677                                "node '{source}' routed to '{route}', which is not a declared target. Declared: {declared:?}",
678                                declared = {
679                                    let mut keys: Vec<&str> =
680                                        targets.keys().map(String::as_str).collect();
681                                    keys.sort_unstable();
682                                    keys
683                                }
684                            )));
685                        }
686                    }
687                }
688                _ => {}
689            }
690        }
691
692        Ok(next)
693    }
694
695    /// Reports the conditional dispatches the executed nodes produce.
696    ///
697    /// Only conditional edges appear: a direct edge involves no decision. Used
698    /// for [`StreamEvent::RouteDispatched`](crate::stream::StreamEvent::RouteDispatched),
699    /// and called only when a caller asked for the debug stream, so a router is
700    /// not evaluated again on the common path.
701    ///
702    /// # Errors
703    ///
704    /// Returns [`GraphError::UnknownRouteTarget`] on an undeclared route key,
705    /// matching [`Self::get_next_nodes`].
706    pub fn route_dispatches(
707        &self,
708        executed: &[String],
709        state: &State,
710    ) -> Result<Vec<(String, Vec<String>)>> {
711        let mut dispatches = Vec::new();
712        for edge in &self.edges {
713            if let Edge::Conditional { source, router, targets } = edge
714                && executed.contains(source)
715            {
716                let route = router(state);
717                match targets.get(&route) {
718                    Some(EdgeTarget::Node(n)) => {
719                        dispatches.push((source.clone(), vec![n.clone()]));
720                    }
721                    Some(_) => dispatches.push((source.clone(), Vec::new())),
722                    None => {
723                        return Err(GraphError::UnknownRouteTarget(format!(
724                            "node '{source}' routed to '{route}', which is not a declared target"
725                        )));
726                    }
727                }
728            }
729        }
730        Ok(dispatches)
731    }
732
733    /// Check if any of the executed nodes lead to END
734    pub fn leads_to_end(&self, executed: &[String], state: &State) -> bool {
735        for edge in &self.edges {
736            match edge {
737                Edge::Direct { source, target } if executed.contains(source) && target.is_end() => {
738                    return true;
739                }
740                Edge::Conditional { source, router, targets } if executed.contains(source) => {
741                    let route = router(state);
742                    if route == END {
743                        return true;
744                    }
745                    if let Some(target) = targets.get(&route)
746                        && target.is_end()
747                    {
748                        return true;
749                    }
750                }
751                _ => {}
752            }
753        }
754        false
755    }
756
757    /// Get all upstream source nodes for a given target node.
758    ///
759    /// Returns the names of all nodes that have an edge pointing to the given
760    /// target node. This is used by the deferred node scheduler to determine
761    /// which upstream paths must complete before a fan-in node can execute.
762    ///
763    /// For conditional edges, all possible source nodes are included since any
764    /// of them could route to the target at runtime.
765    pub fn get_upstream_nodes(&self, target_node: &str) -> Vec<String> {
766        let mut sources = Vec::new();
767
768        for edge in &self.edges {
769            match edge {
770                Edge::Direct { source, target } => {
771                    if let EdgeTarget::Node(name) = target
772                        && name == target_node
773                        && !sources.contains(source)
774                    {
775                        sources.push(source.clone());
776                    }
777                }
778                Edge::Conditional { source, targets, .. } => {
779                    for target in targets.values() {
780                        if let EdgeTarget::Node(name) = target
781                            && name == target_node
782                            && !sources.contains(source)
783                        {
784                            sources.push(source.clone());
785                        }
786                    }
787                }
788                Edge::Entry { targets } => {
789                    if targets.contains(&target_node.to_string()) {
790                        // Entry nodes come from START, which is not a real node
791                        // so we don't add it as an upstream source
792                    }
793                }
794            }
795        }
796
797        sources
798    }
799
800    /// Get the state schema
801    pub fn schema(&self) -> &StateSchema {
802        &self.schema
803    }
804
805    /// Get the checkpointer if configured
806    pub fn checkpointer(&self) -> Option<&Arc<dyn Checkpointer>> {
807        self.checkpointer.as_ref()
808    }
809}
810
811#[cfg(test)]
812mod tests {
813    use super::*;
814    use serde_json::json;
815
816    #[test]
817    fn test_basic_graph_construction() {
818        let graph = StateGraph::with_channels(&["input", "output"])
819            .add_node_fn("process", |_ctx| async { Ok(NodeOutput::new()) })
820            .add_edge(START, "process")
821            .add_edge("process", END)
822            .compile();
823
824        assert!(graph.is_ok());
825    }
826
827    #[test]
828    fn test_graph_missing_entry() {
829        let graph = StateGraph::with_channels(&["input"])
830            .add_node_fn("process", |_ctx| async { Ok(NodeOutput::new()) })
831            .add_edge("process", END) // No START -> process edge
832            .compile();
833
834        assert!(matches!(graph, Err(GraphError::NoEntryPoint)));
835    }
836
837    #[test]
838    fn test_graph_missing_node() {
839        let graph = StateGraph::with_channels(&["input"]).add_edge(START, "nonexistent").compile();
840
841        assert!(matches!(graph, Err(GraphError::EdgeTargetNotFound(_))));
842    }
843
844    #[test]
845    fn test_conditional_edges() {
846        let graph = StateGraph::with_channels(&["next"])
847            .add_node_fn("router", |_ctx| async { Ok(NodeOutput::new()) })
848            .add_node_fn("path_a", |_ctx| async { Ok(NodeOutput::new()) })
849            .add_node_fn("path_b", |_ctx| async { Ok(NodeOutput::new()) })
850            .add_edge(START, "router")
851            .add_conditional_edges(
852                "router",
853                |state| state.get("next").and_then(|v| v.as_str()).unwrap_or(END).to_string(),
854                [("path_a", "path_a"), ("path_b", "path_b"), (END, END)],
855            )
856            .compile()
857            .unwrap();
858
859        // Test routing
860        let mut state = State::new();
861        state.insert("next".to_string(), json!("path_a"));
862        let next = graph.get_next_nodes(&["router".to_string()], &state).unwrap();
863        assert_eq!(next, vec!["path_a".to_string()]);
864
865        state.insert("next".to_string(), json!("path_b"));
866        let next = graph.get_next_nodes(&["router".to_string()], &state).unwrap();
867        assert_eq!(next, vec!["path_b".to_string()]);
868    }
869}