Skip to main content

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 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    /// Generate description with dynamic subagent list.
47    ///
48    /// Use this method when building system prompts to include all registered
49    /// subagents (both built-in and custom) in the tool description.
50    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    /// A short (3-5 word) description of the task
179    pub description: String,
180    /// The task for the agent to perform
181    pub prompt: String,
182    /// The type of specialized agent to use for this task
183    pub subagent_type: String,
184    /// Optional model to use (sonnet/opus/haiku). Prefer haiku for quick tasks.
185    #[serde(default)]
186    pub model: Option<String>,
187    /// Set to true to run in background. Use TaskOutput to read the output later.
188    #[serde(default)]
189    pub run_in_background: Option<bool>,
190    /// Optional agent ID to resume from. The agent continues with preserved context.
191    #[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}