Skip to main content

a3s_code_core/
subagent.rs

1//! Subagent System
2//!
3//! Provides a system for delegating specialized tasks to focused child agents.
4//! Each subagent runs in an isolated child session with restricted permissions.
5//!
6//! ## Architecture
7//!
8//! ```text
9//! Parent Session
10//!   └── Task Tool
11//!         ├── AgentRegistry (lookup agent definitions)
12//!         └── Child Session (isolated execution)
13//!               ├── Restricted permissions
14//!               ├── Optional model override
15//!               └── Event forwarding to parent
16//! ```
17//!
18//! ## Built-in Agents
19//!
20//! - `explore`: Fast codebase exploration (read-only)
21//! - `general`: Multi-step task execution
22//! - `plan`: Read-only planning mode
23//! - `verification`: Adversarial verification specialist
24//! - `review`: Code review specialist
25//! - `title`: Session title generation (hidden)
26//! - `summary`: Session summarization (hidden)
27//!
28//! ## Loading Agents from Files
29//!
30//! Agents can be loaded from YAML or Markdown files:
31//!
32//! ### YAML Format
33//! ```yaml
34//! name: my-agent
35//! description: Custom agent for specific tasks
36//! hidden: false
37//! max_steps: 30
38//! permissions:
39//!   allow:
40//!     - read
41//!     - grep
42//!   deny:
43//!     - write
44//! prompt: |
45//!   You are a specialized agent...
46//! ```
47//!
48//! ### Markdown Format
49//! ```markdown
50//! ---
51//! name: my-agent
52//! description: Custom agent
53//! max_steps: 30
54//! ---
55//! # System Prompt
56//! You are a specialized agent...
57//! ```
58
59use crate::config::CodeConfig;
60use crate::permissions::PermissionPolicy;
61use serde::{Deserialize, Serialize};
62use std::collections::HashMap;
63use std::path::Path;
64use std::sync::RwLock;
65
66use crate::error::{read_or_recover, write_or_recover};
67
68/// Model configuration for agent
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct ModelConfig {
71    /// Model identifier (e.g., "claude-3-5-sonnet-20241022")
72    pub model: String,
73    /// Optional provider override
74    pub provider: Option<String>,
75}
76
77/// Agent definition
78///
79/// Defines the configuration and capabilities of an agent type.
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct AgentDefinition {
82    /// Agent identifier (e.g., "explore", "plan", "general")
83    pub name: String,
84    /// Description of what the agent does
85    pub description: String,
86    /// Whether this is a built-in agent
87    #[serde(default)]
88    pub native: bool,
89    /// Whether to hide from UI
90    #[serde(default)]
91    pub hidden: bool,
92    /// Permission rules for this agent
93    #[serde(default)]
94    pub permissions: PermissionPolicy,
95    /// Optional model override
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub model: Option<ModelConfig>,
98    /// System prompt for this agent
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub prompt: Option<String>,
101    /// Maximum execution steps (tool rounds)
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub max_steps: Option<usize>,
104}
105
106impl AgentDefinition {
107    /// Create a new agent definition
108    pub fn new(name: &str, description: &str) -> Self {
109        Self {
110            name: name.to_string(),
111            description: description.to_string(),
112            native: false,
113            hidden: false,
114            permissions: PermissionPolicy::default(),
115            model: None,
116            prompt: None,
117            max_steps: None,
118        }
119    }
120
121    /// Mark as native (built-in)
122    pub fn native(mut self) -> Self {
123        self.native = true;
124        self
125    }
126
127    /// Mark as hidden from UI
128    pub fn hidden(mut self) -> Self {
129        self.hidden = true;
130        self
131    }
132
133    /// Set permission policy
134    pub fn with_permissions(mut self, permissions: PermissionPolicy) -> Self {
135        self.permissions = permissions;
136        self
137    }
138
139    /// Set model override
140    pub fn with_model(mut self, model: ModelConfig) -> Self {
141        self.model = Some(model);
142        self
143    }
144
145    /// Set system prompt
146    pub fn with_prompt(mut self, prompt: &str) -> Self {
147        self.prompt = Some(prompt.to_string());
148        self
149    }
150
151    /// Set maximum execution steps
152    pub fn with_max_steps(mut self, max_steps: usize) -> Self {
153        self.max_steps = Some(max_steps);
154        self
155    }
156}
157
158/// Agent registry for managing agent definitions
159///
160/// Thread-safe registry that stores agent definitions and provides
161/// lookup functionality.
162pub struct AgentRegistry {
163    agents: RwLock<HashMap<String, AgentDefinition>>,
164}
165
166impl Default for AgentRegistry {
167    fn default() -> Self {
168        Self::new()
169    }
170}
171
172impl AgentRegistry {
173    /// Create a new agent registry with built-in agents
174    pub fn new() -> Self {
175        let registry = Self {
176            agents: RwLock::new(HashMap::new()),
177        };
178
179        // Register built-in agents
180        for agent in builtin_agents() {
181            registry.register(agent);
182        }
183
184        registry
185    }
186
187    /// Create a new agent registry with configuration
188    ///
189    /// Loads built-in agents first, then loads agents from configured directories.
190    pub fn with_config(config: &CodeConfig) -> Self {
191        let registry = Self::new();
192
193        // Load agents from configured directories
194        for dir in &config.agent_dirs {
195            let agents = load_agents_from_dir(dir);
196            for agent in agents {
197                tracing::info!("Loaded agent '{}' from {}", agent.name, dir.display());
198                registry.register(agent);
199            }
200        }
201
202        registry
203    }
204
205    /// Register an agent definition
206    pub fn register(&self, agent: AgentDefinition) {
207        let mut agents = write_or_recover(&self.agents);
208        tracing::debug!("Registering agent: {}", agent.name);
209        agents.insert(agent.name.clone(), agent);
210    }
211
212    /// Unregister an agent by name
213    ///
214    /// Returns true if the agent was removed, false if not found.
215    pub fn unregister(&self, name: &str) -> bool {
216        let mut agents = write_or_recover(&self.agents);
217        agents.remove(name).is_some()
218    }
219
220    /// Get an agent definition by name
221    pub fn get(&self, name: &str) -> Option<AgentDefinition> {
222        let agents = read_or_recover(&self.agents);
223        agents.get(name).cloned()
224    }
225
226    /// List all registered agents
227    pub fn list(&self) -> Vec<AgentDefinition> {
228        let agents = read_or_recover(&self.agents);
229        agents.values().cloned().collect()
230    }
231
232    /// List visible agents (not hidden)
233    pub fn list_visible(&self) -> Vec<AgentDefinition> {
234        let agents = read_or_recover(&self.agents);
235        agents.values().filter(|a| !a.hidden).cloned().collect()
236    }
237
238    /// Check if an agent exists
239    pub fn exists(&self, name: &str) -> bool {
240        let agents = read_or_recover(&self.agents);
241        agents.contains_key(name)
242    }
243
244    /// Get the number of registered agents
245    pub fn len(&self) -> usize {
246        let agents = read_or_recover(&self.agents);
247        agents.len()
248    }
249
250    /// Check if the registry is empty
251    pub fn is_empty(&self) -> bool {
252        self.len() == 0
253    }
254}
255
256// ============================================================================
257// Agent File Loading
258// ============================================================================
259
260/// Parse an agent definition from YAML content
261///
262/// The YAML should contain fields matching AgentDefinition structure.
263pub fn parse_agent_yaml(content: &str) -> anyhow::Result<AgentDefinition> {
264    let agent: AgentDefinition = serde_yaml::from_str(content)
265        .map_err(|e| anyhow::anyhow!("Failed to parse agent YAML: {}", e))?;
266
267    if agent.name.is_empty() {
268        return Err(anyhow::anyhow!("Agent name is required"));
269    }
270
271    Ok(agent)
272}
273
274/// Parse an agent definition from Markdown with YAML frontmatter
275///
276/// The frontmatter contains agent metadata, and the body becomes the prompt.
277pub fn parse_agent_md(content: &str) -> anyhow::Result<AgentDefinition> {
278    // Parse frontmatter (YAML between --- markers)
279    let parts: Vec<&str> = content.splitn(3, "---").collect();
280
281    if parts.len() < 3 {
282        return Err(anyhow::anyhow!(
283            "Invalid markdown format: missing YAML frontmatter"
284        ));
285    }
286
287    let frontmatter = parts[1].trim();
288    let body = parts[2].trim();
289
290    // Parse the frontmatter as YAML
291    let mut agent: AgentDefinition = serde_yaml::from_str(frontmatter)
292        .map_err(|e| anyhow::anyhow!("Failed to parse agent frontmatter: {}", e))?;
293
294    if agent.name.is_empty() {
295        return Err(anyhow::anyhow!("Agent name is required"));
296    }
297
298    // Use body as prompt if not already set in frontmatter
299    if agent.prompt.is_none() && !body.is_empty() {
300        agent.prompt = Some(body.to_string());
301    }
302
303    Ok(agent)
304}
305
306/// Load all agent definitions from a directory
307///
308/// Scans for *.yaml and *.md files and parses them as agent definitions.
309/// Invalid files are logged and skipped.
310pub fn load_agents_from_dir(dir: &Path) -> Vec<AgentDefinition> {
311    let mut agents = Vec::new();
312
313    let Ok(entries) = std::fs::read_dir(dir) else {
314        tracing::warn!("Failed to read agent directory: {}", dir.display());
315        return agents;
316    };
317
318    for entry in entries.flatten() {
319        let path = entry.path();
320
321        // Skip non-files
322        if !path.is_file() {
323            continue;
324        }
325
326        let Some(ext) = path.extension().and_then(|e| e.to_str()) else {
327            continue;
328        };
329
330        // Read file content
331        let Ok(content) = std::fs::read_to_string(&path) else {
332            tracing::warn!("Failed to read agent file: {}", path.display());
333            continue;
334        };
335
336        // Parse based on extension
337        let result = match ext {
338            "yaml" | "yml" => parse_agent_yaml(&content),
339            "md" => parse_agent_md(&content),
340            _ => continue,
341        };
342
343        match result {
344            Ok(agent) => {
345                tracing::debug!("Loaded agent '{}' from {}", agent.name, path.display());
346                agents.push(agent);
347            }
348            Err(e) => {
349                tracing::warn!("Failed to parse agent file {}: {}", path.display(), e);
350            }
351        }
352    }
353
354    agents
355}
356
357/// Create built-in agent definitions
358pub fn builtin_agents() -> Vec<AgentDefinition> {
359    vec![
360        // Explore agent: Fast codebase exploration (read-only)
361        AgentDefinition::new(
362            "explore",
363            "Fast codebase exploration agent. Use for searching files, reading code, \
364             and understanding codebase structure. Read-only operations only.",
365        )
366        .native()
367        .with_permissions(explore_permissions())
368        .with_max_steps(20)
369        .with_prompt(EXPLORE_PROMPT),
370        // General agent: Multi-step task execution
371        AgentDefinition::new(
372            "general",
373            "General-purpose agent for multi-step task execution. Can read, write, \
374             and execute commands.",
375        )
376        .native()
377        .with_permissions(general_permissions())
378        .with_max_steps(50),
379        // Plan agent: Read-only planning mode
380        AgentDefinition::new(
381            "plan",
382            "Planning agent for designing implementation approaches. Read-only access \
383             to explore codebase and create plans.",
384        )
385        .native()
386        .with_permissions(plan_permissions())
387        .with_max_steps(30)
388        .with_prompt(PLAN_PROMPT),
389        // Verification agent: adversarial validation and repro
390        AgentDefinition::new(
391            "verification",
392            "Verification agent for adversarial validation. Prefer real checks, \
393             reproductions, and regression testing over code reading alone.",
394        )
395        .native()
396        .with_permissions(verification_permissions())
397        .with_max_steps(30)
398        .with_prompt(VERIFICATION_PROMPT),
399        // Review agent: review-focused analysis
400        AgentDefinition::new(
401            "review",
402            "Code review agent focused on correctness, regressions, security, \
403             maintainability, and clear findings.",
404        )
405        .native()
406        .with_permissions(review_permissions())
407        .with_max_steps(25)
408        .with_prompt(REVIEW_PROMPT),
409        // Title agent: Session title generation (hidden)
410        AgentDefinition::new(
411            "title",
412            "Generate a concise title for the session based on conversation content.",
413        )
414        .native()
415        .hidden()
416        .with_permissions(PermissionPolicy::new())
417        .with_max_steps(1)
418        .with_prompt(TITLE_PROMPT),
419        // Summary agent: Session summarization (hidden)
420        AgentDefinition::new(
421            "summary",
422            "Summarize the session conversation for context compaction.",
423        )
424        .native()
425        .hidden()
426        .with_permissions(summary_permissions())
427        .with_max_steps(5)
428        .with_prompt(SUMMARY_PROMPT),
429    ]
430}
431
432// ============================================================================
433// Permission Policies for Built-in Agents
434// ============================================================================
435
436/// Permission policy for explore agent (read-only)
437fn explore_permissions() -> PermissionPolicy {
438    PermissionPolicy::new()
439        .allow_all(&["read", "grep", "glob", "ls"])
440        .deny_all(&["write", "edit", "task"])
441        .allow("Bash(ls:*)")
442        .allow("Bash(cat:*)")
443        .allow("Bash(head:*)")
444        .allow("Bash(tail:*)")
445        .allow("Bash(find:*)")
446        .allow("Bash(wc:*)")
447        .deny("Bash(rm:*)")
448        .deny("Bash(mv:*)")
449        .deny("Bash(cp:*)")
450}
451
452/// Permission policy for general agent (full access except task)
453fn general_permissions() -> PermissionPolicy {
454    PermissionPolicy::new()
455        .allow_all(&["read", "write", "edit", "grep", "glob", "ls", "bash"])
456        .deny("task")
457}
458
459/// Permission policy for plan agent (read-only)
460fn plan_permissions() -> PermissionPolicy {
461    PermissionPolicy::new()
462        .allow_all(&["read", "grep", "glob", "ls"])
463        .deny_all(&["write", "edit", "bash", "task"])
464}
465
466/// Permission policy for summary agent (read-only)
467fn summary_permissions() -> PermissionPolicy {
468    PermissionPolicy::new()
469        .allow("read")
470        .deny_all(&["write", "edit", "bash", "grep", "glob", "ls", "task"])
471}
472
473/// Permission policy for verification agent (read-heavy with runtime checks)
474fn verification_permissions() -> PermissionPolicy {
475    PermissionPolicy::new()
476        .allow_all(&["read", "grep", "glob", "ls", "bash"])
477        .deny_all(&["write", "edit", "task"])
478}
479
480/// Permission policy for review agent (read-heavy with optional lightweight checks)
481fn review_permissions() -> PermissionPolicy {
482    PermissionPolicy::new()
483        .allow_all(&["read", "grep", "glob", "ls", "bash"])
484        .deny_all(&["write", "edit", "task"])
485}
486
487// ============================================================================
488// System Prompts for Built-in Agents
489// ============================================================================
490
491const EXPLORE_PROMPT: &str = crate::prompts::SUBAGENT_EXPLORE;
492
493const PLAN_PROMPT: &str = crate::prompts::SUBAGENT_PLAN;
494
495const VERIFICATION_PROMPT: &str = crate::prompts::AGENT_VERIFICATION;
496
497const REVIEW_PROMPT: &str = crate::prompts::SUBAGENT_CODE_REVIEW;
498
499const TITLE_PROMPT: &str = crate::prompts::SUBAGENT_TITLE;
500
501const SUMMARY_PROMPT: &str = crate::prompts::SUBAGENT_SUMMARY;
502
503// ============================================================================
504// Tests
505// ============================================================================
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510
511    #[test]
512    fn test_agent_definition_builder() {
513        let agent = AgentDefinition::new("test", "Test agent")
514            .native()
515            .hidden()
516            .with_max_steps(10);
517
518        assert_eq!(agent.name, "test");
519        assert_eq!(agent.description, "Test agent");
520        assert!(agent.native);
521        assert!(agent.hidden);
522        assert_eq!(agent.max_steps, Some(10));
523    }
524
525    #[test]
526    fn test_agent_registry_new() {
527        let registry = AgentRegistry::new();
528
529        // Should have built-in agents
530        assert!(registry.exists("explore"));
531        assert!(registry.exists("general"));
532        assert!(registry.exists("plan"));
533        assert!(registry.exists("verification"));
534        assert!(registry.exists("review"));
535        assert!(registry.exists("title"));
536        assert!(registry.exists("summary"));
537        assert_eq!(registry.len(), 7);
538    }
539
540    #[test]
541    fn test_agent_registry_get() {
542        let registry = AgentRegistry::new();
543
544        let explore = registry.get("explore").unwrap();
545        assert_eq!(explore.name, "explore");
546        assert!(explore.native);
547        assert!(!explore.hidden);
548
549        let title = registry.get("title").unwrap();
550        assert!(title.hidden);
551
552        assert!(registry.get("nonexistent").is_none());
553    }
554
555    #[test]
556    fn test_agent_registry_register_unregister() {
557        let registry = AgentRegistry::new();
558        let initial_count = registry.len();
559
560        // Register custom agent
561        let custom = AgentDefinition::new("custom", "Custom agent");
562        registry.register(custom);
563        assert_eq!(registry.len(), initial_count + 1);
564        assert!(registry.exists("custom"));
565
566        // Unregister
567        assert!(registry.unregister("custom"));
568        assert_eq!(registry.len(), initial_count);
569        assert!(!registry.exists("custom"));
570
571        // Unregister non-existent
572        assert!(!registry.unregister("nonexistent"));
573    }
574
575    #[test]
576    fn test_agent_registry_list_visible() {
577        let registry = AgentRegistry::new();
578
579        let visible = registry.list_visible();
580        let all = registry.list();
581
582        // Hidden agents should not be in visible list
583        assert!(visible.len() < all.len());
584        assert!(visible.iter().all(|a| !a.hidden));
585    }
586
587    #[test]
588    fn test_builtin_agents() {
589        let agents = builtin_agents();
590
591        // Check we have expected agents
592        let names: Vec<&str> = agents.iter().map(|a| a.name.as_str()).collect();
593        assert!(names.contains(&"explore"));
594        assert!(names.contains(&"general"));
595        assert!(names.contains(&"plan"));
596        assert!(names.contains(&"verification"));
597        assert!(names.contains(&"review"));
598        assert!(names.contains(&"title"));
599        assert!(names.contains(&"summary"));
600
601        // Check explore is read-only (has deny rules for write)
602        let explore = agents.iter().find(|a| a.name == "explore").unwrap();
603        assert!(!explore.permissions.deny.is_empty());
604    }
605
606    // ========================================================================
607    // Agent File Loading Tests
608    // ========================================================================
609
610    #[test]
611    fn test_parse_agent_yaml() {
612        let yaml = r#"
613name: test-agent
614description: A test agent
615hidden: false
616max_steps: 20
617"#;
618        let agent = parse_agent_yaml(yaml).unwrap();
619        assert_eq!(agent.name, "test-agent");
620        assert_eq!(agent.description, "A test agent");
621        assert!(!agent.hidden);
622        assert_eq!(agent.max_steps, Some(20));
623    }
624
625    #[test]
626    fn test_parse_agent_yaml_with_permissions() {
627        let yaml = r#"
628name: restricted-agent
629description: Agent with permissions
630permissions:
631  allow:
632    - rule: read
633    - rule: grep
634  deny:
635    - rule: write
636"#;
637        let agent = parse_agent_yaml(yaml).unwrap();
638        assert_eq!(agent.name, "restricted-agent");
639        assert_eq!(agent.permissions.allow.len(), 2);
640        assert_eq!(agent.permissions.deny.len(), 1);
641        // Verify that deserialized rules actually match (tool_name populated)
642        assert!(agent.permissions.allow[0].matches("read", &serde_json::json!({})));
643        assert!(agent.permissions.allow[1].matches("grep", &serde_json::json!({})));
644        assert!(agent.permissions.deny[0].matches("write", &serde_json::json!({})));
645    }
646
647    #[test]
648    fn test_parse_agent_yaml_with_plain_string_permissions() {
649        // Users naturally write plain strings in allow/deny lists
650        let yaml = r#"
651name: plain-agent
652description: Agent with plain string permissions
653permissions:
654  allow:
655    - read
656    - grep
657    - "Bash(cargo:*)"
658  deny:
659    - write
660"#;
661        let agent = parse_agent_yaml(yaml).unwrap();
662        assert_eq!(agent.name, "plain-agent");
663        assert_eq!(agent.permissions.allow.len(), 3);
664        assert_eq!(agent.permissions.deny.len(), 1);
665        // Verify rules are functional
666        assert!(agent.permissions.allow[0].matches("read", &serde_json::json!({})));
667        assert!(agent.permissions.allow[1].matches("grep", &serde_json::json!({})));
668        assert!(agent.permissions.allow[2]
669            .matches("Bash", &serde_json::json!({"command": "cargo build"})));
670        assert!(agent.permissions.deny[0].matches("write", &serde_json::json!({})));
671    }
672
673    #[test]
674    fn test_parse_agent_yaml_missing_name() {
675        let yaml = r#"
676description: Agent without name
677"#;
678        let result = parse_agent_yaml(yaml);
679        assert!(result.is_err());
680    }
681
682    #[test]
683    fn test_parse_agent_md() {
684        let md = r#"---
685name: md-agent
686description: Agent from markdown
687max_steps: 15
688---
689# System Prompt
690
691You are a helpful agent.
692Do your best work.
693"#;
694        let agent = parse_agent_md(md).unwrap();
695        assert_eq!(agent.name, "md-agent");
696        assert_eq!(agent.description, "Agent from markdown");
697        assert_eq!(agent.max_steps, Some(15));
698        assert!(agent.prompt.is_some());
699        assert!(agent.prompt.unwrap().contains("helpful agent"));
700    }
701
702    #[test]
703    fn test_parse_agent_md_with_prompt_in_frontmatter() {
704        let md = r#"---
705name: prompt-agent
706description: Agent with prompt in frontmatter
707prompt: "Frontmatter prompt"
708---
709Body content that should be ignored
710"#;
711        let agent = parse_agent_md(md).unwrap();
712        assert_eq!(agent.prompt.unwrap(), "Frontmatter prompt");
713    }
714
715    #[test]
716    fn test_parse_agent_md_missing_frontmatter() {
717        let md = "Just markdown without frontmatter";
718        let result = parse_agent_md(md);
719        assert!(result.is_err());
720    }
721
722    #[test]
723    fn test_load_agents_from_dir() {
724        let temp_dir = tempfile::tempdir().unwrap();
725
726        // Create a YAML agent file
727        std::fs::write(
728            temp_dir.path().join("agent1.yaml"),
729            r#"
730name: yaml-agent
731description: Agent from YAML file
732"#,
733        )
734        .unwrap();
735
736        // Create a Markdown agent file
737        std::fs::write(
738            temp_dir.path().join("agent2.md"),
739            r#"---
740name: md-agent
741description: Agent from Markdown file
742---
743System prompt here
744"#,
745        )
746        .unwrap();
747
748        // Create an invalid file (should be skipped)
749        std::fs::write(temp_dir.path().join("invalid.yaml"), "not: valid: yaml: [").unwrap();
750
751        // Create a non-agent file (should be skipped)
752        std::fs::write(temp_dir.path().join("readme.txt"), "Just a text file").unwrap();
753
754        let agents = load_agents_from_dir(temp_dir.path());
755        assert_eq!(agents.len(), 2);
756
757        let names: Vec<&str> = agents.iter().map(|a| a.name.as_str()).collect();
758        assert!(names.contains(&"yaml-agent"));
759        assert!(names.contains(&"md-agent"));
760    }
761
762    #[test]
763    fn test_load_agents_from_nonexistent_dir() {
764        let agents = load_agents_from_dir(std::path::Path::new("/nonexistent/dir"));
765        assert!(agents.is_empty());
766    }
767
768    #[test]
769    fn test_registry_with_config() {
770        let temp_dir = tempfile::tempdir().unwrap();
771
772        // Create an agent file
773        std::fs::write(
774            temp_dir.path().join("custom.yaml"),
775            r#"
776name: custom-agent
777description: Custom agent from config
778"#,
779        )
780        .unwrap();
781
782        let config = CodeConfig::new().add_agent_dir(temp_dir.path());
783        let registry = AgentRegistry::with_config(&config);
784
785        // Should have built-in agents plus custom agent
786        assert!(registry.exists("explore"));
787        assert!(registry.exists("custom-agent"));
788        assert_eq!(registry.len(), 8); // 7 built-in + 1 custom
789    }
790
791    #[test]
792    fn test_agent_definition_with_model() {
793        let model = ModelConfig {
794            model: "claude-3-5-sonnet".to_string(),
795            provider: Some("anthropic".to_string()),
796        };
797        let agent = AgentDefinition::new("test", "Test").with_model(model);
798        assert!(agent.model.is_some());
799        assert_eq!(agent.model.unwrap().provider, Some("anthropic".to_string()));
800    }
801
802    #[test]
803    fn test_agent_registry_default() {
804        let registry = AgentRegistry::default();
805        assert!(!registry.is_empty());
806        assert_eq!(registry.len(), 7);
807    }
808
809    #[test]
810    fn test_agent_registry_is_empty() {
811        let registry = AgentRegistry {
812            agents: RwLock::new(HashMap::new()),
813        };
814        assert!(registry.is_empty());
815        assert_eq!(registry.len(), 0);
816    }
817}