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