claude_agent/subagents/
mod.rs

1mod builtin;
2#[cfg(feature = "cli-integration")]
3mod loader;
4pub mod provider;
5
6pub use crate::common::Provider as SubagentProviderTrait;
7pub use builtin::{builtin_subagents, find_builtin};
8#[cfg(feature = "cli-integration")]
9pub use loader::SubagentLoader;
10pub use provider::{ChainSubagentProvider, InMemorySubagentProvider};
11#[cfg(feature = "cli-integration")]
12pub use provider::{FileSubagentProvider, file_subagent_provider};
13
14use serde::{Deserialize, Serialize};
15
16use crate::client::{ModelConfig, ModelType};
17use crate::common::{BaseRegistry, Named, RegistryItem, SourceType, ToolRestricted};
18
19pub use crate::common::SourceType as SubagentSourceType;
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct SubagentDefinition {
23    pub name: String,
24    pub description: String,
25    pub prompt: String,
26    #[serde(default)]
27    pub source_type: SubagentSourceType,
28    #[serde(default)]
29    pub tools: Vec<String>,
30    #[serde(default)]
31    pub model: Option<String>,
32    #[serde(default)]
33    pub model_type: Option<ModelType>,
34    #[serde(default)]
35    pub skills: Vec<String>,
36}
37
38impl SubagentDefinition {
39    pub fn new(
40        name: impl Into<String>,
41        description: impl Into<String>,
42        prompt: impl Into<String>,
43    ) -> Self {
44        Self {
45            name: name.into(),
46            description: description.into(),
47            prompt: prompt.into(),
48            source_type: SubagentSourceType::default(),
49            tools: Vec::new(),
50            model: None,
51            model_type: None,
52            skills: Vec::new(),
53        }
54    }
55
56    pub fn with_source_type(mut self, source_type: SubagentSourceType) -> Self {
57        self.source_type = source_type;
58        self
59    }
60
61    pub fn with_tools(mut self, tools: impl IntoIterator<Item = impl Into<String>>) -> Self {
62        self.tools = tools.into_iter().map(Into::into).collect();
63        self
64    }
65
66    pub fn with_model(mut self, model: impl Into<String>) -> Self {
67        self.model = Some(model.into());
68        self
69    }
70
71    pub fn with_model_type(mut self, model_type: ModelType) -> Self {
72        self.model_type = Some(model_type);
73        self
74    }
75
76    pub fn with_skills(mut self, skills: impl IntoIterator<Item = impl Into<String>>) -> Self {
77        self.skills = skills.into_iter().map(Into::into).collect();
78        self
79    }
80
81    /// Resolve the model to use for this subagent.
82    ///
83    /// Supports both direct model IDs and aliases:
84    /// - `"opus"` → resolves to reasoning model (e.g., claude-opus-4-5)
85    /// - `"sonnet"` → resolves to primary model (e.g., claude-sonnet-4-5)
86    /// - `"haiku"` → resolves to small model (e.g., claude-haiku-4-5)
87    /// - Direct model ID → passed through unchanged
88    ///
89    /// Falls back to `model_type` if `model` is not set.
90    pub fn resolve_model<'a>(&'a self, config: &'a ModelConfig) -> &'a str {
91        if let Some(ref model) = self.model {
92            return config.resolve_alias(model);
93        }
94        config.get(self.model_type.unwrap_or_default())
95    }
96}
97
98impl Named for SubagentDefinition {
99    fn name(&self) -> &str {
100        &self.name
101    }
102}
103
104impl ToolRestricted for SubagentDefinition {
105    fn allowed_tools(&self) -> &[String] {
106        &self.tools
107    }
108}
109
110impl RegistryItem for SubagentDefinition {
111    fn source_type(&self) -> SourceType {
112        self.source_type
113    }
114}
115
116#[cfg(feature = "cli-integration")]
117pub type SubagentRegistry = BaseRegistry<SubagentDefinition, SubagentLoader>;
118
119#[cfg(feature = "cli-integration")]
120impl SubagentRegistry {
121    pub fn with_builtins() -> Self {
122        let mut registry = Self::new();
123        registry.register_all(builtin_subagents());
124        registry
125    }
126
127    pub async fn load_from_directories(
128        &mut self,
129        working_dir: Option<&std::path::Path>,
130    ) -> crate::Result<()> {
131        let builtins = InMemorySubagentProvider::new()
132            .with_items(builtin_subagents())
133            .with_priority(0)
134            .with_source_type(SourceType::Builtin);
135
136        let mut chain = ChainSubagentProvider::new().with(builtins);
137
138        if let Some(dir) = working_dir {
139            let project = file_subagent_provider()
140                .with_project_path(dir)
141                .with_priority(20)
142                .with_source_type(SourceType::Project);
143            chain = chain.with(project);
144        }
145
146        let user = file_subagent_provider()
147            .with_user_path()
148            .with_priority(10)
149            .with_source_type(SourceType::User);
150        let chain = chain.with(user);
151
152        let loaded = chain.load_all().await?;
153        self.register_all(loaded);
154        Ok(())
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use crate::common::ToolRestricted;
162
163    #[test]
164    fn test_subagent_definition() {
165        let subagent = SubagentDefinition::new("reviewer", "Code reviewer", "Review the code")
166            .with_source_type(SubagentSourceType::Project)
167            .with_tools(["Read", "Grep", "Glob"])
168            .with_model("claude-haiku-4-5-20251001");
169
170        assert_eq!(subagent.name, "reviewer");
171        assert!(subagent.has_tool_restrictions());
172        assert!(subagent.is_tool_allowed("Read"));
173        assert!(!subagent.is_tool_allowed("Bash"));
174
175        let config = ModelConfig::default();
176        assert_eq!(subagent.resolve_model(&config), "claude-haiku-4-5-20251001");
177    }
178
179    #[test]
180    fn test_tool_pattern_matching() {
181        let subagent = SubagentDefinition::new("git-agent", "Git helper", "Help with git")
182            .with_tools(["Bash(git:*)", "Read"]);
183
184        assert!(subagent.is_tool_allowed("Bash"));
185        assert!(subagent.is_tool_allowed("Read"));
186        assert!(!subagent.is_tool_allowed("Write"));
187    }
188
189    #[test]
190    fn test_no_tool_restrictions() {
191        let subagent = SubagentDefinition::new("general", "General agent", "Do anything");
192
193        assert!(!subagent.has_tool_restrictions());
194        assert!(subagent.is_tool_allowed("Anything"));
195    }
196
197    #[test]
198    fn test_resolve_model_with_alias() {
199        let config = ModelConfig::default();
200
201        // Test alias resolution
202        let subagent =
203            SubagentDefinition::new("fast", "Fast agent", "Be quick").with_model("haiku");
204        assert!(subagent.resolve_model(&config).contains("haiku"));
205
206        let subagent =
207            SubagentDefinition::new("smart", "Smart agent", "Think deep").with_model("opus");
208        assert!(subagent.resolve_model(&config).contains("opus"));
209
210        let subagent = SubagentDefinition::new("balanced", "Balanced agent", "Be balanced")
211            .with_model("sonnet");
212        assert!(subagent.resolve_model(&config).contains("sonnet"));
213
214        // Test direct model ID passthrough
215        let subagent = SubagentDefinition::new("custom", "Custom agent", "Custom")
216            .with_model("claude-custom-model-v1");
217        assert_eq!(subagent.resolve_model(&config), "claude-custom-model-v1");
218
219        // Test fallback to model_type
220        let subagent = SubagentDefinition::new("typed", "Typed agent", "Use type")
221            .with_model_type(ModelType::Small);
222        assert!(subagent.resolve_model(&config).contains("haiku"));
223    }
224}