1#![allow(deprecated)] use crate::checkpoint::Checkpoint;
4use crate::checkpoint_store::{CheckpointStore, RunStatus, RunSummary};
5use crate::command::{Navigation, NodeOutput};
6use crate::config::GraphConfig;
7use crate::edge::EdgeType;
8use crate::error::{AgentGraphError, CheckpointStoreOperation, Result};
9use crate::event_sink::{EventSink, GraphEvent, NodeOutcomeKind};
10use crate::graph::{AgentGraph, END, START};
11use crate::interrupt::{ExecutionResult, InterruptCheckpoint};
12use crate::retry::RetryPolicy;
13use crate::router::RouterOutput;
14use crate::state::AgentState;
15use crate::stream::StreamEvent;
16use serde_json::Value;
17use std::collections::{HashMap, HashSet};
18use std::future::Future;
19use std::sync::atomic::{AtomicBool, Ordering};
20use std::sync::Arc;
21use tokio::sync::mpsc;
22
23fn checkpoint_store_error(
24 operation: CheckpointStoreOperation,
25 error: AgentGraphError,
26) -> AgentGraphError {
27 AgentGraphError::CheckpointStore {
28 operation,
29 message: error.to_string(),
30 }
31}
32
33impl AgentGraph {
35 fn unstarted_run_summary(&self, trace_ctx: stack_ids::TraceCtx) -> RunSummary {
36 let now = chrono::Utc::now();
37 RunSummary {
38 run_id: String::new(),
40 graph_name: self
41 .graph_name
42 .clone()
43 .unwrap_or_else(|| "unnamed".to_string()),
44 status: RunStatus::Failed,
45 total_nodes_executed: 0,
46 total_attempts: 0,
47 failed_attempts: 0,
48 trace_id: Some(trace_ctx.to_legacy_trace_id().to_string()),
49 trace_ctx: Some(trace_ctx),
50 started_at: now,
51 finished_at: Some(now),
52 }
53 }
54
55 pub async fn execute(&self, start_node: &str, state: AgentState) -> Result<AgentState> {
57 self.execute_with_config(start_node, state, GraphConfig::default())
58 .await
59 }
60
61 pub async fn execute_with_summary(
63 &self,
64 start_node: &str,
65 state: AgentState,
66 config: GraphConfig,
67 ) -> (Result<AgentState>, RunSummary) {
68 self.register_reducers_on_state(&state).await;
69 let trace_ctx = config.resolve_trace_ctx();
70 let run_id = match self.create_run_id().await {
71 Ok(run_id) => run_id,
72 Err(error) => return (Err(error), self.unstarted_run_summary(trace_ctx)),
73 };
74 let event_sink = self.resolve_event_sink(None);
75 let cancel = Arc::new(AtomicBool::new(false));
76
77 let executor = GraphExecutor {
78 graph: self,
79 state,
80 config,
81 iteration: 0,
82 event_sink,
83 run_id,
84 trace_ctx,
85 cancel_flag: cancel,
86 started_at: chrono::Utc::now(),
87 total_attempts: 0,
88 failed_attempts: 0,
89 executed_nodes: HashSet::new(),
90 };
91
92 executor.execute(start_node).await
93 }
94
95 pub async fn execute_with_config(
97 &self,
98 start_node: &str,
99 state: AgentState,
100 config: GraphConfig,
101 ) -> Result<AgentState> {
102 let (result, _summary) = self.execute_with_summary(start_node, state, config).await;
103 result
104 }
105
106 pub async fn execute_with_interrupt(
108 &self,
109 start_node: &str,
110 state: AgentState,
111 config: GraphConfig,
112 ) -> ExecutionResult {
113 self.register_reducers_on_state(&state).await;
114 let state_clone = state.clone();
115 let trace_ctx = config.resolve_trace_ctx();
116 let run_id = match self.create_run_id().await {
117 Ok(run_id) => run_id,
118 Err(error) => {
119 return ExecutionResult::Failed {
120 error,
121 state: state_clone,
122 }
123 }
124 };
125 let event_sink = self.resolve_event_sink(None);
126 let cancel = Arc::new(AtomicBool::new(false));
127
128 let executor = GraphExecutor {
129 graph: self,
130 state,
131 config,
132 iteration: 0,
133 event_sink,
134 run_id,
135 trace_ctx,
136 cancel_flag: cancel,
137 started_at: chrono::Utc::now(),
138 total_attempts: 0,
139 failed_attempts: 0,
140 executed_nodes: HashSet::new(),
141 };
142
143 let (result, _summary) = executor.execute(start_node).await;
144 match result {
145 Ok(final_state) => ExecutionResult::Complete(final_state),
146 Err(AgentGraphError::InterruptError {
147 ref node,
148 ref value,
149 }) => ExecutionResult::Interrupted {
150 state: state_clone,
151 node: node.clone(),
152 interrupt_value: value.clone(),
153 checkpoint_data: Some(InterruptCheckpoint {
154 resume_node: node.clone(),
155 resume_before: false,
156 iteration: 0,
157 active_nodes: Vec::new(),
158 graph_hash: Some(self.compute_graph_hash()),
159 }),
160 },
161 Err(e) => ExecutionResult::Failed {
165 error: e,
166 state: state_clone,
167 },
168 }
169 }
170
171 pub fn execute_cancellable(
173 self: Arc<Self>,
174 start_node: &str,
175 state: AgentState,
176 config: GraphConfig,
177 ) -> (tokio::task::JoinHandle<Result<AgentState>>, Arc<AtomicBool>) {
178 let cancel = Arc::new(AtomicBool::new(false));
179 let cancel_clone = cancel.clone();
180 let start = start_node.to_string();
181 let graph = self;
182
183 let handle = tokio::spawn(async move {
184 graph.register_reducers_on_state(&state).await;
185 let trace_ctx = config.resolve_trace_ctx();
186 let run_id = graph.create_run_id().await?;
187 let event_sink = graph.resolve_event_sink(None);
188
189 let executor = GraphExecutor {
190 graph: &graph,
191 state,
192 config,
193 iteration: 0,
194 event_sink,
195 run_id,
196 trace_ctx,
197 cancel_flag: cancel_clone,
198 started_at: chrono::Utc::now(),
199 total_attempts: 0,
200 failed_attempts: 0,
201 executed_nodes: HashSet::new(),
202 };
203
204 let (result, _summary) = executor.execute(&start).await;
205 result
206 });
207
208 (handle, cancel)
209 }
210
211 pub fn stream(
214 self: Arc<Self>,
215 start_node: &str,
216 state: AgentState,
217 config: GraphConfig,
218 ) -> (
219 tokio::task::JoinHandle<Result<AgentState>>,
220 mpsc::Receiver<StreamEvent>,
221 ) {
222 let (tx, rx) = mpsc::channel(256);
223 let start = start_node.to_string();
224 let graph = self;
225
226 let handle = tokio::spawn(async move {
227 graph.register_reducers_on_state(&state).await;
228 let trace_ctx = config.resolve_trace_ctx();
229 let run_id = graph.create_run_id().await?;
230 let event_sink = graph.resolve_event_sink(Some(tx));
231 let cancel = Arc::new(AtomicBool::new(false));
232
233 let executor = GraphExecutor {
234 graph: &graph,
235 state,
236 config,
237 iteration: 0,
238 event_sink,
239 run_id,
240 trace_ctx,
241 cancel_flag: cancel,
242 started_at: chrono::Utc::now(),
243 total_attempts: 0,
244 failed_attempts: 0,
245 executed_nodes: HashSet::new(),
246 };
247
248 let (result, _summary) = executor.execute(&start).await;
249 result
250 });
251
252 (handle, rx)
253 }
254}
255
256struct GraphExecutor<'a> {
258 graph: &'a AgentGraph,
259 state: AgentState,
260 config: GraphConfig,
261 iteration: usize,
262 event_sink: Arc<dyn EventSink>,
263 run_id: String,
264 trace_ctx: stack_ids::TraceCtx,
268 cancel_flag: Arc<AtomicBool>,
269 started_at: chrono::DateTime<chrono::Utc>,
270 total_attempts: usize,
271 failed_attempts: usize,
272 executed_nodes: HashSet<String>,
273}
274
275impl<'a> GraphExecutor<'a> {
276 fn legacy_trace_id(&self) -> String {
278 self.trace_ctx.to_legacy_trace_id().to_string()
279 }
280
281 async fn execute(mut self, start_node: &str) -> (Result<AgentState>, RunSummary) {
282 let run_legacy_tid = self.legacy_trace_id();
284
285 self.event_sink.emit(GraphEvent::RunStart {
287 run_id: self.run_id.clone(),
288 trace_id: run_legacy_tid.clone(),
289 trace_ctx: Some(self.trace_ctx.clone()),
290 graph_name: self.graph.graph_name.clone(),
291 });
292
293 let mut result = self.execute_inner(start_node).await;
294
295 if let Some(ref store) = self.graph.checkpoint_store {
299 let terminal_result = match &result {
300 Ok(_) => store.complete_run(&self.run_id).await.map_err(|error| {
301 checkpoint_store_error(CheckpointStoreOperation::CompleteRun, error)
302 }),
303 Err(AgentGraphError::Cancelled) => store
304 .fail_run(&self.run_id, "cancelled")
305 .await
306 .map_err(|error| {
307 checkpoint_store_error(CheckpointStoreOperation::FailRun, error)
308 }),
309 Err(error) => store
310 .fail_run(&self.run_id, &error.to_string())
311 .await
312 .map_err(|error| {
313 checkpoint_store_error(CheckpointStoreOperation::FailRun, error)
314 }),
315 };
316 if let Err(error) = terminal_result {
317 result = Err(error);
318 }
319 }
320
321 let status = match &result {
322 Ok(_) => RunStatus::Completed,
323 Err(AgentGraphError::Cancelled) => RunStatus::Cancelled,
324 Err(AgentGraphError::InterruptError { .. }) => RunStatus::Interrupted,
325 Err(_) => RunStatus::Failed,
326 };
327
328 let summary = self.build_run_summary(status, run_legacy_tid.clone());
329
330 self.event_sink.emit(GraphEvent::RunEnd {
332 run_id: self.run_id.clone(),
333 trace_id: run_legacy_tid,
334 trace_ctx: Some(self.trace_ctx.clone()),
335 });
336
337 (result, summary)
338 }
339
340 async fn execute_inner(&mut self, start_node: &str) -> Result<AgentState> {
341 let mut current_superstep = if start_node == START {
342 self.get_edge_targets(START).await?
343 } else {
344 vec![start_node.to_string()]
345 };
346
347 let mut step_number: usize = 0;
348 let max_iter = self.config.recursion_limit.min(self.graph.max_iterations);
349
350 let loop_legacy_tid = self.legacy_trace_id();
352
353 loop {
354 current_superstep.retain(|n| n != END);
355
356 if current_superstep.is_empty() {
357 break;
358 }
359
360 if self.cancel_flag.load(Ordering::Relaxed) {
362 return Err(AgentGraphError::Cancelled);
363 }
364
365 if self.iteration >= max_iter {
367 return Err(AgentGraphError::MaxIterationsExceeded {
368 current: self.iteration,
369 max: max_iter,
370 });
371 }
372
373 if self.graph.enable_cycle_detection && step_number > max_iter * 2 {
375 return Err(AgentGraphError::CycleDetected {
376 path: current_superstep.clone(),
377 });
378 }
379
380 self.event_sink.emit(GraphEvent::SuperstepStart {
382 run_id: self.run_id.clone(),
383 trace_id: loop_legacy_tid.clone(),
384 trace_ctx: Some(self.trace_ctx.clone()),
385 step: step_number,
386 nodes: current_superstep.clone(),
387 });
388
389 if let Some(ref interrupt_cfg) = self.graph.interrupt_config {
391 for node_name in ¤t_superstep {
392 if interrupt_cfg.should_interrupt_before(node_name) {
393 self.event_sink.emit(GraphEvent::InterruptRaised {
394 run_id: self.run_id.clone(),
395 trace_id: loop_legacy_tid.clone(),
396 trace_ctx: Some(self.trace_ctx.clone()),
397 node_id: node_name.clone(),
398 kind: "before".to_string(),
399 payload: Value::Null,
400 });
401
402 if let Some(ref checkpointer) = self.graph.checkpointer {
404 if let Some(ref thread_id) = self.config.thread_id {
405 let cp = Checkpoint {
406 execution_id: thread_id.clone(),
407 timestamp: chrono::Utc::now(),
408 current_node: node_name.clone(),
409 iteration: self.iteration,
410 state: self.state.snapshot().await,
411 step_number,
412 active_nodes: current_superstep.clone(),
413 };
414 let _ = checkpointer.save(&cp).await;
415 }
416 }
417
418 if let Some(ref store) = self.graph.checkpoint_store {
420 let state_data = self.state.export().await;
421 store
422 .save_state_snapshot(&self.run_id, &state_data)
423 .await
424 .map_err(|error| {
425 checkpoint_store_error(
426 CheckpointStoreOperation::SaveStateSnapshot,
427 error,
428 )
429 })?;
430 }
431
432 return Err(AgentGraphError::InterruptError {
433 node: node_name.clone(),
434 value: None,
435 });
436 }
437 }
438 }
439
440 let mut next_nodes = Vec::new();
442
443 if current_superstep.len() == 1 {
444 let node_name = ¤t_superstep[0];
445 let output = self.execute_single_node(node_name).await?;
446 let targets = self.resolve_output(node_name, output).await?;
447 next_nodes.extend(targets);
448 } else {
449 let snapshot_data = self.state.export().await;
451 let mut join_set = tokio::task::JoinSet::new();
452 let max_parallelism = self.config.max_parallelism.max(1);
453 let mut pending = current_superstep.iter();
454
455 for _ in 0..max_parallelism {
456 let Some(node_name) = pending.next() else {
457 break;
458 };
459 self.spawn_parallel_branch(&mut join_set, node_name).await?;
460 }
461
462 let mut branch_results = Vec::new();
463 let mut active_branches: std::collections::HashSet<String> = current_superstep
464 .iter()
465 .take(max_parallelism)
466 .cloned()
467 .collect();
468 while let Some(result) = join_set.join_next().await {
469 let inner = result.map_err(|e| AgentGraphError::ExecutionError(e.to_string()));
470 let branch = match inner {
471 Ok(Ok(branch)) => {
472 active_branches.remove(&branch.0);
473 branch
474 }
475 Ok(Err(error)) => {
476 self.failed_attempts += 1;
477 join_set.abort_all();
481 while join_set.join_next().await.is_some() {}
482 for node_id in active_branches {
483 self.event_sink.emit(GraphEvent::NodeEnd {
484 run_id: self.run_id.clone(),
485 trace_id: self.legacy_trace_id(),
486 trace_ctx: Some(self.trace_ctx.clone()),
487 node_id,
488 outcome: NodeOutcomeKind::Interrupted,
489 attempt_id: None,
490 trial_id: None,
491 });
492 }
493 self.event_sink.emit(GraphEvent::ParallelCancellation {
494 run_id: self.run_id.clone(),
495 trace_id: self.legacy_trace_id(),
496 trace_ctx: Some(self.trace_ctx.clone()),
497 external_effects_may_have_escaped: true,
498 });
499 return Err(error);
500 }
501 Err(error) => {
502 self.failed_attempts += 1;
503 join_set.abort_all();
504 while join_set.join_next().await.is_some() {}
505 for node_id in active_branches {
506 self.event_sink.emit(GraphEvent::NodeEnd {
507 run_id: self.run_id.clone(),
508 trace_id: self.legacy_trace_id(),
509 trace_ctx: Some(self.trace_ctx.clone()),
510 node_id,
511 outcome: NodeOutcomeKind::Interrupted,
512 attempt_id: None,
513 trial_id: None,
514 });
515 }
516 self.event_sink.emit(GraphEvent::ParallelCancellation {
517 run_id: self.run_id.clone(),
518 trace_id: self.legacy_trace_id(),
519 trace_ctx: Some(self.trace_ctx.clone()),
520 external_effects_may_have_escaped: true,
521 });
522 return Err(error);
523 }
524 };
525 branch_results.push(branch);
526
527 if let Some(node_name) = pending.next() {
528 active_branches.insert(node_name.to_string());
529 self.spawn_parallel_branch(&mut join_set, node_name).await?;
530 }
531 }
532
533 let order: HashMap<&str, usize> = current_superstep
534 .iter()
535 .enumerate()
536 .map(|(index, node)| (node.as_str(), index))
537 .collect();
538 branch_results.sort_by_key(|(name, _, _)| {
539 order.get(name.as_str()).copied().unwrap_or(usize::MAX)
540 });
541
542 self.merge_parallel_states(&snapshot_data, &branch_results)
543 .await?;
544
545 for (name, _, output) in branch_results {
546 let targets = self.resolve_output(&name, output).await?;
547 next_nodes.extend(targets);
548 }
549 }
550
551 if let Some(ref interrupt_cfg) = self.graph.interrupt_config {
553 for node_name in ¤t_superstep {
554 if interrupt_cfg.should_interrupt_after(node_name) {
555 if let Some(ref checkpointer) = self.graph.checkpointer {
556 if let Some(ref thread_id) = self.config.thread_id {
557 let cp = Checkpoint {
558 execution_id: thread_id.clone(),
559 timestamp: chrono::Utc::now(),
560 current_node: node_name.clone(),
561 iteration: self.iteration,
562 state: self.state.snapshot().await,
563 step_number,
564 active_nodes: next_nodes.clone(),
565 };
566 let _ = checkpointer.save(&cp).await;
567 }
568 }
569
570 if let Some(ref store) = self.graph.checkpoint_store {
571 let state_data = self.state.export().await;
572 store
573 .save_state_snapshot(&self.run_id, &state_data)
574 .await
575 .map_err(|error| {
576 checkpoint_store_error(
577 CheckpointStoreOperation::SaveStateSnapshot,
578 error,
579 )
580 })?;
581 }
582
583 return Err(AgentGraphError::InterruptError {
584 node: node_name.clone(),
585 value: None,
586 });
587 }
588 }
589 }
590
591 if let Some(ref checkpointer) = self.graph.checkpointer {
593 if let Some(ref thread_id) = self.config.thread_id {
594 let current = current_superstep.first().cloned().unwrap_or_default();
595 let cp = Checkpoint {
596 execution_id: thread_id.clone(),
597 timestamp: chrono::Utc::now(),
598 current_node: current,
599 iteration: self.iteration,
600 state: self.state.snapshot().await,
601 step_number,
602 active_nodes: next_nodes.clone(),
603 };
604 let _ = checkpointer.save(&cp).await;
605 }
606 }
607
608 if let Some(ref store) = self.graph.checkpoint_store {
610 let state_data = self.state.export().await;
611 store
612 .save_state_snapshot(&self.run_id, &state_data)
613 .await
614 .map_err(|error| {
615 checkpoint_store_error(CheckpointStoreOperation::SaveStateSnapshot, error)
616 })?;
617 }
618
619 self.event_sink.emit(GraphEvent::SuperstepEnd {
621 run_id: self.run_id.clone(),
622 trace_id: loop_legacy_tid.clone(),
623 trace_ctx: Some(self.trace_ctx.clone()),
624 step: step_number,
625 });
626
627 let mut seen = std::collections::HashSet::new();
629 next_nodes.retain(|n| seen.insert(n.clone()));
630
631 self.iteration += 1;
632 step_number += 1;
633 current_superstep = next_nodes;
634 }
635
636 Ok(self.state.clone())
637 }
638
639 async fn execute_single_node(&mut self, name: &str) -> Result<NodeOutput> {
641 self.total_attempts += 1;
642 self.executed_nodes.insert(name.to_string());
643 let node = self
644 .graph
645 .nodes
646 .get(name)
647 .cloned()
648 .ok_or_else(|| AgentGraphError::NodeNotFound(name.to_string()))?;
649
650 let canonical_attempt_id = stack_ids::AttemptId::generate();
652 let family_attempt = self.total_attempts as u32;
653
654 let legacy_tid = self.legacy_trace_id();
657 let trace_ctx = Some(self.trace_ctx.clone());
658 let node_name = name.to_string();
659 let retry = self.graph.retry_policies.get(name).cloned();
660
661 let before = self.state.export().await;
662 let outcome = if let Some(ref executor) = self.graph.executor {
663 let executor = executor.clone();
664 let state = self.state.clone();
665 let config = self.config.clone();
666 execute_node_attempt_family(
667 move || {
668 let executor = executor.clone();
669 let node = node.clone();
670 let state = state.clone();
671 let config = config.clone();
672 async move { executor.execute_node(node, state, config).await }
673 },
674 retry,
675 self.cancel_flag.clone(),
676 self.state.clone(),
677 self.event_sink.clone(),
678 self.graph.checkpoint_store.clone(),
679 self.run_id.clone(),
680 node_name.clone(),
681 legacy_tid.clone(),
682 trace_ctx.clone(),
683 family_attempt,
684 canonical_attempt_id.clone(),
685 )
686 .await
687 } else {
688 let state = self.state.clone();
689 let config = self.config.clone();
690 execute_node_attempt_family(
691 move || {
692 let node = node.clone();
693 let state = state.clone();
694 let config = config.clone();
695 async move { node.execute(&state, &config).await }
696 },
697 retry,
698 self.cancel_flag.clone(),
699 self.state.clone(),
700 self.event_sink.clone(),
701 self.graph.checkpoint_store.clone(),
702 self.run_id.clone(),
703 node_name.clone(),
704 legacy_tid.clone(),
705 trace_ctx.clone(),
706 family_attempt,
707 canonical_attempt_id.clone(),
708 )
709 .await
710 };
711
712 match outcome {
713 Ok(outcome) => {
714 let after = self.state.export().await;
716 let mut updates = HashMap::new();
717 for (key, val) in &after {
718 match before.get(key) {
719 Some(old_val) if old_val != val => {
720 updates.insert(key.clone(), val.clone());
721 }
722 None => {
723 updates.insert(key.clone(), val.clone());
724 }
725 _ => {}
726 }
727 }
728 if !updates.is_empty() {
729 self.event_sink.emit(GraphEvent::StateUpdate {
730 run_id: self.run_id.clone(),
731 trace_id: legacy_tid.clone(),
732 trace_ctx: trace_ctx.clone(),
733 node_id: node_name.clone(),
734 updates,
735 });
736 }
737
738 self.event_sink.emit(GraphEvent::NodeEnd {
739 run_id: self.run_id.clone(),
740 trace_id: legacy_tid.clone(),
741 trace_ctx,
742 node_id: node_name,
743 outcome: NodeOutcomeKind::Success,
744 attempt_id: Some(canonical_attempt_id),
745 trial_id: Some(outcome.trial_id),
746 });
747 Ok(outcome.output)
748 }
749 Err(failure) => {
750 self.failed_attempts += 1;
751 self.event_sink.emit(GraphEvent::NodeEnd {
752 run_id: self.run_id.clone(),
753 trace_id: legacy_tid,
754 trace_ctx,
755 node_id: node_name,
756 outcome: failure.outcome,
757 attempt_id: Some(canonical_attempt_id),
758 trial_id: Some(failure.trial_id),
759 });
760 Err(failure.error)
761 }
762 }
763 }
764
765 async fn resolve_output(&self, node_name: &str, output: NodeOutput) -> Result<Vec<String>> {
767 match output {
768 NodeOutput::Done => self.get_edge_targets(node_name).await,
769 NodeOutput::Command(cmd) => match cmd.goto {
770 Navigation::Default => self.get_edge_targets(node_name).await,
771 Navigation::Node(n) => Ok(vec![n]),
772 Navigation::Nodes(ns) => Ok(ns),
773 Navigation::End => Ok(vec![END.to_string()]),
774 Navigation::Send(ops) => Ok(ops.into_iter().map(|op| op.node).collect()),
775 },
776 }
777 }
778
779 async fn get_edge_targets(&self, node_name: &str) -> Result<Vec<String>> {
781 let mut targets = Vec::new();
782 if let Some(edge_list) = self.graph.edges.get(node_name) {
783 for edge in edge_list {
784 match edge {
785 EdgeType::Normal(to) => targets.push(to.clone()),
786 EdgeType::Conditional(router) => {
787 match router.route(&self.state, &self.config).await? {
788 RouterOutput::Next(Some(n)) => targets.push(n),
789 RouterOutput::Next(None) => {}
790 RouterOutput::FanOut(ns) => targets.extend(ns),
791 }
792 }
793 }
794 }
795 }
796 Ok(targets)
797 }
798
799 async fn merge_parallel_states(
801 &self,
802 snapshot: &HashMap<String, Value>,
803 branches: &[(String, AgentState, NodeOutput)],
804 ) -> Result<()> {
805 let mut changes: HashMap<String, Vec<Value>> = HashMap::new();
806
807 for (_, branch_state, _) in branches {
808 let branch_data = branch_state.export().await;
809 for (key, new_value) in &branch_data {
810 let changed = match snapshot.get(key) {
811 Some(old_val) => old_val != new_value,
812 None => true,
813 };
814 if changed {
815 changes
816 .entry(key.clone())
817 .or_default()
818 .push(new_value.clone());
819 }
820 }
821 }
822
823 for (key, values) in changes {
824 let base = snapshot.get(&key).cloned().unwrap_or(Value::Null);
825 let mut current = base;
826 for value in values {
827 current = self.state.apply_reducer(&key, ¤t, &value).await?;
828 }
829 self.state.set_raw(&key, current).await?;
830 }
831
832 Ok(())
833 }
834
835 async fn spawn_parallel_branch(
836 &mut self,
837 join_set: &mut tokio::task::JoinSet<Result<(String, AgentState, NodeOutput)>>,
838 node_name: &str,
839 ) -> Result<()> {
840 self.total_attempts += 1;
841 self.executed_nodes.insert(node_name.to_string());
842
843 let forked_state = self.state.fork().await;
844 let node = self
845 .graph
846 .nodes
847 .get(node_name)
848 .cloned()
849 .ok_or_else(|| AgentGraphError::NodeNotFound(node_name.to_string()))?;
850 let config = self.config.clone();
851 let name = node_name.to_string();
852 let retry_policy = self.graph.retry_policies.get(node_name).cloned();
853 let event_sink = self.event_sink.clone();
854 let run_id = self.run_id.clone();
855 let trace_id = self.legacy_trace_id();
856 let trace_ctx = Some(self.trace_ctx.clone());
857 let checkpoint_store = self.graph.checkpoint_store.clone();
858 let node_attempt_count = self.total_attempts as u32;
859 let cancel_flag = self.cancel_flag.clone();
860
861 if let Some(ref executor) = self.graph.executor {
862 let exec = executor.clone();
863 join_set.spawn(async move {
864 let canonical_attempt_id = stack_ids::AttemptId::generate();
866
867 let before = forked_state.export().await;
868 let execution_state = forked_state.clone();
869 let outcome = execute_node_attempt_family(
870 move || {
871 let exec = exec.clone();
872 let node = node.clone();
873 let forked_state = execution_state.clone();
874 let config = config.clone();
875 async move { exec.execute_node(node, forked_state, config).await }
876 },
877 retry_policy,
878 cancel_flag.clone(),
879 forked_state.clone(),
880 event_sink.clone(),
881 checkpoint_store.clone(),
882 run_id.clone(),
883 name.clone(),
884 trace_id.clone(),
885 trace_ctx.clone(),
886 node_attempt_count,
887 canonical_attempt_id.clone(),
888 )
889 .await;
890
891 match outcome {
892 Ok(outcome) => {
893 let after = forked_state.export().await;
894 let mut updates = HashMap::new();
895 for (key, val) in &after {
896 match before.get(key) {
897 Some(old_val) if old_val != val => {
898 updates.insert(key.clone(), val.clone());
899 }
900 None => {
901 updates.insert(key.clone(), val.clone());
902 }
903 _ => {}
904 }
905 }
906 if !updates.is_empty() {
907 event_sink.emit(GraphEvent::StateUpdate {
908 run_id: run_id.clone(),
909 trace_id: trace_id.clone(),
910 trace_ctx: trace_ctx.clone(),
911 node_id: name.clone(),
912 updates,
913 });
914 }
915
916 event_sink.emit(GraphEvent::NodeEnd {
917 run_id: run_id.clone(),
918 trace_id: trace_id.clone(),
919 trace_ctx: trace_ctx.clone(),
920 node_id: name.clone(),
921 outcome: NodeOutcomeKind::Success,
922 attempt_id: Some(canonical_attempt_id),
923 trial_id: Some(outcome.trial_id),
924 });
925
926 Ok::<_, AgentGraphError>((name, forked_state, outcome.output))
927 }
928 Err(failure) => {
929 event_sink.emit(GraphEvent::NodeEnd {
930 run_id: run_id.clone(),
931 trace_id: trace_id.clone(),
932 trace_ctx: trace_ctx.clone(),
933 node_id: name.clone(),
934 outcome: failure.outcome,
935 attempt_id: Some(canonical_attempt_id),
936 trial_id: Some(failure.trial_id),
937 });
938 Err(failure.error)
939 }
940 }
941 });
942 } else {
943 join_set.spawn(async move {
944 let canonical_attempt_id = stack_ids::AttemptId::generate();
946
947 let before = forked_state.export().await;
948 let execution_state = forked_state.clone();
949 let outcome = execute_node_attempt_family(
950 move || {
951 let node = node.clone();
952 let forked_state = execution_state.clone();
953 let config = config.clone();
954 async move { node.execute(&forked_state, &config).await }
955 },
956 retry_policy,
957 cancel_flag.clone(),
958 forked_state.clone(),
959 event_sink.clone(),
960 checkpoint_store.clone(),
961 run_id.clone(),
962 name.clone(),
963 trace_id.clone(),
964 trace_ctx.clone(),
965 node_attempt_count,
966 canonical_attempt_id.clone(),
967 )
968 .await;
969
970 match outcome {
971 Ok(outcome) => {
972 let after = forked_state.export().await;
973 let mut updates = HashMap::new();
974 for (key, val) in &after {
975 match before.get(key) {
976 Some(old_val) if old_val != val => {
977 updates.insert(key.clone(), val.clone());
978 }
979 None => {
980 updates.insert(key.clone(), val.clone());
981 }
982 _ => {}
983 }
984 }
985 if !updates.is_empty() {
986 event_sink.emit(GraphEvent::StateUpdate {
987 run_id: run_id.clone(),
988 trace_id: trace_id.clone(),
989 trace_ctx: trace_ctx.clone(),
990 node_id: name.clone(),
991 updates,
992 });
993 }
994
995 event_sink.emit(GraphEvent::NodeEnd {
996 run_id: run_id.clone(),
997 trace_id: trace_id.clone(),
998 trace_ctx: trace_ctx.clone(),
999 node_id: name.clone(),
1000 outcome: NodeOutcomeKind::Success,
1001 attempt_id: Some(canonical_attempt_id),
1002 trial_id: Some(outcome.trial_id),
1003 });
1004
1005 Ok::<_, AgentGraphError>((name, forked_state, outcome.output))
1006 }
1007 Err(failure) => {
1008 event_sink.emit(GraphEvent::NodeEnd {
1009 run_id: run_id.clone(),
1010 trace_id: trace_id.clone(),
1011 trace_ctx: trace_ctx.clone(),
1012 node_id: name.clone(),
1013 outcome: failure.outcome,
1014 attempt_id: Some(canonical_attempt_id),
1015 trial_id: Some(failure.trial_id),
1016 });
1017 Err(failure.error)
1018 }
1019 }
1020 });
1021 }
1022
1023 Ok(())
1024 }
1025
1026 fn build_run_summary(&self, status: RunStatus, legacy_tid: String) -> RunSummary {
1029 RunSummary {
1030 run_id: self.run_id.clone(),
1031 graph_name: self
1032 .graph
1033 .graph_name
1034 .clone()
1035 .unwrap_or_else(|| "unnamed".to_string()),
1036 status,
1037 total_nodes_executed: self.executed_nodes.len(),
1038 total_attempts: self.total_attempts,
1039 failed_attempts: self.failed_attempts,
1040 trace_id: Some(legacy_tid),
1041 trace_ctx: Some(self.trace_ctx.clone()),
1042 started_at: self.started_at,
1043 finished_at: Some(chrono::Utc::now()),
1044 }
1045 }
1046}
1047
1048struct AttemptFamilySuccess {
1050 output: NodeOutput,
1051 trial_id: stack_ids::TrialId,
1052}
1053
1054struct AttemptFamilyFailure {
1056 error: AgentGraphError,
1057 outcome: NodeOutcomeKind,
1058 trial_id: stack_ids::TrialId,
1059}
1060
1061#[allow(clippy::too_many_arguments)]
1062async fn execute_node_attempt_family<ExecOnce, ExecFut>(
1063 mut exec_once: ExecOnce,
1064 retry: Option<RetryPolicy>,
1065 cancel_flag: Arc<AtomicBool>,
1066 state: AgentState,
1067 event_sink: Arc<dyn EventSink>,
1068 checkpoint_store: Option<Arc<dyn CheckpointStore>>,
1069 run_id: String,
1070 node_id: String,
1071 legacy_trace_id: String,
1072 trace_ctx: Option<stack_ids::TraceCtx>,
1073 family_attempt: u32,
1074 canonical_attempt_id: stack_ids::AttemptId,
1075) -> std::result::Result<AttemptFamilySuccess, AttemptFamilyFailure>
1076where
1077 ExecOnce: FnMut() -> ExecFut,
1078 ExecFut: Future<Output = Result<NodeOutput>>,
1079{
1080 let max_attempts = retry
1081 .as_ref()
1082 .map_or(1, |policy| policy.max_attempts.max(1));
1083
1084 for attempt_index in 0..max_attempts {
1085 if cancel_flag.load(Ordering::SeqCst) {
1086 return Err(AttemptFamilyFailure {
1087 error: AgentGraphError::Cancelled,
1088 outcome: NodeOutcomeKind::Interrupted,
1089 trial_id: stack_ids::TrialId::generate(),
1090 });
1091 }
1092 let trial_id = stack_ids::TrialId::generate();
1093 event_sink.emit(GraphEvent::NodeStart {
1094 run_id: run_id.clone(),
1095 trace_id: legacy_trace_id.clone(),
1096 trace_ctx: trace_ctx.clone(),
1097 node_id: node_id.clone(),
1098 attempt: family_attempt,
1099 attempt_id: Some(canonical_attempt_id.clone()),
1100 trial_id: Some(trial_id.clone()),
1101 });
1102
1103 let checkpoint_attempt_id = if let Some(ref store) = checkpoint_store {
1104 let state_val = serde_json::to_value(&state.export().await).unwrap_or(Value::Null);
1105 match store
1106 .record_attempt(&run_id, &node_id, attempt_index as u32, &state_val)
1107 .await
1108 {
1109 Ok(attempt_id) => Some(attempt_id),
1110 Err(error) => {
1111 return Err(AttemptFamilyFailure {
1112 error: checkpoint_store_error(
1113 CheckpointStoreOperation::RecordAttempt,
1114 error,
1115 ),
1116 outcome: NodeOutcomeKind::Failed,
1117 trial_id,
1118 });
1119 }
1120 }
1121 } else {
1122 None
1123 };
1124
1125 match exec_once().await {
1126 Ok(output) => {
1127 if let NodeOutput::Command(ref cmd) = output {
1128 if let Some(ref updates) = cmd.update {
1129 for (key, value) in updates {
1130 if let Err(error) = state.set(key, value.clone()).await {
1131 if let Some(ref store) = checkpoint_store {
1132 if let Some(ref attempt_id) = checkpoint_attempt_id {
1133 if let Err(store_error) =
1134 store.fail_attempt(attempt_id, &error.to_string()).await
1135 {
1136 return Err(AttemptFamilyFailure {
1137 error: checkpoint_store_error(
1138 CheckpointStoreOperation::FailAttempt,
1139 store_error,
1140 ),
1141 outcome: NodeOutcomeKind::Failed,
1142 trial_id: trial_id.clone(),
1143 });
1144 }
1145 }
1146 }
1147 return Err(AttemptFamilyFailure {
1148 error,
1149 outcome: NodeOutcomeKind::Failed,
1150 trial_id: trial_id.clone(),
1151 });
1152 }
1153 }
1154 }
1155 }
1156
1157 if let Some(ref store) = checkpoint_store {
1158 if let Some(ref attempt_id) = checkpoint_attempt_id {
1159 let mut meta = HashMap::new();
1160 meta.insert(
1161 "trace_id".to_string(),
1162 Value::String(legacy_trace_id.clone()),
1163 );
1164 let state_val =
1165 serde_json::to_value(&state.export().await).unwrap_or(Value::Null);
1166 if let Err(error) =
1167 store.complete_attempt(attempt_id, &state_val, &meta).await
1168 {
1169 return Err(AttemptFamilyFailure {
1170 error: checkpoint_store_error(
1171 CheckpointStoreOperation::CompleteAttempt,
1172 error,
1173 ),
1174 outcome: NodeOutcomeKind::Failed,
1175 trial_id: trial_id.clone(),
1176 });
1177 }
1178 }
1179 }
1180
1181 return Ok(AttemptFamilySuccess { output, trial_id });
1182 }
1183 Err(error) => {
1184 if let Some(ref store) = checkpoint_store {
1185 if let Some(ref attempt_id) = checkpoint_attempt_id {
1186 if let Err(store_error) =
1187 store.fail_attempt(attempt_id, &error.to_string()).await
1188 {
1189 return Err(AttemptFamilyFailure {
1190 error: checkpoint_store_error(
1191 CheckpointStoreOperation::FailAttempt,
1192 store_error,
1193 ),
1194 outcome: NodeOutcomeKind::Failed,
1195 trial_id,
1196 });
1197 }
1198 }
1199 }
1200
1201 let outcome = if matches!(&error, AgentGraphError::InterruptError { .. }) {
1202 NodeOutcomeKind::Interrupted
1203 } else {
1204 NodeOutcomeKind::Failed
1205 };
1206
1207 let should_retry = retry.as_ref().is_some_and(|policy| {
1208 attempt_index + 1 < max_attempts && policy.should_retry(&error)
1209 });
1210
1211 if should_retry {
1212 event_sink.emit(GraphEvent::NodeEnd {
1213 run_id: run_id.clone(),
1214 trace_id: legacy_trace_id.clone(),
1215 trace_ctx: trace_ctx.clone(),
1216 node_id: node_id.clone(),
1217 outcome: outcome.clone(),
1218 attempt_id: Some(canonical_attempt_id.clone()),
1219 trial_id: Some(trial_id.clone()),
1220 });
1221 let Some(policy) = retry.as_ref() else {
1222 return Err(AttemptFamilyFailure {
1223 error,
1224 outcome,
1225 trial_id,
1226 });
1227 };
1228 let delay = policy.delay_for_attempt(attempt_index);
1229 let mut remaining = delay;
1230 loop {
1231 if cancel_flag.load(Ordering::SeqCst) {
1232 return Err(AttemptFamilyFailure {
1233 error: AgentGraphError::Cancelled,
1234 outcome: NodeOutcomeKind::Interrupted,
1235 trial_id,
1236 });
1237 }
1238 let tick = std::time::Duration::from_millis(10).min(remaining);
1239 tokio::time::sleep(tick).await;
1240 if remaining <= tick {
1241 break;
1242 }
1243 remaining -= tick;
1244 }
1245 } else {
1246 return Err(AttemptFamilyFailure {
1247 error,
1248 outcome,
1249 trial_id,
1250 });
1251 }
1252 }
1253 }
1254 }
1255
1256 Err(AttemptFamilyFailure {
1257 error: AgentGraphError::ExecutionError("Retry exhausted with no error".to_string()),
1258 outcome: NodeOutcomeKind::Failed,
1259 trial_id: stack_ids::TrialId::generate(),
1260 })
1261}