agcodex_core/subagents/
invocation.rs

1//! Agent invocation parsing and execution planning
2//!
3//! This module handles parsing user input for agent invocations and
4//! building execution plans for sequential and parallel agent execution.
5
6use regex_lite::Regex;
7use std::collections::HashMap;
8use uuid::Uuid;
9
10/// A request to invoke one or more subagents
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct InvocationRequest {
13    /// Unique identifier for this invocation request
14    pub id: Uuid,
15
16    /// The original user input that triggered this invocation
17    pub original_input: String,
18
19    /// The execution plan (chain or parallel)
20    pub execution_plan: ExecutionPlan,
21
22    /// Additional context extracted from the input
23    pub context: String,
24}
25
26/// Execution plan for agents
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum ExecutionPlan {
29    /// Single agent execution
30    Single(AgentInvocation),
31    /// Sequential execution (agent1 → agent2 → agent3)
32    Sequential(AgentChain),
33    /// Parallel execution (agent1 + agent2 + agent3)
34    Parallel(Vec<AgentInvocation>),
35    /// Conditional execution (agent if condition)
36    Conditional(ConditionalExecution),
37    /// Mixed execution (complex combinations)
38    Mixed(Vec<ExecutionStep>),
39}
40
41/// A single step in a complex execution plan
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum ExecutionStep {
44    /// Execute a single agent
45    Single(AgentInvocation),
46    /// Execute multiple agents in parallel
47    Parallel(Vec<AgentInvocation>),
48    /// Execute agents conditionally
49    Conditional(ConditionalExecution),
50    /// Wait for completion of previous steps
51    Barrier,
52}
53
54/// A single agent invocation
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct AgentInvocation {
57    /// Name of the agent to invoke
58    pub agent_name: String,
59
60    /// Parameters passed to the agent
61    pub parameters: HashMap<String, String>,
62
63    /// Raw parameter string (for complex parsing)
64    pub raw_parameters: String,
65
66    /// Position in the original input
67    pub position: usize,
68
69    /// Override operating mode for this invocation
70    pub mode_override: Option<crate::modes::OperatingMode>,
71
72    /// Override intelligence level for this invocation
73    pub intelligence_override: Option<String>,
74}
75
76/// A chain of agents for sequential execution
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct AgentChain {
79    /// Agents in execution order
80    pub agents: Vec<AgentInvocation>,
81
82    /// Whether to pass output from one agent to the next
83    pub pass_output: bool,
84}
85
86/// Conditional execution configuration
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct ConditionalExecution {
89    /// Agents to execute if condition is met
90    pub agents: Vec<AgentInvocation>,
91
92    /// Condition type
93    pub condition: ExecutionCondition,
94
95    /// Condition parameters
96    pub condition_params: HashMap<String, String>,
97}
98
99/// Supported execution conditions
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub enum ExecutionCondition {
102    /// Execute if previous agent(s) failed
103    OnError,
104    /// Execute if previous agent(s) succeeded
105    OnSuccess,
106    /// Execute if specific files have errors
107    OnFileError,
108    /// Execute if test failures detected
109    OnTestFailure,
110    /// Execute based on file patterns
111    OnFilePattern(String),
112    /// Execute based on custom condition
113    Custom(String),
114}
115
116/// Parser for agent invocation patterns
117pub struct InvocationParser {
118    /// Regex for matching @agent-name patterns
119    agent_pattern: Regex,
120
121    /// Regex for matching sequential chains (→)
122    chain_pattern: Regex,
123
124    /// Regex for matching parallel execution (+)
125    parallel_pattern: Regex,
126
127    /// Regex for matching conditional patterns (if)
128    conditional_pattern: Regex,
129
130    /// Regex for simple agent names (for context extraction)
131    simple_agent_pattern: Regex,
132
133    /// Regex for multiple spaces (for context extraction)
134    multiple_spaces_pattern: Regex,
135
136    /// Registry for validating agent names
137    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    /// Create a new invocation parser
148    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    /// Create a new invocation parser with registry validation
181    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    /// Parse user input for agent invocations
188    pub fn parse(&self, input: &str) -> Result<Option<InvocationRequest>, super::SubagentError> {
189        // Quick check if there are any agent invocations
190        if !input.contains('@') {
191            return Ok(None);
192        }
193
194        // Extract all agent invocations (including conditional ones)
195        let invocations = self.extract_invocations(input)?;
196        if invocations.is_empty() {
197            return Ok(None);
198        }
199
200        // Validate agent names against registry if available
201        self.validate_agent_names(&invocations)?;
202
203        // Determine execution plan
204        let execution_plan = self.build_execution_plan(input, invocations)?;
205
206        // Validate execution plan
207        self.validate_execution_plan(&execution_plan)?;
208
209        // Extract context (text that's not part of agent invocations)
210        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    /// Extract all agent invocations from the input
221    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        // Sort by position to maintain order
248        invocations.sort_by_key(|inv| inv.position);
249
250        Ok(invocations)
251    }
252
253    /// Parse parameters from a parameter string
254    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        // Simple parameter parsing - can be enhanced
265        // Supports: key=value, "quoted values", and positional arguments
266
267        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                        // Positional argument
299                        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        // Handle remaining content
327        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    /// Build execution plan based on the input pattern
341    fn build_execution_plan(
342        &self,
343        input: &str,
344        invocations: Vec<AgentInvocation>,
345    ) -> Result<ExecutionPlan, super::SubagentError> {
346        // Check for conditional patterns first
347        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        // Check for sequential chains (→)
358        if self.chain_pattern.is_match(input) {
359            return Ok(ExecutionPlan::Sequential(AgentChain {
360                agents: invocations,
361                pass_output: true,
362            }));
363        }
364
365        // Check for parallel execution (+)
366        if self.parallel_pattern.is_match(input) {
367            return Ok(ExecutionPlan::Parallel(invocations));
368        }
369
370        // Complex mixed execution (both → and + and if)
371        if (input.contains('→') && input.contains('+')) || input.contains(" if ") {
372            return self.build_mixed_execution_plan(input, invocations);
373        }
374
375        // Default to parallel if multiple agents without explicit operators
376        Ok(ExecutionPlan::Parallel(invocations))
377    }
378
379    /// Build a complex mixed execution plan
380    fn build_mixed_execution_plan(
381        &self,
382        input: &str,
383        invocations: Vec<AgentInvocation>,
384    ) -> Result<ExecutionPlan, super::SubagentError> {
385        // This is a simplified implementation
386        // A full implementation would parse the operators properly
387
388        let mut steps = Vec::new();
389        let mut current_parallel = Vec::new();
390
391        for invocation in invocations {
392            // Check if there's a → after this invocation
393            let next_pos = invocation.position + invocation.agent_name.len();
394            let remaining = &input[next_pos..];
395
396            if remaining.trim_start().starts_with('→') {
397                // This is the end of a parallel group
398                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        // Handle remaining parallel group
412        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    /// Build a conditional execution plan
424    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; // default
431        let mut condition_params = HashMap::new();
432
433        // Parse conditional invocations
434        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            // Parse condition
445            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            // Fall back to regular parsing if no conditional agents found
461            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    /// Build execution plan without conditional support (fallback)
472    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        // Check for sequential chains (→)
484        if self.chain_pattern.is_match(input) {
485            return Ok(ExecutionPlan::Sequential(AgentChain {
486                agents: invocations,
487                pass_output: true,
488            }));
489        }
490
491        // Check for parallel execution (+)
492        if self.parallel_pattern.is_match(input) {
493            return Ok(ExecutionPlan::Parallel(invocations));
494        }
495
496        // Complex mixed execution
497        if input.contains('→') && input.contains('+') {
498            return self.build_mixed_execution_plan(input, invocations);
499        }
500
501        // Default to parallel if multiple agents
502        Ok(ExecutionPlan::Parallel(invocations))
503    }
504
505    /// Parse execution condition from text
506    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                // Parse file patterns from "file error in *.rs"
519                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                // File pattern condition
536                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                // Custom condition
545                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    /// Validate agent names against the registry
554    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    /// Extract context (non-agent text) from the input
571    fn extract_context(&self, input: &str) -> String {
572        let mut context = input.to_string();
573
574        // Remove agent invocations more precisely
575        // First, match @agent-name patterns without their parameters
576        context = self
577            .simple_agent_pattern
578            .replace_all(&context, "")
579            .to_string();
580
581        // Remove chain and parallel operators
582        context = context.replace('→', " ");
583        context = context.replace('+', " ");
584
585        // Collapse multiple spaces into single spaces
586        context = self
587            .multiple_spaces_pattern
588            .replace_all(&context, " ")
589            .to_string();
590
591        // Clean up whitespace
592        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    /// Validate that an execution plan doesn't have circular dependencies
603    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(()), // No cycles possible
609            ExecutionPlan::Sequential(chain) => {
610                chain.agents.iter().map(|a| &a.agent_name).collect()
611            }
612            ExecutionPlan::Parallel(_) => return Ok(()), // No cycles in parallel
613            ExecutionPlan::Conditional(cond) => {
614                cond.agents.iter().map(|inv| &inv.agent_name).collect()
615            }
616            ExecutionPlan::Mixed(steps) => {
617                // Flatten all agent names
618                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        // Check for duplicate agents in sequential execution
635        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        // Test key=value parameters
713        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        // Test quoted values
720        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        // Test positional arguments
727        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        // This should create a mixed execution plan with conditional step
797        match result.execution_plan {
798            ExecutionPlan::Mixed(_) => {
799                // Complex mixed parsing works
800            }
801            _ => {
802                // Fallback to other patterns is also valid
803            }
804        }
805    }
806
807    #[test]
808    fn test_condition_parsing() {
809        let parser = InvocationParser::new();
810
811        // Test error condition
812        let (condition, _) = parser.parse_condition("errors").unwrap();
813        assert!(matches!(condition, ExecutionCondition::OnError));
814
815        // Test success condition
816        let (condition, _) = parser.parse_condition("success").unwrap();
817        assert!(matches!(condition, ExecutionCondition::OnSuccess));
818
819        // Test file pattern condition
820        let (condition, _) = parser.parse_condition("*.rs").unwrap();
821        assert!(matches!(condition, ExecutionCondition::OnFilePattern(_)));
822
823        // Test custom condition
824        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        // This test would require a mock registry
832        // For now, just test that validation passes without a registry
833        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()); // Should pass without registry
845    }
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(), // Duplicate!
862                    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}