Skip to main content

a3s_code_core/tools/
task.rs

1//! Task Tool for Spawning Subagents
2//!
3//! The Task tool allows the main agent to delegate specialized tasks to
4//! focused child agents (subagents). Each subagent runs in an isolated
5//! child session with restricted permissions.
6//!
7//! ## Usage
8//!
9//! ```json
10//! {
11//!   "agent": "explore",
12//!   "description": "Find authentication code",
13//!   "prompt": "Search for files related to user authentication..."
14//! }
15//! ```
16
17use 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/// Task tool parameters
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct TaskParams {
33    /// Agent type to use (explore, general, plan, etc.)
34    pub agent: String,
35    /// Short description of the task (for display)
36    pub description: String,
37    /// Detailed prompt for the agent
38    pub prompt: String,
39    /// Optional: run in background (default: false)
40    #[serde(default)]
41    pub background: bool,
42    /// Optional: maximum steps for this task
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub max_steps: Option<usize>,
45    /// Optional: allow all tool execution without confirmation (default: false)
46    #[serde(default)]
47    pub permissive: bool,
48}
49
50/// Task tool result
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct TaskResult {
53    /// Task output from the subagent
54    pub output: String,
55    /// Child session ID
56    pub session_id: String,
57    /// Agent type used
58    pub agent: String,
59    /// Whether the task succeeded
60    pub success: bool,
61    /// Task ID for tracking
62    pub task_id: String,
63}
64
65/// Task executor for running subagent tasks
66pub struct TaskExecutor {
67    /// Agent registry for looking up agent definitions
68    registry: Arc<AgentRegistry>,
69    /// LLM client used to power child agent loops
70    llm_client: Arc<dyn LlmClient>,
71    /// Workspace path shared with child agents
72    workspace: String,
73    /// Optional MCP manager for registering MCP tools in child sessions
74    mcp_manager: Option<Arc<McpManager>>,
75}
76
77impl TaskExecutor {
78    /// Create a new task executor
79    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    /// Create a new task executor with MCP manager for tool inheritance
93    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    /// Execute a task by spawning an isolated child AgentLoop.
108    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(&params.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        // Build a child ToolExecutor. Task tools are intentionally omitted
132        // here to prevent unlimited subagent nesting.
133        let mut child_executor = crate::tools::ToolExecutor::new(self.workspace.clone());
134
135        // Register MCP tools so child agents can access MCP servers.
136        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        // Inject the agent system prompt via the extra slot.
161        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        // Create an mpsc channel for the child agent and forward events to broadcast
192        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            // Spawn a task to forward events from mpsc to broadcast
197            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(&[], &params.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    /// Execute a task in the background.
236    ///
237    /// Returns immediately with the task ID. Use events to track progress.
238    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    /// Execute multiple tasks in parallel.
256    ///
257    /// Spawns all tasks concurrently and waits for all to complete.
258    /// Returns results in the same order as the input tasks.
259    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
310/// Get the JSON schema for TaskParams
311pub 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
346/// TaskTool wraps TaskExecutor as a Tool for registration in ToolExecutor.
347/// This allows the LLM to delegate tasks to subagents via the standard tool interface.
348pub struct TaskTool {
349    executor: Arc<TaskExecutor>,
350}
351
352impl TaskTool {
353    /// Create a new TaskTool
354    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/// Parameters for parallel task execution
400#[derive(Debug, Clone, Serialize, Deserialize)]
401pub struct ParallelTaskParams {
402    /// List of tasks to execute concurrently
403    pub tasks: Vec<TaskParams>,
404}
405
406/// Get the JSON schema for ParallelTaskParams
407pub 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
439/// ParallelTaskTool allows the LLM to fan-out multiple subagent tasks concurrently.
440///
441/// All tasks execute in parallel and the tool returns when all complete.
442pub struct ParallelTaskTool {
443    executor: Arc<TaskExecutor>,
444}
445
446impl ParallelTaskTool {
447    /// Create a new ParallelTaskTool
448    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        // Format results
483        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(&params).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(&params).unwrap();
725        // max_steps should not appear when None
726        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(&params).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(&params).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(&params).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        // Validate that ParallelTaskParams can hold 150 tasks without truncation
1028        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(&params).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        // max_steps = 0 is a valid edge case (callers decide enforcement)
1050        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(&params).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 &params.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); // Should default to false
1105    }
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}