1#[cfg(feature = "node-cache")]
6use crate::cache::{NodeCache, compute_cache_key};
7use crate::deferred::FanInTracker;
8use crate::error::{GraphError, InterruptedExecution, Result};
9use crate::graph::CompiledGraph;
10use crate::interrupt::{GraphToolConfirmationPause, Interrupt};
11use crate::node::{ExecutionConfig, NodeContext};
12use crate::state::{Checkpoint, State};
13use crate::stream::{StreamEvent, StreamMode};
14use crate::timeout::{OnTimeout, ProgressHandle, execute_with_timeout, item_timeout_budget};
15use futures::stream::{self, StreamExt};
16use std::collections::HashMap;
17use std::sync::Arc;
18use std::time::Instant;
19
20#[derive(Default)]
22pub struct SuperStepResult {
23 pub executed_nodes: Vec<String>,
25 pub interrupt: Option<Interrupt>,
27 pub events: Vec<StreamEvent>,
29 pub goto: HashMap<String, Vec<String>>,
31}
32
33#[derive(Debug, Clone)]
35pub struct GraphOutcome {
36 pub state: State,
38 pub goto_parent: Option<Vec<String>>,
43}
44
45pub struct PregelExecutor<'a> {
47 graph: &'a CompiledGraph,
48 config: ExecutionConfig,
49 run_config: Option<adk_core::RunConfig>,
51 state: State,
52 step: usize,
53 pending_nodes: Vec<String>,
54 goto_parent: Option<Vec<String>>,
56 pending_deferred: HashMap<String, FanInTracker>,
58 deferred_start_times: HashMap<String, Instant>,
60 attempts: HashMap<String, u32>,
63 child_ledger: Arc<std::sync::Mutex<HashMap<String, serde_json::Value>>>,
66 cleared_interrupt: Option<String>,
71 #[cfg(feature = "node-cache")]
73 node_caches: HashMap<String, NodeCache>,
74}
75
76impl<'a> PregelExecutor<'a> {
77 pub fn new(graph: &'a CompiledGraph, config: ExecutionConfig) -> Self {
79 Self::new_with_run_config(graph, config, None)
80 }
81
82 pub(crate) fn new_with_run_config(
83 graph: &'a CompiledGraph,
84 config: ExecutionConfig,
85 run_config: Option<adk_core::RunConfig>,
86 ) -> Self {
87 #[cfg(feature = "node-cache")]
88 let node_caches = graph
89 .cache_policies
90 .iter()
91 .map(|(name, policy)| (name.clone(), NodeCache::from_policy(policy)))
92 .collect();
93
94 Self {
95 graph,
96 config,
97 run_config,
98 state: State::new(),
99 step: 0,
100 pending_nodes: vec![],
101 goto_parent: None,
102 pending_deferred: HashMap::new(),
103 deferred_start_times: HashMap::new(),
104 attempts: HashMap::new(),
105 child_ledger: Arc::new(std::sync::Mutex::new(HashMap::new())),
106 cleared_interrupt: None,
107 #[cfg(feature = "node-cache")]
108 node_caches,
109 }
110 }
111
112 async fn try_resume_from_checkpoint(&mut self, input: &State) -> Result<bool> {
121 let checkpoint = if let Some(checkpoint_id) = &self.config.resume_from {
122 if let Some(cp) = self.graph.checkpointer.as_ref() {
124 cp.load_by_id(checkpoint_id).await?
125 } else {
126 None
127 }
128 } else if let Some(cp) = self.graph.checkpointer.as_ref() {
129 cp.load(&self.config.thread_id).await?
131 } else {
132 None
133 };
134
135 if let Some(checkpoint) = checkpoint {
136 self.state = checkpoint.state;
138 self.pending_nodes = checkpoint.pending_nodes;
139 self.step = checkpoint.step;
140 self.cleared_interrupt = checkpoint.cleared_interrupt;
141 self.attempts = checkpoint.attempts;
142 *self.child_ledger.lock().expect("child ledger") = checkpoint.child_ledger;
143
144 for (key, value) in input {
146 self.graph.schema.apply_update(&mut self.state, key, value.clone());
147 }
148
149 Ok(true)
150 } else {
151 Ok(false)
152 }
153 }
154
155 pub async fn run(&mut self, input: State) -> Result<State> {
157 let resumed = self.try_resume_from_checkpoint(&input).await?;
159
160 if !resumed {
161 self.state = self.initialize_state(input).await?;
163 self.pending_nodes = self.graph.get_entry_nodes();
164 }
165
166 while !self.pending_nodes.is_empty() {
168 if self.step >= self.config.recursion_limit {
170 return Err(GraphError::RecursionLimitExceeded(self.step));
171 }
172
173 let result = match self.execute_super_step().await {
175 Ok(result) => result,
176 Err(error) => {
177 let any_retryable = self
182 .pending_nodes
183 .iter()
184 .any(|node| self.graph.retry_policy_for(node).is_some());
185 if any_retryable {
186 let _ = self.save_checkpoint().await;
187 }
188 return Err(error);
189 }
190 };
191
192 if let Some(interrupt) = result.interrupt {
194 if let Interrupt::Before(node) = &interrupt {
197 self.cleared_interrupt = Some(node.clone());
198 }
199 if matches!(interrupt, Interrupt::After(_)) {
203 let next = self.next_frontier(&result.executed_nodes, &result.goto)?;
204 self.pending_nodes =
205 self.filter_deferred_nodes(next, &result.executed_nodes)?;
206 } else if !matches!(interrupt, Interrupt::Before(_)) {
207 self.pending_nodes.retain(|node| !result.executed_nodes.contains(node));
213 }
214 let checkpoint_id = self.save_checkpoint().await?;
218 return Err(GraphError::Interrupted(Box::new(InterruptedExecution::new(
219 self.config.thread_id.clone(),
220 checkpoint_id,
221 interrupt,
222 self.state.clone(),
223 self.step,
224 ))));
225 }
226
227 if let Some(cleared) = &self.cleared_interrupt
230 && result.executed_nodes.iter().any(|n| n == cleared)
231 {
232 self.cleared_interrupt = None;
233 }
234
235 let next_candidates = self.next_frontier(&result.executed_nodes, &result.goto)?;
239 self.pending_nodes =
240 self.filter_deferred_nodes(next_candidates, &result.executed_nodes)?;
241 self.step += 1;
242
243 self.save_checkpoint().await?;
246
247 if self.pending_nodes.is_empty() {
248 break;
249 }
250 }
251
252 Ok(self.state.clone())
253 }
254
255 pub fn run_stream(
257 mut self,
258 input: State,
259 mode: StreamMode,
260 ) -> impl futures::Stream<Item = Result<StreamEvent>> + 'a {
261 async_stream::stream! {
262 let resumed = match self.try_resume_from_checkpoint(&input).await {
264 Ok(r) => r,
265 Err(e) => {
266 yield Err(e);
267 return;
268 }
269 };
270
271 if resumed {
272 yield Ok(StreamEvent::resumed(self.step, self.pending_nodes.clone()));
274 } else {
275 match self.initialize_state(input).await {
277 Ok(state) => self.state = state,
278 Err(e) => {
279 yield Err(e);
280 return;
281 }
282 }
283 self.pending_nodes = self.graph.get_entry_nodes();
284 }
285
286 if matches!(mode, StreamMode::Values) {
288 yield Ok(StreamEvent::state(self.state.clone(), self.step));
289 }
290
291 while !self.pending_nodes.is_empty() {
293 if self.step >= self.config.recursion_limit {
295 yield Err(GraphError::RecursionLimitExceeded(self.step));
296 return;
297 }
298
299 if matches!(mode, StreamMode::Debug | StreamMode::Custom | StreamMode::Messages) {
301 for node_name in &self.pending_nodes {
302 yield Ok(StreamEvent::node_start(node_name, self.step));
303 }
304 }
305
306 if matches!(mode, StreamMode::Messages) {
308 let mut result = SuperStepResult::default();
309
310 if let Some(interrupt) = self.gate_before(&self.pending_nodes) {
313 result.interrupt = Some(interrupt);
314 }
315
316 for node_name in &self.pending_nodes {
317 if result.interrupt.is_some() {
318 break;
319 }
320 if let Some(node) = self.graph.nodes.get(node_name) {
321 let mut ctx = NodeContext::new(self.state.clone(), self.config.clone(), self.step);
322 if let Some(run_config) = self.run_config.clone() {
323 ctx.set_run_config(run_config);
324 }
325 ctx.set_parent_schema(Arc::new(self.graph.schema.clone()));
326 ctx.set_child_invoker(Arc::new(crate::child::ChildInvoker::new(
327 self.graph.nodes.clone(),
328 Arc::clone(&self.child_ledger),
329 node_name.clone(),
330 )));
331
332 let policy = self.graph.timeout_policy_for(node_name).cloned();
334 if let Some(ref p) = policy
335 && p.idle_timeout.is_some() {
336 ctx.set_progress_handle(ProgressHandle::new());
337 }
338
339 let start = std::time::Instant::now();
340
341 let max_attempts = match policy.as_ref().map(|p| &p.on_timeout) {
345 Some(OnTimeout::Retry { max_attempts }) => (*max_attempts).max(1),
346 _ => 1,
347 };
348 let mut collected_events = Vec::new();
349 let mut streamed_updates = Vec::new();
350 let mut streamed_goto: Option<(String, Vec<String>)> = None;
351 let mut streamed_interrupt: Option<Interrupt> = None;
352 let mut timed_out_after;
353 let mut attempt = 0;
354
355 loop {
356 attempt += 1;
357 collected_events.clear();
358 streamed_updates.clear();
359 timed_out_after = None;
360 let attempt_start = std::time::Instant::now();
361 let mut node_stream = node.execute_stream(&ctx);
362 let mut failure = None;
363
364 loop {
365 let budget = policy
366 .as_ref()
367 .and_then(|p| item_timeout_budget(p, attempt_start.elapsed()));
368 let item = match budget {
369 Some(budget) => {
370 match tokio::time::timeout(budget, node_stream.next()).await {
371 Ok(item) => item,
372 Err(_) => {
373 timed_out_after = Some(attempt_start.elapsed());
374 break;
375 }
376 }
377 }
378 None => node_stream.next().await,
379 };
380
381 match item {
382 Some(Ok(event)) => {
383 if matches!(event, StreamEvent::Message { .. }) {
385 yield Ok(event.clone());
386 }
387 if let StreamEvent::Updates { ref updates, .. } = event {
391 streamed_updates.push(updates.clone());
392 }
393 if let StreamEvent::RouteDispatched {
395 ref source,
396 ref targets,
397 } = event
398 {
399 streamed_goto =
400 Some((source.clone(), targets.clone()));
401 }
402 if let StreamEvent::NodeInterrupt {
404 ref message,
405 ref data,
406 ..
407 } = event
408 {
409 streamed_interrupt =
410 Some(Interrupt::Dynamic {
411 message: message.clone(),
412 data: data.clone(),
413 });
414 }
415 collected_events.push(event);
416 }
417 Some(Err(e)) => {
418 failure = Some(e);
419 break;
420 }
421 None => break,
422 }
423 }
424 drop(node_stream);
425
426 if let Some(e) = failure {
427 yield Err(e);
428 return;
429 }
430 if timed_out_after.is_none() || attempt >= max_attempts {
431 break;
432 }
433 }
434
435 if let Some(elapsed) = timed_out_after {
436 let on_timeout =
437 policy.as_ref().map(|p| p.on_timeout.clone()).unwrap_or_default();
438 match on_timeout {
439 OnTimeout::Skip => {
440 tracing::warn!(
441 node = %node_name,
442 elapsed = ?elapsed,
443 "node timed out while streaming, skipping"
444 );
445 streamed_updates.clear();
446 }
447 OnTimeout::Fail | OnTimeout::Retry { .. } => {
448 yield Err(GraphError::NodeTimedOut {
449 node: node_name.clone(),
450 elapsed,
451 });
452 return;
453 }
454 }
455 }
456
457 let duration_ms = start.elapsed().as_millis() as u64;
458 result.events.push(StreamEvent::node_end(node_name, self.step, duration_ms));
459 result.events.extend(collected_events);
460
461 let node_interrupt = streamed_interrupt;
462 if let Some(interrupt) = node_interrupt {
463 result.interrupt = Some(interrupt);
464 } else {
465 result.executed_nodes.push(node_name.clone());
466 if let Some((source, targets)) = streamed_goto {
467 result.goto.insert(source, targets);
468 }
469 for updates in streamed_updates {
470 self.ensure_channels_declared(
471 node_name,
472 updates.keys().map(String::as_str),
473 )?;
474 for (key, value) in updates {
475 self.graph.schema.apply_update(&mut self.state, &key, value);
476 }
477 }
478 }
479 }
480 }
481
482 for event in &result.events {
484 if matches!(event, StreamEvent::NodeEnd { .. }) {
485 yield Ok(event.clone());
486 }
487 }
488
489 if result.interrupt.is_none()
492 && let Some(interrupt) = self.gate_after(&result.executed_nodes)
493 {
494 result.interrupt = Some(interrupt);
495 }
496
497 if let Some(interrupt) = result.interrupt {
500 if let Interrupt::Before(node) = &interrupt {
501 self.cleared_interrupt = Some(node.clone());
502 }
503 if matches!(interrupt, Interrupt::After(_)) {
506 let next =
507 self.next_frontier(&result.executed_nodes, &result.goto)?;
508 match self.filter_deferred_nodes(next, &result.executed_nodes) {
509 Ok(frontier) => self.pending_nodes = frontier,
510 Err(error) => {
511 yield Err(error);
512 return;
513 }
514 }
515 } else if !matches!(interrupt, Interrupt::Before(_)) {
516 self.pending_nodes
517 .retain(|node| !result.executed_nodes.contains(node));
518 }
519 let checkpoint_id = match self.save_checkpoint().await {
522 Ok(checkpoint_id) => checkpoint_id,
523 Err(error) => {
524 yield Err(error);
525 return;
526 }
527 };
528 if let Some(pause) = GraphToolConfirmationPause::from_interrupted_execution(
529 &InterruptedExecution::new(
530 self.config.thread_id.clone(),
531 checkpoint_id,
532 interrupt.clone(),
533 self.state.clone(),
534 self.step,
535 ),
536 ) {
537 yield Ok(StreamEvent::custom(
538 &pause.node,
539 GraphToolConfirmationPause::KIND,
540 serde_json::to_value(&pause).unwrap_or(serde_json::Value::Null),
541 ));
542 }
543 yield Ok(StreamEvent::interrupted(
544 result.executed_nodes.first().map(|s| s.as_str()).unwrap_or("unknown"),
545 &interrupt.to_string(),
546 ));
547 return;
548 }
549
550 if let Some(cleared) = &self.cleared_interrupt
552 && result.executed_nodes.iter().any(|n| n == cleared)
553 {
554 self.cleared_interrupt = None;
555 }
556
557 self.pending_nodes = {
558 let next_candidates = self.next_frontier(&result.executed_nodes, &result.goto)?;
559 match self.filter_deferred_nodes(next_candidates, &result.executed_nodes) {
560 Ok(nodes) => nodes,
561 Err(e) => {
562 yield Err(e);
563 return;
564 }
565 }
566 };
567 self.step += 1;
568
569 if let Err(e) = self.save_checkpoint().await {
573 yield Err(e);
574 return;
575 }
576 continue;
577 }
578
579 let result = match self.execute_super_step().await {
581 Ok(r) => r,
582 Err(e) => {
583 yield Err(e);
584 return;
585 }
586 };
587
588 for event in &result.events {
590 match (&mode, &event) {
591 (StreamMode::Custom | StreamMode::Debug, StreamEvent::NodeStart { .. }) => {}
593 (StreamMode::Custom, _) => yield Ok(event.clone()),
594 (StreamMode::Debug, _) => yield Ok(event.clone()),
595 _ => {}
596 }
597 }
598
599 match mode {
601 StreamMode::Values => {
602 yield Ok(StreamEvent::state(self.state.clone(), self.step));
603 }
604 StreamMode::Updates => {
605 yield Ok(StreamEvent::step_complete(
606 self.step,
607 result.executed_nodes.clone(),
608 ));
609 }
610 _ => {}
611 }
612
613 if let Some(interrupt) = result.interrupt {
615 if let Interrupt::Before(node) = &interrupt {
617 self.cleared_interrupt = Some(node.clone());
618 }
619 if matches!(interrupt, Interrupt::After(_)) {
621 let next =
622 self.next_frontier(&result.executed_nodes, &result.goto)?;
623 match self.filter_deferred_nodes(next, &result.executed_nodes) {
624 Ok(frontier) => self.pending_nodes = frontier,
625 Err(error) => {
626 yield Err(error);
627 return;
628 }
629 }
630 } else if !matches!(interrupt, Interrupt::Before(_)) {
631 self.pending_nodes
632 .retain(|node| !result.executed_nodes.contains(node));
633 }
634 let checkpoint_id = match self.save_checkpoint().await {
639 Ok(checkpoint_id) => checkpoint_id,
640 Err(error) => {
641 yield Err(error);
642 return;
643 }
644 };
645 if let Some(pause) = GraphToolConfirmationPause::from_interrupted_execution(
646 &InterruptedExecution::new(
647 self.config.thread_id.clone(),
648 checkpoint_id,
649 interrupt.clone(),
650 self.state.clone(),
651 self.step,
652 ),
653 ) {
654 yield Ok(StreamEvent::custom(
655 &pause.node,
656 GraphToolConfirmationPause::KIND,
657 serde_json::to_value(&pause).unwrap_or(serde_json::Value::Null),
658 ));
659 }
660 yield Ok(StreamEvent::interrupted(
661 result.executed_nodes.first().map(|s| s.as_str()).unwrap_or("unknown"),
662 &interrupt.to_string(),
663 ));
664 return;
665 }
666
667 if let Some(cleared) = &self.cleared_interrupt
669 && result.executed_nodes.iter().any(|n| n == cleared)
670 {
671 self.cleared_interrupt = None;
672 }
673
674 if matches!(mode, StreamMode::Debug) {
680 match self.graph.route_dispatches(&result.executed_nodes, &self.state) {
681 Ok(dispatches) => {
682 for (source, targets) in dispatches {
683 yield Ok(StreamEvent::route_dispatched(&source, targets));
684 }
685 }
686 Err(error) => {
687 yield Err(error);
688 return;
689 }
690 }
691 }
692
693 self.pending_nodes = {
694 let next_candidates = self.next_frontier(&result.executed_nodes, &result.goto)?;
695 match self.filter_deferred_nodes(next_candidates, &result.executed_nodes) {
696 Ok(nodes) => nodes,
697 Err(e) => {
698 yield Err(e);
699 return;
700 }
701 }
702 };
703 self.step += 1;
704
705 if let Err(e) = self.save_checkpoint().await {
706 yield Err(e);
707 return;
708 }
709 }
710
711 yield Ok(StreamEvent::done(self.state.clone(), self.step + 1));
712 }
713 }
714
715 fn filter_deferred_nodes(
728 &mut self,
729 candidates: Vec<String>,
730 executed_nodes: &[String],
731 ) -> Result<Vec<String>> {
732 let mut ready_nodes = Vec::new();
733
734 for candidate in candidates {
735 if let Some(config) = self.graph.deferred_configs.get(&candidate) {
736 let upstream = self.graph.get_upstream_nodes(&candidate);
738
739 let tracker = self.pending_deferred.entry(candidate.clone()).or_insert_with(|| {
741 let sources: Vec<&str> = upstream.iter().map(|s| s.as_str()).collect();
742 FanInTracker::new(sources)
743 });
744
745 self.deferred_start_times.entry(candidate.clone()).or_insert_with(Instant::now);
747
748 for executed in executed_nodes {
750 if upstream.contains(executed) {
751 let output = self.state.get(executed).cloned().unwrap_or_else(|| {
754 serde_json::Value::Object(
756 self.state.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
757 )
758 });
759 tracker.record(executed, output);
760 }
761 }
762
763 if tracker.is_ready() {
764 let merged = tracker.merge(&config.merge_strategy);
766 let fan_in_key = format!("{candidate}_fan_in");
767 self.graph.schema.apply_update(&mut self.state, &fan_in_key, merged);
768
769 self.pending_deferred.remove(&candidate);
771 self.deferred_start_times.remove(&candidate);
772 ready_nodes.push(candidate);
773 } else if let Some(timeout_duration) = config.fan_in_timeout {
774 let start_time = self.deferred_start_times[&candidate];
776 if start_time.elapsed() >= timeout_duration {
777 let received = tracker.received_count();
778 let expected = tracker.expected_count();
779
780 let required = config.min_predecessors.unwrap_or(1).max(1);
783 if received >= required {
784 tracing::warn!(
786 node = %candidate,
787 received,
788 expected,
789 "fan-in timeout expired, proceeding with partial results"
790 );
791 let merged = tracker.merge(&config.merge_strategy);
792 let fan_in_key = format!("{candidate}_fan_in");
793 self.graph.schema.apply_update(&mut self.state, &fan_in_key, merged);
794
795 self.pending_deferred.remove(&candidate);
797 self.deferred_start_times.remove(&candidate);
798 ready_nodes.push(candidate);
799 } else {
800 self.pending_deferred.remove(&candidate);
802 self.deferred_start_times.remove(&candidate);
803 return Err(GraphError::FanInTimedOut {
804 node: candidate,
805 received,
806 expected,
807 });
808 }
809 }
810 }
811 } else {
814 ready_nodes.push(candidate);
816 }
817 }
818
819 Ok(ready_nodes)
820 }
821
822 async fn initialize_state(&self, input: State) -> Result<State> {
824 let mut state = self.graph.schema.initialize_state();
826
827 if let Some(checkpoint_id) = &self.config.resume_from {
829 if let Some(cp) = self.graph.checkpointer.as_ref()
830 && let Some(checkpoint) = cp.load_by_id(checkpoint_id).await?
831 {
832 state = checkpoint.state;
833 }
834 } else if let Some(cp) = self.graph.checkpointer.as_ref() {
835 if let Some(checkpoint) = cp.load(&self.config.thread_id).await? {
837 state = checkpoint.state;
838 }
839 }
840
841 for (key, value) in input {
843 self.graph.schema.apply_update(&mut state, &key, value);
844 }
845
846 Ok(state)
847 }
848
849 async fn execute_super_step(&mut self) -> Result<SuperStepResult> {
851 let mut result = SuperStepResult::default();
852
853 if let Some(interrupt) = self.gate_before(&self.pending_nodes) {
854 return Ok(SuperStepResult { interrupt: Some(interrupt), ..Default::default() });
855 }
856
857 #[cfg(feature = "node-cache")]
859 let mut cached_results: HashMap<String, serde_json::Value> = HashMap::new();
860 #[cfg(feature = "node-cache")]
861 let mut nodes_to_execute: Vec<String> = Vec::new();
862
863 #[cfg(feature = "node-cache")]
864 {
865 for node_name in &self.pending_nodes {
866 if let Some(cache) = self.node_caches.get(node_name) {
867 let cache_key = compute_cache_key(node_name, &self.state);
868 let cached_value = cache.get(&cache_key).await;
869 tracing::debug!(
870 node = %node_name,
871 cache_hit = cached_value.is_some(),
872 cache_key = %cache_key,
873 "node cache lookup"
874 );
875 if let Some(value) = cached_value {
876 cached_results.insert(node_name.clone(), value);
878 } else {
879 nodes_to_execute.push(node_name.clone());
881 }
882 } else {
883 nodes_to_execute.push(node_name.clone());
885 }
886 }
887 }
888
889 #[cfg(feature = "node-cache")]
891 {
892 for (node_name, cached_value) in &cached_results {
893 result.executed_nodes.push(node_name.clone());
894 result.events.push(StreamEvent::node_end(node_name, self.step, 0));
895
896 if let Some(updates_map) = cached_value.as_object() {
898 self.ensure_channels_declared(
899 node_name,
900 updates_map.keys().map(String::as_str),
901 )?;
902 for (key, value) in updates_map {
903 self.graph.schema.apply_update(&mut self.state, key, value.clone());
904 }
905 }
906 }
907 }
908
909 #[cfg(feature = "node-cache")]
914 let pending_for_execution = {
915 nodes_to_execute.sort();
916 &nodes_to_execute
917 };
918 #[cfg(not(feature = "node-cache"))]
919 let pending_for_execution = {
920 self.pending_nodes.sort();
921 &self.pending_nodes
922 };
923
924 let nodes: Vec<_> = pending_for_execution
926 .iter()
927 .filter_map(|name| self.graph.nodes.get(name).map(|n| (name.clone(), n.clone())))
928 .collect();
929
930 let timeout_policies: Vec<_> =
932 nodes.iter().map(|(name, _)| self.graph.timeout_policy_for(name).cloned()).collect();
933 let retry_policies: Vec<_> =
934 nodes.iter().map(|(name, _)| self.graph.retry_policy_for(name).cloned()).collect();
935 let prior_attempts: Vec<u32> =
938 nodes.iter().map(|(name, _)| self.attempts.get(name).copied().unwrap_or(0)).collect();
939
940 let futures: Vec<_> = nodes
941 .into_iter()
942 .zip(timeout_policies)
943 .zip(retry_policies)
944 .zip(prior_attempts)
945 .map(|((((name, node), policy), retry), spent)| {
946 let mut ctx = NodeContext::new(self.state.clone(), self.config.clone(), self.step);
947 if let Some(run_config) = self.run_config.clone() {
948 ctx.set_run_config(run_config);
949 }
950 ctx.set_parent_schema(Arc::new(self.graph.schema.clone()));
957 ctx.set_child_invoker(Arc::new(crate::child::ChildInvoker::new(
958 self.graph.nodes.clone(),
959 Arc::clone(&self.child_ledger),
960 name.clone(),
961 )));
962
963 if let Some(ref p) = policy
965 && p.idle_timeout.is_some()
966 {
967 ctx.set_progress_handle(ProgressHandle::new());
968 }
969
970 let step = self.step;
971 async move {
972 let start = Instant::now();
973 let mut attempts = spent;
974 let output = loop {
975 let result = match policy {
976 Some(ref timeout_policy) => {
977 execute_with_timeout(node.as_ref(), &ctx, timeout_policy).await
978 }
979 None => node.execute(&ctx).await,
980 };
981 attempts += 1;
982
983 let Err(ref error) = result else { break result };
984 let Some(ref retry) = retry else { break result };
985 if !retry.allows_another_attempt(attempts)
986 || !retry.retry_on.should_retry(error)
987 {
988 break result;
989 }
990
991 let delay = retry.delay_for_attempt(attempts);
992 tracing::warn!(
993 node = %name,
994 attempt = attempts,
995 max_attempts = retry.max_attempts,
996 delay_ms = delay.as_millis(),
997 error = %error,
998 "node failed, retrying after backoff"
999 );
1000 tokio::time::sleep(delay).await;
1001 };
1002 let duration_ms = start.elapsed().as_millis() as u64;
1003 (name, output, duration_ms, step, attempts)
1004 }
1005 })
1006 .collect();
1007
1008 let concurrency = self
1012 .graph
1013 .max_concurrency
1014 .map_or(pending_for_execution.len(), |limit| limit.min(pending_for_execution.len()))
1015 .max(1);
1016 let mut outputs: Vec<_> =
1017 stream::iter(futures).buffer_unordered(concurrency).collect().await;
1018 outputs.sort_by(|left, right| left.0.cmp(&right.0));
1022
1023 let mut all_updates = Vec::new();
1025 let mut interrupt = None;
1026
1027 for (node_name, output_result, duration_ms, step, attempts) in outputs {
1028 if output_result.is_err() {
1032 self.attempts.insert(node_name.clone(), attempts);
1033 } else {
1034 self.attempts.remove(&node_name);
1035 }
1036 result.events.push(StreamEvent::node_end(&node_name, step, duration_ms));
1037
1038 match output_result {
1039 Ok(output) => {
1040 if let Some(node_interrupt) = output.interrupt {
1042 if interrupt.is_none() {
1043 interrupt = Some(node_interrupt);
1044 }
1045 continue;
1046 }
1047 result.executed_nodes.push(node_name.clone());
1048
1049 result.events.extend(output.events);
1051
1052 #[cfg(feature = "node-cache")]
1054 {
1055 if let Some(cache) = self.node_caches.get(&node_name) {
1056 let cache_key = compute_cache_key(&node_name, &self.state);
1057 let updates_value = serde_json::to_value(&output.updates)
1058 .unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
1059 let ttl = self.graph.cache_policies.get(&node_name).and_then(|p| p.ttl);
1060 cache.set(&cache_key, updates_value, ttl).await;
1061 }
1062 }
1063
1064 if let Some(targets) = output.goto {
1066 result.goto.insert(node_name.clone(), targets);
1067 }
1068 if let Some(targets) = output.goto_parent {
1071 self.goto_parent = Some(targets);
1072 }
1073
1074 all_updates.push((node_name.clone(), output.updates));
1077 }
1078 Err(e) => {
1079 match self.graph.error_handler_for(&node_name) {
1083 Some(handler) if !matches!(e, GraphError::Interrupted(_)) => {
1084 let recovery = handler(&node_name, &e, &self.state)?;
1085 if let Some(targets) = recovery.goto {
1086 result.goto.insert(node_name.clone(), targets);
1087 }
1088 result.executed_nodes.push(node_name.clone());
1089 all_updates.push((node_name, recovery.updates));
1090 }
1091 _ => {
1092 return Err(GraphError::NodeExecutionFailed {
1093 node: node_name,
1094 message: e.to_string(),
1095 });
1096 }
1097 }
1098 }
1099 }
1100 }
1101
1102 all_updates.sort_by(|(left, _), (right, _)| left.cmp(right));
1112 for (node, updates) in all_updates {
1113 let mut keys: Vec<_> = updates.keys().cloned().collect();
1114 keys.sort();
1115 self.ensure_channels_declared(&node, keys.iter().map(String::as_str))?;
1116 for key in keys {
1117 if let Some(value) = updates.get(&key) {
1118 self.graph.schema.apply_update(&mut self.state, &key, value.clone());
1119 }
1120 }
1121 }
1122
1123 if let Some(interrupt) = interrupt {
1124 return Ok(SuperStepResult { interrupt: Some(interrupt), ..result });
1125 }
1126
1127 if let Some(interrupt) = self.gate_after(&result.executed_nodes) {
1128 return Ok(SuperStepResult { interrupt: Some(interrupt), ..result });
1129 }
1130
1131 Ok(result)
1132 }
1133
1134 fn gate_before(&self, pending: &[String]) -> Option<Interrupt> {
1145 pending
1146 .iter()
1147 .find(|node| {
1148 self.graph.interrupt_before.contains(*node)
1149 && self.cleared_interrupt.as_deref() != Some(node.as_str())
1150 })
1151 .map(|node| Interrupt::Before(node.clone()))
1152 }
1153
1154 fn gate_after(&self, executed: &[String]) -> Option<Interrupt> {
1156 executed
1157 .iter()
1158 .find(|node| self.graph.interrupt_after.contains(*node))
1159 .map(|node| Interrupt::After(node.clone()))
1160 }
1161
1162 fn next_frontier(
1173 &self,
1174 executed: &[String],
1175 goto: &HashMap<String, Vec<String>>,
1176 ) -> Result<Vec<String>> {
1177 let followed_edges: Vec<String> =
1179 executed.iter().filter(|node| !goto.contains_key(*node)).cloned().collect();
1180 let mut next = self.graph.get_next_nodes(&followed_edges, &self.state)?;
1181
1182 let mut routed: Vec<(&String, &Vec<String>)> = goto.iter().collect();
1184 routed.sort_by_key(|(node, _)| node.as_str());
1185
1186 for (node, targets) in routed {
1187 for target in targets {
1188 if target == crate::edge::END {
1189 continue;
1190 }
1191 if self.graph.node(target).is_none() {
1192 return Err(GraphError::UnknownRouteTarget(format!(
1193 "node '{node}' routed to '{target}', which is not a node in this graph"
1194 )));
1195 }
1196 if !next.contains(target) {
1197 next.push(target.clone());
1198 }
1199 }
1200 }
1201 Ok(next)
1202 }
1203
1204 fn ensure_channels_declared<'k>(
1209 &self,
1210 node: &str,
1211 keys: impl IntoIterator<Item = &'k str>,
1212 ) -> Result<()> {
1213 if !self.graph.strict_channels {
1214 return Ok(());
1215 }
1216 match self.graph.schema.first_undeclared(keys) {
1217 Some(channel) => Err(GraphError::UndeclaredChannel {
1218 node: node.to_string(),
1219 channel: channel.to_string(),
1220 }),
1221 None => Ok(()),
1222 }
1223 }
1224
1225 async fn save_checkpoint(&self) -> Result<String> {
1226 if let Some(cp) = &self.graph.checkpointer {
1227 let mut checkpoint = Checkpoint::new(
1228 &self.config.thread_id,
1229 self.state.clone(),
1230 self.step,
1231 self.pending_nodes.clone(),
1232 );
1233 checkpoint.cleared_interrupt = self.cleared_interrupt.clone();
1234 checkpoint.attempts = self.attempts.clone();
1235 checkpoint.child_ledger = self.child_ledger.lock().expect("child ledger").clone();
1236 let id = cp.save(&checkpoint).await?;
1237
1238 if let Some(policy) = &self.graph.retention {
1241 let removed = cp.prune(&self.config.thread_id, policy).await?;
1242 if removed > 0 {
1243 tracing::debug!(
1244 thread_id = %self.config.thread_id,
1245 removed,
1246 "pruned old checkpoints"
1247 );
1248 }
1249 }
1250 return Ok(id);
1251 }
1252 Ok(String::new())
1253 }
1254}
1255
1256impl CompiledGraph {
1258 pub async fn invoke(&self, input: State, config: ExecutionConfig) -> Result<State> {
1260 self.invoke_detailed(input, config).await.map(|outcome| outcome.state)
1261 }
1262
1263 pub async fn invoke_with_run_config(
1269 &self,
1270 input: State,
1271 config: ExecutionConfig,
1272 run_config: adk_core::RunConfig,
1273 ) -> Result<State> {
1274 let mut executor = PregelExecutor::new_with_run_config(self, config, Some(run_config));
1275 executor.run(input).await
1276 }
1277
1278 pub async fn invoke_detailed(
1284 &self,
1285 input: State,
1286 config: ExecutionConfig,
1287 ) -> Result<GraphOutcome> {
1288 let mut executor = PregelExecutor::new(self, config);
1289 let state = executor.run(input).await?;
1290 Ok(GraphOutcome { state, goto_parent: executor.goto_parent })
1291 }
1292
1293 pub async fn invoke_detailed_with_run_config(
1295 &self,
1296 input: State,
1297 config: ExecutionConfig,
1298 run_config: adk_core::RunConfig,
1299 ) -> Result<GraphOutcome> {
1300 let mut executor = PregelExecutor::new_with_run_config(self, config, Some(run_config));
1301 let state = executor.run(input).await?;
1302 Ok(GraphOutcome { state, goto_parent: executor.goto_parent })
1303 }
1304
1305 pub fn stream(
1307 &self,
1308 input: State,
1309 config: ExecutionConfig,
1310 mode: StreamMode,
1311 ) -> impl futures::Stream<Item = Result<StreamEvent>> + '_ {
1312 tracing::debug!("CompiledGraph::stream called with mode {:?}", mode);
1313 let executor = PregelExecutor::new(self, config);
1314 executor.run_stream(input, mode)
1315 }
1316
1317 pub fn stream_with_run_config(
1322 &self,
1323 input: State,
1324 config: ExecutionConfig,
1325 mode: StreamMode,
1326 run_config: adk_core::RunConfig,
1327 ) -> impl futures::Stream<Item = Result<StreamEvent>> + '_ {
1328 tracing::debug!("CompiledGraph::stream_with_run_config called with mode {:?}", mode);
1329 let executor = PregelExecutor::new_with_run_config(self, config, Some(run_config));
1330 executor.run_stream(input, mode)
1331 }
1332
1333 pub async fn get_state(&self, thread_id: &str) -> Result<Option<State>> {
1335 if let Some(cp) = &self.checkpointer {
1336 Ok(cp.load(thread_id).await?.map(|c| c.state))
1337 } else {
1338 Ok(None)
1339 }
1340 }
1341
1342 pub async fn update_state(
1344 &self,
1345 thread_id: &str,
1346 updates: impl IntoIterator<Item = (String, serde_json::Value)>,
1347 ) -> Result<()> {
1348 if let Some(cp) = &self.checkpointer
1349 && let Some(checkpoint) = cp.load(thread_id).await?
1350 {
1351 let mut state = checkpoint.state;
1352 for (key, value) in updates {
1353 self.schema.apply_update(&mut state, &key, value);
1354 }
1355 let new_checkpoint =
1356 Checkpoint::new(thread_id, state, checkpoint.step, checkpoint.pending_nodes);
1357 cp.save(&new_checkpoint).await?;
1358 }
1359 Ok(())
1360 }
1361}
1362
1363#[cfg(test)]
1364mod tests {
1365 use super::*;
1366 use crate::edge::{END, START};
1367 use crate::graph::StateGraph;
1368 use crate::node::NodeOutput;
1369 use serde_json::json;
1370
1371 #[tokio::test]
1372 async fn test_simple_execution() {
1373 let graph = StateGraph::with_channels(&["value"])
1374 .add_node_fn("set_value", |_ctx| async {
1375 Ok(NodeOutput::new().with_update("value", json!(42)))
1376 })
1377 .add_edge(START, "set_value")
1378 .add_edge("set_value", END)
1379 .compile()
1380 .unwrap();
1381
1382 let result = graph.invoke(State::new(), ExecutionConfig::new("test")).await.unwrap();
1383
1384 assert_eq!(result.get("value"), Some(&json!(42)));
1385 }
1386
1387 #[tokio::test]
1388 async fn test_sequential_execution() {
1389 let graph = StateGraph::with_channels(&["value"])
1390 .add_node_fn("step1", |_ctx| async {
1391 Ok(NodeOutput::new().with_update("value", json!(1)))
1392 })
1393 .add_node_fn("step2", |ctx| async move {
1394 let current = ctx.get("value").and_then(|v| v.as_i64()).unwrap_or(0);
1395 Ok(NodeOutput::new().with_update("value", json!(current + 10)))
1396 })
1397 .add_edge(START, "step1")
1398 .add_edge("step1", "step2")
1399 .add_edge("step2", END)
1400 .compile()
1401 .unwrap();
1402
1403 let result = graph.invoke(State::new(), ExecutionConfig::new("test")).await.unwrap();
1404
1405 assert_eq!(result.get("value"), Some(&json!(11)));
1406 }
1407
1408 #[tokio::test]
1409 async fn test_conditional_routing() {
1410 let graph = StateGraph::with_channels(&["path", "result"])
1411 .add_node_fn("router", |ctx| async move {
1412 let path = ctx.get("path").and_then(|v| v.as_str()).unwrap_or("a");
1413 Ok(NodeOutput::new().with_update("route", json!(path)))
1414 })
1415 .add_node_fn("path_a", |_ctx| async {
1416 Ok(NodeOutput::new().with_update("result", json!("went to A")))
1417 })
1418 .add_node_fn("path_b", |_ctx| async {
1419 Ok(NodeOutput::new().with_update("result", json!("went to B")))
1420 })
1421 .add_edge(START, "router")
1422 .add_conditional_edges(
1423 "router",
1424 |state| state.get("route").and_then(|v| v.as_str()).unwrap_or(END).to_string(),
1425 [("a", "path_a"), ("b", "path_b"), (END, END)],
1426 )
1427 .add_edge("path_a", END)
1428 .add_edge("path_b", END)
1429 .compile()
1430 .unwrap();
1431
1432 let mut input = State::new();
1434 input.insert("path".to_string(), json!("a"));
1435 let result = graph.invoke(input, ExecutionConfig::new("test")).await.unwrap();
1436 assert_eq!(result.get("result"), Some(&json!("went to A")));
1437
1438 let mut input = State::new();
1440 input.insert("path".to_string(), json!("b"));
1441 let result = graph.invoke(input, ExecutionConfig::new("test")).await.unwrap();
1442 assert_eq!(result.get("result"), Some(&json!("went to B")));
1443 }
1444
1445 #[tokio::test]
1446 async fn test_cycle_with_limit() {
1447 let graph = StateGraph::with_channels(&["count"])
1448 .add_node_fn("increment", |ctx| async move {
1449 let count = ctx.get("count").and_then(|v| v.as_i64()).unwrap_or(0);
1450 Ok(NodeOutput::new().with_update("count", json!(count + 1)))
1451 })
1452 .add_edge(START, "increment")
1453 .add_conditional_edges(
1454 "increment",
1455 |state| {
1456 let count = state.get("count").and_then(|v| v.as_i64()).unwrap_or(0);
1457 if count < 5 { "increment".to_string() } else { END.to_string() }
1458 },
1459 [("increment", "increment"), (END, END)],
1460 )
1461 .compile()
1462 .unwrap();
1463
1464 let result = graph.invoke(State::new(), ExecutionConfig::new("test")).await.unwrap();
1465
1466 assert_eq!(result.get("count"), Some(&json!(5)));
1467 }
1468
1469 #[tokio::test]
1470 async fn test_recursion_limit() {
1471 let graph = StateGraph::with_channels(&["count"])
1472 .add_node_fn("loop", |ctx| async move {
1473 let count = ctx.get("count").and_then(|v| v.as_i64()).unwrap_or(0);
1474 Ok(NodeOutput::new().with_update("count", json!(count + 1)))
1475 })
1476 .add_edge(START, "loop")
1477 .add_edge("loop", "loop") .compile()
1479 .unwrap()
1480 .with_recursion_limit(10);
1481
1482 let result = graph.invoke(State::new(), ExecutionConfig::new("test")).await;
1483
1484 assert!(
1486 matches!(result, Err(GraphError::RecursionLimitExceeded(_))),
1487 "Expected RecursionLimitExceeded error, got: {:?}",
1488 result
1489 );
1490 }
1491}