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        sentinel_hook: Option<std::sync::Arc<dyn crate::hooks::HookExecutor>>,
113    ) -> Result<TaskResult> {
114        let task_id = format!("task-{}", uuid::Uuid::new_v4());
115        let session_id = format!("subagent-{}", task_id);
116
117        let agent = self
118            .registry
119            .get(&params.agent)
120            .context(format!("Unknown agent type: '{}'", params.agent))?;
121
122        if let Some(ref tx) = event_tx {
123            let _ = tx.send(AgentEvent::SubagentStart {
124                task_id: task_id.clone(),
125                session_id: session_id.clone(),
126                parent_session_id: String::new(),
127                agent: params.agent.clone(),
128                description: params.description.clone(),
129            });
130        }
131
132        // Build a child ToolExecutor. Task tools are intentionally omitted
133        // here to prevent unlimited subagent nesting.
134        let mut child_executor = crate::tools::ToolExecutor::new(self.workspace.clone());
135
136        // Register MCP tools so child agents can access MCP servers.
137        if let Some(ref mcp) = self.mcp_manager {
138            let all_tools = mcp.get_all_tools().await;
139            let mut by_server: std::collections::HashMap<
140                String,
141                Vec<crate::mcp::protocol::McpTool>,
142            > = std::collections::HashMap::new();
143            for (server, tool) in all_tools {
144                by_server.entry(server).or_default().push(tool);
145            }
146            for (server_name, tools) in by_server {
147                let wrappers =
148                    crate::mcp::tools::create_mcp_tools(&server_name, tools, Arc::clone(mcp));
149                for wrapper in wrappers {
150                    child_executor.register_dynamic_tool(wrapper);
151                }
152            }
153        }
154
155        if !agent.permissions.allow.is_empty() || !agent.permissions.deny.is_empty() {
156            child_executor.set_guard_policy(Arc::new(agent.permissions.clone())
157                as Arc<dyn crate::permissions::PermissionChecker>);
158        }
159        let child_executor = Arc::new(child_executor);
160
161        // Inject the agent system prompt via the extra slot.
162        let mut prompt_slots = crate::prompts::SystemPromptSlots::default();
163        if let Some(ref p) = agent.prompt {
164            prompt_slots.extra = Some(p.clone());
165        }
166
167        let child_config = AgentConfig {
168            prompt_slots,
169            tools: child_executor.definitions(),
170            max_tool_rounds: params
171                .max_steps
172                .unwrap_or_else(|| agent.max_steps.unwrap_or(20)),
173            permission_checker: if params.permissive {
174                Some(Arc::new(crate::permissions::PermissionPolicy::permissive())
175                    as Arc<dyn crate::permissions::PermissionChecker>)
176            } else {
177                None
178            },
179            // Inherit sentinel hook so this sub-agent is covered by the sentinel.
180            hook_engine: sentinel_hook.clone(),
181            ..AgentConfig::default()
182        };
183
184        // Propagate sentinel hook into the child ToolContext so any grandchild
185        // sub-agents spawned by this sub-agent also inherit sentinel coverage.
186        let mut tool_context =
187            ToolContext::new(PathBuf::from(&self.workspace)).with_session_id(session_id.clone());
188        if let Some(ref hook) = sentinel_hook {
189            tool_context = tool_context.with_sentinel_hook(hook.clone());
190        }
191
192        let agent_loop = AgentLoop::new(
193            Arc::clone(&self.llm_client),
194            child_executor,
195            tool_context,
196            child_config,
197        );
198
199        // Create an mpsc channel for the child agent and forward events to broadcast
200        let child_event_tx = if let Some(ref broadcast_tx) = event_tx {
201            let (mpsc_tx, mut mpsc_rx) = tokio::sync::mpsc::channel(100);
202            let broadcast_tx_clone = broadcast_tx.clone();
203
204            // Spawn a task to forward events from mpsc to broadcast
205            tokio::spawn(async move {
206                while let Some(event) = mpsc_rx.recv().await {
207                    let _ = broadcast_tx_clone.send(event);
208                }
209            });
210
211            Some(mpsc_tx)
212        } else {
213            None
214        };
215
216        let (output, success) = match agent_loop
217            .execute(&[], &params.prompt, child_event_tx)
218            .await
219        {
220            Ok(result) => (result.text, true),
221            Err(e) => (format!("Task failed: {}", e), false),
222        };
223
224        if let Some(ref tx) = event_tx {
225            let _ = tx.send(AgentEvent::SubagentEnd {
226                task_id: task_id.clone(),
227                session_id: session_id.clone(),
228                agent: params.agent.clone(),
229                output: output.clone(),
230                success,
231            });
232        }
233
234        Ok(TaskResult {
235            output,
236            session_id,
237            agent: params.agent,
238            success,
239            task_id,
240        })
241    }
242
243    /// Execute a task in the background.
244    ///
245    /// Returns immediately with the task ID. Use events to track progress.
246    pub fn execute_background(
247        self: Arc<Self>,
248        params: TaskParams,
249        event_tx: Option<broadcast::Sender<AgentEvent>>,
250        sentinel_hook: Option<std::sync::Arc<dyn crate::hooks::HookExecutor>>,
251    ) -> String {
252        let task_id = format!("task-{}", uuid::Uuid::new_v4());
253        let task_id_clone = task_id.clone();
254
255        tokio::spawn(async move {
256            if let Err(e) = self.execute(params, event_tx, sentinel_hook).await {
257                tracing::error!("Background task {} failed: {}", task_id_clone, e);
258            }
259        });
260
261        task_id
262    }
263
264    /// Execute multiple tasks in parallel.
265    ///
266    /// Spawns all tasks concurrently and waits for all to complete.
267    /// Returns results in the same order as the input tasks.
268    pub async fn execute_parallel(
269        self: &Arc<Self>,
270        tasks: Vec<TaskParams>,
271        event_tx: Option<broadcast::Sender<AgentEvent>>,
272        sentinel_hook: Option<std::sync::Arc<dyn crate::hooks::HookExecutor>>,
273    ) -> Vec<TaskResult> {
274        let mut join_set: JoinSet<(usize, TaskResult)> = JoinSet::new();
275
276        for (idx, params) in tasks.into_iter().enumerate() {
277            let executor = Arc::clone(self);
278            let tx = event_tx.clone();
279            let hook = sentinel_hook.clone();
280
281            join_set.spawn(async move {
282                let result = match executor.execute(params.clone(), tx, hook).await {
283                    Ok(result) => result,
284                    Err(e) => TaskResult {
285                        output: format!("Task failed: {}", e),
286                        session_id: String::new(),
287                        agent: params.agent,
288                        success: false,
289                        task_id: format!("task-{}", uuid::Uuid::new_v4()),
290                    },
291                };
292                (idx, result)
293            });
294        }
295
296        let mut indexed_results = Vec::new();
297        while let Some(result) = join_set.join_next().await {
298            match result {
299                Ok((idx, task_result)) => indexed_results.push((idx, task_result)),
300                Err(e) => {
301                    tracing::error!("Parallel task panicked: {}", e);
302                    indexed_results.push((
303                        usize::MAX,
304                        TaskResult {
305                            output: format!("Task panicked: {}", e),
306                            session_id: String::new(),
307                            agent: "unknown".to_string(),
308                            success: false,
309                            task_id: format!("task-{}", uuid::Uuid::new_v4()),
310                        },
311                    ));
312                }
313            }
314        }
315
316        indexed_results.sort_by_key(|(idx, _)| *idx);
317        indexed_results.into_iter().map(|(_, r)| r).collect()
318    }
319}
320
321/// Get the JSON schema for TaskParams
322pub fn task_params_schema() -> serde_json::Value {
323    serde_json::json!({
324        "type": "object",
325        "properties": {
326            "agent": {
327                "type": "string",
328                "description": "Agent type to use (explore, general, plan, etc.)"
329            },
330            "description": {
331                "type": "string",
332                "description": "Short description of the task (for display)"
333            },
334            "prompt": {
335                "type": "string",
336                "description": "Detailed prompt for the agent"
337            },
338            "background": {
339                "type": "boolean",
340                "description": "Run in background (default: false)",
341                "default": false
342            },
343            "max_steps": {
344                "type": "integer",
345                "description": "Maximum steps for this task"
346            },
347            "permissive": {
348                "type": "boolean",
349                "description": "Allow all tool execution without confirmation (default: false)",
350                "default": false
351            }
352        },
353        "required": ["agent", "description", "prompt"]
354    })
355}
356
357/// TaskTool wraps TaskExecutor as a Tool for registration in ToolExecutor.
358/// This allows the LLM to delegate tasks to subagents via the standard tool interface.
359pub struct TaskTool {
360    executor: Arc<TaskExecutor>,
361}
362
363impl TaskTool {
364    /// Create a new TaskTool
365    pub fn new(executor: Arc<TaskExecutor>) -> Self {
366        Self { executor }
367    }
368}
369
370#[async_trait]
371impl Tool for TaskTool {
372    fn name(&self) -> &str {
373        "task"
374    }
375
376    fn description(&self) -> &str {
377        "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."
378    }
379
380    fn parameters(&self) -> serde_json::Value {
381        task_params_schema()
382    }
383
384    async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> {
385        let params: TaskParams =
386            serde_json::from_value(args.clone()).context("Invalid task parameters")?;
387
388        if params.background {
389            let task_id = Arc::clone(&self.executor).execute_background(
390                params,
391                ctx.agent_event_tx.clone(),
392                ctx.sentinel_hook.clone(),
393            );
394            return Ok(ToolOutput::success(format!(
395                "Task started in background. Task ID: {}",
396                task_id
397            )));
398        }
399
400        let result = self
401            .executor
402            .execute(
403                params,
404                ctx.agent_event_tx.clone(),
405                ctx.sentinel_hook.clone(),
406            )
407            .await?;
408
409        if result.success {
410            Ok(ToolOutput::success(result.output))
411        } else {
412            Ok(ToolOutput::error(result.output))
413        }
414    }
415}
416
417/// Parameters for parallel task execution
418#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct ParallelTaskParams {
420    /// List of tasks to execute concurrently
421    pub tasks: Vec<TaskParams>,
422}
423
424/// Get the JSON schema for ParallelTaskParams
425pub fn parallel_task_params_schema() -> serde_json::Value {
426    serde_json::json!({
427        "type": "object",
428        "properties": {
429            "tasks": {
430                "type": "array",
431                "description": "List of tasks to execute in parallel. Each task runs as an independent subagent concurrently.",
432                "items": {
433                    "type": "object",
434                    "properties": {
435                        "agent": {
436                            "type": "string",
437                            "description": "Agent type to use (explore, general, plan, etc.)"
438                        },
439                        "description": {
440                            "type": "string",
441                            "description": "Short description of the task (for display)"
442                        },
443                        "prompt": {
444                            "type": "string",
445                            "description": "Detailed prompt for the agent"
446                        }
447                    },
448                    "required": ["agent", "description", "prompt"]
449                },
450                "minItems": 1
451            }
452        },
453        "required": ["tasks"]
454    })
455}
456
457/// ParallelTaskTool allows the LLM to fan-out multiple subagent tasks concurrently.
458///
459/// All tasks execute in parallel and the tool returns when all complete.
460pub struct ParallelTaskTool {
461    executor: Arc<TaskExecutor>,
462}
463
464impl ParallelTaskTool {
465    /// Create a new ParallelTaskTool
466    pub fn new(executor: Arc<TaskExecutor>) -> Self {
467        Self { executor }
468    }
469}
470
471#[async_trait]
472impl Tool for ParallelTaskTool {
473    fn name(&self) -> &str {
474        "parallel_task"
475    }
476
477    fn description(&self) -> &str {
478        "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."
479    }
480
481    fn parameters(&self) -> serde_json::Value {
482        parallel_task_params_schema()
483    }
484
485    async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> {
486        let params: ParallelTaskParams =
487            serde_json::from_value(args.clone()).context("Invalid parallel task parameters")?;
488
489        if params.tasks.is_empty() {
490            return Ok(ToolOutput::error("No tasks provided".to_string()));
491        }
492
493        let task_count = params.tasks.len();
494
495        let results = self
496            .executor
497            .execute_parallel(
498                params.tasks,
499                ctx.agent_event_tx.clone(),
500                ctx.sentinel_hook.clone(),
501            )
502            .await;
503
504        // Format results
505        let mut output = format!("Executed {} tasks in parallel:\n\n", task_count);
506        for (i, result) in results.iter().enumerate() {
507            let status = if result.success { "[OK]" } else { "[ERR]" };
508            output.push_str(&format!(
509                "--- Task {} ({}) {} ---\n{}\n\n",
510                i + 1,
511                result.agent,
512                status,
513                result.output
514            ));
515        }
516
517        Ok(ToolOutput::success(output))
518    }
519}
520
521/// Parameters for team-based task execution
522#[derive(Debug, Deserialize)]
523pub struct RunTeamParams {
524    /// Goal for the team to accomplish
525    pub goal: String,
526    /// Agent type for the Lead member (default: "general")
527    #[serde(default = "default_general")]
528    pub lead_agent: String,
529    /// Agent type for the Worker member (default: "general")
530    #[serde(default = "default_general")]
531    pub worker_agent: String,
532    /// Agent type for the Reviewer member (default: "general")
533    #[serde(default = "default_general")]
534    pub reviewer_agent: String,
535    /// Maximum steps per team member agent
536    pub max_steps: Option<usize>,
537}
538
539fn default_general() -> String {
540    "general".to_string()
541}
542
543/// Get the JSON schema for RunTeamParams
544pub fn run_team_params_schema() -> serde_json::Value {
545    serde_json::json!({
546        "type": "object",
547        "properties": {
548            "goal": {
549                "type": "string",
550                "description": "Goal for the team to accomplish. The Lead decomposes it into tasks, Workers execute them, and the Reviewer approves results."
551            },
552            "lead_agent": {
553                "type": "string",
554                "description": "Agent type for the Lead member (default: general)",
555                "default": "general"
556            },
557            "worker_agent": {
558                "type": "string",
559                "description": "Agent type for the Worker member (default: general)",
560                "default": "general"
561            },
562            "reviewer_agent": {
563                "type": "string",
564                "description": "Agent type for the Reviewer member (default: general)",
565                "default": "general"
566            },
567            "max_steps": {
568                "type": "integer",
569                "description": "Maximum steps per team member agent"
570            }
571        },
572        "required": ["goal"]
573    })
574}
575
576/// Bridge between TeamRunner's AgentExecutor trait and TaskExecutor.
577struct MemberExecutor {
578    executor: Arc<TaskExecutor>,
579    agent_type: String,
580    max_steps: Option<usize>,
581    event_tx: Option<tokio::sync::broadcast::Sender<crate::agent::AgentEvent>>,
582    sentinel_hook: Option<Arc<dyn crate::hooks::HookExecutor>>,
583}
584
585#[async_trait::async_trait]
586impl crate::agent_teams::AgentExecutor for MemberExecutor {
587    async fn execute(&self, prompt: &str) -> crate::error::Result<String> {
588        let params = TaskParams {
589            agent: self.agent_type.clone(),
590            description: "team-member".to_string(),
591            prompt: prompt.to_string(),
592            background: false,
593            max_steps: self.max_steps,
594            permissive: true,
595        };
596        let result = self
597            .executor
598            .execute(params, self.event_tx.clone(), self.sentinel_hook.clone())
599            .await
600            .map_err(|e| crate::error::CodeError::Internal(anyhow::anyhow!("{}", e)))?;
601        Ok(result.output)
602    }
603}
604
605/// RunTeamTool allows the LLM to trigger the Lead→Worker→Reviewer team workflow.
606///
607/// Completes the delegation triad alongside `task` and `parallel_task`. Use when a
608/// goal is complex enough to need dynamic decomposition, parallel execution, and
609/// quality review before acceptance.
610pub struct RunTeamTool {
611    executor: Arc<TaskExecutor>,
612}
613
614impl RunTeamTool {
615    /// Create a new RunTeamTool
616    pub fn new(executor: Arc<TaskExecutor>) -> Self {
617        Self { executor }
618    }
619}
620
621#[async_trait]
622impl Tool for RunTeamTool {
623    fn name(&self) -> &str {
624        "run_team"
625    }
626
627    fn description(&self) -> &str {
628        "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."
629    }
630
631    fn parameters(&self) -> serde_json::Value {
632        run_team_params_schema()
633    }
634
635    async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> {
636        let params: RunTeamParams =
637            serde_json::from_value(args.clone()).context("Invalid run_team parameters")?;
638
639        let make = |agent_type: String| -> Arc<dyn crate::agent_teams::AgentExecutor> {
640            Arc::new(MemberExecutor {
641                executor: Arc::clone(&self.executor),
642                agent_type,
643                max_steps: params.max_steps,
644                event_tx: ctx.agent_event_tx.clone(),
645                sentinel_hook: ctx.sentinel_hook.clone(),
646            })
647        };
648
649        let team_id = format!("team-{}", uuid::Uuid::new_v4());
650        let mut team =
651            crate::agent_teams::AgentTeam::new(&team_id, crate::agent_teams::TeamConfig::default());
652        team.add_member("lead", crate::agent_teams::TeamRole::Lead);
653        team.add_member("worker", crate::agent_teams::TeamRole::Worker);
654        team.add_member("reviewer", crate::agent_teams::TeamRole::Reviewer);
655
656        let mut runner = crate::agent_teams::TeamRunner::new(team);
657        runner
658            .bind_session("lead", make(params.lead_agent))
659            .context("Failed to bind lead session")?;
660        runner
661            .bind_session("worker", make(params.worker_agent))
662            .context("Failed to bind worker session")?;
663        runner
664            .bind_session("reviewer", make(params.reviewer_agent))
665            .context("Failed to bind reviewer session")?;
666
667        let result = runner
668            .run_until_done(&params.goal)
669            .await
670            .context("Team run failed")?;
671
672        let mut out = format!(
673            "Team run complete. Done: {}, Rejected: {}, Rounds: {}\n\n",
674            result.done_tasks.len(),
675            result.rejected_tasks.len(),
676            result.rounds
677        );
678        for task in &result.done_tasks {
679            out.push_str(&format!(
680                "[DONE] {}\n  Result: {}\n\n",
681                task.description,
682                task.result.as_deref().unwrap_or("(no result)")
683            ));
684        }
685        for task in &result.rejected_tasks {
686            out.push_str(&format!("[REJECTED] {}\n\n", task.description));
687        }
688
689        Ok(ToolOutput::success(out))
690    }
691}
692
693#[cfg(test)]
694mod tests {
695    use super::*;
696
697    #[test]
698    fn test_task_params_deserialize() {
699        let json = r#"{
700            "agent": "explore",
701            "description": "Find auth code",
702            "prompt": "Search for authentication files"
703        }"#;
704
705        let params: TaskParams = serde_json::from_str(json).unwrap();
706        assert_eq!(params.agent, "explore");
707        assert_eq!(params.description, "Find auth code");
708        assert!(!params.background);
709        assert!(!params.permissive);
710    }
711
712    #[test]
713    fn test_task_params_with_background() {
714        let json = r#"{
715            "agent": "general",
716            "description": "Long task",
717            "prompt": "Do something complex",
718            "background": true
719        }"#;
720
721        let params: TaskParams = serde_json::from_str(json).unwrap();
722        assert!(params.background);
723    }
724
725    #[test]
726    fn test_task_params_with_max_steps() {
727        let json = r#"{
728            "agent": "plan",
729            "description": "Planning task",
730            "prompt": "Create a plan",
731            "max_steps": 10
732        }"#;
733
734        let params: TaskParams = serde_json::from_str(json).unwrap();
735        assert_eq!(params.agent, "plan");
736        assert_eq!(params.max_steps, Some(10));
737        assert!(!params.background);
738    }
739
740    #[test]
741    fn test_task_params_all_fields() {
742        let json = r#"{
743            "agent": "general",
744            "description": "Complex task",
745            "prompt": "Do everything",
746            "background": true,
747            "max_steps": 20,
748            "permissive": true
749        }"#;
750
751        let params: TaskParams = serde_json::from_str(json).unwrap();
752        assert_eq!(params.agent, "general");
753        assert_eq!(params.description, "Complex task");
754        assert_eq!(params.prompt, "Do everything");
755        assert!(params.background);
756        assert_eq!(params.max_steps, Some(20));
757        assert!(params.permissive);
758    }
759
760    #[test]
761    fn test_task_params_missing_required_field() {
762        let json = r#"{
763            "agent": "explore",
764            "description": "Missing prompt"
765        }"#;
766
767        let result: Result<TaskParams, _> = serde_json::from_str(json);
768        assert!(result.is_err());
769    }
770
771    #[test]
772    fn test_task_params_serialize() {
773        let params = TaskParams {
774            agent: "explore".to_string(),
775            description: "Test task".to_string(),
776            prompt: "Test prompt".to_string(),
777            background: false,
778            max_steps: Some(5),
779            permissive: false,
780        };
781
782        let json = serde_json::to_string(&params).unwrap();
783        assert!(json.contains("explore"));
784        assert!(json.contains("Test task"));
785        assert!(json.contains("Test prompt"));
786    }
787
788    #[test]
789    fn test_task_params_clone() {
790        let params = TaskParams {
791            agent: "explore".to_string(),
792            description: "Test".to_string(),
793            prompt: "Prompt".to_string(),
794            background: true,
795            max_steps: None,
796            permissive: false,
797        };
798
799        let cloned = params.clone();
800        assert_eq!(params.agent, cloned.agent);
801        assert_eq!(params.description, cloned.description);
802        assert_eq!(params.background, cloned.background);
803    }
804
805    #[test]
806    fn test_task_result_serialize() {
807        let result = TaskResult {
808            output: "Found 5 files".to_string(),
809            session_id: "session-123".to_string(),
810            agent: "explore".to_string(),
811            success: true,
812            task_id: "task-456".to_string(),
813        };
814
815        let json = serde_json::to_string(&result).unwrap();
816        assert!(json.contains("Found 5 files"));
817        assert!(json.contains("explore"));
818    }
819
820    #[test]
821    fn test_task_result_deserialize() {
822        let json = r#"{
823            "output": "Task completed",
824            "session_id": "sess-789",
825            "agent": "general",
826            "success": false,
827            "task_id": "task-123"
828        }"#;
829
830        let result: TaskResult = serde_json::from_str(json).unwrap();
831        assert_eq!(result.output, "Task completed");
832        assert_eq!(result.session_id, "sess-789");
833        assert_eq!(result.agent, "general");
834        assert!(!result.success);
835        assert_eq!(result.task_id, "task-123");
836    }
837
838    #[test]
839    fn test_task_result_clone() {
840        let result = TaskResult {
841            output: "Output".to_string(),
842            session_id: "session-1".to_string(),
843            agent: "explore".to_string(),
844            success: true,
845            task_id: "task-1".to_string(),
846        };
847
848        let cloned = result.clone();
849        assert_eq!(result.output, cloned.output);
850        assert_eq!(result.success, cloned.success);
851    }
852
853    #[test]
854    fn test_task_params_schema() {
855        let schema = task_params_schema();
856        assert_eq!(schema["type"], "object");
857        assert!(schema["properties"]["agent"].is_object());
858        assert!(schema["properties"]["prompt"].is_object());
859    }
860
861    #[test]
862    fn test_task_params_schema_required_fields() {
863        let schema = task_params_schema();
864        let required = schema["required"].as_array().unwrap();
865        assert!(required.contains(&serde_json::json!("agent")));
866        assert!(required.contains(&serde_json::json!("description")));
867        assert!(required.contains(&serde_json::json!("prompt")));
868    }
869
870    #[test]
871    fn test_task_params_schema_properties() {
872        let schema = task_params_schema();
873        let props = &schema["properties"];
874
875        assert_eq!(props["agent"]["type"], "string");
876        assert_eq!(props["description"]["type"], "string");
877        assert_eq!(props["prompt"]["type"], "string");
878        assert_eq!(props["background"]["type"], "boolean");
879        assert_eq!(props["background"]["default"], false);
880        assert_eq!(props["max_steps"]["type"], "integer");
881    }
882
883    #[test]
884    fn test_task_params_schema_descriptions() {
885        let schema = task_params_schema();
886        let props = &schema["properties"];
887
888        assert!(props["agent"]["description"].is_string());
889        assert!(props["description"]["description"].is_string());
890        assert!(props["prompt"]["description"].is_string());
891        assert!(props["background"]["description"].is_string());
892        assert!(props["max_steps"]["description"].is_string());
893    }
894
895    #[test]
896    fn test_task_params_default_background() {
897        let params = TaskParams {
898            agent: "explore".to_string(),
899            description: "Test".to_string(),
900            prompt: "Test prompt".to_string(),
901            background: false,
902            max_steps: None,
903            permissive: false,
904        };
905        assert!(!params.background);
906    }
907
908    #[test]
909    fn test_task_params_serialize_skip_none() {
910        let params = TaskParams {
911            agent: "explore".to_string(),
912            description: "Test".to_string(),
913            prompt: "Test prompt".to_string(),
914            background: false,
915            max_steps: None,
916            permissive: false,
917        };
918        let json = serde_json::to_string(&params).unwrap();
919        // max_steps should not appear when None
920        assert!(!json.contains("max_steps"));
921    }
922
923    #[test]
924    fn test_task_params_serialize_with_max_steps() {
925        let params = TaskParams {
926            agent: "explore".to_string(),
927            description: "Test".to_string(),
928            prompt: "Test prompt".to_string(),
929            background: false,
930            max_steps: Some(15),
931            permissive: false,
932        };
933        let json = serde_json::to_string(&params).unwrap();
934        assert!(json.contains("max_steps"));
935        assert!(json.contains("15"));
936    }
937
938    #[test]
939    fn test_task_result_success_true() {
940        let result = TaskResult {
941            output: "Success".to_string(),
942            session_id: "sess-1".to_string(),
943            agent: "explore".to_string(),
944            success: true,
945            task_id: "task-1".to_string(),
946        };
947        assert!(result.success);
948    }
949
950    #[test]
951    fn test_task_result_success_false() {
952        let result = TaskResult {
953            output: "Failed".to_string(),
954            session_id: "sess-1".to_string(),
955            agent: "explore".to_string(),
956            success: false,
957            task_id: "task-1".to_string(),
958        };
959        assert!(!result.success);
960    }
961
962    #[test]
963    fn test_task_params_empty_strings() {
964        let params = TaskParams {
965            agent: "".to_string(),
966            description: "".to_string(),
967            prompt: "".to_string(),
968            background: false,
969            max_steps: None,
970            permissive: false,
971        };
972        let json = serde_json::to_string(&params).unwrap();
973        let deserialized: TaskParams = serde_json::from_str(&json).unwrap();
974        assert_eq!(deserialized.agent, "");
975        assert_eq!(deserialized.description, "");
976        assert_eq!(deserialized.prompt, "");
977    }
978
979    #[test]
980    fn test_task_result_empty_output() {
981        let result = TaskResult {
982            output: "".to_string(),
983            session_id: "sess-1".to_string(),
984            agent: "explore".to_string(),
985            success: true,
986            task_id: "task-1".to_string(),
987        };
988        assert_eq!(result.output, "");
989    }
990
991    #[test]
992    fn test_task_params_debug_format() {
993        let params = TaskParams {
994            agent: "explore".to_string(),
995            description: "Test".to_string(),
996            prompt: "Test prompt".to_string(),
997            background: false,
998            max_steps: None,
999            permissive: false,
1000        };
1001        let debug_str = format!("{:?}", params);
1002        assert!(debug_str.contains("explore"));
1003        assert!(debug_str.contains("Test"));
1004    }
1005
1006    #[test]
1007    fn test_task_result_debug_format() {
1008        let result = TaskResult {
1009            output: "Output".to_string(),
1010            session_id: "sess-1".to_string(),
1011            agent: "explore".to_string(),
1012            success: true,
1013            task_id: "task-1".to_string(),
1014        };
1015        let debug_str = format!("{:?}", result);
1016        assert!(debug_str.contains("Output"));
1017        assert!(debug_str.contains("explore"));
1018    }
1019
1020    #[test]
1021    fn test_task_params_roundtrip() {
1022        let original = TaskParams {
1023            agent: "general".to_string(),
1024            description: "Roundtrip test".to_string(),
1025            prompt: "Test roundtrip serialization".to_string(),
1026            background: true,
1027            max_steps: Some(42),
1028            permissive: true,
1029        };
1030        let json = serde_json::to_string(&original).unwrap();
1031        let deserialized: TaskParams = serde_json::from_str(&json).unwrap();
1032        assert_eq!(original.agent, deserialized.agent);
1033        assert_eq!(original.description, deserialized.description);
1034        assert_eq!(original.prompt, deserialized.prompt);
1035        assert_eq!(original.background, deserialized.background);
1036        assert_eq!(original.max_steps, deserialized.max_steps);
1037        assert_eq!(original.permissive, deserialized.permissive);
1038    }
1039
1040    #[test]
1041    fn test_task_result_roundtrip() {
1042        let original = TaskResult {
1043            output: "Roundtrip output".to_string(),
1044            session_id: "sess-roundtrip".to_string(),
1045            agent: "plan".to_string(),
1046            success: false,
1047            task_id: "task-roundtrip".to_string(),
1048        };
1049        let json = serde_json::to_string(&original).unwrap();
1050        let deserialized: TaskResult = serde_json::from_str(&json).unwrap();
1051        assert_eq!(original.output, deserialized.output);
1052        assert_eq!(original.session_id, deserialized.session_id);
1053        assert_eq!(original.agent, deserialized.agent);
1054        assert_eq!(original.success, deserialized.success);
1055        assert_eq!(original.task_id, deserialized.task_id);
1056    }
1057
1058    #[test]
1059    fn test_parallel_task_params_deserialize() {
1060        let json = r#"{
1061            "tasks": [
1062                { "agent": "explore", "description": "Find auth", "prompt": "Search auth files" },
1063                { "agent": "general", "description": "Fix bug", "prompt": "Fix the login bug" }
1064            ]
1065        }"#;
1066
1067        let params: ParallelTaskParams = serde_json::from_str(json).unwrap();
1068        assert_eq!(params.tasks.len(), 2);
1069        assert_eq!(params.tasks[0].agent, "explore");
1070        assert_eq!(params.tasks[1].agent, "general");
1071    }
1072
1073    #[test]
1074    fn test_parallel_task_params_single_task() {
1075        let json = r#"{
1076            "tasks": [
1077                { "agent": "plan", "description": "Plan work", "prompt": "Create a plan" }
1078            ]
1079        }"#;
1080
1081        let params: ParallelTaskParams = serde_json::from_str(json).unwrap();
1082        assert_eq!(params.tasks.len(), 1);
1083    }
1084
1085    #[test]
1086    fn test_parallel_task_params_empty_tasks() {
1087        let json = r#"{ "tasks": [] }"#;
1088        let params: ParallelTaskParams = serde_json::from_str(json).unwrap();
1089        assert!(params.tasks.is_empty());
1090    }
1091
1092    #[test]
1093    fn test_parallel_task_params_missing_tasks() {
1094        let json = r#"{}"#;
1095        let result: Result<ParallelTaskParams, _> = serde_json::from_str(json);
1096        assert!(result.is_err());
1097    }
1098
1099    #[test]
1100    fn test_parallel_task_params_serialize() {
1101        let params = ParallelTaskParams {
1102            tasks: vec![
1103                TaskParams {
1104                    agent: "explore".to_string(),
1105                    description: "Task 1".to_string(),
1106                    prompt: "Prompt 1".to_string(),
1107                    background: false,
1108                    max_steps: None,
1109                    permissive: false,
1110                },
1111                TaskParams {
1112                    agent: "general".to_string(),
1113                    description: "Task 2".to_string(),
1114                    prompt: "Prompt 2".to_string(),
1115                    background: false,
1116                    max_steps: Some(10),
1117                    permissive: false,
1118                },
1119            ],
1120        };
1121        let json = serde_json::to_string(&params).unwrap();
1122        assert!(json.contains("explore"));
1123        assert!(json.contains("general"));
1124        assert!(json.contains("Prompt 1"));
1125        assert!(json.contains("Prompt 2"));
1126    }
1127
1128    #[test]
1129    fn test_parallel_task_params_roundtrip() {
1130        let original = ParallelTaskParams {
1131            tasks: vec![
1132                TaskParams {
1133                    agent: "explore".to_string(),
1134                    description: "Explore".to_string(),
1135                    prompt: "Find files".to_string(),
1136                    background: false,
1137                    max_steps: None,
1138                    permissive: false,
1139                },
1140                TaskParams {
1141                    agent: "plan".to_string(),
1142                    description: "Plan".to_string(),
1143                    prompt: "Make plan".to_string(),
1144                    background: false,
1145                    max_steps: Some(5),
1146                    permissive: false,
1147                },
1148            ],
1149        };
1150        let json = serde_json::to_string(&original).unwrap();
1151        let deserialized: ParallelTaskParams = serde_json::from_str(&json).unwrap();
1152        assert_eq!(original.tasks.len(), deserialized.tasks.len());
1153        assert_eq!(original.tasks[0].agent, deserialized.tasks[0].agent);
1154        assert_eq!(original.tasks[1].agent, deserialized.tasks[1].agent);
1155        assert_eq!(original.tasks[1].max_steps, deserialized.tasks[1].max_steps);
1156    }
1157
1158    #[test]
1159    fn test_parallel_task_params_clone() {
1160        let params = ParallelTaskParams {
1161            tasks: vec![TaskParams {
1162                agent: "explore".to_string(),
1163                description: "Test".to_string(),
1164                prompt: "Prompt".to_string(),
1165                background: false,
1166                max_steps: None,
1167                permissive: false,
1168            }],
1169        };
1170        let cloned = params.clone();
1171        assert_eq!(params.tasks.len(), cloned.tasks.len());
1172        assert_eq!(params.tasks[0].agent, cloned.tasks[0].agent);
1173    }
1174
1175    #[test]
1176    fn test_parallel_task_params_schema() {
1177        let schema = parallel_task_params_schema();
1178        assert_eq!(schema["type"], "object");
1179        assert!(schema["properties"]["tasks"].is_object());
1180        assert_eq!(schema["properties"]["tasks"]["type"], "array");
1181        assert_eq!(schema["properties"]["tasks"]["minItems"], 1);
1182    }
1183
1184    #[test]
1185    fn test_parallel_task_params_schema_required() {
1186        let schema = parallel_task_params_schema();
1187        let required = schema["required"].as_array().unwrap();
1188        assert!(required.contains(&serde_json::json!("tasks")));
1189    }
1190
1191    #[test]
1192    fn test_parallel_task_params_schema_items() {
1193        let schema = parallel_task_params_schema();
1194        let items = &schema["properties"]["tasks"]["items"];
1195        assert_eq!(items["type"], "object");
1196        let item_required = items["required"].as_array().unwrap();
1197        assert!(item_required.contains(&serde_json::json!("agent")));
1198        assert!(item_required.contains(&serde_json::json!("description")));
1199        assert!(item_required.contains(&serde_json::json!("prompt")));
1200    }
1201
1202    #[test]
1203    fn test_parallel_task_params_debug() {
1204        let params = ParallelTaskParams {
1205            tasks: vec![TaskParams {
1206                agent: "explore".to_string(),
1207                description: "Debug test".to_string(),
1208                prompt: "Test".to_string(),
1209                background: false,
1210                max_steps: None,
1211                permissive: false,
1212            }],
1213        };
1214        let debug_str = format!("{:?}", params);
1215        assert!(debug_str.contains("explore"));
1216        assert!(debug_str.contains("Debug test"));
1217    }
1218
1219    #[test]
1220    fn test_parallel_task_params_large_count() {
1221        // Validate that ParallelTaskParams can hold 150 tasks without truncation
1222        let tasks: Vec<TaskParams> = (0..150)
1223            .map(|i| TaskParams {
1224                agent: "explore".to_string(),
1225                description: format!("Task {}", i),
1226                prompt: format!("Prompt for task {}", i),
1227                background: false,
1228                max_steps: Some(10),
1229                permissive: false,
1230            })
1231            .collect();
1232
1233        let params = ParallelTaskParams { tasks };
1234        let json = serde_json::to_string(&params).unwrap();
1235        let deserialized: ParallelTaskParams = serde_json::from_str(&json).unwrap();
1236        assert_eq!(deserialized.tasks.len(), 150);
1237        assert_eq!(deserialized.tasks[0].description, "Task 0");
1238        assert_eq!(deserialized.tasks[149].description, "Task 149");
1239    }
1240
1241    #[test]
1242    fn test_task_params_max_steps_zero() {
1243        // max_steps = 0 is a valid edge case (callers decide enforcement)
1244        let params = TaskParams {
1245            agent: "explore".to_string(),
1246            description: "Edge case".to_string(),
1247            prompt: "Zero steps".to_string(),
1248            background: false,
1249            max_steps: Some(0),
1250            permissive: false,
1251        };
1252        let json = serde_json::to_string(&params).unwrap();
1253        let deserialized: TaskParams = serde_json::from_str(&json).unwrap();
1254        assert_eq!(deserialized.max_steps, Some(0));
1255    }
1256
1257    #[test]
1258    fn test_parallel_task_params_all_background() {
1259        let tasks: Vec<TaskParams> = (0..5)
1260            .map(|i| TaskParams {
1261                agent: "general".to_string(),
1262                description: format!("BG task {}", i),
1263                prompt: "Run in background".to_string(),
1264                background: true,
1265                max_steps: None,
1266                permissive: false,
1267            })
1268            .collect();
1269        let params = ParallelTaskParams { tasks };
1270        for task in &params.tasks {
1271            assert!(task.background);
1272        }
1273    }
1274
1275    #[test]
1276    fn test_task_params_permissive_true() {
1277        let json = r#"{
1278            "agent": "general",
1279            "description": "Permissive task",
1280            "prompt": "Run without confirmation",
1281            "permissive": true
1282        }"#;
1283
1284        let params: TaskParams = serde_json::from_str(json).unwrap();
1285        assert_eq!(params.agent, "general");
1286        assert!(params.permissive);
1287    }
1288
1289    #[test]
1290    fn test_task_params_permissive_default() {
1291        let json = r#"{
1292            "agent": "general",
1293            "description": "Default task",
1294            "prompt": "Run with default settings"
1295        }"#;
1296
1297        let params: TaskParams = serde_json::from_str(json).unwrap();
1298        assert!(!params.permissive); // Should default to false
1299    }
1300
1301    #[test]
1302    fn test_task_params_schema_permissive_field() {
1303        let schema = task_params_schema();
1304        let props = &schema["properties"];
1305
1306        assert_eq!(props["permissive"]["type"], "boolean");
1307        assert_eq!(props["permissive"]["default"], false);
1308        assert!(props["permissive"]["description"].is_string());
1309    }
1310
1311    #[test]
1312    fn test_run_team_params_deserialize_minimal() {
1313        let json = r#"{"goal": "Audit the auth system"}"#;
1314        let params: RunTeamParams = serde_json::from_str(json).unwrap();
1315        assert_eq!(params.goal, "Audit the auth system");
1316    }
1317
1318    #[test]
1319    fn test_run_team_params_defaults() {
1320        let json = r#"{"goal": "Do something complex"}"#;
1321        let params: RunTeamParams = serde_json::from_str(json).unwrap();
1322        assert_eq!(params.lead_agent, "general");
1323        assert_eq!(params.worker_agent, "general");
1324        assert_eq!(params.reviewer_agent, "general");
1325        assert!(params.max_steps.is_none());
1326    }
1327
1328    #[test]
1329    fn test_run_team_params_schema() {
1330        let schema = run_team_params_schema();
1331        assert_eq!(schema["type"], "object");
1332        let required = schema["required"].as_array().unwrap();
1333        assert!(required.contains(&serde_json::json!("goal")));
1334        assert!(!required.contains(&serde_json::json!("lead_agent")));
1335        assert!(!required.contains(&serde_json::json!("worker_agent")));
1336        assert!(!required.contains(&serde_json::json!("reviewer_agent")));
1337        assert!(!required.contains(&serde_json::json!("max_steps")));
1338    }
1339}