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