claude_code_acp/mcp/tools/
task.rs

1//! Task tool for launching sub-agents
2//!
3//! Launches specialized agents to handle complex, multi-step tasks autonomously.
4
5use async_trait::async_trait;
6use serde::Deserialize;
7use serde_json::{Value, json};
8
9use super::base::Tool;
10use crate::mcp::registry::{ToolContext, ToolResult};
11
12/// Input parameters for Task
13#[derive(Debug, Deserialize)]
14struct TaskInput {
15    /// A short description of the task (3-5 words)
16    description: String,
17    /// The task prompt for the agent
18    prompt: String,
19    /// The type of specialized agent to use
20    subagent_type: String,
21    /// Optional model to use for the agent
22    #[serde(default)]
23    model: Option<String>,
24    /// Optional agent ID to resume from
25    #[serde(default)]
26    resume: Option<String>,
27    /// Whether to run this agent in the background
28    #[serde(default)]
29    run_in_background: Option<bool>,
30}
31
32/// Available agent types
33const AGENT_TYPES: &[&str] = &[
34    "general-purpose",
35    "statusline-setup",
36    "Explore",
37    "Plan",
38    "claude-code-guide",
39];
40
41/// Task tool for launching sub-agents
42#[derive(Debug, Default)]
43pub struct TaskTool;
44
45impl TaskTool {
46    /// Create a new Task tool
47    pub fn new() -> Self {
48        Self
49    }
50
51    /// Validate the agent type
52    fn validate_agent_type(agent_type: &str) -> bool {
53        AGENT_TYPES.contains(&agent_type)
54    }
55}
56
57#[async_trait]
58impl Tool for TaskTool {
59    fn name(&self) -> &str {
60        "Task"
61    }
62
63    fn description(&self) -> &str {
64        "Launch a new agent to handle complex, multi-step tasks autonomously. \
65         The Task tool launches specialized agents (subprocesses) that autonomously \
66         handle complex tasks. Each agent type has specific capabilities and tools available to it."
67    }
68
69    fn input_schema(&self) -> Value {
70        json!({
71            "$schema": "http://json-schema.org/draft-07/schema#",
72            "type": "object",
73            "required": ["description", "prompt", "subagent_type"],
74            "additionalProperties": false,
75            "properties": {
76                "description": {
77                    "type": "string",
78                    "description": "A short (3-5 word) description of the task"
79                },
80                "prompt": {
81                    "type": "string",
82                    "description": "The task for the agent to perform"
83                },
84                "subagent_type": {
85                    "type": "string",
86                    "description": "The type of specialized agent to use for this task"
87                },
88                "model": {
89                    "type": "string",
90                    "enum": ["sonnet", "opus", "haiku"],
91                    "description": "Optional model to use for this agent. If not specified, inherits from parent."
92                },
93                "resume": {
94                    "type": "string",
95                    "description": "Optional agent ID to resume from. If provided, the agent continues from the previous execution transcript."
96                },
97                "run_in_background": {
98                    "type": "boolean",
99                    "description": "Set to true to run this agent in the background. Use TaskOutput to read the output later."
100                }
101            }
102        })
103    }
104
105    async fn execute(&self, input: Value, context: &ToolContext) -> ToolResult {
106        // Parse input
107        let params: TaskInput = match serde_json::from_value(input) {
108            Ok(p) => p,
109            Err(e) => return ToolResult::error(format!("Invalid input: {}", e)),
110        };
111
112        // Validate description length
113        let word_count = params.description.split_whitespace().count();
114        if word_count > 10 {
115            return ToolResult::error(
116                "Description should be short (3-5 words). Provided description is too long.",
117            );
118        }
119
120        // Validate agent type
121        if !Self::validate_agent_type(&params.subagent_type) {
122            return ToolResult::error(format!(
123                "Unknown agent type '{}'. Available types: {}",
124                params.subagent_type,
125                AGENT_TYPES.join(", ")
126            ));
127        }
128
129        tracing::info!(
130            "Task request: type={}, description='{}' (session: {})",
131            params.subagent_type,
132            params.description,
133            context.session_id
134        );
135
136        // Generate a task ID
137        let task_id = uuid::Uuid::new_v4().to_string();
138
139        // Build response based on parameters
140        let mut output = String::new();
141
142        if let Some(resume_id) = &params.resume {
143            output.push_str(&format!(
144                "Resuming agent {} with ID: {}\n\n",
145                params.subagent_type, resume_id
146            ));
147        } else {
148            output.push_str(&format!(
149                "Launched {} agent: {}\n\n",
150                params.subagent_type, params.description
151            ));
152        }
153
154        output.push_str(&format!("Agent ID: {}\n", task_id));
155        output.push_str(&format!("Subagent type: {}\n", params.subagent_type));
156
157        if let Some(model) = &params.model {
158            output.push_str(&format!("Model: {}\n", model));
159        }
160
161        if params.run_in_background.unwrap_or(false) {
162            output.push_str("Status: Running in background\n");
163            output.push_str("Use TaskOutput tool to retrieve results when ready.\n");
164        } else {
165            output.push_str("Status: Completed\n");
166        }
167
168        output.push_str(&format!("\nPrompt: {}\n", params.prompt));
169
170        // Note: Full implementation would:
171        // 1. Create a new agent instance with the specified type
172        // 2. Configure it with appropriate tools based on agent type
173        // 3. Execute the prompt and capture results
174        // 4. Support background execution and resume functionality
175        output.push_str(
176            "\nNote: Task tool requires agent orchestration integration for full functionality.",
177        );
178
179        ToolResult::success(output).with_metadata(json!({
180            "task_id": task_id,
181            "subagent_type": params.subagent_type,
182            "description": params.description,
183            "model": params.model,
184            "run_in_background": params.run_in_background.unwrap_or(false),
185            "status": if params.run_in_background.unwrap_or(false) { "running" } else { "completed" }
186        }))
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use tempfile::TempDir;
194
195    #[test]
196    fn test_task_properties() {
197        let tool = TaskTool::new();
198        assert_eq!(tool.name(), "Task");
199        assert!(tool.description().contains("agent"));
200        assert!(tool.description().contains("complex"));
201    }
202
203    #[test]
204    fn test_task_input_schema() {
205        let tool = TaskTool::new();
206        let schema = tool.input_schema();
207
208        assert_eq!(schema["type"], "object");
209        assert!(schema["properties"]["description"].is_object());
210        assert!(schema["properties"]["prompt"].is_object());
211        assert!(schema["properties"]["subagent_type"].is_object());
212        assert!(schema["properties"]["model"].is_object());
213        assert!(schema["properties"]["resume"].is_object());
214        assert!(schema["properties"]["run_in_background"].is_object());
215
216        let required = schema["required"].as_array().unwrap();
217        assert!(required.contains(&json!("description")));
218        assert!(required.contains(&json!("prompt")));
219        assert!(required.contains(&json!("subagent_type")));
220    }
221
222    #[tokio::test]
223    async fn test_task_execute() {
224        let temp_dir = TempDir::new().unwrap();
225        let tool = TaskTool::new();
226        let context = ToolContext::new("test-session", temp_dir.path());
227
228        let result = tool
229            .execute(
230                json!({
231                    "description": "Search for files",
232                    "prompt": "Find all Rust source files",
233                    "subagent_type": "Explore"
234                }),
235                &context,
236            )
237            .await;
238
239        assert!(!result.is_error);
240        assert!(result.content.contains("Explore"));
241        assert!(result.content.contains("Agent ID"));
242    }
243
244    #[tokio::test]
245    async fn test_task_with_model() {
246        let temp_dir = TempDir::new().unwrap();
247        let tool = TaskTool::new();
248        let context = ToolContext::new("test-session", temp_dir.path());
249
250        let result = tool
251            .execute(
252                json!({
253                    "description": "Quick task",
254                    "prompt": "Do something simple",
255                    "subagent_type": "general-purpose",
256                    "model": "haiku"
257                }),
258                &context,
259            )
260            .await;
261
262        assert!(!result.is_error);
263        assert!(result.content.contains("haiku"));
264    }
265
266    #[tokio::test]
267    async fn test_task_background() {
268        let temp_dir = TempDir::new().unwrap();
269        let tool = TaskTool::new();
270        let context = ToolContext::new("test-session", temp_dir.path());
271
272        let result = tool
273            .execute(
274                json!({
275                    "description": "Background task",
276                    "prompt": "Run something in background",
277                    "subagent_type": "general-purpose",
278                    "run_in_background": true
279                }),
280                &context,
281            )
282            .await;
283
284        assert!(!result.is_error);
285        assert!(result.content.contains("Running in background"));
286        assert!(result.content.contains("TaskOutput"));
287    }
288
289    #[tokio::test]
290    async fn test_task_resume() {
291        let temp_dir = TempDir::new().unwrap();
292        let tool = TaskTool::new();
293        let context = ToolContext::new("test-session", temp_dir.path());
294
295        let result = tool
296            .execute(
297                json!({
298                    "description": "Resume task",
299                    "prompt": "Continue previous work",
300                    "subagent_type": "Explore",
301                    "resume": "previous-agent-id-123"
302                }),
303                &context,
304            )
305            .await;
306
307        assert!(!result.is_error);
308        assert!(result.content.contains("Resuming"));
309        assert!(result.content.contains("previous-agent-id-123"));
310    }
311
312    #[tokio::test]
313    async fn test_task_invalid_agent_type() {
314        let temp_dir = TempDir::new().unwrap();
315        let tool = TaskTool::new();
316        let context = ToolContext::new("test-session", temp_dir.path());
317
318        let result = tool
319            .execute(
320                json!({
321                    "description": "Test task",
322                    "prompt": "Do something",
323                    "subagent_type": "invalid-type"
324                }),
325                &context,
326            )
327            .await;
328
329        assert!(result.is_error);
330        assert!(result.content.contains("Unknown agent type"));
331    }
332
333    #[tokio::test]
334    async fn test_task_long_description() {
335        let temp_dir = TempDir::new().unwrap();
336        let tool = TaskTool::new();
337        let context = ToolContext::new("test-session", temp_dir.path());
338
339        let result = tool
340            .execute(
341                json!({
342                    "description": "This is a very long description that contains way too many words",
343                    "prompt": "Do something",
344                    "subagent_type": "Explore"
345                }),
346                &context,
347            )
348            .await;
349
350        assert!(result.is_error);
351        assert!(result.content.contains("too long"));
352    }
353
354    #[test]
355    fn test_validate_agent_type() {
356        assert!(TaskTool::validate_agent_type("general-purpose"));
357        assert!(TaskTool::validate_agent_type("Explore"));
358        assert!(TaskTool::validate_agent_type("Plan"));
359        assert!(!TaskTool::validate_agent_type("unknown"));
360    }
361}