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