1use crate::agent::{AgentConfig, AgentEvent, AgentLoop};
18use crate::llm::LlmClient;
19use crate::subagent::AgentRegistry;
20use crate::tools::types::{Tool, ToolContext, ToolOutput};
21use anyhow::{Context, Result};
22use async_trait::async_trait;
23use serde::{Deserialize, Serialize};
24use std::path::PathBuf;
25use std::sync::Arc;
26use tokio::sync::broadcast;
27use tokio::task::JoinSet;
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct TaskParams {
32 pub agent: String,
34 pub description: String,
36 pub prompt: String,
38 #[serde(default)]
40 pub background: bool,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub max_steps: Option<usize>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct TaskResult {
49 pub output: String,
51 pub session_id: String,
53 pub agent: String,
55 pub success: bool,
57 pub task_id: String,
59}
60
61pub struct TaskExecutor {
63 registry: Arc<AgentRegistry>,
65 llm_client: Arc<dyn LlmClient>,
67 workspace: String,
69}
70
71impl TaskExecutor {
72 pub fn new(
74 registry: Arc<AgentRegistry>,
75 llm_client: Arc<dyn LlmClient>,
76 workspace: String,
77 ) -> Self {
78 Self {
79 registry,
80 llm_client,
81 workspace,
82 }
83 }
84
85 pub async fn execute(
87 &self,
88 params: TaskParams,
89 event_tx: Option<broadcast::Sender<AgentEvent>>,
90 ) -> Result<TaskResult> {
91 let task_id = format!("task-{}", uuid::Uuid::new_v4());
92 let session_id = format!("subagent-{}", task_id);
93
94 let agent = self
95 .registry
96 .get(¶ms.agent)
97 .context(format!("Unknown agent type: '{}'", params.agent))?;
98
99 if let Some(ref tx) = event_tx {
100 let _ = tx.send(AgentEvent::SubagentStart {
101 task_id: task_id.clone(),
102 session_id: session_id.clone(),
103 parent_session_id: String::new(),
104 agent: params.agent.clone(),
105 description: params.description.clone(),
106 });
107 }
108
109 let mut child_executor = crate::tools::ToolExecutor::new(self.workspace.clone());
112 if !agent.permissions.allow.is_empty() || !agent.permissions.deny.is_empty() {
113 child_executor.set_guard_policy(Arc::new(agent.permissions.clone())
114 as Arc<dyn crate::permissions::PermissionChecker>);
115 }
116 let child_executor = Arc::new(child_executor);
117
118 let mut prompt_slots = crate::prompts::SystemPromptSlots::default();
120 if let Some(ref p) = agent.prompt {
121 prompt_slots.extra = Some(p.clone());
122 }
123
124 let child_config = AgentConfig {
125 prompt_slots,
126 tools: child_executor.definitions(),
127 max_tool_rounds: params
128 .max_steps
129 .unwrap_or_else(|| agent.max_steps.unwrap_or(20)),
130 ..AgentConfig::default()
131 };
132
133 let tool_context =
134 ToolContext::new(PathBuf::from(&self.workspace)).with_session_id(session_id.clone());
135
136 let agent_loop = AgentLoop::new(
137 Arc::clone(&self.llm_client),
138 child_executor,
139 tool_context,
140 child_config,
141 );
142
143 let (output, success) = match agent_loop.execute(&[], ¶ms.prompt, None).await {
144 Ok(result) => (result.text, true),
145 Err(e) => (format!("Task failed: {}", e), false),
146 };
147
148 if let Some(ref tx) = event_tx {
149 let _ = tx.send(AgentEvent::SubagentEnd {
150 task_id: task_id.clone(),
151 session_id: session_id.clone(),
152 agent: params.agent.clone(),
153 output: output.clone(),
154 success,
155 });
156 }
157
158 Ok(TaskResult {
159 output,
160 session_id,
161 agent: params.agent,
162 success,
163 task_id,
164 })
165 }
166
167 pub fn execute_background(
171 self: Arc<Self>,
172 params: TaskParams,
173 event_tx: Option<broadcast::Sender<AgentEvent>>,
174 ) -> String {
175 let task_id = format!("task-{}", uuid::Uuid::new_v4());
176 let task_id_clone = task_id.clone();
177
178 tokio::spawn(async move {
179 if let Err(e) = self.execute(params, event_tx).await {
180 tracing::error!("Background task {} failed: {}", task_id_clone, e);
181 }
182 });
183
184 task_id
185 }
186
187 pub async fn execute_parallel(
192 self: &Arc<Self>,
193 tasks: Vec<TaskParams>,
194 event_tx: Option<broadcast::Sender<AgentEvent>>,
195 ) -> Vec<TaskResult> {
196 let mut join_set: JoinSet<(usize, TaskResult)> = JoinSet::new();
197
198 for (idx, params) in tasks.into_iter().enumerate() {
199 let executor = Arc::clone(self);
200 let tx = event_tx.clone();
201
202 join_set.spawn(async move {
203 let result = match executor.execute(params.clone(), tx).await {
204 Ok(result) => result,
205 Err(e) => TaskResult {
206 output: format!("Task failed: {}", e),
207 session_id: String::new(),
208 agent: params.agent,
209 success: false,
210 task_id: format!("task-{}", uuid::Uuid::new_v4()),
211 },
212 };
213 (idx, result)
214 });
215 }
216
217 let mut indexed_results = Vec::new();
218 while let Some(result) = join_set.join_next().await {
219 match result {
220 Ok((idx, task_result)) => indexed_results.push((idx, task_result)),
221 Err(e) => {
222 tracing::error!("Parallel task panicked: {}", e);
223 indexed_results.push((
224 usize::MAX,
225 TaskResult {
226 output: format!("Task panicked: {}", e),
227 session_id: String::new(),
228 agent: "unknown".to_string(),
229 success: false,
230 task_id: format!("task-{}", uuid::Uuid::new_v4()),
231 },
232 ));
233 }
234 }
235 }
236
237 indexed_results.sort_by_key(|(idx, _)| *idx);
238 indexed_results.into_iter().map(|(_, r)| r).collect()
239 }
240}
241
242pub fn task_params_schema() -> serde_json::Value {
244 serde_json::json!({
245 "type": "object",
246 "properties": {
247 "agent": {
248 "type": "string",
249 "description": "Agent type to use (explore, general, plan, etc.)"
250 },
251 "description": {
252 "type": "string",
253 "description": "Short description of the task (for display)"
254 },
255 "prompt": {
256 "type": "string",
257 "description": "Detailed prompt for the agent"
258 },
259 "background": {
260 "type": "boolean",
261 "description": "Run in background (default: false)",
262 "default": false
263 },
264 "max_steps": {
265 "type": "integer",
266 "description": "Maximum steps for this task"
267 }
268 },
269 "required": ["agent", "description", "prompt"]
270 })
271}
272
273pub struct TaskTool {
276 executor: Arc<TaskExecutor>,
277}
278
279impl TaskTool {
280 pub fn new(executor: Arc<TaskExecutor>) -> Self {
282 Self { executor }
283 }
284}
285
286#[async_trait]
287impl Tool for TaskTool {
288 fn name(&self) -> &str {
289 "task"
290 }
291
292 fn description(&self) -> &str {
293 "Delegate a task to a specialized subagent. Built-in agents: explore (read-only codebase search), general (full access multi-step), plan (read-only planning). Custom agents from agent_dirs are also available."
294 }
295
296 fn parameters(&self) -> serde_json::Value {
297 task_params_schema()
298 }
299
300 async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> {
301 let params: TaskParams =
302 serde_json::from_value(args.clone()).context("Invalid task parameters")?;
303
304 if params.background {
305 let task_id =
306 Arc::clone(&self.executor).execute_background(params, ctx.agent_event_tx.clone());
307 return Ok(ToolOutput::success(format!(
308 "Task started in background. Task ID: {}",
309 task_id
310 )));
311 }
312
313 let result = self
314 .executor
315 .execute(params, ctx.agent_event_tx.clone())
316 .await?;
317
318 if result.success {
319 Ok(ToolOutput::success(result.output))
320 } else {
321 Ok(ToolOutput::error(result.output))
322 }
323 }
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct ParallelTaskParams {
329 pub tasks: Vec<TaskParams>,
331}
332
333pub fn parallel_task_params_schema() -> serde_json::Value {
335 serde_json::json!({
336 "type": "object",
337 "properties": {
338 "tasks": {
339 "type": "array",
340 "description": "List of tasks to execute in parallel. Each task runs as an independent subagent concurrently.",
341 "items": {
342 "type": "object",
343 "properties": {
344 "agent": {
345 "type": "string",
346 "description": "Agent type to use (explore, general, plan, etc.)"
347 },
348 "description": {
349 "type": "string",
350 "description": "Short description of the task (for display)"
351 },
352 "prompt": {
353 "type": "string",
354 "description": "Detailed prompt for the agent"
355 }
356 },
357 "required": ["agent", "description", "prompt"]
358 },
359 "minItems": 1
360 }
361 },
362 "required": ["tasks"]
363 })
364}
365
366pub struct ParallelTaskTool {
370 executor: Arc<TaskExecutor>,
371}
372
373impl ParallelTaskTool {
374 pub fn new(executor: Arc<TaskExecutor>) -> Self {
376 Self { executor }
377 }
378}
379
380#[async_trait]
381impl Tool for ParallelTaskTool {
382 fn name(&self) -> &str {
383 "parallel_task"
384 }
385
386 fn description(&self) -> &str {
387 "Execute multiple subagent tasks in parallel. All tasks run concurrently and results are returned when all complete. Built-in agents: explore (read-only codebase search), general (full access multi-step), plan (read-only planning). Custom agents from agent_dirs are also available."
388 }
389
390 fn parameters(&self) -> serde_json::Value {
391 parallel_task_params_schema()
392 }
393
394 async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> {
395 let params: ParallelTaskParams =
396 serde_json::from_value(args.clone()).context("Invalid parallel task parameters")?;
397
398 if params.tasks.is_empty() {
399 return Ok(ToolOutput::error("No tasks provided".to_string()));
400 }
401
402 let task_count = params.tasks.len();
403
404 let results = self
405 .executor
406 .execute_parallel(params.tasks, ctx.agent_event_tx.clone())
407 .await;
408
409 let mut output = format!("Executed {} tasks in parallel:\n\n", task_count);
411 for (i, result) in results.iter().enumerate() {
412 let status = if result.success { "[OK]" } else { "[ERR]" };
413 output.push_str(&format!(
414 "--- Task {} ({}) {} ---\n{}\n\n",
415 i + 1,
416 result.agent,
417 status,
418 result.output
419 ));
420 }
421
422 Ok(ToolOutput::success(output))
423 }
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429
430 #[test]
431 fn test_task_params_deserialize() {
432 let json = r#"{
433 "agent": "explore",
434 "description": "Find auth code",
435 "prompt": "Search for authentication files"
436 }"#;
437
438 let params: TaskParams = serde_json::from_str(json).unwrap();
439 assert_eq!(params.agent, "explore");
440 assert_eq!(params.description, "Find auth code");
441 assert!(!params.background);
442 }
443
444 #[test]
445 fn test_task_params_with_background() {
446 let json = r#"{
447 "agent": "general",
448 "description": "Long task",
449 "prompt": "Do something complex",
450 "background": true
451 }"#;
452
453 let params: TaskParams = serde_json::from_str(json).unwrap();
454 assert!(params.background);
455 }
456
457 #[test]
458 fn test_task_params_with_max_steps() {
459 let json = r#"{
460 "agent": "plan",
461 "description": "Planning task",
462 "prompt": "Create a plan",
463 "max_steps": 10
464 }"#;
465
466 let params: TaskParams = serde_json::from_str(json).unwrap();
467 assert_eq!(params.agent, "plan");
468 assert_eq!(params.max_steps, Some(10));
469 assert!(!params.background);
470 }
471
472 #[test]
473 fn test_task_params_all_fields() {
474 let json = r#"{
475 "agent": "general",
476 "description": "Complex task",
477 "prompt": "Do everything",
478 "background": true,
479 "max_steps": 20
480 }"#;
481
482 let params: TaskParams = serde_json::from_str(json).unwrap();
483 assert_eq!(params.agent, "general");
484 assert_eq!(params.description, "Complex task");
485 assert_eq!(params.prompt, "Do everything");
486 assert!(params.background);
487 assert_eq!(params.max_steps, Some(20));
488 }
489
490 #[test]
491 fn test_task_params_missing_required_field() {
492 let json = r#"{
493 "agent": "explore",
494 "description": "Missing prompt"
495 }"#;
496
497 let result: Result<TaskParams, _> = serde_json::from_str(json);
498 assert!(result.is_err());
499 }
500
501 #[test]
502 fn test_task_params_serialize() {
503 let params = TaskParams {
504 agent: "explore".to_string(),
505 description: "Test task".to_string(),
506 prompt: "Test prompt".to_string(),
507 background: false,
508 max_steps: Some(5),
509 };
510
511 let json = serde_json::to_string(¶ms).unwrap();
512 assert!(json.contains("explore"));
513 assert!(json.contains("Test task"));
514 assert!(json.contains("Test prompt"));
515 }
516
517 #[test]
518 fn test_task_params_clone() {
519 let params = TaskParams {
520 agent: "explore".to_string(),
521 description: "Test".to_string(),
522 prompt: "Prompt".to_string(),
523 background: true,
524 max_steps: None,
525 };
526
527 let cloned = params.clone();
528 assert_eq!(params.agent, cloned.agent);
529 assert_eq!(params.description, cloned.description);
530 assert_eq!(params.background, cloned.background);
531 }
532
533 #[test]
534 fn test_task_result_serialize() {
535 let result = TaskResult {
536 output: "Found 5 files".to_string(),
537 session_id: "session-123".to_string(),
538 agent: "explore".to_string(),
539 success: true,
540 task_id: "task-456".to_string(),
541 };
542
543 let json = serde_json::to_string(&result).unwrap();
544 assert!(json.contains("Found 5 files"));
545 assert!(json.contains("explore"));
546 }
547
548 #[test]
549 fn test_task_result_deserialize() {
550 let json = r#"{
551 "output": "Task completed",
552 "session_id": "sess-789",
553 "agent": "general",
554 "success": false,
555 "task_id": "task-123"
556 }"#;
557
558 let result: TaskResult = serde_json::from_str(json).unwrap();
559 assert_eq!(result.output, "Task completed");
560 assert_eq!(result.session_id, "sess-789");
561 assert_eq!(result.agent, "general");
562 assert!(!result.success);
563 assert_eq!(result.task_id, "task-123");
564 }
565
566 #[test]
567 fn test_task_result_clone() {
568 let result = TaskResult {
569 output: "Output".to_string(),
570 session_id: "session-1".to_string(),
571 agent: "explore".to_string(),
572 success: true,
573 task_id: "task-1".to_string(),
574 };
575
576 let cloned = result.clone();
577 assert_eq!(result.output, cloned.output);
578 assert_eq!(result.success, cloned.success);
579 }
580
581 #[test]
582 fn test_task_params_schema() {
583 let schema = task_params_schema();
584 assert_eq!(schema["type"], "object");
585 assert!(schema["properties"]["agent"].is_object());
586 assert!(schema["properties"]["prompt"].is_object());
587 }
588
589 #[test]
590 fn test_task_params_schema_required_fields() {
591 let schema = task_params_schema();
592 let required = schema["required"].as_array().unwrap();
593 assert!(required.contains(&serde_json::json!("agent")));
594 assert!(required.contains(&serde_json::json!("description")));
595 assert!(required.contains(&serde_json::json!("prompt")));
596 }
597
598 #[test]
599 fn test_task_params_schema_properties() {
600 let schema = task_params_schema();
601 let props = &schema["properties"];
602
603 assert_eq!(props["agent"]["type"], "string");
604 assert_eq!(props["description"]["type"], "string");
605 assert_eq!(props["prompt"]["type"], "string");
606 assert_eq!(props["background"]["type"], "boolean");
607 assert_eq!(props["background"]["default"], false);
608 assert_eq!(props["max_steps"]["type"], "integer");
609 }
610
611 #[test]
612 fn test_task_params_schema_descriptions() {
613 let schema = task_params_schema();
614 let props = &schema["properties"];
615
616 assert!(props["agent"]["description"].is_string());
617 assert!(props["description"]["description"].is_string());
618 assert!(props["prompt"]["description"].is_string());
619 assert!(props["background"]["description"].is_string());
620 assert!(props["max_steps"]["description"].is_string());
621 }
622
623 #[test]
624 fn test_task_params_default_background() {
625 let params = TaskParams {
626 agent: "explore".to_string(),
627 description: "Test".to_string(),
628 prompt: "Test prompt".to_string(),
629 background: false,
630 max_steps: None,
631 };
632 assert!(!params.background);
633 }
634
635 #[test]
636 fn test_task_params_serialize_skip_none() {
637 let params = TaskParams {
638 agent: "explore".to_string(),
639 description: "Test".to_string(),
640 prompt: "Test prompt".to_string(),
641 background: false,
642 max_steps: None,
643 };
644 let json = serde_json::to_string(¶ms).unwrap();
645 assert!(!json.contains("max_steps"));
647 }
648
649 #[test]
650 fn test_task_params_serialize_with_max_steps() {
651 let params = TaskParams {
652 agent: "explore".to_string(),
653 description: "Test".to_string(),
654 prompt: "Test prompt".to_string(),
655 background: false,
656 max_steps: Some(15),
657 };
658 let json = serde_json::to_string(¶ms).unwrap();
659 assert!(json.contains("max_steps"));
660 assert!(json.contains("15"));
661 }
662
663 #[test]
664 fn test_task_result_success_true() {
665 let result = TaskResult {
666 output: "Success".to_string(),
667 session_id: "sess-1".to_string(),
668 agent: "explore".to_string(),
669 success: true,
670 task_id: "task-1".to_string(),
671 };
672 assert!(result.success);
673 }
674
675 #[test]
676 fn test_task_result_success_false() {
677 let result = TaskResult {
678 output: "Failed".to_string(),
679 session_id: "sess-1".to_string(),
680 agent: "explore".to_string(),
681 success: false,
682 task_id: "task-1".to_string(),
683 };
684 assert!(!result.success);
685 }
686
687 #[test]
688 fn test_task_params_empty_strings() {
689 let params = TaskParams {
690 agent: "".to_string(),
691 description: "".to_string(),
692 prompt: "".to_string(),
693 background: false,
694 max_steps: None,
695 };
696 let json = serde_json::to_string(¶ms).unwrap();
697 let deserialized: TaskParams = serde_json::from_str(&json).unwrap();
698 assert_eq!(deserialized.agent, "");
699 assert_eq!(deserialized.description, "");
700 assert_eq!(deserialized.prompt, "");
701 }
702
703 #[test]
704 fn test_task_result_empty_output() {
705 let result = TaskResult {
706 output: "".to_string(),
707 session_id: "sess-1".to_string(),
708 agent: "explore".to_string(),
709 success: true,
710 task_id: "task-1".to_string(),
711 };
712 assert_eq!(result.output, "");
713 }
714
715 #[test]
716 fn test_task_params_debug_format() {
717 let params = TaskParams {
718 agent: "explore".to_string(),
719 description: "Test".to_string(),
720 prompt: "Test prompt".to_string(),
721 background: false,
722 max_steps: None,
723 };
724 let debug_str = format!("{:?}", params);
725 assert!(debug_str.contains("explore"));
726 assert!(debug_str.contains("Test"));
727 }
728
729 #[test]
730 fn test_task_result_debug_format() {
731 let result = TaskResult {
732 output: "Output".to_string(),
733 session_id: "sess-1".to_string(),
734 agent: "explore".to_string(),
735 success: true,
736 task_id: "task-1".to_string(),
737 };
738 let debug_str = format!("{:?}", result);
739 assert!(debug_str.contains("Output"));
740 assert!(debug_str.contains("explore"));
741 }
742
743 #[test]
744 fn test_task_params_roundtrip() {
745 let original = TaskParams {
746 agent: "general".to_string(),
747 description: "Roundtrip test".to_string(),
748 prompt: "Test roundtrip serialization".to_string(),
749 background: true,
750 max_steps: Some(42),
751 };
752 let json = serde_json::to_string(&original).unwrap();
753 let deserialized: TaskParams = serde_json::from_str(&json).unwrap();
754 assert_eq!(original.agent, deserialized.agent);
755 assert_eq!(original.description, deserialized.description);
756 assert_eq!(original.prompt, deserialized.prompt);
757 assert_eq!(original.background, deserialized.background);
758 assert_eq!(original.max_steps, deserialized.max_steps);
759 }
760
761 #[test]
762 fn test_task_result_roundtrip() {
763 let original = TaskResult {
764 output: "Roundtrip output".to_string(),
765 session_id: "sess-roundtrip".to_string(),
766 agent: "plan".to_string(),
767 success: false,
768 task_id: "task-roundtrip".to_string(),
769 };
770 let json = serde_json::to_string(&original).unwrap();
771 let deserialized: TaskResult = serde_json::from_str(&json).unwrap();
772 assert_eq!(original.output, deserialized.output);
773 assert_eq!(original.session_id, deserialized.session_id);
774 assert_eq!(original.agent, deserialized.agent);
775 assert_eq!(original.success, deserialized.success);
776 assert_eq!(original.task_id, deserialized.task_id);
777 }
778
779 #[test]
780 fn test_parallel_task_params_deserialize() {
781 let json = r#"{
782 "tasks": [
783 { "agent": "explore", "description": "Find auth", "prompt": "Search auth files" },
784 { "agent": "general", "description": "Fix bug", "prompt": "Fix the login bug" }
785 ]
786 }"#;
787
788 let params: ParallelTaskParams = serde_json::from_str(json).unwrap();
789 assert_eq!(params.tasks.len(), 2);
790 assert_eq!(params.tasks[0].agent, "explore");
791 assert_eq!(params.tasks[1].agent, "general");
792 }
793
794 #[test]
795 fn test_parallel_task_params_single_task() {
796 let json = r#"{
797 "tasks": [
798 { "agent": "plan", "description": "Plan work", "prompt": "Create a plan" }
799 ]
800 }"#;
801
802 let params: ParallelTaskParams = serde_json::from_str(json).unwrap();
803 assert_eq!(params.tasks.len(), 1);
804 }
805
806 #[test]
807 fn test_parallel_task_params_empty_tasks() {
808 let json = r#"{ "tasks": [] }"#;
809 let params: ParallelTaskParams = serde_json::from_str(json).unwrap();
810 assert!(params.tasks.is_empty());
811 }
812
813 #[test]
814 fn test_parallel_task_params_missing_tasks() {
815 let json = r#"{}"#;
816 let result: Result<ParallelTaskParams, _> = serde_json::from_str(json);
817 assert!(result.is_err());
818 }
819
820 #[test]
821 fn test_parallel_task_params_serialize() {
822 let params = ParallelTaskParams {
823 tasks: vec![
824 TaskParams {
825 agent: "explore".to_string(),
826 description: "Task 1".to_string(),
827 prompt: "Prompt 1".to_string(),
828 background: false,
829 max_steps: None,
830 },
831 TaskParams {
832 agent: "general".to_string(),
833 description: "Task 2".to_string(),
834 prompt: "Prompt 2".to_string(),
835 background: false,
836 max_steps: Some(10),
837 },
838 ],
839 };
840 let json = serde_json::to_string(¶ms).unwrap();
841 assert!(json.contains("explore"));
842 assert!(json.contains("general"));
843 assert!(json.contains("Prompt 1"));
844 assert!(json.contains("Prompt 2"));
845 }
846
847 #[test]
848 fn test_parallel_task_params_roundtrip() {
849 let original = ParallelTaskParams {
850 tasks: vec![
851 TaskParams {
852 agent: "explore".to_string(),
853 description: "Explore".to_string(),
854 prompt: "Find files".to_string(),
855 background: false,
856 max_steps: None,
857 },
858 TaskParams {
859 agent: "plan".to_string(),
860 description: "Plan".to_string(),
861 prompt: "Make plan".to_string(),
862 background: false,
863 max_steps: Some(5),
864 },
865 ],
866 };
867 let json = serde_json::to_string(&original).unwrap();
868 let deserialized: ParallelTaskParams = serde_json::from_str(&json).unwrap();
869 assert_eq!(original.tasks.len(), deserialized.tasks.len());
870 assert_eq!(original.tasks[0].agent, deserialized.tasks[0].agent);
871 assert_eq!(original.tasks[1].agent, deserialized.tasks[1].agent);
872 assert_eq!(original.tasks[1].max_steps, deserialized.tasks[1].max_steps);
873 }
874
875 #[test]
876 fn test_parallel_task_params_clone() {
877 let params = ParallelTaskParams {
878 tasks: vec![TaskParams {
879 agent: "explore".to_string(),
880 description: "Test".to_string(),
881 prompt: "Prompt".to_string(),
882 background: false,
883 max_steps: None,
884 }],
885 };
886 let cloned = params.clone();
887 assert_eq!(params.tasks.len(), cloned.tasks.len());
888 assert_eq!(params.tasks[0].agent, cloned.tasks[0].agent);
889 }
890
891 #[test]
892 fn test_parallel_task_params_schema() {
893 let schema = parallel_task_params_schema();
894 assert_eq!(schema["type"], "object");
895 assert!(schema["properties"]["tasks"].is_object());
896 assert_eq!(schema["properties"]["tasks"]["type"], "array");
897 assert_eq!(schema["properties"]["tasks"]["minItems"], 1);
898 }
899
900 #[test]
901 fn test_parallel_task_params_schema_required() {
902 let schema = parallel_task_params_schema();
903 let required = schema["required"].as_array().unwrap();
904 assert!(required.contains(&serde_json::json!("tasks")));
905 }
906
907 #[test]
908 fn test_parallel_task_params_schema_items() {
909 let schema = parallel_task_params_schema();
910 let items = &schema["properties"]["tasks"]["items"];
911 assert_eq!(items["type"], "object");
912 let item_required = items["required"].as_array().unwrap();
913 assert!(item_required.contains(&serde_json::json!("agent")));
914 assert!(item_required.contains(&serde_json::json!("description")));
915 assert!(item_required.contains(&serde_json::json!("prompt")));
916 }
917
918 #[test]
919 fn test_parallel_task_params_debug() {
920 let params = ParallelTaskParams {
921 tasks: vec![TaskParams {
922 agent: "explore".to_string(),
923 description: "Debug test".to_string(),
924 prompt: "Test".to_string(),
925 background: false,
926 max_steps: None,
927 }],
928 };
929 let debug_str = format!("{:?}", params);
930 assert!(debug_str.contains("explore"));
931 assert!(debug_str.contains("Debug test"));
932 }
933
934 #[test]
935 fn test_parallel_task_params_large_count() {
936 let tasks: Vec<TaskParams> = (0..150)
938 .map(|i| TaskParams {
939 agent: "explore".to_string(),
940 description: format!("Task {}", i),
941 prompt: format!("Prompt for task {}", i),
942 background: false,
943 max_steps: Some(10),
944 })
945 .collect();
946
947 let params = ParallelTaskParams { tasks };
948 let json = serde_json::to_string(¶ms).unwrap();
949 let deserialized: ParallelTaskParams = serde_json::from_str(&json).unwrap();
950 assert_eq!(deserialized.tasks.len(), 150);
951 assert_eq!(deserialized.tasks[0].description, "Task 0");
952 assert_eq!(deserialized.tasks[149].description, "Task 149");
953 }
954
955 #[test]
956 fn test_task_params_max_steps_zero() {
957 let params = TaskParams {
959 agent: "explore".to_string(),
960 description: "Edge case".to_string(),
961 prompt: "Zero steps".to_string(),
962 background: false,
963 max_steps: Some(0),
964 };
965 let json = serde_json::to_string(¶ms).unwrap();
966 let deserialized: TaskParams = serde_json::from_str(&json).unwrap();
967 assert_eq!(deserialized.max_steps, Some(0));
968 }
969
970 #[test]
971 fn test_parallel_task_params_all_background() {
972 let tasks: Vec<TaskParams> = (0..5)
973 .map(|i| TaskParams {
974 agent: "general".to_string(),
975 description: format!("BG task {}", i),
976 prompt: "Run in background".to_string(),
977 background: true,
978 max_steps: None,
979 })
980 .collect();
981 let params = ParallelTaskParams { tasks };
982 for task in ¶ms.tasks {
983 assert!(task.background);
984 }
985 }
986}