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