1use crate::compiled::CompiledGraph;
5use crate::edge::{ConditionalEdge, GraphEdge};
6use crate::errors::{GraphError, GraphResult};
7use crate::node::{AsyncFn, AsyncNode, GraphNode, SyncNode};
8use crate::state::{MergeReducer, Reducer, ReplaceReducer, StateSchema, StateUpdate};
9use crate::subgraph::SubgraphNode;
10use std::collections::HashMap;
11use std::sync::Arc;
12
13pub const START: &str = "__start__";
15pub const END: &str = "__end__";
17
18pub struct StateGraph<S: StateSchema> {
23 nodes: HashMap<String, Arc<dyn GraphNode<S>>>,
24 edges: Vec<GraphEdge>,
25 entry_point: Option<String>,
26 reducers: HashMap<String, Arc<dyn Reducer<S>>>,
27 default_reducer: Arc<dyn Reducer<S>>,
28 conditional_routers: HashMap<String, Arc<dyn ConditionalEdge<S>>>,
29}
30
31impl<S: StateSchema + 'static> StateGraph<S> {
32 pub fn new() -> Self {
34 Self {
35 nodes: HashMap::new(),
36 edges: Vec::new(),
37 entry_point: None,
38 reducers: HashMap::new(),
39 default_reducer: Arc::new(ReplaceReducer),
40 conditional_routers: HashMap::new(),
41 }
42 }
43
44 pub fn add_node<N: GraphNode<S> + 'static>(&mut self, node: N) -> &mut Self {
46 let name = node.name().to_string();
47 self.nodes.insert(name, Arc::new(node));
48 self
49 }
50
51 pub fn add_node_fn<F>(&mut self, name: impl Into<String>, func: F) -> &mut Self
53 where
54 F: Fn(&S) -> Result<StateUpdate<S>, GraphError> + Send + Sync + 'static,
55 {
56 let node = SyncNode::new(name, func);
57 let node_name = node.name().to_string();
58 self.nodes.insert(node_name, Arc::new(node));
59 self
60 }
61
62 pub fn add_async_node<F>(&mut self, name: impl Into<String>, func: F) -> &mut Self
64 where
65 F: AsyncFn<S> + 'static,
66 {
67 let node = AsyncNode::new(name, func);
68 let node_name = node.name().to_string();
69 self.nodes.insert(node_name, Arc::new(node));
70 self
71 }
72
73 pub fn add_subgraph<SubS: StateSchema + 'static>(
75 &mut self,
76 name: impl Into<String>,
77 subgraph: CompiledGraph<SubS>,
78 input_mapper: impl Fn(&S) -> SubS + Send + Sync + 'static,
79 output_mapper: impl Fn(&SubS, &mut S) + Send + Sync + 'static,
80 ) -> &mut Self {
81 let node = SubgraphNode::new(name, subgraph, input_mapper, output_mapper);
82 let node_name = node.name().to_string();
83 self.nodes.insert(node_name, Arc::new(node));
84 self
85 }
86
87 pub fn add_subgraph_same_state(
89 &mut self,
90 name: impl Into<String>,
91 subgraph: CompiledGraph<S>,
92 ) -> &mut Self {
93 let node: SubgraphNode<S, S> = SubgraphNode::same_state(name, subgraph);
94 let node_name = node.name().to_string();
95 self.nodes.insert(node_name, Arc::new(node));
96 self
97 }
98
99 pub fn add_edge(&mut self, source: impl Into<String>, target: impl Into<String>) -> &mut Self {
101 let edge = GraphEdge::fixed(source, target);
102 self.edges.push(edge);
103 self
104 }
105
106 pub fn add_conditional_edges(
108 &mut self,
109 source: impl Into<String>,
110 router_name: impl Into<String>,
111 targets: HashMap<String, String>,
112 default: Option<String>,
113 ) -> &mut Self {
114 let edge = GraphEdge::conditional(source, router_name, targets, default);
115 self.edges.push(edge);
116 self
117 }
118
119 pub fn add_fan_out(&mut self, source: impl Into<String>, targets: Vec<String>) -> &mut Self {
121 let edge = GraphEdge::fan_out(source, targets);
122 self.edges.push(edge);
123 self
124 }
125
126 pub fn add_fan_in(&mut self, sources: Vec<String>, target: impl Into<String>) -> &mut Self {
128 let edge = GraphEdge::fan_in(sources, target);
129 self.edges.push(edge);
130 self
131 }
132
133 pub fn set_conditional_router<R: ConditionalEdge<S> + 'static>(
135 &mut self,
136 name: impl Into<String>,
137 router: R,
138 ) -> &mut Self {
139 self.conditional_routers
140 .insert(name.into(), Arc::new(router));
141 self
142 }
143
144 pub fn set_entry_point(&mut self, node: impl Into<String>) -> &mut Self {
146 self.entry_point = Some(node.into());
147 self
148 }
149
150 pub fn set_reducer(
152 &mut self,
153 field: impl Into<String>,
154 reducer: Arc<dyn Reducer<S>>,
155 ) -> &mut Self {
156 self.reducers.insert(field.into(), reducer);
157 self
158 }
159
160 pub fn compile(&self) -> GraphResult<CompiledGraph<S>> {
162 if self.nodes.is_empty() {
163 return Err(GraphError::ValidationError(
164 "Graph has no nodes".to_string(),
165 ));
166 }
167
168 let entry = self
169 .entry_point
170 .clone()
171 .or_else(|| self.find_first_node_after_start())
172 .ok_or_else(|| GraphError::ValidationError("No entry point defined".to_string()))?;
173
174 if !self.nodes.contains_key(&entry) && entry != START {
175 return Err(GraphError::ValidationError(format!(
176 "Entry point '{}' not found",
177 entry
178 )));
179 }
180
181 let merge_reducer: Arc<dyn Reducer<S>> = if self.reducers.is_empty() {
185 self.default_reducer.clone()
186 } else {
187 Arc::new(MergeReducer::new(
188 self.default_reducer.clone(),
189 self.reducers.values().cloned().collect(),
190 ))
191 };
192
193 let mut compiled =
194 CompiledGraph::new(self.nodes.clone(), self.edges.clone(), entry, merge_reducer);
195
196 for (name, router) in &self.conditional_routers {
197 compiled.add_router(name.clone(), router.clone());
198 }
199
200 compiled.validate()?;
201 Ok(compiled)
202 }
203
204 fn find_first_node_after_start(&self) -> Option<String> {
205 for edge in &self.edges {
206 if edge.source() == START {
207 if let Some(target) = edge.fixed_target() {
208 return Some(target.to_string());
209 }
210 }
211 }
212 None
213 }
214}
215
216impl<S: StateSchema + 'static> Default for StateGraph<S> {
217 fn default() -> Self {
218 Self::new()
219 }
220}
221
222pub struct GraphBuilder<S: StateSchema> {
224 graph: StateGraph<S>,
225}
226
227impl<S: StateSchema + 'static> GraphBuilder<S> {
228 pub fn new() -> Self {
230 Self {
231 graph: StateGraph::new(),
232 }
233 }
234
235 pub fn add_node<N: GraphNode<S> + 'static>(mut self, node: N) -> Self {
237 self.graph.add_node(node);
238 self
239 }
240
241 pub fn add_node_fn<F>(mut self, name: impl Into<String>, func: F) -> Self
243 where
244 F: Fn(&S) -> Result<StateUpdate<S>, GraphError> + Send + Sync + 'static,
245 {
246 self.graph.add_node_fn(name, func);
247 self
248 }
249
250 pub fn add_async_node<F>(mut self, name: impl Into<String>, func: F) -> Self
252 where
253 F: AsyncFn<S> + 'static,
254 {
255 self.graph.add_async_node(name, func);
256 self
257 }
258
259 pub fn add_subgraph<SubS: StateSchema + 'static>(
261 mut self,
262 name: impl Into<String>,
263 subgraph: CompiledGraph<SubS>,
264 input_mapper: impl Fn(&S) -> SubS + Send + Sync + 'static,
265 output_mapper: impl Fn(&SubS, &mut S) + Send + Sync + 'static,
266 ) -> Self {
267 self.graph
268 .add_subgraph(name, subgraph, input_mapper, output_mapper);
269 self
270 }
271
272 pub fn add_subgraph_same_state(
274 mut self,
275 name: impl Into<String>,
276 subgraph: CompiledGraph<S>,
277 ) -> Self {
278 self.graph.add_subgraph_same_state(name, subgraph);
279 self
280 }
281
282 pub fn add_edge(mut self, source: impl Into<String>, target: impl Into<String>) -> Self {
284 self.graph.add_edge(source, target);
285 self
286 }
287
288 pub fn add_conditional_edges(
290 mut self,
291 source: impl Into<String>,
292 router_name: impl Into<String>,
293 targets: HashMap<String, String>,
294 default: Option<String>,
295 ) -> Self {
296 self.graph
297 .add_conditional_edges(source, router_name, targets, default);
298 self
299 }
300
301 pub fn add_fan_out(mut self, source: impl Into<String>, targets: Vec<String>) -> Self {
303 self.graph.add_fan_out(source, targets);
304 self
305 }
306
307 pub fn add_fan_in(mut self, sources: Vec<String>, target: impl Into<String>) -> Self {
309 self.graph.add_fan_in(sources, target);
310 self
311 }
312
313 pub fn set_entry_point(mut self, node: impl Into<String>) -> Self {
315 self.graph.set_entry_point(node);
316 self
317 }
318
319 pub fn compile(self) -> GraphResult<CompiledGraph<S>> {
321 self.graph.compile()
322 }
323
324 pub fn build(self) -> StateGraph<S> {
326 self.graph
327 }
328}
329
330impl<S: StateSchema + 'static> Default for GraphBuilder<S> {
331 fn default() -> Self {
332 Self::new()
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339 use crate::state::AgentState;
340
341 #[test]
342 fn test_graph_creation() {
343 let graph: StateGraph<AgentState> = StateGraph::new();
344 assert!(graph.nodes.is_empty());
345 assert!(graph.edges.is_empty());
346 }
347
348 #[test]
349 fn test_add_node_fn() {
350 let mut graph: StateGraph<AgentState> = StateGraph::new();
351 graph.add_node_fn("test_node", |state: &AgentState| {
352 Ok(StateUpdate::full(state.clone()))
353 });
354 assert_eq!(graph.nodes.len(), 1);
355 }
356
357 #[test]
358 fn test_add_edge() {
359 let mut graph: StateGraph<AgentState> = StateGraph::new();
360 graph.add_edge(START, "node1");
361 graph.add_edge("node1", END);
362 assert_eq!(graph.edges.len(), 2);
363 }
364
365 #[test]
366 fn test_compile_empty_graph() {
367 let graph: StateGraph<AgentState> = StateGraph::new();
368 let result = graph.compile();
369 assert!(result.is_err());
370 }
371
372 #[test]
373 fn test_builder_pattern() {
374 let compiled = GraphBuilder::<AgentState>::new()
375 .add_node_fn("process", |state| Ok(StateUpdate::full(state.clone())))
376 .add_edge(START, "process")
377 .add_edge("process", END)
378 .compile();
379 assert!(compiled.is_ok());
380 }
381
382 use crate::compiled::types::StreamEvent;
388 use crate::state::{AppendMessagesReducer, MessageEntry, MessageRole};
389
390 #[tokio::test]
394 async fn test_field_reducer_accumulates_across_nodes() {
395 let mut graph: StateGraph<AgentState> = StateGraph::new();
396 graph.add_node_fn("a", |_state: &AgentState| {
397 Ok(StateUpdate {
399 update: Some(AgentState {
400 input: String::new(),
401 messages: vec![MessageEntry {
402 role: MessageRole::AI,
403 content: "from-a".into(),
404 }],
405 steps: vec![],
406 output: None,
407 }),
408 metadata: Default::default(),
409 })
410 });
411 graph.add_node_fn("b", |_state: &AgentState| {
412 Ok(StateUpdate {
413 update: Some(AgentState {
414 input: String::new(),
415 messages: vec![MessageEntry {
416 role: MessageRole::AI,
417 content: "from-b".into(),
418 }],
419 steps: vec![],
420 output: None,
421 }),
422 metadata: Default::default(),
423 })
424 });
425 graph.set_entry_point("a");
426 graph.add_edge("a", "b");
427 graph.add_edge("b", END);
428 graph.set_reducer("messages", Arc::new(AppendMessagesReducer));
429
430 let compiled = graph.compile().unwrap();
431 let mut initial = AgentState::new("start"); initial.messages.clear();
433 let result = compiled.invoke(initial).await.unwrap();
434 let contents: Vec<&str> = result
435 .final_state
436 .messages
437 .iter()
438 .map(|m| m.content.as_str())
439 .collect();
440 assert!(
442 contents.contains(&"from-a") && contents.contains(&"from-b"),
443 "field reducer should accumulate, got {contents:?}"
444 );
445 }
446
447 #[tokio::test]
450 async fn test_parallel_merge_keeps_main_path_state() {
451 let mut graph: StateGraph<AgentState> = StateGraph::new();
452 graph.add_node_fn("source", |state: &AgentState| {
453 let mut s = state.clone();
454 s.messages.push(MessageEntry {
455 role: MessageRole::AI,
456 content: "main-path".into(),
457 });
458 Ok(StateUpdate::full(s))
459 });
460 graph.add_node_fn("branch1", |state: &AgentState| {
461 let mut s = state.clone();
462 s.messages.push(MessageEntry {
463 role: MessageRole::AI,
464 content: "b1".into(),
465 });
466 Ok(StateUpdate::full(s))
467 });
468 graph.add_node_fn("branch2", |state: &AgentState| {
469 let mut s = state.clone();
470 s.messages.push(MessageEntry {
471 role: MessageRole::AI,
472 content: "b2".into(),
473 });
474 Ok(StateUpdate::full(s))
475 });
476 graph.add_node_fn("merge", |state: &AgentState| {
477 let mut s = state.clone();
478 s.set_output("merged");
479 Ok(StateUpdate::full(s))
480 });
481 graph.set_entry_point("source");
482 graph.add_fan_out("source", vec!["branch1".into(), "branch2".into()]);
483 graph.add_fan_in(vec!["branch1".into(), "branch2".into()], "merge");
484 graph.add_edge("merge", END);
485 graph.set_reducer("messages", Arc::new(AppendMessagesReducer));
486
487 let compiled = graph.compile().unwrap();
488 let mut initial = AgentState::new("start");
489 initial.messages.clear();
490 let result = compiled.invoke(initial).await.unwrap();
491 let contents: Vec<&str> = result
492 .final_state
493 .messages
494 .iter()
495 .map(|m| m.content.as_str())
496 .collect();
497 assert!(
499 contents.contains(&"main-path") && contents.contains(&"b1") && contents.contains(&"b2"),
500 "merge must keep main-path + all branch writes, got {contents:?}"
501 );
502 assert_eq!(result.final_state.output.as_deref(), Some("merged"));
503 }
504
505 #[tokio::test]
508 async fn test_stream_fan_out_runs_all_branches() {
509 let mut graph: StateGraph<AgentState> = StateGraph::new();
510 graph.add_node_fn("source", |state: &AgentState| {
511 Ok(StateUpdate::full(state.clone()))
512 });
513 graph.add_node_fn("branch1", |state: &AgentState| {
514 let mut s = state.clone();
515 s.messages.push(MessageEntry {
516 role: MessageRole::AI,
517 content: "s-b1".into(),
518 });
519 Ok(StateUpdate::full(s))
520 });
521 graph.add_node_fn("branch2", |state: &AgentState| {
522 let mut s = state.clone();
523 s.messages.push(MessageEntry {
524 role: MessageRole::AI,
525 content: "s-b2".into(),
526 });
527 Ok(StateUpdate::full(s))
528 });
529 graph.set_entry_point("source");
530 graph.add_fan_out("source", vec!["branch1".into(), "branch2".into()]);
531 graph.add_fan_in(vec!["branch1".into(), "branch2".into()], END);
532 graph.set_reducer("messages", Arc::new(AppendMessagesReducer));
533
534 let compiled = graph.compile().unwrap();
535 let mut initial = AgentState::new("start");
536 initial.messages.clear();
537 let events = compiled.stream_collected(initial).await.unwrap();
538 let end = events
539 .into_iter()
540 .find_map(|e| match e {
541 StreamEvent::End(s) => Some(s),
542 _ => None,
543 })
544 .expect("stream should end");
545 let contents: Vec<&str> = end.messages.iter().map(|m| m.content.as_str()).collect();
546 assert!(
547 contents.contains(&"s-b1") && contents.contains(&"s-b2"),
548 "stream must execute all fan-out branches, got {contents:?}"
549 );
550 }
551
552 #[test]
556 fn test_fan_in_topology_compiles() {
557 let mut graph: StateGraph<AgentState> = StateGraph::new();
558 graph.add_node_fn("source", |state: &AgentState| {
559 Ok(StateUpdate::full(state.clone()))
560 });
561 graph.add_node_fn("b1", |state: &AgentState| {
562 Ok(StateUpdate::full(state.clone()))
563 });
564 graph.add_node_fn("b2", |state: &AgentState| {
565 Ok(StateUpdate::full(state.clone()))
566 });
567 graph.add_node_fn("merge", |state: &AgentState| {
568 Ok(StateUpdate::full(state.clone()))
569 });
570 graph.set_entry_point("source");
571 graph.add_fan_out("source", vec!["b1".into(), "b2".into()]);
572 graph.add_fan_in(vec!["b1".into(), "b2".into()], "merge");
573 graph.add_edge("merge", END);
574 assert!(
575 graph.compile().is_ok(),
576 "fan-out + fan-in topology must compile"
577 );
578 }
579}