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            .context("Failed to create child session")?;
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) = 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::new();
204
205        // Spawn all tasks concurrently
206        for params in tasks {
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                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            });
223        }
224
225        // Collect all results
226        let mut results = Vec::new();
227        while let Some(result) = join_set.join_next().await {
228            match result {
229                Ok(task_result) => results.push(task_result),
230                Err(e) => {
231                    tracing::error!("Parallel task panicked: {}", e);
232                    results.push(TaskResult {
233                        output: format!("Task panicked: {}", e),
234                        session_id: String::new(),
235                        agent: "unknown".to_string(),
236                        success: false,
237                        task_id: format!("task-{}", uuid::Uuid::new_v4()),
238                    });
239                }
240            }
241        }
242
243        results
244    }
245}
246
247/// Get the JSON schema for TaskParams
248pub fn task_params_schema() -> serde_json::Value {
249    serde_json::json!({
250        "type": "object",
251        "properties": {
252            "agent": {
253                "type": "string",
254                "description": "Agent type to use (explore, general, plan, etc.)"
255            },
256            "description": {
257                "type": "string",
258                "description": "Short description of the task (for display)"
259            },
260            "prompt": {
261                "type": "string",
262                "description": "Detailed prompt for the agent"
263            },
264            "background": {
265                "type": "boolean",
266                "description": "Run in background (default: false)",
267                "default": false
268            },
269            "max_steps": {
270                "type": "integer",
271                "description": "Maximum steps for this task"
272            }
273        },
274        "required": ["agent", "description", "prompt"]
275    })
276}
277
278/// TaskTool wraps TaskExecutor as a Tool for registration in ToolExecutor.
279/// This allows the LLM to delegate tasks to subagents via the standard tool interface.
280pub struct TaskTool {
281    executor: Arc<TaskExecutor>,
282}
283
284impl TaskTool {
285    /// Create a new TaskTool
286    pub fn new(executor: Arc<TaskExecutor>) -> Self {
287        Self { executor }
288    }
289}
290
291#[async_trait]
292impl Tool for TaskTool {
293    fn name(&self) -> &str {
294        "task"
295    }
296
297    fn description(&self) -> &str {
298        "Delegate a task to a specialized subagent. Available agents: explore, general, plan."
299    }
300
301    fn parameters(&self) -> serde_json::Value {
302        task_params_schema()
303    }
304
305    async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> {
306        let params: TaskParams =
307            serde_json::from_value(args.clone()).context("Invalid task parameters")?;
308
309        let session_id = ctx.session_id.as_deref().unwrap_or("unknown");
310
311        let result = self.executor.execute(session_id, params, None).await?;
312
313        if result.success {
314            Ok(ToolOutput::success(result.output))
315        } else {
316            Ok(ToolOutput::error(result.output))
317        }
318    }
319}
320
321/// Parameters for parallel task execution
322#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct ParallelTaskParams {
324    /// List of tasks to execute concurrently
325    pub tasks: Vec<TaskParams>,
326}
327
328/// Get the JSON schema for ParallelTaskParams
329pub fn parallel_task_params_schema() -> serde_json::Value {
330    serde_json::json!({
331        "type": "object",
332        "properties": {
333            "tasks": {
334                "type": "array",
335                "description": "List of tasks to execute in parallel. Each task runs as an independent subagent concurrently.",
336                "items": {
337                    "type": "object",
338                    "properties": {
339                        "agent": {
340                            "type": "string",
341                            "description": "Agent type to use (explore, general, plan, etc.)"
342                        },
343                        "description": {
344                            "type": "string",
345                            "description": "Short description of the task (for display)"
346                        },
347                        "prompt": {
348                            "type": "string",
349                            "description": "Detailed prompt for the agent"
350                        }
351                    },
352                    "required": ["agent", "description", "prompt"]
353                },
354                "minItems": 1
355            }
356        },
357        "required": ["tasks"]
358    })
359}
360
361/// ParallelTaskTool allows the LLM to fan-out multiple subagent tasks concurrently.
362///
363/// All tasks execute in parallel and the tool returns when all complete.
364pub struct ParallelTaskTool {
365    executor: Arc<TaskExecutor>,
366}
367
368impl ParallelTaskTool {
369    /// Create a new ParallelTaskTool
370    pub fn new(executor: Arc<TaskExecutor>) -> Self {
371        Self { executor }
372    }
373}
374
375#[async_trait]
376impl Tool for ParallelTaskTool {
377    fn name(&self) -> &str {
378        "parallel_task"
379    }
380
381    fn description(&self) -> &str {
382        "Execute multiple subagent tasks in parallel. All tasks run concurrently and results are returned when all complete. Use this when you need to fan-out independent work to multiple agents simultaneously."
383    }
384
385    fn parameters(&self) -> serde_json::Value {
386        parallel_task_params_schema()
387    }
388
389    async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> {
390        let params: ParallelTaskParams =
391            serde_json::from_value(args.clone()).context("Invalid parallel task parameters")?;
392
393        if params.tasks.is_empty() {
394            return Ok(ToolOutput::error("No tasks provided".to_string()));
395        }
396
397        let session_id = ctx.session_id.as_deref().unwrap_or("unknown");
398        let task_count = params.tasks.len();
399
400        let results = self
401            .executor
402            .execute_parallel(session_id, params.tasks, None)
403            .await;
404
405        // Format results
406        let mut output = format!("Executed {} tasks in parallel:\n\n", task_count);
407        for (i, result) in results.iter().enumerate() {
408            let status = if result.success { "[OK]" } else { "[ERR]" };
409            output.push_str(&format!(
410                "--- Task {} ({}) {} ---\n{}\n\n",
411                i + 1,
412                result.agent,
413                status,
414                result.output
415            ));
416        }
417
418        Ok(ToolOutput::success(output))
419    }
420}
421
422#[cfg(test)]
423mod tests {
424    use super::*;
425
426    #[test]
427    fn test_task_params_deserialize() {
428        let json = r#"{
429            "agent": "explore",
430            "description": "Find auth code",
431            "prompt": "Search for authentication files"
432        }"#;
433
434        let params: TaskParams = serde_json::from_str(json).unwrap();
435        assert_eq!(params.agent, "explore");
436        assert_eq!(params.description, "Find auth code");
437        assert!(!params.background);
438    }
439
440    #[test]
441    fn test_task_params_with_background() {
442        let json = r#"{
443            "agent": "general",
444            "description": "Long task",
445            "prompt": "Do something complex",
446            "background": true
447        }"#;
448
449        let params: TaskParams = serde_json::from_str(json).unwrap();
450        assert!(params.background);
451    }
452
453    #[test]
454    fn test_task_params_with_max_steps() {
455        let json = r#"{
456            "agent": "plan",
457            "description": "Planning task",
458            "prompt": "Create a plan",
459            "max_steps": 10
460        }"#;
461
462        let params: TaskParams = serde_json::from_str(json).unwrap();
463        assert_eq!(params.agent, "plan");
464        assert_eq!(params.max_steps, Some(10));
465        assert!(!params.background);
466    }
467
468    #[test]
469    fn test_task_params_all_fields() {
470        let json = r#"{
471            "agent": "general",
472            "description": "Complex task",
473            "prompt": "Do everything",
474            "background": true,
475            "max_steps": 20
476        }"#;
477
478        let params: TaskParams = serde_json::from_str(json).unwrap();
479        assert_eq!(params.agent, "general");
480        assert_eq!(params.description, "Complex task");
481        assert_eq!(params.prompt, "Do everything");
482        assert!(params.background);
483        assert_eq!(params.max_steps, Some(20));
484    }
485
486    #[test]
487    fn test_task_params_missing_required_field() {
488        let json = r#"{
489            "agent": "explore",
490            "description": "Missing prompt"
491        }"#;
492
493        let result: Result<TaskParams, _> = serde_json::from_str(json);
494        assert!(result.is_err());
495    }
496
497    #[test]
498    fn test_task_params_serialize() {
499        let params = TaskParams {
500            agent: "explore".to_string(),
501            description: "Test task".to_string(),
502            prompt: "Test prompt".to_string(),
503            background: false,
504            max_steps: Some(5),
505        };
506
507        let json = serde_json::to_string(&params).unwrap();
508        assert!(json.contains("explore"));
509        assert!(json.contains("Test task"));
510        assert!(json.contains("Test prompt"));
511    }
512
513    #[test]
514    fn test_task_params_clone() {
515        let params = TaskParams {
516            agent: "explore".to_string(),
517            description: "Test".to_string(),
518            prompt: "Prompt".to_string(),
519            background: true,
520            max_steps: None,
521        };
522
523        let cloned = params.clone();
524        assert_eq!(params.agent, cloned.agent);
525        assert_eq!(params.description, cloned.description);
526        assert_eq!(params.background, cloned.background);
527    }
528
529    #[test]
530    fn test_task_result_serialize() {
531        let result = TaskResult {
532            output: "Found 5 files".to_string(),
533            session_id: "session-123".to_string(),
534            agent: "explore".to_string(),
535            success: true,
536            task_id: "task-456".to_string(),
537        };
538
539        let json = serde_json::to_string(&result).unwrap();
540        assert!(json.contains("Found 5 files"));
541        assert!(json.contains("explore"));
542    }
543
544    #[test]
545    fn test_task_result_deserialize() {
546        let json = r#"{
547            "output": "Task completed",
548            "session_id": "sess-789",
549            "agent": "general",
550            "success": false,
551            "task_id": "task-123"
552        }"#;
553
554        let result: TaskResult = serde_json::from_str(json).unwrap();
555        assert_eq!(result.output, "Task completed");
556        assert_eq!(result.session_id, "sess-789");
557        assert_eq!(result.agent, "general");
558        assert!(!result.success);
559        assert_eq!(result.task_id, "task-123");
560    }
561
562    #[test]
563    fn test_task_result_clone() {
564        let result = TaskResult {
565            output: "Output".to_string(),
566            session_id: "session-1".to_string(),
567            agent: "explore".to_string(),
568            success: true,
569            task_id: "task-1".to_string(),
570        };
571
572        let cloned = result.clone();
573        assert_eq!(result.output, cloned.output);
574        assert_eq!(result.success, cloned.success);
575    }
576
577    #[test]
578    fn test_task_params_schema() {
579        let schema = task_params_schema();
580        assert_eq!(schema["type"], "object");
581        assert!(schema["properties"]["agent"].is_object());
582        assert!(schema["properties"]["prompt"].is_object());
583    }
584
585    #[test]
586    fn test_task_params_schema_required_fields() {
587        let schema = task_params_schema();
588        let required = schema["required"].as_array().unwrap();
589        assert!(required.contains(&serde_json::json!("agent")));
590        assert!(required.contains(&serde_json::json!("description")));
591        assert!(required.contains(&serde_json::json!("prompt")));
592    }
593
594    #[test]
595    fn test_task_params_schema_properties() {
596        let schema = task_params_schema();
597        let props = &schema["properties"];
598
599        assert_eq!(props["agent"]["type"], "string");
600        assert_eq!(props["description"]["type"], "string");
601        assert_eq!(props["prompt"]["type"], "string");
602        assert_eq!(props["background"]["type"], "boolean");
603        assert_eq!(props["background"]["default"], false);
604        assert_eq!(props["max_steps"]["type"], "integer");
605    }
606
607    #[test]
608    fn test_task_params_schema_descriptions() {
609        let schema = task_params_schema();
610        let props = &schema["properties"];
611
612        assert!(props["agent"]["description"].is_string());
613        assert!(props["description"]["description"].is_string());
614        assert!(props["prompt"]["description"].is_string());
615        assert!(props["background"]["description"].is_string());
616        assert!(props["max_steps"]["description"].is_string());
617    }
618
619    #[test]
620    fn test_task_params_default_background() {
621        let params = TaskParams {
622            agent: "explore".to_string(),
623            description: "Test".to_string(),
624            prompt: "Test prompt".to_string(),
625            background: false,
626            max_steps: None,
627        };
628        assert!(!params.background);
629    }
630
631    #[test]
632    fn test_task_params_serialize_skip_none() {
633        let params = TaskParams {
634            agent: "explore".to_string(),
635            description: "Test".to_string(),
636            prompt: "Test prompt".to_string(),
637            background: false,
638            max_steps: None,
639        };
640        let json = serde_json::to_string(&params).unwrap();
641        // max_steps should not appear when None
642        assert!(!json.contains("max_steps"));
643    }
644
645    #[test]
646    fn test_task_params_serialize_with_max_steps() {
647        let params = TaskParams {
648            agent: "explore".to_string(),
649            description: "Test".to_string(),
650            prompt: "Test prompt".to_string(),
651            background: false,
652            max_steps: Some(15),
653        };
654        let json = serde_json::to_string(&params).unwrap();
655        assert!(json.contains("max_steps"));
656        assert!(json.contains("15"));
657    }
658
659    #[test]
660    fn test_task_result_success_true() {
661        let result = TaskResult {
662            output: "Success".to_string(),
663            session_id: "sess-1".to_string(),
664            agent: "explore".to_string(),
665            success: true,
666            task_id: "task-1".to_string(),
667        };
668        assert!(result.success);
669    }
670
671    #[test]
672    fn test_task_result_success_false() {
673        let result = TaskResult {
674            output: "Failed".to_string(),
675            session_id: "sess-1".to_string(),
676            agent: "explore".to_string(),
677            success: false,
678            task_id: "task-1".to_string(),
679        };
680        assert!(!result.success);
681    }
682
683    #[test]
684    fn test_task_params_empty_strings() {
685        let params = TaskParams {
686            agent: "".to_string(),
687            description: "".to_string(),
688            prompt: "".to_string(),
689            background: false,
690            max_steps: None,
691        };
692        let json = serde_json::to_string(&params).unwrap();
693        let deserialized: TaskParams = serde_json::from_str(&json).unwrap();
694        assert_eq!(deserialized.agent, "");
695        assert_eq!(deserialized.description, "");
696        assert_eq!(deserialized.prompt, "");
697    }
698
699    #[test]
700    fn test_task_result_empty_output() {
701        let result = TaskResult {
702            output: "".to_string(),
703            session_id: "sess-1".to_string(),
704            agent: "explore".to_string(),
705            success: true,
706            task_id: "task-1".to_string(),
707        };
708        assert_eq!(result.output, "");
709    }
710
711    #[test]
712    fn test_task_params_debug_format() {
713        let params = TaskParams {
714            agent: "explore".to_string(),
715            description: "Test".to_string(),
716            prompt: "Test prompt".to_string(),
717            background: false,
718            max_steps: None,
719        };
720        let debug_str = format!("{:?}", params);
721        assert!(debug_str.contains("explore"));
722        assert!(debug_str.contains("Test"));
723    }
724
725    #[test]
726    fn test_task_result_debug_format() {
727        let result = TaskResult {
728            output: "Output".to_string(),
729            session_id: "sess-1".to_string(),
730            agent: "explore".to_string(),
731            success: true,
732            task_id: "task-1".to_string(),
733        };
734        let debug_str = format!("{:?}", result);
735        assert!(debug_str.contains("Output"));
736        assert!(debug_str.contains("explore"));
737    }
738
739    #[test]
740    fn test_task_params_roundtrip() {
741        let original = TaskParams {
742            agent: "general".to_string(),
743            description: "Roundtrip test".to_string(),
744            prompt: "Test roundtrip serialization".to_string(),
745            background: true,
746            max_steps: Some(42),
747        };
748        let json = serde_json::to_string(&original).unwrap();
749        let deserialized: TaskParams = serde_json::from_str(&json).unwrap();
750        assert_eq!(original.agent, deserialized.agent);
751        assert_eq!(original.description, deserialized.description);
752        assert_eq!(original.prompt, deserialized.prompt);
753        assert_eq!(original.background, deserialized.background);
754        assert_eq!(original.max_steps, deserialized.max_steps);
755    }
756
757    #[test]
758    fn test_task_result_roundtrip() {
759        let original = TaskResult {
760            output: "Roundtrip output".to_string(),
761            session_id: "sess-roundtrip".to_string(),
762            agent: "plan".to_string(),
763            success: false,
764            task_id: "task-roundtrip".to_string(),
765        };
766        let json = serde_json::to_string(&original).unwrap();
767        let deserialized: TaskResult = serde_json::from_str(&json).unwrap();
768        assert_eq!(original.output, deserialized.output);
769        assert_eq!(original.session_id, deserialized.session_id);
770        assert_eq!(original.agent, deserialized.agent);
771        assert_eq!(original.success, deserialized.success);
772        assert_eq!(original.task_id, deserialized.task_id);
773    }
774
775    #[test]
776    fn test_parallel_task_params_deserialize() {
777        let json = r#"{
778            "tasks": [
779                { "agent": "explore", "description": "Find auth", "prompt": "Search auth files" },
780                { "agent": "general", "description": "Fix bug", "prompt": "Fix the login bug" }
781            ]
782        }"#;
783
784        let params: ParallelTaskParams = serde_json::from_str(json).unwrap();
785        assert_eq!(params.tasks.len(), 2);
786        assert_eq!(params.tasks[0].agent, "explore");
787        assert_eq!(params.tasks[1].agent, "general");
788    }
789
790    #[test]
791    fn test_parallel_task_params_single_task() {
792        let json = r#"{
793            "tasks": [
794                { "agent": "plan", "description": "Plan work", "prompt": "Create a plan" }
795            ]
796        }"#;
797
798        let params: ParallelTaskParams = serde_json::from_str(json).unwrap();
799        assert_eq!(params.tasks.len(), 1);
800    }
801
802    #[test]
803    fn test_parallel_task_params_empty_tasks() {
804        let json = r#"{ "tasks": [] }"#;
805        let params: ParallelTaskParams = serde_json::from_str(json).unwrap();
806        assert!(params.tasks.is_empty());
807    }
808
809    #[test]
810    fn test_parallel_task_params_missing_tasks() {
811        let json = r#"{}"#;
812        let result: Result<ParallelTaskParams, _> = serde_json::from_str(json);
813        assert!(result.is_err());
814    }
815
816    #[test]
817    fn test_parallel_task_params_serialize() {
818        let params = ParallelTaskParams {
819            tasks: vec![
820                TaskParams {
821                    agent: "explore".to_string(),
822                    description: "Task 1".to_string(),
823                    prompt: "Prompt 1".to_string(),
824                    background: false,
825                    max_steps: None,
826                },
827                TaskParams {
828                    agent: "general".to_string(),
829                    description: "Task 2".to_string(),
830                    prompt: "Prompt 2".to_string(),
831                    background: false,
832                    max_steps: Some(10),
833                },
834            ],
835        };
836        let json = serde_json::to_string(&params).unwrap();
837        assert!(json.contains("explore"));
838        assert!(json.contains("general"));
839        assert!(json.contains("Prompt 1"));
840        assert!(json.contains("Prompt 2"));
841    }
842
843    #[test]
844    fn test_parallel_task_params_roundtrip() {
845        let original = ParallelTaskParams {
846            tasks: vec![
847                TaskParams {
848                    agent: "explore".to_string(),
849                    description: "Explore".to_string(),
850                    prompt: "Find files".to_string(),
851                    background: false,
852                    max_steps: None,
853                },
854                TaskParams {
855                    agent: "plan".to_string(),
856                    description: "Plan".to_string(),
857                    prompt: "Make plan".to_string(),
858                    background: false,
859                    max_steps: Some(5),
860                },
861            ],
862        };
863        let json = serde_json::to_string(&original).unwrap();
864        let deserialized: ParallelTaskParams = serde_json::from_str(&json).unwrap();
865        assert_eq!(original.tasks.len(), deserialized.tasks.len());
866        assert_eq!(original.tasks[0].agent, deserialized.tasks[0].agent);
867        assert_eq!(original.tasks[1].agent, deserialized.tasks[1].agent);
868        assert_eq!(original.tasks[1].max_steps, deserialized.tasks[1].max_steps);
869    }
870
871    #[test]
872    fn test_parallel_task_params_clone() {
873        let params = ParallelTaskParams {
874            tasks: vec![TaskParams {
875                agent: "explore".to_string(),
876                description: "Test".to_string(),
877                prompt: "Prompt".to_string(),
878                background: false,
879                max_steps: None,
880            }],
881        };
882        let cloned = params.clone();
883        assert_eq!(params.tasks.len(), cloned.tasks.len());
884        assert_eq!(params.tasks[0].agent, cloned.tasks[0].agent);
885    }
886
887    #[test]
888    fn test_parallel_task_params_schema() {
889        let schema = parallel_task_params_schema();
890        assert_eq!(schema["type"], "object");
891        assert!(schema["properties"]["tasks"].is_object());
892        assert_eq!(schema["properties"]["tasks"]["type"], "array");
893        assert_eq!(schema["properties"]["tasks"]["minItems"], 1);
894    }
895
896    #[test]
897    fn test_parallel_task_params_schema_required() {
898        let schema = parallel_task_params_schema();
899        let required = schema["required"].as_array().unwrap();
900        assert!(required.contains(&serde_json::json!("tasks")));
901    }
902
903    #[test]
904    fn test_parallel_task_params_schema_items() {
905        let schema = parallel_task_params_schema();
906        let items = &schema["properties"]["tasks"]["items"];
907        assert_eq!(items["type"], "object");
908        let item_required = items["required"].as_array().unwrap();
909        assert!(item_required.contains(&serde_json::json!("agent")));
910        assert!(item_required.contains(&serde_json::json!("description")));
911        assert!(item_required.contains(&serde_json::json!("prompt")));
912    }
913
914    #[test]
915    fn test_parallel_task_params_debug() {
916        let params = ParallelTaskParams {
917            tasks: vec![TaskParams {
918                agent: "explore".to_string(),
919                description: "Debug test".to_string(),
920                prompt: "Test".to_string(),
921                background: false,
922                max_steps: None,
923            }],
924        };
925        let debug_str = format!("{:?}", params);
926        assert!(debug_str.contains("explore"));
927        assert!(debug_str.contains("Debug test"));
928    }
929}