1use crate::config::CodeConfig;
58use crate::permissions::PermissionPolicy;
59use serde::{Deserialize, Serialize};
60use std::collections::HashMap;
61use std::path::Path;
62use std::sync::RwLock;
63
64use crate::error::{read_or_recover, write_or_recover};
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct ModelConfig {
69 pub model: String,
71 pub provider: Option<String>,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct AgentDefinition {
80 pub name: String,
82 pub description: String,
84 #[serde(default)]
86 pub native: bool,
87 #[serde(default)]
89 pub hidden: bool,
90 #[serde(default)]
92 pub permissions: PermissionPolicy,
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub model: Option<ModelConfig>,
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub prompt: Option<String>,
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub max_steps: Option<usize>,
102}
103
104impl AgentDefinition {
105 pub fn new(name: &str, description: &str) -> Self {
107 Self {
108 name: name.to_string(),
109 description: description.to_string(),
110 native: false,
111 hidden: false,
112 permissions: PermissionPolicy::default(),
113 model: None,
114 prompt: None,
115 max_steps: None,
116 }
117 }
118
119 pub fn native(mut self) -> Self {
121 self.native = true;
122 self
123 }
124
125 pub fn hidden(mut self) -> Self {
127 self.hidden = true;
128 self
129 }
130
131 pub fn with_permissions(mut self, permissions: PermissionPolicy) -> Self {
133 self.permissions = permissions;
134 self
135 }
136
137 pub fn with_model(mut self, model: ModelConfig) -> Self {
139 self.model = Some(model);
140 self
141 }
142
143 pub fn with_prompt(mut self, prompt: &str) -> Self {
145 self.prompt = Some(prompt.to_string());
146 self
147 }
148
149 pub fn with_max_steps(mut self, max_steps: usize) -> Self {
151 self.max_steps = Some(max_steps);
152 self
153 }
154}
155
156pub struct AgentRegistry {
161 agents: RwLock<HashMap<String, AgentDefinition>>,
162}
163
164impl Default for AgentRegistry {
165 fn default() -> Self {
166 Self::new()
167 }
168}
169
170impl AgentRegistry {
171 pub fn new() -> Self {
173 let registry = Self {
174 agents: RwLock::new(HashMap::new()),
175 };
176
177 for agent in builtin_agents() {
179 registry.register(agent);
180 }
181
182 registry
183 }
184
185 pub fn with_config(config: &CodeConfig) -> Self {
189 let registry = Self::new();
190
191 for dir in &config.agent_dirs {
193 let agents = load_agents_from_dir(dir);
194 for agent in agents {
195 tracing::info!("Loaded agent '{}' from {}", agent.name, dir.display());
196 registry.register(agent);
197 }
198 }
199
200 registry
201 }
202
203 pub fn register(&self, agent: AgentDefinition) {
205 let mut agents = write_or_recover(&self.agents);
206 tracing::debug!("Registering agent: {}", agent.name);
207 agents.insert(agent.name.clone(), agent);
208 }
209
210 pub fn unregister(&self, name: &str) -> bool {
214 let mut agents = write_or_recover(&self.agents);
215 agents.remove(name).is_some()
216 }
217
218 pub fn get(&self, name: &str) -> Option<AgentDefinition> {
220 let agents = read_or_recover(&self.agents);
221 agents.get(name).cloned()
222 }
223
224 pub fn list(&self) -> Vec<AgentDefinition> {
226 let agents = read_or_recover(&self.agents);
227 agents.values().cloned().collect()
228 }
229
230 pub fn list_visible(&self) -> Vec<AgentDefinition> {
232 let agents = read_or_recover(&self.agents);
233 agents.values().filter(|a| !a.hidden).cloned().collect()
234 }
235
236 pub fn exists(&self, name: &str) -> bool {
238 let agents = read_or_recover(&self.agents);
239 agents.contains_key(name)
240 }
241
242 pub fn len(&self) -> usize {
244 let agents = read_or_recover(&self.agents);
245 agents.len()
246 }
247
248 pub fn is_empty(&self) -> bool {
250 self.len() == 0
251 }
252}
253
254pub fn parse_agent_yaml(content: &str) -> anyhow::Result<AgentDefinition> {
262 let agent: AgentDefinition = serde_yaml::from_str(content)
263 .map_err(|e| anyhow::anyhow!("Failed to parse agent YAML: {}", e))?;
264
265 if agent.name.is_empty() {
266 return Err(anyhow::anyhow!("Agent name is required"));
267 }
268
269 Ok(agent)
270}
271
272pub fn parse_agent_md(content: &str) -> anyhow::Result<AgentDefinition> {
276 let parts: Vec<&str> = content.splitn(3, "---").collect();
278
279 if parts.len() < 3 {
280 return Err(anyhow::anyhow!(
281 "Invalid markdown format: missing YAML frontmatter"
282 ));
283 }
284
285 let frontmatter = parts[1].trim();
286 let body = parts[2].trim();
287
288 let mut agent: AgentDefinition = serde_yaml::from_str(frontmatter)
290 .map_err(|e| anyhow::anyhow!("Failed to parse agent frontmatter: {}", e))?;
291
292 if agent.name.is_empty() {
293 return Err(anyhow::anyhow!("Agent name is required"));
294 }
295
296 if agent.prompt.is_none() && !body.is_empty() {
298 agent.prompt = Some(body.to_string());
299 }
300
301 Ok(agent)
302}
303
304pub fn load_agents_from_dir(dir: &Path) -> Vec<AgentDefinition> {
309 let mut agents = Vec::new();
310
311 let Ok(entries) = std::fs::read_dir(dir) else {
312 tracing::warn!("Failed to read agent directory: {}", dir.display());
313 return agents;
314 };
315
316 for entry in entries.flatten() {
317 let path = entry.path();
318
319 if !path.is_file() {
321 continue;
322 }
323
324 let Some(ext) = path.extension().and_then(|e| e.to_str()) else {
325 continue;
326 };
327
328 let Ok(content) = std::fs::read_to_string(&path) else {
330 tracing::warn!("Failed to read agent file: {}", path.display());
331 continue;
332 };
333
334 let result = match ext {
336 "yaml" | "yml" => parse_agent_yaml(&content),
337 "md" => parse_agent_md(&content),
338 _ => continue,
339 };
340
341 match result {
342 Ok(agent) => {
343 tracing::debug!("Loaded agent '{}' from {}", agent.name, path.display());
344 agents.push(agent);
345 }
346 Err(e) => {
347 tracing::warn!("Failed to parse agent file {}: {}", path.display(), e);
348 }
349 }
350 }
351
352 agents
353}
354
355pub fn builtin_agents() -> Vec<AgentDefinition> {
357 vec![
358 AgentDefinition::new(
360 "explore",
361 "Fast codebase exploration agent. Use for searching files, reading code, \
362 and understanding codebase structure. Read-only operations only.",
363 )
364 .native()
365 .with_permissions(explore_permissions())
366 .with_max_steps(20)
367 .with_prompt(EXPLORE_PROMPT),
368 AgentDefinition::new(
370 "general",
371 "General-purpose agent for multi-step task execution. Can read, write, \
372 and execute commands.",
373 )
374 .native()
375 .with_permissions(general_permissions())
376 .with_max_steps(50),
377 AgentDefinition::new(
379 "plan",
380 "Planning agent for designing implementation approaches. Read-only access \
381 to explore codebase and create plans.",
382 )
383 .native()
384 .with_permissions(plan_permissions())
385 .with_max_steps(30)
386 .with_prompt(PLAN_PROMPT),
387 AgentDefinition::new(
389 "verification",
390 "Verification agent for adversarial validation. Prefer real checks, \
391 reproductions, and regression testing over code reading alone.",
392 )
393 .native()
394 .with_permissions(verification_permissions())
395 .with_max_steps(30)
396 .with_prompt(VERIFICATION_PROMPT),
397 AgentDefinition::new(
399 "review",
400 "Code review agent focused on correctness, regressions, security, \
401 maintainability, and clear findings.",
402 )
403 .native()
404 .with_permissions(review_permissions())
405 .with_max_steps(25)
406 .with_prompt(REVIEW_PROMPT),
407 ]
408}
409
410fn explore_permissions() -> PermissionPolicy {
416 PermissionPolicy::new()
417 .allow_all(&["read", "grep", "glob", "ls"])
418 .deny_all(&["write", "edit", "task"])
419 .allow("Bash(ls:*)")
420 .allow("Bash(cat:*)")
421 .allow("Bash(head:*)")
422 .allow("Bash(tail:*)")
423 .allow("Bash(find:*)")
424 .allow("Bash(wc:*)")
425 .deny("Bash(rm:*)")
426 .deny("Bash(mv:*)")
427 .deny("Bash(cp:*)")
428}
429
430fn general_permissions() -> PermissionPolicy {
432 PermissionPolicy::new()
433 .allow_all(&["read", "write", "edit", "grep", "glob", "ls", "bash"])
434 .deny("task")
435}
436
437fn plan_permissions() -> PermissionPolicy {
439 PermissionPolicy::new()
440 .allow_all(&["read", "grep", "glob", "ls"])
441 .deny_all(&["write", "edit", "bash", "task"])
442}
443
444fn verification_permissions() -> PermissionPolicy {
446 PermissionPolicy::new()
447 .allow_all(&["read", "grep", "glob", "ls", "bash"])
448 .deny_all(&["write", "edit", "task"])
449}
450
451fn review_permissions() -> PermissionPolicy {
453 PermissionPolicy::new()
454 .allow_all(&["read", "grep", "glob", "ls", "bash"])
455 .deny_all(&["write", "edit", "task"])
456}
457
458const EXPLORE_PROMPT: &str = crate::prompts::AGENT_EXPLORE;
463
464const PLAN_PROMPT: &str = crate::prompts::AGENT_PLAN;
465
466const VERIFICATION_PROMPT: &str = crate::prompts::AGENT_VERIFICATION;
467
468const REVIEW_PROMPT: &str = crate::prompts::AGENT_CODE_REVIEW;
469
470#[cfg(test)]
475mod tests {
476 use super::*;
477
478 #[test]
479 fn test_agent_definition_builder() {
480 let agent = AgentDefinition::new("test", "Test agent")
481 .native()
482 .hidden()
483 .with_max_steps(10);
484
485 assert_eq!(agent.name, "test");
486 assert_eq!(agent.description, "Test agent");
487 assert!(agent.native);
488 assert!(agent.hidden);
489 assert_eq!(agent.max_steps, Some(10));
490 }
491
492 #[test]
493 fn test_agent_registry_new() {
494 let registry = AgentRegistry::new();
495
496 assert!(registry.exists("explore"));
498 assert!(registry.exists("general"));
499 assert!(registry.exists("plan"));
500 assert!(registry.exists("verification"));
501 assert!(registry.exists("review"));
502 assert_eq!(registry.len(), 5);
503 }
504
505 #[test]
506 fn test_agent_registry_get() {
507 let registry = AgentRegistry::new();
508
509 let explore = registry.get("explore").unwrap();
510 assert_eq!(explore.name, "explore");
511 assert!(explore.native);
512 assert!(!explore.hidden);
513
514 assert!(registry.get("nonexistent").is_none());
515 }
516
517 #[test]
518 fn test_agent_registry_register_unregister() {
519 let registry = AgentRegistry::new();
520 let initial_count = registry.len();
521
522 let custom = AgentDefinition::new("custom", "Custom agent");
524 registry.register(custom);
525 assert_eq!(registry.len(), initial_count + 1);
526 assert!(registry.exists("custom"));
527
528 assert!(registry.unregister("custom"));
530 assert_eq!(registry.len(), initial_count);
531 assert!(!registry.exists("custom"));
532
533 assert!(!registry.unregister("nonexistent"));
535 }
536
537 #[test]
538 fn test_agent_registry_list_visible() {
539 let registry = AgentRegistry::new();
540
541 let visible = registry.list_visible();
542 let all = registry.list();
543
544 assert_eq!(visible.len(), all.len());
545 assert!(visible.iter().all(|a| !a.hidden));
546 }
547
548 #[test]
549 fn test_builtin_agents() {
550 let agents = builtin_agents();
551
552 let names: Vec<&str> = agents.iter().map(|a| a.name.as_str()).collect();
554 assert!(names.contains(&"explore"));
555 assert!(names.contains(&"general"));
556 assert!(names.contains(&"plan"));
557 assert!(names.contains(&"verification"));
558 assert!(names.contains(&"review"));
559
560 let explore = agents.iter().find(|a| a.name == "explore").unwrap();
562 assert!(!explore.permissions.deny.is_empty());
563 }
564
565 #[test]
570 fn test_parse_agent_yaml() {
571 let yaml = r#"
572name: test-agent
573description: A test agent
574hidden: false
575max_steps: 20
576"#;
577 let agent = parse_agent_yaml(yaml).unwrap();
578 assert_eq!(agent.name, "test-agent");
579 assert_eq!(agent.description, "A test agent");
580 assert!(!agent.hidden);
581 assert_eq!(agent.max_steps, Some(20));
582 }
583
584 #[test]
585 fn test_parse_agent_yaml_with_permissions() {
586 let yaml = r#"
587name: restricted-agent
588description: Agent with permissions
589permissions:
590 allow:
591 - rule: read
592 - rule: grep
593 deny:
594 - rule: write
595"#;
596 let agent = parse_agent_yaml(yaml).unwrap();
597 assert_eq!(agent.name, "restricted-agent");
598 assert_eq!(agent.permissions.allow.len(), 2);
599 assert_eq!(agent.permissions.deny.len(), 1);
600 assert!(agent.permissions.allow[0].matches("read", &serde_json::json!({})));
602 assert!(agent.permissions.allow[1].matches("grep", &serde_json::json!({})));
603 assert!(agent.permissions.deny[0].matches("write", &serde_json::json!({})));
604 }
605
606 #[test]
607 fn test_parse_agent_yaml_with_plain_string_permissions() {
608 let yaml = r#"
610name: plain-agent
611description: Agent with plain string permissions
612permissions:
613 allow:
614 - read
615 - grep
616 - "Bash(cargo:*)"
617 deny:
618 - write
619"#;
620 let agent = parse_agent_yaml(yaml).unwrap();
621 assert_eq!(agent.name, "plain-agent");
622 assert_eq!(agent.permissions.allow.len(), 3);
623 assert_eq!(agent.permissions.deny.len(), 1);
624 assert!(agent.permissions.allow[0].matches("read", &serde_json::json!({})));
626 assert!(agent.permissions.allow[1].matches("grep", &serde_json::json!({})));
627 assert!(agent.permissions.allow[2]
628 .matches("Bash", &serde_json::json!({"command": "cargo build"})));
629 assert!(agent.permissions.deny[0].matches("write", &serde_json::json!({})));
630 }
631
632 #[test]
633 fn test_parse_agent_yaml_missing_name() {
634 let yaml = r#"
635description: Agent without name
636"#;
637 let result = parse_agent_yaml(yaml);
638 assert!(result.is_err());
639 }
640
641 #[test]
642 fn test_parse_agent_md() {
643 let md = r#"---
644name: md-agent
645description: Agent from markdown
646max_steps: 15
647---
648# System Prompt
649
650You are a helpful agent.
651Do your best work.
652"#;
653 let agent = parse_agent_md(md).unwrap();
654 assert_eq!(agent.name, "md-agent");
655 assert_eq!(agent.description, "Agent from markdown");
656 assert_eq!(agent.max_steps, Some(15));
657 assert!(agent.prompt.is_some());
658 assert!(agent.prompt.unwrap().contains("helpful agent"));
659 }
660
661 #[test]
662 fn test_parse_agent_md_with_prompt_in_frontmatter() {
663 let md = r#"---
664name: prompt-agent
665description: Agent with prompt in frontmatter
666prompt: "Frontmatter prompt"
667---
668Body content that should be ignored
669"#;
670 let agent = parse_agent_md(md).unwrap();
671 assert_eq!(agent.prompt.unwrap(), "Frontmatter prompt");
672 }
673
674 #[test]
675 fn test_parse_agent_md_missing_frontmatter() {
676 let md = "Just markdown without frontmatter";
677 let result = parse_agent_md(md);
678 assert!(result.is_err());
679 }
680
681 #[test]
682 fn test_load_agents_from_dir() {
683 let temp_dir = tempfile::tempdir().unwrap();
684
685 std::fs::write(
687 temp_dir.path().join("agent1.yaml"),
688 r#"
689name: yaml-agent
690description: Agent from YAML file
691"#,
692 )
693 .unwrap();
694
695 std::fs::write(
697 temp_dir.path().join("agent2.md"),
698 r#"---
699name: md-agent
700description: Agent from Markdown file
701---
702System prompt here
703"#,
704 )
705 .unwrap();
706
707 std::fs::write(temp_dir.path().join("invalid.yaml"), "not: valid: yaml: [").unwrap();
709
710 std::fs::write(temp_dir.path().join("readme.txt"), "Just a text file").unwrap();
712
713 let agents = load_agents_from_dir(temp_dir.path());
714 assert_eq!(agents.len(), 2);
715
716 let names: Vec<&str> = agents.iter().map(|a| a.name.as_str()).collect();
717 assert!(names.contains(&"yaml-agent"));
718 assert!(names.contains(&"md-agent"));
719 }
720
721 #[test]
722 fn test_load_agents_from_nonexistent_dir() {
723 let agents = load_agents_from_dir(std::path::Path::new("/nonexistent/dir"));
724 assert!(agents.is_empty());
725 }
726
727 #[test]
728 fn test_registry_with_config() {
729 let temp_dir = tempfile::tempdir().unwrap();
730
731 std::fs::write(
733 temp_dir.path().join("custom.yaml"),
734 r#"
735name: custom-agent
736description: Custom agent from config
737"#,
738 )
739 .unwrap();
740
741 let config = CodeConfig::new().add_agent_dir(temp_dir.path());
742 let registry = AgentRegistry::with_config(&config);
743
744 assert!(registry.exists("explore"));
746 assert!(registry.exists("custom-agent"));
747 assert_eq!(registry.len(), 6); }
749
750 #[test]
751 fn test_agent_definition_with_model() {
752 let model = ModelConfig {
753 model: "claude-3-5-sonnet".to_string(),
754 provider: Some("anthropic".to_string()),
755 };
756 let agent = AgentDefinition::new("test", "Test").with_model(model);
757 assert!(agent.model.is_some());
758 assert_eq!(agent.model.unwrap().provider, Some("anthropic".to_string()));
759 }
760
761 #[test]
762 fn test_agent_registry_default() {
763 let registry = AgentRegistry::default();
764 assert!(!registry.is_empty());
765 assert_eq!(registry.len(), 5);
766 }
767
768 #[test]
769 fn test_agent_registry_is_empty() {
770 let registry = AgentRegistry {
771 agents: RwLock::new(HashMap::new()),
772 };
773 assert!(registry.is_empty());
774 assert_eq!(registry.len(), 0);
775 }
776}