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 subagent_registry(mut self, subagent_registry: IndexRegistry<SubagentIndex>) -> Self {
37 self.subagent_registry = subagent_registry;
38 self
39 }
40
41 pub fn max_background_tasks(mut self, max: usize) -> Self {
42 self.max_background_tasks = max;
43 self
44 }
45
46 pub fn description_with_subagents(&self) -> String {
51 let subagents_desc = self
52 .subagent_registry
53 .iter()
54 .map(|subagent| subagent.to_summary_line())
55 .collect::<Vec<_>>()
56 .join("\n");
57
58 format!(
59 r#"Launch a new agent to handle complex, multi-step tasks autonomously.
60
61The Task tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.
62
63Available agent types and the tools they have access to:
64{}
65
66When using the Task tool, you must specify a subagent_type parameter to select which agent type to use.
67
68When NOT to use the Task tool:
69- 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
70- If you are searching for a specific class definition like "class Foo", use the Grep tool instead, to find the match more quickly
71- 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
72- Other tasks that are not related to the agent descriptions above
73
74Usage notes:
75- Always include a short description (3-5 words) summarizing what the agent will do
76- Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
77- 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.
78- 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.
79- 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.
80- Provide clear, detailed prompts so the agent can work autonomously and return exactly the information you need.
81- The agent's outputs should generally be trusted
82- 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
83- If you need to launch multiple agents in parallel, send a single message with multiple Task tool calls.
84- Use model="haiku" for quick, straightforward tasks to minimize cost and latency"#,
85 subagents_desc
86 )
87 }
88
89 async fn spawn_agent(
90 &self,
91 input: &TaskInput,
92 previous_messages: Option<Vec<Message>>,
93 ) -> crate::Result<super::AgentResult> {
94 let subagent = self
95 .subagent_registry
96 .get(&input.subagent_type)
97 .ok_or_else(|| {
98 crate::Error::Config(format!("Unknown subagent type: {}", input.subagent_type))
99 })?;
100
101 let provider = CloudProvider::from_env();
102 let model_config = provider.default_models();
103
104 let model = input
105 .model
106 .as_deref()
107 .map(|m| model_config.resolve_alias(m))
108 .or(subagent.model.as_deref())
109 .unwrap_or_else(|| subagent.resolve_model(&model_config))
110 .to_string();
111
112 let agent = AgentBuilder::new()
113 .auth(Auth::FromEnv)
114 .await?
115 .model(&model)
116 .max_iterations(50)
117 .build()
118 .await?;
119
120 match previous_messages {
121 Some(messages) if !messages.is_empty() => {
122 debug!(
123 message_count = messages.len(),
124 "Resuming agent with previous context"
125 );
126 agent.execute_with_messages(messages, &input.prompt).await
127 }
128 _ => agent.execute(&input.prompt).await,
129 }
130 }
131}
132
133impl TaskTool {
134 async fn fire_start_hook(
135 context: &ExecutionContext,
136 session_id: &str,
137 agent_id: &str,
138 subagent_type: &str,
139 description: &str,
140 ) {
141 context
142 .fire_hook(
143 HookEvent::SubagentStart,
144 HookInput::subagent_start(session_id, agent_id, subagent_type, description),
145 )
146 .await;
147 }
148
149 async fn fire_stop_hook(
150 context: &ExecutionContext,
151 session_id: &str,
152 agent_id: &str,
153 success: bool,
154 error: Option<String>,
155 ) {
156 context
157 .fire_hook(
158 HookEvent::SubagentStop,
159 HookInput::subagent_stop(session_id, agent_id, success, error),
160 )
161 .await;
162 }
163}
164
165impl Clone for TaskTool {
166 fn clone(&self) -> Self {
167 Self {
168 registry: self.registry.clone(),
169 subagent_registry: self.subagent_registry.clone(),
170 max_background_tasks: self.max_background_tasks,
171 }
172 }
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
176#[schemars(deny_unknown_fields)]
177pub struct TaskInput {
178 pub description: String,
180 pub prompt: String,
182 pub subagent_type: String,
184 #[serde(default)]
186 pub model: Option<String>,
187 #[serde(default)]
189 pub run_in_background: Option<bool>,
190 #[serde(default)]
192 pub resume: Option<String>,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct TaskOutput {
197 pub agent_id: String,
198 pub result: String,
199 pub is_running: bool,
200 #[serde(skip_serializing_if = "Option::is_none")]
201 pub error: Option<String>,
202}
203
204#[async_trait]
205impl SchemaTool for TaskTool {
206 type Input = TaskInput;
207
208 const NAME: &'static str = "Task";
209 const DESCRIPTION: &'static str = "Launch a new agent to handle complex, multi-step tasks autonomously. Use description_with_subagents() for the full dynamic description including available agent types.";
210
211 fn custom_description(&self) -> Option<String> {
212 Some(self.description_with_subagents())
213 }
214
215 async fn handle(&self, input: TaskInput, context: &ExecutionContext) -> ToolResult {
216 let previous_messages = if let Some(ref resume_id) = input.resume {
217 self.registry.get_messages(resume_id).await
218 } else {
219 None
220 };
221
222 let agent_id = input
223 .resume
224 .clone()
225 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()[..7].to_string());
226
227 let session_id = context.session_id().unwrap_or("").to_string();
228 let run_in_background = input.run_in_background.unwrap_or(false);
229
230 if run_in_background {
231 let running = self.registry.running_count().await;
232 if running >= self.max_background_tasks {
233 return ToolResult::error(format!(
234 "Maximum background tasks ({}) reached. Wait for existing tasks to complete.",
235 self.max_background_tasks
236 ));
237 }
238
239 let cancel_rx = self
240 .registry
241 .register(
242 agent_id.clone(),
243 input.subagent_type.clone(),
244 input.description.clone(),
245 )
246 .await;
247
248 Self::fire_start_hook(
249 context,
250 &session_id,
251 &agent_id,
252 &input.subagent_type,
253 &input.description,
254 )
255 .await;
256
257 let registry = self.registry.clone();
258 let task_id = agent_id.clone();
259 let tool_clone = self.clone();
260 let input_clone = input.clone();
261 let prev_messages = previous_messages.clone();
262 let context_clone = context.clone();
263 let session_id_clone = session_id.clone();
264
265 let handle = tokio::spawn(async move {
266 select! {
267 result = tool_clone.spawn_agent(&input_clone, prev_messages) => {
268 match result {
269 Ok(agent_result) => {
270 registry.save_messages(&task_id, agent_result.messages.clone()).await;
271 registry.complete(&task_id, agent_result).await;
272 Self::fire_stop_hook(&context_clone, &session_id_clone, &task_id, true, None).await;
273 }
274 Err(e) => {
275 let error_msg = e.to_string();
276 registry.fail(&task_id, error_msg.clone()).await;
277 Self::fire_stop_hook(&context_clone, &session_id_clone, &task_id, false, Some(error_msg)).await;
278 }
279 }
280 }
281 _ = cancel_rx => {
282 Self::fire_stop_hook(&context_clone, &session_id_clone, &task_id, false, Some("Cancelled".to_string())).await;
283 }
284 }
285 });
286
287 self.registry.set_handle(&agent_id, handle).await;
288
289 let output = TaskOutput {
290 agent_id: agent_id.clone(),
291 result: String::new(),
292 is_running: true,
293 error: None,
294 };
295
296 ToolResult::success(serde_json::to_string_pretty(&output).unwrap_or_else(|_| {
297 format!(
298 "Task '{}' started in background. Agent ID: {}",
299 input.description, agent_id
300 )
301 }))
302 } else {
303 Self::fire_start_hook(
304 context,
305 &session_id,
306 &agent_id,
307 &input.subagent_type,
308 &input.description,
309 )
310 .await;
311
312 match self.spawn_agent(&input, previous_messages).await {
313 Ok(agent_result) => {
314 self.registry
315 .save_messages(&agent_id, agent_result.messages.clone())
316 .await;
317 Self::fire_stop_hook(context, &session_id, &agent_id, true, None).await;
318
319 let output = TaskOutput {
320 agent_id,
321 result: agent_result.text.clone(),
322 is_running: false,
323 error: None,
324 };
325 ToolResult::success(
326 serde_json::to_string_pretty(&output).unwrap_or(agent_result.text),
327 )
328 }
329 Err(e) => {
330 let error_msg = e.to_string();
331 Self::fire_stop_hook(
332 context,
333 &session_id,
334 &agent_id,
335 false,
336 Some(error_msg.clone()),
337 )
338 .await;
339
340 let output = TaskOutput {
341 agent_id,
342 result: String::new(),
343 is_running: false,
344 error: Some(error_msg.clone()),
345 };
346 ToolResult::error(serde_json::to_string_pretty(&output).unwrap_or(error_msg))
347 }
348 }
349 }
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356 use crate::tools::{ExecutionContext, Tool};
357
358 fn test_context() -> ExecutionContext {
359 ExecutionContext::default()
360 }
361
362 #[test]
363 fn test_task_input_parsing() {
364 let input: TaskInput = serde_json::from_value(serde_json::json!({
365 "description": "Search files",
366 "prompt": "Find all Rust files",
367 "subagent_type": "Explore"
368 }))
369 .unwrap();
370
371 assert_eq!(input.description, "Search files");
372 assert_eq!(input.subagent_type, "Explore");
373 }
374
375 #[tokio::test]
376 async fn test_max_background_limit() {
377 use crate::session::MemoryPersistence;
378 let registry = TaskRegistry::new(std::sync::Arc::new(MemoryPersistence::new()));
379 let tool = TaskTool::new(registry.clone()).max_background_tasks(1);
380 let context = test_context();
381
382 registry
383 .register("existing".into(), "Explore".into(), "Existing task".into())
384 .await;
385
386 let result = tool
387 .execute(
388 serde_json::json!({
389 "description": "New task",
390 "prompt": "Do something",
391 "subagent_type": "general-purpose",
392 "run_in_background": true
393 }),
394 &context,
395 )
396 .await;
397
398 assert!(result.is_error());
399 }
400
401 #[test]
402 fn test_subagent_registry_integration() {
403 use crate::session::MemoryPersistence;
404 let registry = TaskRegistry::new(std::sync::Arc::new(MemoryPersistence::new()));
405 let mut subagent_registry = IndexRegistry::new();
406 subagent_registry.register_all(builtin_subagents());
407
408 assert!(subagent_registry.contains("Bash"));
409 assert!(subagent_registry.contains("Explore"));
410 assert!(subagent_registry.contains("Plan"));
411 assert!(subagent_registry.contains("general-purpose"));
412
413 let _tool = TaskTool::new(registry).subagent_registry(subagent_registry);
414 }
415}