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
30const TASK_OUTPUT_CONTEXT_LIMIT: usize = 4_000;
31const TASK_OUTPUT_CONTEXT_HEAD: usize = 3_000;
32const TASK_OUTPUT_CONTEXT_TAIL: usize = 800;
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36#[serde(deny_unknown_fields)]
37pub struct TaskParams {
38 pub agent: String,
40 pub description: String,
42 pub prompt: String,
44 #[serde(default)]
46 pub background: bool,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub max_steps: Option<usize>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct TaskResult {
55 pub output: String,
57 pub session_id: String,
59 pub agent: String,
61 pub success: bool,
63 pub task_id: String,
65}
66
67fn compact_task_output(output: &str) -> (String, bool) {
68 if output.len() <= TASK_OUTPUT_CONTEXT_LIMIT {
69 return (output.to_string(), false);
70 }
71
72 let head = crate::text::truncate_utf8(output, TASK_OUTPUT_CONTEXT_HEAD);
73 let tail_start = output
74 .char_indices()
75 .find_map(|(idx, _)| {
76 if output.len().saturating_sub(idx) <= TASK_OUTPUT_CONTEXT_TAIL {
77 Some(idx)
78 } else {
79 None
80 }
81 })
82 .unwrap_or(output.len());
83 let tail = &output[tail_start..];
84
85 (
86 format!(
87 "{}\n\n[{} bytes omitted from subagent output]\n\n{}",
88 head,
89 output.len().saturating_sub(head.len() + tail.len()),
90 tail
91 ),
92 true,
93 )
94}
95
96fn task_artifact_id(result: &TaskResult) -> String {
97 format!("subagent-output:{}", result.task_id)
98}
99
100fn task_artifact_uri(result: &TaskResult) -> String {
101 format!(
102 "a3s://subagent/{}/tasks/{}/output",
103 result.session_id, result.task_id
104 )
105}
106
107fn format_task_result_for_context(result: &TaskResult) -> (String, bool) {
108 let (output, truncated) = compact_task_output(&result.output);
109 let status = if result.success {
110 "completed"
111 } else {
112 "failed"
113 };
114 let artifact_id = task_artifact_id(result);
115 let artifact_uri = task_artifact_uri(result);
116 let mut formatted = format!(
117 "Task {status}: {}\nAgent: {}\nSession: {}\nTask ID: {}\nArtifact ID: {}\nArtifact URI: {}\n",
118 result.task_id, result.agent, result.session_id, result.task_id, artifact_id, artifact_uri
119 );
120 if truncated {
121 formatted.push_str(
122 "Output excerpt: truncated for parent context. Use the artifact URI or subagent session/events if exact omitted content is needed.\n",
123 );
124 } else {
125 formatted.push_str("Output:\n");
126 }
127 formatted.push_str(&output);
128 (formatted, truncated)
129}
130
131pub struct TaskExecutor {
133 registry: Arc<AgentRegistry>,
135 llm_client: Arc<dyn LlmClient>,
137 workspace: String,
139 mcp_manager: Option<Arc<McpManager>>,
141}
142
143impl TaskExecutor {
144 pub fn new(
146 registry: Arc<AgentRegistry>,
147 llm_client: Arc<dyn LlmClient>,
148 workspace: String,
149 ) -> Self {
150 Self {
151 registry,
152 llm_client,
153 workspace,
154 mcp_manager: None,
155 }
156 }
157
158 pub fn with_mcp(
160 registry: Arc<AgentRegistry>,
161 llm_client: Arc<dyn LlmClient>,
162 workspace: String,
163 mcp_manager: Arc<McpManager>,
164 ) -> Self {
165 Self {
166 registry,
167 llm_client,
168 workspace,
169 mcp_manager: Some(mcp_manager),
170 }
171 }
172
173 pub async fn execute(
175 &self,
176 params: TaskParams,
177 event_tx: Option<broadcast::Sender<AgentEvent>>,
178 ) -> Result<TaskResult> {
179 let task_id = format!("task-{}", uuid::Uuid::new_v4());
180 let session_id = format!("subagent-{}", task_id);
181
182 let agent = self
183 .registry
184 .get(¶ms.agent)
185 .context(format!("Unknown agent type: '{}'", params.agent))?;
186
187 if let Some(ref tx) = event_tx {
188 let _ = tx.send(AgentEvent::SubagentStart {
189 task_id: task_id.clone(),
190 session_id: session_id.clone(),
191 parent_session_id: String::new(),
192 agent: params.agent.clone(),
193 description: params.description.clone(),
194 });
195 }
196
197 let mut child_executor = crate::tools::ToolExecutor::new(self.workspace.clone());
200
201 if let Some(ref mcp) = self.mcp_manager {
203 let all_tools = mcp.get_all_tools().await;
204 let mut by_server: std::collections::HashMap<
205 String,
206 Vec<crate::mcp::protocol::McpTool>,
207 > = std::collections::HashMap::new();
208 for (server, tool) in all_tools {
209 by_server.entry(server).or_default().push(tool);
210 }
211 for (server_name, tools) in by_server {
212 let wrappers =
213 crate::mcp::tools::create_mcp_tools(&server_name, tools, Arc::clone(mcp));
214 for wrapper in wrappers {
215 child_executor.register_dynamic_tool(wrapper);
216 }
217 }
218 }
219
220 if !agent.permissions.allow.is_empty() || !agent.permissions.deny.is_empty() {
221 child_executor.set_guard_policy(Arc::new(agent.permissions.clone())
222 as Arc<dyn crate::permissions::PermissionChecker>);
223 }
224 let child_executor = Arc::new(child_executor);
225
226 let mut prompt_slots = crate::prompts::SystemPromptSlots::default();
228 if let Some(ref p) = agent.prompt {
229 prompt_slots.extra = Some(p.clone());
230 }
231
232 let child_config = AgentConfig {
233 prompt_slots,
234 tools: child_executor.definitions(),
235 max_tool_rounds: params
236 .max_steps
237 .unwrap_or_else(|| agent.max_steps.unwrap_or(20)),
238 ..AgentConfig::default()
239 };
240
241 let tool_context =
242 ToolContext::new(PathBuf::from(&self.workspace)).with_session_id(session_id.clone());
243
244 let agent_loop = AgentLoop::new(
245 Arc::clone(&self.llm_client),
246 child_executor,
247 tool_context,
248 child_config,
249 );
250
251 let child_event_tx = if let Some(ref broadcast_tx) = event_tx {
253 let (mpsc_tx, mut mpsc_rx) = tokio::sync::mpsc::channel(100);
254 let broadcast_tx_clone = broadcast_tx.clone();
255
256 tokio::spawn(async move {
258 while let Some(event) = mpsc_rx.recv().await {
259 let _ = broadcast_tx_clone.send(event);
260 }
261 });
262
263 Some(mpsc_tx)
264 } else {
265 None
266 };
267
268 let (output, success) = match agent_loop
269 .execute(&[], ¶ms.prompt, child_event_tx)
270 .await
271 {
272 Ok(result) => (result.text, true),
273 Err(e) => (format!("Task failed: {}", e), false),
274 };
275
276 if let Some(ref tx) = event_tx {
277 let _ = tx.send(AgentEvent::SubagentEnd {
278 task_id: task_id.clone(),
279 session_id: session_id.clone(),
280 agent: params.agent.clone(),
281 output: output.clone(),
282 success,
283 });
284 }
285
286 Ok(TaskResult {
287 output,
288 session_id,
289 agent: params.agent,
290 success,
291 task_id,
292 })
293 }
294
295 pub fn execute_background(
299 self: Arc<Self>,
300 params: TaskParams,
301 event_tx: Option<broadcast::Sender<AgentEvent>>,
302 ) -> String {
303 let task_id = format!("task-{}", uuid::Uuid::new_v4());
304 let task_id_clone = task_id.clone();
305
306 tokio::spawn(async move {
307 if let Err(e) = self.execute(params, event_tx).await {
308 tracing::error!("Background task {} failed: {}", task_id_clone, e);
309 }
310 });
311
312 task_id
313 }
314
315 pub async fn execute_parallel(
320 self: &Arc<Self>,
321 tasks: Vec<TaskParams>,
322 event_tx: Option<broadcast::Sender<AgentEvent>>,
323 ) -> Vec<TaskResult> {
324 let mut join_set: JoinSet<(usize, TaskResult)> = JoinSet::new();
325
326 for (idx, params) in tasks.into_iter().enumerate() {
327 let executor = Arc::clone(self);
328 let tx = event_tx.clone();
329
330 join_set.spawn(async move {
331 let result = match executor.execute(params.clone(), tx).await {
332 Ok(result) => result,
333 Err(e) => TaskResult {
334 output: format!("Task failed: {}", e),
335 session_id: String::new(),
336 agent: params.agent,
337 success: false,
338 task_id: format!("task-{}", uuid::Uuid::new_v4()),
339 },
340 };
341 (idx, result)
342 });
343 }
344
345 let mut indexed_results = Vec::new();
346 while let Some(result) = join_set.join_next().await {
347 match result {
348 Ok((idx, task_result)) => indexed_results.push((idx, task_result)),
349 Err(e) => {
350 tracing::error!("Parallel task panicked: {}", e);
351 indexed_results.push((
352 usize::MAX,
353 TaskResult {
354 output: format!("Task panicked: {}", e),
355 session_id: String::new(),
356 agent: "unknown".to_string(),
357 success: false,
358 task_id: format!("task-{}", uuid::Uuid::new_v4()),
359 },
360 ));
361 }
362 }
363 }
364
365 indexed_results.sort_by_key(|(idx, _)| *idx);
366 indexed_results.into_iter().map(|(_, r)| r).collect()
367 }
368}
369
370pub fn task_params_schema() -> serde_json::Value {
372 serde_json::json!({
373 "type": "object",
374 "additionalProperties": false,
375 "properties": {
376 "agent": {
377 "type": "string",
378 "description": "Required. Canonical agent type to use (for example: explore, general, plan, verification, review). Always provide this exact field name: 'agent'."
379 },
380 "description": {
381 "type": "string",
382 "description": "Required. Short task label for display and tracking. Always provide this exact field name: 'description'."
383 },
384 "prompt": {
385 "type": "string",
386 "description": "Required. Detailed instruction for the delegated subagent. Always provide this exact field name: 'prompt'."
387 },
388 "background": {
389 "type": "boolean",
390 "description": "Optional. Run the task in the background. Default: false.",
391 "default": false
392 },
393 "max_steps": {
394 "type": "integer",
395 "description": "Optional. Maximum number of steps for this task."
396 }
397 },
398 "required": ["agent", "description", "prompt"],
399 "examples": [
400 {
401 "agent": "explore",
402 "description": "Find Rust files",
403 "prompt": "Search the workspace for Rust files and summarize the layout."
404 },
405 {
406 "agent": "general",
407 "description": "Investigate test failure",
408 "prompt": "Inspect the failing tests and explain the root cause.",
409 "max_steps": 6
410 }
411 ]
412 })
413}
414
415pub struct TaskTool {
418 executor: Arc<TaskExecutor>,
419}
420
421impl TaskTool {
422 pub fn new(executor: Arc<TaskExecutor>) -> Self {
424 Self { executor }
425 }
426}
427
428#[async_trait]
429impl Tool for TaskTool {
430 fn name(&self) -> &str {
431 "task"
432 }
433
434 fn description(&self) -> &str {
435 "Delegate a task to a specialized subagent. Built-in agents: explore (read-only codebase search), general (full access multi-step), plan (read-only planning), verification (adversarial validation), review (code review). Custom agents from agent_dirs are also available."
436 }
437
438 fn parameters(&self) -> serde_json::Value {
439 task_params_schema()
440 }
441
442 async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> {
443 let params: TaskParams =
444 serde_json::from_value(args.clone()).context("Invalid task parameters")?;
445
446 if params.background {
447 let task_id =
448 Arc::clone(&self.executor).execute_background(params, ctx.agent_event_tx.clone());
449 return Ok(ToolOutput::success(format!(
450 "Task started in background. Task ID: {}",
451 task_id
452 )));
453 }
454
455 let result = self
456 .executor
457 .execute(params, ctx.agent_event_tx.clone())
458 .await?;
459 let (content, truncated) = format_task_result_for_context(&result);
460 let metadata = serde_json::json!({
461 "task_id": result.task_id,
462 "session_id": result.session_id,
463 "agent": result.agent,
464 "success": result.success,
465 "output_bytes": result.output.len(),
466 "truncated_for_context": truncated,
467 "artifact_id": task_artifact_id(&result),
468 "artifact_uri": task_artifact_uri(&result),
469 });
470
471 if result.success {
472 Ok(ToolOutput::success(content).with_metadata(metadata))
473 } else {
474 Ok(ToolOutput::error(content).with_metadata(metadata))
475 }
476 }
477}
478
479#[derive(Debug, Clone, Serialize, Deserialize)]
481#[serde(deny_unknown_fields)]
482pub struct ParallelTaskParams {
483 pub tasks: Vec<TaskParams>,
485}
486
487pub fn parallel_task_params_schema() -> serde_json::Value {
489 serde_json::json!({
490 "type": "object",
491 "additionalProperties": false,
492 "properties": {
493 "tasks": {
494 "type": "array",
495 "description": "List of tasks to execute in parallel. Each task runs as an independent subagent concurrently.",
496 "items": {
497 "type": "object",
498 "additionalProperties": false,
499 "properties": {
500 "agent": {
501 "type": "string",
502 "description": "Required. Canonical agent type for this task."
503 },
504 "description": {
505 "type": "string",
506 "description": "Required. Short task label for display and tracking."
507 },
508 "prompt": {
509 "type": "string",
510 "description": "Required. Detailed instruction for the delegated subagent."
511 }
512 },
513 "required": ["agent", "description", "prompt"]
514 },
515 "minItems": 1
516 }
517 },
518 "required": ["tasks"],
519 "examples": [
520 {
521 "tasks": [
522 {
523 "agent": "explore",
524 "description": "Find Rust files",
525 "prompt": "List Rust files under src/."
526 },
527 {
528 "agent": "explore",
529 "description": "Find tests",
530 "prompt": "List test files and summarize their purpose."
531 }
532 ]
533 }
534 ]
535 })
536}
537
538pub struct ParallelTaskTool {
542 executor: Arc<TaskExecutor>,
543}
544
545impl ParallelTaskTool {
546 pub fn new(executor: Arc<TaskExecutor>) -> Self {
548 Self { executor }
549 }
550}
551
552#[async_trait]
553impl Tool for ParallelTaskTool {
554 fn name(&self) -> &str {
555 "parallel_task"
556 }
557
558 fn description(&self) -> &str {
559 "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), verification (adversarial validation), review (code review). Custom agents from agent_dirs are also available."
560 }
561
562 fn parameters(&self) -> serde_json::Value {
563 parallel_task_params_schema()
564 }
565
566 async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> {
567 let params: ParallelTaskParams =
568 serde_json::from_value(args.clone()).context("Invalid parallel task parameters")?;
569
570 if params.tasks.is_empty() {
571 return Ok(ToolOutput::error("No tasks provided".to_string()));
572 }
573
574 let task_count = params.tasks.len();
575
576 let results = self
577 .executor
578 .execute_parallel(params.tasks, ctx.agent_event_tx.clone())
579 .await;
580
581 let mut output = format!("Executed {} tasks in parallel:\n\n", task_count);
583 let mut metadata_results = Vec::new();
584 for (i, result) in results.iter().enumerate() {
585 let status = if result.success { "[OK]" } else { "[ERR]" };
586 let (formatted, truncated) = format_task_result_for_context(result);
587 metadata_results.push(serde_json::json!({
588 "task_id": result.task_id,
589 "session_id": result.session_id,
590 "agent": result.agent,
591 "success": result.success,
592 "output_bytes": result.output.len(),
593 "truncated_for_context": truncated,
594 "artifact_id": task_artifact_id(result),
595 "artifact_uri": task_artifact_uri(result),
596 }));
597 output.push_str(&format!(
598 "--- Task {} ({}) {} ---\n{}\n\n",
599 i + 1,
600 result.agent,
601 status,
602 formatted
603 ));
604 }
605
606 Ok(
607 ToolOutput::success(output).with_metadata(serde_json::json!({
608 "task_count": task_count,
609 "results": metadata_results,
610 })),
611 )
612 }
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618
619 #[test]
620 fn test_task_params_deserialize() {
621 let json = r#"{
622 "agent": "explore",
623 "description": "Find auth code",
624 "prompt": "Search for authentication files"
625 }"#;
626
627 let params: TaskParams = serde_json::from_str(json).unwrap();
628 assert_eq!(params.agent, "explore");
629 assert_eq!(params.description, "Find auth code");
630 assert!(!params.background);
631 }
632
633 #[test]
634 fn test_task_params_with_background() {
635 let json = r#"{
636 "agent": "general",
637 "description": "Long task",
638 "prompt": "Do something complex",
639 "background": true
640 }"#;
641
642 let params: TaskParams = serde_json::from_str(json).unwrap();
643 assert!(params.background);
644 }
645
646 #[test]
647 fn test_task_params_with_max_steps() {
648 let json = r#"{
649 "agent": "plan",
650 "description": "Planning task",
651 "prompt": "Create a plan",
652 "max_steps": 10
653 }"#;
654
655 let params: TaskParams = serde_json::from_str(json).unwrap();
656 assert_eq!(params.agent, "plan");
657 assert_eq!(params.max_steps, Some(10));
658 assert!(!params.background);
659 }
660
661 #[test]
662 fn test_task_params_all_fields() {
663 let json = r#"{
664 "agent": "general",
665 "description": "Complex task",
666 "prompt": "Do everything",
667 "background": true,
668 "max_steps": 20
669 }"#;
670
671 let params: TaskParams = serde_json::from_str(json).unwrap();
672 assert_eq!(params.agent, "general");
673 assert_eq!(params.description, "Complex task");
674 assert_eq!(params.prompt, "Do everything");
675 assert!(params.background);
676 assert_eq!(params.max_steps, Some(20));
677 }
678
679 #[test]
680 fn test_task_params_missing_required_field() {
681 let json = r#"{
682 "agent": "explore",
683 "description": "Missing prompt"
684 }"#;
685
686 let result: Result<TaskParams, _> = serde_json::from_str(json);
687 assert!(result.is_err());
688 }
689
690 #[test]
691 fn test_task_params_serialize() {
692 let params = TaskParams {
693 agent: "explore".to_string(),
694 description: "Test task".to_string(),
695 prompt: "Test prompt".to_string(),
696 background: false,
697 max_steps: Some(5),
698 };
699
700 let json = serde_json::to_string(¶ms).unwrap();
701 assert!(json.contains("explore"));
702 assert!(json.contains("Test task"));
703 assert!(json.contains("Test prompt"));
704 }
705
706 #[test]
707 fn test_task_params_clone() {
708 let params = TaskParams {
709 agent: "explore".to_string(),
710 description: "Test".to_string(),
711 prompt: "Prompt".to_string(),
712 background: true,
713 max_steps: None,
714 };
715
716 let cloned = params.clone();
717 assert_eq!(params.agent, cloned.agent);
718 assert_eq!(params.description, cloned.description);
719 assert_eq!(params.background, cloned.background);
720 }
721
722 #[test]
723 fn test_task_result_serialize() {
724 let result = TaskResult {
725 output: "Found 5 files".to_string(),
726 session_id: "session-123".to_string(),
727 agent: "explore".to_string(),
728 success: true,
729 task_id: "task-456".to_string(),
730 };
731
732 let json = serde_json::to_string(&result).unwrap();
733 assert!(json.contains("Found 5 files"));
734 assert!(json.contains("explore"));
735 }
736
737 #[test]
738 fn test_task_result_deserialize() {
739 let json = r#"{
740 "output": "Task completed",
741 "session_id": "sess-789",
742 "agent": "general",
743 "success": false,
744 "task_id": "task-123"
745 }"#;
746
747 let result: TaskResult = serde_json::from_str(json).unwrap();
748 assert_eq!(result.output, "Task completed");
749 assert_eq!(result.session_id, "sess-789");
750 assert_eq!(result.agent, "general");
751 assert!(!result.success);
752 assert_eq!(result.task_id, "task-123");
753 }
754
755 #[test]
756 fn test_task_result_clone() {
757 let result = TaskResult {
758 output: "Output".to_string(),
759 session_id: "session-1".to_string(),
760 agent: "explore".to_string(),
761 success: true,
762 task_id: "task-1".to_string(),
763 };
764
765 let cloned = result.clone();
766 assert_eq!(result.output, cloned.output);
767 assert_eq!(result.success, cloned.success);
768 }
769
770 #[test]
771 fn test_compact_task_output_preserves_small_output() {
772 let (output, truncated) = compact_task_output("short result");
773 assert_eq!(output, "short result");
774 assert!(!truncated);
775 }
776
777 #[test]
778 fn test_format_task_result_for_context_truncates_large_output() {
779 let result = TaskResult {
780 output: format!("{}TAIL", "x".repeat(TASK_OUTPUT_CONTEXT_LIMIT + 500)),
781 session_id: "session-1".to_string(),
782 agent: "explore".to_string(),
783 success: true,
784 task_id: "task-1".to_string(),
785 };
786
787 let (formatted, truncated) = format_task_result_for_context(&result);
788 assert!(truncated);
789 assert!(formatted.contains("Output excerpt"));
790 assert!(formatted.contains("bytes omitted"));
791 assert!(formatted.contains("Artifact ID: subagent-output:task-1"));
792 assert!(formatted.contains("Artifact URI: a3s://subagent/session-1/tasks/task-1/output"));
793 assert!(formatted.contains("TAIL"));
794 assert!(formatted.len() < result.output.len());
795 }
796
797 #[test]
798 fn test_task_artifact_reference_is_stable() {
799 let result = TaskResult {
800 output: "done".to_string(),
801 session_id: "session-1".to_string(),
802 agent: "explore".to_string(),
803 success: true,
804 task_id: "task-1".to_string(),
805 };
806
807 assert_eq!(task_artifact_id(&result), "subagent-output:task-1");
808 assert_eq!(
809 task_artifact_uri(&result),
810 "a3s://subagent/session-1/tasks/task-1/output"
811 );
812
813 let (formatted, truncated) = format_task_result_for_context(&result);
814 assert!(!truncated);
815 assert!(formatted.contains("Artifact URI: a3s://subagent/session-1/tasks/task-1/output"));
816 }
817
818 #[test]
819 fn test_task_params_schema() {
820 let schema = task_params_schema();
821 assert_eq!(schema["type"], "object");
822 assert_eq!(schema["additionalProperties"], false);
823 assert!(schema["properties"]["agent"].is_object());
824 assert!(schema["properties"]["prompt"].is_object());
825 }
826
827 #[test]
828 fn test_task_params_schema_required_fields() {
829 let schema = task_params_schema();
830 let required = schema["required"].as_array().unwrap();
831 assert!(required.contains(&serde_json::json!("agent")));
832 assert!(required.contains(&serde_json::json!("description")));
833 assert!(required.contains(&serde_json::json!("prompt")));
834 }
835
836 #[test]
837 fn test_task_params_schema_properties() {
838 let schema = task_params_schema();
839 let props = &schema["properties"];
840
841 assert_eq!(props["agent"]["type"], "string");
842 assert_eq!(props["description"]["type"], "string");
843 assert_eq!(props["prompt"]["type"], "string");
844 assert_eq!(props["background"]["type"], "boolean");
845 assert_eq!(props["background"]["default"], false);
846 assert_eq!(props["max_steps"]["type"], "integer");
847 }
848
849 #[test]
850 fn test_task_params_schema_descriptions() {
851 let schema = task_params_schema();
852 let props = &schema["properties"];
853
854 assert!(props["agent"]["description"].is_string());
855 assert!(props["description"]["description"].is_string());
856 assert!(props["prompt"]["description"].is_string());
857 assert!(props["background"]["description"].is_string());
858 assert!(props["max_steps"]["description"].is_string());
859 }
860
861 #[test]
862 fn test_task_params_default_background() {
863 let params = TaskParams {
864 agent: "explore".to_string(),
865 description: "Test".to_string(),
866 prompt: "Test prompt".to_string(),
867 background: false,
868 max_steps: None,
869 };
870 assert!(!params.background);
871 }
872
873 #[test]
874 fn test_task_params_serialize_skip_none() {
875 let params = TaskParams {
876 agent: "explore".to_string(),
877 description: "Test".to_string(),
878 prompt: "Test prompt".to_string(),
879 background: false,
880 max_steps: None,
881 };
882 let json = serde_json::to_string(¶ms).unwrap();
883 assert!(!json.contains("max_steps"));
885 }
886
887 #[test]
888 fn test_task_params_serialize_with_max_steps() {
889 let params = TaskParams {
890 agent: "explore".to_string(),
891 description: "Test".to_string(),
892 prompt: "Test prompt".to_string(),
893 background: false,
894 max_steps: Some(15),
895 };
896 let json = serde_json::to_string(¶ms).unwrap();
897 assert!(json.contains("max_steps"));
898 assert!(json.contains("15"));
899 }
900
901 #[test]
902 fn test_task_result_success_true() {
903 let result = TaskResult {
904 output: "Success".to_string(),
905 session_id: "sess-1".to_string(),
906 agent: "explore".to_string(),
907 success: true,
908 task_id: "task-1".to_string(),
909 };
910 assert!(result.success);
911 }
912
913 #[test]
914 fn test_task_result_success_false() {
915 let result = TaskResult {
916 output: "Failed".to_string(),
917 session_id: "sess-1".to_string(),
918 agent: "explore".to_string(),
919 success: false,
920 task_id: "task-1".to_string(),
921 };
922 assert!(!result.success);
923 }
924
925 #[test]
926 fn test_task_params_empty_strings() {
927 let params = TaskParams {
928 agent: "".to_string(),
929 description: "".to_string(),
930 prompt: "".to_string(),
931 background: false,
932 max_steps: None,
933 };
934 let json = serde_json::to_string(¶ms).unwrap();
935 let deserialized: TaskParams = serde_json::from_str(&json).unwrap();
936 assert_eq!(deserialized.agent, "");
937 assert_eq!(deserialized.description, "");
938 assert_eq!(deserialized.prompt, "");
939 }
940
941 #[test]
942 fn test_task_result_empty_output() {
943 let result = TaskResult {
944 output: "".to_string(),
945 session_id: "sess-1".to_string(),
946 agent: "explore".to_string(),
947 success: true,
948 task_id: "task-1".to_string(),
949 };
950 assert_eq!(result.output, "");
951 }
952
953 #[test]
954 fn test_task_params_debug_format() {
955 let params = TaskParams {
956 agent: "explore".to_string(),
957 description: "Test".to_string(),
958 prompt: "Test prompt".to_string(),
959 background: false,
960 max_steps: None,
961 };
962 let debug_str = format!("{:?}", params);
963 assert!(debug_str.contains("explore"));
964 assert!(debug_str.contains("Test"));
965 }
966
967 #[test]
968 fn test_task_result_debug_format() {
969 let result = TaskResult {
970 output: "Output".to_string(),
971 session_id: "sess-1".to_string(),
972 agent: "explore".to_string(),
973 success: true,
974 task_id: "task-1".to_string(),
975 };
976 let debug_str = format!("{:?}", result);
977 assert!(debug_str.contains("Output"));
978 assert!(debug_str.contains("explore"));
979 }
980
981 #[test]
982 fn test_task_params_roundtrip() {
983 let original = TaskParams {
984 agent: "general".to_string(),
985 description: "Roundtrip test".to_string(),
986 prompt: "Test roundtrip serialization".to_string(),
987 background: true,
988 max_steps: Some(42),
989 };
990 let json = serde_json::to_string(&original).unwrap();
991 let deserialized: TaskParams = serde_json::from_str(&json).unwrap();
992 assert_eq!(original.agent, deserialized.agent);
993 assert_eq!(original.description, deserialized.description);
994 assert_eq!(original.prompt, deserialized.prompt);
995 assert_eq!(original.background, deserialized.background);
996 assert_eq!(original.max_steps, deserialized.max_steps);
997 }
998
999 #[test]
1000 fn test_task_result_roundtrip() {
1001 let original = TaskResult {
1002 output: "Roundtrip output".to_string(),
1003 session_id: "sess-roundtrip".to_string(),
1004 agent: "plan".to_string(),
1005 success: false,
1006 task_id: "task-roundtrip".to_string(),
1007 };
1008 let json = serde_json::to_string(&original).unwrap();
1009 let deserialized: TaskResult = serde_json::from_str(&json).unwrap();
1010 assert_eq!(original.output, deserialized.output);
1011 assert_eq!(original.session_id, deserialized.session_id);
1012 assert_eq!(original.agent, deserialized.agent);
1013 assert_eq!(original.success, deserialized.success);
1014 assert_eq!(original.task_id, deserialized.task_id);
1015 }
1016
1017 #[test]
1018 fn test_parallel_task_params_deserialize() {
1019 let json = r#"{
1020 "tasks": [
1021 { "agent": "explore", "description": "Find auth", "prompt": "Search auth files" },
1022 { "agent": "general", "description": "Fix bug", "prompt": "Fix the login bug" }
1023 ]
1024 }"#;
1025
1026 let params: ParallelTaskParams = serde_json::from_str(json).unwrap();
1027 assert_eq!(params.tasks.len(), 2);
1028 assert_eq!(params.tasks[0].agent, "explore");
1029 assert_eq!(params.tasks[1].agent, "general");
1030 }
1031
1032 #[test]
1033 fn test_parallel_task_params_single_task() {
1034 let json = r#"{
1035 "tasks": [
1036 { "agent": "plan", "description": "Plan work", "prompt": "Create a plan" }
1037 ]
1038 }"#;
1039
1040 let params: ParallelTaskParams = serde_json::from_str(json).unwrap();
1041 assert_eq!(params.tasks.len(), 1);
1042 }
1043
1044 #[test]
1045 fn test_parallel_task_params_empty_tasks() {
1046 let json = r#"{ "tasks": [] }"#;
1047 let params: ParallelTaskParams = serde_json::from_str(json).unwrap();
1048 assert!(params.tasks.is_empty());
1049 }
1050
1051 #[test]
1052 fn test_parallel_task_params_missing_tasks() {
1053 let json = r#"{}"#;
1054 let result: Result<ParallelTaskParams, _> = serde_json::from_str(json);
1055 assert!(result.is_err());
1056 }
1057
1058 #[test]
1059 fn test_parallel_task_params_serialize() {
1060 let params = ParallelTaskParams {
1061 tasks: vec![
1062 TaskParams {
1063 agent: "explore".to_string(),
1064 description: "Task 1".to_string(),
1065 prompt: "Prompt 1".to_string(),
1066 background: false,
1067 max_steps: None,
1068 },
1069 TaskParams {
1070 agent: "general".to_string(),
1071 description: "Task 2".to_string(),
1072 prompt: "Prompt 2".to_string(),
1073 background: false,
1074 max_steps: Some(10),
1075 },
1076 ],
1077 };
1078 let json = serde_json::to_string(¶ms).unwrap();
1079 assert!(json.contains("explore"));
1080 assert!(json.contains("general"));
1081 assert!(json.contains("Prompt 1"));
1082 assert!(json.contains("Prompt 2"));
1083 }
1084
1085 #[test]
1086 fn test_parallel_task_params_roundtrip() {
1087 let original = ParallelTaskParams {
1088 tasks: vec![
1089 TaskParams {
1090 agent: "explore".to_string(),
1091 description: "Explore".to_string(),
1092 prompt: "Find files".to_string(),
1093 background: false,
1094 max_steps: None,
1095 },
1096 TaskParams {
1097 agent: "plan".to_string(),
1098 description: "Plan".to_string(),
1099 prompt: "Make plan".to_string(),
1100 background: false,
1101 max_steps: Some(5),
1102 },
1103 ],
1104 };
1105 let json = serde_json::to_string(&original).unwrap();
1106 let deserialized: ParallelTaskParams = serde_json::from_str(&json).unwrap();
1107 assert_eq!(original.tasks.len(), deserialized.tasks.len());
1108 assert_eq!(original.tasks[0].agent, deserialized.tasks[0].agent);
1109 assert_eq!(original.tasks[1].agent, deserialized.tasks[1].agent);
1110 assert_eq!(original.tasks[1].max_steps, deserialized.tasks[1].max_steps);
1111 }
1112
1113 #[test]
1114 fn test_parallel_task_params_clone() {
1115 let params = ParallelTaskParams {
1116 tasks: vec![TaskParams {
1117 agent: "explore".to_string(),
1118 description: "Test".to_string(),
1119 prompt: "Prompt".to_string(),
1120 background: false,
1121 max_steps: None,
1122 }],
1123 };
1124 let cloned = params.clone();
1125 assert_eq!(params.tasks.len(), cloned.tasks.len());
1126 assert_eq!(params.tasks[0].agent, cloned.tasks[0].agent);
1127 }
1128
1129 #[test]
1130 fn test_parallel_task_params_schema() {
1131 let schema = parallel_task_params_schema();
1132 assert_eq!(schema["type"], "object");
1133 assert_eq!(schema["additionalProperties"], false);
1134 assert!(schema["properties"]["tasks"].is_object());
1135 assert_eq!(schema["properties"]["tasks"]["type"], "array");
1136 assert_eq!(schema["properties"]["tasks"]["minItems"], 1);
1137 }
1138
1139 #[test]
1140 fn test_parallel_task_params_schema_required() {
1141 let schema = parallel_task_params_schema();
1142 let required = schema["required"].as_array().unwrap();
1143 assert!(required.contains(&serde_json::json!("tasks")));
1144 }
1145
1146 #[test]
1147 fn test_parallel_task_params_schema_items() {
1148 let schema = parallel_task_params_schema();
1149 let items = &schema["properties"]["tasks"]["items"];
1150 assert_eq!(items["type"], "object");
1151 assert_eq!(items["additionalProperties"], false);
1152 let item_required = items["required"].as_array().unwrap();
1153 assert!(item_required.contains(&serde_json::json!("agent")));
1154 assert!(item_required.contains(&serde_json::json!("description")));
1155 assert!(item_required.contains(&serde_json::json!("prompt")));
1156 }
1157
1158 #[test]
1159 fn test_task_schema_examples_use_delegation_core() {
1160 let task = task_params_schema();
1161 let task_examples = task["examples"].as_array().unwrap();
1162 assert_eq!(task_examples[0]["agent"], "explore");
1163 assert!(task_examples[0].get("task").is_none());
1164
1165 let parallel = parallel_task_params_schema();
1166 let parallel_examples = parallel["examples"].as_array().unwrap();
1167 assert!(!parallel_examples[0]["tasks"].as_array().unwrap().is_empty());
1168 }
1169
1170 #[test]
1171 fn test_parallel_task_params_debug() {
1172 let params = ParallelTaskParams {
1173 tasks: vec![TaskParams {
1174 agent: "explore".to_string(),
1175 description: "Debug test".to_string(),
1176 prompt: "Test".to_string(),
1177 background: false,
1178 max_steps: None,
1179 }],
1180 };
1181 let debug_str = format!("{:?}", params);
1182 assert!(debug_str.contains("explore"));
1183 assert!(debug_str.contains("Debug test"));
1184 }
1185
1186 #[test]
1187 fn test_parallel_task_params_large_count() {
1188 let tasks: Vec<TaskParams> = (0..150)
1190 .map(|i| TaskParams {
1191 agent: "explore".to_string(),
1192 description: format!("Task {}", i),
1193 prompt: format!("Prompt for task {}", i),
1194 background: false,
1195 max_steps: Some(10),
1196 })
1197 .collect();
1198
1199 let params = ParallelTaskParams { tasks };
1200 let json = serde_json::to_string(¶ms).unwrap();
1201 let deserialized: ParallelTaskParams = serde_json::from_str(&json).unwrap();
1202 assert_eq!(deserialized.tasks.len(), 150);
1203 assert_eq!(deserialized.tasks[0].description, "Task 0");
1204 assert_eq!(deserialized.tasks[149].description, "Task 149");
1205 }
1206
1207 #[test]
1208 fn test_task_params_max_steps_zero() {
1209 let params = TaskParams {
1211 agent: "explore".to_string(),
1212 description: "Edge case".to_string(),
1213 prompt: "Zero steps".to_string(),
1214 background: false,
1215 max_steps: Some(0),
1216 };
1217 let json = serde_json::to_string(¶ms).unwrap();
1218 let deserialized: TaskParams = serde_json::from_str(&json).unwrap();
1219 assert_eq!(deserialized.max_steps, Some(0));
1220 }
1221
1222 #[test]
1223 fn test_parallel_task_params_all_background() {
1224 let tasks: Vec<TaskParams> = (0..5)
1225 .map(|i| TaskParams {
1226 agent: "general".to_string(),
1227 description: format!("BG task {}", i),
1228 prompt: "Run in background".to_string(),
1229 background: true,
1230 max_steps: None,
1231 })
1232 .collect();
1233 let params = ParallelTaskParams { tasks };
1234 for task in ¶ms.tasks {
1235 assert!(task.background);
1236 }
1237 }
1238
1239 #[test]
1240 fn test_task_params_rejects_permissive_field() {
1241 let json = r#"{
1242 "agent": "general",
1243 "description": "Legacy field rejection",
1244 "prompt": "Verify legacy fields are rejected",
1245 "permissive": true
1246 }"#;
1247
1248 let result: Result<TaskParams, _> = serde_json::from_str(json);
1249 assert!(result.is_err());
1250 }
1251
1252 #[test]
1253 fn test_task_params_schema_hides_permissive_field() {
1254 let schema = task_params_schema();
1255 let props = &schema["properties"];
1256
1257 assert!(props.get("permissive").is_none());
1258 }
1259}