1#![allow(deprecated)] use crate::checkpoint::Checkpoint;
4use crate::checkpoint_store::CheckpointStore;
5use crate::checkpointer::CheckpointSaver;
6use crate::command::NodeOutput;
7use crate::config::GraphConfig;
8use crate::edge::EdgeType;
9use crate::error::{AgentGraphError, Result};
10use crate::event_sink::{ChannelEventSink, EventSink, NoopEventSink};
11use crate::executor::Executor;
12use crate::interrupt::InterruptConfig;
13use crate::node::Node;
14use crate::reducer::Reducer;
15use crate::retry::RetryPolicy;
16use crate::state::AgentState;
17use crate::stream::StreamEvent;
18use serde_json::Value;
19use std::collections::HashMap;
20use std::sync::Arc;
21use tokio::sync::mpsc;
22
23pub use crate::builder::AgentGraphBuilder;
25
26pub const START: &str = "__start__";
28pub const END: &str = "__end__";
30
31pub struct AgentGraph {
34 pub(crate) nodes: HashMap<String, Arc<dyn Node>>,
35 pub(crate) edges: HashMap<String, Vec<EdgeType>>,
36 pub(crate) max_iterations: usize,
37 pub(crate) enable_cycle_detection: bool,
38 pub(crate) retry_policies: HashMap<String, RetryPolicy>,
39 pub(crate) interrupt_config: Option<InterruptConfig>,
40 pub(crate) checkpointer: Option<Arc<dyn CheckpointSaver>>,
42 pub(crate) checkpoint_store: Option<Arc<dyn CheckpointStore>>,
44 pub(crate) reducers: Vec<(String, Arc<dyn Reducer>)>,
45 pub(crate) graph_name: Option<String>,
46 pub(crate) event_sink: Option<Arc<dyn EventSink>>,
48 pub(crate) executor: Option<Arc<dyn Executor>>,
49}
50
51impl AgentGraph {
52 pub fn builder() -> AgentGraphBuilder {
54 AgentGraphBuilder::new()
55 }
56
57 pub(crate) async fn register_reducers_on_state(&self, state: &AgentState) {
60 let mut state_reducers = state.reducers.write().await;
61 for (key, reducer) in &self.reducers {
62 state_reducers.insert(key.clone(), reducer.clone());
63 }
64 }
65
66 pub(crate) fn resolve_event_sink(
68 &self,
69 stream_tx: Option<mpsc::Sender<StreamEvent>>,
70 ) -> Arc<dyn EventSink> {
71 if let Some(tx) = stream_tx {
72 if let Some(ref configured_sink) = self.event_sink {
74 Arc::new(crate::event_sink::CompositeEventSink::new(vec![
76 configured_sink.clone(),
77 Arc::new(ChannelEventSink::new(tx)),
78 ]))
79 } else {
80 Arc::new(ChannelEventSink::new(tx))
81 }
82 } else if let Some(ref sink) = self.event_sink {
83 sink.clone()
84 } else {
85 Arc::new(NoopEventSink)
86 }
87 }
88
89 pub(crate) async fn create_run_id(&self) -> Result<String> {
94 if let Some(ref store) = self.checkpoint_store {
95 let name = self.graph_name.as_deref().unwrap_or("unnamed");
96 store
97 .create_run(name)
98 .await
99 .map_err(|error| AgentGraphError::CheckpointStore {
100 operation: crate::error::CheckpointStoreOperation::CreateRun,
101 message: error.to_string(),
102 })
103 } else {
104 Ok(stack_ids::GraphRunId::random("agent-graph").to_string())
105 }
106 }
107
108 pub async fn resume(
114 &self,
115 state: AgentState,
116 config: GraphConfig,
117 checkpoint: crate::interrupt::InterruptCheckpoint,
118 ) -> Result<AgentState> {
119 if let Some(ref saved_hash) = checkpoint.graph_hash {
120 let current_hash = self.compute_graph_hash();
121 if *saved_hash != current_hash {
122 return Err(AgentGraphError::CheckpointMismatch {
123 expected: saved_hash.clone(),
124 actual: current_hash,
125 });
126 }
127 }
128 self.execute_with_config(&checkpoint.resume_node, state, config)
129 .await
130 }
131
132 pub async fn resume_force(
134 &self,
135 state: AgentState,
136 config: GraphConfig,
137 checkpoint: crate::interrupt::InterruptCheckpoint,
138 ) -> Result<AgentState> {
139 self.execute_with_config(&checkpoint.resume_node, state, config)
140 .await
141 }
142
143 pub async fn get_state(&self, config: &GraphConfig) -> Result<Option<AgentState>> {
145 if let (Some(checkpointer), Some(thread_id)) = (&self.checkpointer, &config.thread_id) {
146 if let Some(cp) = checkpointer.load(thread_id).await? {
147 let state = AgentState::new();
148 state.restore(&cp.state).await;
149 return Ok(Some(state));
150 }
151 }
152 Ok(None)
153 }
154
155 pub async fn get_state_history(&self, config: &GraphConfig) -> Result<Vec<Checkpoint>> {
157 if let (Some(checkpointer), Some(thread_id)) = (&self.checkpointer, &config.thread_id) {
158 return checkpointer.load_history(thread_id).await;
159 }
160 Ok(Vec::new())
161 }
162
163 pub async fn update_state(
165 &self,
166 config: &GraphConfig,
167 updates: HashMap<String, Value>,
168 ) -> Result<()> {
169 if let (Some(checkpointer), Some(thread_id)) = (&self.checkpointer, &config.thread_id) {
170 if let Some(mut cp) = checkpointer.load(thread_id).await? {
171 for (k, v) in updates {
172 cp.state.data.insert(k, v);
173 }
174 checkpointer.save(&cp).await?;
175 }
176 }
177 Ok(())
178 }
179
180 pub fn to_mermaid(&self) -> String {
182 let mut lines = vec!["graph TD".to_string()];
183 lines.push(format!(" {}([START])", START));
184 lines.push(format!(" {}([END])", END));
185
186 let mut node_names: Vec<&String> = self.nodes.keys().collect();
187 node_names.sort();
188 for name in &node_names {
189 lines.push(format!(" {0}[{0}]", name));
190 }
191
192 let mut edge_sources: Vec<&String> = self.edges.keys().collect();
193 edge_sources.sort();
194 for from in edge_sources {
195 if let Some(edge_list) = self.edges.get(from) {
196 for edge in edge_list {
197 match edge {
198 EdgeType::Normal(to) => {
199 lines.push(format!(" {} --> {}", from, to));
200 }
201 EdgeType::Conditional(_) => {
202 lines.push(format!(" {} -.->|condition| ?", from));
203 }
204 }
205 }
206 }
207 }
208
209 lines.join("\n")
210 }
211
212 pub fn name(&self) -> Option<&str> {
214 self.graph_name.as_deref()
215 }
216
217 pub fn node_names(&self) -> Vec<&String> {
219 self.nodes.keys().collect()
220 }
221
222 pub fn edge_map(&self) -> &HashMap<String, Vec<EdgeType>> {
224 &self.edges
225 }
226
227 pub fn compute_graph_hash(&self) -> String {
232 use std::collections::BTreeMap;
233 use std::hash::{Hash, Hasher};
234
235 let mut hasher = std::hash::DefaultHasher::new();
236
237 let mut sorted_nodes: Vec<&String> = self.nodes.keys().collect();
239 sorted_nodes.sort();
240 for name in &sorted_nodes {
241 name.hash(&mut hasher);
242 }
243
244 let sorted_edges: BTreeMap<&String, &Vec<EdgeType>> = self.edges.iter().collect();
246 for (from, edges) in &sorted_edges {
247 from.hash(&mut hasher);
248 for edge in *edges {
249 match edge {
250 EdgeType::Normal(to) => {
251 "normal".hash(&mut hasher);
252 to.hash(&mut hasher);
253 }
254 EdgeType::Conditional(router) => {
255 "conditional".hash(&mut hasher);
256 from.hash(&mut hasher);
257 router.semantic_digest().hash(&mut hasher);
258 }
259 }
260 }
261 }
262
263 format!("{:016x}", hasher.finish())
264 }
265}
266
267#[async_trait::async_trait]
269impl Node for AgentGraph {
270 async fn execute(&self, state: &AgentState, config: &GraphConfig) -> Result<NodeOutput> {
271 let subgraph_state = state.fork().await;
272
273 let start = if self.edges.contains_key(START) {
274 START
275 } else {
276 let all_targets: std::collections::HashSet<&str> = self
277 .edges
278 .values()
279 .flat_map(|edges| {
280 edges.iter().filter_map(|e| match e {
281 EdgeType::Normal(to) => Some(to.as_str()),
282 _ => None,
283 })
284 })
285 .collect();
286 let entry = self
287 .nodes
288 .keys()
289 .find(|n| !all_targets.contains(n.as_str()))
290 .ok_or_else(|| {
291 AgentGraphError::ExecutionError("Subgraph has no entry point".to_string())
292 })?;
293 entry.as_str()
294 };
295
296 let result = self
297 .execute_with_config(start, subgraph_state, config.clone())
298 .await?;
299
300 let result_data = result.export().await;
301 for (key, value) in result_data {
302 state.set(&key, value).await?;
303 }
304
305 Ok(NodeOutput::Done)
306 }
307
308 fn name(&self) -> Option<&str> {
309 self.graph_name.as_deref()
310 }
311}
312
313impl std::fmt::Debug for AgentGraph {
314 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315 f.debug_struct("AgentGraph")
316 .field("nodes", &self.nodes.keys().collect::<Vec<_>>())
317 .field("edges", &format!("{} edge groups", self.edges.len()))
318 .field("max_iterations", &self.max_iterations)
319 .finish()
320 }
321}