claude_agent/agent/
task.rs

1//! TaskTool - spawns and manages subagent tasks.
2
3use 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    /// Generate description with dynamic subagent list.
50    ///
51    /// Use this method when building system prompts to include all registered
52    /// subagents (both built-in and custom) in the tool description.
53    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    /// A short (3-5 word) description of the task
150    pub description: String,
151    /// The task for the agent to perform
152    pub prompt: String,
153    /// The type of specialized agent to use for this task
154    pub subagent_type: String,
155    /// Optional model to use (sonnet/opus/haiku). Prefer haiku for quick tasks.
156    #[serde(default)]
157    pub model: Option<String>,
158    /// Set to true to run in background. Use TaskOutput to read the output later.
159    #[serde(default)]
160    pub run_in_background: Option<bool>,
161    /// Optional agent ID to resume from. The agent continues with preserved context.
162    #[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            // Fire SubagentStart hook
243            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                                // Fire SubagentStop hook (success)
271                                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                                // Fire SubagentStop hook (failure)
280                                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                        // Fire SubagentStop hook (cancelled)
289                        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            // Fire SubagentStart hook
314            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                    // Fire SubagentStop hook (success)
333                    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                    // Fire SubagentStop hook (failure)
354                    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}