1use 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
15pub 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 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 pub fn build(mut self, ast: &AgentFile) -> Result<RefGraph, GraphBuildError> {
42 self.add_variables(ast)?;
44 self.add_start_agent(ast)?;
45 self.add_topics(ast)?;
46
47 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 }
345 }
346
347 for clause in &action.node.with_clauses {
349 self.add_with_value_edges(reasoning_idx, &clause.node.value);
350 }
351
352 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 fn scan_instructions(&mut self, node_idx: NodeIndex, instructions: &Instructions) {
376 match instructions {
377 Instructions::Simple(_) | Instructions::Static(_) => {
378 }
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 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 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 Expr::String(_) | Expr::Number(_) | Expr::Bool(_) | Expr::None => {}
518 }
519 }
520
521 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 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}