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        "additionalProperties": false,
315        "properties": {
316            "agent": {
317                "type": "string",
318                "description": "Required. Canonical agent type to use (for example: explore, general, plan). Always provide this exact field name: 'agent'."
319            },
320            "description": {
321                "type": "string",
322                "description": "Required. Short task label for display and tracking. Always provide this exact field name: 'description'."
323            },
324            "prompt": {
325                "type": "string",
326                "description": "Required. Detailed instruction for the delegated subagent. Always provide this exact field name: 'prompt'."
327            },
328            "background": {
329                "type": "boolean",
330                "description": "Optional. Run the task in the background. Default: false.",
331                "default": false
332            },
333            "max_steps": {
334                "type": "integer",
335                "description": "Optional. Maximum number of steps for this task."
336            },
337            "permissive": {
338                "type": "boolean",
339                "description": "Optional. Allow tool execution without confirmation. Default: false.",
340                "default": false
341            }
342        },
343        "required": ["agent", "description", "prompt"],
344        "examples": [
345            {
346                "agent": "explore",
347                "description": "Find Rust files",
348                "prompt": "Search the workspace for Rust files and summarize the layout."
349            },
350            {
351                "agent": "general",
352                "description": "Investigate test failure",
353                "prompt": "Inspect the failing tests and explain the root cause.",
354                "max_steps": 6,
355                "permissive": true
356            }
357        ]
358    })
359}
360
361/// TaskTool wraps TaskExecutor as a Tool for registration in ToolExecutor.
362/// This allows the LLM to delegate tasks to subagents via the standard tool interface.
363pub struct TaskTool {
364    executor: Arc<TaskExecutor>,
365}
366
367impl TaskTool {
368    /// Create a new TaskTool
369    pub fn new(executor: Arc<TaskExecutor>) -> Self {
370        Self { executor }
371    }
372}
373
374#[async_trait]
375impl Tool for TaskTool {
376    fn name(&self) -> &str {
377        "task"
378    }
379
380    fn description(&self) -> &str {
381        "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."
382    }
383
384    fn parameters(&self) -> serde_json::Value {
385        task_params_schema()
386    }
387
388    async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> {
389        let params: TaskParams =
390            serde_json::from_value(args.clone()).context("Invalid task parameters")?;
391
392        if params.background {
393            let task_id =
394                Arc::clone(&self.executor).execute_background(params, ctx.agent_event_tx.clone());
395            return Ok(ToolOutput::success(format!(
396                "Task started in background. Task ID: {}",
397                task_id
398            )));
399        }
400
401        let result = self
402            .executor
403            .execute(params, ctx.agent_event_tx.clone())
404            .await?;
405
406        if result.success {
407            Ok(ToolOutput::success(result.output))
408        } else {
409            Ok(ToolOutput::error(result.output))
410        }
411    }
412}
413
414/// Parameters for parallel task execution
415#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct ParallelTaskParams {
417    /// List of tasks to execute concurrently
418    pub tasks: Vec<TaskParams>,
419}
420
421/// Get the JSON schema for ParallelTaskParams
422pub fn parallel_task_params_schema() -> serde_json::Value {
423    serde_json::json!({
424        "type": "object",
425        "additionalProperties": false,
426        "properties": {
427            "tasks": {
428                "type": "array",
429                "description": "List of tasks to execute in parallel. Each task runs as an independent subagent concurrently.",
430                "items": {
431                    "type": "object",
432                    "additionalProperties": false,
433                    "properties": {
434                        "agent": {
435                            "type": "string",
436                            "description": "Required. Canonical agent type for this task."
437                        },
438                        "description": {
439                            "type": "string",
440                            "description": "Required. Short task label for display and tracking."
441                        },
442                        "prompt": {
443                            "type": "string",
444                            "description": "Required. Detailed instruction for the delegated subagent."
445                        }
446                    },
447                    "required": ["agent", "description", "prompt"]
448                },
449                "minItems": 1
450            }
451        },
452        "required": ["tasks"],
453        "examples": [
454            {
455                "tasks": [
456                    {
457                        "agent": "explore",
458                        "description": "Find Rust files",
459                        "prompt": "List Rust files under src/."
460                    },
461                    {
462                        "agent": "explore",
463                        "description": "Find tests",
464                        "prompt": "List test files and summarize their purpose."
465                    }
466                ]
467            }
468        ]
469    })
470}
471
472/// ParallelTaskTool allows the LLM to fan-out multiple subagent tasks concurrently.
473///
474/// All tasks execute in parallel and the tool returns when all complete.
475pub struct ParallelTaskTool {
476    executor: Arc<TaskExecutor>,
477}
478
479impl ParallelTaskTool {
480    /// Create a new ParallelTaskTool
481    pub fn new(executor: Arc<TaskExecutor>) -> Self {
482        Self { executor }
483    }
484}
485
486#[async_trait]
487impl Tool for ParallelTaskTool {
488    fn name(&self) -> &str {
489        "parallel_task"
490    }
491
492    fn description(&self) -> &str {
493        "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."
494    }
495
496    fn parameters(&self) -> serde_json::Value {
497        parallel_task_params_schema()
498    }
499
500    async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> {
501        let params: ParallelTaskParams =
502            serde_json::from_value(args.clone()).context("Invalid parallel task parameters")?;
503
504        if params.tasks.is_empty() {
505            return Ok(ToolOutput::error("No tasks provided".to_string()));
506        }
507
508        let task_count = params.tasks.len();
509
510        let results = self
511            .executor
512            .execute_parallel(params.tasks, ctx.agent_event_tx.clone())
513            .await;
514
515        // Format results
516        let mut output = format!("Executed {} tasks in parallel:\n\n", task_count);
517        for (i, result) in results.iter().enumerate() {
518            let status = if result.success { "[OK]" } else { "[ERR]" };
519            output.push_str(&format!(
520                "--- Task {} ({}) {} ---\n{}\n\n",
521                i + 1,
522                result.agent,
523                status,
524                result.output
525            ));
526        }
527
528        Ok(ToolOutput::success(output))
529    }
530}
531
532/// Parameters for team-based task execution
533#[derive(Debug, Deserialize)]
534pub struct RunTeamParams {
535    /// Goal for the team to accomplish
536    pub goal: String,
537    /// Agent type for the Lead member (default: "general")
538    #[serde(default = "default_general")]
539    pub lead_agent: String,
540    /// Agent type for the Worker member (default: "general")
541    #[serde(default = "default_general")]
542    pub worker_agent: String,
543    /// Agent type for the Reviewer member (default: "general")
544    #[serde(default = "default_general")]
545    pub reviewer_agent: String,
546    /// Maximum steps per team member agent
547    pub max_steps: Option<usize>,
548}
549
550fn default_general() -> String {
551    "general".to_string()
552}
553
554/// Get the JSON schema for RunTeamParams
555pub fn run_team_params_schema() -> serde_json::Value {
556    serde_json::json!({
557        "type": "object",
558        "additionalProperties": false,
559        "properties": {
560            "goal": {
561                "type": "string",
562                "description": "Required. Goal for the team to accomplish. The Lead decomposes it into tasks, Workers execute them, and the Reviewer approves results."
563            },
564            "lead_agent": {
565                "type": "string",
566                "description": "Optional. Agent type for the Lead member. Default: general.",
567                "default": "general"
568            },
569            "worker_agent": {
570                "type": "string",
571                "description": "Optional. Agent type for the Worker member. Default: general.",
572                "default": "general"
573            },
574            "reviewer_agent": {
575                "type": "string",
576                "description": "Optional. Agent type for the Reviewer member. Default: general.",
577                "default": "general"
578            },
579            "max_steps": {
580                "type": "integer",
581                "description": "Optional. Maximum steps per team member agent."
582            }
583        },
584        "required": ["goal"],
585        "examples": [
586            {
587                "goal": "Fix the failing integration test and explain the root cause.",
588                "lead_agent": "general",
589                "worker_agent": "explore",
590                "reviewer_agent": "general",
591                "max_steps": 6
592            }
593        ]
594    })
595}
596
597/// Bridge between TeamRunner's AgentExecutor trait and TaskExecutor.
598struct MemberExecutor {
599    executor: Arc<TaskExecutor>,
600    agent_type: String,
601    max_steps: Option<usize>,
602    event_tx: Option<tokio::sync::broadcast::Sender<crate::agent::AgentEvent>>,
603}
604
605#[async_trait::async_trait]
606impl crate::agent_teams::AgentExecutor for MemberExecutor {
607    async fn execute(&self, prompt: &str) -> crate::error::Result<String> {
608        let params = TaskParams {
609            agent: self.agent_type.clone(),
610            description: "team-member".to_string(),
611            prompt: prompt.to_string(),
612            background: false,
613            max_steps: self.max_steps,
614            permissive: true,
615        };
616        let result = self
617            .executor
618            .execute(params, self.event_tx.clone())
619            .await
620            .map_err(|e| crate::error::CodeError::Internal(anyhow::anyhow!("{}", e)))?;
621        Ok(result.output)
622    }
623}
624
625/// RunTeamTool allows the LLM to trigger the Lead→Worker→Reviewer team workflow.
626///
627/// Completes the delegation triad alongside `task` and `parallel_task`. Use when a
628/// goal is complex enough to need dynamic decomposition, parallel execution, and
629/// quality review before acceptance.
630pub struct RunTeamTool {
631    executor: Arc<TaskExecutor>,
632}
633
634impl RunTeamTool {
635    /// Create a new RunTeamTool
636    pub fn new(executor: Arc<TaskExecutor>) -> Self {
637        Self { executor }
638    }
639}
640
641#[async_trait]
642impl Tool for RunTeamTool {
643    fn name(&self) -> &str {
644        "run_team"
645    }
646
647    fn description(&self) -> &str {
648        "Run a complex goal through a Lead→Worker→Reviewer team. The Lead decomposes the goal into tasks, Workers execute them concurrently, and the Reviewer approves or rejects results (with rejected tasks retried). Use when: the goal has an unknown number of subtasks, results need quality verification, or tasks may need retry with feedback."
649    }
650
651    fn parameters(&self) -> serde_json::Value {
652        run_team_params_schema()
653    }
654
655    async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> {
656        let params: RunTeamParams =
657            serde_json::from_value(args.clone()).context("Invalid run_team parameters")?;
658
659        let make = |agent_type: String| -> Arc<dyn crate::agent_teams::AgentExecutor> {
660            Arc::new(MemberExecutor {
661                executor: Arc::clone(&self.executor),
662                agent_type,
663                max_steps: params.max_steps,
664                event_tx: ctx.agent_event_tx.clone(),
665            })
666        };
667
668        let team_id = format!("team-{}", uuid::Uuid::new_v4());
669        let mut team =
670            crate::agent_teams::AgentTeam::new(&team_id, crate::agent_teams::TeamConfig::default());
671        team.add_member("lead", crate::agent_teams::TeamRole::Lead);
672        team.add_member("worker", crate::agent_teams::TeamRole::Worker);
673        team.add_member("reviewer", crate::agent_teams::TeamRole::Reviewer);
674
675        let mut runner = crate::agent_teams::TeamRunner::new(team);
676        runner
677            .bind_session("lead", make(params.lead_agent))
678            .context("Failed to bind lead session")?;
679        runner
680            .bind_session("worker", make(params.worker_agent))
681            .context("Failed to bind worker session")?;
682        runner
683            .bind_session("reviewer", make(params.reviewer_agent))
684            .context("Failed to bind reviewer session")?;
685
686        let result = runner
687            .run_until_done(&params.goal)
688            .await
689            .context("Team run failed")?;
690
691        let mut out = format!(
692            "Team run complete. Done: {}, Rejected: {}, Rounds: {}\n\n",
693            result.done_tasks.len(),
694            result.rejected_tasks.len(),
695            result.rounds
696        );
697        for task in &result.done_tasks {
698            out.push_str(&format!(
699                "[DONE] {}\n  Result: {}\n\n",
700                task.description,
701                task.result.as_deref().unwrap_or("(no result)")
702            ));
703        }
704        for task in &result.rejected_tasks {
705            out.push_str(&format!("[REJECTED] {}\n\n", task.description));
706        }
707
708        Ok(ToolOutput::success(out))
709    }
710}
711
712#[cfg(test)]
713mod tests {
714    use super::*;
715
716    #[test]
717    fn test_task_params_deserialize() {
718        let json = r#"{
719            "agent": "explore",
720            "description": "Find auth code",
721            "prompt": "Search for authentication files"
722        }"#;
723
724        let params: TaskParams = serde_json::from_str(json).unwrap();
725        assert_eq!(params.agent, "explore");
726        assert_eq!(params.description, "Find auth code");
727        assert!(!params.background);
728        assert!(!params.permissive);
729    }
730
731    #[test]
732    fn test_task_params_with_background() {
733        let json = r#"{
734            "agent": "general",
735            "description": "Long task",
736            "prompt": "Do something complex",
737            "background": true
738        }"#;
739
740        let params: TaskParams = serde_json::from_str(json).unwrap();
741        assert!(params.background);
742    }
743
744    #[test]
745    fn test_task_params_with_max_steps() {
746        let json = r#"{
747            "agent": "plan",
748            "description": "Planning task",
749            "prompt": "Create a plan",
750            "max_steps": 10
751        }"#;
752
753        let params: TaskParams = serde_json::from_str(json).unwrap();
754        assert_eq!(params.agent, "plan");
755        assert_eq!(params.max_steps, Some(10));
756        assert!(!params.background);
757    }
758
759    #[test]
760    fn test_task_params_all_fields() {
761        let json = r#"{
762            "agent": "general",
763            "description": "Complex task",
764            "prompt": "Do everything",
765            "background": true,
766            "max_steps": 20,
767            "permissive": true
768        }"#;
769
770        let params: TaskParams = serde_json::from_str(json).unwrap();
771        assert_eq!(params.agent, "general");
772        assert_eq!(params.description, "Complex task");
773        assert_eq!(params.prompt, "Do everything");
774        assert!(params.background);
775        assert_eq!(params.max_steps, Some(20));
776        assert!(params.permissive);
777    }
778
779    #[test]
780    fn test_task_params_missing_required_field() {
781        let json = r#"{
782            "agent": "explore",
783            "description": "Missing prompt"
784        }"#;
785
786        let result: Result<TaskParams, _> = serde_json::from_str(json);
787        assert!(result.is_err());
788    }
789
790    #[test]
791    fn test_task_params_serialize() {
792        let params = TaskParams {
793            agent: "explore".to_string(),
794            description: "Test task".to_string(),
795            prompt: "Test prompt".to_string(),
796            background: false,
797            max_steps: Some(5),
798            permissive: false,
799        };
800
801        let json = serde_json::to_string(&params).unwrap();
802        assert!(json.contains("explore"));
803        assert!(json.contains("Test task"));
804        assert!(json.contains("Test prompt"));
805    }
806
807    #[test]
808    fn test_task_params_clone() {
809        let params = TaskParams {
810            agent: "explore".to_string(),
811            description: "Test".to_string(),
812            prompt: "Prompt".to_string(),
813            background: true,
814            max_steps: None,
815            permissive: false,
816        };
817
818        let cloned = params.clone();
819        assert_eq!(params.agent, cloned.agent);
820        assert_eq!(params.description, cloned.description);
821        assert_eq!(params.background, cloned.background);
822    }
823
824    #[test]
825    fn test_task_result_serialize() {
826        let result = TaskResult {
827            output: "Found 5 files".to_string(),
828            session_id: "session-123".to_string(),
829            agent: "explore".to_string(),
830            success: true,
831            task_id: "task-456".to_string(),
832        };
833
834        let json = serde_json::to_string(&result).unwrap();
835        assert!(json.contains("Found 5 files"));
836        assert!(json.contains("explore"));
837    }
838
839    #[test]
840    fn test_task_result_deserialize() {
841        let json = r#"{
842            "output": "Task completed",
843            "session_id": "sess-789",
844            "agent": "general",
845            "success": false,
846            "task_id": "task-123"
847        }"#;
848
849        let result: TaskResult = serde_json::from_str(json).unwrap();
850        assert_eq!(result.output, "Task completed");
851        assert_eq!(result.session_id, "sess-789");
852        assert_eq!(result.agent, "general");
853        assert!(!result.success);
854        assert_eq!(result.task_id, "task-123");
855    }
856
857    #[test]
858    fn test_task_result_clone() {
859        let result = TaskResult {
860            output: "Output".to_string(),
861            session_id: "session-1".to_string(),
862            agent: "explore".to_string(),
863            success: true,
864            task_id: "task-1".to_string(),
865        };
866
867        let cloned = result.clone();
868        assert_eq!(result.output, cloned.output);
869        assert_eq!(result.success, cloned.success);
870    }
871
872    #[test]
873    fn test_task_params_schema() {
874        let schema = task_params_schema();
875        assert_eq!(schema["type"], "object");
876        assert_eq!(schema["additionalProperties"], false);
877        assert!(schema["properties"]["agent"].is_object());
878        assert!(schema["properties"]["prompt"].is_object());
879    }
880
881    #[test]
882    fn test_task_params_schema_required_fields() {
883        let schema = task_params_schema();
884        let required = schema["required"].as_array().unwrap();
885        assert!(required.contains(&serde_json::json!("agent")));
886        assert!(required.contains(&serde_json::json!("description")));
887        assert!(required.contains(&serde_json::json!("prompt")));
888    }
889
890    #[test]
891    fn test_task_params_schema_properties() {
892        let schema = task_params_schema();
893        let props = &schema["properties"];
894
895        assert_eq!(props["agent"]["type"], "string");
896        assert_eq!(props["description"]["type"], "string");
897        assert_eq!(props["prompt"]["type"], "string");
898        assert_eq!(props["background"]["type"], "boolean");
899        assert_eq!(props["background"]["default"], false);
900        assert_eq!(props["max_steps"]["type"], "integer");
901    }
902
903    #[test]
904    fn test_task_params_schema_descriptions() {
905        let schema = task_params_schema();
906        let props = &schema["properties"];
907
908        assert!(props["agent"]["description"].is_string());
909        assert!(props["description"]["description"].is_string());
910        assert!(props["prompt"]["description"].is_string());
911        assert!(props["background"]["description"].is_string());
912        assert!(props["max_steps"]["description"].is_string());
913    }
914
915    #[test]
916    fn test_task_params_default_background() {
917        let params = TaskParams {
918            agent: "explore".to_string(),
919            description: "Test".to_string(),
920            prompt: "Test prompt".to_string(),
921            background: false,
922            max_steps: None,
923            permissive: false,
924        };
925        assert!(!params.background);
926    }
927
928    #[test]
929    fn test_task_params_serialize_skip_none() {
930        let params = TaskParams {
931            agent: "explore".to_string(),
932            description: "Test".to_string(),
933            prompt: "Test prompt".to_string(),
934            background: false,
935            max_steps: None,
936            permissive: false,
937        };
938        let json = serde_json::to_string(&params).unwrap();
939        // max_steps should not appear when None
940        assert!(!json.contains("max_steps"));
941    }
942
943    #[test]
944    fn test_task_params_serialize_with_max_steps() {
945        let params = TaskParams {
946            agent: "explore".to_string(),
947            description: "Test".to_string(),
948            prompt: "Test prompt".to_string(),
949            background: false,
950            max_steps: Some(15),
951            permissive: false,
952        };
953        let json = serde_json::to_string(&params).unwrap();
954        assert!(json.contains("max_steps"));
955        assert!(json.contains("15"));
956    }
957
958    #[test]
959    fn test_task_result_success_true() {
960        let result = TaskResult {
961            output: "Success".to_string(),
962            session_id: "sess-1".to_string(),
963            agent: "explore".to_string(),
964            success: true,
965            task_id: "task-1".to_string(),
966        };
967        assert!(result.success);
968    }
969
970    #[test]
971    fn test_task_result_success_false() {
972        let result = TaskResult {
973            output: "Failed".to_string(),
974            session_id: "sess-1".to_string(),
975            agent: "explore".to_string(),
976            success: false,
977            task_id: "task-1".to_string(),
978        };
979        assert!(!result.success);
980    }
981
982    #[test]
983    fn test_task_params_empty_strings() {
984        let params = TaskParams {
985            agent: "".to_string(),
986            description: "".to_string(),
987            prompt: "".to_string(),
988            background: false,
989            max_steps: None,
990            permissive: false,
991        };
992        let json = serde_json::to_string(&params).unwrap();
993        let deserialized: TaskParams = serde_json::from_str(&json).unwrap();
994        assert_eq!(deserialized.agent, "");
995        assert_eq!(deserialized.description, "");
996        assert_eq!(deserialized.prompt, "");
997    }
998
999    #[test]
1000    fn test_task_result_empty_output() {
1001        let result = TaskResult {
1002            output: "".to_string(),
1003            session_id: "sess-1".to_string(),
1004            agent: "explore".to_string(),
1005            success: true,
1006            task_id: "task-1".to_string(),
1007        };
1008        assert_eq!(result.output, "");
1009    }
1010
1011    #[test]
1012    fn test_task_params_debug_format() {
1013        let params = TaskParams {
1014            agent: "explore".to_string(),
1015            description: "Test".to_string(),
1016            prompt: "Test prompt".to_string(),
1017            background: false,
1018            max_steps: None,
1019            permissive: false,
1020        };
1021        let debug_str = format!("{:?}", params);
1022        assert!(debug_str.contains("explore"));
1023        assert!(debug_str.contains("Test"));
1024    }
1025
1026    #[test]
1027    fn test_task_result_debug_format() {
1028        let result = TaskResult {
1029            output: "Output".to_string(),
1030            session_id: "sess-1".to_string(),
1031            agent: "explore".to_string(),
1032            success: true,
1033            task_id: "task-1".to_string(),
1034        };
1035        let debug_str = format!("{:?}", result);
1036        assert!(debug_str.contains("Output"));
1037        assert!(debug_str.contains("explore"));
1038    }
1039
1040    #[test]
1041    fn test_task_params_roundtrip() {
1042        let original = TaskParams {
1043            agent: "general".to_string(),
1044            description: "Roundtrip test".to_string(),
1045            prompt: "Test roundtrip serialization".to_string(),
1046            background: true,
1047            max_steps: Some(42),
1048            permissive: true,
1049        };
1050        let json = serde_json::to_string(&original).unwrap();
1051        let deserialized: TaskParams = serde_json::from_str(&json).unwrap();
1052        assert_eq!(original.agent, deserialized.agent);
1053        assert_eq!(original.description, deserialized.description);
1054        assert_eq!(original.prompt, deserialized.prompt);
1055        assert_eq!(original.background, deserialized.background);
1056        assert_eq!(original.max_steps, deserialized.max_steps);
1057        assert_eq!(original.permissive, deserialized.permissive);
1058    }
1059
1060    #[test]
1061    fn test_task_result_roundtrip() {
1062        let original = TaskResult {
1063            output: "Roundtrip output".to_string(),
1064            session_id: "sess-roundtrip".to_string(),
1065            agent: "plan".to_string(),
1066            success: false,
1067            task_id: "task-roundtrip".to_string(),
1068        };
1069        let json = serde_json::to_string(&original).unwrap();
1070        let deserialized: TaskResult = serde_json::from_str(&json).unwrap();
1071        assert_eq!(original.output, deserialized.output);
1072        assert_eq!(original.session_id, deserialized.session_id);
1073        assert_eq!(original.agent, deserialized.agent);
1074        assert_eq!(original.success, deserialized.success);
1075        assert_eq!(original.task_id, deserialized.task_id);
1076    }
1077
1078    #[test]
1079    fn test_parallel_task_params_deserialize() {
1080        let json = r#"{
1081            "tasks": [
1082                { "agent": "explore", "description": "Find auth", "prompt": "Search auth files" },
1083                { "agent": "general", "description": "Fix bug", "prompt": "Fix the login bug" }
1084            ]
1085        }"#;
1086
1087        let params: ParallelTaskParams = serde_json::from_str(json).unwrap();
1088        assert_eq!(params.tasks.len(), 2);
1089        assert_eq!(params.tasks[0].agent, "explore");
1090        assert_eq!(params.tasks[1].agent, "general");
1091    }
1092
1093    #[test]
1094    fn test_parallel_task_params_single_task() {
1095        let json = r#"{
1096            "tasks": [
1097                { "agent": "plan", "description": "Plan work", "prompt": "Create a plan" }
1098            ]
1099        }"#;
1100
1101        let params: ParallelTaskParams = serde_json::from_str(json).unwrap();
1102        assert_eq!(params.tasks.len(), 1);
1103    }
1104
1105    #[test]
1106    fn test_parallel_task_params_empty_tasks() {
1107        let json = r#"{ "tasks": [] }"#;
1108        let params: ParallelTaskParams = serde_json::from_str(json).unwrap();
1109        assert!(params.tasks.is_empty());
1110    }
1111
1112    #[test]
1113    fn test_parallel_task_params_missing_tasks() {
1114        let json = r#"{}"#;
1115        let result: Result<ParallelTaskParams, _> = serde_json::from_str(json);
1116        assert!(result.is_err());
1117    }
1118
1119    #[test]
1120    fn test_parallel_task_params_serialize() {
1121        let params = ParallelTaskParams {
1122            tasks: vec![
1123                TaskParams {
1124                    agent: "explore".to_string(),
1125                    description: "Task 1".to_string(),
1126                    prompt: "Prompt 1".to_string(),
1127                    background: false,
1128                    max_steps: None,
1129                    permissive: false,
1130                },
1131                TaskParams {
1132                    agent: "general".to_string(),
1133                    description: "Task 2".to_string(),
1134                    prompt: "Prompt 2".to_string(),
1135                    background: false,
1136                    max_steps: Some(10),
1137                    permissive: false,
1138                },
1139            ],
1140        };
1141        let json = serde_json::to_string(&params).unwrap();
1142        assert!(json.contains("explore"));
1143        assert!(json.contains("general"));
1144        assert!(json.contains("Prompt 1"));
1145        assert!(json.contains("Prompt 2"));
1146    }
1147
1148    #[test]
1149    fn test_parallel_task_params_roundtrip() {
1150        let original = ParallelTaskParams {
1151            tasks: vec![
1152                TaskParams {
1153                    agent: "explore".to_string(),
1154                    description: "Explore".to_string(),
1155                    prompt: "Find files".to_string(),
1156                    background: false,
1157                    max_steps: None,
1158                    permissive: false,
1159                },
1160                TaskParams {
1161                    agent: "plan".to_string(),
1162                    description: "Plan".to_string(),
1163                    prompt: "Make plan".to_string(),
1164                    background: false,
1165                    max_steps: Some(5),
1166                    permissive: false,
1167                },
1168            ],
1169        };
1170        let json = serde_json::to_string(&original).unwrap();
1171        let deserialized: ParallelTaskParams = serde_json::from_str(&json).unwrap();
1172        assert_eq!(original.tasks.len(), deserialized.tasks.len());
1173        assert_eq!(original.tasks[0].agent, deserialized.tasks[0].agent);
1174        assert_eq!(original.tasks[1].agent, deserialized.tasks[1].agent);
1175        assert_eq!(original.tasks[1].max_steps, deserialized.tasks[1].max_steps);
1176    }
1177
1178    #[test]
1179    fn test_parallel_task_params_clone() {
1180        let params = ParallelTaskParams {
1181            tasks: vec![TaskParams {
1182                agent: "explore".to_string(),
1183                description: "Test".to_string(),
1184                prompt: "Prompt".to_string(),
1185                background: false,
1186                max_steps: None,
1187                permissive: false,
1188            }],
1189        };
1190        let cloned = params.clone();
1191        assert_eq!(params.tasks.len(), cloned.tasks.len());
1192        assert_eq!(params.tasks[0].agent, cloned.tasks[0].agent);
1193    }
1194
1195    #[test]
1196    fn test_parallel_task_params_schema() {
1197        let schema = parallel_task_params_schema();
1198        assert_eq!(schema["type"], "object");
1199        assert_eq!(schema["additionalProperties"], false);
1200        assert!(schema["properties"]["tasks"].is_object());
1201        assert_eq!(schema["properties"]["tasks"]["type"], "array");
1202        assert_eq!(schema["properties"]["tasks"]["minItems"], 1);
1203    }
1204
1205    #[test]
1206    fn test_parallel_task_params_schema_required() {
1207        let schema = parallel_task_params_schema();
1208        let required = schema["required"].as_array().unwrap();
1209        assert!(required.contains(&serde_json::json!("tasks")));
1210    }
1211
1212    #[test]
1213    fn test_parallel_task_params_schema_items() {
1214        let schema = parallel_task_params_schema();
1215        let items = &schema["properties"]["tasks"]["items"];
1216        assert_eq!(items["type"], "object");
1217        assert_eq!(items["additionalProperties"], false);
1218        let item_required = items["required"].as_array().unwrap();
1219        assert!(item_required.contains(&serde_json::json!("agent")));
1220        assert!(item_required.contains(&serde_json::json!("description")));
1221        assert!(item_required.contains(&serde_json::json!("prompt")));
1222    }
1223
1224    #[test]
1225    fn test_task_and_team_schema_examples() {
1226        let task = task_params_schema();
1227        let task_examples = task["examples"].as_array().unwrap();
1228        assert_eq!(task_examples[0]["agent"], "explore");
1229        assert!(task_examples[0].get("task").is_none());
1230
1231        let parallel = parallel_task_params_schema();
1232        let parallel_examples = parallel["examples"].as_array().unwrap();
1233        assert!(parallel_examples[0]["tasks"].as_array().unwrap().len() >= 1);
1234
1235        let team = run_team_params_schema();
1236        let team_examples = team["examples"].as_array().unwrap();
1237        assert!(team_examples[0]["goal"].is_string());
1238        assert!(team_examples[0].get("task").is_none());
1239    }
1240
1241    #[test]
1242    fn test_parallel_task_params_debug() {
1243        let params = ParallelTaskParams {
1244            tasks: vec![TaskParams {
1245                agent: "explore".to_string(),
1246                description: "Debug test".to_string(),
1247                prompt: "Test".to_string(),
1248                background: false,
1249                max_steps: None,
1250                permissive: false,
1251            }],
1252        };
1253        let debug_str = format!("{:?}", params);
1254        assert!(debug_str.contains("explore"));
1255        assert!(debug_str.contains("Debug test"));
1256    }
1257
1258    #[test]
1259    fn test_parallel_task_params_large_count() {
1260        // Validate that ParallelTaskParams can hold 150 tasks without truncation
1261        let tasks: Vec<TaskParams> = (0..150)
1262            .map(|i| TaskParams {
1263                agent: "explore".to_string(),
1264                description: format!("Task {}", i),
1265                prompt: format!("Prompt for task {}", i),
1266                background: false,
1267                max_steps: Some(10),
1268                permissive: false,
1269            })
1270            .collect();
1271
1272        let params = ParallelTaskParams { tasks };
1273        let json = serde_json::to_string(&params).unwrap();
1274        let deserialized: ParallelTaskParams = serde_json::from_str(&json).unwrap();
1275        assert_eq!(deserialized.tasks.len(), 150);
1276        assert_eq!(deserialized.tasks[0].description, "Task 0");
1277        assert_eq!(deserialized.tasks[149].description, "Task 149");
1278    }
1279
1280    #[test]
1281    fn test_task_params_max_steps_zero() {
1282        // max_steps = 0 is a valid edge case (callers decide enforcement)
1283        let params = TaskParams {
1284            agent: "explore".to_string(),
1285            description: "Edge case".to_string(),
1286            prompt: "Zero steps".to_string(),
1287            background: false,
1288            max_steps: Some(0),
1289            permissive: false,
1290        };
1291        let json = serde_json::to_string(&params).unwrap();
1292        let deserialized: TaskParams = serde_json::from_str(&json).unwrap();
1293        assert_eq!(deserialized.max_steps, Some(0));
1294    }
1295
1296    #[test]
1297    fn test_parallel_task_params_all_background() {
1298        let tasks: Vec<TaskParams> = (0..5)
1299            .map(|i| TaskParams {
1300                agent: "general".to_string(),
1301                description: format!("BG task {}", i),
1302                prompt: "Run in background".to_string(),
1303                background: true,
1304                max_steps: None,
1305                permissive: false,
1306            })
1307            .collect();
1308        let params = ParallelTaskParams { tasks };
1309        for task in &params.tasks {
1310            assert!(task.background);
1311        }
1312    }
1313
1314    #[test]
1315    fn test_task_params_permissive_true() {
1316        let json = r#"{
1317            "agent": "general",
1318            "description": "Permissive task",
1319            "prompt": "Run without confirmation",
1320            "permissive": true
1321        }"#;
1322
1323        let params: TaskParams = serde_json::from_str(json).unwrap();
1324        assert_eq!(params.agent, "general");
1325        assert!(params.permissive);
1326    }
1327
1328    #[test]
1329    fn test_task_params_permissive_default() {
1330        let json = r#"{
1331            "agent": "general",
1332            "description": "Default task",
1333            "prompt": "Run with default settings"
1334        }"#;
1335
1336        let params: TaskParams = serde_json::from_str(json).unwrap();
1337        assert!(!params.permissive); // Should default to false
1338    }
1339
1340    #[test]
1341    fn test_task_params_schema_permissive_field() {
1342        let schema = task_params_schema();
1343        let props = &schema["properties"];
1344
1345        assert_eq!(props["permissive"]["type"], "boolean");
1346        assert_eq!(props["permissive"]["default"], false);
1347        assert!(props["permissive"]["description"].is_string());
1348    }
1349
1350    #[test]
1351    fn test_run_team_params_deserialize_minimal() {
1352        let json = r#"{"goal": "Audit the auth system"}"#;
1353        let params: RunTeamParams = serde_json::from_str(json).unwrap();
1354        assert_eq!(params.goal, "Audit the auth system");
1355    }
1356
1357    #[test]
1358    fn test_run_team_params_defaults() {
1359        let json = r#"{"goal": "Do something complex"}"#;
1360        let params: RunTeamParams = serde_json::from_str(json).unwrap();
1361        assert_eq!(params.lead_agent, "general");
1362        assert_eq!(params.worker_agent, "general");
1363        assert_eq!(params.reviewer_agent, "general");
1364        assert!(params.max_steps.is_none());
1365    }
1366
1367    #[test]
1368    fn test_run_team_params_schema() {
1369        let schema = run_team_params_schema();
1370        assert_eq!(schema["type"], "object");
1371        assert_eq!(schema["additionalProperties"], false);
1372        let required = schema["required"].as_array().unwrap();
1373        assert!(required.contains(&serde_json::json!("goal")));
1374        assert!(!required.contains(&serde_json::json!("lead_agent")));
1375        assert!(!required.contains(&serde_json::json!("worker_agent")));
1376        assert!(!required.contains(&serde_json::json!("reviewer_agent")));
1377        assert!(!required.contains(&serde_json::json!("max_steps")));
1378    }
1379}