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::new();
204
205 for params in tasks {
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 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 });
223 }
224
225 let mut results = Vec::new();
227 while let Some(result) = join_set.join_next().await {
228 match result {
229 Ok(task_result) => results.push(task_result),
230 Err(e) => {
231 tracing::error!("Parallel task panicked: {}", e);
232 results.push(TaskResult {
233 output: format!("Task panicked: {}", e),
234 session_id: String::new(),
235 agent: "unknown".to_string(),
236 success: false,
237 task_id: format!("task-{}", uuid::Uuid::new_v4()),
238 });
239 }
240 }
241 }
242
243 results
244 }
245}
246
247pub fn task_params_schema() -> serde_json::Value {
249 serde_json::json!({
250 "type": "object",
251 "properties": {
252 "agent": {
253 "type": "string",
254 "description": "Agent type to use (explore, general, plan, etc.)"
255 },
256 "description": {
257 "type": "string",
258 "description": "Short description of the task (for display)"
259 },
260 "prompt": {
261 "type": "string",
262 "description": "Detailed prompt for the agent"
263 },
264 "background": {
265 "type": "boolean",
266 "description": "Run in background (default: false)",
267 "default": false
268 },
269 "max_steps": {
270 "type": "integer",
271 "description": "Maximum steps for this task"
272 }
273 },
274 "required": ["agent", "description", "prompt"]
275 })
276}
277
278pub struct TaskTool {
281 executor: Arc<TaskExecutor>,
282}
283
284impl TaskTool {
285 pub fn new(executor: Arc<TaskExecutor>) -> Self {
287 Self { executor }
288 }
289}
290
291#[async_trait]
292impl Tool for TaskTool {
293 fn name(&self) -> &str {
294 "task"
295 }
296
297 fn description(&self) -> &str {
298 "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."
299 }
300
301 fn parameters(&self) -> serde_json::Value {
302 task_params_schema()
303 }
304
305 async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> {
306 let params: TaskParams =
307 serde_json::from_value(args.clone()).context("Invalid task parameters")?;
308
309 let session_id = ctx.session_id.as_deref().unwrap_or("unknown");
310
311 if params.background {
312 let task_id = Arc::clone(&self.executor).execute_background(
313 session_id.to_string(),
314 params,
315 ctx.agent_event_tx.clone(),
316 );
317 return Ok(ToolOutput::success(format!(
318 "Task started in background. Task ID: {}",
319 task_id
320 )));
321 }
322
323 let result = self
324 .executor
325 .execute(session_id, params, ctx.agent_event_tx.clone())
326 .await?;
327
328 if result.success {
329 Ok(ToolOutput::success(result.output))
330 } else {
331 Ok(ToolOutput::error(result.output))
332 }
333 }
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize)]
338pub struct ParallelTaskParams {
339 pub tasks: Vec<TaskParams>,
341}
342
343pub fn parallel_task_params_schema() -> serde_json::Value {
345 serde_json::json!({
346 "type": "object",
347 "properties": {
348 "tasks": {
349 "type": "array",
350 "description": "List of tasks to execute in parallel. Each task runs as an independent subagent concurrently.",
351 "items": {
352 "type": "object",
353 "properties": {
354 "agent": {
355 "type": "string",
356 "description": "Agent type to use (explore, general, plan, etc.)"
357 },
358 "description": {
359 "type": "string",
360 "description": "Short description of the task (for display)"
361 },
362 "prompt": {
363 "type": "string",
364 "description": "Detailed prompt for the agent"
365 }
366 },
367 "required": ["agent", "description", "prompt"]
368 },
369 "minItems": 1
370 }
371 },
372 "required": ["tasks"]
373 })
374}
375
376pub struct ParallelTaskTool {
380 executor: Arc<TaskExecutor>,
381}
382
383impl ParallelTaskTool {
384 pub fn new(executor: Arc<TaskExecutor>) -> Self {
386 Self { executor }
387 }
388}
389
390#[async_trait]
391impl Tool for ParallelTaskTool {
392 fn name(&self) -> &str {
393 "parallel_task"
394 }
395
396 fn description(&self) -> &str {
397 "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."
398 }
399
400 fn parameters(&self) -> serde_json::Value {
401 parallel_task_params_schema()
402 }
403
404 async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> {
405 let params: ParallelTaskParams =
406 serde_json::from_value(args.clone()).context("Invalid parallel task parameters")?;
407
408 if params.tasks.is_empty() {
409 return Ok(ToolOutput::error("No tasks provided".to_string()));
410 }
411
412 let session_id = ctx.session_id.as_deref().unwrap_or("unknown");
413 let task_count = params.tasks.len();
414
415 let results = self
416 .executor
417 .execute_parallel(session_id, params.tasks, ctx.agent_event_tx.clone())
418 .await;
419
420 let mut output = format!("Executed {} tasks in parallel:\n\n", task_count);
422 for (i, result) in results.iter().enumerate() {
423 let status = if result.success { "[OK]" } else { "[ERR]" };
424 output.push_str(&format!(
425 "--- Task {} ({}) {} ---\n{}\n\n",
426 i + 1,
427 result.agent,
428 status,
429 result.output
430 ));
431 }
432
433 Ok(ToolOutput::success(output))
434 }
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440
441 #[test]
442 fn test_task_params_deserialize() {
443 let json = r#"{
444 "agent": "explore",
445 "description": "Find auth code",
446 "prompt": "Search for authentication files"
447 }"#;
448
449 let params: TaskParams = serde_json::from_str(json).unwrap();
450 assert_eq!(params.agent, "explore");
451 assert_eq!(params.description, "Find auth code");
452 assert!(!params.background);
453 }
454
455 #[test]
456 fn test_task_params_with_background() {
457 let json = r#"{
458 "agent": "general",
459 "description": "Long task",
460 "prompt": "Do something complex",
461 "background": true
462 }"#;
463
464 let params: TaskParams = serde_json::from_str(json).unwrap();
465 assert!(params.background);
466 }
467
468 #[test]
469 fn test_task_params_with_max_steps() {
470 let json = r#"{
471 "agent": "plan",
472 "description": "Planning task",
473 "prompt": "Create a plan",
474 "max_steps": 10
475 }"#;
476
477 let params: TaskParams = serde_json::from_str(json).unwrap();
478 assert_eq!(params.agent, "plan");
479 assert_eq!(params.max_steps, Some(10));
480 assert!(!params.background);
481 }
482
483 #[test]
484 fn test_task_params_all_fields() {
485 let json = r#"{
486 "agent": "general",
487 "description": "Complex task",
488 "prompt": "Do everything",
489 "background": true,
490 "max_steps": 20
491 }"#;
492
493 let params: TaskParams = serde_json::from_str(json).unwrap();
494 assert_eq!(params.agent, "general");
495 assert_eq!(params.description, "Complex task");
496 assert_eq!(params.prompt, "Do everything");
497 assert!(params.background);
498 assert_eq!(params.max_steps, Some(20));
499 }
500
501 #[test]
502 fn test_task_params_missing_required_field() {
503 let json = r#"{
504 "agent": "explore",
505 "description": "Missing prompt"
506 }"#;
507
508 let result: Result<TaskParams, _> = serde_json::from_str(json);
509 assert!(result.is_err());
510 }
511
512 #[test]
513 fn test_task_params_serialize() {
514 let params = TaskParams {
515 agent: "explore".to_string(),
516 description: "Test task".to_string(),
517 prompt: "Test prompt".to_string(),
518 background: false,
519 max_steps: Some(5),
520 };
521
522 let json = serde_json::to_string(¶ms).unwrap();
523 assert!(json.contains("explore"));
524 assert!(json.contains("Test task"));
525 assert!(json.contains("Test prompt"));
526 }
527
528 #[test]
529 fn test_task_params_clone() {
530 let params = TaskParams {
531 agent: "explore".to_string(),
532 description: "Test".to_string(),
533 prompt: "Prompt".to_string(),
534 background: true,
535 max_steps: None,
536 };
537
538 let cloned = params.clone();
539 assert_eq!(params.agent, cloned.agent);
540 assert_eq!(params.description, cloned.description);
541 assert_eq!(params.background, cloned.background);
542 }
543
544 #[test]
545 fn test_task_result_serialize() {
546 let result = TaskResult {
547 output: "Found 5 files".to_string(),
548 session_id: "session-123".to_string(),
549 agent: "explore".to_string(),
550 success: true,
551 task_id: "task-456".to_string(),
552 };
553
554 let json = serde_json::to_string(&result).unwrap();
555 assert!(json.contains("Found 5 files"));
556 assert!(json.contains("explore"));
557 }
558
559 #[test]
560 fn test_task_result_deserialize() {
561 let json = r#"{
562 "output": "Task completed",
563 "session_id": "sess-789",
564 "agent": "general",
565 "success": false,
566 "task_id": "task-123"
567 }"#;
568
569 let result: TaskResult = serde_json::from_str(json).unwrap();
570 assert_eq!(result.output, "Task completed");
571 assert_eq!(result.session_id, "sess-789");
572 assert_eq!(result.agent, "general");
573 assert!(!result.success);
574 assert_eq!(result.task_id, "task-123");
575 }
576
577 #[test]
578 fn test_task_result_clone() {
579 let result = TaskResult {
580 output: "Output".to_string(),
581 session_id: "session-1".to_string(),
582 agent: "explore".to_string(),
583 success: true,
584 task_id: "task-1".to_string(),
585 };
586
587 let cloned = result.clone();
588 assert_eq!(result.output, cloned.output);
589 assert_eq!(result.success, cloned.success);
590 }
591
592 #[test]
593 fn test_task_params_schema() {
594 let schema = task_params_schema();
595 assert_eq!(schema["type"], "object");
596 assert!(schema["properties"]["agent"].is_object());
597 assert!(schema["properties"]["prompt"].is_object());
598 }
599
600 #[test]
601 fn test_task_params_schema_required_fields() {
602 let schema = task_params_schema();
603 let required = schema["required"].as_array().unwrap();
604 assert!(required.contains(&serde_json::json!("agent")));
605 assert!(required.contains(&serde_json::json!("description")));
606 assert!(required.contains(&serde_json::json!("prompt")));
607 }
608
609 #[test]
610 fn test_task_params_schema_properties() {
611 let schema = task_params_schema();
612 let props = &schema["properties"];
613
614 assert_eq!(props["agent"]["type"], "string");
615 assert_eq!(props["description"]["type"], "string");
616 assert_eq!(props["prompt"]["type"], "string");
617 assert_eq!(props["background"]["type"], "boolean");
618 assert_eq!(props["background"]["default"], false);
619 assert_eq!(props["max_steps"]["type"], "integer");
620 }
621
622 #[test]
623 fn test_task_params_schema_descriptions() {
624 let schema = task_params_schema();
625 let props = &schema["properties"];
626
627 assert!(props["agent"]["description"].is_string());
628 assert!(props["description"]["description"].is_string());
629 assert!(props["prompt"]["description"].is_string());
630 assert!(props["background"]["description"].is_string());
631 assert!(props["max_steps"]["description"].is_string());
632 }
633
634 #[test]
635 fn test_task_params_default_background() {
636 let params = TaskParams {
637 agent: "explore".to_string(),
638 description: "Test".to_string(),
639 prompt: "Test prompt".to_string(),
640 background: false,
641 max_steps: None,
642 };
643 assert!(!params.background);
644 }
645
646 #[test]
647 fn test_task_params_serialize_skip_none() {
648 let params = TaskParams {
649 agent: "explore".to_string(),
650 description: "Test".to_string(),
651 prompt: "Test prompt".to_string(),
652 background: false,
653 max_steps: None,
654 };
655 let json = serde_json::to_string(¶ms).unwrap();
656 assert!(!json.contains("max_steps"));
658 }
659
660 #[test]
661 fn test_task_params_serialize_with_max_steps() {
662 let params = TaskParams {
663 agent: "explore".to_string(),
664 description: "Test".to_string(),
665 prompt: "Test prompt".to_string(),
666 background: false,
667 max_steps: Some(15),
668 };
669 let json = serde_json::to_string(¶ms).unwrap();
670 assert!(json.contains("max_steps"));
671 assert!(json.contains("15"));
672 }
673
674 #[test]
675 fn test_task_result_success_true() {
676 let result = TaskResult {
677 output: "Success".to_string(),
678 session_id: "sess-1".to_string(),
679 agent: "explore".to_string(),
680 success: true,
681 task_id: "task-1".to_string(),
682 };
683 assert!(result.success);
684 }
685
686 #[test]
687 fn test_task_result_success_false() {
688 let result = TaskResult {
689 output: "Failed".to_string(),
690 session_id: "sess-1".to_string(),
691 agent: "explore".to_string(),
692 success: false,
693 task_id: "task-1".to_string(),
694 };
695 assert!(!result.success);
696 }
697
698 #[test]
699 fn test_task_params_empty_strings() {
700 let params = TaskParams {
701 agent: "".to_string(),
702 description: "".to_string(),
703 prompt: "".to_string(),
704 background: false,
705 max_steps: None,
706 };
707 let json = serde_json::to_string(¶ms).unwrap();
708 let deserialized: TaskParams = serde_json::from_str(&json).unwrap();
709 assert_eq!(deserialized.agent, "");
710 assert_eq!(deserialized.description, "");
711 assert_eq!(deserialized.prompt, "");
712 }
713
714 #[test]
715 fn test_task_result_empty_output() {
716 let result = TaskResult {
717 output: "".to_string(),
718 session_id: "sess-1".to_string(),
719 agent: "explore".to_string(),
720 success: true,
721 task_id: "task-1".to_string(),
722 };
723 assert_eq!(result.output, "");
724 }
725
726 #[test]
727 fn test_task_params_debug_format() {
728 let params = TaskParams {
729 agent: "explore".to_string(),
730 description: "Test".to_string(),
731 prompt: "Test prompt".to_string(),
732 background: false,
733 max_steps: None,
734 };
735 let debug_str = format!("{:?}", params);
736 assert!(debug_str.contains("explore"));
737 assert!(debug_str.contains("Test"));
738 }
739
740 #[test]
741 fn test_task_result_debug_format() {
742 let result = TaskResult {
743 output: "Output".to_string(),
744 session_id: "sess-1".to_string(),
745 agent: "explore".to_string(),
746 success: true,
747 task_id: "task-1".to_string(),
748 };
749 let debug_str = format!("{:?}", result);
750 assert!(debug_str.contains("Output"));
751 assert!(debug_str.contains("explore"));
752 }
753
754 #[test]
755 fn test_task_params_roundtrip() {
756 let original = TaskParams {
757 agent: "general".to_string(),
758 description: "Roundtrip test".to_string(),
759 prompt: "Test roundtrip serialization".to_string(),
760 background: true,
761 max_steps: Some(42),
762 };
763 let json = serde_json::to_string(&original).unwrap();
764 let deserialized: TaskParams = serde_json::from_str(&json).unwrap();
765 assert_eq!(original.agent, deserialized.agent);
766 assert_eq!(original.description, deserialized.description);
767 assert_eq!(original.prompt, deserialized.prompt);
768 assert_eq!(original.background, deserialized.background);
769 assert_eq!(original.max_steps, deserialized.max_steps);
770 }
771
772 #[test]
773 fn test_task_result_roundtrip() {
774 let original = TaskResult {
775 output: "Roundtrip output".to_string(),
776 session_id: "sess-roundtrip".to_string(),
777 agent: "plan".to_string(),
778 success: false,
779 task_id: "task-roundtrip".to_string(),
780 };
781 let json = serde_json::to_string(&original).unwrap();
782 let deserialized: TaskResult = serde_json::from_str(&json).unwrap();
783 assert_eq!(original.output, deserialized.output);
784 assert_eq!(original.session_id, deserialized.session_id);
785 assert_eq!(original.agent, deserialized.agent);
786 assert_eq!(original.success, deserialized.success);
787 assert_eq!(original.task_id, deserialized.task_id);
788 }
789
790 #[test]
791 fn test_parallel_task_params_deserialize() {
792 let json = r#"{
793 "tasks": [
794 { "agent": "explore", "description": "Find auth", "prompt": "Search auth files" },
795 { "agent": "general", "description": "Fix bug", "prompt": "Fix the login bug" }
796 ]
797 }"#;
798
799 let params: ParallelTaskParams = serde_json::from_str(json).unwrap();
800 assert_eq!(params.tasks.len(), 2);
801 assert_eq!(params.tasks[0].agent, "explore");
802 assert_eq!(params.tasks[1].agent, "general");
803 }
804
805 #[test]
806 fn test_parallel_task_params_single_task() {
807 let json = r#"{
808 "tasks": [
809 { "agent": "plan", "description": "Plan work", "prompt": "Create a plan" }
810 ]
811 }"#;
812
813 let params: ParallelTaskParams = serde_json::from_str(json).unwrap();
814 assert_eq!(params.tasks.len(), 1);
815 }
816
817 #[test]
818 fn test_parallel_task_params_empty_tasks() {
819 let json = r#"{ "tasks": [] }"#;
820 let params: ParallelTaskParams = serde_json::from_str(json).unwrap();
821 assert!(params.tasks.is_empty());
822 }
823
824 #[test]
825 fn test_parallel_task_params_missing_tasks() {
826 let json = r#"{}"#;
827 let result: Result<ParallelTaskParams, _> = serde_json::from_str(json);
828 assert!(result.is_err());
829 }
830
831 #[test]
832 fn test_parallel_task_params_serialize() {
833 let params = ParallelTaskParams {
834 tasks: vec![
835 TaskParams {
836 agent: "explore".to_string(),
837 description: "Task 1".to_string(),
838 prompt: "Prompt 1".to_string(),
839 background: false,
840 max_steps: None,
841 },
842 TaskParams {
843 agent: "general".to_string(),
844 description: "Task 2".to_string(),
845 prompt: "Prompt 2".to_string(),
846 background: false,
847 max_steps: Some(10),
848 },
849 ],
850 };
851 let json = serde_json::to_string(¶ms).unwrap();
852 assert!(json.contains("explore"));
853 assert!(json.contains("general"));
854 assert!(json.contains("Prompt 1"));
855 assert!(json.contains("Prompt 2"));
856 }
857
858 #[test]
859 fn test_parallel_task_params_roundtrip() {
860 let original = ParallelTaskParams {
861 tasks: vec![
862 TaskParams {
863 agent: "explore".to_string(),
864 description: "Explore".to_string(),
865 prompt: "Find files".to_string(),
866 background: false,
867 max_steps: None,
868 },
869 TaskParams {
870 agent: "plan".to_string(),
871 description: "Plan".to_string(),
872 prompt: "Make plan".to_string(),
873 background: false,
874 max_steps: Some(5),
875 },
876 ],
877 };
878 let json = serde_json::to_string(&original).unwrap();
879 let deserialized: ParallelTaskParams = serde_json::from_str(&json).unwrap();
880 assert_eq!(original.tasks.len(), deserialized.tasks.len());
881 assert_eq!(original.tasks[0].agent, deserialized.tasks[0].agent);
882 assert_eq!(original.tasks[1].agent, deserialized.tasks[1].agent);
883 assert_eq!(original.tasks[1].max_steps, deserialized.tasks[1].max_steps);
884 }
885
886 #[test]
887 fn test_parallel_task_params_clone() {
888 let params = ParallelTaskParams {
889 tasks: vec![TaskParams {
890 agent: "explore".to_string(),
891 description: "Test".to_string(),
892 prompt: "Prompt".to_string(),
893 background: false,
894 max_steps: None,
895 }],
896 };
897 let cloned = params.clone();
898 assert_eq!(params.tasks.len(), cloned.tasks.len());
899 assert_eq!(params.tasks[0].agent, cloned.tasks[0].agent);
900 }
901
902 #[test]
903 fn test_parallel_task_params_schema() {
904 let schema = parallel_task_params_schema();
905 assert_eq!(schema["type"], "object");
906 assert!(schema["properties"]["tasks"].is_object());
907 assert_eq!(schema["properties"]["tasks"]["type"], "array");
908 assert_eq!(schema["properties"]["tasks"]["minItems"], 1);
909 }
910
911 #[test]
912 fn test_parallel_task_params_schema_required() {
913 let schema = parallel_task_params_schema();
914 let required = schema["required"].as_array().unwrap();
915 assert!(required.contains(&serde_json::json!("tasks")));
916 }
917
918 #[test]
919 fn test_parallel_task_params_schema_items() {
920 let schema = parallel_task_params_schema();
921 let items = &schema["properties"]["tasks"]["items"];
922 assert_eq!(items["type"], "object");
923 let item_required = items["required"].as_array().unwrap();
924 assert!(item_required.contains(&serde_json::json!("agent")));
925 assert!(item_required.contains(&serde_json::json!("description")));
926 assert!(item_required.contains(&serde_json::json!("prompt")));
927 }
928
929 #[test]
930 fn test_parallel_task_params_debug() {
931 let params = ParallelTaskParams {
932 tasks: vec![TaskParams {
933 agent: "explore".to_string(),
934 description: "Debug test".to_string(),
935 prompt: "Test".to_string(),
936 background: false,
937 max_steps: None,
938 }],
939 };
940 let debug_str = format!("{:?}", params);
941 assert!(debug_str.contains("explore"));
942 assert!(debug_str.contains("Debug test"));
943 }
944}