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