flowgentra_ai/core/graph/mod.rs
1//! # Graph Data Structure and Operations
2//!
3//! The Graph represents your agent's workflow as a Directed Acyclic Graph (DAG).
4//! It provides efficient queries, validation, and execution planning for complex workflows.
5//!
6//! ## Key Concepts
7//!
8//! - **Nodes** - Computational steps in your workflow
9//! - **Edges** - Connections between nodes with optional conditions
10//! - **Conditions** - Optional logic to decide which edges to take
11//! - **Adjacency Lists** - Cached edge mappings for O(1) lookups
12//!
13//! ## Validation
14//!
15//! The graph is automatically validated for:
16//! - No cycles (must be acyclic) unless explicitly allowed
17//! - All referenced nodes exist
18//! - START and END nodes are properly connected
19//! - No orphaned nodes
20//!
21//! ## Performance Considerations
22//!
23//! - Edge queries use cached adjacency lists (O(1) avg case)
24//! - Cycle detection is O(V + E) using iterative DFS
25//! - Topological sort is O(V + E)
26//!
27//! ## Example DAG
28//!
29//! ```text
30//! START
31//! ├─→ extract_info
32//! └─→ analyze_content
33//! └─→ decide_routing
34//! ├─→ complex_path (if complexity > 50)
35//! └─→ simple_path
36//! └─→ format_results
37//! └─→ END
38//! ```
39
40use crate::core::error::Result;
41use crate::core::node::{Edge, Node};
42use crate::core::state::State;
43use std::collections::{HashMap, VecDeque};
44
45/// Unique identifier for a node in the graph
46pub type NodeId = String;
47
48/// The execution graph for the agent
49///
50/// A graph represents the complete workflow structure:
51/// - Which nodes exist and what they do
52/// - How nodes are connected (edges)
53/// - Which nodes are entry and exit points
54/// - Optional: Loop configurations for repeated execution
55///
56/// # Performance Features
57///
58/// - **Adjacency List Cache**: Maintains HashMap<NodeId, Vec<usize>> for O(1) edge lookups
59/// - **Lazy Evaluation**: Validation deferred until explicitly called
60/// - **Memory Efficient**: Single edge vec with indexed access
61pub struct Graph<T: State> {
62 /// All nodes in the graph, indexed by name
63 pub(crate) nodes: HashMap<NodeId, Node<T>>,
64
65 /// All edges (connections) between nodes, stored in a single vec
66 pub(crate) edges: Vec<Edge<T>>,
67
68 /// Cached adjacency list: node_id → indices into self.edges (outgoing edges)
69 /// Built lazily and maintained on edge additions
70 adjacency_list: HashMap<NodeId, Vec<usize>>,
71
72 /// Entry point nodes (connected from START)
73 start_nodes: Vec<NodeId>,
74
75 /// Exit point nodes (connected to END)
76 end_nodes: Vec<NodeId>,
77
78 /// Whether to allow cycles (loops) in the graph
79 /// When true, graph validation will not enforce DAG property
80 allow_cycles: bool,
81
82 /// Maximum iterations for any loop in the graph
83 /// Prevents infinite loops
84 max_loop_iterations: usize,
85}
86
87impl<T: State> Graph<T> {
88 /// Create a new empty graph. Cycles are allowed by default.
89 /// Call `set_allow_cycles(false)` to enforce strict DAG mode.
90 pub fn new() -> Self {
91 Graph {
92 nodes: HashMap::new(),
93 edges: Vec::new(),
94 adjacency_list: HashMap::new(),
95 start_nodes: Vec::new(),
96 end_nodes: Vec::new(),
97 allow_cycles: true,
98 max_loop_iterations: 25,
99 }
100 }
101
102 /// Create a new graph that allows loops (cycles)
103 ///
104 /// Use this for workflows where nodes can intentionally feed back to earlier nodes.
105 /// Remember to configure `set_max_loop_iterations()` to prevent infinite loops.
106 ///
107 /// # Example
108 ///
109 /// ```ignore
110 /// use flowgentra_ai::core::graph::Graph;
111 ///
112 /// let mut graph = Graph::with_loops();
113 /// graph.set_max_loop_iterations(5);
114 /// // graph allows cycles up to 5 iterations
115 /// ```
116 pub fn with_loops() -> Self {
117 Graph {
118 nodes: HashMap::new(),
119 edges: Vec::new(),
120 adjacency_list: HashMap::new(),
121 start_nodes: Vec::new(),
122 end_nodes: Vec::new(),
123 allow_cycles: true,
124 max_loop_iterations: 10,
125 }
126 }
127
128 // =========================================================================
129 // Graph Construction
130 // =========================================================================
131
132 /// Add a node to the graph
133 ///
134 /// Nodes should be added before edges are created that reference them.
135 ///
136 /// # Arguments
137 /// * `node` - The node to add to the graph
138 pub fn add_node(&mut self, node: Node<T>) {
139 self.nodes.insert(node.name.clone(), node);
140 }
141
142 /// Add an edge to the graph and update adjacency list cache
143 ///
144 /// Both the source and target nodes should already exist in the graph.
145 /// The adjacency list is automatically updated for O(1) future lookups.
146 ///
147 /// # Arguments
148 /// * `edge` - The edge to add to the graph
149 pub fn add_edge(&mut self, edge: Edge<T>) {
150 // Get the index where this edge will be stored
151 let edge_index = self.edges.len();
152
153 // Store the edge
154 self.edges.push(edge.clone());
155
156 // Update adjacency list: map from -> [edge indices]
157 self.adjacency_list
158 .entry(edge.from.clone())
159 .or_default()
160 .push(edge_index);
161 }
162
163 /// Set the entry nodes (those connected from START)
164 ///
165 /// # Arguments
166 /// * `nodes` - Vector of node IDs that act as entry points
167 pub fn set_start_nodes(&mut self, nodes: Vec<NodeId>) {
168 self.start_nodes = nodes;
169 }
170
171 /// Set the exit nodes (those connected to END)
172 ///
173 /// # Arguments
174 /// * `nodes` - Vector of node IDs that act as exit points
175 pub fn set_end_nodes(&mut self, nodes: Vec<NodeId>) {
176 self.end_nodes = nodes;
177 }
178
179 /// Enable or disable loop support (allow cycles in the graph)
180 ///
181 /// When enabled, the graph validation will not enforce DAG property.
182 /// You should configure `set_max_loop_iterations()` to prevent infinite loops.
183 ///
184 /// # Arguments
185 /// * `allow` - Whether to allow cycles in the graph
186 ///
187 /// # Returns
188 /// * `&mut Self` - For builder pattern chaining
189 pub fn set_allow_cycles(&mut self, allow: bool) -> &mut Self {
190 self.allow_cycles = allow;
191 self
192 }
193
194 /// Check if cycles are allowed in this graph
195 ///
196 /// # Returns
197 /// * `true` if the graph allows cycles, `false` if it enforces DAG property
198 pub fn allows_cycles(&self) -> bool {
199 self.allow_cycles
200 }
201
202 /// Set maximum iterations for loops (builder-style, deprecated name)
203 ///
204 /// ⚠️ **Deprecated**: Use `set_max_loop_iterations()` instead.
205 /// This method is kept for backwards compatibility.
206 #[deprecated(since = "0.2.0", note = "Use `set_max_loop_iterations` instead")]
207 pub fn allow_loops(&mut self, allow: bool) -> &mut Self {
208 self.set_allow_cycles(allow)
209 }
210
211 /// Check if loops are allowed (deprecated name)
212 ///
213 /// ⚠️ **Deprecated**: Use `allows_cycles()` instead.
214 /// This method is kept for backwards compatibility.
215 #[deprecated(since = "0.2.0", note = "Use `allows_cycles` instead")]
216 pub fn loops_allowed(&self) -> bool {
217 self.allows_cycles()
218 }
219
220 /// Set maximum iterations for loops
221 ///
222 /// This prevents infinite loops by limiting the number of iterations
223 /// the runtime will execute for any loop structure.
224 ///
225 /// # Arguments
226 /// * `max` - Maximum number of loop iterations (0 = unlimited)
227 ///
228 /// # Returns
229 /// * `&mut Self` - For builder pattern chaining
230 ///
231 /// # Example
232 ///
233 /// ```ignore
234 /// use flowgentra_ai::core::graph::Graph;
235 ///
236 /// let mut graph = Graph::with_loops();
237 /// graph.set_max_loop_iterations(5); // Allow max 5 iterations
238 /// ```
239 pub fn set_max_loop_iterations(&mut self, max: usize) -> &mut Self {
240 self.max_loop_iterations = max;
241 self
242 }
243
244 /// Get maximum iterations for loops
245 pub fn max_loop_iterations(&self) -> usize {
246 self.max_loop_iterations
247 }
248
249 // =========================================================================
250 // Graph Queries
251 // =========================================================================
252
253 /// Get a specific node by ID (borrowed reference)
254 ///
255 /// # Arguments
256 /// * `id` - The node ID (name)
257 ///
258 /// # Returns
259 /// * `Some(&Node)` if the node exists, `None` otherwise
260 pub fn get_node(&self, id: &str) -> Option<&Node<T>> {
261 self.nodes.get(id)
262 }
263
264 /// Get all nodes in the graph
265 pub fn nodes(&self) -> &HashMap<NodeId, Node<T>> {
266 &self.nodes
267 }
268
269 /// Get all edges in the graph
270 pub fn edges(&self) -> &[Edge<T>] {
271 &self.edges
272 }
273
274 /// Get the entry nodes (START nodes)
275 pub fn start_nodes(&self) -> &[NodeId] {
276 &self.start_nodes
277 }
278
279 /// Get the exit nodes (END nodes)
280 pub fn end_nodes(&self) -> &[NodeId] {
281 &self.end_nodes
282 }
283
284 // =========================================================================
285 // Graph Operations (Optimized for Performance)
286 // =========================================================================
287
288 /// Get all outgoing edges from a node (O(1) average case using adjacency list)
289 ///
290 /// # Arguments
291 /// * `node_id` - ID of the source node (borrowed reference)
292 ///
293 /// # Returns
294 /// * Vector of references to edges originating from this node
295 ///
296 /// # Performance
297 /// * Time: O(1) average case + O(k) where k is number of outgoing edges to copy vec refs
298 /// * Space: O(k) for the returned vector
299 ///
300 /// # Example
301 ///
302 /// ```ignore
303 /// let edges = graph.get_next_nodes("my_node");
304 /// for edge in edges {
305 /// println!("Can go to: {}", edge.to);
306 /// }
307 /// ```
308 pub fn get_next_nodes(&self, node_id: &str) -> Vec<&Edge<T>> {
309 self.adjacency_list
310 .get(node_id)
311 .map(|indices| {
312 indices
313 .iter()
314 .filter_map(|&idx| self.edges.get(idx))
315 .collect()
316 })
317 .unwrap_or_default()
318 }
319
320 /// Get the IDs of nodes reachable in one step from the given node
321 ///
322 /// Returns unique target node names from outgoing edges (includes "END" if present).
323 ///
324 /// # Arguments
325 /// * `from` - ID of the source node
326 ///
327 /// # Returns
328 /// * Vector of unique target node IDs
329 ///
330 /// # Performance
331 /// * Time: O(k) where k is the number of outgoing edges
332 /// * Space: O(k) for result vector + O(k) for HashSet
333 pub fn get_reachable_node_ids(&self, from: &str) -> Vec<String> {
334 use std::collections::HashSet;
335 self.adjacency_list
336 .get(from)
337 .map(|indices| {
338 indices
339 .iter()
340 .filter_map(|&idx| self.edges.get(idx).map(|e| e.to.clone()))
341 .collect::<HashSet<_>>()
342 .into_iter()
343 .collect()
344 })
345 .unwrap_or_default()
346 }
347
348 /// Get all incoming edges to a node (O(E) linear scan - consider caching if frequently used)
349 ///
350 /// # Arguments
351 /// * `node_id` - ID of the target node
352 ///
353 /// # Returns
354 /// * Vector of references to edges ending at this node
355 ///
356 /// # Performance
357 /// * Time: O(E) where E is total number of edges
358 /// * Space: O(k) where k is number of incoming edges
359 ///
360 /// # Note
361 /// If this operation is called frequently, consider maintaining a reverse adjacency list.
362 pub fn get_prev_nodes(&self, node_id: &str) -> Vec<&Edge<T>> {
363 self.edges.iter().filter(|e| e.to == node_id).collect()
364 }
365
366 /// Validate the graph structure for correctness
367 ///
368 /// Checks:
369 /// - No cycles exist (DAG property) - unless loops are explicitly allowed
370 /// - All referenced nodes exist
371 /// - START and END nodes are properly connected
372 /// - No orphaned nodes
373 ///
374 /// # Errors
375 /// Returns `FlowgentraError` if validation fails with detailed reason
376 ///
377 /// # Performance
378 /// * Time: O(V + E) for cycle detection using iterative DFS
379 /// * Additional O(E) for edge validation
380 ///
381 /// # Example
382 ///
383 /// ```ignore
384 /// let mut graph = Graph::new();
385 /// // ... add nodes and edges ...
386 /// graph.validate()?; // Will return Err if graph is invalid
387 /// ```
388 pub fn validate(&self) -> Result<()> {
389 // Check for cycles using DFS (unless loops are allowed)
390 if !self.allow_cycles && self.has_cycle() {
391 return Err(crate::core::error::FlowgentraError::CycleDetected);
392 }
393
394 // Check all nodes referenced in edges exist
395 for edge in &self.edges {
396 if edge.from != "START" && !self.nodes.contains_key(&edge.from) {
397 return Err(crate::core::error::FlowgentraError::GraphError(format!(
398 "Edge references non-existent node: {}",
399 edge.from
400 )));
401 }
402 if edge.to != "END" && !self.nodes.contains_key(&edge.to) {
403 return Err(crate::core::error::FlowgentraError::GraphError(format!(
404 "Edge references non-existent node: {}",
405 edge.to
406 )));
407 }
408 }
409
410 // Check start and end nodes exist
411 for start in &self.start_nodes {
412 if !self.nodes.contains_key(start) {
413 return Err(crate::core::error::FlowgentraError::GraphError(format!(
414 "Start node does not exist: {}",
415 start
416 )));
417 }
418 }
419
420 for end in &self.end_nodes {
421 if !self.nodes.contains_key(end) {
422 return Err(crate::core::error::FlowgentraError::GraphError(format!(
423 "End node does not exist: {}",
424 end
425 )));
426 }
427 }
428
429 // When cycles are allowed, check that every node with outgoing edges has a path to END.
430 // A node with no outgoing edges cannot cause an infinite loop — it is either managed
431 // externally (e.g. as a supervisor child) or terminates naturally. Only nodes that
432 // participate in the graph's own routing can create unbounded cycles.
433 if self.allow_cycles {
434 let unreachable: Vec<String> = self
435 .nodes
436 .keys()
437 .filter(|name| {
438 let has_outgoing = self.edges.iter().any(|e| &e.from == *name);
439 has_outgoing && !self.can_reach_end(name)
440 })
441 .cloned()
442 .collect();
443 if !unreachable.is_empty() {
444 let mut sorted = unreachable;
445 sorted.sort();
446 return Err(crate::core::error::FlowgentraError::NoTerminationPath {
447 nodes: sorted.join(", "),
448 });
449 }
450 }
451
452 Ok(())
453 }
454
455 /// Returns true if `start` has at least one path that eventually reaches END.
456 /// Used to detect missing termination conditions in cyclic graphs.
457 fn can_reach_end(&self, start: &str) -> bool {
458 let mut visited = std::collections::HashSet::new();
459 let mut stack = vec![start.to_string()];
460 while let Some(node) = stack.pop() {
461 if !visited.insert(node.clone()) {
462 continue;
463 }
464 for edge in &self.edges {
465 if edge.from == node {
466 if edge.to == "END" {
467 return true;
468 }
469 stack.push(edge.to.clone());
470 }
471 }
472 }
473 false
474 }
475
476 // =========================================================================
477 // Cycle Detection (Internal)
478 // =========================================================================
479
480 /// Check if the graph contains a cycle (detects non-DAG property)
481 ///
482 /// Uses iterative DFS with explicit stack to avoid recursion limits.
483 /// Skips "END" node during traversal as it's a virtual terminal node.
484 ///
485 /// # Performance
486 /// * Time: O(V + E) - visits each vertex and edge once
487 /// * Space: O(V) - for visited set and recursion stack
488 fn has_cycle(&self) -> bool {
489 let mut visited = std::collections::HashSet::new();
490 let mut rec_stack = std::collections::HashSet::new();
491
492 for node_id in self.nodes.keys() {
493 if !visited.contains(node_id)
494 && self.has_cycle_util(node_id, &mut visited, &mut rec_stack)
495 {
496 return true;
497 }
498 }
499
500 false
501 }
502
503 /// Recursive DFS helper for cycle detection
504 ///
505 /// # Arguments
506 /// * `node` - Current node being explored
507 /// * `visited` - Set of all visited nodes
508 /// * `rec_stack` - Stack of nodes in current recursion path (for cycle detection)
509 ///
510 /// # Returns
511 /// * `true` if a cycle is detected, `false` otherwise
512 ///
513 /// # Algorithm
514 /// Uses color-based approach:
515 /// - White (not in visited): Unvisited
516 /// - Gray (in rec_stack): Currently being explored
517 /// - Black (visited, not in rec_stack): Fully explored
518 /// A back edge (to node in rec_stack) indicates a cycle.
519 fn has_cycle_util(
520 &self,
521 node: &str,
522 visited: &mut std::collections::HashSet<String>,
523 rec_stack: &mut std::collections::HashSet<String>,
524 ) -> bool {
525 visited.insert(node.to_string());
526 rec_stack.insert(node.to_string());
527
528 // Get all neighbors using adjacency list - avoid cloning by using borrowed strings
529 if let Some(indices) = self.adjacency_list.get(node) {
530 for &idx in indices {
531 if let Some(edge) = self.edges.get(idx) {
532 let neighbor = &edge.to;
533
534 // Skip virtual END node
535 if neighbor == "END" {
536 continue;
537 }
538
539 if !visited.contains(neighbor) {
540 if self.has_cycle_util(neighbor, visited, rec_stack) {
541 return true;
542 }
543 } else if rec_stack.contains(neighbor) {
544 return true;
545 }
546 }
547 }
548 }
549
550 rec_stack.remove(node);
551 false
552 }
553
554 // =========================================================================
555 // Execution Helpers
556 // =========================================================================
557
558 /// Topological sort of the graph
559 ///
560 /// Returns nodes in execution order (respecting dependencies).
561 /// Useful for analysis, optimization, and execution planning.
562 ///
563 /// # Algorithm
564 /// Uses Kahn's algorithm (BFS-based):
565 /// 1. Compute in-degree for each node
566 /// 2. Enqueue all nodes with in-degree 0
567 /// 3. Dequeue node, add to result, decrement in-degree of neighbors
568 /// 4. Repeat until queue empty
569 /// 5. If result size != nodes size, graph has cycles
570 ///
571 /// # Returns
572 /// * `Ok(Vec<NodeId>)` - Nodes in topological order
573 /// * `Err(FlowgentraError::CycleDetected)` - If graph has cycles
574 ///
575 /// # Performance
576 /// * Time: O(V + E)
577 /// * Space: O(V)
578 ///
579 /// # Example
580 ///
581 /// ```ignore
582 /// let sorted = graph.topological_sort()?;
583 /// println!("Execution order: {:?}", sorted);
584 /// ```
585 pub fn topological_sort(&self) -> Result<Vec<NodeId>> {
586 let mut in_degree: HashMap<&str, usize> = HashMap::new();
587
588 // Initialize in-degrees for all nodes
589 for node in self.nodes.keys() {
590 in_degree.insert(node.as_str(), 0);
591 }
592
593 // Count incoming edges
594 for edge in &self.edges {
595 if edge.to != "END" {
596 if let Some(degree) = in_degree.get_mut(edge.to.as_str()) {
597 *degree += 1;
598 }
599 }
600 }
601
602 // Enqueue all nodes with in-degree 0
603 let mut queue: VecDeque<&str> = in_degree
604 .iter()
605 .filter(|(_, °ree)| degree == 0)
606 .map(|(&id, _)| id)
607 .collect();
608
609 let mut result = Vec::new();
610
611 while let Some(node) = queue.pop_front() {
612 result.push(node.to_string());
613
614 // For each outgoing edge, decrement in-degree of target
615 for edge in self.get_next_nodes(node) {
616 if edge.to != "END" {
617 if let Some(degree) = in_degree.get_mut(edge.to.as_str()) {
618 *degree -= 1;
619 if *degree == 0 {
620 queue.push_back(edge.to.as_str());
621 }
622 }
623 }
624 }
625 }
626
627 // If we didn't process all nodes, there's a cycle
628 if result.len() != self.nodes.len() {
629 return Err(crate::core::error::FlowgentraError::CycleDetected);
630 }
631
632 Ok(result)
633 }
634}
635
636impl<T: crate::core::state::State + Default> Default for Graph<T> {
637 fn default() -> Self {
638 Self::new()
639 }
640}
641
642// =============================================================================
643// Graph Builder - Fluent API for Building Graphs
644// =============================================================================
645
646/// A fluent builder for constructing graphs with a convenient, chainable API.
647///
648/// This provides a more user-friendly way to programmatically build graphs
649/// compared to manually calling `add_node` and `add_edge`.
650///
651/// # Example: Basic Graph
652///
653/// ```ignore
654/// use flowgentra_ai::core::graph::GraphBuilder;
655/// use flowgentra_ai::core::node::Node;
656///
657/// let graph = GraphBuilder::new()
658/// .add_node(Node::new("start"))
659/// .add_node(Node::new("process"))
660/// .add_node(Node::new("end"))
661/// .add_edge("start", "process", None)
662/// .add_edge("process", "end", None)
663/// .build();
664/// ```
665///
666/// # Example: With Conditions
667///
668/// ```ignore
669/// use flowgentra_ai::core::graph::{GraphBuilder, RoutingCondition, Condition, ComparisonOp};
670/// use flowgentra_ai::core::node::Node;
671///
672/// let condition = RoutingCondition::dsl(
673/// Condition::compare("score", ComparisonOp::GreaterThan, 0.7)
674/// );
675///
676/// let graph = GraphBuilder::new()
677/// .add_node(Node::new("router"))
678/// .add_node(Node::new("high_confidence_path"))
679/// .add_node(Node::new("low_confidence_path"))
680/// .add_edge_with_condition("router", "high_confidence_path", condition)
681/// .build();
682/// ```
683pub struct GraphBuilder<T: State> {
684 graph: Graph<T>,
685}
686
687impl<T: State> GraphBuilder<T> {
688 /// Create a new graph builder starting with an empty DAG
689 pub fn new() -> Self {
690 GraphBuilder {
691 graph: Graph::new(),
692 }
693 }
694
695 /// Create a new graph builder with loop support
696 pub fn with_loops() -> Self {
697 GraphBuilder {
698 graph: Graph::with_loops(),
699 }
700 }
701
702 /// Add a node to the graph being built
703 pub fn add_node(mut self, node: Node<T>) -> Self {
704 self.graph.add_node(node);
705 self
706 }
707
708 /// Add multiple nodes at once
709 pub fn add_nodes(mut self, nodes: Vec<Node<T>>) -> Self {
710 for node in nodes {
711 self.graph.add_node(node);
712 }
713 self
714 }
715
716 /// Add an edge between two nodes
717 pub fn add_edge(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
718 let edge = crate::core::node::Edge::new(from, to, None);
719 self.graph.add_edge(edge);
720 self
721 }
722
723 /// Add an edge with a routing condition
724 pub fn add_edge_with_condition(
725 mut self,
726 from: impl Into<String>,
727 to: impl Into<String>,
728 condition: routing::RoutingCondition<T>,
729 ) -> Self {
730 let from_str = from.into();
731 let to_str = to.into();
732
733 // Convert RoutingCondition to EdgeCondition if using function-based
734 match condition {
735 routing::RoutingCondition::Function(f) => {
736 let edge = crate::core::node::Edge::<T>::new(from_str, to_str, Some(f));
737 self.graph.add_edge(edge);
738 }
739 routing::RoutingCondition::DSL(cond) => {
740 // For DSL-based conditions, create an edge and set the routing condition
741 let mut edge = crate::core::node::Edge::<T>::new(from_str, to_str, None);
742 edge.routing_condition = Some(cond);
743 self.graph.add_edge(edge);
744 }
745 }
746 self
747 }
748
749 /// Add an edge and its target node in one call (convenience method)
750 ///
751 /// This is useful for building linear or simple graphs where you want
752 /// to create nodes and edges together.
753 pub fn add_step(mut self, from: impl Into<String>, to_node: Node<T>) -> Self {
754 let from_str = from.into();
755 let to_str = to_node.name.clone();
756
757 self.graph.add_node(to_node);
758 let edge = crate::core::node::Edge::<T>::new(from_str, to_str, None);
759 self.graph.add_edge(edge);
760 self
761 }
762
763 /// Set whether the graph allows cycles (loops)
764 pub fn allow_cycles(mut self, allow: bool) -> Self {
765 self.graph.set_allow_cycles(allow);
766 self
767 }
768
769 /// Set the maximum number of loop iterations (prevents infinite loops)
770 pub fn max_loop_iterations(mut self, max: usize) -> Self {
771 self.graph.set_max_loop_iterations(max);
772 self
773 }
774
775 /// Build and return the graph
776 ///
777 /// Note: This does NOT validate the graph. Call `validate()` on the
778 /// returned graph if you need to check for cycles, orphaned nodes, etc.
779 pub fn build(self) -> Graph<T> {
780 self.graph
781 }
782
783 /// Build and validate the graph
784 ///
785 /// Returns an error if the graph is invalid (cycles, missing nodes, etc.)
786 pub fn build_and_validate(self) -> Result<Graph<T>> {
787 let graph = self.graph;
788 graph.validate()?;
789 Ok(graph)
790 }
791}
792
793impl<T: crate::core::state::State> Default for GraphBuilder<T> {
794 fn default() -> Self {
795 Self::new()
796 }
797}
798
799// Sub-modules
800pub mod analysis;
801pub mod compiler;
802pub mod routing;
803
804pub use analysis::{BranchAnalysis, GraphAnalyzer};
805pub use compiler::{
806 CompilationError, CompilationStats, CompiledGraph, CompilerOptions, ExecutionStage,
807 GraphCompiler,
808};
809pub use routing::*;