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, Type,
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 variable_types: HashMap<String, Type>,
24 start_agent: Option<NodeIndex>,
25 unresolved_references: Vec<ValidationError>,
26}
27
28impl RefGraphBuilder {
29 pub fn new() -> Self {
31 Self {
32 graph: DiGraph::new(),
33 topics: HashMap::new(),
34 action_defs: HashMap::new(),
35 reasoning_actions: HashMap::new(),
36 variables: HashMap::new(),
37 variable_types: HashMap::new(),
38 start_agent: None,
39 unresolved_references: Vec::new(),
40 }
41 }
42
43 pub fn build(mut self, ast: &AgentFile) -> Result<RefGraph, GraphBuildError> {
45 self.add_variables(ast)?;
47 self.add_start_agent(ast)?;
48 self.add_topics(ast)?;
49
50 self.add_start_agent_edges(ast)?;
52 self.add_topic_edges(ast)?;
53
54 Ok(RefGraph {
55 graph: self.graph,
56 topics: self.topics,
57 action_defs: self.action_defs,
58 reasoning_actions: self.reasoning_actions,
59 variables: self.variables,
60 start_agent: self.start_agent,
61 unresolved_references: self.unresolved_references,
62 })
63 }
64
65 fn add_variables(&mut self, ast: &AgentFile) -> Result<(), GraphBuildError> {
67 if let Some(variables) = &ast.variables {
68 for var in &variables.node.variables {
69 let name = var.node.name.node.clone();
70 let mutable = matches!(var.node.kind, VariableKind::Mutable);
71 let span = (var.span.start, var.span.end);
72
73 let node = RefNode::Variable {
74 name: name.clone(),
75 mutable,
76 span,
77 };
78
79 let idx = self.graph.add_node(node);
80 self.variable_types
81 .insert(name.clone(), var.node.ty.node.clone());
82 self.variables.insert(name, idx);
83 }
84 }
85 Ok(())
86 }
87
88 fn add_start_agent(&mut self, ast: &AgentFile) -> Result<(), GraphBuildError> {
90 if let Some(start) = &ast.start_agent {
91 let span = (start.span.start, start.span.end);
92 let node = RefNode::StartAgent { span };
93 let idx = self.graph.add_node(node);
94 self.start_agent = Some(idx);
95
96 if let Some(actions) = &start.node.actions {
98 for action in &actions.node.actions {
99 let action_name = action.node.name.node.clone();
100 let action_span = (action.span.start, action.span.end);
101
102 let action_node = RefNode::ActionDef {
103 name: action_name.clone(),
104 topic: "start_agent".to_string(),
105 span: action_span,
106 };
107 let action_idx = self.graph.add_node(action_node);
108 self.action_defs
109 .insert(("start_agent".to_string(), action_name), action_idx);
110 }
111 }
112
113 if let Some(reasoning) = &start.node.reasoning {
115 if let Some(actions) = &reasoning.node.actions {
116 for action in &actions.node {
117 let action_name = action.node.name.node.clone();
118 let action_span = (action.span.start, action.span.end);
119 let target = Self::extract_target(&action.node.target.node);
120
121 let reasoning_node = RefNode::ReasoningAction {
122 name: action_name.clone(),
123 topic: "start_agent".to_string(),
124 target,
125 span: action_span,
126 };
127 let reasoning_idx = self.graph.add_node(reasoning_node);
128 self.reasoning_actions
129 .insert(("start_agent".to_string(), action_name), reasoning_idx);
130 }
131 }
132 }
133 }
134 Ok(())
135 }
136
137 fn add_topics(&mut self, ast: &AgentFile) -> Result<(), GraphBuildError> {
139 for topic in &ast.topics {
140 let topic_name = topic.node.name.node.clone();
141 let span = (topic.span.start, topic.span.end);
142
143 let topic_node = RefNode::Topic {
145 name: topic_name.clone(),
146 span,
147 };
148 let topic_idx = self.graph.add_node(topic_node);
149 self.topics.insert(topic_name.clone(), topic_idx);
150
151 if let Some(actions) = &topic.node.actions {
153 for action in &actions.node.actions {
154 let action_name = action.node.name.node.clone();
155 let action_span = (action.span.start, action.span.end);
156
157 let action_node = RefNode::ActionDef {
158 name: action_name.clone(),
159 topic: topic_name.clone(),
160 span: action_span,
161 };
162 let action_idx = self.graph.add_node(action_node);
163 self.action_defs
164 .insert((topic_name.clone(), action_name), action_idx);
165 }
166 }
167
168 if let Some(reasoning) = &topic.node.reasoning {
170 if let Some(actions) = &reasoning.node.actions {
171 for action in &actions.node {
172 let action_name = action.node.name.node.clone();
173 let action_span = (action.span.start, action.span.end);
174 let target = Self::extract_target(&action.node.target.node);
175
176 let reasoning_node = RefNode::ReasoningAction {
177 name: action_name.clone(),
178 topic: topic_name.clone(),
179 target,
180 span: action_span,
181 };
182 let reasoning_idx = self.graph.add_node(reasoning_node);
183 self.reasoning_actions
184 .insert((topic_name.clone(), action_name), reasoning_idx);
185 }
186 }
187 }
188 }
189 Ok(())
190 }
191
192 fn extract_target(target: &ReasoningActionTarget) -> Option<String> {
194 match target {
195 ReasoningActionTarget::Action(reference) => Some(reference.full_path()),
196 ReasoningActionTarget::TransitionTo(reference) => Some(reference.full_path()),
197 ReasoningActionTarget::TopicDelegate(reference) => Some(reference.full_path()),
198 ReasoningActionTarget::Escalate => Some("@utils.escalate".to_string()),
199 ReasoningActionTarget::SetVariables => Some("@utils.setVariables".to_string()),
200 }
201 }
202
203 fn add_start_agent_edges(&mut self, ast: &AgentFile) -> Result<(), GraphBuildError> {
205 let start_idx = match self.start_agent {
206 Some(idx) => idx,
207 None => return Ok(()),
208 };
209
210 if let Some(start) = &ast.start_agent {
211 if let Some(reasoning) = &start.node.reasoning {
213 if let Some(instructions) = &reasoning.node.instructions {
214 self.scan_instructions(start_idx, &instructions.node);
215 }
216
217 if let Some(actions) = &reasoning.node.actions {
218 for action in &actions.node {
219 let routing_ref = match &action.node.target.node {
221 ReasoningActionTarget::TransitionTo(r)
222 | ReasoningActionTarget::TopicDelegate(r) => Some(r),
223 _ => None,
224 };
225 if let Some(reference) = routing_ref {
226 if let Some(topic_name) = Self::extract_topic_from_ref(reference) {
227 if let Some(&topic_idx) = self.topics.get(&topic_name) {
228 self.graph.add_edge(start_idx, topic_idx, RefEdge::Routes);
229 } else {
230 self.unresolved_references.push(
231 ValidationError::UnresolvedReference {
232 reference: reference.full_path(),
233 namespace: "topic".to_string(),
234 span: (
235 action.node.target.span.start,
236 action.node.target.span.end,
237 ),
238 context: "start_agent".to_string(),
239 },
240 );
241 }
242 }
243 }
244 }
245 }
246 }
247 }
248 Ok(())
249 }
250
251 fn add_topic_edges(&mut self, ast: &AgentFile) -> Result<(), GraphBuildError> {
253 for topic in &ast.topics {
254 let topic_name = &topic.node.name.node;
255 let topic_idx = self.topics[topic_name];
256
257 if let Some(reasoning) = &topic.node.reasoning {
259 if let Some(instructions) = &reasoning.node.instructions {
260 self.scan_instructions(topic_idx, &instructions.node);
261 }
262
263 if let Some(actions) = &reasoning.node.actions {
264 self.add_reasoning_action_edges(topic_name, topic_idx, &actions.node)?;
265 }
266 }
267 }
268 Ok(())
269 }
270
271 fn add_reasoning_action_edges(
273 &mut self,
274 topic_name: &str,
275 topic_idx: NodeIndex,
276 actions: &[crate::Spanned<ReasoningAction>],
277 ) -> Result<(), GraphBuildError> {
278 for action in actions {
279 let action_name = &action.node.name.node;
280 let reasoning_idx =
281 self.reasoning_actions[&(topic_name.to_string(), action_name.clone())];
282
283 match &action.node.target.node {
284 ReasoningActionTarget::Action(reference) => {
285 if let Some(action_ref) = Self::extract_action_name(reference) {
287 if let Some(&target_idx) = self
288 .action_defs
289 .get(&(topic_name.to_string(), action_ref.clone()))
290 {
291 self.graph
292 .add_edge(reasoning_idx, target_idx, RefEdge::Invokes);
293 } else {
294 self.unresolved_references
295 .push(ValidationError::UnresolvedReference {
296 reference: reference.full_path(),
297 namespace: "actions".to_string(),
298 span: (
299 action.node.target.span.start,
300 action.node.target.span.end,
301 ),
302 context: format!("topic {}", topic_name),
303 });
304 }
305 }
306 }
307 ReasoningActionTarget::TransitionTo(reference) => {
308 if let Some(target_topic) = Self::extract_topic_from_ref(reference) {
310 if let Some(&target_idx) = self.topics.get(&target_topic) {
311 self.graph
312 .add_edge(topic_idx, target_idx, RefEdge::TransitionsTo);
313 } else {
314 self.unresolved_references
315 .push(ValidationError::UnresolvedReference {
316 reference: reference.full_path(),
317 namespace: "topic".to_string(),
318 span: (
319 action.node.target.span.start,
320 action.node.target.span.end,
321 ),
322 context: format!("topic {}", topic_name),
323 });
324 }
325 }
326 }
327 ReasoningActionTarget::TopicDelegate(reference) => {
328 if let Some(target_topic) = Self::extract_topic_from_ref(reference) {
330 if let Some(&target_idx) = self.topics.get(&target_topic) {
331 self.graph
332 .add_edge(topic_idx, target_idx, RefEdge::Delegates);
333 } else {
334 self.unresolved_references
335 .push(ValidationError::UnresolvedReference {
336 reference: reference.full_path(),
337 namespace: "topic".to_string(),
338 span: (
339 action.node.target.span.start,
340 action.node.target.span.end,
341 ),
342 context: format!("topic {}", topic_name),
343 });
344 }
345 }
346 }
347 ReasoningActionTarget::Escalate | ReasoningActionTarget::SetVariables => {
348 }
350 }
351
352 for clause in &action.node.with_clauses {
354 self.add_with_value_edges(reasoning_idx, &clause.node.value);
355 }
356
357 for clause in &action.node.set_clauses {
359 let target_ref = &clause.node.target.node;
360 if target_ref.namespace == "variables" {
361 let var_name = target_ref
365 .path
366 .first()
367 .map_or_else(|| target_ref.path.join("."), |first| first.clone());
368 if let Some(&var_idx) = self.variables.get(&var_name) {
369 self.graph.add_edge(reasoning_idx, var_idx, RefEdge::Writes);
370 if target_ref.path.len() > 1 {
372 if let Some(ty) = self.variable_types.get(&var_name) {
373 if *ty != Type::Object {
374 self.unresolved_references.push(
375 ValidationError::InvalidPropertyAccess {
376 reference: target_ref.full_path(),
377 variable: var_name,
378 variable_type: Self::type_display_name(ty),
379 span: (
380 clause.node.target.span.start,
381 clause.node.target.span.end,
382 ),
383 },
384 );
385 }
386 }
387 }
388 } else {
389 self.unresolved_references
390 .push(ValidationError::UnresolvedReference {
391 reference: target_ref.full_path(),
392 namespace: "variables".to_string(),
393 span: (clause.node.target.span.start, clause.node.target.span.end),
394 context: format!("set clause in topic {}", topic_name),
395 });
396 }
397 }
398 }
399 }
400 Ok(())
401 }
402
403 fn scan_instructions(&mut self, node_idx: NodeIndex, instructions: &Instructions) {
405 match instructions {
406 Instructions::Simple(_) | Instructions::Static(_) => {
407 }
409 Instructions::Dynamic(parts) => {
410 for part in parts {
411 self.scan_instruction_part(node_idx, part);
412 }
413 }
414 }
415 }
416
417 fn scan_instruction_part(
418 &mut self,
419 node_idx: NodeIndex,
420 part: &crate::Spanned<InstructionPart>,
421 ) {
422 match &part.node {
423 InstructionPart::Text(_) => {}
424 InstructionPart::Interpolation(expr) => {
425 let spanned_expr = crate::Spanned {
426 node: expr.clone(),
427 span: part.span.clone(),
428 };
429 self.add_expression_edges(node_idx, &spanned_expr);
430 }
431 InstructionPart::Conditional {
432 condition,
433 then_parts,
434 else_parts,
435 } => {
436 self.add_expression_edges(node_idx, condition);
437 for p in then_parts {
438 self.scan_instruction_part(node_idx, p);
439 }
440 if let Some(parts) = else_parts {
441 for p in parts {
442 self.scan_instruction_part(node_idx, p);
443 }
444 }
445 }
446 }
447 }
448
449 fn add_with_value_edges(
451 &mut self,
452 from_idx: NodeIndex,
453 value: &crate::Spanned<crate::ast::WithValue>,
454 ) {
455 match &value.node {
456 crate::ast::WithValue::Expr(expr) => {
457 let spanned_expr = crate::Spanned {
458 node: expr.clone(),
459 span: value.span.clone(),
460 };
461 self.add_expression_edges(from_idx, &spanned_expr);
462 }
463 }
464 }
465
466 fn add_expression_edges(&mut self, from_idx: NodeIndex, expr: &crate::Spanned<Expr>) {
468 match &expr.node {
469 Expr::Reference(reference) => {
470 if reference.namespace == "variables" {
471 let var_name = reference
475 .path
476 .first()
477 .map_or_else(|| reference.path.join("."), |first| first.clone());
478 if let Some(&var_idx) = self.variables.get(&var_name) {
479 self.graph.add_edge(from_idx, var_idx, RefEdge::Reads);
480 if reference.path.len() > 1 {
482 if let Some(ty) = self.variable_types.get(&var_name) {
483 if *ty != Type::Object {
484 self.unresolved_references.push(
485 ValidationError::InvalidPropertyAccess {
486 reference: reference.full_path(),
487 variable: var_name,
488 variable_type: Self::type_display_name(ty),
489 span: (expr.span.start, expr.span.end),
490 },
491 );
492 }
493 }
494 }
495 } else {
496 self.unresolved_references
497 .push(ValidationError::UnresolvedReference {
498 reference: reference.full_path(),
499 namespace: "variables".to_string(),
500 span: (expr.span.start, expr.span.end),
501 context: "variable read".to_string(),
502 });
503 }
504 } else if reference.namespace == "actions" {
505 let topic_name = match self.graph.node_weight(from_idx) {
506 Some(RefNode::Topic { name, .. }) => Some(name.clone()),
507 Some(RefNode::StartAgent { .. }) => Some("start_agent".to_string()),
508 Some(RefNode::ReasoningAction { topic, .. }) => Some(topic.clone()),
509 _ => None,
510 };
511
512 if let Some(topic_name) = topic_name {
513 if let Some(action_ref) = Self::extract_action_name(reference) {
514 if let Some(&action_idx) = self
515 .action_defs
516 .get(&(topic_name.clone(), action_ref.clone()))
517 {
518 self.graph.add_edge(from_idx, action_idx, RefEdge::Invokes);
519 } else {
520 self.unresolved_references.push(
521 ValidationError::UnresolvedReference {
522 reference: reference.full_path(),
523 namespace: "actions".to_string(),
524 span: (expr.span.start, expr.span.end),
525 context: format!("topic {}", topic_name),
526 },
527 );
528 }
529 }
530 }
531 }
532 }
533 Expr::BinOp { left, right, .. } => {
534 self.add_expression_edges(from_idx, left);
535 self.add_expression_edges(from_idx, right);
536 }
537 Expr::UnaryOp { operand, .. } => {
538 self.add_expression_edges(from_idx, operand);
539 }
540 Expr::Ternary {
541 condition,
542 then_expr,
543 else_expr,
544 } => {
545 self.add_expression_edges(from_idx, condition);
546 self.add_expression_edges(from_idx, then_expr);
547 self.add_expression_edges(from_idx, else_expr);
548 }
549 Expr::List(items) => {
550 for item in items {
551 self.add_expression_edges(from_idx, item);
552 }
553 }
554 Expr::Object(entries) => {
555 for (_, value) in entries {
556 self.add_expression_edges(from_idx, value);
557 }
558 }
559 Expr::Property { object, .. } => {
560 self.add_expression_edges(from_idx, object);
561 }
562 Expr::Index { object, index } => {
563 self.add_expression_edges(from_idx, object);
564 self.add_expression_edges(from_idx, index);
565 }
566 Expr::String(_) | Expr::Number(_) | Expr::Bool(_) | Expr::None | Expr::SlotFill => {}
568 }
569 }
570
571 fn extract_topic_from_ref(reference: &Reference) -> Option<String> {
573 if reference.namespace == "topic" && !reference.path.is_empty() {
574 Some(reference.path[0].clone())
575 } else {
576 None
577 }
578 }
579
580 fn extract_action_name(reference: &Reference) -> Option<String> {
582 if reference.namespace == "actions" && !reference.path.is_empty() {
583 Some(reference.path[0].clone())
584 } else {
585 None
586 }
587 }
588
589 fn type_display_name(ty: &Type) -> String {
591 match ty {
592 Type::String => "string".to_string(),
593 Type::Number => "number".to_string(),
594 Type::Boolean => "boolean".to_string(),
595 Type::Object => "object".to_string(),
596 Type::Date => "date".to_string(),
597 Type::Timestamp => "timestamp".to_string(),
598 Type::Currency => "currency".to_string(),
599 Type::Id => "id".to_string(),
600 Type::Datetime => "datetime".to_string(),
601 Type::Time => "time".to_string(),
602 Type::Integer => "integer".to_string(),
603 Type::Long => "long".to_string(),
604 Type::List(inner) => format!("list[{}]", Self::type_display_name(inner)),
605 }
606 }
607}
608
609impl Default for RefGraphBuilder {
610 fn default() -> Self {
611 Self::new()
612 }
613}