1use async_trait::async_trait;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use tokio::select;
7use tracing::debug;
8
9use super::AgentBuilder;
10use super::task_registry::TaskRegistry;
11use crate::auth::Auth;
12use crate::client::CloudProvider;
13use crate::common::{Index, IndexRegistry};
14use crate::hooks::{HookEvent, HookInput};
15use crate::subagents::{SubagentIndex, builtin_subagents};
16use crate::tools::{ExecutionContext, SchemaTool};
17use crate::types::{Message, ToolResult};
18
19pub struct TaskTool {
20 registry: TaskRegistry,
21 subagent_registry: IndexRegistry<SubagentIndex>,
22 max_background_tasks: usize,
23}
24
25impl TaskTool {
26 pub fn new(registry: TaskRegistry) -> Self {
27 let mut subagent_registry = IndexRegistry::new();
28 subagent_registry.register_all(builtin_subagents());
29 Self {
30 registry,
31 subagent_registry,
32 max_background_tasks: 10,
33 }
34 }
35
36 pub fn with_subagent_registry(
37 mut self,
38 subagent_registry: IndexRegistry<SubagentIndex>,
39 ) -> Self {
40 self.subagent_registry = subagent_registry;
41 self
42 }
43
44 pub fn with_max_background_tasks(mut self, max: usize) -> Self {
45 self.max_background_tasks = max;
46 self
47 }
48
49 pub fn description_with_subagents(&self) -> String {
54 let subagents_desc = self
55 .subagent_registry
56 .iter()
57 .map(|subagent| subagent.to_summary_line())
58 .collect::<Vec<_>>()
59 .join("\n");
60
61 format!(
62 r#"Launch a new agent to handle complex, multi-step tasks autonomously.
63
64The Task tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.
65
66Available agent types and the tools they have access to:
67{}
68
69When using the Task tool, you must specify a subagent_type parameter to select which agent type to use.
70
71When NOT to use the Task tool:
72- If you want to read a specific file path, use the Read or Glob tool instead of the Task tool, to find the match more quickly
73- If you are searching for a specific class definition like "class Foo", use the Grep tool instead, to find the match more quickly
74- If you are searching for code within a specific file or set of 2-3 files, use the Read tool instead of the Task tool, to find the match more quickly
75- Other tasks that are not related to the agent descriptions above
76
77Usage notes:
78- Always include a short description (3-5 words) summarizing what the agent will do
79- Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
80- When the agent is done, it will return a single message back to you along with its agent_id. You can use this ID to resume the agent later if needed for follow-up work.
81- You can optionally run agents in the background using the run_in_background parameter. When an agent runs in the background, you will need to use TaskOutput to retrieve its results once it's done. You can continue to work while background agents run - when you need their results to continue you can use TaskOutput in blocking mode to pause and wait for their results.
82- Agents can be resumed using the `resume` parameter by passing the agent ID from a previous invocation. When resumed, the agent continues with its full previous context preserved. When NOT resuming, each invocation starts fresh and you should provide a detailed task description with all necessary context.
83- Provide clear, detailed prompts so the agent can work autonomously and return exactly the information you need.
84- The agent's outputs should generally be trusted
85- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent
86- If you need to launch multiple agents in parallel, send a single message with multiple Task tool calls.
87- Use model="haiku" for quick, straightforward tasks to minimize cost and latency"#,
88 subagents_desc
89 )
90 }
91
92 async fn spawn_agent(
93 &self,
94 input: &TaskInput,
95 previous_messages: Option<Vec<Message>>,
96 ) -> crate::Result<super::AgentResult> {
97 let subagent = self
98 .subagent_registry
99 .get(&input.subagent_type)
100 .ok_or_else(|| {
101 crate::Error::Config(format!("Unknown subagent type: {}", input.subagent_type))
102 })?;
103
104 let provider = CloudProvider::from_env();
105 let model_config = provider.default_models();
106
107 let model = input
108 .model
109 .as_deref()
110 .map(|m| model_config.resolve_alias(m))
111 .or(subagent.model.as_deref())
112 .unwrap_or_else(|| subagent.resolve_model(&model_config))
113 .to_string();
114
115 let agent = AgentBuilder::new()
116 .auth(Auth::FromEnv)
117 .await?
118 .model(&model)
119 .max_iterations(50)
120 .build()
121 .await?;
122
123 match previous_messages {
124 Some(messages) if !messages.is_empty() => {
125 debug!(
126 message_count = messages.len(),
127 "Resuming agent with previous context"
128 );
129 agent.execute_with_messages(messages, &input.prompt).await
130 }
131 _ => agent.execute(&input.prompt).await,
132 }
133 }
134}
135
136impl Clone for TaskTool {
137 fn clone(&self) -> Self {
138 Self {
139 registry: self.registry.clone(),
140 subagent_registry: self.subagent_registry.clone(),
141 max_background_tasks: self.max_background_tasks,
142 }
143 }
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
147#[schemars(deny_unknown_fields)]
148pub struct TaskInput {
149 pub description: String,
151 pub prompt: String,
153 pub subagent_type: String,
155 #[serde(default)]
157 pub model: Option<String>,
158 #[serde(default)]
160 pub run_in_background: Option<bool>,
161 #[serde(default)]
163 pub resume: Option<String>,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct TaskOutput {
168 pub agent_id: String,
169 pub result: String,
170 pub is_running: bool,
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub error: Option<String>,
173}
174
175#[async_trait]
176impl SchemaTool for TaskTool {
177 type Input = TaskInput;
178
179 const NAME: &'static str = "Task";
180 const DESCRIPTION: &'static str = r#"Launch a new agent to handle complex, multi-step tasks autonomously.
181
182The Task tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.
183
184Available agent types and the tools they have access to:
185- general: General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you. (Tools: *)
186- explore: Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (e.g., "src/**/*.ts"), search code for keywords (e.g., "API endpoints"), or answer questions about the codebase (e.g., "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions. (Tools: Read, Grep, Glob, Bash)
187- plan: Software architect agent for designing implementation plans. Use this when you need to plan the implementation strategy for a task. Returns step-by-step plans, identifies critical files, and considers architectural trade-offs. (Tools: *)
188
189When using the Task tool, you must specify a subagent_type parameter to select which agent type to use.
190
191When NOT to use the Task tool:
192- If you want to read a specific file path, use the Read or Glob tool instead of the Task tool, to find the match more quickly
193- If you are searching for a specific class definition like "class Foo", use the Grep tool instead, to find the match more quickly
194- If you are searching for code within a specific file or set of 2-3 files, use the Read tool instead of the Task tool, to find the match more quickly
195- Other tasks that are not related to the agent descriptions above
196
197Usage notes:
198- Always include a short description (3-5 words) summarizing what the agent will do
199- Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
200- When the agent is done, it will return a single message back to you along with its agent_id. You can use this ID to resume the agent later if needed for follow-up work.
201- You can optionally run agents in the background using the run_in_background parameter. When an agent runs in the background, you will need to use TaskOutput to retrieve its results once it's done. You can continue to work while background agents run - when you need their results to continue you can use TaskOutput in blocking mode to pause and wait for their results.
202- Agents can be resumed using the `resume` parameter by passing the agent ID from a previous invocation. When resumed, the agent continues with its full previous context preserved. When NOT resuming, each invocation starts fresh and you should provide a detailed task description with all necessary context.
203- Provide clear, detailed prompts so the agent can work autonomously and return exactly the information you need.
204- The agent's outputs should generally be trusted
205- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent
206- If you need to launch multiple agents in parallel, send a single message with multiple Task tool calls.
207- Use model="haiku" for quick, straightforward tasks to minimize cost and latency"#;
208
209 async fn handle(&self, input: TaskInput, context: &ExecutionContext) -> ToolResult {
210 let previous_messages = if let Some(ref resume_id) = input.resume {
211 self.registry.get_messages(resume_id).await
212 } else {
213 None
214 };
215
216 let agent_id = input
217 .resume
218 .clone()
219 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()[..7].to_string());
220
221 let session_id = context.session_id().unwrap_or("").to_string();
222 let run_in_background = input.run_in_background.unwrap_or(false);
223
224 if run_in_background {
225 let running = self.registry.running_count().await;
226 if running >= self.max_background_tasks {
227 return ToolResult::error(format!(
228 "Maximum background tasks ({}) reached. Wait for existing tasks to complete.",
229 self.max_background_tasks
230 ));
231 }
232
233 let cancel_rx = self
234 .registry
235 .register(
236 agent_id.clone(),
237 input.subagent_type.clone(),
238 input.description.clone(),
239 )
240 .await;
241
242 context
244 .fire_hook(
245 HookEvent::SubagentStart,
246 HookInput::subagent_start(
247 &session_id,
248 &agent_id,
249 &input.subagent_type,
250 &input.description,
251 ),
252 )
253 .await;
254
255 let registry = self.registry.clone();
256 let task_id = agent_id.clone();
257 let tool_clone = self.clone();
258 let input_clone = input.clone();
259 let prev_messages = previous_messages.clone();
260 let context_clone = context.clone();
261 let session_id_clone = session_id.clone();
262
263 let handle = tokio::spawn(async move {
264 select! {
265 result = tool_clone.spawn_agent(&input_clone, prev_messages) => {
266 match result {
267 Ok(agent_result) => {
268 registry.save_messages(&task_id, agent_result.messages.clone()).await;
269 registry.complete(&task_id, agent_result).await;
270 context_clone.fire_hook(
272 HookEvent::SubagentStop,
273 HookInput::subagent_stop(&session_id_clone, &task_id, true, None),
274 ).await;
275 }
276 Err(e) => {
277 let error_msg = e.to_string();
278 registry.fail(&task_id, error_msg.clone()).await;
279 context_clone.fire_hook(
281 HookEvent::SubagentStop,
282 HookInput::subagent_stop(&session_id_clone, &task_id, false, Some(error_msg)),
283 ).await;
284 }
285 }
286 }
287 _ = cancel_rx => {
288 context_clone.fire_hook(
290 HookEvent::SubagentStop,
291 HookInput::subagent_stop(&session_id_clone, &task_id, false, Some("Cancelled".to_string())),
292 ).await;
293 }
294 }
295 });
296
297 self.registry.set_handle(&agent_id, handle).await;
298
299 let output = TaskOutput {
300 agent_id: agent_id.clone(),
301 result: String::new(),
302 is_running: true,
303 error: None,
304 };
305
306 ToolResult::success(serde_json::to_string_pretty(&output).unwrap_or_else(|_| {
307 format!(
308 "Task '{}' started in background. Agent ID: {}",
309 input.description, agent_id
310 )
311 }))
312 } else {
313 context
315 .fire_hook(
316 HookEvent::SubagentStart,
317 HookInput::subagent_start(
318 &session_id,
319 &agent_id,
320 &input.subagent_type,
321 &input.description,
322 ),
323 )
324 .await;
325
326 match self.spawn_agent(&input, previous_messages).await {
327 Ok(agent_result) => {
328 self.registry
329 .save_messages(&agent_id, agent_result.messages.clone())
330 .await;
331
332 context
334 .fire_hook(
335 HookEvent::SubagentStop,
336 HookInput::subagent_stop(&session_id, &agent_id, true, None),
337 )
338 .await;
339
340 let output = TaskOutput {
341 agent_id,
342 result: agent_result.text.clone(),
343 is_running: false,
344 error: None,
345 };
346 ToolResult::success(
347 serde_json::to_string_pretty(&output).unwrap_or(agent_result.text),
348 )
349 }
350 Err(e) => {
351 let error_msg = e.to_string();
352
353 context
355 .fire_hook(
356 HookEvent::SubagentStop,
357 HookInput::subagent_stop(
358 &session_id,
359 &agent_id,
360 false,
361 Some(error_msg.clone()),
362 ),
363 )
364 .await;
365
366 let output = TaskOutput {
367 agent_id,
368 result: String::new(),
369 is_running: false,
370 error: Some(error_msg.clone()),
371 };
372 ToolResult::error(serde_json::to_string_pretty(&output).unwrap_or(error_msg))
373 }
374 }
375 }
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382 use crate::tools::{ExecutionContext, Tool};
383
384 fn test_context() -> ExecutionContext {
385 ExecutionContext::default()
386 }
387
388 #[test]
389 fn test_task_input_parsing() {
390 let input: TaskInput = serde_json::from_value(serde_json::json!({
391 "description": "Search files",
392 "prompt": "Find all Rust files",
393 "subagent_type": "Explore"
394 }))
395 .unwrap();
396
397 assert_eq!(input.description, "Search files");
398 assert_eq!(input.subagent_type, "Explore");
399 }
400
401 #[tokio::test]
402 async fn test_max_background_limit() {
403 use crate::session::MemoryPersistence;
404 let registry = TaskRegistry::new(std::sync::Arc::new(MemoryPersistence::new()));
405 let tool = TaskTool::new(registry.clone()).with_max_background_tasks(1);
406 let context = test_context();
407
408 registry
409 .register("existing".into(), "Explore".into(), "Existing task".into())
410 .await;
411
412 let result = tool
413 .execute(
414 serde_json::json!({
415 "description": "New task",
416 "prompt": "Do something",
417 "subagent_type": "general-purpose",
418 "run_in_background": true
419 }),
420 &context,
421 )
422 .await;
423
424 assert!(result.is_error());
425 }
426
427 #[test]
428 fn test_subagent_registry_integration() {
429 use crate::session::MemoryPersistence;
430 let registry = TaskRegistry::new(std::sync::Arc::new(MemoryPersistence::new()));
431 let mut subagent_registry = IndexRegistry::new();
432 subagent_registry.register_all(builtin_subagents());
433
434 assert!(subagent_registry.contains("Bash"));
435 assert!(subagent_registry.contains("Explore"));
436 assert!(subagent_registry.contains("Plan"));
437 assert!(subagent_registry.contains("general-purpose"));
438
439 let _tool = TaskTool::new(registry).with_subagent_registry(subagent_registry);
440 }
441}