claude_agent/subagents/
index.rs1use async_trait::async_trait;
8use serde::{Deserialize, Serialize};
9
10use crate::client::{ModelConfig, ModelType};
11use crate::common::{ContentSource, Index, Named, SourceType, ToolRestricted};
12
13#[derive(Clone, Debug, Serialize, Deserialize)]
26pub struct SubagentIndex {
27 pub name: String,
29
30 pub description: String,
32
33 #[serde(default, alias = "tools")]
35 pub allowed_tools: Vec<String>,
36
37 pub source: ContentSource,
39
40 #[serde(default)]
42 pub source_type: SourceType,
43
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub model: Option<String>,
47
48 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub model_type: Option<ModelType>,
51
52 #[serde(default, skip_serializing_if = "Vec::is_empty")]
54 pub skills: Vec<String>,
55}
56
57impl SubagentIndex {
58 pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
60 Self {
61 name: name.into(),
62 description: description.into(),
63 allowed_tools: Vec::new(),
64 source: ContentSource::default(),
65 source_type: SourceType::default(),
66 model: None,
67 model_type: None,
68 skills: Vec::new(),
69 }
70 }
71
72 pub fn with_allowed_tools(
74 mut self,
75 tools: impl IntoIterator<Item = impl Into<String>>,
76 ) -> Self {
77 self.allowed_tools = tools.into_iter().map(Into::into).collect();
78 self
79 }
80
81 pub fn with_tools(self, tools: impl IntoIterator<Item = impl Into<String>>) -> Self {
83 self.with_allowed_tools(tools)
84 }
85
86 pub fn with_source(mut self, source: ContentSource) -> Self {
88 self.source = source;
89 self
90 }
91
92 pub fn with_source_type(mut self, source_type: SourceType) -> Self {
94 self.source_type = source_type;
95 self
96 }
97
98 pub fn with_model(mut self, model: impl Into<String>) -> Self {
100 self.model = Some(model.into());
101 self
102 }
103
104 pub fn with_model_type(mut self, model_type: ModelType) -> Self {
106 self.model_type = Some(model_type);
107 self
108 }
109
110 pub fn with_skills(mut self, skills: impl IntoIterator<Item = impl Into<String>>) -> Self {
112 self.skills = skills.into_iter().map(Into::into).collect();
113 self
114 }
115
116 pub fn resolve_model<'a>(&'a self, config: &'a ModelConfig) -> &'a str {
126 if let Some(ref model) = self.model {
127 return config.resolve_alias(model);
128 }
129 config.get(self.model_type.unwrap_or_default())
130 }
131
132 pub async fn load_prompt(&self) -> crate::Result<String> {
134 self.source.load().await
135 }
136}
137
138impl Named for SubagentIndex {
139 fn name(&self) -> &str {
140 &self.name
141 }
142}
143
144impl ToolRestricted for SubagentIndex {
145 fn allowed_tools(&self) -> &[String] {
146 &self.allowed_tools
147 }
148}
149
150#[async_trait]
151impl Index for SubagentIndex {
152 fn source(&self) -> &ContentSource {
153 &self.source
154 }
155
156 fn source_type(&self) -> SourceType {
157 self.source_type
158 }
159
160 fn to_summary_line(&self) -> String {
161 let tools_str = if self.allowed_tools.is_empty() {
162 "*".to_string()
163 } else {
164 self.allowed_tools.join(", ")
165 };
166 format!(
167 "- {}: {} (Tools: {})",
168 self.name, self.description, tools_str
169 )
170 }
171
172 fn description(&self) -> &str {
173 &self.description
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn test_subagent_index_creation() {
183 let subagent = SubagentIndex::new("reviewer", "Code reviewer")
184 .with_source(ContentSource::in_memory("Review the code"))
185 .with_source_type(SourceType::Project)
186 .with_tools(["Read", "Grep", "Glob"])
187 .with_model("haiku");
188
189 assert_eq!(subagent.name, "reviewer");
190 assert!(subagent.has_tool_restrictions());
191 assert!(subagent.is_tool_allowed("Read"));
192 assert!(!subagent.is_tool_allowed("Bash"));
193 }
194
195 #[test]
196 fn test_summary_line() {
197 let subagent = SubagentIndex::new("Explore", "Fast codebase exploration")
198 .with_tools(["Read", "Grep", "Glob", "Bash"]);
199
200 let summary = subagent.to_summary_line();
201 assert!(summary.contains("Explore"));
202 assert!(summary.contains("Fast codebase exploration"));
203 assert!(summary.contains("Read, Grep, Glob, Bash"));
204 }
205
206 #[test]
207 fn test_summary_line_no_tools() {
208 let subagent = SubagentIndex::new("general-purpose", "General purpose agent");
209 let summary = subagent.to_summary_line();
210 assert!(summary.contains("(Tools: *)"));
211 }
212
213 #[tokio::test]
214 async fn test_load_prompt() {
215 let subagent = SubagentIndex::new("test", "Test agent")
216 .with_source(ContentSource::in_memory("You are a test agent."));
217
218 let prompt = subagent.load_prompt().await.unwrap();
219 assert_eq!(prompt, "You are a test agent.");
220 }
221
222 #[test]
223 fn test_resolve_model_with_alias() {
224 let config = ModelConfig::default();
225
226 let subagent = SubagentIndex::new("fast", "Fast agent")
227 .with_source(ContentSource::in_memory("Be quick"))
228 .with_model("haiku");
229 assert!(subagent.resolve_model(&config).contains("haiku"));
230
231 let subagent = SubagentIndex::new("smart", "Smart agent")
232 .with_source(ContentSource::in_memory("Think deep"))
233 .with_model("opus");
234 assert!(subagent.resolve_model(&config).contains("opus"));
235 }
236
237 #[test]
238 fn test_resolve_model_with_type() {
239 let config = ModelConfig::default();
240
241 let subagent = SubagentIndex::new("typed", "Typed agent")
242 .with_source(ContentSource::in_memory("Use type"))
243 .with_model_type(ModelType::Small);
244 assert!(subagent.resolve_model(&config).contains("haiku"));
245 }
246}