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