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