agcodex_core/subagents/
parser.rs1use 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
15pub struct AgentParser {
17 base_parser: InvocationParser,
19 _registry: Option<Arc<SubagentRegistry>>,
21 default_mode: OperatingMode,
23}
24
25impl AgentParser {
26 pub fn new() -> Self {
28 Self {
29 base_parser: InvocationParser::new(),
30 _registry: None,
31 default_mode: OperatingMode::Build,
32 }
33 }
34
35 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 pub const fn with_default_mode(mut self, mode: OperatingMode) -> Self {
46 self.default_mode = mode;
47 self
48 }
49
50 pub fn parse(&self, input: &str) -> Result<Option<ParsedInvocation>, SubagentError> {
58 let base_result = self.base_parser.parse(input)?;
60
61 match base_result {
62 Some(request) => {
63 let enhanced = self.enhance_invocation(request)?;
65 Ok(Some(enhanced))
66 }
67 None => Ok(None),
68 }
69 }
70
71 pub fn extract_parameters(&self, input: &str) -> HashMap<String, String> {
73 let mut params = HashMap::new();
74
75 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 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 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 pub fn extract_context(&self, input: &str) -> String {
111 let mut context = input.to_string();
112
113 let agent_pattern = regex_lite::Regex::new(r"@[a-zA-Z0-9_-]+").unwrap();
115 context = agent_pattern.replace_all(&context, "").to_string();
116
117 context = context.replace('→', " ");
119 context = context.replace('+', " ");
120
121 context = context.replace(|c: char| c == '-' && context.contains("--"), " ");
123
124 context
126 .split_whitespace()
127 .collect::<Vec<_>>()
128 .join(" ")
129 .trim()
130 .to_string()
131 }
132
133 fn enhance_invocation(
135 &self,
136 request: InvocationRequest,
137 ) -> Result<ParsedInvocation, SubagentError> {
138 let global_params = self.extract_parameters(&request.original_input);
139
140 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 let intelligence = global_params.get("intelligence").cloned();
153
154 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 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 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#[derive(Debug, Clone)]
203pub struct ParsedInvocation {
204 pub id: uuid::Uuid,
206 pub original_input: String,
208 pub execution_plan: ExecutionPlan,
210 pub context: String,
212 pub mode_override: Option<OperatingMode>,
214 pub intelligence_override: Option<String>,
216 pub timeout_override: Option<std::time::Duration>,
218 pub global_parameters: HashMap<String, String>,
220}
221
222#[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 assert!(
326 parser
327 .validate_chain(&["agent-a".to_string(), "agent-b".to_string()])
328 .is_ok()
329 );
330
331 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}