1use crate::agent::{AgentConfig, AgentEvent, AgentLoop};
18use crate::llm::structured::{
19 generate_blocking, parse_validated_output, StructuredMode, StructuredRequest,
20};
21use crate::llm::{LlmClient, ToolDefinition};
22use crate::mcp::{McpBinding, McpManager};
23use crate::orchestration::{AgentExecutor, AgentStepSpec, StepOutcome, ToolSourceAnchor};
24use crate::subagent::{AgentDefinition, 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::collections::HashSet;
32use std::panic::AssertUnwindSafe;
33use std::path::PathBuf;
34use std::sync::{Arc, Mutex, MutexGuard};
35use tokio::sync::broadcast;
36use tokio::task::JoinSet;
37use tokio_util::sync::CancellationToken;
38
39const TASK_OUTPUT_CONTEXT_LIMIT: usize = 4_000;
40const TASK_OUTPUT_CONTEXT_HEAD: usize = 3_000;
41const TASK_OUTPUT_CONTEXT_TAIL: usize = 800;
42const MAX_TASK_SOURCE_ANCHORS: usize = 64;
43const MAX_TASK_SOURCE_CANDIDATES: usize = MAX_TASK_SOURCE_ANCHORS * 4;
44const MAX_TASK_SOURCE_TOOL_BYTES: usize = 64;
45const MAX_TASK_SOURCE_VALUE_BYTES: usize = 4 * 1024;
46const MAX_PARALLEL_TASK_SOURCE_ANCHORS: usize = MAX_TASK_SOURCE_ANCHORS;
47const TASK_TOOL_DESCRIPTION: &str = "Delegate one or more bounded tasks to specialized child runs. Pass one item for a focused child run or multiple INDEPENDENT items for concurrent fan-out. A single item may run in the background; multi-item calls are collected by the parent. By default any failed child makes a multi-item call fail; evidence-gathering callers may set allow_partial_failure=true. Choose canonical worker names from the live agent catalog. Custom agents from agent_dirs and .a3s/agents are supported; .claude/agents is read for compatibility.";
48const PARALLEL_TASK_TOOL_DESCRIPTION: &str = "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. Child output never authorizes branch replay; provider and child runtimes own any typed retry policy below this boundary. Use this only when the work genuinely splits into branches that can be investigated or implemented separately. Do not use it for trivial, conversational, single-step, or dependent work. Choose canonical worker names from the live agent catalog.";
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52#[serde(deny_unknown_fields)]
53pub struct TaskParams {
54 pub agent: String,
56 pub description: String,
58 pub prompt: String,
60 #[serde(default)]
62 pub background: bool,
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub max_steps: Option<usize>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub output_schema: Option<serde_json::Value>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct TaskResult {
74 pub output: String,
76 pub session_id: String,
78 pub agent: String,
80 pub success: bool,
82 pub task_id: String,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub structured: Option<serde_json::Value>,
87 #[serde(default, skip_serializing_if = "Vec::is_empty")]
89 pub source_anchors: Vec<ToolSourceAnchor>,
90}
91
92struct ScopedTaskExecution<'a> {
93 event_tx: Option<broadcast::Sender<AgentEvent>>,
94 parent_session_id: Option<&'a str>,
95 emit_start: bool,
96 parent_cancellation: Option<&'a CancellationToken>,
97 admitted_capability_subtask: Option<crate::capability::AgentCapabilitySubtask>,
98 parallel_lifecycle: Option<Arc<ParallelTaskLifecycle>>,
99}
100
101#[derive(Default)]
110pub(super) struct ParallelTaskLifecycle {
111 state: Mutex<ParallelTaskLifecycleState>,
112}
113
114#[derive(Default)]
115struct ParallelTaskLifecycleState {
116 started: HashSet<String>,
117 ended: HashSet<String>,
118}
119
120impl ParallelTaskLifecycle {
121 fn lock_state(&self) -> MutexGuard<'_, ParallelTaskLifecycleState> {
122 match self.state.lock() {
123 Ok(guard) => guard,
124 Err(poisoned) => poisoned.into_inner(),
128 }
129 }
130
131 fn mark_started(&self, task_id: &str) {
135 self.lock_state().started.insert(task_id.to_string());
136 }
137
138 fn is_started(&self, task_id: &str) -> bool {
139 self.lock_state().started.contains(task_id)
140 }
141
142 fn mark_ended(&self, task_id: &str) {
146 let mut state = self.lock_state();
147 if state.started.contains(task_id) {
148 state.ended.insert(task_id.to_string());
149 }
150 }
151
152 fn is_ended(&self, task_id: &str) -> bool {
153 self.lock_state().ended.contains(task_id)
154 }
155}
156
157mod result_projection;
158use result_projection::*;
159
160mod parallel_execution;
161
162const MAX_PARALLEL_TASKS_PER_CALL: usize = 32;
163
164#[derive(Clone)]
166pub struct TaskExecutor {
167 registry: Arc<AgentRegistry>,
169 llm_client: Arc<dyn LlmClient>,
171 workspace: String,
173 mcp_managers: Vec<Arc<McpManager>>,
175 mcp_bindings: Vec<Arc<McpBinding>>,
177 scoped_tools: Vec<Arc<dyn Tool>>,
181 parent_context: Option<crate::child_run::ChildRunContext>,
183 search_config: Option<Arc<crate::config::SearchConfig>>,
185 search_bulkhead: Option<a3s_search::Bulkhead>,
187 search_retry_budget: Option<a3s_search::RetryBudget>,
189 search_request_coalescer: Option<a3s_search::SearchCoalescer>,
191 capability_context: Option<crate::capability::AgentToolCapabilityContext>,
193 parent_cancellation: Option<CancellationToken>,
197 max_parallel_tasks: usize,
198 parallel_permits: Arc<tokio::sync::Semaphore>,
202 subagent_tracker: Option<Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>>,
205 task_scheduler: Option<Arc<crate::task_scheduler::TaskScheduler>>,
207 schedule_foreground: bool,
211}
212
213impl TaskExecutor {
214 pub fn new(
216 registry: Arc<AgentRegistry>,
217 llm_client: Arc<dyn LlmClient>,
218 workspace: String,
219 ) -> Self {
220 Self {
221 registry,
222 llm_client,
223 workspace,
224 mcp_managers: Vec::new(),
225 mcp_bindings: Vec::new(),
226 scoped_tools: Vec::new(),
227 parent_context: None,
228 search_config: None,
229 search_bulkhead: None,
230 search_retry_budget: None,
231 search_request_coalescer: None,
232 capability_context: None,
233 parent_cancellation: None,
234 max_parallel_tasks: crate::agent::DEFAULT_MAX_PARALLEL_TASKS,
235 parallel_permits: Arc::new(tokio::sync::Semaphore::new(
236 crate::agent::DEFAULT_MAX_PARALLEL_TASKS,
237 )),
238 subagent_tracker: None,
239 task_scheduler: None,
240 schedule_foreground: false,
241 }
242 }
243
244 pub fn with_mcp(
246 registry: Arc<AgentRegistry>,
247 llm_client: Arc<dyn LlmClient>,
248 workspace: String,
249 mcp_manager: Arc<McpManager>,
250 ) -> Self {
251 Self::with_mcp_managers(registry, llm_client, workspace, vec![mcp_manager])
252 }
253
254 pub fn with_mcp_managers(
256 registry: Arc<AgentRegistry>,
257 llm_client: Arc<dyn LlmClient>,
258 workspace: String,
259 mcp_managers: Vec<Arc<McpManager>>,
260 ) -> Self {
261 Self {
262 registry,
263 llm_client,
264 workspace,
265 mcp_managers,
266 mcp_bindings: Vec::new(),
267 scoped_tools: Vec::new(),
268 parent_context: None,
269 search_config: None,
270 search_bulkhead: None,
271 search_retry_budget: None,
272 search_request_coalescer: None,
273 capability_context: None,
274 parent_cancellation: None,
275 max_parallel_tasks: crate::agent::DEFAULT_MAX_PARALLEL_TASKS,
276 parallel_permits: Arc::new(tokio::sync::Semaphore::new(
277 crate::agent::DEFAULT_MAX_PARALLEL_TASKS,
278 )),
279 subagent_tracker: None,
280 task_scheduler: None,
281 schedule_foreground: false,
282 }
283 }
284
285 pub(crate) fn with_projected_mcp_bindings(mut self, bindings: Vec<Arc<McpBinding>>) -> Self {
288 self.mcp_bindings = bindings;
289 self
290 }
291
292 pub fn with_scoped_tools(mut self, tools: Vec<Arc<dyn Tool>>) -> Self {
297 self.scoped_tools = tools;
298 self
299 }
300
301 pub fn with_parent_context(mut self, ctx: crate::child_run::ChildRunContext) -> Self {
303 if let Some(max_parallel_tasks) = ctx.max_parallel_tasks {
304 let max_parallel_tasks = max_parallel_tasks.max(1);
305 self.max_parallel_tasks = max_parallel_tasks;
306 self.parallel_permits = Arc::new(tokio::sync::Semaphore::new(max_parallel_tasks));
307 }
308 self.parent_context = Some(ctx);
309 self
310 }
311
312 fn scoped_for_invocation(self: &Arc<Self>, ctx: &ToolContext) -> Arc<Self> {
313 let mut scoped = self.as_ref().clone();
314 scoped.search_config = ctx.search_config.clone();
315 scoped.search_bulkhead = Some(ctx.search_bulkhead());
316 scoped.search_retry_budget = Some(ctx.search_retry_budget());
317 scoped.search_request_coalescer = Some(ctx.search_request_coalescer());
318 scoped.capability_context = ctx.capability_context();
319 if ctx.has_run_governance() {
320 scoped.parent_context = scoped.parent_context.take().map(|parent| {
321 parent.with_run_governance(
322 ctx.run_permission_checker(),
323 ctx.run_confirmation_manager(),
324 )
325 });
326 }
327 Arc::new(scoped)
328 }
329
330 fn child_tool_context(
331 &self,
332 session_id: String,
333 cancellation: CancellationToken,
334 ) -> ToolContext {
335 let mut context = ToolContext::new(PathBuf::from(&self.workspace))
336 .with_session_id(session_id)
337 .with_cancellation(cancellation);
338 if let (Some(bulkhead), Some(retry_budget)) =
339 (&self.search_bulkhead, &self.search_retry_budget)
340 {
341 context = context.with_search_runtime(bulkhead.clone(), retry_budget.clone());
342 }
343 if let Some(search_config) = &self.search_config {
344 context = context.with_search_config(search_config.as_ref().clone());
345 }
346 if let Some(coalescer) = &self.search_request_coalescer {
347 context = context.with_search_request_coalescer(coalescer.clone());
348 }
349 context
350 }
351
352 pub fn with_parent_cancellation(mut self, cancellation: CancellationToken) -> Self {
359 self.parent_cancellation = Some(cancellation);
360 self
361 }
362
363 pub fn with_max_parallel_tasks(mut self, max_parallel_tasks: usize) -> Self {
364 let max_parallel_tasks = max_parallel_tasks.max(1);
365 self.max_parallel_tasks = max_parallel_tasks;
366 self.parallel_permits = Arc::new(tokio::sync::Semaphore::new(max_parallel_tasks));
367 self
368 }
369
370 pub fn with_subagent_tracker(
374 mut self,
375 tracker: Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>,
376 ) -> Self {
377 self.subagent_tracker = Some(tracker);
378 self
379 }
380
381 pub fn with_task_scheduler(
383 mut self,
384 scheduler: Arc<crate::task_scheduler::TaskScheduler>,
385 schedule_foreground: bool,
386 ) -> Self {
387 self.task_scheduler = Some(scheduler);
388 self.schedule_foreground = schedule_foreground;
389 self
390 }
391
392 fn visible_agents(&self) -> Vec<AgentDefinition> {
393 self.registry.list_visible()
394 }
395
396 pub async fn execute(
401 &self,
402 params: TaskParams,
403 event_tx: Option<broadcast::Sender<AgentEvent>>,
404 parent_session_id: Option<&str>,
405 ) -> Result<TaskResult> {
406 self.execute_with_parent_cancellation(
407 params,
408 event_tx,
409 parent_session_id,
410 self.parent_cancellation.as_ref(),
411 )
412 .await
413 }
414
415 async fn execute_with_parent_cancellation(
416 &self,
417 params: TaskParams,
418 event_tx: Option<broadcast::Sender<AgentEvent>>,
419 parent_session_id: Option<&str>,
420 parent_cancellation: Option<&CancellationToken>,
421 ) -> Result<TaskResult> {
422 let task_id = format!("task-{}", uuid::Uuid::new_v4());
423 self.execute_with_task_id_scoped(
424 task_id,
425 params,
426 ScopedTaskExecution {
427 event_tx,
428 parent_session_id,
429 emit_start: true,
430 parent_cancellation,
431 admitted_capability_subtask: None,
432 parallel_lifecycle: None,
433 },
434 )
435 .await
436 }
437
438 pub async fn execute_with_task_id(
443 &self,
444 task_id: String,
445 params: TaskParams,
446 event_tx: Option<broadcast::Sender<AgentEvent>>,
447 parent_session_id: Option<&str>,
448 emit_start: bool,
449 ) -> Result<TaskResult> {
450 self.execute_with_task_id_scoped(
451 task_id,
452 params,
453 ScopedTaskExecution {
454 event_tx,
455 parent_session_id,
456 emit_start,
457 parent_cancellation: self.parent_cancellation.as_ref(),
458 admitted_capability_subtask: None,
459 parallel_lifecycle: None,
460 },
461 )
462 .await
463 }
464
465 async fn execute_with_task_id_scoped(
466 &self,
467 task_id: String,
468 params: TaskParams,
469 execution: ScopedTaskExecution<'_>,
470 ) -> Result<TaskResult> {
471 let ScopedTaskExecution {
472 event_tx,
473 parent_session_id,
474 emit_start,
475 parent_cancellation,
476 admitted_capability_subtask,
477 parallel_lifecycle,
478 } = execution;
479 let was_promoted = admitted_capability_subtask.is_some();
480 if !was_promoted && parent_cancellation.is_some_and(CancellationToken::is_cancelled) {
481 anyhow::bail!("Operation cancelled by parent session");
482 }
483
484 let capability_subtask = match admitted_capability_subtask {
485 Some(subtask) => Some(subtask),
486 None => self
487 .capability_context
488 .as_ref()
489 .map(|context| context.admit_subtask(task_id.clone(), params.background))
490 .transpose()?,
491 };
492 let cancel_token = capability_subtask.as_ref().map_or_else(
493 || {
494 parent_cancellation
495 .map(CancellationToken::child_token)
496 .unwrap_or_default()
497 },
498 crate::capability::AgentCapabilitySubtask::cancellation,
499 );
500 let capability_runtime = capability_subtask
501 .as_ref()
502 .map(crate::capability::AgentCapabilitySubtask::runtime);
503 let execution = self
504 .execute_with_task_id_in_scope(
505 task_id,
506 params,
507 event_tx,
508 parent_session_id,
509 emit_start,
510 cancel_token,
511 capability_runtime,
512 parallel_lifecycle,
513 )
514 .await;
515 let close = close_capability_subtask(capability_subtask.as_ref()).await;
516 match (execution, close) {
517 (Ok(result), Ok(())) => Ok(result),
518 (Ok(_), Err(close_error)) => Err(close_error),
519 (Err(error), Ok(())) => Err(error),
520 (Err(error), Err(close_error)) => {
521 tracing::warn!(
522 error = %close_error,
523 "Capability Subtask close also failed after delegated execution failure"
524 );
525 Err(error)
526 }
527 }
528 }
529
530 #[allow(clippy::too_many_arguments)]
531 async fn execute_with_task_id_in_scope(
532 &self,
533 task_id: String,
534 params: TaskParams,
535 event_tx: Option<broadcast::Sender<AgentEvent>>,
536 parent_session_id: Option<&str>,
537 emit_start: bool,
538 cancel_token: CancellationToken,
539 capability_runtime: Option<crate::capability::AgentCapabilityRuntime>,
540 parallel_lifecycle: Option<Arc<ParallelTaskLifecycle>>,
541 ) -> Result<TaskResult> {
542 if params.background {
546 if let Some(ref tracker) = self.subagent_tracker {
547 tracker
548 .register_canceller(&task_id, cancel_token.clone())
549 .await;
550 }
551 }
552 let _task_lease = if params.background || self.schedule_foreground {
553 match &self.task_scheduler {
554 Some(scheduler) => Some(
555 scheduler
556 .acquire(
557 if params.background {
558 crate::task_scheduler::TaskPriority::Background
559 } else {
560 crate::task_scheduler::TaskPriority::Foreground
561 },
562 format!(
563 "{}:subagent:{}",
564 parent_session_id.unwrap_or("host"),
565 task_id
566 ),
567 &cancel_token,
568 )
569 .await
570 .map_err(|error| anyhow::anyhow!(error))?,
571 ),
572 None => None,
573 }
574 } else {
575 None
576 };
577
578 let session_id = format!("task-run-{}", task_id);
579 let started_ms = epoch_ms();
580 let output_schema = params.output_schema.clone();
581
582 let agent = self
583 .registry
584 .get_arc(¶ms.agent)
585 .context(format!("Unknown agent type: '{}'", params.agent))?;
586 let tool_free = agent.tool_free;
587 let tool_free_system = agent.prompt.clone();
588 let inherited_security_provider = self
589 .parent_context
590 .as_ref()
591 .and_then(|context| context.security_provider.clone());
592
593 if emit_start {
594 let event = AgentEvent::SubagentStart {
595 task_id: task_id.clone(),
596 session_id: session_id.clone(),
597 parent_session_id: parent_session_id.unwrap_or_default().to_string(),
598 agent: params.agent.clone(),
599 description: params.description.clone(),
600 started_ms,
601 };
602 let event = inherited_security_provider
603 .as_deref()
604 .map(|provider| crate::security::sanitize_agent_event(provider, &event))
605 .unwrap_or(event);
606 if let Some(ref tracker) = self.subagent_tracker {
607 tracker.record_event(&event).await;
608 }
609 if let Some(ref tx) = event_tx {
610 let _ = tx.send(event);
611 }
612 if let Some(lifecycle) = ¶llel_lifecycle {
613 lifecycle.mark_started(&task_id);
617 }
618 }
619
620 let child_executor = if let Some(ref parent_ctx) = self.parent_context {
623 if let Some(ref services) = parent_ctx.workspace_services {
624 crate::tools::ToolExecutor::new_with_workspace_services_artifact_limits_and_immutable_content_adapter(
625 self.workspace.clone(),
626 Arc::clone(services),
627 crate::tools::ArtifactStoreLimits::default(),
628 parent_ctx.immutable_content_adapter.clone(),
629 )
630 } else if let Some(adapter) = parent_ctx.immutable_content_adapter.clone() {
631 crate::tools::ToolExecutor::new_with_immutable_content_adapter(
632 self.workspace.clone(),
633 adapter,
634 )
635 } else {
636 crate::tools::ToolExecutor::new(self.workspace.clone())
637 }
638 } else {
639 crate::tools::ToolExecutor::new(self.workspace.clone())
640 };
641
642 for mcp in &self.mcp_managers {
644 let all_tools = tokio::select! {
645 biased;
646 _ = cancel_token.cancelled() => {
647 anyhow::bail!("Operation cancelled before child execution");
648 }
649 tools = mcp.get_all_tools() => tools,
650 };
651 let mut by_server: std::collections::HashMap<
652 String,
653 Vec<crate::mcp::protocol::McpTool>,
654 > = std::collections::HashMap::new();
655 for (server, tool) in all_tools {
656 by_server.entry(server).or_default().push(tool);
657 }
658 for (server_name, tools) in by_server {
659 let wrappers =
660 crate::mcp::tools::create_mcp_tools(&server_name, tools, Arc::clone(mcp));
661 for wrapper in wrappers {
662 child_executor.register_dynamic_tool(wrapper);
663 }
664 }
665 }
666 for binding in &self.mcp_bindings {
672 if cancel_token.is_cancelled() {
673 anyhow::bail!("Operation cancelled before child execution");
674 }
675 for wrapper in binding.projected_tools() {
676 child_executor.register_dynamic_tool(wrapper);
677 }
678 }
679
680 for tool in &self.scoped_tools {
684 if !child_executor.register_dynamic_tool_if_absent(Arc::clone(tool)) {
685 anyhow::bail!(
686 "Workflow-scoped tool '{}' conflicts with another child capability",
687 tool.name()
688 );
689 }
690 }
691
692 let child_executor = Arc::new(child_executor);
693
694 let mut child_config = AgentConfig {
695 tools: child_executor.definitions(),
696 ..AgentConfig::default()
697 };
698 agent.apply_to(&mut child_config);
699 if let Some(ref parent_ctx) = self.parent_context {
700 parent_ctx.apply_to(&mut child_config);
701 }
702 child_config.planning_mode = crate::prompts::PlanningMode::Disabled;
707 if let Some(max_steps) = params.max_steps {
708 child_config.max_tool_rounds = max_steps;
709 }
710 let child_security_provider = child_config.security_provider.clone();
711 let source_security_provider = child_security_provider.clone();
712
713 let mut tool_context = self.child_tool_context(session_id.clone(), cancel_token.clone());
714 if let Some(ref parent_ctx) = self.parent_context {
715 if let Some(ref services) = parent_ctx.workspace_services {
716 tool_context = tool_context.with_workspace_services(Arc::clone(services));
717 }
718 if let Some(ref sandbox) = parent_ctx.sandbox_handle {
719 child_executor.registry().set_sandbox(Arc::clone(sandbox));
720 tool_context = tool_context.with_sandbox(Arc::clone(sandbox));
721 }
722 }
723
724 let source_context = tool_context.clone();
725 let mut agent_loop = AgentLoop::new(
726 Arc::clone(&self.llm_client),
727 child_executor,
728 tool_context,
729 child_config,
730 );
731 if let Some(runtime) = capability_runtime {
732 agent_loop = agent_loop.with_capability_runtime(runtime);
733 }
734
735 let (mpsc_tx, mut mpsc_rx) = tokio::sync::mpsc::channel(100);
739 let broadcast_tx = event_tx.clone();
740 let progress_task_id = task_id.clone();
741 let progress_session_id = session_id.clone();
742 let child_event_forwarder = tokio::spawn(async move {
743 let mut source_anchors = Vec::new();
744 let mut seen_source_anchors = std::collections::HashSet::new();
745 let mut scanned_source_candidates = 0usize;
746 while let Some(event) = mpsc_rx.recv().await {
747 let event = source_security_provider
748 .as_deref()
749 .map(|provider| crate::security::sanitize_agent_event(provider, &event))
750 .unwrap_or(event);
751 collect_tool_source_anchors(
752 &event,
753 &source_context,
754 &mut source_anchors,
755 &mut seen_source_anchors,
756 &mut scanned_source_candidates,
757 );
758 if let Some(ref broadcast_tx) = broadcast_tx {
759 if let Some(progress) = synthesize_subagent_progress(
760 &event,
761 &progress_task_id,
762 &progress_session_id,
763 ) {
764 let _ = broadcast_tx.send(progress);
765 }
766 let _ = broadcast_tx.send(event);
767 }
768 }
769 source_anchors
770 });
771 let child_event_tx = Some(mpsc_tx);
772 let child_llm_event_tx = child_event_tx.clone();
773
774 if !params.background {
777 if let Some(ref tracker) = self.subagent_tracker {
778 tracker
779 .register_canceller(&task_id, cancel_token.clone())
780 .await;
781 }
782 }
783
784 let structured_prompt = output_schema
785 .as_ref()
786 .filter(|_| !tool_free)
787 .map(|schema| structured_task_prompt(¶ms.prompt, schema));
788 let execution_prompt = structured_prompt.as_deref().unwrap_or(¶ms.prompt);
789
790 let mut structured = None;
791 let (mut output, mut success, raw_output) = if tool_free && output_schema.is_some() {
792 let operation = agent_loop.begin_capability_operation(
793 0,
794 &cancel_token,
795 "structured task generation",
796 )?;
797 let llm_client = agent_loop.scoped_llm_client_for_parts(
798 Some(&session_id),
799 &child_llm_event_tx,
800 operation.cancellation(),
801 );
802 let generation = Self::generate_structured_task(
803 &*llm_client,
804 ¶ms.prompt,
805 tool_free_system.as_deref(),
806 output_schema.clone().expect("schema checked above"),
807 operation.cancellation(),
808 )
809 .await;
810 let generation = settle_task_capability_operation(
811 generation,
812 operation.close().await,
813 "structured task generation",
814 );
815 match generation {
816 Ok(object) => {
817 let output = serde_json::to_string_pretty(&object)
818 .unwrap_or_else(|_| object.to_string());
819 structured = Some(object);
820 (output, true, None)
821 }
822 Err(error) if cancel_token.is_cancelled() => {
823 (format!("Task cancelled by caller: {error}"), false, None)
824 }
825 Err(error) => (format!("Task failed: {error}"), false, None),
826 }
827 } else {
828 match agent_loop
829 .execute_with_session(
830 &[],
831 execution_prompt,
832 Some(&session_id),
833 child_event_tx.clone(),
834 Some(&cancel_token),
835 )
836 .await
837 {
838 Ok(_) if cancel_token.is_cancelled() => {
839 ("Task cancelled by caller".to_string(), false, None)
840 }
841 Ok(result) if result.text.trim().is_empty() => (
842 "Task failed: child agent returned no final output".to_string(),
843 false,
844 None,
845 ),
846 Ok(result) if AgentLoop::is_synthetic_failure_output(&result.text) => {
847 (format!("Task failed: {}", result.text), false, None)
848 }
849 Ok(result) => {
850 let raw_output = result
851 .messages
852 .last()
853 .filter(|message| message.role == "assistant")
854 .map(crate::llm::Message::text)
855 .filter(|text| !text.trim().is_empty());
856 (result.text, true, raw_output)
857 }
858 Err(e) if cancel_token.is_cancelled() => {
859 (format!("Task cancelled by caller: {}", e), false, None)
860 }
861 Err(e) => (format!("Task failed: {}", e), false, None),
862 }
863 };
864
865 if success && !tool_free {
866 if let Some(schema) = output_schema.as_ref() {
867 if let Some(object) = raw_output
868 .as_deref()
869 .and_then(|raw| parse_validated_output(raw, schema))
870 .or_else(|| parse_validated_output(&output, schema))
871 {
872 structured = Some(object);
873 } else {
874 let operation = agent_loop.begin_capability_operation(
875 0,
876 &cancel_token,
877 "structured task coercion",
878 )?;
879 let llm_client = agent_loop.scoped_llm_client_for_parts(
880 Some(&session_id),
881 &child_llm_event_tx,
882 operation.cancellation(),
883 );
884 let coercion = Self::coerce_to_schema(
885 &*llm_client,
886 &output,
887 schema.clone(),
888 operation.cancellation(),
889 )
890 .await;
891 let coercion = settle_task_capability_operation(
892 coercion,
893 operation.close().await,
894 "structured task coercion",
895 );
896 match coercion {
897 Ok(object) => structured = Some(object),
898 Err(error) => {
899 success = false;
900 output = format!("{output}\n\n[structured output failed: {error}]");
901 }
902 }
903 }
904 }
905 }
906 if let Some(provider) = child_security_provider.as_deref() {
907 output = provider.sanitize_output(&output);
908 if let Some(value) = structured.take() {
909 let sanitized = output_schema.as_ref().map_or_else(
910 || sanitize_task_json(provider, &value),
911 |schema| sanitize_task_json_with_schema(provider, &value, schema),
912 );
913 if output_schema
914 .as_ref()
915 .is_none_or(|schema| value_matches_schema(&sanitized, schema))
916 {
917 structured = Some(sanitized);
918 } else {
919 success = false;
920 }
921 }
922 }
923
924 drop(child_event_tx);
929 drop(child_llm_event_tx);
930 let source_anchors = match child_event_forwarder.await {
931 Ok(source_anchors) => source_anchors,
932 Err(error) => {
933 tracing::warn!(%error, task_id = %task_id, "subagent event bridge failed");
934 Vec::new()
935 }
936 };
937
938 let end_event = AgentEvent::SubagentEnd {
939 task_id: task_id.clone(),
940 session_id: session_id.clone(),
941 agent: params.agent.clone(),
942 output: output.clone(),
943 success,
944 finished_ms: epoch_ms(),
945 };
946 if let Some(ref tracker) = self.subagent_tracker {
947 if success {
950 tracker
951 .record_source_anchors(&task_id, &source_anchors)
952 .await;
953 }
954 tracker.record_event(&end_event).await;
955 tracker.clear_canceller(&task_id).await;
956 }
957 if let Some(ref tx) = event_tx {
958 let _ = tx.send(end_event);
959 }
960 if let Some(lifecycle) = ¶llel_lifecycle {
961 lifecycle.mark_ended(&task_id);
965 }
966
967 Ok(TaskResult {
968 output,
969 session_id,
970 agent: params.agent,
971 success,
972 task_id,
973 structured,
974 source_anchors,
975 })
976 }
977
978 pub fn execute_background(
986 self: Arc<Self>,
987 params: TaskParams,
988 event_tx: Option<broadcast::Sender<AgentEvent>>,
989 parent_session_id: Option<String>,
990 ) -> String {
991 let parent_cancellation = self.parent_cancellation.clone();
992 self.execute_background_with_parent_cancellation(
993 params,
994 event_tx,
995 parent_session_id,
996 parent_cancellation,
997 )
998 }
999
1000 fn execute_background_with_parent_cancellation(
1001 self: Arc<Self>,
1002 params: TaskParams,
1003 event_tx: Option<broadcast::Sender<AgentEvent>>,
1004 parent_session_id: Option<String>,
1005 parent_cancellation: Option<CancellationToken>,
1006 ) -> String {
1007 let task_id = format!("task-{}", uuid::Uuid::new_v4());
1008 let session_id = format!("task-run-{}", task_id);
1009 let failure_session_id = session_id.clone();
1010 let failure_agent = params.agent.clone();
1011 let start_event = AgentEvent::SubagentStart {
1012 task_id: task_id.clone(),
1013 session_id,
1014 parent_session_id: parent_session_id.clone().unwrap_or_default(),
1015 agent: params.agent.clone(),
1016 description: params.description.clone(),
1017 started_ms: epoch_ms(),
1018 };
1019 let security_provider = self
1020 .parent_context
1021 .as_ref()
1022 .and_then(|context| context.security_provider.clone());
1023 let start_event = security_provider
1024 .as_deref()
1025 .map(|provider| crate::security::sanitize_agent_event(provider, &start_event))
1026 .unwrap_or(start_event);
1027
1028 if let Some(ref tx) = event_tx {
1029 let _ = tx.send(start_event.clone());
1030 }
1031
1032 let capability_admission = self
1033 .capability_context
1034 .as_ref()
1035 .map(|context| {
1036 context
1037 .admit_subtask(task_id.clone(), true)
1038 .map(|subtask| (context.background_scope().clone(), subtask))
1039 })
1040 .transpose();
1041 let (capability_run, admitted_capability_subtask) = match capability_admission {
1042 Ok(Some((run, subtask))) => (Some(run), Some(subtask)),
1043 Ok(None) => (None, None),
1044 Err(error) => {
1045 let message = format!("Background task capability admission failed: {error}");
1046 let end_event = AgentEvent::SubagentEnd {
1047 task_id: task_id.clone(),
1048 session_id: failure_session_id,
1049 agent: failure_agent,
1050 output: message.clone(),
1051 success: false,
1052 finished_ms: epoch_ms(),
1053 };
1054 let end_event = security_provider
1055 .as_deref()
1056 .map(|provider| crate::security::sanitize_agent_event(provider, &end_event))
1057 .unwrap_or(end_event);
1058 if let Some(tx) = event_tx {
1059 let _ = tx.send(end_event);
1060 }
1061 tracing::error!(task_id = %task_id, "{message}");
1062 return task_id;
1063 }
1064 };
1065
1066 let task_id_for_spawn = task_id.clone();
1067 let task_id_for_log = task_id.clone();
1068 let admission_failure_task_id = task_id.clone();
1069 let admission_failure_session_id = failure_session_id.clone();
1070 let admission_failure_agent = failure_agent.clone();
1071 let admission_failure_events = event_tx.clone();
1072 let admission_failure_security = security_provider.clone();
1073 let background = async move {
1074 if let Some(ref tracker) = self.subagent_tracker {
1075 tracker.record_event(&start_event).await;
1076 }
1077 let failure_event_tx = event_tx.clone();
1078 if let Err(error) = self
1079 .execute_with_task_id_scoped(
1080 task_id_for_spawn,
1081 params,
1082 ScopedTaskExecution {
1083 event_tx,
1084 parent_session_id: parent_session_id.as_deref(),
1085 emit_start: false,
1086 parent_cancellation: parent_cancellation.as_ref(),
1087 admitted_capability_subtask,
1088 parallel_lifecycle: None,
1089 },
1090 )
1091 .await
1092 {
1093 let end_event = AgentEvent::SubagentEnd {
1094 task_id: task_id_for_log.clone(),
1095 session_id: failure_session_id,
1096 agent: failure_agent,
1097 output: format!("Task failed before child execution started: {error}"),
1098 success: false,
1099 finished_ms: epoch_ms(),
1100 };
1101 let end_event = security_provider
1102 .as_deref()
1103 .map(|provider| crate::security::sanitize_agent_event(provider, &end_event))
1104 .unwrap_or(end_event);
1105 if let Some(ref tracker) = self.subagent_tracker {
1106 tracker.record_event(&end_event).await;
1107 tracker.clear_canceller(&task_id_for_log).await;
1108 }
1109 if let Some(tx) = failure_event_tx {
1110 let _ = tx.send(end_event);
1111 }
1112 tracing::error!("Background task {} failed: {}", task_id_for_log, error);
1113 }
1114 };
1115 if let Some(run) = capability_run {
1116 let task_name = format!("subagent.{task_id}");
1117 if let Err(error) = run.spawn_task(task_name, async move {
1118 background.await;
1119 Ok(())
1120 }) {
1121 let message = format!("Background task capability admission failed: {error}");
1125 let end_event = AgentEvent::SubagentEnd {
1126 task_id: admission_failure_task_id.clone(),
1127 session_id: admission_failure_session_id,
1128 agent: admission_failure_agent,
1129 output: message.clone(),
1130 success: false,
1131 finished_ms: epoch_ms(),
1132 };
1133 let end_event = admission_failure_security
1134 .as_deref()
1135 .map(|provider| crate::security::sanitize_agent_event(provider, &end_event))
1136 .unwrap_or(end_event);
1137 if let Some(tx) = admission_failure_events {
1138 let _ = tx.send(end_event);
1139 }
1140 tracing::error!(task_id = %admission_failure_task_id, "{message}");
1141 }
1142 } else {
1143 tokio::spawn(background);
1144 }
1145
1146 task_id
1147 }
1148}
1149
1150async fn close_capability_subtask(
1151 subtask: Option<&crate::capability::AgentCapabilitySubtask>,
1152) -> Result<()> {
1153 let Some(subtask) = subtask else {
1154 return Ok(());
1155 };
1156 let report = subtask.close().await?;
1157 if !report.is_clean() {
1158 anyhow::bail!(
1159 "Capability Subtask close was incomplete (tasks failed: {}, tasks timed out: {}, child scopes failed: {}, child scopes timed out: {}, effects failed: {}, effects timed out: {})",
1160 report.tasks_failed,
1161 report.tasks_timed_out,
1162 report.child_scopes_failed,
1163 report.child_scopes_timed_out,
1164 report.effects_failed,
1165 report.effects_timed_out,
1166 );
1167 }
1168 Ok(())
1169}
1170
1171fn settle_task_capability_operation<T>(
1172 execution: Result<T>,
1173 close: Result<()>,
1174 label: &str,
1175) -> Result<T> {
1176 match (execution, close) {
1177 (Ok(result), Ok(())) => Ok(result),
1178 (Ok(_), Err(close_error)) => Err(close_error),
1179 (Err(error), Ok(())) => Err(error),
1180 (Err(error), Err(close_error)) => {
1181 tracing::warn!(
1182 error = %close_error,
1183 operation = label,
1184 "Capability orchestration Turn close also failed after model failure"
1185 );
1186 Err(error)
1187 }
1188 }
1189}
1190
1191fn structured_task_prompt(prompt: &str, schema: &serde_json::Value) -> String {
1192 let schema = serde_json::to_string_pretty(schema).unwrap_or_else(|_| schema.to_string());
1193 format!(
1194 "{prompt}\n\n\
1195 FINAL OUTPUT CONTRACT\n\
1196 Complete the requested investigation before answering. Your final response must contain \
1197 exactly one JSON value matching the JSON Schema below, with no Markdown fence or prose \
1198 outside the JSON. This contract applies to the final response only; use the available \
1199 tools as needed before finalizing.\n\n\
1200 {schema}"
1201 )
1202}
1203
1204fn value_matches_schema(value: &serde_json::Value, schema: &serde_json::Value) -> bool {
1205 serde_json::to_string(value)
1206 .ok()
1207 .and_then(|encoded| parse_validated_output(&encoded, schema))
1208 .is_some()
1209}
1210
1211#[derive(Debug, Clone)]
1212struct AgentCatalogEntry {
1213 name: String,
1214 description: String,
1215}
1216
1217fn agent_catalog_entries(agents: &[AgentDefinition]) -> Vec<AgentCatalogEntry> {
1218 let mut entries = agents
1219 .iter()
1220 .map(|agent| AgentCatalogEntry {
1221 name: agent.name.clone(),
1222 description: agent
1223 .description
1224 .split_whitespace()
1225 .collect::<Vec<_>>()
1226 .join(" "),
1227 })
1228 .collect::<Vec<_>>();
1229 entries.sort_by(|left, right| left.name.cmp(&right.name));
1230 entries
1231}
1232
1233fn agent_catalog_text(agents: &[AgentDefinition]) -> String {
1234 agent_catalog_entries(agents)
1235 .into_iter()
1236 .map(|entry| format!("{}: {}", entry.name, entry.description))
1237 .collect::<Vec<_>>()
1238 .join("\n")
1239}
1240
1241fn delegation_tool_description(base: &str, agents: &[AgentDefinition]) -> String {
1242 format!(
1243 "{base}\n\nAvailable agents (live catalog; use canonical names):\n{}",
1244 agent_catalog_text(agents)
1245 )
1246}
1247
1248pub(super) fn task_agent_parameter_schema(agents: &[AgentDefinition]) -> serde_json::Value {
1249 let entries = agent_catalog_entries(agents);
1250 let examples = entries
1251 .iter()
1252 .map(|entry| serde_json::Value::String(entry.name.clone()))
1253 .collect::<Vec<_>>();
1254 let catalog = entries
1255 .into_iter()
1256 .map(|entry| format!("{}: {}", entry.name, entry.description))
1257 .collect::<Vec<_>>()
1258 .join("\n");
1259 serde_json::json!({
1260 "type": "string",
1261 "description": format!(
1262 "Required. Canonical agent type to use. Always provide this exact field name: 'agent'. Live agent catalog:\n{catalog}"
1263 ),
1264 "examples": examples
1265 })
1266}
1267
1268pub fn task_params_schema() -> serde_json::Value {
1274 task_params_schema_for_agents(&AgentRegistry::new().list_visible())
1275}
1276
1277fn task_params_schema_for_agents(agents: &[AgentDefinition]) -> serde_json::Value {
1278 serde_json::json!({
1279 "oneOf": [
1280 legacy_task_params_schema_for_agents(agents),
1281 task_model_params_schema_for_agents(agents)
1282 ]
1283 })
1284}
1285
1286fn legacy_task_params_schema_for_agents(agents: &[AgentDefinition]) -> serde_json::Value {
1287 let mut schema = task_item_params_schema_for_agents(agents, true);
1288 schema["examples"] = serde_json::json!([
1289 {
1290 "agent": "explore",
1291 "description": "Find Rust files",
1292 "prompt": "Search the workspace for Rust files and summarize the layout."
1293 },
1294 {
1295 "agent": "general",
1296 "description": "Investigate test failure",
1297 "prompt": "Inspect the failing tests and explain the root cause.",
1298 "max_steps": 6
1299 }
1300 ]);
1301 schema
1302}
1303
1304fn task_model_params_schema_for_agents(agents: &[AgentDefinition]) -> serde_json::Value {
1305 parallel_params::task_tool_params_schema_for_agents(agents)
1306}
1307
1308pub(super) fn task_item_params_schema_for_agents(
1309 agents: &[AgentDefinition],
1310 include_background: bool,
1311) -> serde_json::Value {
1312 let mut properties = serde_json::Map::from_iter([
1313 ("agent".to_string(), task_agent_parameter_schema(agents)),
1314 (
1315 "description".to_string(),
1316 serde_json::json!({
1317 "type": "string",
1318 "description": "Required. Short task label for display and tracking. Always provide this exact field name: 'description'."
1319 }),
1320 ),
1321 (
1322 "prompt".to_string(),
1323 serde_json::json!({
1324 "type": "string",
1325 "description": "Required. Detailed instruction for the delegated child run. Always provide this exact field name: 'prompt'."
1326 }),
1327 ),
1328 (
1329 "max_steps".to_string(),
1330 serde_json::json!({
1331 "type": "integer",
1332 "description": "Optional. Maximum number of steps for this task."
1333 }),
1334 ),
1335 (
1336 "output_schema".to_string(),
1337 serde_json::json!({
1338 "type": "object",
1339 "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."
1340 }),
1341 ),
1342 ]);
1343 if include_background {
1344 properties.insert(
1345 "background".to_string(),
1346 serde_json::json!({
1347 "type": "boolean",
1348 "description": "Optional. Run this task in the background. Only valid when the outer tasks array contains one item. Default: false.",
1349 "default": false
1350 }),
1351 );
1352 }
1353
1354 serde_json::json!({
1355 "type": "object",
1356 "additionalProperties": false,
1357 "properties": properties,
1358 "required": ["agent", "description", "prompt"]
1359 })
1360}
1361
1362pub struct TaskTool {
1365 executor: Arc<TaskExecutor>,
1366}
1367
1368impl TaskTool {
1369 pub fn new(executor: Arc<TaskExecutor>) -> Self {
1371 Self { executor }
1372 }
1373
1374 async fn execute_single(&self, params: TaskParams, ctx: &ToolContext) -> Result<ToolOutput> {
1375 let parent_cancellation = ctx.cancellation_token();
1376 let executor = self.executor.scoped_for_invocation(ctx);
1377
1378 if params.background {
1379 let task_id = executor.execute_background_with_parent_cancellation(
1380 params,
1381 ctx.agent_event_tx.clone(),
1382 ctx.session_id.clone(),
1383 Some(parent_cancellation),
1384 );
1385 return Ok(ToolOutput::success(format!(
1386 "Task started in background. Task ID: {}",
1387 task_id
1388 )));
1389 }
1390
1391 let result = executor
1392 .execute_with_parent_cancellation(
1393 params,
1394 ctx.agent_event_tx.clone(),
1395 ctx.session_id.as_deref(),
1396 Some(&parent_cancellation),
1397 )
1398 .await?;
1399 let (content, truncated) = format_task_result_for_context(&result);
1400 let metadata = serde_json::json!({
1401 "task_id": result.task_id,
1402 "session_id": result.session_id,
1403 "agent": result.agent,
1404 "success": result.success,
1405 "output_bytes": result.output.len(),
1406 "truncated_for_context": truncated,
1407 "artifact_id": task_artifact_id(&result),
1408 "artifact_uri": task_artifact_uri(&result),
1409 "structured": result.structured,
1410 "source_anchors": result.source_anchors,
1411 });
1412
1413 if result.success {
1414 Ok(ToolOutput::success(content).with_metadata(metadata))
1415 } else {
1416 Ok(ToolOutput::error(content).with_metadata(metadata))
1417 }
1418 }
1419}
1420
1421#[async_trait]
1422impl Tool for TaskTool {
1423 fn name(&self) -> &str {
1424 "task"
1425 }
1426
1427 fn description(&self) -> &str {
1428 TASK_TOOL_DESCRIPTION
1429 }
1430
1431 fn parameters(&self) -> serde_json::Value {
1432 task_params_schema_for_agents(&self.executor.visible_agents())
1433 }
1434
1435 fn definition(&self) -> ToolDefinition {
1436 let agents = self.executor.visible_agents();
1437 ToolDefinition {
1438 name: self.name().to_string(),
1439 description: delegation_tool_description(self.description(), &agents),
1440 parameters: task_model_params_schema_for_agents(&agents),
1441 }
1442 }
1443
1444 async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> {
1445 if args.get("tasks").is_some() {
1446 let mut params: ParallelTaskParams = match serde_json::from_value(args.clone()) {
1447 Ok(params) => params,
1448 Err(error) => {
1449 return Ok(invalid_delegation_argument(format!(
1450 "Invalid task parameters: {error}"
1451 )));
1452 }
1453 };
1454 if params.tasks.is_empty() {
1455 return Ok(invalid_delegation_argument(
1456 "task requires at least 1 task".to_string(),
1457 ));
1458 }
1459
1460 let has_fanout_options = params.allow_partial_failure
1461 || params.timeout_ms.is_some()
1462 || params.min_success_count.is_some();
1463 if params.tasks.len() == 1 && !has_fanout_options {
1464 return self.execute_single(params.tasks.remove(0), ctx).await;
1465 }
1466
1467 return ParallelTaskTool::new(Arc::clone(&self.executor))
1468 .execute_params(params, ctx, "task", 1)
1469 .await;
1470 }
1471
1472 let params: TaskParams = match serde_json::from_value(args.clone()) {
1473 Ok(params) => params,
1474 Err(error) => {
1475 return Ok(invalid_delegation_argument(format!(
1476 "Invalid task parameters: {error}"
1477 )));
1478 }
1479 };
1480 self.execute_single(params, ctx).await
1481 }
1482}
1483
1484mod parallel_params;
1485pub use parallel_params::{parallel_task_params_schema, ParallelTaskParams};
1486
1487pub struct ParallelTaskTool {
1491 executor: Arc<TaskExecutor>,
1492}
1493
1494impl ParallelTaskTool {
1495 pub fn new(executor: Arc<TaskExecutor>) -> Self {
1497 Self { executor }
1498 }
1499
1500 async fn execute_params(
1501 &self,
1502 params: ParallelTaskParams,
1503 ctx: &ToolContext,
1504 tool_name: &str,
1505 min_tasks: usize,
1506 ) -> Result<ToolOutput> {
1507 let started_at = std::time::Instant::now();
1508 let parent_cancellation = ctx.cancellation_token();
1509 let executor = self.executor.scoped_for_invocation(ctx);
1510
1511 if params.tasks.len() < min_tasks {
1512 return Ok(invalid_delegation_argument(format!(
1513 "{tool_name} requires at least {min_tasks} task{}",
1514 if min_tasks == 1 { "" } else { "s" }
1515 )));
1516 }
1517 if params.tasks.len() > MAX_PARALLEL_TASKS_PER_CALL {
1518 return Ok(invalid_delegation_argument(format!(
1519 "{tool_name} accepts at most {MAX_PARALLEL_TASKS_PER_CALL} tasks"
1520 )));
1521 }
1522 if let Some((index, _)) = params
1523 .tasks
1524 .iter()
1525 .enumerate()
1526 .find(|(_, task)| task.background)
1527 {
1528 return Ok(invalid_delegation_argument(format!(
1529 "{tool_name} task {} cannot set background=true when fan-out options are used or multiple tasks are submitted; every branch is collected by the parent call",
1530 index + 1
1531 )));
1532 }
1533 if params.timeout_ms == Some(0) {
1534 return Ok(invalid_delegation_argument(format!(
1535 "{tool_name} timeout_ms must be at least 1"
1536 )));
1537 }
1538 if let Some(min_success_count) = params.min_success_count {
1539 if !params.allow_partial_failure {
1540 return Ok(invalid_delegation_argument(format!(
1541 "{tool_name} min_success_count requires allow_partial_failure=true"
1542 )));
1543 }
1544 if min_success_count == 0 || min_success_count > params.tasks.len() {
1545 return Ok(invalid_delegation_argument(format!(
1546 "{tool_name} min_success_count must be between 1 and the task count ({})",
1547 params.tasks.len()
1548 )));
1549 }
1550 }
1551
1552 let task_count = params.tasks.len();
1553 let run = executor
1554 .execute_parallel_for_tool(
1555 params.tasks.clone(),
1556 ctx.agent_event_tx.clone(),
1557 parallel_execution::ParallelToolOptions {
1558 parent_session_id: ctx.session_id.as_deref(),
1559 timeout_ms: params.timeout_ms,
1560 min_success_count: params.min_success_count,
1561 allow_partial_failure: params.allow_partial_failure,
1562 parent_cancellation: Some(&parent_cancellation),
1563 },
1564 )
1565 .await;
1566 let results = run.results;
1567
1568 let mut output = format!("Executed {} tasks concurrently:\n\n", task_count);
1569 let mut metadata_results = Vec::new();
1570 let source_anchor_counts = parallel_source_anchor_counts(&results);
1571 for (i, result) in results.iter().enumerate() {
1572 let status = if result.success { "[OK]" } else { "[ERR]" };
1573 let (formatted, truncated) = format_task_result_for_context(result);
1574 let (output_excerpt, _) = compact_task_output(&result.output);
1575 let source_anchors = &result.source_anchors[..source_anchor_counts[i]];
1576 metadata_results.push(serde_json::json!({
1577 "task_id": result.task_id,
1578 "session_id": result.session_id,
1579 "agent": result.agent,
1580 "success": result.success,
1581 "error_message": (!result.success).then(|| {
1582 crate::text::truncate_utf8(&result.output, 1024).to_string()
1583 }),
1584 "output_excerpt": output_excerpt,
1585 "structured": result.structured,
1586 "source_anchors": source_anchors,
1587 "output_bytes": result.output.len(),
1588 "truncated_for_context": truncated,
1589 "artifact_id": task_artifact_id(result),
1590 "artifact_uri": task_artifact_uri(result),
1591 }));
1592 output.push_str(&format!(
1593 "--- Task {} ({}) {} ---\n{}\n\n",
1594 i + 1,
1595 result.agent,
1596 status,
1597 formatted
1598 ));
1599 }
1600
1601 let success_count = results.iter().filter(|result| result.success).count();
1602 let failed_count = results.len().saturating_sub(success_count);
1603 let all_success = failed_count == 0;
1604 let partial_failure = failed_count > 0 && success_count > 0;
1605 if params.allow_partial_failure && partial_failure {
1606 output.push_str(&format!(
1607 "Partial failure tolerated: {success_count} succeeded, {failed_count} failed.\n"
1608 ));
1609 }
1610 if run.timed_out {
1611 output.push_str(&format!(
1612 "Task fan-out timed out after {} ms; returned completed child results and marked unfinished children failed.\n",
1613 run.timeout_ms.unwrap_or_default()
1614 ));
1615 } else if run.returned_early {
1616 output.push_str(&format!(
1617 "Task fan-out returned after reaching min_success_count={}; unfinished children were marked failed.\n",
1618 run.min_success_count.unwrap_or_default()
1619 ));
1620 }
1621
1622 let tool_success = all_success || (params.allow_partial_failure && success_count > 0);
1623 let mut output = if tool_success {
1624 ToolOutput::success(output)
1625 } else {
1626 ToolOutput::error(output)
1627 };
1628 if !tool_success && failed_count > 0 {
1629 output.error_kind = Some(crate::tools::ToolErrorKind::PartialFailure {
1630 failed: failed_count,
1631 total: results.len(),
1632 });
1633 }
1634
1635 Ok(output.with_metadata(serde_json::json!({
1636 "task_count": task_count,
1637 "result_count": results.len(),
1638 "success_count": success_count,
1639 "failed_count": failed_count,
1640 "all_success": all_success,
1641 "partial_failure": partial_failure,
1642 "allow_partial_failure": params.allow_partial_failure,
1643 "timeout_ms": params.timeout_ms,
1644 "timed_out": run.timed_out,
1645 "min_success_count": params.min_success_count,
1646 "returned_early": run.returned_early,
1647 "duration_ms": started_at.elapsed().as_millis().min(u128::from(u64::MAX)) as u64,
1648 "results": metadata_results,
1649 })))
1650 }
1651}
1652
1653#[async_trait]
1654impl Tool for ParallelTaskTool {
1655 fn name(&self) -> &str {
1656 "parallel_task"
1657 }
1658
1659 fn description(&self) -> &str {
1660 PARALLEL_TASK_TOOL_DESCRIPTION
1661 }
1662
1663 fn parameters(&self) -> serde_json::Value {
1664 parallel_params::parallel_task_params_schema_for_agents(&self.executor.visible_agents())
1665 }
1666
1667 fn definition(&self) -> ToolDefinition {
1668 let agents = self.executor.visible_agents();
1669 ToolDefinition {
1670 name: self.name().to_string(),
1671 description: delegation_tool_description(self.description(), &agents),
1672 parameters: parallel_params::parallel_task_params_schema_for_agents(&agents),
1673 }
1674 }
1675
1676 fn is_model_visible(&self) -> bool {
1677 false
1678 }
1679
1680 async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> {
1681 let params: ParallelTaskParams = match serde_json::from_value(args.clone()) {
1682 Ok(params) => params,
1683 Err(error) => {
1684 return Ok(invalid_delegation_argument(format!(
1685 "Invalid parallel_task parameters: {error}"
1686 )));
1687 }
1688 };
1689 self.execute_params(params, ctx, "parallel_task", 2).await
1690 }
1691}
1692
1693fn invalid_delegation_argument(message: String) -> ToolOutput {
1694 ToolOutput::error(&message)
1695 .with_error_kind(crate::tools::ToolErrorKind::InvalidArgument { message })
1696}
1697
1698#[cfg(test)]
1699mod tests;