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 allowed_tools(mut self, tools: impl IntoIterator<Item = impl Into<String>>) -> Self {
92 self.allowed_tools = tools.into_iter().map(Into::into).collect();
93 self
94 }
95
96 pub fn tools(self, t: impl IntoIterator<Item = impl Into<String>>) -> Self {
98 self.allowed_tools(t)
99 }
100
101 pub fn source(mut self, source: ContentSource) -> Self {
103 self.source = source;
104 self
105 }
106
107 pub fn source_type(mut self, source_type: SourceType) -> Self {
109 self.source_type = source_type;
110 self
111 }
112
113 pub fn model(mut self, model: impl Into<String>) -> Self {
115 self.model = Some(model.into());
116 self
117 }
118
119 pub fn model_type(mut self, model_type: ModelType) -> Self {
121 self.model_type = Some(model_type);
122 self
123 }
124
125 pub fn skills(mut self, skills: impl IntoIterator<Item = impl Into<String>>) -> Self {
127 self.skills = skills.into_iter().map(Into::into).collect();
128 self
129 }
130
131 pub fn resolve_model<'a>(&'a self, config: &'a ModelConfig) -> &'a str {
141 if let Some(ref model) = self.model {
142 return config.resolve_alias(model);
143 }
144 config.get(self.model_type.unwrap_or_default())
145 }
146
147 pub async fn load_prompt(&self) -> crate::Result<String> {
149 self.source.load().await
150 }
151}
152
153impl Named for SubagentIndex {
154 fn name(&self) -> &str {
155 &self.name
156 }
157}
158
159impl ToolRestricted for SubagentIndex {
160 fn allowed_tools(&self) -> &[String] {
161 &self.allowed_tools
162 }
163}
164
165#[async_trait]
166impl Index for SubagentIndex {
167 fn source(&self) -> &ContentSource {
168 &self.source
169 }
170
171 fn source_type(&self) -> SourceType {
172 self.source_type
173 }
174
175 fn to_summary_line(&self) -> String {
176 let tools_str = if self.allowed_tools.is_empty() {
177 "*".to_string()
178 } else {
179 self.allowed_tools.join(", ")
180 };
181 format!(
182 "- {}: {} (Tools: {})",
183 self.name, self.description, tools_str
184 )
185 }
186
187 fn description(&self) -> &str {
188 &self.description
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn test_subagent_index_creation() {
198 let subagent = SubagentIndex::new("reviewer", "Code reviewer")
199 .source(ContentSource::in_memory("Review the code"))
200 .source_type(SourceType::Project)
201 .tools(["Read", "Grep", "Glob"])
202 .model("haiku");
203
204 assert_eq!(subagent.name, "reviewer");
205 assert!(subagent.has_tool_restrictions());
206 assert!(subagent.is_tool_allowed("Read"));
207 assert!(!subagent.is_tool_allowed("Bash"));
208 }
209
210 #[test]
211 fn test_summary_line() {
212 let subagent = SubagentIndex::new("Explore", "Fast codebase exploration")
213 .tools(["Read", "Grep", "Glob", "Bash"]);
214
215 let summary = subagent.to_summary_line();
216 assert!(summary.contains("Explore"));
217 assert!(summary.contains("Fast codebase exploration"));
218 assert!(summary.contains("Read, Grep, Glob, Bash"));
219 }
220
221 #[test]
222 fn test_summary_line_no_tools() {
223 let subagent = SubagentIndex::new("general-purpose", "General purpose agent");
224 let summary = subagent.to_summary_line();
225 assert!(summary.contains("(Tools: *)"));
226 }
227
228 #[tokio::test]
229 async fn test_load_prompt() {
230 let subagent = SubagentIndex::new("test", "Test agent")
231 .source(ContentSource::in_memory("You are a test agent."));
232
233 let prompt = subagent.load_prompt().await.unwrap();
234 assert_eq!(prompt, "You are a test agent.");
235 }
236
237 #[test]
238 fn test_resolve_model_with_alias() {
239 let config = ModelConfig::default();
240
241 let subagent = SubagentIndex::new("fast", "Fast agent")
242 .source(ContentSource::in_memory("Be quick"))
243 .model("haiku");
244 assert!(subagent.resolve_model(&config).contains("haiku"));
245
246 let subagent = SubagentIndex::new("smart", "Smart agent")
247 .source(ContentSource::in_memory("Think deep"))
248 .model("opus");
249 assert!(subagent.resolve_model(&config).contains("opus"));
250 }
251
252 #[test]
253 fn test_resolve_model_with_type() {
254 let config = ModelConfig::default();
255
256 let subagent = SubagentIndex::new("typed", "Typed agent")
257 .source(ContentSource::in_memory("Use type"))
258 .model_type(ModelType::Small);
259 assert!(subagent.resolve_model(&config).contains("haiku"));
260 }
261}