claude_code_acp/mcp/tools/
task.rs1use 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#[derive(Debug, Deserialize)]
14struct TaskInput {
15 description: String,
17 prompt: String,
19 subagent_type: String,
21 #[serde(default)]
23 model: Option<String>,
24 #[serde(default)]
26 resume: Option<String>,
27 #[serde(default)]
29 run_in_background: Option<bool>,
30}
31
32const AGENT_TYPES: &[&str] = &[
34 "general-purpose",
35 "statusline-setup",
36 "Explore",
37 "Plan",
38 "claude-code-guide",
39];
40
41#[derive(Debug, Default)]
43pub struct TaskTool;
44
45impl TaskTool {
46 pub fn new() -> Self {
48 Self
49 }
50
51 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 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 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 if !Self::validate_agent_type(¶ms.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 let task_id = uuid::Uuid::new_v4().to_string();
138
139 let mut output = String::new();
141
142 if let Some(resume_id) = ¶ms.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) = ¶ms.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 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}