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::AgentEvent;
18use crate::session::{SessionConfig, SessionManager};
19use crate::subagent::AgentRegistry;
20use crate::tools::types::{Tool, ToolContext, ToolOutput};
21use anyhow::{Context, Result};
22use async_trait::async_trait;
23use serde::{Deserialize, Serialize};
24use std::sync::Arc;
25use tokio::sync::broadcast;
26use tokio::task::JoinSet;
27
28/// Task tool parameters
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct TaskParams {
31    /// Agent type to use (explore, general, plan, etc.)
32    pub agent: String,
33    /// Short description of the task (for display)
34    pub description: String,
35    /// Detailed prompt for the agent
36    pub prompt: String,
37    /// Optional: run in background (default: false)
38    #[serde(default)]
39    pub background: bool,
40    /// Optional: maximum steps for this task
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub max_steps: Option<usize>,
43}
44
45/// Task tool result
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct TaskResult {
48    /// Task output from the subagent
49    pub output: String,
50    /// Child session ID
51    pub session_id: String,
52    /// Agent type used
53    pub agent: String,
54    /// Whether the task succeeded
55    pub success: bool,
56    /// Task ID for tracking
57    pub task_id: String,
58}
59
60/// Task executor for running subagent tasks
61pub struct TaskExecutor {
62    /// Agent registry for looking up agent definitions
63    registry: Arc<AgentRegistry>,
64    /// Session manager for creating child sessions
65    session_manager: Arc<SessionManager>,
66}
67
68impl TaskExecutor {
69    /// Create a new task executor
70    pub fn new(registry: Arc<AgentRegistry>, session_manager: Arc<SessionManager>) -> Self {
71        Self {
72            registry,
73            session_manager,
74        }
75    }
76
77    /// Execute a task in a subagent
78    ///
79    /// This creates a child session, runs the prompt, and returns the result.
80    pub async fn execute(
81        &self,
82        parent_session_id: &str,
83        params: TaskParams,
84        event_tx: Option<broadcast::Sender<AgentEvent>>,
85    ) -> Result<TaskResult> {
86        // Generate unique task ID
87        let task_id = format!("task-{}", uuid::Uuid::new_v4());
88
89        // Get agent definition
90        let agent = self
91            .registry
92            .get(&params.agent)
93            .context(format!("Unknown agent type: {}", params.agent))?;
94
95        // Check if parent session can spawn subagents
96        // (This would be checked against the parent's agent definition)
97
98        // Create child session config
99        let child_config = SessionConfig {
100            name: format!("{} - {}", params.agent, params.description),
101            workspace: String::new(), // Inherit from parent
102            system_prompt: agent.prompt.clone(),
103            max_context_length: 0,
104            auto_compact: false,
105            auto_compact_threshold: crate::session::DEFAULT_AUTO_COMPACT_THRESHOLD,
106            storage_type: crate::config::StorageBackend::Memory, // Subagents use memory storage
107            queue_config: None,
108            confirmation_policy: None,
109            permission_policy: Some(agent.permissions.clone()),
110            parent_id: Some(parent_session_id.to_string()),
111            security_config: None,
112            hook_engine: None,
113            planning_enabled: false,
114            goal_tracking: false,
115        };
116
117        // Generate child session ID
118        let child_session_id = format!("{}-{}", parent_session_id, task_id);
119
120        // Emit SubagentStart event
121        if let Some(ref tx) = event_tx {
122            let _ = tx.send(AgentEvent::SubagentStart {
123                task_id: task_id.clone(),
124                session_id: child_session_id.clone(),
125                parent_session_id: parent_session_id.to_string(),
126                agent: params.agent.clone(),
127                description: params.description.clone(),
128            });
129        }
130
131        // Create child session
132        let session_id = self
133            .session_manager
134            .create_child_session(parent_session_id, child_session_id.clone(), child_config)
135            .await
136            .map_err(|e| anyhow::anyhow!("Failed to create child session: {}", e))?;
137
138        // Execute the prompt in the child session
139        let result = self
140            .session_manager
141            .generate(&session_id, &params.prompt)
142            .await;
143
144        // Process result
145        let (output, success): (String, bool) = match result {
146            Ok(agent_result) => (agent_result.text, true),
147            Err(e) => (format!("Task failed: {}", e), false),
148        };
149
150        // Emit SubagentEnd event
151        if let Some(ref tx) = event_tx {
152            let _ = tx.send(AgentEvent::SubagentEnd {
153                task_id: task_id.clone(),
154                session_id: session_id.clone(),
155                agent: params.agent.clone(),
156                output: output.clone(),
157                success,
158            });
159        }
160
161        Ok(TaskResult {
162            output,
163            session_id,
164            agent: params.agent,
165            success,
166            task_id,
167        })
168    }
169
170    /// Execute a task in the background
171    ///
172    /// Returns immediately with the task ID. Use events to track progress.
173    pub fn execute_background(
174        self: Arc<Self>,
175        parent_session_id: String,
176        params: TaskParams,
177        event_tx: Option<broadcast::Sender<AgentEvent>>,
178    ) -> String {
179        let task_id = format!("task-{}", uuid::Uuid::new_v4());
180        let task_id_clone = task_id.clone();
181
182        tokio::spawn(async move {
183            let result = self.execute(&parent_session_id, params, event_tx).await;
184
185            if let Err(e) = result {
186                tracing::error!("Background task {} failed: {}", task_id_clone, e);
187            }
188        });
189
190        task_id
191    }
192
193    /// Execute multiple tasks in parallel
194    ///
195    /// Spawns all tasks concurrently and waits for all to complete.
196    /// Returns results in the same order as the input tasks.
197    pub async fn execute_parallel(
198        self: &Arc<Self>,
199        parent_session_id: &str,
200        tasks: Vec<TaskParams>,
201        event_tx: Option<broadcast::Sender<AgentEvent>>,
202    ) -> Vec<TaskResult> {
203        let mut join_set: JoinSet<(usize, TaskResult)> = JoinSet::new();
204
205        // Spawn all tasks concurrently, tracking original index
206        for (idx, params) in tasks.into_iter().enumerate() {
207            let executor = Arc::clone(self);
208            let parent_id = parent_session_id.to_string();
209            let tx = event_tx.clone();
210
211            join_set.spawn(async move {
212                let result = match executor.execute(&parent_id, params.clone(), tx).await {
213                    Ok(result) => result,
214                    Err(e) => TaskResult {
215                        output: format!("Task failed: {}", e),
216                        session_id: String::new(),
217                        agent: params.agent,
218                        success: false,
219                        task_id: format!("task-{}", uuid::Uuid::new_v4()),
220                    },
221                };
222                (idx, result)
223            });
224        }
225
226        // Collect all results (completion order), then sort by original index
227        let mut indexed_results = Vec::new();
228        while let Some(result) = join_set.join_next().await {
229            match result {
230                Ok((idx, task_result)) => indexed_results.push((idx, task_result)),
231                Err(e) => {
232                    tracing::error!("Parallel task panicked: {}", e);
233                    // Can't recover original index on panic; append at end
234                    indexed_results.push((
235                        usize::MAX,
236                        TaskResult {
237                            output: format!("Task panicked: {}", e),
238                            session_id: String::new(),
239                            agent: "unknown".to_string(),
240                            success: false,
241                            task_id: format!("task-{}", uuid::Uuid::new_v4()),
242                        },
243                    ));
244                }
245            }
246        }
247
248        indexed_results.sort_by_key(|(idx, _)| *idx);
249        indexed_results.into_iter().map(|(_, r)| r).collect()
250    }
251}
252
253/// Get the JSON schema for TaskParams
254pub fn task_params_schema() -> serde_json::Value {
255    serde_json::json!({
256        "type": "object",
257        "properties": {
258            "agent": {
259                "type": "string",
260                "description": "Agent type to use (explore, general, plan, etc.)"
261            },
262            "description": {
263                "type": "string",
264                "description": "Short description of the task (for display)"
265            },
266            "prompt": {
267                "type": "string",
268                "description": "Detailed prompt for the agent"
269            },
270            "background": {
271                "type": "boolean",
272                "description": "Run in background (default: false)",
273                "default": false
274            },
275            "max_steps": {
276                "type": "integer",
277                "description": "Maximum steps for this task"
278            }
279        },
280        "required": ["agent", "description", "prompt"]
281    })
282}
283
284/// TaskTool wraps TaskExecutor as a Tool for registration in ToolExecutor.
285/// This allows the LLM to delegate tasks to subagents via the standard tool interface.
286pub struct TaskTool {
287    executor: Arc<TaskExecutor>,
288}
289
290impl TaskTool {
291    /// Create a new TaskTool
292    pub fn new(executor: Arc<TaskExecutor>) -> Self {
293        Self { executor }
294    }
295}
296
297#[async_trait]
298impl Tool for TaskTool {
299    fn name(&self) -> &str {
300        "task"
301    }
302
303    fn description(&self) -> &str {
304        "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."
305    }
306
307    fn parameters(&self) -> serde_json::Value {
308        task_params_schema()
309    }
310
311    async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> {
312        let params: TaskParams =
313            serde_json::from_value(args.clone()).context("Invalid task parameters")?;
314
315        let session_id = ctx.session_id.as_deref().unwrap_or("unknown");
316
317        if params.background {
318            let task_id = Arc::clone(&self.executor).execute_background(
319                session_id.to_string(),
320                params,
321                ctx.agent_event_tx.clone(),
322            );
323            return Ok(ToolOutput::success(format!(
324                "Task started in background. Task ID: {}",
325                task_id
326            )));
327        }
328
329        let result = self
330            .executor
331            .execute(session_id, params, ctx.agent_event_tx.clone())
332            .await?;
333
334        if result.success {
335            Ok(ToolOutput::success(result.output))
336        } else {
337            Ok(ToolOutput::error(result.output))
338        }
339    }
340}
341
342/// Parameters for parallel task execution
343#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct ParallelTaskParams {
345    /// List of tasks to execute concurrently
346    pub tasks: Vec<TaskParams>,
347}
348
349/// Get the JSON schema for ParallelTaskParams
350pub fn parallel_task_params_schema() -> serde_json::Value {
351    serde_json::json!({
352        "type": "object",
353        "properties": {
354            "tasks": {
355                "type": "array",
356                "description": "List of tasks to execute in parallel. Each task runs as an independent subagent concurrently.",
357                "items": {
358                    "type": "object",
359                    "properties": {
360                        "agent": {
361                            "type": "string",
362                            "description": "Agent type to use (explore, general, plan, etc.)"
363                        },
364                        "description": {
365                            "type": "string",
366                            "description": "Short description of the task (for display)"
367                        },
368                        "prompt": {
369                            "type": "string",
370                            "description": "Detailed prompt for the agent"
371                        }
372                    },
373                    "required": ["agent", "description", "prompt"]
374                },
375                "minItems": 1
376            }
377        },
378        "required": ["tasks"]
379    })
380}
381
382/// ParallelTaskTool allows the LLM to fan-out multiple subagent tasks concurrently.
383///
384/// All tasks execute in parallel and the tool returns when all complete.
385pub struct ParallelTaskTool {
386    executor: Arc<TaskExecutor>,
387}
388
389impl ParallelTaskTool {
390    /// Create a new ParallelTaskTool
391    pub fn new(executor: Arc<TaskExecutor>) -> Self {
392        Self { executor }
393    }
394}
395
396#[async_trait]
397impl Tool for ParallelTaskTool {
398    fn name(&self) -> &str {
399        "parallel_task"
400    }
401
402    fn description(&self) -> &str {
403        "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."
404    }
405
406    fn parameters(&self) -> serde_json::Value {
407        parallel_task_params_schema()
408    }
409
410    async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> {
411        let params: ParallelTaskParams =
412            serde_json::from_value(args.clone()).context("Invalid parallel task parameters")?;
413
414        if params.tasks.is_empty() {
415            return Ok(ToolOutput::error("No tasks provided".to_string()));
416        }
417
418        let session_id = ctx.session_id.as_deref().unwrap_or("unknown");
419        let task_count = params.tasks.len();
420
421        let results = self
422            .executor
423            .execute_parallel(session_id, params.tasks, ctx.agent_event_tx.clone())
424            .await;
425
426        // Format results
427        let mut output = format!("Executed {} tasks in parallel:\n\n", task_count);
428        for (i, result) in results.iter().enumerate() {
429            let status = if result.success { "[OK]" } else { "[ERR]" };
430            output.push_str(&format!(
431                "--- Task {} ({}) {} ---\n{}\n\n",
432                i + 1,
433                result.agent,
434                status,
435                result.output
436            ));
437        }
438
439        Ok(ToolOutput::success(output))
440    }
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446
447    #[test]
448    fn test_task_params_deserialize() {
449        let json = r#"{
450            "agent": "explore",
451            "description": "Find auth code",
452            "prompt": "Search for authentication files"
453        }"#;
454
455        let params: TaskParams = serde_json::from_str(json).unwrap();
456        assert_eq!(params.agent, "explore");
457        assert_eq!(params.description, "Find auth code");
458        assert!(!params.background);
459    }
460
461    #[test]
462    fn test_task_params_with_background() {
463        let json = r#"{
464            "agent": "general",
465            "description": "Long task",
466            "prompt": "Do something complex",
467            "background": true
468        }"#;
469
470        let params: TaskParams = serde_json::from_str(json).unwrap();
471        assert!(params.background);
472    }
473
474    #[test]
475    fn test_task_params_with_max_steps() {
476        let json = r#"{
477            "agent": "plan",
478            "description": "Planning task",
479            "prompt": "Create a plan",
480            "max_steps": 10
481        }"#;
482
483        let params: TaskParams = serde_json::from_str(json).unwrap();
484        assert_eq!(params.agent, "plan");
485        assert_eq!(params.max_steps, Some(10));
486        assert!(!params.background);
487    }
488
489    #[test]
490    fn test_task_params_all_fields() {
491        let json = r#"{
492            "agent": "general",
493            "description": "Complex task",
494            "prompt": "Do everything",
495            "background": true,
496            "max_steps": 20
497        }"#;
498
499        let params: TaskParams = serde_json::from_str(json).unwrap();
500        assert_eq!(params.agent, "general");
501        assert_eq!(params.description, "Complex task");
502        assert_eq!(params.prompt, "Do everything");
503        assert!(params.background);
504        assert_eq!(params.max_steps, Some(20));
505    }
506
507    #[test]
508    fn test_task_params_missing_required_field() {
509        let json = r#"{
510            "agent": "explore",
511            "description": "Missing prompt"
512        }"#;
513
514        let result: Result<TaskParams, _> = serde_json::from_str(json);
515        assert!(result.is_err());
516    }
517
518    #[test]
519    fn test_task_params_serialize() {
520        let params = TaskParams {
521            agent: "explore".to_string(),
522            description: "Test task".to_string(),
523            prompt: "Test prompt".to_string(),
524            background: false,
525            max_steps: Some(5),
526        };
527
528        let json = serde_json::to_string(&params).unwrap();
529        assert!(json.contains("explore"));
530        assert!(json.contains("Test task"));
531        assert!(json.contains("Test prompt"));
532    }
533
534    #[test]
535    fn test_task_params_clone() {
536        let params = TaskParams {
537            agent: "explore".to_string(),
538            description: "Test".to_string(),
539            prompt: "Prompt".to_string(),
540            background: true,
541            max_steps: None,
542        };
543
544        let cloned = params.clone();
545        assert_eq!(params.agent, cloned.agent);
546        assert_eq!(params.description, cloned.description);
547        assert_eq!(params.background, cloned.background);
548    }
549
550    #[test]
551    fn test_task_result_serialize() {
552        let result = TaskResult {
553            output: "Found 5 files".to_string(),
554            session_id: "session-123".to_string(),
555            agent: "explore".to_string(),
556            success: true,
557            task_id: "task-456".to_string(),
558        };
559
560        let json = serde_json::to_string(&result).unwrap();
561        assert!(json.contains("Found 5 files"));
562        assert!(json.contains("explore"));
563    }
564
565    #[test]
566    fn test_task_result_deserialize() {
567        let json = r#"{
568            "output": "Task completed",
569            "session_id": "sess-789",
570            "agent": "general",
571            "success": false,
572            "task_id": "task-123"
573        }"#;
574
575        let result: TaskResult = serde_json::from_str(json).unwrap();
576        assert_eq!(result.output, "Task completed");
577        assert_eq!(result.session_id, "sess-789");
578        assert_eq!(result.agent, "general");
579        assert!(!result.success);
580        assert_eq!(result.task_id, "task-123");
581    }
582
583    #[test]
584    fn test_task_result_clone() {
585        let result = TaskResult {
586            output: "Output".to_string(),
587            session_id: "session-1".to_string(),
588            agent: "explore".to_string(),
589            success: true,
590            task_id: "task-1".to_string(),
591        };
592
593        let cloned = result.clone();
594        assert_eq!(result.output, cloned.output);
595        assert_eq!(result.success, cloned.success);
596    }
597
598    #[test]
599    fn test_task_params_schema() {
600        let schema = task_params_schema();
601        assert_eq!(schema["type"], "object");
602        assert!(schema["properties"]["agent"].is_object());
603        assert!(schema["properties"]["prompt"].is_object());
604    }
605
606    #[test]
607    fn test_task_params_schema_required_fields() {
608        let schema = task_params_schema();
609        let required = schema["required"].as_array().unwrap();
610        assert!(required.contains(&serde_json::json!("agent")));
611        assert!(required.contains(&serde_json::json!("description")));
612        assert!(required.contains(&serde_json::json!("prompt")));
613    }
614
615    #[test]
616    fn test_task_params_schema_properties() {
617        let schema = task_params_schema();
618        let props = &schema["properties"];
619
620        assert_eq!(props["agent"]["type"], "string");
621        assert_eq!(props["description"]["type"], "string");
622        assert_eq!(props["prompt"]["type"], "string");
623        assert_eq!(props["background"]["type"], "boolean");
624        assert_eq!(props["background"]["default"], false);
625        assert_eq!(props["max_steps"]["type"], "integer");
626    }
627
628    #[test]
629    fn test_task_params_schema_descriptions() {
630        let schema = task_params_schema();
631        let props = &schema["properties"];
632
633        assert!(props["agent"]["description"].is_string());
634        assert!(props["description"]["description"].is_string());
635        assert!(props["prompt"]["description"].is_string());
636        assert!(props["background"]["description"].is_string());
637        assert!(props["max_steps"]["description"].is_string());
638    }
639
640    #[test]
641    fn test_task_params_default_background() {
642        let params = TaskParams {
643            agent: "explore".to_string(),
644            description: "Test".to_string(),
645            prompt: "Test prompt".to_string(),
646            background: false,
647            max_steps: None,
648        };
649        assert!(!params.background);
650    }
651
652    #[test]
653    fn test_task_params_serialize_skip_none() {
654        let params = TaskParams {
655            agent: "explore".to_string(),
656            description: "Test".to_string(),
657            prompt: "Test prompt".to_string(),
658            background: false,
659            max_steps: None,
660        };
661        let json = serde_json::to_string(&params).unwrap();
662        // max_steps should not appear when None
663        assert!(!json.contains("max_steps"));
664    }
665
666    #[test]
667    fn test_task_params_serialize_with_max_steps() {
668        let params = TaskParams {
669            agent: "explore".to_string(),
670            description: "Test".to_string(),
671            prompt: "Test prompt".to_string(),
672            background: false,
673            max_steps: Some(15),
674        };
675        let json = serde_json::to_string(&params).unwrap();
676        assert!(json.contains("max_steps"));
677        assert!(json.contains("15"));
678    }
679
680    #[test]
681    fn test_task_result_success_true() {
682        let result = TaskResult {
683            output: "Success".to_string(),
684            session_id: "sess-1".to_string(),
685            agent: "explore".to_string(),
686            success: true,
687            task_id: "task-1".to_string(),
688        };
689        assert!(result.success);
690    }
691
692    #[test]
693    fn test_task_result_success_false() {
694        let result = TaskResult {
695            output: "Failed".to_string(),
696            session_id: "sess-1".to_string(),
697            agent: "explore".to_string(),
698            success: false,
699            task_id: "task-1".to_string(),
700        };
701        assert!(!result.success);
702    }
703
704    #[test]
705    fn test_task_params_empty_strings() {
706        let params = TaskParams {
707            agent: "".to_string(),
708            description: "".to_string(),
709            prompt: "".to_string(),
710            background: false,
711            max_steps: None,
712        };
713        let json = serde_json::to_string(&params).unwrap();
714        let deserialized: TaskParams = serde_json::from_str(&json).unwrap();
715        assert_eq!(deserialized.agent, "");
716        assert_eq!(deserialized.description, "");
717        assert_eq!(deserialized.prompt, "");
718    }
719
720    #[test]
721    fn test_task_result_empty_output() {
722        let result = TaskResult {
723            output: "".to_string(),
724            session_id: "sess-1".to_string(),
725            agent: "explore".to_string(),
726            success: true,
727            task_id: "task-1".to_string(),
728        };
729        assert_eq!(result.output, "");
730    }
731
732    #[test]
733    fn test_task_params_debug_format() {
734        let params = TaskParams {
735            agent: "explore".to_string(),
736            description: "Test".to_string(),
737            prompt: "Test prompt".to_string(),
738            background: false,
739            max_steps: None,
740        };
741        let debug_str = format!("{:?}", params);
742        assert!(debug_str.contains("explore"));
743        assert!(debug_str.contains("Test"));
744    }
745
746    #[test]
747    fn test_task_result_debug_format() {
748        let result = TaskResult {
749            output: "Output".to_string(),
750            session_id: "sess-1".to_string(),
751            agent: "explore".to_string(),
752            success: true,
753            task_id: "task-1".to_string(),
754        };
755        let debug_str = format!("{:?}", result);
756        assert!(debug_str.contains("Output"));
757        assert!(debug_str.contains("explore"));
758    }
759
760    #[test]
761    fn test_task_params_roundtrip() {
762        let original = TaskParams {
763            agent: "general".to_string(),
764            description: "Roundtrip test".to_string(),
765            prompt: "Test roundtrip serialization".to_string(),
766            background: true,
767            max_steps: Some(42),
768        };
769        let json = serde_json::to_string(&original).unwrap();
770        let deserialized: TaskParams = serde_json::from_str(&json).unwrap();
771        assert_eq!(original.agent, deserialized.agent);
772        assert_eq!(original.description, deserialized.description);
773        assert_eq!(original.prompt, deserialized.prompt);
774        assert_eq!(original.background, deserialized.background);
775        assert_eq!(original.max_steps, deserialized.max_steps);
776    }
777
778    #[test]
779    fn test_task_result_roundtrip() {
780        let original = TaskResult {
781            output: "Roundtrip output".to_string(),
782            session_id: "sess-roundtrip".to_string(),
783            agent: "plan".to_string(),
784            success: false,
785            task_id: "task-roundtrip".to_string(),
786        };
787        let json = serde_json::to_string(&original).unwrap();
788        let deserialized: TaskResult = serde_json::from_str(&json).unwrap();
789        assert_eq!(original.output, deserialized.output);
790        assert_eq!(original.session_id, deserialized.session_id);
791        assert_eq!(original.agent, deserialized.agent);
792        assert_eq!(original.success, deserialized.success);
793        assert_eq!(original.task_id, deserialized.task_id);
794    }
795
796    #[test]
797    fn test_parallel_task_params_deserialize() {
798        let json = r#"{
799            "tasks": [
800                { "agent": "explore", "description": "Find auth", "prompt": "Search auth files" },
801                { "agent": "general", "description": "Fix bug", "prompt": "Fix the login bug" }
802            ]
803        }"#;
804
805        let params: ParallelTaskParams = serde_json::from_str(json).unwrap();
806        assert_eq!(params.tasks.len(), 2);
807        assert_eq!(params.tasks[0].agent, "explore");
808        assert_eq!(params.tasks[1].agent, "general");
809    }
810
811    #[test]
812    fn test_parallel_task_params_single_task() {
813        let json = r#"{
814            "tasks": [
815                { "agent": "plan", "description": "Plan work", "prompt": "Create a plan" }
816            ]
817        }"#;
818
819        let params: ParallelTaskParams = serde_json::from_str(json).unwrap();
820        assert_eq!(params.tasks.len(), 1);
821    }
822
823    #[test]
824    fn test_parallel_task_params_empty_tasks() {
825        let json = r#"{ "tasks": [] }"#;
826        let params: ParallelTaskParams = serde_json::from_str(json).unwrap();
827        assert!(params.tasks.is_empty());
828    }
829
830    #[test]
831    fn test_parallel_task_params_missing_tasks() {
832        let json = r#"{}"#;
833        let result: Result<ParallelTaskParams, _> = serde_json::from_str(json);
834        assert!(result.is_err());
835    }
836
837    #[test]
838    fn test_parallel_task_params_serialize() {
839        let params = ParallelTaskParams {
840            tasks: vec![
841                TaskParams {
842                    agent: "explore".to_string(),
843                    description: "Task 1".to_string(),
844                    prompt: "Prompt 1".to_string(),
845                    background: false,
846                    max_steps: None,
847                },
848                TaskParams {
849                    agent: "general".to_string(),
850                    description: "Task 2".to_string(),
851                    prompt: "Prompt 2".to_string(),
852                    background: false,
853                    max_steps: Some(10),
854                },
855            ],
856        };
857        let json = serde_json::to_string(&params).unwrap();
858        assert!(json.contains("explore"));
859        assert!(json.contains("general"));
860        assert!(json.contains("Prompt 1"));
861        assert!(json.contains("Prompt 2"));
862    }
863
864    #[test]
865    fn test_parallel_task_params_roundtrip() {
866        let original = ParallelTaskParams {
867            tasks: vec![
868                TaskParams {
869                    agent: "explore".to_string(),
870                    description: "Explore".to_string(),
871                    prompt: "Find files".to_string(),
872                    background: false,
873                    max_steps: None,
874                },
875                TaskParams {
876                    agent: "plan".to_string(),
877                    description: "Plan".to_string(),
878                    prompt: "Make plan".to_string(),
879                    background: false,
880                    max_steps: Some(5),
881                },
882            ],
883        };
884        let json = serde_json::to_string(&original).unwrap();
885        let deserialized: ParallelTaskParams = serde_json::from_str(&json).unwrap();
886        assert_eq!(original.tasks.len(), deserialized.tasks.len());
887        assert_eq!(original.tasks[0].agent, deserialized.tasks[0].agent);
888        assert_eq!(original.tasks[1].agent, deserialized.tasks[1].agent);
889        assert_eq!(original.tasks[1].max_steps, deserialized.tasks[1].max_steps);
890    }
891
892    #[test]
893    fn test_parallel_task_params_clone() {
894        let params = ParallelTaskParams {
895            tasks: vec![TaskParams {
896                agent: "explore".to_string(),
897                description: "Test".to_string(),
898                prompt: "Prompt".to_string(),
899                background: false,
900                max_steps: None,
901            }],
902        };
903        let cloned = params.clone();
904        assert_eq!(params.tasks.len(), cloned.tasks.len());
905        assert_eq!(params.tasks[0].agent, cloned.tasks[0].agent);
906    }
907
908    #[test]
909    fn test_parallel_task_params_schema() {
910        let schema = parallel_task_params_schema();
911        assert_eq!(schema["type"], "object");
912        assert!(schema["properties"]["tasks"].is_object());
913        assert_eq!(schema["properties"]["tasks"]["type"], "array");
914        assert_eq!(schema["properties"]["tasks"]["minItems"], 1);
915    }
916
917    #[test]
918    fn test_parallel_task_params_schema_required() {
919        let schema = parallel_task_params_schema();
920        let required = schema["required"].as_array().unwrap();
921        assert!(required.contains(&serde_json::json!("tasks")));
922    }
923
924    #[test]
925    fn test_parallel_task_params_schema_items() {
926        let schema = parallel_task_params_schema();
927        let items = &schema["properties"]["tasks"]["items"];
928        assert_eq!(items["type"], "object");
929        let item_required = items["required"].as_array().unwrap();
930        assert!(item_required.contains(&serde_json::json!("agent")));
931        assert!(item_required.contains(&serde_json::json!("description")));
932        assert!(item_required.contains(&serde_json::json!("prompt")));
933    }
934
935    #[test]
936    fn test_parallel_task_params_debug() {
937        let params = ParallelTaskParams {
938            tasks: vec![TaskParams {
939                agent: "explore".to_string(),
940                description: "Debug test".to_string(),
941                prompt: "Test".to_string(),
942                background: false,
943                max_steps: None,
944            }],
945        };
946        let debug_str = format!("{:?}", params);
947        assert!(debug_str.contains("explore"));
948        assert!(debug_str.contains("Debug test"));
949    }
950}