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