1use std::collections::BTreeMap;
25use std::sync::Arc;
26
27use async_trait::async_trait;
28use tokio::sync::Mutex;
29
30use crate::agent::capability::Capability;
31use crate::agent::driver::{LlmDriver, ToolDefinition};
32use crate::agent::manifest::{AgentManifest, ResourceQuota};
33use crate::agent::pool::{AgentPool, SpawnConfig};
34
35use super::tool::{Tool, ToolResult};
36
37#[derive(Debug, Clone)]
43pub struct SubagentSpec {
44 pub name: String,
46 pub description: String,
48 pub system_prompt: String,
50 pub max_iterations: u32,
52}
53
54impl SubagentSpec {
55 pub fn general_purpose() -> Self {
57 Self {
58 name: "general-purpose".into(),
59 description: "General-purpose agent for researching questions, searching for code, \
60 and executing multi-step tasks. Use when the target is not known \
61 and you are not confident you will find the right match in the \
62 first few tries."
63 .into(),
64 system_prompt: "You are a general-purpose research subagent. Your job is to answer \
65 the user's question by gathering evidence from the codebase. Return \
66 a concise summary of findings, not running commentary."
67 .into(),
68 max_iterations: 10,
69 }
70 }
71
72 pub fn explore() -> Self {
74 Self {
75 name: "explore".into(),
76 description: "Fast agent specialized for exploring codebases. Use to find files \
77 by pattern, search code for keywords, or answer questions about \
78 structure. Returns a digest of findings."
79 .into(),
80 system_prompt: "You are a codebase exploration subagent. Prefer pmat_query over \
81 raw grep. Never edit files. Return a short digest of what you found \
82 with file:line citations."
83 .into(),
84 max_iterations: 8,
85 }
86 }
87
88 pub fn plan() -> Self {
90 Self {
91 name: "plan".into(),
92 description: "Software architect subagent for designing implementation plans. \
93 Use when you need a step-by-step plan, critical-file list, or \
94 architectural trade-offs before implementing."
95 .into(),
96 system_prompt: "You are a planning subagent. Read the relevant code, then return a \
97 numbered plan with (a) files to change, (b) order of changes, and \
98 (c) the key trade-off. Do not write code."
99 .into(),
100 max_iterations: 6,
101 }
102 }
103}
104
105#[derive(Debug, Clone, Default)]
111pub struct SubagentRegistry {
112 by_name: BTreeMap<String, SubagentSpec>,
113}
114
115impl SubagentRegistry {
116 pub fn new() -> Self {
118 Self { by_name: BTreeMap::new() }
119 }
120
121 pub fn register(&mut self, spec: SubagentSpec) {
124 self.by_name.insert(spec.name.clone(), spec);
125 }
126
127 pub fn resolve(&self, name: &str) -> Option<&SubagentSpec> {
129 self.by_name.get(name)
130 }
131
132 pub fn len(&self) -> usize {
134 self.by_name.len()
135 }
136
137 pub fn is_empty(&self) -> bool {
139 self.by_name.is_empty()
140 }
141
142 pub fn names(&self) -> Vec<String> {
144 self.by_name.keys().cloned().collect()
145 }
146}
147
148pub fn default_registry() -> SubagentRegistry {
152 let mut r = SubagentRegistry::new();
153 r.register(SubagentSpec::general_purpose());
154 r.register(SubagentSpec::explore());
155 r.register(SubagentSpec::plan());
156 r
157}
158
159pub struct TaskTool {
166 registry: Arc<SubagentRegistry>,
167 pool: Arc<Mutex<AgentPool>>,
168 parent_manifest: AgentManifest,
169 current_depth: u32,
170 max_depth: u32,
171}
172
173impl TaskTool {
174 pub fn new(
182 registry: Arc<SubagentRegistry>,
183 pool: Arc<Mutex<AgentPool>>,
184 parent_manifest: AgentManifest,
185 current_depth: u32,
186 max_depth: u32,
187 ) -> Self {
188 Self { registry, pool, parent_manifest, current_depth, max_depth }
189 }
190
191 pub fn from_driver(
195 driver: Arc<dyn LlmDriver>,
196 parent_manifest: AgentManifest,
197 max_depth: u32,
198 ) -> Self {
199 Self::from_driver_with_registry(driver, parent_manifest, max_depth, default_registry())
200 }
201
202 pub fn from_driver_with_registry(
207 driver: Arc<dyn LlmDriver>,
208 parent_manifest: AgentManifest,
209 max_depth: u32,
210 registry: SubagentRegistry,
211 ) -> Self {
212 let pool = Arc::new(Mutex::new(AgentPool::new(driver, 4)));
213 Self::new(Arc::new(registry), pool, parent_manifest, 0, max_depth)
214 }
215
216 fn build_child_manifest(&self, spec: &SubagentSpec) -> AgentManifest {
217 let mut child = self.parent_manifest.clone();
218 child.name = format!("{}/{}", self.parent_manifest.name, spec.name);
219 child.model.system_prompt = spec.system_prompt.clone();
220 child.resources = ResourceQuota {
221 max_iterations: child.resources.max_iterations.min(spec.max_iterations),
222 ..child.resources.clone()
223 };
224 child
225 }
226}
227
228#[async_trait]
229impl Tool for TaskTool {
230 fn name(&self) -> &'static str {
231 "task"
232 }
233
234 fn definition(&self) -> ToolDefinition {
235 let names = self.registry.names();
236 let listed = if names.is_empty() { "(none registered)".into() } else { names.join(", ") };
237 ToolDefinition {
238 name: "task".into(),
239 description: format!(
240 "Launch a typed sub-agent to handle a delegated task. \
241 Registered subagent types: {listed}. \
242 The child runs its own perceive-reason-act loop and its \
243 final response is returned as the tool result."
244 ),
245 input_schema: serde_json::json!({
246 "type": "object",
247 "properties": {
248 "subagent_type": {
249 "type": "string",
250 "description": "Registered subagent type (e.g. general-purpose, explore, plan)"
251 },
252 "description": {
253 "type": "string",
254 "description": "Short (3-7 word) label for the task — shown in telemetry"
255 },
256 "prompt": {
257 "type": "string",
258 "description": "The full task delegated to the child subagent"
259 }
260 },
261 "required": ["subagent_type", "prompt"]
262 }),
263 }
264 }
265
266 async fn execute(&self, input: serde_json::Value) -> ToolResult {
267 if self.current_depth >= self.max_depth {
268 return ToolResult::error(format!(
269 "task depth limit reached ({}/{})",
270 self.current_depth, self.max_depth,
271 ));
272 }
273
274 let subagent_type = match input.get("subagent_type").and_then(|v| v.as_str()) {
275 Some(s) => s,
276 None => {
277 return ToolResult::error("missing required field: subagent_type");
278 }
279 };
280
281 let prompt = match input.get("prompt").and_then(|v| v.as_str()) {
282 Some(s) => s.to_string(),
283 None => {
284 return ToolResult::error("missing required field: prompt");
285 }
286 };
287
288 let spec = match self.registry.resolve(subagent_type) {
289 Some(s) => s.clone(),
290 None => {
291 let names = self.registry.names().join(", ");
292 return ToolResult::error(format!(
293 "unknown subagent_type '{subagent_type}' (registered: {names})"
294 ));
295 }
296 };
297
298 let child_manifest = self.build_child_manifest(&spec);
299 let config = SpawnConfig { manifest: child_manifest, query: prompt };
300
301 let mut pool = self.pool.lock().await;
302 let id = match pool.spawn(config) {
303 Ok(id) => id,
304 Err(e) => return ToolResult::error(format!("task spawn failed: {e}")),
305 };
306
307 match pool.join_next().await {
308 Some((completed_id, Ok(result))) if completed_id == id => {
309 ToolResult::success(result.text)
310 }
311 Some((_, Ok(result))) => ToolResult::success(result.text),
312 Some((_, Err(e))) => ToolResult::error(format!("subagent error: {e}")),
313 None => ToolResult::error("subagent produced no result"),
314 }
315 }
316
317 fn required_capability(&self) -> Capability {
318 Capability::Spawn { max_depth: self.max_depth }
323 }
324}
325
326pub fn register_task_tool(
335 registry: &mut super::tool::ToolRegistry,
336 manifest: &AgentManifest,
337 driver: Arc<dyn LlmDriver>,
338 max_depth: u32,
339) {
340 let mut subagents = default_registry();
341 if let Ok(cwd) = std::env::current_dir() {
342 let _ = super::custom_agents::register_discovered_into(&mut subagents, &cwd);
343 }
344 let tool = TaskTool::from_driver_with_registry(
345 Arc::clone(&driver),
346 manifest.clone(),
347 max_depth,
348 subagents,
349 );
350 registry.register(Box::new(tool));
351}
352
353#[cfg(test)]
354mod tests;