1use regex_lite::Regex;
7use std::collections::HashMap;
8use uuid::Uuid;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct InvocationRequest {
13 pub id: Uuid,
15
16 pub original_input: String,
18
19 pub execution_plan: ExecutionPlan,
21
22 pub context: String,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum ExecutionPlan {
29 Single(AgentInvocation),
31 Sequential(AgentChain),
33 Parallel(Vec<AgentInvocation>),
35 Conditional(ConditionalExecution),
37 Mixed(Vec<ExecutionStep>),
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum ExecutionStep {
44 Single(AgentInvocation),
46 Parallel(Vec<AgentInvocation>),
48 Conditional(ConditionalExecution),
50 Barrier,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct AgentInvocation {
57 pub agent_name: String,
59
60 pub parameters: HashMap<String, String>,
62
63 pub raw_parameters: String,
65
66 pub position: usize,
68
69 pub mode_override: Option<crate::modes::OperatingMode>,
71
72 pub intelligence_override: Option<String>,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct AgentChain {
79 pub agents: Vec<AgentInvocation>,
81
82 pub pass_output: bool,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct ConditionalExecution {
89 pub agents: Vec<AgentInvocation>,
91
92 pub condition: ExecutionCondition,
94
95 pub condition_params: HashMap<String, String>,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
101pub enum ExecutionCondition {
102 OnError,
104 OnSuccess,
106 OnFileError,
108 OnTestFailure,
110 OnFilePattern(String),
112 Custom(String),
114}
115
116pub struct InvocationParser {
118 agent_pattern: Regex,
120
121 chain_pattern: Regex,
123
124 parallel_pattern: Regex,
126
127 conditional_pattern: Regex,
129
130 simple_agent_pattern: Regex,
132
133 multiple_spaces_pattern: Regex,
135
136 registry: Option<std::sync::Arc<super::SubagentRegistry>>,
138}
139
140impl Default for InvocationParser {
141 fn default() -> Self {
142 Self::new()
143 }
144}
145
146impl InvocationParser {
147 pub fn new() -> Self {
149 let agent_pattern =
150 Regex::new(r"@([a-zA-Z0-9_-]+)(?:\s+([^@→+]*?))?(?:\s*[@→+]|\s*if\s|\s*$)")
151 .expect("Invalid agent pattern regex");
152
153 let chain_pattern = Regex::new(r"@[a-zA-Z0-9_-]+(?:\s+[^@→+\n]*?)?\s*→\s*")
154 .expect("Invalid chain pattern regex");
155
156 let parallel_pattern = Regex::new(r"@[a-zA-Z0-9_-]+(?:\s+[^@→+\n]*?)?\s*\+\s*")
157 .expect("Invalid parallel pattern regex");
158
159 let conditional_pattern =
160 Regex::new(r"@([a-zA-Z0-9_-]+)(?:\s+([^@→+\n]*?))?\s+if\s+([^@→+\n]+)")
161 .expect("Invalid conditional pattern regex");
162
163 let simple_agent_pattern =
164 Regex::new(r"@[a-zA-Z0-9_-]+").expect("Invalid simple agent pattern regex");
165
166 let multiple_spaces_pattern =
167 Regex::new(r"\s+").expect("Invalid multiple spaces pattern regex");
168
169 Self {
170 agent_pattern,
171 chain_pattern,
172 parallel_pattern,
173 conditional_pattern,
174 simple_agent_pattern,
175 multiple_spaces_pattern,
176 registry: None,
177 }
178 }
179
180 pub fn with_registry(registry: std::sync::Arc<super::SubagentRegistry>) -> Self {
182 let mut parser = Self::new();
183 parser.registry = Some(registry);
184 parser
185 }
186
187 pub fn parse(&self, input: &str) -> Result<Option<InvocationRequest>, super::SubagentError> {
189 if !input.contains('@') {
191 return Ok(None);
192 }
193
194 let invocations = self.extract_invocations(input)?;
196 if invocations.is_empty() {
197 return Ok(None);
198 }
199
200 self.validate_agent_names(&invocations)?;
202
203 let execution_plan = self.build_execution_plan(input, invocations)?;
205
206 self.validate_execution_plan(&execution_plan)?;
208
209 let context = self.extract_context(input);
211
212 Ok(Some(InvocationRequest {
213 id: Uuid::new_v4(),
214 original_input: input.to_string(),
215 execution_plan,
216 context,
217 }))
218 }
219
220 fn extract_invocations(
222 &self,
223 input: &str,
224 ) -> Result<Vec<AgentInvocation>, super::SubagentError> {
225 let mut invocations = Vec::new();
226
227 for captures in self.agent_pattern.captures_iter(input) {
228 let full_match = captures.get(0).unwrap();
229 let agent_name = captures.get(1).unwrap().as_str().to_string();
230 let raw_parameters = captures
231 .get(2)
232 .map(|m| m.as_str().trim().to_string())
233 .unwrap_or_default();
234
235 let parameters = self.parse_parameters(&raw_parameters)?;
236
237 invocations.push(AgentInvocation {
238 agent_name,
239 parameters,
240 raw_parameters,
241 position: full_match.start(),
242 mode_override: None,
243 intelligence_override: None,
244 });
245 }
246
247 invocations.sort_by_key(|inv| inv.position);
249
250 Ok(invocations)
251 }
252
253 fn parse_parameters(
255 &self,
256 param_str: &str,
257 ) -> Result<HashMap<String, String>, super::SubagentError> {
258 let mut parameters = HashMap::new();
259
260 if param_str.is_empty() {
261 return Ok(parameters);
262 }
263
264 let mut current_key = String::new();
268 let mut current_value = String::new();
269 let mut in_quotes = false;
270 let mut in_value = false;
271 let chars = param_str.chars().peekable();
272 let mut positional_index = 0;
273
274 for ch in chars {
275 match ch {
276 '"' if !in_quotes => {
277 in_quotes = true;
278 in_value = true;
279 }
280 '"' if in_quotes => {
281 in_quotes = false;
282 if !current_key.is_empty() {
283 parameters.insert(current_key.clone(), current_value.clone());
284 } else {
285 parameters
286 .insert(format!("arg{}", positional_index), current_value.clone());
287 positional_index += 1;
288 }
289 current_key.clear();
290 current_value.clear();
291 in_value = false;
292 }
293 '=' if !in_quotes && !in_value => {
294 in_value = true;
295 }
296 ' ' if !in_quotes && !in_value => {
297 if !current_key.is_empty() {
298 parameters.insert(format!("arg{}", positional_index), current_key.clone());
300 positional_index += 1;
301 current_key.clear();
302 }
303 }
304 ' ' if !in_quotes && in_value => {
305 if !current_key.is_empty() {
306 parameters.insert(current_key.clone(), current_value.clone());
307 } else {
308 parameters
309 .insert(format!("arg{}", positional_index), current_value.clone());
310 positional_index += 1;
311 }
312 current_key.clear();
313 current_value.clear();
314 in_value = false;
315 }
316 _ => {
317 if in_value {
318 current_value.push(ch);
319 } else {
320 current_key.push(ch);
321 }
322 }
323 }
324 }
325
326 if !current_key.is_empty() || !current_value.is_empty() {
328 if in_value && !current_key.is_empty() {
329 parameters.insert(current_key, current_value);
330 } else if !current_key.is_empty() {
331 parameters.insert(format!("arg{}", positional_index), current_key);
332 } else if !current_value.is_empty() {
333 parameters.insert(format!("arg{}", positional_index), current_value);
334 }
335 }
336
337 Ok(parameters)
338 }
339
340 fn build_execution_plan(
342 &self,
343 input: &str,
344 invocations: Vec<AgentInvocation>,
345 ) -> Result<ExecutionPlan, super::SubagentError> {
346 if self.conditional_pattern.is_match(input) {
348 return self.build_conditional_execution_plan(input, invocations);
349 }
350
351 if invocations.len() == 1 {
352 return Ok(ExecutionPlan::Single(
353 invocations.into_iter().next().unwrap(),
354 ));
355 }
356
357 if self.chain_pattern.is_match(input) {
359 return Ok(ExecutionPlan::Sequential(AgentChain {
360 agents: invocations,
361 pass_output: true,
362 }));
363 }
364
365 if self.parallel_pattern.is_match(input) {
367 return Ok(ExecutionPlan::Parallel(invocations));
368 }
369
370 if (input.contains('→') && input.contains('+')) || input.contains(" if ") {
372 return self.build_mixed_execution_plan(input, invocations);
373 }
374
375 Ok(ExecutionPlan::Parallel(invocations))
377 }
378
379 fn build_mixed_execution_plan(
381 &self,
382 input: &str,
383 invocations: Vec<AgentInvocation>,
384 ) -> Result<ExecutionPlan, super::SubagentError> {
385 let mut steps = Vec::new();
389 let mut current_parallel = Vec::new();
390
391 for invocation in invocations {
392 let next_pos = invocation.position + invocation.agent_name.len();
394 let remaining = &input[next_pos..];
395
396 if remaining.trim_start().starts_with('→') {
397 current_parallel.push(invocation);
399 if current_parallel.len() == 1 {
400 steps.push(ExecutionStep::Single(current_parallel.pop().unwrap()));
401 } else {
402 steps.push(ExecutionStep::Parallel(current_parallel.clone()));
403 }
404 current_parallel.clear();
405 steps.push(ExecutionStep::Barrier);
406 } else {
407 current_parallel.push(invocation);
408 }
409 }
410
411 if !current_parallel.is_empty() {
413 if current_parallel.len() == 1 {
414 steps.push(ExecutionStep::Single(current_parallel.pop().unwrap()));
415 } else {
416 steps.push(ExecutionStep::Parallel(current_parallel));
417 }
418 }
419
420 Ok(ExecutionPlan::Mixed(steps))
421 }
422
423 fn build_conditional_execution_plan(
425 &self,
426 input: &str,
427 invocations: Vec<AgentInvocation>,
428 ) -> Result<ExecutionPlan, super::SubagentError> {
429 let mut conditional_agents = Vec::new();
430 let mut condition = ExecutionCondition::OnError; let mut condition_params = HashMap::new();
432
433 for captures in self.conditional_pattern.captures_iter(input) {
435 let agent_name = captures.get(1).unwrap().as_str().to_string();
436 let raw_parameters = captures
437 .get(2)
438 .map(|m| m.as_str().trim().to_string())
439 .unwrap_or_default();
440 let condition_text = captures.get(3).unwrap().as_str().trim();
441
442 let parameters = self.parse_parameters(&raw_parameters)?;
443
444 let (parsed_condition, parsed_params) = self.parse_condition(condition_text)?;
446 condition = parsed_condition;
447 condition_params = parsed_params;
448
449 conditional_agents.push(AgentInvocation {
450 agent_name,
451 parameters,
452 raw_parameters,
453 position: captures.get(0).unwrap().start(),
454 mode_override: None,
455 intelligence_override: None,
456 });
457 }
458
459 if conditional_agents.is_empty() {
460 return self.build_execution_plan_without_conditionals(input, invocations);
462 }
463
464 Ok(ExecutionPlan::Conditional(ConditionalExecution {
465 agents: conditional_agents,
466 condition,
467 condition_params,
468 }))
469 }
470
471 fn build_execution_plan_without_conditionals(
473 &self,
474 input: &str,
475 invocations: Vec<AgentInvocation>,
476 ) -> Result<ExecutionPlan, super::SubagentError> {
477 if invocations.len() == 1 {
478 return Ok(ExecutionPlan::Single(
479 invocations.into_iter().next().unwrap(),
480 ));
481 }
482
483 if self.chain_pattern.is_match(input) {
485 return Ok(ExecutionPlan::Sequential(AgentChain {
486 agents: invocations,
487 pass_output: true,
488 }));
489 }
490
491 if self.parallel_pattern.is_match(input) {
493 return Ok(ExecutionPlan::Parallel(invocations));
494 }
495
496 if input.contains('→') && input.contains('+') {
498 return self.build_mixed_execution_plan(input, invocations);
499 }
500
501 Ok(ExecutionPlan::Parallel(invocations))
503 }
504
505 fn parse_condition(
507 &self,
508 condition_text: &str,
509 ) -> Result<(ExecutionCondition, HashMap<String, String>), super::SubagentError> {
510 let mut params = HashMap::new();
511 let condition_text = condition_text.trim().to_lowercase();
512
513 let condition = match condition_text.as_str() {
514 "error" | "errors" | "failed" | "failure" => ExecutionCondition::OnError,
515 "success" | "succeeded" | "passed" => ExecutionCondition::OnSuccess,
516 "test failure" | "test failures" | "tests fail" => ExecutionCondition::OnTestFailure,
517 text if text.starts_with("file error") => {
518 let pattern = text
520 .strip_prefix("file error")
521 .unwrap_or("")
522 .trim()
523 .strip_prefix("in")
524 .unwrap_or("*")
525 .trim();
526 params.insert("pattern".to_string(), pattern.to_string());
527 ExecutionCondition::OnFileError
528 }
529 text if text.starts_with("pattern")
530 || text.ends_with(".rs")
531 || text.ends_with(".py")
532 || text.ends_with(".js")
533 || text.ends_with(".ts") =>
534 {
535 let pattern = if text.starts_with("pattern ") {
537 text.strip_prefix("pattern ").unwrap_or(text)
538 } else {
539 text
540 };
541 ExecutionCondition::OnFilePattern(pattern.to_string())
542 }
543 _ => {
544 params.insert("expression".to_string(), condition_text.to_string());
546 ExecutionCondition::Custom(condition_text.to_string())
547 }
548 };
549
550 Ok((condition, params))
551 }
552
553 fn validate_agent_names(
555 &self,
556 invocations: &[AgentInvocation],
557 ) -> Result<(), super::SubagentError> {
558 if let Some(ref registry) = self.registry {
559 for invocation in invocations {
560 if registry.get_agent(&invocation.agent_name).is_none() {
561 return Err(super::SubagentError::AgentNotFound {
562 name: invocation.agent_name.clone(),
563 });
564 }
565 }
566 }
567 Ok(())
568 }
569
570 fn extract_context(&self, input: &str) -> String {
572 let mut context = input.to_string();
573
574 context = self
577 .simple_agent_pattern
578 .replace_all(&context, "")
579 .to_string();
580
581 context = context.replace('→', " ");
583 context = context.replace('+', " ");
584
585 context = self
587 .multiple_spaces_pattern
588 .replace_all(&context, " ")
589 .to_string();
590
591 context = context
593 .lines()
594 .map(|line| line.trim())
595 .filter(|line| !line.is_empty())
596 .collect::<Vec<_>>()
597 .join(" ");
598
599 context.trim().to_string()
600 }
601
602 pub fn validate_execution_plan(
604 &self,
605 plan: &ExecutionPlan,
606 ) -> Result<(), super::SubagentError> {
607 let agents: Vec<&String> = match plan {
608 ExecutionPlan::Single(_) => return Ok(()), ExecutionPlan::Sequential(chain) => {
610 chain.agents.iter().map(|a| &a.agent_name).collect()
611 }
612 ExecutionPlan::Parallel(_) => return Ok(()), ExecutionPlan::Conditional(cond) => {
614 cond.agents.iter().map(|inv| &inv.agent_name).collect()
615 }
616 ExecutionPlan::Mixed(steps) => {
617 steps
619 .iter()
620 .flat_map(|step| match step {
621 ExecutionStep::Single(inv) => vec![&inv.agent_name],
622 ExecutionStep::Parallel(invs) => {
623 invs.iter().map(|inv| &inv.agent_name).collect()
624 }
625 ExecutionStep::Conditional(cond) => {
626 cond.agents.iter().map(|inv| &inv.agent_name).collect()
627 }
628 ExecutionStep::Barrier => vec![],
629 })
630 .collect()
631 }
632 };
633
634 if let ExecutionPlan::Sequential(_) = plan {
636 let mut seen = std::collections::HashSet::new();
637 for agent_name in &agents {
638 if !seen.insert(agent_name) {
639 return Err(super::SubagentError::CircularDependency {
640 chain: agents.into_iter().map(|s| s.to_string()).collect(),
641 });
642 }
643 }
644 }
645
646 Ok(())
647 }
648}
649
650#[cfg(test)]
651mod tests {
652 use super::*;
653
654 #[test]
655 fn test_single_agent_parsing() {
656 let parser = InvocationParser::new();
657 let result = parser
658 .parse("@code-reviewer check this file")
659 .unwrap()
660 .unwrap();
661
662 match result.execution_plan {
663 ExecutionPlan::Single(inv) => {
664 assert_eq!(inv.agent_name, "code-reviewer");
665 assert_eq!(inv.raw_parameters, "check this file");
666 }
667 _ => panic!("Expected single execution plan"),
668 }
669 }
670
671 #[test]
672 fn test_sequential_chain_parsing() {
673 let parser = InvocationParser::new();
674 let result = parser
675 .parse("@refactorer fix → @test-writer add tests")
676 .unwrap()
677 .unwrap();
678
679 match result.execution_plan {
680 ExecutionPlan::Sequential(chain) => {
681 assert_eq!(chain.agents.len(), 2);
682 assert_eq!(chain.agents[0].agent_name, "refactorer");
683 assert_eq!(chain.agents[1].agent_name, "test-writer");
684 assert!(chain.pass_output);
685 }
686 _ => panic!("Expected sequential execution plan"),
687 }
688 }
689
690 #[test]
691 fn test_parallel_execution_parsing() {
692 let parser = InvocationParser::new();
693 let result = parser
694 .parse("@performance analyze + @security audit")
695 .unwrap()
696 .unwrap();
697
698 match result.execution_plan {
699 ExecutionPlan::Parallel(agents) => {
700 assert_eq!(agents.len(), 2);
701 assert_eq!(agents[0].agent_name, "performance");
702 assert_eq!(agents[1].agent_name, "security");
703 }
704 _ => panic!("Expected parallel execution plan"),
705 }
706 }
707
708 #[test]
709 fn test_parameter_parsing() {
710 let parser = InvocationParser::new();
711
712 let params = parser
714 .parse_parameters("file=src/main.rs level=high")
715 .unwrap();
716 assert_eq!(params.get("file").unwrap(), "src/main.rs");
717 assert_eq!(params.get("level").unwrap(), "high");
718
719 let params = parser
721 .parse_parameters(r#"message="fix this bug" priority=1"#)
722 .unwrap();
723 assert_eq!(params.get("message").unwrap(), "fix this bug");
724 assert_eq!(params.get("priority").unwrap(), "1");
725
726 let params = parser.parse_parameters("src/main.rs high").unwrap();
728 assert_eq!(params.get("arg0").unwrap(), "src/main.rs");
729 assert_eq!(params.get("arg1").unwrap(), "high");
730 }
731
732 #[test]
733 fn test_context_extraction() {
734 let parser = InvocationParser::new();
735 let result = parser.parse("Please @code-reviewer this file and then @test-writer. Make sure everything works.").unwrap().unwrap();
736
737 assert_eq!(
738 result.context,
739 "Please this file and then . Make sure everything works."
740 );
741 }
742
743 #[test]
744 fn test_no_agents() {
745 let parser = InvocationParser::new();
746 let result = parser
747 .parse("This is just regular text with no agents.")
748 .unwrap();
749 assert!(result.is_none());
750 }
751
752 #[test]
753 fn test_conditional_parsing() {
754 let parser = InvocationParser::new();
755 let result = parser.parse("@debugger if errors").unwrap().unwrap();
756
757 match result.execution_plan {
758 ExecutionPlan::Conditional(cond) => {
759 assert_eq!(cond.agents.len(), 1);
760 assert_eq!(cond.agents[0].agent_name, "debugger");
761 assert!(matches!(cond.condition, ExecutionCondition::OnError));
762 }
763 _ => panic!("Expected conditional execution plan"),
764 }
765 }
766
767 #[test]
768 fn test_file_pattern_conditional() {
769 let parser = InvocationParser::new();
770 let result = parser.parse("@security-scanner if *.rs").unwrap().unwrap();
771
772 match result.execution_plan {
773 ExecutionPlan::Conditional(cond) => {
774 assert_eq!(cond.agents.len(), 1);
775 assert_eq!(cond.agents[0].agent_name, "security-scanner");
776 assert!(matches!(
777 cond.condition,
778 ExecutionCondition::OnFilePattern(_)
779 ));
780 if let ExecutionCondition::OnFilePattern(pattern) = &cond.condition {
781 assert_eq!(pattern, "*.rs");
782 }
783 }
784 _ => panic!("Expected conditional execution plan"),
785 }
786 }
787
788 #[test]
789 fn test_complex_conditional_parsing() {
790 let parser = InvocationParser::new();
791 let result = parser
792 .parse("@performance analyze → @debugger if errors → @refactorer")
793 .unwrap()
794 .unwrap();
795
796 match result.execution_plan {
798 ExecutionPlan::Mixed(_) => {
799 }
801 _ => {
802 }
804 }
805 }
806
807 #[test]
808 fn test_condition_parsing() {
809 let parser = InvocationParser::new();
810
811 let (condition, _) = parser.parse_condition("errors").unwrap();
813 assert!(matches!(condition, ExecutionCondition::OnError));
814
815 let (condition, _) = parser.parse_condition("success").unwrap();
817 assert!(matches!(condition, ExecutionCondition::OnSuccess));
818
819 let (condition, _) = parser.parse_condition("*.rs").unwrap();
821 assert!(matches!(condition, ExecutionCondition::OnFilePattern(_)));
822
823 let (condition, params) = parser.parse_condition("custom logic").unwrap();
825 assert!(matches!(condition, ExecutionCondition::Custom(_)));
826 assert_eq!(params.get("expression").unwrap(), "custom logic");
827 }
828
829 #[test]
830 fn test_registry_validation() {
831 let parser = InvocationParser::new();
834 let invocations = vec![AgentInvocation {
835 agent_name: "test-agent".to_string(),
836 parameters: HashMap::new(),
837 raw_parameters: String::new(),
838 position: 0,
839 mode_override: None,
840 intelligence_override: None,
841 }];
842
843 let result = parser.validate_agent_names(&invocations);
844 assert!(result.is_ok()); }
846
847 #[test]
848 fn test_circular_dependency_detection() {
849 let parser = InvocationParser::new();
850 let chain = AgentChain {
851 agents: vec![
852 AgentInvocation {
853 agent_name: "agent1".to_string(),
854 parameters: HashMap::new(),
855 raw_parameters: String::new(),
856 position: 0,
857 mode_override: None,
858 intelligence_override: None,
859 },
860 AgentInvocation {
861 agent_name: "agent1".to_string(), parameters: HashMap::new(),
863 raw_parameters: String::new(),
864 position: 1,
865 mode_override: None,
866 intelligence_override: None,
867 },
868 ],
869 pass_output: true,
870 };
871
872 let plan = ExecutionPlan::Sequential(chain);
873 let result = parser.validate_execution_plan(&plan);
874 assert!(result.is_err());
875 assert!(matches!(
876 result.unwrap_err(),
877 super::super::SubagentError::CircularDependency { .. }
878 ));
879 }
880}