claude_agent/subagents/
mod.rs1mod 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 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 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 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 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}