Skip to main content

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(|(_, &degree)| 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::*;