1use crate::agent::{AgentConfig, AgentEvent, AgentLoop};
18use crate::llm::structured::{
19 generate_blocking, parse_validated_output, StructuredMode, StructuredRequest,
20};
21use crate::llm::LlmClient;
22use crate::mcp::manager::McpManager;
23use crate::orchestration::{AgentExecutor, AgentStepSpec, StepOutcome, ToolSourceAnchor};
24use crate::subagent::AgentRegistry;
25use crate::tools::types::{Tool, ToolContext, ToolOutput};
26use anyhow::{Context, Result};
27use async_trait::async_trait;
28use futures::FutureExt;
29use serde::{Deserialize, Serialize};
30use std::any::Any;
31use std::panic::AssertUnwindSafe;
32use std::path::PathBuf;
33use std::sync::Arc;
34use tokio::sync::broadcast;
35use tokio::task::JoinSet;
36use tokio_util::sync::CancellationToken;
37
38const TASK_OUTPUT_CONTEXT_LIMIT: usize = 4_000;
39const TASK_OUTPUT_CONTEXT_HEAD: usize = 3_000;
40const TASK_OUTPUT_CONTEXT_TAIL: usize = 800;
41const MAX_TASK_SOURCE_ANCHORS: usize = 64;
42const MAX_TASK_SOURCE_CANDIDATES: usize = MAX_TASK_SOURCE_ANCHORS * 4;
43const MAX_TASK_SOURCE_TOOL_BYTES: usize = 64;
44const MAX_TASK_SOURCE_VALUE_BYTES: usize = 4 * 1024;
45const MAX_PARALLEL_TASK_SOURCE_ANCHORS: usize = MAX_TASK_SOURCE_ANCHORS;
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49#[serde(deny_unknown_fields)]
50pub struct TaskParams {
51 pub agent: String,
53 pub description: String,
55 pub prompt: String,
57 #[serde(default)]
59 pub background: bool,
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub max_steps: Option<usize>,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub output_schema: Option<serde_json::Value>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct TaskResult {
71 pub output: String,
73 pub session_id: String,
75 pub agent: String,
77 pub success: bool,
79 pub task_id: String,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub structured: Option<serde_json::Value>,
84 #[serde(default, skip_serializing_if = "Vec::is_empty")]
86 pub source_anchors: Vec<ToolSourceAnchor>,
87}
88
89mod result_projection;
90use result_projection::*;
91
92mod parallel_execution;
93
94const MAX_PARALLEL_TASKS_PER_CALL: usize = 32;
95
96pub struct TaskExecutor {
98 registry: Arc<AgentRegistry>,
100 llm_client: Arc<dyn LlmClient>,
102 workspace: String,
104 mcp_managers: Vec<Arc<McpManager>>,
106 parent_context: Option<crate::child_run::ChildRunContext>,
108 parent_cancellation: Option<CancellationToken>,
112 max_parallel_tasks: usize,
113 subagent_tracker: Option<Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>>,
116}
117
118impl TaskExecutor {
119 pub fn new(
121 registry: Arc<AgentRegistry>,
122 llm_client: Arc<dyn LlmClient>,
123 workspace: String,
124 ) -> Self {
125 Self {
126 registry,
127 llm_client,
128 workspace,
129 mcp_managers: Vec::new(),
130 parent_context: None,
131 parent_cancellation: None,
132 max_parallel_tasks: crate::agent::DEFAULT_MAX_PARALLEL_TASKS,
133 subagent_tracker: None,
134 }
135 }
136
137 pub fn with_mcp(
139 registry: Arc<AgentRegistry>,
140 llm_client: Arc<dyn LlmClient>,
141 workspace: String,
142 mcp_manager: Arc<McpManager>,
143 ) -> Self {
144 Self::with_mcp_managers(registry, llm_client, workspace, vec![mcp_manager])
145 }
146
147 pub fn with_mcp_managers(
149 registry: Arc<AgentRegistry>,
150 llm_client: Arc<dyn LlmClient>,
151 workspace: String,
152 mcp_managers: Vec<Arc<McpManager>>,
153 ) -> Self {
154 Self {
155 registry,
156 llm_client,
157 workspace,
158 mcp_managers,
159 parent_context: None,
160 parent_cancellation: None,
161 max_parallel_tasks: crate::agent::DEFAULT_MAX_PARALLEL_TASKS,
162 subagent_tracker: None,
163 }
164 }
165
166 pub fn with_parent_context(mut self, ctx: crate::child_run::ChildRunContext) -> Self {
168 if let Some(max_parallel_tasks) = ctx.max_parallel_tasks {
169 self.max_parallel_tasks = max_parallel_tasks.max(1);
170 }
171 self.parent_context = Some(ctx);
172 self
173 }
174
175 pub fn with_parent_cancellation(mut self, cancellation: CancellationToken) -> Self {
182 self.parent_cancellation = Some(cancellation);
183 self
184 }
185
186 pub fn with_max_parallel_tasks(mut self, max_parallel_tasks: usize) -> Self {
187 self.max_parallel_tasks = max_parallel_tasks.max(1);
188 self
189 }
190
191 pub fn with_subagent_tracker(
195 mut self,
196 tracker: Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>,
197 ) -> Self {
198 self.subagent_tracker = Some(tracker);
199 self
200 }
201
202 pub async fn execute(
207 &self,
208 params: TaskParams,
209 event_tx: Option<broadcast::Sender<AgentEvent>>,
210 parent_session_id: Option<&str>,
211 ) -> Result<TaskResult> {
212 self.execute_with_parent_cancellation(
213 params,
214 event_tx,
215 parent_session_id,
216 self.parent_cancellation.as_ref(),
217 )
218 .await
219 }
220
221 async fn execute_with_parent_cancellation(
222 &self,
223 params: TaskParams,
224 event_tx: Option<broadcast::Sender<AgentEvent>>,
225 parent_session_id: Option<&str>,
226 parent_cancellation: Option<&CancellationToken>,
227 ) -> Result<TaskResult> {
228 let task_id = format!("task-{}", uuid::Uuid::new_v4());
229 self.execute_with_task_id_scoped(
230 task_id,
231 params,
232 event_tx,
233 parent_session_id,
234 true,
235 parent_cancellation,
236 )
237 .await
238 }
239
240 pub async fn execute_with_task_id(
245 &self,
246 task_id: String,
247 params: TaskParams,
248 event_tx: Option<broadcast::Sender<AgentEvent>>,
249 parent_session_id: Option<&str>,
250 emit_start: bool,
251 ) -> Result<TaskResult> {
252 self.execute_with_task_id_scoped(
253 task_id,
254 params,
255 event_tx,
256 parent_session_id,
257 emit_start,
258 self.parent_cancellation.as_ref(),
259 )
260 .await
261 }
262
263 async fn execute_with_task_id_scoped(
264 &self,
265 task_id: String,
266 params: TaskParams,
267 event_tx: Option<broadcast::Sender<AgentEvent>>,
268 parent_session_id: Option<&str>,
269 emit_start: bool,
270 parent_cancellation: Option<&CancellationToken>,
271 ) -> Result<TaskResult> {
272 if parent_cancellation.is_some_and(CancellationToken::is_cancelled) {
273 anyhow::bail!("Operation cancelled by parent session");
274 }
275
276 let session_id = format!("task-run-{}", task_id);
277 let started_ms = epoch_ms();
278 let output_schema = params.output_schema.clone();
279
280 let agent = self
281 .registry
282 .get(¶ms.agent)
283 .context(format!("Unknown agent type: '{}'", params.agent))?;
284 let tool_free = agent.tool_free;
285 let tool_free_system = agent.prompt.clone();
286 let inherited_security_provider = self
287 .parent_context
288 .as_ref()
289 .and_then(|context| context.security_provider.clone());
290
291 if emit_start {
292 let event = AgentEvent::SubagentStart {
293 task_id: task_id.clone(),
294 session_id: session_id.clone(),
295 parent_session_id: parent_session_id.unwrap_or_default().to_string(),
296 agent: params.agent.clone(),
297 description: params.description.clone(),
298 started_ms,
299 };
300 let event = inherited_security_provider
301 .as_deref()
302 .map(|provider| crate::security::sanitize_agent_event(provider, &event))
303 .unwrap_or(event);
304 if let Some(ref tracker) = self.subagent_tracker {
305 tracker.record_event(&event).await;
306 }
307 if let Some(ref tx) = event_tx {
308 let _ = tx.send(event);
309 }
310 }
311
312 let child_executor = if let Some(ref parent_ctx) = self.parent_context {
315 if let Some(ref services) = parent_ctx.workspace_services {
316 crate::tools::ToolExecutor::new_with_workspace_services_and_artifact_limits(
317 self.workspace.clone(),
318 Arc::clone(services),
319 crate::tools::ArtifactStoreLimits::default(),
320 )
321 } else {
322 crate::tools::ToolExecutor::new(self.workspace.clone())
323 }
324 } else {
325 crate::tools::ToolExecutor::new(self.workspace.clone())
326 };
327
328 for mcp in &self.mcp_managers {
330 let all_tools = match parent_cancellation {
331 Some(cancellation) => {
332 tokio::select! {
333 biased;
334 _ = cancellation.cancelled() => {
335 anyhow::bail!("Operation cancelled by parent session");
336 }
337 tools = mcp.get_all_tools() => tools,
338 }
339 }
340 None => mcp.get_all_tools().await,
341 };
342 let mut by_server: std::collections::HashMap<
343 String,
344 Vec<crate::mcp::protocol::McpTool>,
345 > = std::collections::HashMap::new();
346 for (server, tool) in all_tools {
347 by_server.entry(server).or_default().push(tool);
348 }
349 for (server_name, tools) in by_server {
350 let wrappers =
351 crate::mcp::tools::create_mcp_tools(&server_name, tools, Arc::clone(mcp));
352 for wrapper in wrappers {
353 child_executor.register_dynamic_tool(wrapper);
354 }
355 }
356 }
357
358 let child_executor = Arc::new(child_executor);
359
360 let mut child_config = AgentConfig {
361 tools: child_executor.definitions(),
362 ..AgentConfig::default()
363 };
364 agent.apply_to(&mut child_config);
365 if let Some(ref parent_ctx) = self.parent_context {
366 parent_ctx.apply_to(&mut child_config);
367 }
368 child_config.planning_mode = crate::prompts::PlanningMode::Disabled;
373 if let Some(max_steps) = params.max_steps {
374 child_config.max_tool_rounds = max_steps;
375 }
376 let child_security_provider = child_config.security_provider.clone();
377 let source_security_provider = child_security_provider.clone();
378
379 let cancel_token = parent_cancellation
380 .map(CancellationToken::child_token)
381 .unwrap_or_default();
382 let mut tool_context = ToolContext::new(PathBuf::from(&self.workspace))
383 .with_session_id(session_id.clone())
384 .with_cancellation(cancel_token.clone());
385 if let Some(ref parent_ctx) = self.parent_context {
386 if let Some(ref services) = parent_ctx.workspace_services {
387 tool_context = tool_context.with_workspace_services(Arc::clone(services));
388 }
389 }
390
391 let source_context = tool_context.clone();
392 let agent_loop = AgentLoop::new(
393 Arc::clone(&self.llm_client),
394 child_executor,
395 tool_context,
396 child_config,
397 );
398
399 let (mpsc_tx, mut mpsc_rx) = tokio::sync::mpsc::channel(100);
403 let broadcast_tx = event_tx.clone();
404 let progress_task_id = task_id.clone();
405 let progress_session_id = session_id.clone();
406 let child_event_forwarder = tokio::spawn(async move {
407 let mut source_anchors = Vec::new();
408 let mut seen_source_anchors = std::collections::HashSet::new();
409 let mut scanned_source_candidates = 0usize;
410 while let Some(event) = mpsc_rx.recv().await {
411 let event = source_security_provider
412 .as_deref()
413 .map(|provider| crate::security::sanitize_agent_event(provider, &event))
414 .unwrap_or(event);
415 collect_tool_source_anchors(
416 &event,
417 &source_context,
418 &mut source_anchors,
419 &mut seen_source_anchors,
420 &mut scanned_source_candidates,
421 );
422 if let Some(ref broadcast_tx) = broadcast_tx {
423 if let Some(progress) = synthesize_subagent_progress(
424 &event,
425 &progress_task_id,
426 &progress_session_id,
427 ) {
428 let _ = broadcast_tx.send(progress);
429 }
430 let _ = broadcast_tx.send(event);
431 }
432 }
433 source_anchors
434 });
435 let child_event_tx = Some(mpsc_tx);
436 let child_llm_event_tx = child_event_tx.clone();
437
438 if let Some(ref tracker) = self.subagent_tracker {
441 tracker
442 .register_canceller(&task_id, cancel_token.clone())
443 .await;
444 }
445
446 let structured_prompt = output_schema
447 .as_ref()
448 .filter(|_| !tool_free)
449 .map(|schema| structured_task_prompt(¶ms.prompt, schema));
450 let execution_prompt = structured_prompt.as_deref().unwrap_or(¶ms.prompt);
451
452 let mut structured = None;
453 let (mut output, mut success) = if tool_free && output_schema.is_some() {
454 let llm_client = agent_loop.scoped_llm_client_for_parts(
455 Some(&session_id),
456 &child_llm_event_tx,
457 &cancel_token,
458 );
459 match Self::generate_structured_task(
460 &*llm_client,
461 ¶ms.prompt,
462 tool_free_system.as_deref(),
463 output_schema.clone().expect("schema checked above"),
464 &cancel_token,
465 )
466 .await
467 {
468 Ok(object) => {
469 let output = serde_json::to_string_pretty(&object)
470 .unwrap_or_else(|_| object.to_string());
471 structured = Some(object);
472 (output, true)
473 }
474 Err(error) if cancel_token.is_cancelled() => {
475 (format!("Task cancelled by caller: {error}"), false)
476 }
477 Err(error) => (format!("Task failed: {error}"), false),
478 }
479 } else {
480 match agent_loop
481 .execute_with_session(
482 &[],
483 execution_prompt,
484 Some(&session_id),
485 child_event_tx.clone(),
486 Some(&cancel_token),
487 )
488 .await
489 {
490 Ok(_) if cancel_token.is_cancelled() => {
491 ("Task cancelled by caller".to_string(), false)
492 }
493 Ok(result) if result.text.trim().is_empty() => (
494 "Task failed: child agent returned no final output".to_string(),
495 false,
496 ),
497 Ok(result) if AgentLoop::is_synthetic_failure_output(&result.text) => {
498 (format!("Task failed: {}", result.text), false)
499 }
500 Ok(result) => (result.text, true),
501 Err(e) if cancel_token.is_cancelled() => {
502 (format!("Task cancelled by caller: {}", e), false)
503 }
504 Err(e) => (format!("Task failed: {}", e), false),
505 }
506 };
507
508 if success && !tool_free {
509 if let Some(schema) = output_schema {
510 if let Some(object) = parse_validated_output(&output, &schema) {
511 structured = Some(object);
512 } else {
513 let llm_client = agent_loop.scoped_llm_client_for_parts(
514 Some(&session_id),
515 &child_llm_event_tx,
516 &cancel_token,
517 );
518 match Self::coerce_to_schema(&*llm_client, &output, schema, &cancel_token).await
519 {
520 Ok(object) => structured = Some(object),
521 Err(error) => {
522 success = false;
523 output = format!("{output}\n\n[structured output failed: {error}]");
524 }
525 }
526 }
527 }
528 }
529 if let Some(provider) = child_security_provider.as_deref() {
530 output = provider.sanitize_output(&output);
531 if let Some(value) = &mut structured {
532 *value = sanitize_task_json(provider, value);
533 }
534 }
535
536 drop(child_event_tx);
541 drop(child_llm_event_tx);
542 let source_anchors = match child_event_forwarder.await {
543 Ok(source_anchors) => source_anchors,
544 Err(error) => {
545 tracing::warn!(%error, task_id = %task_id, "subagent event bridge failed");
546 Vec::new()
547 }
548 };
549
550 let end_event = AgentEvent::SubagentEnd {
551 task_id: task_id.clone(),
552 session_id: session_id.clone(),
553 agent: params.agent.clone(),
554 output: output.clone(),
555 success,
556 finished_ms: epoch_ms(),
557 };
558 if let Some(ref tracker) = self.subagent_tracker {
559 if success {
562 tracker
563 .record_source_anchors(&task_id, &source_anchors)
564 .await;
565 }
566 tracker.record_event(&end_event).await;
567 tracker.clear_canceller(&task_id).await;
568 }
569 if let Some(ref tx) = event_tx {
570 let _ = tx.send(end_event);
571 }
572
573 Ok(TaskResult {
574 output,
575 session_id,
576 agent: params.agent,
577 success,
578 task_id,
579 structured,
580 source_anchors,
581 })
582 }
583
584 pub fn execute_background(
592 self: Arc<Self>,
593 params: TaskParams,
594 event_tx: Option<broadcast::Sender<AgentEvent>>,
595 parent_session_id: Option<String>,
596 ) -> String {
597 let parent_cancellation = self.parent_cancellation.clone();
598 self.execute_background_with_parent_cancellation(
599 params,
600 event_tx,
601 parent_session_id,
602 parent_cancellation,
603 )
604 }
605
606 fn execute_background_with_parent_cancellation(
607 self: Arc<Self>,
608 params: TaskParams,
609 event_tx: Option<broadcast::Sender<AgentEvent>>,
610 parent_session_id: Option<String>,
611 parent_cancellation: Option<CancellationToken>,
612 ) -> String {
613 let task_id = format!("task-{}", uuid::Uuid::new_v4());
614 let session_id = format!("task-run-{}", task_id);
615 let failure_session_id = session_id.clone();
616 let failure_agent = params.agent.clone();
617 let start_event = AgentEvent::SubagentStart {
618 task_id: task_id.clone(),
619 session_id,
620 parent_session_id: parent_session_id.clone().unwrap_or_default(),
621 agent: params.agent.clone(),
622 description: params.description.clone(),
623 started_ms: epoch_ms(),
624 };
625 let security_provider = self
626 .parent_context
627 .as_ref()
628 .and_then(|context| context.security_provider.clone());
629 let start_event = security_provider
630 .as_deref()
631 .map(|provider| crate::security::sanitize_agent_event(provider, &start_event))
632 .unwrap_or(start_event);
633
634 if let Some(ref tx) = event_tx {
635 let _ = tx.send(start_event.clone());
636 }
637
638 let task_id_for_spawn = task_id.clone();
639 let task_id_for_log = task_id.clone();
640 tokio::spawn(async move {
641 if let Some(ref tracker) = self.subagent_tracker {
642 tracker.record_event(&start_event).await;
643 }
644 let failure_event_tx = event_tx.clone();
645 if let Err(error) = self
646 .execute_with_task_id_scoped(
647 task_id_for_spawn,
648 params,
649 event_tx,
650 parent_session_id.as_deref(),
651 false,
652 parent_cancellation.as_ref(),
653 )
654 .await
655 {
656 let end_event = AgentEvent::SubagentEnd {
657 task_id: task_id_for_log.clone(),
658 session_id: failure_session_id,
659 agent: failure_agent,
660 output: format!("Task failed before child execution started: {error}"),
661 success: false,
662 finished_ms: epoch_ms(),
663 };
664 let end_event = security_provider
665 .as_deref()
666 .map(|provider| crate::security::sanitize_agent_event(provider, &end_event))
667 .unwrap_or(end_event);
668 if let Some(ref tracker) = self.subagent_tracker {
669 tracker.record_event(&end_event).await;
670 tracker.clear_canceller(&task_id_for_log).await;
671 }
672 if let Some(tx) = failure_event_tx {
673 let _ = tx.send(end_event);
674 }
675 tracing::error!("Background task {} failed: {}", task_id_for_log, error);
676 }
677 });
678
679 task_id
680 }
681}
682
683fn structured_task_prompt(prompt: &str, schema: &serde_json::Value) -> String {
684 let schema = serde_json::to_string_pretty(schema).unwrap_or_else(|_| schema.to_string());
685 format!(
686 "{prompt}\n\n\
687 FINAL OUTPUT CONTRACT\n\
688 Complete the requested investigation before answering. Your final response must contain \
689 exactly one JSON value matching the JSON Schema below, with no Markdown fence or prose \
690 outside the JSON. This contract applies to the final response only; use the available \
691 tools as needed before finalizing.\n\n\
692 {schema}"
693 )
694}
695
696pub fn task_params_schema() -> serde_json::Value {
698 serde_json::json!({
699 "type": "object",
700 "additionalProperties": false,
701 "properties": {
702 "agent": {
703 "type": "string",
704 "description": "Required. Canonical agent type to use (for example: explore, general, plan, verification, review). Always provide this exact field name: 'agent'."
705 },
706 "description": {
707 "type": "string",
708 "description": "Required. Short task label for display and tracking. Always provide this exact field name: 'description'."
709 },
710 "prompt": {
711 "type": "string",
712 "description": "Required. Detailed instruction for the delegated child run. Always provide this exact field name: 'prompt'."
713 },
714 "background": {
715 "type": "boolean",
716 "description": "Optional. Run the task in the background. Default: false.",
717 "default": false
718 },
719 "max_steps": {
720 "type": "integer",
721 "description": "Optional. Maximum number of steps for this task."
722 },
723 "output_schema": {
724 "type": "object",
725 "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."
726 }
727 },
728 "required": ["agent", "description", "prompt"],
729 "examples": [
730 {
731 "agent": "explore",
732 "description": "Find Rust files",
733 "prompt": "Search the workspace for Rust files and summarize the layout."
734 },
735 {
736 "agent": "general",
737 "description": "Investigate test failure",
738 "prompt": "Inspect the failing tests and explain the root cause.",
739 "max_steps": 6
740 }
741 ]
742 })
743}
744
745pub struct TaskTool {
748 executor: Arc<TaskExecutor>,
749}
750
751impl TaskTool {
752 pub fn new(executor: Arc<TaskExecutor>) -> Self {
754 Self { executor }
755 }
756}
757
758#[async_trait]
759impl Tool for TaskTool {
760 fn name(&self) -> &str {
761 "task"
762 }
763
764 fn description(&self) -> &str {
765 "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."
766 }
767
768 fn parameters(&self) -> serde_json::Value {
769 task_params_schema()
770 }
771
772 async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> {
773 let params: TaskParams =
774 serde_json::from_value(args.clone()).context("Invalid task parameters")?;
775 let parent_cancellation = ctx.cancellation_token();
776
777 if params.background {
778 let task_id = Arc::clone(&self.executor).execute_background_with_parent_cancellation(
779 params,
780 ctx.agent_event_tx.clone(),
781 ctx.session_id.clone(),
782 Some(parent_cancellation),
783 );
784 return Ok(ToolOutput::success(format!(
785 "Task started in background. Task ID: {}",
786 task_id
787 )));
788 }
789
790 let result = self
791 .executor
792 .execute_with_parent_cancellation(
793 params,
794 ctx.agent_event_tx.clone(),
795 ctx.session_id.as_deref(),
796 Some(&parent_cancellation),
797 )
798 .await?;
799 let (content, truncated) = format_task_result_for_context(&result);
800 let metadata = serde_json::json!({
801 "task_id": result.task_id,
802 "session_id": result.session_id,
803 "agent": result.agent,
804 "success": result.success,
805 "output_bytes": result.output.len(),
806 "truncated_for_context": truncated,
807 "artifact_id": task_artifact_id(&result),
808 "artifact_uri": task_artifact_uri(&result),
809 "structured": result.structured,
810 "source_anchors": result.source_anchors,
811 });
812
813 if result.success {
814 Ok(ToolOutput::success(content).with_metadata(metadata))
815 } else {
816 Ok(ToolOutput::error(content).with_metadata(metadata))
817 }
818 }
819}
820
821mod parallel_params;
822pub use parallel_params::{parallel_task_params_schema, ParallelTaskParams};
823
824pub struct ParallelTaskTool {
828 executor: Arc<TaskExecutor>,
829}
830
831impl ParallelTaskTool {
832 pub fn new(executor: Arc<TaskExecutor>) -> Self {
834 Self { executor }
835 }
836}
837
838#[async_trait]
839impl Tool for ParallelTaskTool {
840 fn name(&self) -> &str {
841 "parallel_task"
842 }
843
844 fn description(&self) -> &str {
845 "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."
846 }
847
848 fn parameters(&self) -> serde_json::Value {
849 parallel_task_params_schema()
850 }
851
852 async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> {
853 let started_at = std::time::Instant::now();
854 let params: ParallelTaskParams =
855 serde_json::from_value(args.clone()).context("Invalid parallel task parameters")?;
856 let parent_cancellation = ctx.cancellation_token();
857
858 if params.tasks.is_empty() {
859 return Ok(ToolOutput::error("No tasks provided".to_string()));
860 }
861 if params.tasks.len() > MAX_PARALLEL_TASKS_PER_CALL {
862 return Ok(ToolOutput::error(format!(
863 "parallel_task accepts at most {MAX_PARALLEL_TASKS_PER_CALL} tasks"
864 )));
865 }
866
867 let task_count = params.tasks.len();
868
869 let run = self
870 .executor
871 .execute_parallel_for_tool(
872 params.tasks,
873 ctx.agent_event_tx.clone(),
874 parallel_execution::ParallelToolOptions {
875 parent_session_id: ctx.session_id.as_deref(),
876 timeout_ms: params.timeout_ms,
877 min_success_count: params.min_success_count,
878 allow_partial_failure: params.allow_partial_failure,
879 parent_cancellation: Some(&parent_cancellation),
880 },
881 )
882 .await;
883 let results = run.results;
884
885 let mut output = format!("Executed {} tasks in parallel:\n\n", task_count);
887 let mut metadata_results = Vec::new();
888 let source_anchor_counts = parallel_source_anchor_counts(&results);
889 for (i, result) in results.iter().enumerate() {
890 let status = if result.success { "[OK]" } else { "[ERR]" };
891 let (formatted, truncated) = format_task_result_for_context(result);
892 let source_anchors = &result.source_anchors[..source_anchor_counts[i]];
893 metadata_results.push(serde_json::json!({
894 "task_id": result.task_id,
895 "session_id": result.session_id,
896 "agent": result.agent,
897 "success": result.success,
898 "error_message": (!result.success).then(|| {
899 crate::text::truncate_utf8(&result.output, 1024).to_string()
900 }),
901 "structured": result.structured,
902 "source_anchors": source_anchors,
903 "output_bytes": result.output.len(),
904 "truncated_for_context": truncated,
905 "artifact_id": task_artifact_id(result),
906 "artifact_uri": task_artifact_uri(result),
907 }));
908 output.push_str(&format!(
909 "--- Task {} ({}) {} ---\n{}\n\n",
910 i + 1,
911 result.agent,
912 status,
913 formatted
914 ));
915 }
916
917 let success_count = results.iter().filter(|result| result.success).count();
918 let failed_count = results.len().saturating_sub(success_count);
919 let all_success = failed_count == 0;
920 let partial_failure = failed_count > 0 && success_count > 0;
921 if params.allow_partial_failure && partial_failure {
922 output.push_str(&format!(
923 "Partial failure tolerated: {success_count} succeeded, {failed_count} failed.\n"
924 ));
925 }
926 if run.timed_out {
927 output.push_str(&format!(
928 "Parallel task timed out after {} ms; returned completed child results and marked unfinished children failed.\n",
929 run.timeout_ms.unwrap_or_default()
930 ));
931 } else if run.returned_early {
932 output.push_str(&format!(
933 "Parallel task returned after reaching min_success_count={}; unfinished children were marked failed.\n",
934 run.min_success_count.unwrap_or_default()
935 ));
936 }
937
938 let tool_success = all_success || (params.allow_partial_failure && success_count > 0);
939 let mut output = if tool_success {
940 ToolOutput::success(output)
941 } else {
942 ToolOutput::error(output)
943 };
944 if !tool_success && failed_count > 0 {
945 output.error_kind = Some(crate::tools::ToolErrorKind::PartialFailure {
946 failed: failed_count,
947 total: results.len(),
948 });
949 }
950
951 Ok(output.with_metadata(serde_json::json!({
952 "task_count": task_count,
953 "result_count": results.len(),
954 "success_count": success_count,
955 "failed_count": failed_count,
956 "all_success": all_success,
957 "partial_failure": partial_failure,
958 "allow_partial_failure": params.allow_partial_failure,
959 "timeout_ms": params.timeout_ms,
960 "timed_out": run.timed_out,
961 "min_success_count": params.min_success_count,
962 "returned_early": run.returned_early,
963 "duration_ms": started_at.elapsed().as_millis().min(u128::from(u64::MAX)) as u64,
964 "results": metadata_results,
965 })))
966 }
967}
968
969#[cfg(test)]
970mod tests;