Skip to main content

busbar_sf_agentscript/graph/
builder.rs

1//! Builder for constructing a RefGraph from an AST.
2
3use super::edges::RefEdge;
4use super::error::{GraphBuildError, ValidationError};
5use super::nodes::RefNode;
6use super::RefGraph;
7use crate::ast::{
8    Expr, InstructionPart, Instructions, ReasoningAction, ReasoningActionTarget, Reference,
9    VariableKind,
10};
11use crate::AgentFile;
12use petgraph::graph::{DiGraph, NodeIndex};
13use std::collections::HashMap;
14
15/// Builder for constructing a reference graph from an AST.
16pub struct RefGraphBuilder {
17    graph: DiGraph<RefNode, RefEdge>,
18    topics: HashMap<String, NodeIndex>,
19    action_defs: HashMap<(String, String), NodeIndex>,
20    reasoning_actions: HashMap<(String, String), NodeIndex>,
21    variables: HashMap<String, NodeIndex>,
22    start_agent: Option<NodeIndex>,
23    unresolved_references: Vec<ValidationError>,
24}
25
26impl RefGraphBuilder {
27    /// Create a new builder.
28    pub fn new() -> Self {
29        Self {
30            graph: DiGraph::new(),
31            topics: HashMap::new(),
32            action_defs: HashMap::new(),
33            reasoning_actions: HashMap::new(),
34            variables: HashMap::new(),
35            start_agent: None,
36            unresolved_references: Vec::new(),
37        }
38    }
39
40    /// Build a RefGraph from an AgentFile AST.
41    pub fn build(mut self, ast: &AgentFile) -> Result<RefGraph, GraphBuildError> {
42        // Phase 1: Add all definition nodes
43        self.add_variables(ast)?;
44        self.add_start_agent(ast)?;
45        self.add_topics(ast)?;
46
47        // Phase 2: Add all reference edges
48        self.add_start_agent_edges(ast)?;
49        self.add_topic_edges(ast)?;
50
51        Ok(RefGraph {
52            graph: self.graph,
53            topics: self.topics,
54            action_defs: self.action_defs,
55            reasoning_actions: self.reasoning_actions,
56            variables: self.variables,
57            start_agent: self.start_agent,
58            unresolved_references: self.unresolved_references,
59        })
60    }
61
62    /// Add variable definition nodes.
63    fn add_variables(&mut self, ast: &AgentFile) -> Result<(), GraphBuildError> {
64        if let Some(variables) = &ast.variables {
65            for var in &variables.node.variables {
66                let name = var.node.name.node.clone();
67                let mutable = matches!(var.node.kind, VariableKind::Mutable);
68                let span = (var.span.start, var.span.end);
69
70                let node = RefNode::Variable {
71                    name: name.clone(),
72                    mutable,
73                    span,
74                };
75
76                let idx = self.graph.add_node(node);
77                self.variables.insert(name, idx);
78            }
79        }
80        Ok(())
81    }
82
83    /// Add the start_agent node.
84    fn add_start_agent(&mut self, ast: &AgentFile) -> Result<(), GraphBuildError> {
85        if let Some(start) = &ast.start_agent {
86            let span = (start.span.start, start.span.end);
87            let node = RefNode::StartAgent { span };
88            let idx = self.graph.add_node(node);
89            self.start_agent = Some(idx);
90
91            // Add action definitions from start_agent
92            if let Some(actions) = &start.node.actions {
93                for action in &actions.node.actions {
94                    let action_name = action.node.name.node.clone();
95                    let action_span = (action.span.start, action.span.end);
96
97                    let action_node = RefNode::ActionDef {
98                        name: action_name.clone(),
99                        topic: "start_agent".to_string(),
100                        span: action_span,
101                    };
102                    let action_idx = self.graph.add_node(action_node);
103                    self.action_defs
104                        .insert(("start_agent".to_string(), action_name), action_idx);
105                }
106            }
107
108            // Add reasoning actions from start_agent
109            if let Some(reasoning) = &start.node.reasoning {
110                if let Some(actions) = &reasoning.node.actions {
111                    for action in &actions.node {
112                        let action_name = action.node.name.node.clone();
113                        let action_span = (action.span.start, action.span.end);
114                        let target = Self::extract_target(&action.node.target.node);
115
116                        let reasoning_node = RefNode::ReasoningAction {
117                            name: action_name.clone(),
118                            topic: "start_agent".to_string(),
119                            target,
120                            span: action_span,
121                        };
122                        let reasoning_idx = self.graph.add_node(reasoning_node);
123                        self.reasoning_actions
124                            .insert(("start_agent".to_string(), action_name), reasoning_idx);
125                    }
126                }
127            }
128        }
129        Ok(())
130    }
131
132    /// Add topic nodes and their child action nodes.
133    fn add_topics(&mut self, ast: &AgentFile) -> Result<(), GraphBuildError> {
134        for topic in &ast.topics {
135            let topic_name = topic.node.name.node.clone();
136            let span = (topic.span.start, topic.span.end);
137
138            // Add topic node
139            let topic_node = RefNode::Topic {
140                name: topic_name.clone(),
141                span,
142            };
143            let topic_idx = self.graph.add_node(topic_node);
144            self.topics.insert(topic_name.clone(), topic_idx);
145
146            // Add action definition nodes
147            if let Some(actions) = &topic.node.actions {
148                for action in &actions.node.actions {
149                    let action_name = action.node.name.node.clone();
150                    let action_span = (action.span.start, action.span.end);
151
152                    let action_node = RefNode::ActionDef {
153                        name: action_name.clone(),
154                        topic: topic_name.clone(),
155                        span: action_span,
156                    };
157                    let action_idx = self.graph.add_node(action_node);
158                    self.action_defs
159                        .insert((topic_name.clone(), action_name), action_idx);
160                }
161            }
162
163            // Add reasoning action nodes
164            if let Some(reasoning) = &topic.node.reasoning {
165                if let Some(actions) = &reasoning.node.actions {
166                    for action in &actions.node {
167                        let action_name = action.node.name.node.clone();
168                        let action_span = (action.span.start, action.span.end);
169                        let target = Self::extract_target(&action.node.target.node);
170
171                        let reasoning_node = RefNode::ReasoningAction {
172                            name: action_name.clone(),
173                            topic: topic_name.clone(),
174                            target,
175                            span: action_span,
176                        };
177                        let reasoning_idx = self.graph.add_node(reasoning_node);
178                        self.reasoning_actions
179                            .insert((topic_name.clone(), action_name), reasoning_idx);
180                    }
181                }
182            }
183        }
184        Ok(())
185    }
186
187    /// Extract the target string from a ReasoningActionTarget.
188    fn extract_target(target: &ReasoningActionTarget) -> Option<String> {
189        match target {
190            ReasoningActionTarget::Action(reference) => Some(reference.full_path()),
191            ReasoningActionTarget::TransitionTo(reference) => Some(reference.full_path()),
192            ReasoningActionTarget::TopicDelegate(reference) => Some(reference.full_path()),
193            ReasoningActionTarget::Escalate => Some("@utils.escalate".to_string()),
194            ReasoningActionTarget::SetVariables => Some("@utils.setVariables".to_string()),
195        }
196    }
197
198    /// Add edges from start_agent to routed topics.
199    fn add_start_agent_edges(&mut self, ast: &AgentFile) -> Result<(), GraphBuildError> {
200        let start_idx = match self.start_agent {
201            Some(idx) => idx,
202            None => return Ok(()),
203        };
204
205        if let Some(start) = &ast.start_agent {
206            // Extract topic transitions from reasoning actions
207            if let Some(reasoning) = &start.node.reasoning {
208                if let Some(instructions) = &reasoning.node.instructions {
209                    self.scan_instructions(start_idx, &instructions.node);
210                }
211
212                if let Some(actions) = &reasoning.node.actions {
213                    for action in &actions.node {
214                        // Both TransitionTo and TopicDelegate route to a topic from start_agent
215                        let routing_ref = match &action.node.target.node {
216                            ReasoningActionTarget::TransitionTo(r)
217                            | ReasoningActionTarget::TopicDelegate(r) => Some(r),
218                            _ => None,
219                        };
220                        if let Some(reference) = routing_ref {
221                            if let Some(topic_name) = Self::extract_topic_from_ref(reference) {
222                                if let Some(&topic_idx) = self.topics.get(&topic_name) {
223                                    self.graph.add_edge(start_idx, topic_idx, RefEdge::Routes);
224                                } else {
225                                    self.unresolved_references.push(
226                                        ValidationError::UnresolvedReference {
227                                            reference: reference.full_path(),
228                                            namespace: "topic".to_string(),
229                                            span: (
230                                                action.node.target.span.start,
231                                                action.node.target.span.end,
232                                            ),
233                                            context: "start_agent".to_string(),
234                                        },
235                                    );
236                                }
237                            }
238                        }
239                    }
240                }
241            }
242        }
243        Ok(())
244    }
245
246    /// Add edges within and between topics.
247    fn add_topic_edges(&mut self, ast: &AgentFile) -> Result<(), GraphBuildError> {
248        for topic in &ast.topics {
249            let topic_name = &topic.node.name.node;
250            let topic_idx = self.topics[topic_name];
251
252            // Add edges from reasoning actions to their targets
253            if let Some(reasoning) = &topic.node.reasoning {
254                if let Some(instructions) = &reasoning.node.instructions {
255                    self.scan_instructions(topic_idx, &instructions.node);
256                }
257
258                if let Some(actions) = &reasoning.node.actions {
259                    self.add_reasoning_action_edges(topic_name, topic_idx, &actions.node)?;
260                }
261            }
262        }
263        Ok(())
264    }
265
266    /// Add edges for reasoning actions in a topic.
267    fn add_reasoning_action_edges(
268        &mut self,
269        topic_name: &str,
270        topic_idx: NodeIndex,
271        actions: &[crate::Spanned<ReasoningAction>],
272    ) -> Result<(), GraphBuildError> {
273        for action in actions {
274            let action_name = &action.node.name.node;
275            let reasoning_idx =
276                self.reasoning_actions[&(topic_name.to_string(), action_name.clone())];
277
278            match &action.node.target.node {
279                ReasoningActionTarget::Action(reference) => {
280                    // Reasoning action invokes an action definition
281                    if let Some(action_ref) = Self::extract_action_name(reference) {
282                        if let Some(&target_idx) = self
283                            .action_defs
284                            .get(&(topic_name.to_string(), action_ref.clone()))
285                        {
286                            self.graph
287                                .add_edge(reasoning_idx, target_idx, RefEdge::Invokes);
288                        } else {
289                            self.unresolved_references
290                                .push(ValidationError::UnresolvedReference {
291                                    reference: reference.full_path(),
292                                    namespace: "actions".to_string(),
293                                    span: (
294                                        action.node.target.span.start,
295                                        action.node.target.span.end,
296                                    ),
297                                    context: format!("topic {}", topic_name),
298                                });
299                        }
300                    }
301                }
302                ReasoningActionTarget::TransitionTo(reference) => {
303                    // Transition to another topic
304                    if let Some(target_topic) = Self::extract_topic_from_ref(reference) {
305                        if let Some(&target_idx) = self.topics.get(&target_topic) {
306                            self.graph
307                                .add_edge(topic_idx, target_idx, RefEdge::TransitionsTo);
308                        } else {
309                            self.unresolved_references
310                                .push(ValidationError::UnresolvedReference {
311                                    reference: reference.full_path(),
312                                    namespace: "topic".to_string(),
313                                    span: (
314                                        action.node.target.span.start,
315                                        action.node.target.span.end,
316                                    ),
317                                    context: format!("topic {}", topic_name),
318                                });
319                        }
320                    }
321                }
322                ReasoningActionTarget::TopicDelegate(reference) => {
323                    // Delegate to another topic
324                    if let Some(target_topic) = Self::extract_topic_from_ref(reference) {
325                        if let Some(&target_idx) = self.topics.get(&target_topic) {
326                            self.graph
327                                .add_edge(topic_idx, target_idx, RefEdge::Delegates);
328                        } else {
329                            self.unresolved_references
330                                .push(ValidationError::UnresolvedReference {
331                                    reference: reference.full_path(),
332                                    namespace: "topic".to_string(),
333                                    span: (
334                                        action.node.target.span.start,
335                                        action.node.target.span.end,
336                                    ),
337                                    context: format!("topic {}", topic_name),
338                                });
339                        }
340                    }
341                }
342                ReasoningActionTarget::Escalate | ReasoningActionTarget::SetVariables => {
343                    // Built-in utilities, no edges to add
344                }
345            }
346
347            // Add edges for with_clauses (reading variables)
348            for clause in &action.node.with_clauses {
349                self.add_with_value_edges(reasoning_idx, &clause.node.value);
350            }
351
352            // Add edges for set_clauses (writing variables)
353            for clause in &action.node.set_clauses {
354                let target_ref = &clause.node.target.node;
355                if target_ref.namespace == "variables" {
356                    let var_name = target_ref.path.join(".");
357                    if let Some(&var_idx) = self.variables.get(&var_name) {
358                        self.graph.add_edge(reasoning_idx, var_idx, RefEdge::Writes);
359                    } else {
360                        self.unresolved_references
361                            .push(ValidationError::UnresolvedReference {
362                                reference: target_ref.full_path(),
363                                namespace: "variables".to_string(),
364                                span: (clause.node.target.span.start, clause.node.target.span.end),
365                                context: format!("set clause in topic {}", topic_name),
366                            });
367                    }
368                }
369            }
370        }
371        Ok(())
372    }
373
374    /// Scan instructions for variable and action references.
375    fn scan_instructions(&mut self, node_idx: NodeIndex, instructions: &Instructions) {
376        match instructions {
377            Instructions::Simple(_) | Instructions::Static(_) => {
378                // Simple/static instructions don't contain references
379            }
380            Instructions::Dynamic(parts) => {
381                for part in parts {
382                    self.scan_instruction_part(node_idx, part);
383                }
384            }
385        }
386    }
387
388    fn scan_instruction_part(
389        &mut self,
390        node_idx: NodeIndex,
391        part: &crate::Spanned<InstructionPart>,
392    ) {
393        match &part.node {
394            InstructionPart::Text(_) => {}
395            InstructionPart::Interpolation(expr) => {
396                let spanned_expr = crate::Spanned {
397                    node: expr.clone(),
398                    span: part.span.clone(),
399                };
400                self.add_expression_edges(node_idx, &spanned_expr);
401            }
402            InstructionPart::Conditional {
403                condition,
404                then_parts,
405                else_parts,
406            } => {
407                self.add_expression_edges(node_idx, condition);
408                for p in then_parts {
409                    self.scan_instruction_part(node_idx, p);
410                }
411                if let Some(parts) = else_parts {
412                    for p in parts {
413                        self.scan_instruction_part(node_idx, p);
414                    }
415                }
416            }
417        }
418    }
419
420    /// Add edges for variable reads within a with clause value.
421    fn add_with_value_edges(
422        &mut self,
423        from_idx: NodeIndex,
424        value: &crate::Spanned<crate::ast::WithValue>,
425    ) {
426        match &value.node {
427            crate::ast::WithValue::Expr(expr) => {
428                let spanned_expr = crate::Spanned {
429                    node: expr.clone(),
430                    span: value.span.clone(),
431                };
432                self.add_expression_edges(from_idx, &spanned_expr);
433            }
434        }
435    }
436
437    /// Add edges for variable reads within an expression.
438    fn add_expression_edges(&mut self, from_idx: NodeIndex, expr: &crate::Spanned<Expr>) {
439        match &expr.node {
440            Expr::Reference(reference) => {
441                if reference.namespace == "variables" {
442                    let var_name = reference.path.join(".");
443                    if let Some(&var_idx) = self.variables.get(&var_name) {
444                        self.graph.add_edge(from_idx, var_idx, RefEdge::Reads);
445                    } else {
446                        self.unresolved_references
447                            .push(ValidationError::UnresolvedReference {
448                                reference: reference.full_path(),
449                                namespace: "variables".to_string(),
450                                span: (expr.span.start, expr.span.end),
451                                context: "variable read".to_string(),
452                            });
453                    }
454                } else if reference.namespace == "actions" {
455                    let topic_name = match self.graph.node_weight(from_idx) {
456                        Some(RefNode::Topic { name, .. }) => Some(name.clone()),
457                        Some(RefNode::StartAgent { .. }) => Some("start_agent".to_string()),
458                        Some(RefNode::ReasoningAction { topic, .. }) => Some(topic.clone()),
459                        _ => None,
460                    };
461
462                    if let Some(topic_name) = topic_name {
463                        if let Some(action_ref) = Self::extract_action_name(reference) {
464                            if let Some(&action_idx) = self
465                                .action_defs
466                                .get(&(topic_name.clone(), action_ref.clone()))
467                            {
468                                self.graph.add_edge(from_idx, action_idx, RefEdge::Invokes);
469                            } else {
470                                self.unresolved_references.push(
471                                    ValidationError::UnresolvedReference {
472                                        reference: reference.full_path(),
473                                        namespace: "actions".to_string(),
474                                        span: (expr.span.start, expr.span.end),
475                                        context: format!("topic {}", topic_name),
476                                    },
477                                );
478                            }
479                        }
480                    }
481                }
482            }
483            Expr::BinOp { left, right, .. } => {
484                self.add_expression_edges(from_idx, left);
485                self.add_expression_edges(from_idx, right);
486            }
487            Expr::UnaryOp { operand, .. } => {
488                self.add_expression_edges(from_idx, operand);
489            }
490            Expr::Ternary {
491                condition,
492                then_expr,
493                else_expr,
494            } => {
495                self.add_expression_edges(from_idx, condition);
496                self.add_expression_edges(from_idx, then_expr);
497                self.add_expression_edges(from_idx, else_expr);
498            }
499            Expr::List(items) => {
500                for item in items {
501                    self.add_expression_edges(from_idx, item);
502                }
503            }
504            Expr::Object(entries) => {
505                for (_, value) in entries {
506                    self.add_expression_edges(from_idx, value);
507                }
508            }
509            Expr::Property { object, .. } => {
510                self.add_expression_edges(from_idx, object);
511            }
512            Expr::Index { object, index } => {
513                self.add_expression_edges(from_idx, object);
514                self.add_expression_edges(from_idx, index);
515            }
516            // Literals don't have references
517            Expr::String(_) | Expr::Number(_) | Expr::Bool(_) | Expr::None => {}
518        }
519    }
520
521    /// Extract topic name from a @topic.name reference.
522    fn extract_topic_from_ref(reference: &Reference) -> Option<String> {
523        if reference.namespace == "topic" && !reference.path.is_empty() {
524            Some(reference.path[0].clone())
525        } else {
526            None
527        }
528    }
529
530    /// Extract action name from a @actions.name reference.
531    fn extract_action_name(reference: &Reference) -> Option<String> {
532        if reference.namespace == "actions" && !reference.path.is_empty() {
533            Some(reference.path[0].clone())
534        } else {
535            None
536        }
537    }
538}
539
540impl Default for RefGraphBuilder {
541    fn default() -> Self {
542        Self::new()
543    }
544}