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