Skip to main content

busbar_sf_agentscript/graph/
mod.rs

1//! # graph
2//!
3//! Graph-based analysis and validation for AgentScript ASTs.
4//!
5//! This module provides tools to build a reference graph from a parsed AgentScript AST,
6//! enabling validation, analysis, and querying of relationships between definitions.
7//!
8//! ## Features
9//!
10//! - **Reference Resolution**: Validate that all `@variables.*`, `@actions.*`, `@topic.*` references resolve
11//! - **Cycle Detection**: Ensure topic transitions form a DAG (no cycles)
12//! - **Reachability Analysis**: Find unreachable topics from `start_agent`
13//! - **Usage Queries**: Find all usages of a definition, or all dependencies of a node
14//! - **Dead Code Detection**: Identify unused actions and variables
15//!
16//! ## Example
17//!
18//! ```ignore
19//! use busbar_sf_agentscript::parse;
20//! use busbar_sf_agentscript::graph::RefGraph;
21//!
22//! let source = r#"
23//! config:
24//!    agent_name: "MyAgent"
25//! "#;
26//!
27//! let ast = parse(source).unwrap();
28//! let graph = RefGraph::from_ast(&ast).unwrap();
29//!
30//! // Validate all references
31//! let errors = graph.validate();
32//! for error in errors {
33//!     println!("Validation error: {:?}", error);
34//! }
35//!
36//! // Check for cycles
37//! if let Some(cycle) = graph.find_cycles().first() {
38//!     println!("Cycle detected: {:?}", cycle);
39//! }
40//! ```
41
42mod builder;
43pub mod dependencies;
44mod edges;
45mod error;
46pub mod export;
47mod nodes;
48mod queries;
49pub mod render;
50mod validation;
51
52#[cfg(feature = "wasm")]
53pub mod wasm;
54
55pub use builder::RefGraphBuilder;
56pub use dependencies::{extract_dependencies, Dependency, DependencyReport, DependencyType};
57pub use edges::RefEdge;
58pub use error::{GraphBuildError, ValidationError};
59pub use export::{EdgeRepr, GraphExport, GraphRepr, NodeRepr, ValidationResultRepr};
60pub use nodes::RefNode;
61pub use queries::QueryResult;
62pub use render::{render_actions_view, render_full_view, render_graphml, render_topic_flow};
63pub use validation::ValidationResult;
64
65use petgraph::graph::{DiGraph, NodeIndex};
66use std::collections::HashMap;
67
68/// A reference graph built from an AgentScript AST.
69///
70/// The graph represents relationships between definitions (topics, actions, variables)
71/// and can be used for validation, analysis, and querying.
72#[derive(Debug)]
73pub struct RefGraph {
74    /// The underlying directed graph
75    graph: DiGraph<RefNode, RefEdge>,
76
77    /// Index of topic nodes by name
78    topics: HashMap<String, NodeIndex>,
79
80    /// Index of action definition nodes by (topic_name, action_name)
81    action_defs: HashMap<(String, String), NodeIndex>,
82
83    /// Index of reasoning action nodes by (topic_name, action_name)
84    reasoning_actions: HashMap<(String, String), NodeIndex>,
85
86    /// Index of variable nodes by name
87    variables: HashMap<String, NodeIndex>,
88
89    /// The start_agent node index (if present)
90    start_agent: Option<NodeIndex>,
91
92    /// References that could not be resolved during build
93    unresolved_references: Vec<ValidationError>,
94}
95
96impl RefGraph {
97    /// Build a reference graph from a parsed AgentScript AST.
98    ///
99    /// This traverses the AST and builds nodes for all definitions,
100    /// then creates edges for all references between them.
101    pub fn from_ast(ast: &crate::AgentFile) -> Result<Self, GraphBuildError> {
102        RefGraphBuilder::new().build(ast)
103    }
104
105    /// Get the underlying petgraph for advanced operations.
106    pub fn inner(&self) -> &DiGraph<RefNode, RefEdge> {
107        &self.graph
108    }
109
110    /// Get a node by its index.
111    pub fn get_node(&self, index: NodeIndex) -> Option<&RefNode> {
112        self.graph.node_weight(index)
113    }
114
115    /// Look up a topic node by name.
116    pub fn get_topic(&self, name: &str) -> Option<NodeIndex> {
117        self.topics.get(name).copied()
118    }
119
120    /// Look up an action definition node by topic and action name.
121    pub fn get_action_def(&self, topic: &str, action: &str) -> Option<NodeIndex> {
122        self.action_defs
123            .get(&(topic.to_string(), action.to_string()))
124            .copied()
125    }
126
127    /// Look up a reasoning action node by topic and action name.
128    pub fn get_reasoning_action(&self, topic: &str, action: &str) -> Option<NodeIndex> {
129        self.reasoning_actions
130            .get(&(topic.to_string(), action.to_string()))
131            .copied()
132    }
133
134    /// Look up a variable node by name.
135    pub fn get_variable(&self, name: &str) -> Option<NodeIndex> {
136        self.variables.get(name).copied()
137    }
138
139    /// Get the start_agent node index.
140    pub fn get_start_agent(&self) -> Option<NodeIndex> {
141        self.start_agent
142    }
143
144    /// Get all topic names in the graph.
145    pub fn topic_names(&self) -> impl Iterator<Item = &str> {
146        self.topics.keys().map(|s| s.as_str())
147    }
148
149    /// Get all variable names in the graph.
150    pub fn variable_names(&self) -> impl Iterator<Item = &str> {
151        self.variables.keys().map(|s| s.as_str())
152    }
153
154    /// Get the number of nodes in the graph.
155    pub fn node_count(&self) -> usize {
156        self.graph.node_count()
157    }
158
159    /// Get the number of edges in the graph.
160    pub fn edge_count(&self) -> usize {
161        self.graph.edge_count()
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn test_empty_graph() {
171        // Minimal valid AgentScript
172        let source = r#"config:
173   agent_name: "Test"
174
175start_agent topic_selector:
176   description: "Route to topics"
177   reasoning:
178      instructions: "Select the best topic"
179      actions:
180         go_help: @utils.transition to @topic.help
181            description: "Go to help topic"
182
183topic help:
184   description: "Help topic"
185   reasoning:
186      instructions: "Provide help"
187"#;
188        let ast = crate::parse(source).unwrap();
189        let graph = RefGraph::from_ast(&ast).unwrap();
190
191        assert!(graph.node_count() > 0);
192        assert!(graph.get_topic("help").is_some());
193        assert!(graph.get_start_agent().is_some());
194    }
195}