1use super::Tool;
7use crate::{PawanError, Result};
8use async_trait::async_trait;
9use serde_json::{json, Value};
10use std::path::PathBuf;
11use std::process::Stdio;
12use tokio::io::AsyncReadExt;
13use tokio::process::Command;
14use std::io::Write;
15use tracing;
16
17pub struct SpawnAgentTool {
19 workspace_root: PathBuf,
20}
21
22impl SpawnAgentTool {
23 pub fn new(workspace_root: PathBuf) -> Self {
24 Self { workspace_root }
25 }
26
27 fn find_pawan_binary(&self) -> String {
29 for candidate in &[
31 self.workspace_root.join("target/release/pawan"),
32 self.workspace_root.join("target/debug/pawan"),
33 ] {
34 if candidate.exists() {
35 return candidate.to_string_lossy().to_string();
36 }
37 }
38 "pawan".to_string()
40 }
41}
42
43#[async_trait]
44impl Tool for SpawnAgentTool {
45 fn name(&self) -> &str {
46 "spawn_agent"
47 }
48
49 fn description(&self) -> &str {
50 "Spawn a sub-agent (pawan subprocess) to handle a task independently. \
51 Returns the agent's response as JSON. Use this for parallel or delegated tasks."
52 }
53
54 fn mutating(&self) -> bool {
55 true }
57
58 fn parameters_schema(&self) -> Value {
59 json!({
60 "type": "object",
61 "properties": {
62 "prompt": {
63 "type": "string",
64 "description": "The task/prompt for the sub-agent"
65 },
66 "model": {
67 "type": "string",
68 "description": "Model to use (optional, defaults to parent's model)"
69 },
70 "timeout": {
71 "type": "integer",
72 "description": "Timeout in seconds (default: 120)"
73 },
74 "workspace": {
75 "type": "string",
76 "description": "Workspace directory for the sub-agent (default: same as parent)"
77 },
78 "retries": {
79 "type": "integer",
80 "description": "Number of retry attempts on failure (default: 0, max: 2)"
81 }
82 },
83 "required": ["prompt"]
84 })
85 }
86
87 fn thulp_definition(&self) -> thulp_core::ToolDefinition {
88 use thulp_core::{Parameter, ParameterType};
89 thulp_core::ToolDefinition::builder("spawn_agent")
90 .description(self.description())
91 .parameter(Parameter::builder("prompt").param_type(ParameterType::String).required(true)
92 .description("The task/prompt for the sub-agent").build())
93 .parameter(Parameter::builder("model").param_type(ParameterType::String).required(false)
94 .description("Model to use (optional, defaults to parent's model)").build())
95 .parameter(Parameter::builder("timeout").param_type(ParameterType::Integer).required(false)
96 .description("Timeout in seconds (default: 120)").build())
97 .parameter(Parameter::builder("workspace").param_type(ParameterType::String).required(false)
98 .description("Workspace directory for the sub-agent (default: same as parent)").build())
99 .parameter(Parameter::builder("retries").param_type(ParameterType::Integer).required(false)
100 .description("Number of retry attempts on failure (default: 0, max: 2)").build())
101 .build()
102 }
103
104 async fn execute(&self, args: Value) -> Result<Value> {
105 let prompt = args["prompt"]
106 .as_str()
107 .ok_or_else(|| PawanError::Tool("prompt is required for spawn_agent".into()))?;
108
109 let timeout = args["timeout"].as_u64().unwrap_or(120);
110 let model = args["model"].as_str();
111 let workspace = args["workspace"]
112 .as_str()
113 .map(PathBuf::from)
114 .unwrap_or_else(|| self.workspace_root.clone());
115 let max_retries = args["retries"].as_u64().unwrap_or(0).min(2) as usize;
116
117 let agent_id = uuid::Uuid::new_v4().to_string()[..8].to_string();
119 let status_path = format!("/tmp/pawan-agent-{}.status", agent_id);
120 let started_at = chrono::Utc::now().to_rfc3339();
121
122 let pawan_bin = self.find_pawan_binary();
123
124 for attempt in 0..=max_retries {
125 let mut cmd = Command::new(&pawan_bin);
126 cmd.arg("run")
127 .arg("-o")
128 .arg("json")
129 .arg("--timeout")
130 .arg(timeout.to_string())
131 .arg("-w")
132 .arg(workspace.to_string_lossy().to_string());
133
134 if let Some(m) = model {
135 cmd.arg("-m").arg(m);
136 }
137
138 cmd.arg(prompt);
139
140 cmd.stdout(Stdio::piped())
141 .stderr(Stdio::piped())
142 .stdin(Stdio::null());
143
144 if let Ok(mut f) = std::fs::File::create(&status_path) {
146 let _ = write!(f, r#"{{"state":"running","prompt":"{}","started_at":"{}","attempt":{}}}"#,
147 prompt.chars().take(100).collect::<String>().replace('"', "'"), started_at, attempt + 1);
148 }
149
150 let mut child = cmd.spawn().map_err(|e| {
151 PawanError::Tool(format!(
152 "Failed to spawn sub-agent: {}. Binary: {}",
153 e, pawan_bin
154 ))
155 })?;
156
157 let mut stdout = String::new();
158 let mut stderr = String::new();
159
160 if let Some(mut handle) = child.stdout.take() {
161 handle.read_to_string(&mut stdout).await.ok();
162 }
163 if let Some(mut handle) = child.stderr.take() {
164 handle.read_to_string(&mut stderr).await.ok();
165 }
166
167 let status = child.wait().await.map_err(PawanError::Io)?;
168
169 let result = if let Ok(json_result) = serde_json::from_str::<Value>(&stdout) {
170 json_result
171 } else {
172 json!({
173 "content": stdout.trim(),
174 "raw_output": true
175 })
176 };
177
178 if status.success() || attempt == max_retries {
179 let duration_ms = chrono::Utc::now().signed_duration_since(chrono::DateTime::parse_from_rfc3339(&started_at).unwrap_or_default()).num_milliseconds();
181 if let Ok(mut f) = std::fs::File::create(&status_path) {
182 let state = if status.success() { "done" } else { "failed" };
183 let _ = write!(f, r#"{{"state":"{}","exit_code":{},"duration_ms":{},"attempt":{}}}"#,
184 state, status.code().unwrap_or(-1), duration_ms, attempt + 1);
185 }
186
187 return Ok(json!({
188 "success": status.success(),
189 "attempt": attempt + 1,
190 "total_attempts": attempt + 1,
191 "result": result,
192 "stderr": stderr.trim(),
193 }));
194 }
195 tracing::warn!(attempt = attempt + 1, "spawn_agent attempt failed, retrying");
198 }
199
200 Err(PawanError::Tool("spawn_agent: all retry attempts exhausted".into()))
202 }
203}
204
205pub struct SpawnAgentsTool {
207 workspace_root: PathBuf,
208}
209
210impl SpawnAgentsTool {
211 pub fn new(workspace_root: PathBuf) -> Self {
212 Self { workspace_root }
213 }
214}
215
216#[async_trait]
217impl Tool for SpawnAgentsTool {
218 fn name(&self) -> &str {
219 "spawn_agents"
220 }
221
222 fn description(&self) -> &str {
223 "Spawn multiple sub-agents in parallel. Each task runs concurrently and results are returned as an array."
224 }
225
226 fn parameters_schema(&self) -> Value {
227 json!({
228 "type": "object",
229 "properties": {
230 "tasks": {
231 "type": "array",
232 "items": {
233 "type": "object",
234 "properties": {
235 "prompt": {"type": "string"},
236 "model": {"type": "string"},
237 "timeout": {"type": "integer"},
238 "workspace": {"type": "string"}
239 },
240 "required": ["prompt"]
241 }
242 }
243 },
244 "required": ["tasks"]
245 })
246 }
247
248 fn thulp_definition(&self) -> thulp_core::ToolDefinition {
249 use thulp_core::{Parameter, ParameterType};
250 thulp_core::ToolDefinition::builder("spawn_agents")
251 .description(self.description())
252 .parameter(Parameter::builder("tasks").param_type(ParameterType::Array).required(true)
253 .description("Array of task objects, each with prompt (required), model, timeout, workspace").build())
254 .build()
255 }
256
257 fn mutating(&self) -> bool {
258 true }
260
261 async fn execute(&self, args: Value) -> Result<Value> {
262 let tasks = args["tasks"]
263 .as_array()
264 .ok_or_else(|| PawanError::Tool("tasks array is required for spawn_agents".into()))?;
265
266 let single_tool = SpawnAgentTool::new(self.workspace_root.clone());
267
268 let futures: Vec<_> = tasks
269 .iter()
270 .map(|task| single_tool.execute(task.clone()))
271 .collect();
272
273 let results = futures::future::join_all(futures).await;
274
275 let output: Vec<Value> = results
276 .into_iter()
277 .map(|r| match r {
278 Ok(v) => v,
279 Err(e) => json!({"success": false, "error": e.to_string()}),
280 })
281 .collect();
282
283 Ok(json!({
284 "success": true,
285 "results": output,
286 "total_tasks": tasks.len(),
287 }))
288 }
289}
290#[cfg(test)]
291mod tests {
292 use super::*;
293 use tempfile::TempDir;
294 #[cfg(unix)]
295 use std::os::unix::fs::PermissionsExt;
296 #[test]
297 fn test_spawn_agent_tool_name() {
298 let tmp = TempDir::new().unwrap();
299 let tool = SpawnAgentTool::new(tmp.path().to_path_buf());
300 assert_eq!(tool.name(), "spawn_agent");
301 }
302
303 #[test]
304 fn test_spawn_agents_tool_name() {
305 let tmp = TempDir::new().unwrap();
306 let tool = SpawnAgentsTool::new(tmp.path().to_path_buf());
307 assert_eq!(tool.name(), "spawn_agents");
308 }
309
310 #[test]
311 fn test_spawn_agent_schema_has_prompt() {
312 let tmp = TempDir::new().unwrap();
313 let tool = SpawnAgentTool::new(tmp.path().to_path_buf());
314 let schema = tool.parameters_schema();
315 assert!(schema["properties"]["prompt"].is_object());
316 assert!(schema["required"].as_array().unwrap().iter().any(|v| v == "prompt"));
317 }
318
319 #[test]
320 fn test_find_pawan_binary_prefers_release_over_debug() {
321 let tmp = TempDir::new().unwrap();
322 std::fs::create_dir_all(tmp.path().join("target/release")).unwrap();
324 std::fs::create_dir_all(tmp.path().join("target/debug")).unwrap();
325 let release = tmp.path().join("target/release/pawan");
326 let debug = tmp.path().join("target/debug/pawan");
327 std::fs::write(&release, "#!/bin/sh\necho release").unwrap();
328 std::fs::write(&debug, "#!/bin/sh\necho debug").unwrap();
329
330 let tool = SpawnAgentTool::new(tmp.path().to_path_buf());
331 let binary = tool.find_pawan_binary();
332 assert_eq!(
333 binary,
334 release.to_string_lossy().to_string(),
335 "release binary must win over debug"
336 );
337 }
338
339 #[test]
340 fn test_find_pawan_binary_falls_back_to_debug_when_no_release() {
341 let tmp = TempDir::new().unwrap();
342 std::fs::create_dir_all(tmp.path().join("target/debug")).unwrap();
343 let debug = tmp.path().join("target/debug/pawan");
344 std::fs::write(&debug, "#!/bin/sh\necho debug").unwrap();
345
346 let tool = SpawnAgentTool::new(tmp.path().to_path_buf());
347 let binary = tool.find_pawan_binary();
348 assert_eq!(binary, debug.to_string_lossy().to_string());
349 }
350
351 #[test]
352 fn test_find_pawan_binary_falls_through_to_path_when_nothing_in_workspace() {
353 let tmp = TempDir::new().unwrap();
354 let tool = SpawnAgentTool::new(tmp.path().to_path_buf());
356 let binary = tool.find_pawan_binary();
357 assert_eq!(binary, "pawan");
359 }
360
361 #[tokio::test]
362 async fn test_spawn_agent_missing_prompt_errors() {
363 let tmp = TempDir::new().unwrap();
364 let tool = SpawnAgentTool::new(tmp.path().to_path_buf());
365 let result = tool.execute(json!({ "model": "test-model" })).await;
367 assert!(result.is_err(), "missing prompt must error");
368 let err = format!("{}", result.unwrap_err());
369 assert!(
370 err.contains("prompt"),
371 "error message should mention prompt, got: {}",
372 err
373 );
374 }
375
376 #[test]
377 fn test_spawn_agents_schema_requires_tasks_array() {
378 let tmp = TempDir::new().unwrap();
379 let tool = SpawnAgentsTool::new(tmp.path().to_path_buf());
380 let schema = tool.parameters_schema();
381 let required = schema["required"].as_array().unwrap();
382 assert!(required.iter().any(|v| v == "tasks"), "tasks must be required");
383 let tasks_type = schema["properties"]["tasks"]["type"].as_str();
385 assert_eq!(tasks_type, Some("array"));
386 }
387
388 #[tokio::test]
389 async fn test_spawn_agents_empty_tasks_succeeds_with_zero_results() {
390 let tmp = TempDir::new().unwrap();
391 let tool = SpawnAgentsTool::new(tmp.path().to_path_buf());
392 let result = tool.execute(json!({ "tasks": [] })).await.unwrap();
393 assert_eq!(result["success"], true);
394 assert_eq!(result["total_tasks"], 0);
395 assert_eq!(result["results"].as_array().unwrap().len(), 0);
396 }
397
398 #[tokio::test]
399 async fn test_spawn_agents_missing_tasks_errors() {
400 let tmp = TempDir::new().unwrap();
401 let tool = SpawnAgentsTool::new(tmp.path().to_path_buf());
402 let result = tool.execute(json!({})).await;
404 assert!(result.is_err());
405 let err = format!("{}", result.unwrap_err());
406 assert!(err.contains("tasks"));
407 }
408
409 #[tokio::test]
410 async fn test_spawn_agent_prompt_non_string_errors() {
411 let tmp = TempDir::new().unwrap();
416 let tool = SpawnAgentTool::new(tmp.path().to_path_buf());
417 let result = tool.execute(json!({ "prompt": 42 })).await;
418 assert!(result.is_err(), "non-string prompt must error");
419 let err = format!("{}", result.unwrap_err());
420 assert!(err.contains("prompt"), "error should mention 'prompt', got: {}", err);
421 }
422
423 #[tokio::test]
424 async fn test_spawn_agents_tasks_non_array_errors() {
425 let tmp = TempDir::new().unwrap();
428 let tool = SpawnAgentsTool::new(tmp.path().to_path_buf());
429 let result = tool.execute(json!({ "tasks": "not an array" })).await;
430 assert!(result.is_err(), "non-array tasks must error");
431 let err = format!("{}", result.unwrap_err());
432 assert!(err.contains("tasks"), "error should mention 'tasks', got: {}", err);
433 }
434
435 #[test]
436 fn test_spawn_agent_schema_lists_all_optional_params() {
437 let tmp = TempDir::new().unwrap();
441 let tool = SpawnAgentTool::new(tmp.path().to_path_buf());
442 let schema = tool.parameters_schema();
443 let props = schema["properties"].as_object().unwrap();
444 for p in &["prompt", "model", "timeout", "workspace", "retries"] {
445 assert!(props.contains_key(*p), "schema missing '{}'", p);
446 }
447 let required = schema["required"].as_array().unwrap();
449 assert_eq!(required.len(), 1);
450 assert_eq!(required[0], "prompt");
451 }
452
453 #[test]
454 fn test_spawn_agents_schema_tasks_items_has_prompt_required() {
455 let tmp = TempDir::new().unwrap();
460 let tool = SpawnAgentsTool::new(tmp.path().to_path_buf());
461 let schema = tool.parameters_schema();
462 let items_required = schema["properties"]["tasks"]["items"]["required"]
463 .as_array()
464 .expect("tasks.items.required should exist");
465 assert!(items_required.iter().any(|v| v == "prompt"));
466 }
467
468 #[test]
469 fn test_spawn_agent_thulp_definition_has_all_5_params() {
470 let tmp = TempDir::new().unwrap();
473 let tool = SpawnAgentTool::new(tmp.path().to_path_buf());
474 let def = tool.thulp_definition();
475 assert_eq!(def.name, "spawn_agent");
476 let param_names: Vec<&str> = def.parameters.iter().map(|p| p.name.as_str()).collect();
477 for p in &["prompt", "model", "timeout", "workspace", "retries"] {
478 assert!(param_names.contains(p), "thulp definition missing '{}'", p);
479 }
480 let required_count = def.parameters.iter().filter(|p| p.required).count();
482 assert_eq!(required_count, 1, "only prompt should be required");
483 }
484
485 #[test]
486 fn test_spawn_agents_thulp_definition_has_tasks_param() {
487 let tmp = TempDir::new().unwrap();
491 let tool = SpawnAgentsTool::new(tmp.path().to_path_buf());
492 let def = tool.thulp_definition();
493 assert_eq!(def.name, "spawn_agents");
494 assert_eq!(def.parameters.len(), 1);
495 let tasks_param = &def.parameters[0];
496 assert_eq!(tasks_param.name, "tasks");
497 assert!(tasks_param.required);
498 }
499
500 #[test]
501 fn test_spawn_agent_mutating_returns_true() {
502 let tmp = TempDir::new().unwrap();
503 let tool = SpawnAgentTool::new(tmp.path().to_path_buf());
504 assert!(tool.mutating(), "spawn_agent can mutate state");
505 }
506
507 #[test]
508 fn test_spawn_agents_mutating_returns_true() {
509 let tmp = TempDir::new().unwrap();
510 let tool = SpawnAgentsTool::new(tmp.path().to_path_buf());
511 assert!(tool.mutating(), "spawn_agents can mutate state");
512 }
513
514 #[test]
515 fn test_spawn_agent_description_non_empty() {
516 let tmp = TempDir::new().unwrap();
517 let tool = SpawnAgentTool::new(tmp.path().to_path_buf());
518 let desc = tool.description();
519 assert!(!desc.is_empty());
520 assert!(desc.contains("sub-agent"));
521 }
522
523 #[test]
524 fn test_spawn_agents_description_non_empty() {
525 let tmp = TempDir::new().unwrap();
526 let tool = SpawnAgentsTool::new(tmp.path().to_path_buf());
527 let desc = tool.description();
528 assert!(!desc.is_empty());
529 assert!(desc.contains("parallel"));
530 }
531
532 #[tokio::test]
533 async fn test_spawn_agent_timeout_defaults_to_120() {
534 let tmp = TempDir::new().unwrap();
535 std::fs::create_dir_all(tmp.path().join("target/release")).unwrap();
536 let binary = tmp.path().join("target/release/pawan");
537 std::fs::write(&binary, r"#!/bin/sh
538exit 0").unwrap();
539 #[cfg(unix)]
540 std::fs::set_permissions(&binary, std::os::unix::fs::PermissionsExt::from_mode(0o755)).unwrap();
541
542 let tool = SpawnAgentTool::new(tmp.path().to_path_buf());
543 let result = tool.execute(json!({"prompt": "test"})).await.unwrap();
544 assert_eq!(result["success"], true);
545 }
546
547 #[tokio::test]
548 async fn test_spawn_agent_custom_timeout() {
549 let tmp = TempDir::new().unwrap();
550 std::fs::create_dir_all(tmp.path().join("target/release")).unwrap();
551 let binary = tmp.path().join("target/release/pawan");
552 std::fs::write(&binary, r"#!/bin/sh
553exit 0").unwrap();
554 #[cfg(unix)]
555 std::fs::set_permissions(&binary, std::os::unix::fs::PermissionsExt::from_mode(0o755)).unwrap();
556
557 let tool = SpawnAgentTool::new(tmp.path().to_path_buf());
558 let result = tool.execute(json!({"prompt": "test", "timeout": 60})).await.unwrap();
559 assert_eq!(result["success"], true);
560 }
561
562 #[tokio::test]
563 async fn test_spawn_agent_custom_model() {
564 let tmp = TempDir::new().unwrap();
565 std::fs::create_dir_all(tmp.path().join("target/release")).unwrap();
566 let binary = tmp.path().join("target/release/pawan");
567 std::fs::write(&binary, "#!/bin/sh\necho '{\"content\":\"test response\"}'").unwrap();
568 #[cfg(unix)]
569 std::fs::set_permissions(&binary, std::os::unix::fs::PermissionsExt::from_mode(0o755)).unwrap();
570
571 let tool = SpawnAgentTool::new(tmp.path().to_path_buf());
572 let result = tool.execute(json!({"prompt": "test", "model": "gpt-4"})).await.unwrap();
573 assert_eq!(result["success"], true);
574 assert_eq!(result["result"]["content"], "test response");
575 }
576
577
578 #[tokio::test]
579 async fn test_spawn_agent_retries_on_failure() {
580 let tmp = TempDir::new().unwrap();
581 std::fs::create_dir_all(tmp.path().join("target/release")).unwrap();
582 let binary = tmp.path().join("target/release/pawan");
583 let counter_file = tmp.path().join("counter");
584 std::fs::write(&counter_file, "0").unwrap();
585 let script = format!(
586 "#!/bin/sh\ncount=$(cat {})\necho $((count + 1)) > {}\nif [ $count -eq 0 ]; then\n exit 1\nelse\n exit 0\nfi",
587 counter_file.display(), counter_file.display()
588 );
589 std::fs::write(&binary, script).unwrap();
590 #[cfg(unix)]
591 std::fs::set_permissions(&binary, std::os::unix::fs::PermissionsExt::from_mode(0o755)).unwrap();
592
593 let tool = SpawnAgentTool::new(tmp.path().to_path_buf());
594 let result = tool.execute(json!({
595 "prompt": "test",
596 "retries": 1
597 })).await.unwrap();
598 assert_eq!(result["success"], true);
599 assert_eq!(result["attempt"], 2);
600 assert_eq!(result["total_attempts"], 2);
601 }
602
603
604
605 #[tokio::test]
606 async fn test_spawn_agent_stderr_captured() {
607 let tmp = TempDir::new().unwrap();
608 std::fs::create_dir_all(tmp.path().join("target/release")).unwrap();
609 let binary = tmp.path().join("target/release/pawan");
610 std::fs::write(&binary, r"#!/bin/sh
611echo 'error message' >&2
612exit 0").unwrap();
613 #[cfg(unix)]
614 std::fs::set_permissions(&binary, std::os::unix::fs::PermissionsExt::from_mode(0o755)).unwrap();
615
616 let tool = SpawnAgentTool::new(tmp.path().to_path_buf());
617 let result = tool.execute(json!({"prompt": "test"})).await.unwrap();
618 assert_eq!(result["success"], true);
619 assert_eq!(result["stderr"], "error message");
620 }
621
622 #[serial_test::serial(pawan_session_tests)]
623 #[tokio::test]
624 async fn test_spawn_agents_single_task() {
625 let tmp = TempDir::new().unwrap();
626 std::fs::create_dir_all(tmp.path().join("target/release")).unwrap();
627 let binary = tmp.path().join("target/release/pawan");
628 std::fs::write(&binary, "#!/bin/sh\necho '{\"result\":\"done\"}'").unwrap();
629 #[cfg(unix)]
630 std::fs::set_permissions(&binary, std::os::unix::fs::PermissionsExt::from_mode(0o755)).unwrap();
631
632 let tool = SpawnAgentsTool::new(tmp.path().to_path_buf());
633 let result = tool.execute(json!({
634 "tasks": [
635 {"prompt": "task1"}
636 ]
637 })).await.unwrap();
638 assert_eq!(result["success"], true);
639 assert_eq!(result["total_tasks"], 1);
640 assert_eq!(result["results"].as_array().unwrap().len(), 1);
641 assert_eq!(result["results"][0]["result"]["result"], "done");
642 }
643
644 #[serial_test::serial(pawan_session_tests)]
645 #[tokio::test]
646 async fn test_spawn_agents_multiple_tasks() {
647 let tmp = TempDir::new().unwrap();
648 std::fs::create_dir_all(tmp.path().join("target/release")).unwrap();
649 let binary = tmp.path().join("target/release/pawan");
650 std::fs::write(&binary, "#!/bin/sh\necho '{\"result\":\"done\"}'").unwrap();
651 #[cfg(unix)]
652 std::fs::set_permissions(&binary, std::os::unix::fs::PermissionsExt::from_mode(0o755)).unwrap();
653
654 let tool = SpawnAgentsTool::new(tmp.path().to_path_buf());
655 let result = tool.execute(json!({
656 "tasks": [
657 {"prompt": "task1"},
658 {"prompt": "task2"},
659 {"prompt": "task3"}
660 ]
661 })).await.unwrap();
662 assert_eq!(result["success"], true);
663 assert_eq!(result["total_tasks"], 3);
664 assert_eq!(result["results"].as_array().unwrap().len(), 3);
665 }
666
667 #[tokio::test]
668 async fn test_spawn_agents_task_missing_prompt() {
669 let tmp = TempDir::new().unwrap();
670 let tool = SpawnAgentsTool::new(tmp.path().to_path_buf());
671 let result = tool.execute(json!({
672 "tasks": [{"model": "gpt-4"}]
673 })).await.unwrap();
674 assert_eq!(result["success"], true);
675 assert_eq!(result["total_tasks"], 1);
676 assert_eq!(result["results"][0]["success"], false);
677 assert!(result["results"][0]["error"].as_str().unwrap().contains("prompt"));
678 }
679}