agcodex_core/subagents/
parser.rs

1//! Parser module for agent invocations
2//!
3//! This module provides a specialized parser for @agent-name invocations,
4//! working alongside the existing invocation.rs infrastructure.
5
6use super::SubagentError;
7use super::SubagentRegistry;
8use super::invocation::ExecutionPlan;
9use super::invocation::InvocationParser;
10use super::invocation::InvocationRequest;
11use crate::modes::OperatingMode;
12use std::collections::HashMap;
13use std::sync::Arc;
14
15/// Enhanced parser for agent invocations with additional features
16pub struct AgentParser {
17    /// Base invocation parser
18    base_parser: InvocationParser,
19    /// Registry for agent validation
20    _registry: Option<Arc<SubagentRegistry>>,
21    /// Default operating mode
22    default_mode: OperatingMode,
23}
24
25impl AgentParser {
26    /// Create a new agent parser
27    pub fn new() -> Self {
28        Self {
29            base_parser: InvocationParser::new(),
30            _registry: None,
31            default_mode: OperatingMode::Build,
32        }
33    }
34
35    /// Create parser with registry for validation
36    pub fn with_registry(registry: Arc<SubagentRegistry>) -> Self {
37        Self {
38            base_parser: InvocationParser::with_registry(registry.clone()),
39            _registry: Some(registry),
40            default_mode: OperatingMode::Build,
41        }
42    }
43
44    /// Set the default operating mode
45    pub const fn with_default_mode(mut self, mode: OperatingMode) -> Self {
46        self.default_mode = mode;
47        self
48    }
49
50    /// Parse @agent-name invocations from user input
51    ///
52    /// Supports:
53    /// - Single agents: @agent-refactor
54    /// - Sequential chains: @agent-refactor → @agent-test
55    /// - Parallel execution: @agent-security + @agent-performance
56    /// - Mixed execution: @agent-analyze → (@agent-fix + @agent-document)
57    pub fn parse(&self, input: &str) -> Result<Option<ParsedInvocation>, SubagentError> {
58        // Use base parser for core parsing
59        let base_result = self.base_parser.parse(input)?;
60
61        match base_result {
62            Some(request) => {
63                // Enhance with additional parsing features
64                let enhanced = self.enhance_invocation(request)?;
65                Ok(Some(enhanced))
66            }
67            None => Ok(None),
68        }
69    }
70
71    /// Extract agent parameters from input
72    pub fn extract_parameters(&self, input: &str) -> HashMap<String, String> {
73        let mut params = HashMap::new();
74
75        // Extract mode override
76        if input.contains("--mode=plan") {
77            params.insert("mode".to_string(), "plan".to_string());
78        } else if input.contains("--mode=review") {
79            params.insert("mode".to_string(), "review".to_string());
80        } else if input.contains("--mode=build") {
81            params.insert("mode".to_string(), "build".to_string());
82        }
83
84        // Extract intelligence level
85        if input.contains("--intelligence=light") {
86            params.insert("intelligence".to_string(), "light".to_string());
87        } else if input.contains("--intelligence=medium") {
88            params.insert("intelligence".to_string(), "medium".to_string());
89        } else if input.contains("--intelligence=hard") {
90            params.insert("intelligence".to_string(), "hard".to_string());
91        }
92
93        // Extract timeout
94        if let Some(timeout_match) = input.find("--timeout=") {
95            let start = timeout_match + 10;
96            if let Some(end) = input[start..]
97                .find(' ')
98                .map(|i| start + i)
99                .or(Some(input.len()))
100            {
101                let timeout_str = &input[start..end];
102                params.insert("timeout".to_string(), timeout_str.to_string());
103            }
104        }
105
106        params
107    }
108
109    /// Extract context for agents (non-agent text)
110    pub fn extract_context(&self, input: &str) -> String {
111        let mut context = input.to_string();
112
113        // Remove agent invocations
114        let agent_pattern = regex_lite::Regex::new(r"@[a-zA-Z0-9_-]+").unwrap();
115        context = agent_pattern.replace_all(&context, "").to_string();
116
117        // Remove operators
118        context = context.replace('→', " ");
119        context = context.replace('+', " ");
120
121        // Remove parameters
122        context = context.replace(|c: char| c == '-' && context.contains("--"), " ");
123
124        // Clean up whitespace
125        context
126            .split_whitespace()
127            .collect::<Vec<_>>()
128            .join(" ")
129            .trim()
130            .to_string()
131    }
132
133    /// Enhance invocation with additional metadata
134    fn enhance_invocation(
135        &self,
136        request: InvocationRequest,
137    ) -> Result<ParsedInvocation, SubagentError> {
138        let global_params = self.extract_parameters(&request.original_input);
139
140        // Apply mode overrides from parameters
141        let mode = global_params
142            .get("mode")
143            .and_then(|m| match m.as_str() {
144                "plan" => Some(OperatingMode::Plan),
145                "build" => Some(OperatingMode::Build),
146                "review" => Some(OperatingMode::Review),
147                _ => None,
148            })
149            .unwrap_or(self.default_mode);
150
151        // Apply intelligence overrides
152        let intelligence = global_params.get("intelligence").cloned();
153
154        // Apply timeout overrides
155        let timeout = global_params
156            .get("timeout")
157            .and_then(|t| t.parse::<u64>().ok())
158            .map(std::time::Duration::from_secs);
159
160        Ok(ParsedInvocation {
161            id: request.id,
162            original_input: request.original_input,
163            execution_plan: request.execution_plan,
164            context: request.context,
165            mode_override: Some(mode),
166            intelligence_override: intelligence,
167            timeout_override: timeout,
168            global_parameters: global_params,
169        })
170    }
171
172    /// Validate agent chain for circular dependencies
173    pub fn validate_chain(&self, agents: &[String]) -> Result<(), SubagentError> {
174        let mut seen = std::collections::HashSet::new();
175
176        for agent in agents {
177            if !seen.insert(agent.clone()) {
178                return Err(SubagentError::CircularDependency {
179                    chain: agents.to_vec(),
180                });
181            }
182        }
183
184        Ok(())
185    }
186
187    /// Support for chaining operators
188    pub fn parse_chain_operators(&self, input: &str) -> ChainOperator {
189        if input.contains('→') && input.contains('+') {
190            ChainOperator::Mixed
191        } else if input.contains('→') {
192            ChainOperator::Sequential
193        } else if input.contains('+') {
194            ChainOperator::Parallel
195        } else {
196            ChainOperator::Single
197        }
198    }
199}
200
201/// Enhanced parsed invocation with additional metadata
202#[derive(Debug, Clone)]
203pub struct ParsedInvocation {
204    /// Unique identifier
205    pub id: uuid::Uuid,
206    /// Original input string
207    pub original_input: String,
208    /// Execution plan
209    pub execution_plan: ExecutionPlan,
210    /// Extracted context
211    pub context: String,
212    /// Mode override for all agents
213    pub mode_override: Option<OperatingMode>,
214    /// Intelligence level override
215    pub intelligence_override: Option<String>,
216    /// Timeout override
217    pub timeout_override: Option<std::time::Duration>,
218    /// Global parameters
219    pub global_parameters: HashMap<String, String>,
220}
221
222/// Chain operator types
223#[derive(Debug, Clone, PartialEq, Eq)]
224pub enum ChainOperator {
225    Single,
226    Sequential,
227    Parallel,
228    Mixed,
229}
230
231impl Default for AgentParser {
232    fn default() -> Self {
233        Self::new()
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    #[test]
242    fn test_parse_single_agent() {
243        let parser = AgentParser::new();
244        let result = parser.parse("@agent-refactor clean up this code").unwrap();
245
246        assert!(result.is_some());
247        let parsed = result.unwrap();
248        assert!(matches!(parsed.execution_plan, ExecutionPlan::Single(_)));
249    }
250
251    #[test]
252    fn test_parse_sequential_chain() {
253        let parser = AgentParser::new();
254        let result = parser
255            .parse("@agent-refactor → @agent-test → @agent-document")
256            .unwrap();
257
258        assert!(result.is_some());
259        let parsed = result.unwrap();
260        assert!(matches!(
261            parsed.execution_plan,
262            ExecutionPlan::Sequential(_)
263        ));
264    }
265
266    #[test]
267    fn test_parse_parallel_execution() {
268        let parser = AgentParser::new();
269        let result = parser
270            .parse("@agent-security + @agent-performance + @agent-docs")
271            .unwrap();
272
273        assert!(result.is_some());
274        let parsed = result.unwrap();
275        assert!(matches!(parsed.execution_plan, ExecutionPlan::Parallel(_)));
276    }
277
278    #[test]
279    fn test_extract_parameters() {
280        let parser = AgentParser::new();
281        let params = parser
282            .extract_parameters("@agent-refactor --mode=review --intelligence=hard --timeout=300");
283
284        assert_eq!(params.get("mode").unwrap(), "review");
285        assert_eq!(params.get("intelligence").unwrap(), "hard");
286        assert_eq!(params.get("timeout").unwrap(), "300");
287    }
288
289    #[test]
290    fn test_extract_context() {
291        let parser = AgentParser::new();
292        let context = parser
293            .extract_context("Please @agent-refactor this code and then @agent-test it thoroughly");
294
295        assert_eq!(context, "Please this code and then it thoroughly");
296    }
297
298    #[test]
299    fn test_chain_operator_detection() {
300        let parser = AgentParser::new();
301
302        assert_eq!(
303            parser.parse_chain_operators("@agent-refactor"),
304            ChainOperator::Single
305        );
306        assert_eq!(
307            parser.parse_chain_operators("@agent-a → @agent-b"),
308            ChainOperator::Sequential
309        );
310        assert_eq!(
311            parser.parse_chain_operators("@agent-a + @agent-b"),
312            ChainOperator::Parallel
313        );
314        assert_eq!(
315            parser.parse_chain_operators("@agent-a → @agent-b + @agent-c"),
316            ChainOperator::Mixed
317        );
318    }
319
320    #[test]
321    fn test_circular_dependency_detection() {
322        let parser = AgentParser::new();
323
324        // No circular dependency
325        assert!(
326            parser
327                .validate_chain(&["agent-a".to_string(), "agent-b".to_string()])
328                .is_ok()
329        );
330
331        // Circular dependency
332        assert!(
333            parser
334                .validate_chain(&[
335                    "agent-a".to_string(),
336                    "agent-b".to_string(),
337                    "agent-a".to_string(),
338                ])
339                .is_err()
340        );
341    }
342}