1use crate::agent::{AgentConfig, AgentEvent, AgentLoop};
18use crate::llm::structured::{generate_blocking, StructuredMode, StructuredRequest};
19use crate::llm::LlmClient;
20use crate::mcp::manager::McpManager;
21use crate::orchestration::{AgentExecutor, AgentStepSpec, StepOutcome, ToolSourceAnchor};
22use crate::subagent::AgentRegistry;
23use crate::tools::types::{Tool, ToolContext, ToolOutput};
24use anyhow::{Context, Result};
25use async_trait::async_trait;
26use futures::FutureExt;
27use serde::{Deserialize, Serialize};
28use std::any::Any;
29use std::panic::AssertUnwindSafe;
30use std::path::PathBuf;
31use std::sync::Arc;
32use tokio::sync::broadcast;
33use tokio::task::JoinSet;
34use tokio_util::sync::CancellationToken;
35
36const TASK_OUTPUT_CONTEXT_LIMIT: usize = 4_000;
37const TASK_OUTPUT_CONTEXT_HEAD: usize = 3_000;
38const TASK_OUTPUT_CONTEXT_TAIL: usize = 800;
39const MAX_TASK_SOURCE_ANCHORS: usize = 64;
40const MAX_TASK_SOURCE_CANDIDATES: usize = MAX_TASK_SOURCE_ANCHORS * 4;
41const MAX_TASK_SOURCE_TOOL_BYTES: usize = 64;
42const MAX_TASK_SOURCE_VALUE_BYTES: usize = 4 * 1024;
43const MAX_PARALLEL_TASK_SOURCE_ANCHORS: usize = MAX_TASK_SOURCE_ANCHORS;
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47#[serde(deny_unknown_fields)]
48pub struct TaskParams {
49 pub agent: String,
51 pub description: String,
53 pub prompt: String,
55 #[serde(default)]
57 pub background: bool,
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub max_steps: Option<usize>,
61 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub output_schema: Option<serde_json::Value>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct TaskResult {
69 pub output: String,
71 pub session_id: String,
73 pub agent: String,
75 pub success: bool,
77 pub task_id: String,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub structured: Option<serde_json::Value>,
82 #[serde(default, skip_serializing_if = "Vec::is_empty")]
84 pub source_anchors: Vec<ToolSourceAnchor>,
85}
86
87mod result_projection;
88use result_projection::*;
89
90mod parallel_execution;
91
92pub struct TaskExecutor {
94 registry: Arc<AgentRegistry>,
96 llm_client: Arc<dyn LlmClient>,
98 workspace: String,
100 mcp_managers: Vec<Arc<McpManager>>,
102 parent_context: Option<crate::child_run::ChildRunContext>,
104 parent_cancellation: Option<CancellationToken>,
108 max_parallel_tasks: usize,
109 subagent_tracker: Option<Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>>,
112}
113
114impl TaskExecutor {
115 pub fn new(
117 registry: Arc<AgentRegistry>,
118 llm_client: Arc<dyn LlmClient>,
119 workspace: String,
120 ) -> Self {
121 Self {
122 registry,
123 llm_client,
124 workspace,
125 mcp_managers: Vec::new(),
126 parent_context: None,
127 parent_cancellation: None,
128 max_parallel_tasks: crate::agent::DEFAULT_MAX_PARALLEL_TASKS,
129 subagent_tracker: None,
130 }
131 }
132
133 pub fn with_mcp(
135 registry: Arc<AgentRegistry>,
136 llm_client: Arc<dyn LlmClient>,
137 workspace: String,
138 mcp_manager: Arc<McpManager>,
139 ) -> Self {
140 Self::with_mcp_managers(registry, llm_client, workspace, vec![mcp_manager])
141 }
142
143 pub fn with_mcp_managers(
145 registry: Arc<AgentRegistry>,
146 llm_client: Arc<dyn LlmClient>,
147 workspace: String,
148 mcp_managers: Vec<Arc<McpManager>>,
149 ) -> Self {
150 Self {
151 registry,
152 llm_client,
153 workspace,
154 mcp_managers,
155 parent_context: None,
156 parent_cancellation: None,
157 max_parallel_tasks: crate::agent::DEFAULT_MAX_PARALLEL_TASKS,
158 subagent_tracker: None,
159 }
160 }
161
162 pub fn with_parent_context(mut self, ctx: crate::child_run::ChildRunContext) -> Self {
164 if let Some(max_parallel_tasks) = ctx.max_parallel_tasks {
165 self.max_parallel_tasks = max_parallel_tasks.max(1);
166 }
167 self.parent_context = Some(ctx);
168 self
169 }
170
171 pub fn with_parent_cancellation(mut self, cancellation: CancellationToken) -> Self {
178 self.parent_cancellation = Some(cancellation);
179 self
180 }
181
182 pub fn with_max_parallel_tasks(mut self, max_parallel_tasks: usize) -> Self {
183 self.max_parallel_tasks = max_parallel_tasks.max(1);
184 self
185 }
186
187 pub fn with_subagent_tracker(
191 mut self,
192 tracker: Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>,
193 ) -> Self {
194 self.subagent_tracker = Some(tracker);
195 self
196 }
197
198 pub async fn execute(
203 &self,
204 params: TaskParams,
205 event_tx: Option<broadcast::Sender<AgentEvent>>,
206 parent_session_id: Option<&str>,
207 ) -> Result<TaskResult> {
208 self.execute_with_parent_cancellation(
209 params,
210 event_tx,
211 parent_session_id,
212 self.parent_cancellation.as_ref(),
213 )
214 .await
215 }
216
217 async fn execute_with_parent_cancellation(
218 &self,
219 params: TaskParams,
220 event_tx: Option<broadcast::Sender<AgentEvent>>,
221 parent_session_id: Option<&str>,
222 parent_cancellation: Option<&CancellationToken>,
223 ) -> Result<TaskResult> {
224 let task_id = format!("task-{}", uuid::Uuid::new_v4());
225 self.execute_with_task_id_scoped(
226 task_id,
227 params,
228 event_tx,
229 parent_session_id,
230 true,
231 parent_cancellation,
232 )
233 .await
234 }
235
236 pub async fn execute_with_task_id(
241 &self,
242 task_id: String,
243 params: TaskParams,
244 event_tx: Option<broadcast::Sender<AgentEvent>>,
245 parent_session_id: Option<&str>,
246 emit_start: bool,
247 ) -> Result<TaskResult> {
248 self.execute_with_task_id_scoped(
249 task_id,
250 params,
251 event_tx,
252 parent_session_id,
253 emit_start,
254 self.parent_cancellation.as_ref(),
255 )
256 .await
257 }
258
259 async fn execute_with_task_id_scoped(
260 &self,
261 task_id: String,
262 params: TaskParams,
263 event_tx: Option<broadcast::Sender<AgentEvent>>,
264 parent_session_id: Option<&str>,
265 emit_start: bool,
266 parent_cancellation: Option<&CancellationToken>,
267 ) -> Result<TaskResult> {
268 if parent_cancellation.is_some_and(CancellationToken::is_cancelled) {
269 anyhow::bail!("Operation cancelled by parent session");
270 }
271
272 let session_id = format!("task-run-{}", task_id);
273 let started_ms = epoch_ms();
274 let output_schema = params.output_schema.clone();
275
276 let agent = self
277 .registry
278 .get(¶ms.agent)
279 .context(format!("Unknown agent type: '{}'", params.agent))?;
280 let inherited_security_provider = self
281 .parent_context
282 .as_ref()
283 .and_then(|context| context.security_provider.clone());
284
285 if emit_start {
286 let event = AgentEvent::SubagentStart {
287 task_id: task_id.clone(),
288 session_id: session_id.clone(),
289 parent_session_id: parent_session_id.unwrap_or_default().to_string(),
290 agent: params.agent.clone(),
291 description: params.description.clone(),
292 started_ms,
293 };
294 let event = inherited_security_provider
295 .as_deref()
296 .map(|provider| crate::security::sanitize_agent_event(provider, &event))
297 .unwrap_or(event);
298 if let Some(ref tracker) = self.subagent_tracker {
299 tracker.record_event(&event).await;
300 }
301 if let Some(ref tx) = event_tx {
302 let _ = tx.send(event);
303 }
304 }
305
306 let child_executor = if let Some(ref parent_ctx) = self.parent_context {
309 if let Some(ref services) = parent_ctx.workspace_services {
310 crate::tools::ToolExecutor::new_with_workspace_services_and_artifact_limits(
311 self.workspace.clone(),
312 Arc::clone(services),
313 crate::tools::ArtifactStoreLimits::default(),
314 )
315 } else {
316 crate::tools::ToolExecutor::new(self.workspace.clone())
317 }
318 } else {
319 crate::tools::ToolExecutor::new(self.workspace.clone())
320 };
321
322 for mcp in &self.mcp_managers {
324 let all_tools = match parent_cancellation {
325 Some(cancellation) => {
326 tokio::select! {
327 biased;
328 _ = cancellation.cancelled() => {
329 anyhow::bail!("Operation cancelled by parent session");
330 }
331 tools = mcp.get_all_tools() => tools,
332 }
333 }
334 None => mcp.get_all_tools().await,
335 };
336 let mut by_server: std::collections::HashMap<
337 String,
338 Vec<crate::mcp::protocol::McpTool>,
339 > = std::collections::HashMap::new();
340 for (server, tool) in all_tools {
341 by_server.entry(server).or_default().push(tool);
342 }
343 for (server_name, tools) in by_server {
344 let wrappers =
345 crate::mcp::tools::create_mcp_tools(&server_name, tools, Arc::clone(mcp));
346 for wrapper in wrappers {
347 child_executor.register_dynamic_tool(wrapper);
348 }
349 }
350 }
351
352 let child_executor = Arc::new(child_executor);
353
354 let mut child_config = AgentConfig {
355 tools: child_executor.definitions(),
356 ..AgentConfig::default()
357 };
358 agent.apply_to(&mut child_config);
359 if let Some(ref parent_ctx) = self.parent_context {
360 parent_ctx.apply_to(&mut child_config);
361 }
362 if let Some(max_steps) = params.max_steps {
363 child_config.max_tool_rounds = max_steps;
364 }
365 let child_security_provider = child_config.security_provider.clone();
366 let source_security_provider = child_security_provider.clone();
367
368 let cancel_token = parent_cancellation
369 .map(CancellationToken::child_token)
370 .unwrap_or_default();
371 let mut tool_context = ToolContext::new(PathBuf::from(&self.workspace))
372 .with_session_id(session_id.clone())
373 .with_cancellation(cancel_token.clone());
374 if let Some(ref parent_ctx) = self.parent_context {
375 if let Some(ref services) = parent_ctx.workspace_services {
376 tool_context = tool_context.with_workspace_services(Arc::clone(services));
377 }
378 }
379
380 let source_context = tool_context.clone();
381 let agent_loop = AgentLoop::new(
382 Arc::clone(&self.llm_client),
383 child_executor,
384 tool_context,
385 child_config,
386 );
387
388 let (mpsc_tx, mut mpsc_rx) = tokio::sync::mpsc::channel(100);
392 let broadcast_tx = event_tx.clone();
393 let progress_task_id = task_id.clone();
394 let progress_session_id = session_id.clone();
395 let child_event_forwarder = tokio::spawn(async move {
396 let mut source_anchors = Vec::new();
397 let mut seen_source_anchors = std::collections::HashSet::new();
398 let mut scanned_source_candidates = 0usize;
399 while let Some(event) = mpsc_rx.recv().await {
400 let event = source_security_provider
401 .as_deref()
402 .map(|provider| crate::security::sanitize_agent_event(provider, &event))
403 .unwrap_or(event);
404 collect_tool_source_anchors(
405 &event,
406 &source_context,
407 &mut source_anchors,
408 &mut seen_source_anchors,
409 &mut scanned_source_candidates,
410 );
411 if let Some(ref broadcast_tx) = broadcast_tx {
412 if let Some(progress) = synthesize_subagent_progress(
413 &event,
414 &progress_task_id,
415 &progress_session_id,
416 ) {
417 let _ = broadcast_tx.send(progress);
418 }
419 let _ = broadcast_tx.send(event);
420 }
421 }
422 source_anchors
423 });
424 let child_event_tx = Some(mpsc_tx);
425 let child_llm_event_tx = child_event_tx.clone();
426
427 if let Some(ref tracker) = self.subagent_tracker {
430 tracker
431 .register_canceller(&task_id, cancel_token.clone())
432 .await;
433 }
434
435 let (mut output, mut success) = match agent_loop
436 .execute_with_session(
437 &[],
438 ¶ms.prompt,
439 Some(&session_id),
440 child_event_tx,
441 Some(&cancel_token),
442 )
443 .await
444 {
445 Ok(result) => (result.text, true),
446 Err(e) if cancel_token.is_cancelled() => {
447 (format!("Task cancelled by caller: {}", e), false)
448 }
449 Err(e) => (format!("Task failed: {}", e), false),
450 };
451
452 let mut structured = None;
453 if success {
454 if let Some(schema) = output_schema {
455 let llm_client = agent_loop.scoped_llm_client_for_parts(
456 Some(&session_id),
457 &child_llm_event_tx,
458 &cancel_token,
459 );
460 match Self::coerce_to_schema(&*llm_client, &output, schema, &cancel_token).await {
461 Ok(object) => structured = Some(object),
462 Err(error) => {
463 success = false;
464 output = format!("{output}\n\n[structured output failed: {error}]");
465 }
466 }
467 }
468 }
469 if let Some(provider) = child_security_provider.as_deref() {
470 output = provider.sanitize_output(&output);
471 if let Some(value) = &mut structured {
472 *value = sanitize_task_json(provider, value);
473 }
474 }
475
476 drop(child_llm_event_tx);
481 let source_anchors = match child_event_forwarder.await {
482 Ok(source_anchors) => source_anchors,
483 Err(error) => {
484 tracing::warn!(%error, task_id = %task_id, "subagent event bridge failed");
485 Vec::new()
486 }
487 };
488
489 let end_event = AgentEvent::SubagentEnd {
490 task_id: task_id.clone(),
491 session_id: session_id.clone(),
492 agent: params.agent.clone(),
493 output: output.clone(),
494 success,
495 finished_ms: epoch_ms(),
496 };
497 if let Some(ref tracker) = self.subagent_tracker {
498 if success {
501 tracker
502 .record_source_anchors(&task_id, &source_anchors)
503 .await;
504 }
505 tracker.record_event(&end_event).await;
506 tracker.clear_canceller(&task_id).await;
507 }
508 if let Some(ref tx) = event_tx {
509 let _ = tx.send(end_event);
510 }
511
512 Ok(TaskResult {
513 output,
514 session_id,
515 agent: params.agent,
516 success,
517 task_id,
518 structured,
519 source_anchors,
520 })
521 }
522
523 pub fn execute_background(
531 self: Arc<Self>,
532 params: TaskParams,
533 event_tx: Option<broadcast::Sender<AgentEvent>>,
534 parent_session_id: Option<String>,
535 ) -> String {
536 let parent_cancellation = self.parent_cancellation.clone();
537 self.execute_background_with_parent_cancellation(
538 params,
539 event_tx,
540 parent_session_id,
541 parent_cancellation,
542 )
543 }
544
545 fn execute_background_with_parent_cancellation(
546 self: Arc<Self>,
547 params: TaskParams,
548 event_tx: Option<broadcast::Sender<AgentEvent>>,
549 parent_session_id: Option<String>,
550 parent_cancellation: Option<CancellationToken>,
551 ) -> String {
552 let task_id = format!("task-{}", uuid::Uuid::new_v4());
553 let session_id = format!("task-run-{}", task_id);
554 let failure_session_id = session_id.clone();
555 let failure_agent = params.agent.clone();
556 let start_event = AgentEvent::SubagentStart {
557 task_id: task_id.clone(),
558 session_id,
559 parent_session_id: parent_session_id.clone().unwrap_or_default(),
560 agent: params.agent.clone(),
561 description: params.description.clone(),
562 started_ms: epoch_ms(),
563 };
564 let security_provider = self
565 .parent_context
566 .as_ref()
567 .and_then(|context| context.security_provider.clone());
568 let start_event = security_provider
569 .as_deref()
570 .map(|provider| crate::security::sanitize_agent_event(provider, &start_event))
571 .unwrap_or(start_event);
572
573 if let Some(ref tx) = event_tx {
574 let _ = tx.send(start_event.clone());
575 }
576
577 let task_id_for_spawn = task_id.clone();
578 let task_id_for_log = task_id.clone();
579 tokio::spawn(async move {
580 if let Some(ref tracker) = self.subagent_tracker {
581 tracker.record_event(&start_event).await;
582 }
583 let failure_event_tx = event_tx.clone();
584 if let Err(error) = self
585 .execute_with_task_id_scoped(
586 task_id_for_spawn,
587 params,
588 event_tx,
589 parent_session_id.as_deref(),
590 false,
591 parent_cancellation.as_ref(),
592 )
593 .await
594 {
595 let end_event = AgentEvent::SubagentEnd {
596 task_id: task_id_for_log.clone(),
597 session_id: failure_session_id,
598 agent: failure_agent,
599 output: format!("Task failed before child execution started: {error}"),
600 success: false,
601 finished_ms: epoch_ms(),
602 };
603 let end_event = security_provider
604 .as_deref()
605 .map(|provider| crate::security::sanitize_agent_event(provider, &end_event))
606 .unwrap_or(end_event);
607 if let Some(ref tracker) = self.subagent_tracker {
608 tracker.record_event(&end_event).await;
609 tracker.clear_canceller(&task_id_for_log).await;
610 }
611 if let Some(tx) = failure_event_tx {
612 let _ = tx.send(end_event);
613 }
614 tracing::error!("Background task {} failed: {}", task_id_for_log, error);
615 }
616 });
617
618 task_id
619 }
620}
621
622pub fn task_params_schema() -> serde_json::Value {
624 serde_json::json!({
625 "type": "object",
626 "additionalProperties": false,
627 "properties": {
628 "agent": {
629 "type": "string",
630 "description": "Required. Canonical agent type to use (for example: explore, general, plan, verification, review). Always provide this exact field name: 'agent'."
631 },
632 "description": {
633 "type": "string",
634 "description": "Required. Short task label for display and tracking. Always provide this exact field name: 'description'."
635 },
636 "prompt": {
637 "type": "string",
638 "description": "Required. Detailed instruction for the delegated child run. Always provide this exact field name: 'prompt'."
639 },
640 "background": {
641 "type": "boolean",
642 "description": "Optional. Run the task in the background. Default: false.",
643 "default": false
644 },
645 "max_steps": {
646 "type": "integer",
647 "description": "Optional. Maximum number of steps for this task."
648 },
649 "output_schema": {
650 "type": "object",
651 "description": "Optional. JSON Schema object the delegated result must satisfy. When provided, the child output is coerced into a validated structured object and returned in metadata."
652 }
653 },
654 "required": ["agent", "description", "prompt"],
655 "examples": [
656 {
657 "agent": "explore",
658 "description": "Find Rust files",
659 "prompt": "Search the workspace for Rust files and summarize the layout."
660 },
661 {
662 "agent": "general",
663 "description": "Investigate test failure",
664 "prompt": "Inspect the failing tests and explain the root cause.",
665 "max_steps": 6
666 }
667 ]
668 })
669}
670
671pub struct TaskTool {
674 executor: Arc<TaskExecutor>,
675}
676
677impl TaskTool {
678 pub fn new(executor: Arc<TaskExecutor>) -> Self {
680 Self { executor }
681 }
682}
683
684#[async_trait]
685impl Tool for TaskTool {
686 fn name(&self) -> &str {
687 "task"
688 }
689
690 fn description(&self) -> &str {
691 "Delegate a bounded task to a specialized child run. Built-in agents: explore (read-only codebase and web evidence search), general/general-purpose (full access multi-step), plan (read-only planning), verification (adversarial validation), review (code review). Custom agents from agent_dirs and .a3s/agents are also available; .claude/agents is read for compatibility."
692 }
693
694 fn parameters(&self) -> serde_json::Value {
695 task_params_schema()
696 }
697
698 async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> {
699 let params: TaskParams =
700 serde_json::from_value(args.clone()).context("Invalid task parameters")?;
701 let parent_cancellation = ctx.cancellation_token();
702
703 if params.background {
704 let task_id = Arc::clone(&self.executor).execute_background_with_parent_cancellation(
705 params,
706 ctx.agent_event_tx.clone(),
707 ctx.session_id.clone(),
708 Some(parent_cancellation),
709 );
710 return Ok(ToolOutput::success(format!(
711 "Task started in background. Task ID: {}",
712 task_id
713 )));
714 }
715
716 let result = self
717 .executor
718 .execute_with_parent_cancellation(
719 params,
720 ctx.agent_event_tx.clone(),
721 ctx.session_id.as_deref(),
722 Some(&parent_cancellation),
723 )
724 .await?;
725 let (content, truncated) = format_task_result_for_context(&result);
726 let metadata = serde_json::json!({
727 "task_id": result.task_id,
728 "session_id": result.session_id,
729 "agent": result.agent,
730 "success": result.success,
731 "output_bytes": result.output.len(),
732 "truncated_for_context": truncated,
733 "artifact_id": task_artifact_id(&result),
734 "artifact_uri": task_artifact_uri(&result),
735 "structured": result.structured,
736 "source_anchors": result.source_anchors,
737 });
738
739 if result.success {
740 Ok(ToolOutput::success(content).with_metadata(metadata))
741 } else {
742 Ok(ToolOutput::error(content).with_metadata(metadata))
743 }
744 }
745}
746
747#[derive(Debug, Clone, Serialize, Deserialize)]
749#[serde(deny_unknown_fields)]
750pub struct ParallelTaskParams {
751 pub tasks: Vec<TaskParams>,
753 #[serde(default)]
756 pub allow_partial_failure: bool,
757 #[serde(default, alias = "timeoutMs", skip_serializing_if = "Option::is_none")]
762 pub timeout_ms: Option<u64>,
763 #[serde(
768 default,
769 alias = "minSuccessCount",
770 skip_serializing_if = "Option::is_none"
771 )]
772 pub min_success_count: Option<usize>,
773}
774
775pub fn parallel_task_params_schema() -> serde_json::Value {
777 serde_json::json!({
778 "type": "object",
779 "additionalProperties": false,
780 "properties": {
781 "tasks": {
782 "type": "array",
783 "description": "List of tasks to execute in parallel. Each task runs as an independent delegated child run concurrently.",
784 "items": {
785 "type": "object",
786 "additionalProperties": false,
787 "properties": {
788 "agent": {
789 "type": "string",
790 "description": "Required. Canonical agent type for this task."
791 },
792 "description": {
793 "type": "string",
794 "description": "Required. Short task label for display and tracking."
795 },
796 "prompt": {
797 "type": "string",
798 "description": "Required. Detailed instruction for the delegated child run."
799 },
800 "background": {
801 "type": "boolean",
802 "description": "Optional. Run this delegated child task in the background. Default: false.",
803 "default": false
804 },
805 "max_steps": {
806 "type": "integer",
807 "description": "Optional. Maximum number of tool/model steps for this delegated child task."
808 },
809 "output_schema": {
810 "type": "object",
811 "description": "Optional. JSON Schema object the delegated child result must satisfy. When provided, the validated object is returned in each result's metadata."
812 }
813 },
814 "required": ["agent", "description", "prompt"]
815 },
816 "minItems": 1
817 },
818 "allow_partial_failure": {
819 "type": "boolean",
820 "description": "Optional. Defaults to false. When true, the parallel_task tool succeeds if at least one child task succeeds, while preserving failed child results in the output and metadata.",
821 "default": false
822 },
823 "timeout_ms": {
824 "type": "integer",
825 "minimum": 1,
826 "description": "Optional total timeout in milliseconds. On timeout, completed child results are returned and unfinished children are marked failed."
827 },
828 "min_success_count": {
829 "type": "integer",
830 "minimum": 1,
831 "description": "Optional successful child count that is enough to return early. Early return is only used when allow_partial_failure is true."
832 }
833 },
834 "required": ["tasks"],
835 "examples": [
836 {
837 "tasks": [
838 {
839 "agent": "explore",
840 "description": "Find Rust files",
841 "prompt": "List Rust files under src/."
842 },
843 {
844 "agent": "explore",
845 "description": "Find tests",
846 "prompt": "List test files and summarize their purpose."
847 }
848 ]
849 }
850 ]
851 })
852}
853
854pub struct ParallelTaskTool {
858 executor: Arc<TaskExecutor>,
859}
860
861impl ParallelTaskTool {
862 pub fn new(executor: Arc<TaskExecutor>) -> Self {
864 Self { executor }
865 }
866}
867
868#[async_trait]
869impl Tool for ParallelTaskTool {
870 fn name(&self) -> &str {
871 "parallel_task"
872 }
873
874 fn description(&self) -> &str {
875 "Fan out 2 or more INDEPENDENT subtasks as delegated child runs that execute concurrently; results are returned when all complete. By default any failed child makes the tool fail; evidence-gathering callers may set allow_partial_failure=true to continue when at least one child succeeds. Use this only when the work genuinely splits into branches that can be investigated or implemented separately (e.g. inspect several unrelated modules at once, or run review and verification in parallel). Do NOT use it for trivial, conversational, or single-step requests, or for steps that depend on one another — handle those directly. Built-in agents: explore (read-only codebase and web evidence search), general/general-purpose (full access multi-step), plan (read-only planning), verification (adversarial validation), review (code review). Custom agents from agent_dirs and .a3s/agents are also available; .claude/agents is read for compatibility."
876 }
877
878 fn parameters(&self) -> serde_json::Value {
879 parallel_task_params_schema()
880 }
881
882 async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> {
883 let params: ParallelTaskParams =
884 serde_json::from_value(args.clone()).context("Invalid parallel task parameters")?;
885 let parent_cancellation = ctx.cancellation_token();
886
887 if params.tasks.is_empty() {
888 return Ok(ToolOutput::error("No tasks provided".to_string()));
889 }
890
891 let task_count = params.tasks.len();
892
893 let run = self
894 .executor
895 .execute_parallel_for_tool(
896 params.tasks,
897 ctx.agent_event_tx.clone(),
898 parallel_execution::ParallelToolOptions {
899 parent_session_id: ctx.session_id.as_deref(),
900 timeout_ms: params.timeout_ms,
901 min_success_count: params.min_success_count,
902 allow_partial_failure: params.allow_partial_failure,
903 parent_cancellation: Some(&parent_cancellation),
904 },
905 )
906 .await;
907 let results = run.results;
908
909 let mut output = format!("Executed {} tasks in parallel:\n\n", task_count);
911 let mut metadata_results = Vec::new();
912 let source_anchor_counts = parallel_source_anchor_counts(&results);
913 for (i, result) in results.iter().enumerate() {
914 let status = if result.success { "[OK]" } else { "[ERR]" };
915 let (formatted, truncated) = format_task_result_for_context(result);
916 let source_anchors = &result.source_anchors[..source_anchor_counts[i]];
917 metadata_results.push(serde_json::json!({
918 "task_id": result.task_id,
919 "session_id": result.session_id,
920 "agent": result.agent,
921 "success": result.success,
922 "output": formatted.clone(),
923 "structured": result.structured,
924 "source_anchors": source_anchors,
925 "output_bytes": result.output.len(),
926 "truncated_for_context": truncated,
927 "artifact_id": task_artifact_id(result),
928 "artifact_uri": task_artifact_uri(result),
929 }));
930 output.push_str(&format!(
931 "--- Task {} ({}) {} ---\n{}\n\n",
932 i + 1,
933 result.agent,
934 status,
935 formatted
936 ));
937 }
938
939 let success_count = results.iter().filter(|result| result.success).count();
940 let failed_count = results.len().saturating_sub(success_count);
941 let all_success = failed_count == 0;
942 let partial_failure = failed_count > 0 && success_count > 0;
943 if params.allow_partial_failure && partial_failure {
944 output.push_str(&format!(
945 "Partial failure tolerated: {success_count} succeeded, {failed_count} failed.\n"
946 ));
947 }
948 if run.timed_out {
949 output.push_str(&format!(
950 "Parallel task timed out after {} ms; returned completed child results and marked unfinished children failed.\n",
951 run.timeout_ms.unwrap_or_default()
952 ));
953 } else if run.returned_early {
954 output.push_str(&format!(
955 "Parallel task returned after reaching min_success_count={}; unfinished children were marked failed.\n",
956 run.min_success_count.unwrap_or_default()
957 ));
958 }
959
960 let tool_success = all_success || (params.allow_partial_failure && success_count > 0);
961 let output = if tool_success {
962 ToolOutput::success(output)
963 } else {
964 ToolOutput::error(output)
965 };
966
967 Ok(output.with_metadata(serde_json::json!({
968 "task_count": task_count,
969 "result_count": results.len(),
970 "success_count": success_count,
971 "failed_count": failed_count,
972 "all_success": all_success,
973 "partial_failure": partial_failure,
974 "allow_partial_failure": params.allow_partial_failure,
975 "timeout_ms": params.timeout_ms,
976 "timed_out": run.timed_out,
977 "min_success_count": params.min_success_count,
978 "returned_early": run.returned_early,
979 "results": metadata_results,
980 })))
981 }
982}
983
984#[cfg(test)]
985mod tests;