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