1use crate::agent::{AgentConfig, AgentEvent, AgentLoop};
18use crate::llm::structured::{
19 generate_blocking_with_cancellation, 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 = "REMOVED from the model-visible registry (`HARNESS-CONV4`). Prefer `task` with multiple `tasks[]` items. This type remains for focused unit tests that construct ParallelTaskTool directly.";
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
164fn provider_quota_for_client(
165 client: &dyn LlmClient,
166) -> Option<crate::task_scheduler::TaskSchedulerQuota> {
167 let pool = client.model_generation_pool()?;
168 crate::task_scheduler::TaskSchedulerQuota::new(
169 pool.identity.clone(),
170 pool.max_concurrency().get(),
171 )
172 .ok()
173}
174
175#[derive(Clone)]
177pub struct TaskExecutor {
178 registry: Arc<AgentRegistry>,
180 llm_client: Arc<dyn LlmClient>,
182 workspace: String,
184 mcp_managers: Vec<Arc<McpManager>>,
186 mcp_bindings: Vec<Arc<McpBinding>>,
188 child_tool_presentation: Option<crate::tools::ToolPresentationProfileV1>,
192 scoped_tools: Vec<Arc<dyn Tool>>,
196 parent_context: Option<crate::child_run::ChildRunContext>,
198 search_config: Option<Arc<crate::config::SearchConfig>>,
200 search_bulkhead: Option<a3s_search::Bulkhead>,
202 search_retry_budget: Option<a3s_search::RetryBudget>,
204 search_request_coalescer: Option<a3s_search::SearchCoalescer>,
206 capability_context: Option<crate::capability::AgentToolCapabilityContext>,
208 parent_cancellation: Option<CancellationToken>,
212 max_parallel_tasks: usize,
213 parallel_permits: Arc<tokio::sync::Semaphore>,
217 subagent_tracker: Option<Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>>,
220 task_scheduler: Option<Arc<crate::task_scheduler::TaskScheduler>>,
222 schedule_foreground: bool,
226 admission_scope: Option<String>,
229 provider_quota: Option<crate::task_scheduler::TaskSchedulerQuota>,
233 provider_admission: Option<crate::llm::ModelGenerationAdmission>,
237}
238
239impl TaskExecutor {
240 pub fn new(
242 registry: Arc<AgentRegistry>,
243 llm_client: Arc<dyn LlmClient>,
244 workspace: String,
245 ) -> Self {
246 let provider_quota = provider_quota_for_client(llm_client.as_ref());
247 Self {
248 registry,
249 llm_client,
250 workspace,
251 mcp_managers: Vec::new(),
252 mcp_bindings: Vec::new(),
253 child_tool_presentation: None,
254 scoped_tools: Vec::new(),
255 parent_context: None,
256 search_config: None,
257 search_bulkhead: None,
258 search_retry_budget: None,
259 search_request_coalescer: None,
260 capability_context: None,
261 parent_cancellation: None,
262 max_parallel_tasks: crate::agent::DEFAULT_MAX_PARALLEL_TASKS,
263 parallel_permits: Arc::new(tokio::sync::Semaphore::new(
264 crate::agent::DEFAULT_MAX_PARALLEL_TASKS,
265 )),
266 subagent_tracker: None,
267 task_scheduler: None,
268 schedule_foreground: false,
269 admission_scope: None,
270 provider_quota,
271 provider_admission: None,
272 }
273 }
274
275 pub fn with_mcp(
277 registry: Arc<AgentRegistry>,
278 llm_client: Arc<dyn LlmClient>,
279 workspace: String,
280 mcp_manager: Arc<McpManager>,
281 ) -> Self {
282 Self::with_mcp_managers(registry, llm_client, workspace, vec![mcp_manager])
283 }
284
285 pub fn with_mcp_managers(
287 registry: Arc<AgentRegistry>,
288 llm_client: Arc<dyn LlmClient>,
289 workspace: String,
290 mcp_managers: Vec<Arc<McpManager>>,
291 ) -> Self {
292 let provider_quota = provider_quota_for_client(llm_client.as_ref());
293 Self {
294 registry,
295 llm_client,
296 workspace,
297 mcp_managers,
298 mcp_bindings: Vec::new(),
299 child_tool_presentation: None,
300 scoped_tools: Vec::new(),
301 parent_context: None,
302 search_config: None,
303 search_bulkhead: None,
304 search_retry_budget: None,
305 search_request_coalescer: None,
306 capability_context: None,
307 parent_cancellation: None,
308 max_parallel_tasks: crate::agent::DEFAULT_MAX_PARALLEL_TASKS,
309 parallel_permits: Arc::new(tokio::sync::Semaphore::new(
310 crate::agent::DEFAULT_MAX_PARALLEL_TASKS,
311 )),
312 subagent_tracker: None,
313 task_scheduler: None,
314 schedule_foreground: false,
315 admission_scope: None,
316 provider_quota,
317 provider_admission: None,
318 }
319 }
320
321 pub(crate) fn with_projected_mcp_bindings(mut self, bindings: Vec<Arc<McpBinding>>) -> Self {
324 self.mcp_bindings = bindings;
325 self
326 }
327
328 #[cfg(test)]
330 pub(crate) fn with_child_tool_presentation(
331 mut self,
332 profile: crate::tools::ToolPresentationProfileV1,
333 ) -> Self {
334 self.child_tool_presentation = Some(profile);
335 self
336 }
337
338 pub fn with_scoped_tools(mut self, tools: Vec<Arc<dyn Tool>>) -> Self {
343 self.scoped_tools = tools;
344 self
345 }
346
347 pub fn with_parent_context(mut self, ctx: crate::child_run::ChildRunContext) -> Self {
349 if let Some(max_parallel_tasks) = ctx.max_parallel_tasks {
350 let max_parallel_tasks = max_parallel_tasks.max(1);
351 self.max_parallel_tasks = max_parallel_tasks;
352 self.parallel_permits = Arc::new(tokio::sync::Semaphore::new(max_parallel_tasks));
353 }
354 self.parent_context = Some(ctx);
355 self
356 }
357
358 fn scoped_for_invocation(self: &Arc<Self>, ctx: &ToolContext) -> Arc<Self> {
359 let mut scoped = self.as_ref().clone();
360 scoped.search_config = ctx.search_config.clone();
361 scoped.search_bulkhead = Some(ctx.search_bulkhead());
362 scoped.search_retry_budget = Some(ctx.search_retry_budget());
363 scoped.search_request_coalescer = Some(ctx.search_request_coalescer());
364 scoped.capability_context = ctx.capability_context();
365 scoped.admission_scope = ctx
366 .run_id()
367 .map(|run_id| format!("run:{run_id}"))
368 .or_else(|| ctx.session_id.as_deref().map(|id| format!("session:{id}")));
369 if ctx.has_run_governance() {
370 scoped.parent_context = scoped.parent_context.take().map(|parent| {
371 parent.with_run_governance(
372 ctx.run_permission_checker(),
373 ctx.run_confirmation_manager(),
374 )
375 });
376 }
377 Arc::new(scoped)
378 }
379
380 fn child_tool_context(
381 &self,
382 session_id: String,
383 cancellation: CancellationToken,
384 ) -> ToolContext {
385 let mut context = ToolContext::new(PathBuf::from(&self.workspace))
386 .with_session_id(session_id)
387 .with_cancellation(cancellation);
388 if let (Some(bulkhead), Some(retry_budget)) =
389 (&self.search_bulkhead, &self.search_retry_budget)
390 {
391 context = context.with_search_runtime(bulkhead.clone(), retry_budget.clone());
392 }
393 if let Some(search_config) = &self.search_config {
394 context = context.with_search_config(search_config.as_ref().clone());
395 }
396 if let Some(coalescer) = &self.search_request_coalescer {
397 context = context.with_search_request_coalescer(coalescer.clone());
398 }
399 context
400 }
401
402 pub fn with_parent_cancellation(mut self, cancellation: CancellationToken) -> Self {
409 self.parent_cancellation = Some(cancellation);
410 self
411 }
412
413 pub fn with_max_parallel_tasks(mut self, max_parallel_tasks: usize) -> Self {
414 let max_parallel_tasks = max_parallel_tasks.max(1);
415 self.max_parallel_tasks = max_parallel_tasks;
416 self.parallel_permits = Arc::new(tokio::sync::Semaphore::new(max_parallel_tasks));
417 self
418 }
419
420 pub fn with_subagent_tracker(
424 mut self,
425 tracker: Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>,
426 ) -> Self {
427 self.subagent_tracker = Some(tracker);
428 self
429 }
430
431 pub fn with_task_scheduler(
433 mut self,
434 scheduler: Arc<crate::task_scheduler::TaskScheduler>,
435 schedule_foreground: bool,
436 ) -> Self {
437 self.provider_admission = self.llm_client.model_generation_pool().and_then(|pool| {
442 crate::llm::ModelGenerationAdmission::new(
443 self.llm_client.model_generation_concurrency(),
444 )
445 .with_model_generation_pool(
446 Arc::clone(&scheduler),
447 pool,
448 crate::task_scheduler::TaskPriority::Foreground,
449 "task-child-model-generation",
450 )
451 .ok()
452 });
453 self.task_scheduler = Some(scheduler);
454 self.schedule_foreground = schedule_foreground;
455 self
456 }
457
458 #[cfg(test)]
459 pub(crate) fn has_provider_model_generation_admission(&self) -> bool {
460 self.provider_admission.is_some()
461 }
462
463 #[cfg(test)]
464 pub(crate) fn provider_admission_publishes_typed_pool(&self) -> bool {
465 self.provider_admission
466 .as_ref()
467 .is_some_and(|admission| admission.publishes_model_generation_pool())
468 }
469
470 fn visible_agents(&self) -> Vec<AgentDefinition> {
471 self.registry.list_visible()
472 }
473
474 pub async fn execute(
479 &self,
480 params: TaskParams,
481 event_tx: Option<broadcast::Sender<AgentEvent>>,
482 parent_session_id: Option<&str>,
483 ) -> Result<TaskResult> {
484 self.execute_with_parent_cancellation(
485 params,
486 event_tx,
487 parent_session_id,
488 self.parent_cancellation.as_ref(),
489 )
490 .await
491 }
492
493 async fn execute_with_parent_cancellation(
494 &self,
495 params: TaskParams,
496 event_tx: Option<broadcast::Sender<AgentEvent>>,
497 parent_session_id: Option<&str>,
498 parent_cancellation: Option<&CancellationToken>,
499 ) -> Result<TaskResult> {
500 let task_id = format!("task-{}", uuid::Uuid::new_v4());
501 self.execute_with_task_id_scoped(
502 task_id,
503 params,
504 ScopedTaskExecution {
505 event_tx,
506 parent_session_id,
507 emit_start: true,
508 parent_cancellation,
509 admitted_capability_subtask: None,
510 parallel_lifecycle: None,
511 },
512 )
513 .await
514 }
515
516 pub async fn execute_with_task_id(
521 &self,
522 task_id: String,
523 params: TaskParams,
524 event_tx: Option<broadcast::Sender<AgentEvent>>,
525 parent_session_id: Option<&str>,
526 emit_start: bool,
527 ) -> Result<TaskResult> {
528 self.execute_with_task_id_scoped(
529 task_id,
530 params,
531 ScopedTaskExecution {
532 event_tx,
533 parent_session_id,
534 emit_start,
535 parent_cancellation: self.parent_cancellation.as_ref(),
536 admitted_capability_subtask: None,
537 parallel_lifecycle: None,
538 },
539 )
540 .await
541 }
542
543 async fn execute_with_task_id_scoped(
544 &self,
545 task_id: String,
546 params: TaskParams,
547 execution: ScopedTaskExecution<'_>,
548 ) -> Result<TaskResult> {
549 let ScopedTaskExecution {
550 event_tx,
551 parent_session_id,
552 emit_start,
553 parent_cancellation,
554 admitted_capability_subtask,
555 parallel_lifecycle,
556 } = execution;
557 let was_promoted = admitted_capability_subtask.is_some();
558 if !was_promoted && parent_cancellation.is_some_and(CancellationToken::is_cancelled) {
559 anyhow::bail!("Operation cancelled by parent session");
560 }
561
562 let capability_subtask = match admitted_capability_subtask {
563 Some(subtask) => Some(subtask),
564 None => self
565 .capability_context
566 .as_ref()
567 .map(|context| context.admit_subtask(task_id.clone(), params.background))
568 .transpose()?,
569 };
570 let cancel_token = capability_subtask.as_ref().map_or_else(
571 || {
572 parent_cancellation
573 .map(CancellationToken::child_token)
574 .unwrap_or_default()
575 },
576 crate::capability::AgentCapabilitySubtask::cancellation,
577 );
578 let capability_runtime = capability_subtask
579 .as_ref()
580 .map(crate::capability::AgentCapabilitySubtask::runtime);
581 let execution = self
582 .execute_with_task_id_in_scope(
583 task_id,
584 params,
585 event_tx,
586 parent_session_id,
587 emit_start,
588 cancel_token,
589 capability_runtime,
590 parallel_lifecycle,
591 )
592 .await;
593 let close = close_capability_subtask(capability_subtask.as_ref()).await;
594 match (execution, close) {
595 (Ok(result), Ok(())) => Ok(result),
596 (Ok(_), Err(close_error)) => Err(close_error),
597 (Err(error), Ok(())) => Err(error),
598 (Err(error), Err(close_error)) => {
599 tracing::warn!(
600 error = %close_error,
601 "Capability Subtask close also failed after delegated execution failure"
602 );
603 Err(error)
604 }
605 }
606 }
607
608 #[allow(clippy::too_many_arguments)]
609 async fn execute_with_task_id_in_scope(
610 &self,
611 task_id: String,
612 params: TaskParams,
613 event_tx: Option<broadcast::Sender<AgentEvent>>,
614 parent_session_id: Option<&str>,
615 emit_start: bool,
616 cancel_token: CancellationToken,
617 capability_runtime: Option<crate::capability::AgentCapabilityRuntime>,
618 parallel_lifecycle: Option<Arc<ParallelTaskLifecycle>>,
619 ) -> Result<TaskResult> {
620 if params.background {
624 if let Some(ref tracker) = self.subagent_tracker {
625 tracker
626 .register_canceller(&task_id, cancel_token.clone())
627 .await;
628 }
629 }
630 let execution_identity =
631 if (params.background || self.schedule_foreground) && self.task_scheduler.is_some() {
632 let mut identity_spec = AgentStepSpec::new(
633 task_id.clone(),
634 params.agent.clone(),
635 params.description.clone(),
636 params.prompt.clone(),
637 );
638 if let Some(max_steps) = params.max_steps {
639 identity_spec = identity_spec.with_max_steps(max_steps);
640 }
641 if let Some(output_schema) = params.output_schema.clone() {
642 identity_spec = identity_spec.with_output_schema(output_schema);
643 }
644 if let Some(parent_session_id) = parent_session_id {
645 identity_spec = identity_spec.with_parent_session_id(parent_session_id);
646 }
647 Some(
648 crate::orchestration::workflow_step_execution_identity(
649 parent_session_id.unwrap_or("host"),
650 &identity_spec,
651 )
652 .map_err(|error| {
653 anyhow::anyhow!("derive delegated task execution identity: {error}")
654 })?,
655 )
656 } else {
657 None
658 };
659 let admission_quota =
660 if (params.background || self.schedule_foreground) && self.task_scheduler.is_some() {
661 let scope = self.admission_scope.clone().unwrap_or_else(|| {
662 parent_session_id
663 .map(|session_id| format!("session:{session_id}"))
664 .unwrap_or_else(|| "host".to_string())
665 });
666 Some(
667 crate::task_scheduler::TaskSchedulerQuota::for_scope(
668 &scope,
669 self.max_parallel_tasks,
670 )
671 .map_err(|error| anyhow::anyhow!(error))?,
672 )
673 } else {
674 None
675 };
676 let _task_lease = if params.background || self.schedule_foreground {
677 match &self.task_scheduler {
678 Some(scheduler) => {
679 let mut quotas = Vec::with_capacity(2);
680 if let Some(quota) = admission_quota.as_ref() {
681 quotas.push(quota.clone());
682 }
683 if let Some(quota) = self.provider_quota.as_ref() {
684 if !quotas
685 .iter()
686 .any(|candidate| candidate.identity == quota.identity)
687 {
688 quotas.push(quota.clone());
689 }
690 }
691 let priority = if params.background {
692 crate::task_scheduler::TaskPriority::Background
693 } else {
694 crate::task_scheduler::TaskPriority::Foreground
695 };
696 let label = format!(
697 "{}:subagent:{}",
698 parent_session_id.unwrap_or("host"),
699 task_id
700 );
701 Some(if quotas.is_empty() {
702 scheduler
703 .acquire_with_identity(
704 priority,
705 label,
706 execution_identity.clone(),
707 &cancel_token,
708 )
709 .await
710 .map_err(|error| anyhow::anyhow!(error))?
711 } else {
712 scheduler
713 .acquire_with_quotas(
714 priority,
715 label,
716 "as,
717 execution_identity.clone(),
718 &cancel_token,
719 )
720 .await
721 .map_err(|error| anyhow::anyhow!(error))?
722 })
723 }
724 None => None,
725 }
726 } else {
727 None
728 };
729
730 let session_id = format!("task-run-{}", task_id);
731 let started_ms = epoch_ms();
732 let output_schema = params.output_schema.clone();
733
734 let agent = self
735 .registry
736 .get_arc(¶ms.agent)
737 .context(format!("Unknown agent type: '{}'", params.agent))?;
738 let tool_free = agent.tool_free;
739 let tool_free_system = agent.prompt.clone();
740 let inherited_security_provider = self
741 .parent_context
742 .as_ref()
743 .and_then(|context| context.security_provider.clone());
744
745 if emit_start {
746 let event = AgentEvent::SubagentStart {
747 task_id: task_id.clone(),
748 session_id: session_id.clone(),
749 parent_session_id: parent_session_id.unwrap_or_default().to_string(),
750 agent: params.agent.clone(),
751 description: params.description.clone(),
752 started_ms,
753 };
754 let event = inherited_security_provider
755 .as_deref()
756 .map(|provider| crate::security::sanitize_agent_event(provider, &event))
757 .unwrap_or(event);
758 if let Some(ref tracker) = self.subagent_tracker {
759 tracker.record_event(&event).await;
760 }
761 if let Some(ref tx) = event_tx {
762 let _ = tx.send(event);
763 }
764 if let Some(lifecycle) = ¶llel_lifecycle {
765 lifecycle.mark_started(&task_id);
769 }
770 }
771
772 let child_executor = if let Some(ref parent_ctx) = self.parent_context {
775 if let Some(ref services) = parent_ctx.workspace_services {
776 crate::tools::ToolExecutor::new_with_workspace_services_artifact_limits_and_immutable_content_adapter(
777 self.workspace.clone(),
778 Arc::clone(services),
779 crate::tools::ArtifactStoreLimits::default(),
780 parent_ctx.immutable_content_adapter.clone(),
781 )
782 } else if let Some(adapter) = parent_ctx.immutable_content_adapter.clone() {
783 crate::tools::ToolExecutor::new_with_immutable_content_adapter(
784 self.workspace.clone(),
785 adapter,
786 )
787 } else {
788 crate::tools::ToolExecutor::new(self.workspace.clone())
789 }
790 } else {
791 crate::tools::ToolExecutor::new(self.workspace.clone())
792 };
793
794 if self.mcp_bindings.is_empty() {
804 for mcp in &self.mcp_managers {
805 let all_tools = tokio::select! {
806 biased;
807 _ = cancel_token.cancelled() => {
808 anyhow::bail!("Operation cancelled before child execution");
809 }
810 tools = mcp.get_all_tools() => tools,
811 };
812 let mut by_server: std::collections::HashMap<
813 String,
814 Vec<crate::mcp::protocol::McpTool>,
815 > = std::collections::HashMap::new();
816 for (server, tool) in all_tools {
817 by_server.entry(server).or_default().push(tool);
818 }
819 for (server_name, tools) in by_server {
820 let wrappers =
821 crate::mcp::tools::create_mcp_tools(&server_name, tools, Arc::clone(mcp));
822 for wrapper in wrappers {
823 child_executor.register_dynamic_tool(wrapper);
824 }
825 }
826 }
827 }
828 for binding in &self.mcp_bindings {
830 if cancel_token.is_cancelled() {
831 anyhow::bail!("Operation cancelled before child execution");
832 }
833 for wrapper in binding.projected_tools() {
834 child_executor.register_dynamic_tool(wrapper);
835 }
836 }
837
838 for tool in &self.scoped_tools {
842 if !child_executor.register_dynamic_tool_if_absent(Arc::clone(tool)) {
843 anyhow::bail!(
844 "Workflow-scoped tool '{}' conflicts with another child capability",
845 tool.name()
846 );
847 }
848 }
849
850 let child_executor = Arc::new(child_executor);
851
852 let mut child_config = AgentConfig {
853 tools: child_executor.definitions(),
854 ..AgentConfig::default()
855 };
856 agent.apply_to(&mut child_config);
857 if let Some(ref parent_ctx) = self.parent_context {
858 parent_ctx.apply_to(&mut child_config);
859 }
860 if let Some(profile) = self.child_tool_presentation.clone() {
861 child_config.tool_presentation_profile = profile;
862 }
863 child_config.planning_mode = crate::prompts::PlanningMode::Disabled;
868 if let Some(max_steps) = params.max_steps {
869 child_config.max_tool_rounds = max_steps;
870 }
871 let child_security_provider = child_config.security_provider.clone();
872 let source_security_provider = child_security_provider.clone();
873
874 let mut tool_context = self.child_tool_context(session_id.clone(), cancel_token.clone());
875 if let Some(ref parent_ctx) = self.parent_context {
876 if let Some(ref services) = parent_ctx.workspace_services {
877 tool_context = tool_context.with_workspace_services(Arc::clone(services));
878 }
879 if let Some(ref sandbox) = parent_ctx.sandbox_handle {
880 child_executor.registry().set_sandbox(Arc::clone(sandbox));
881 tool_context = tool_context.with_sandbox(Arc::clone(sandbox));
882 }
883 }
884
885 let source_context = tool_context.clone();
886 let mut agent_loop = AgentLoop::new(
887 Arc::clone(&self.llm_client),
888 child_executor,
889 tool_context,
890 child_config,
891 );
892 if !params.background && !self.schedule_foreground {
893 if let Some(admission) = &self.provider_admission {
894 agent_loop = agent_loop.with_model_generation_admission(admission.clone());
895 }
896 }
897 if let Some(runtime) = capability_runtime {
898 agent_loop = agent_loop.with_capability_runtime(runtime);
899 }
900
901 let (mpsc_tx, mut mpsc_rx) = tokio::sync::mpsc::channel(100);
905 let broadcast_tx = event_tx.clone();
906 let progress_task_id = task_id.clone();
907 let progress_session_id = session_id.clone();
908 let child_event_forwarder = tokio::spawn(async move {
909 let mut source_anchors = Vec::new();
910 let mut seen_source_anchors = std::collections::HashSet::new();
911 let mut scanned_source_candidates = 0usize;
912 while let Some(event) = mpsc_rx.recv().await {
913 let event = source_security_provider
914 .as_deref()
915 .map(|provider| crate::security::sanitize_agent_event(provider, &event))
916 .unwrap_or(event);
917 collect_tool_source_anchors(
918 &event,
919 &source_context,
920 &mut source_anchors,
921 &mut seen_source_anchors,
922 &mut scanned_source_candidates,
923 );
924 if let Some(ref broadcast_tx) = broadcast_tx {
925 if let Some(progress) = synthesize_subagent_progress(
926 &event,
927 &progress_task_id,
928 &progress_session_id,
929 ) {
930 let _ = broadcast_tx.send(progress);
931 }
932 let _ = broadcast_tx.send(event);
933 }
934 }
935 source_anchors
936 });
937 let child_event_tx = Some(mpsc_tx);
938 let child_llm_event_tx = child_event_tx.clone();
939
940 if !params.background {
943 if let Some(ref tracker) = self.subagent_tracker {
944 tracker
945 .register_canceller(&task_id, cancel_token.clone())
946 .await;
947 }
948 }
949
950 let structured_prompt = output_schema
951 .as_ref()
952 .filter(|_| !tool_free)
953 .map(|schema| structured_task_prompt(¶ms.prompt, schema));
954 let execution_prompt = structured_prompt.as_deref().unwrap_or(¶ms.prompt);
955
956 let mut structured = None;
957 let (mut output, mut success, raw_output) = if tool_free && output_schema.is_some() {
958 let operation = agent_loop.begin_capability_operation(
959 0,
960 &cancel_token,
961 "structured task generation",
962 )?;
963 let llm_client = agent_loop.scoped_llm_client_for_parts(
964 Some(&session_id),
965 &child_llm_event_tx,
966 operation.cancellation(),
967 );
968 let generation = Self::generate_structured_task(
969 &*llm_client,
970 ¶ms.prompt,
971 tool_free_system.as_deref(),
972 output_schema.clone().expect("schema checked above"),
973 operation.cancellation(),
974 )
975 .await;
976 let generation = settle_task_capability_operation(
977 generation,
978 operation.close().await,
979 "structured task generation",
980 );
981 match generation {
982 Ok(object) => {
983 let output = serde_json::to_string_pretty(&object)
984 .unwrap_or_else(|_| object.to_string());
985 structured = Some(object);
986 (output, true, None)
987 }
988 Err(error) if cancel_token.is_cancelled() => {
989 (format!("Task cancelled by caller: {error}"), false, None)
990 }
991 Err(error) => (format!("Task failed: {error}"), false, None),
992 }
993 } else {
994 match agent_loop
995 .execute_with_session(
996 &[],
997 execution_prompt,
998 Some(&session_id),
999 child_event_tx.clone(),
1000 Some(&cancel_token),
1001 )
1002 .await
1003 {
1004 Ok(_) if cancel_token.is_cancelled() => {
1005 ("Task cancelled by caller".to_string(), false, None)
1006 }
1007 Ok(result) if result.text.trim().is_empty() => (
1008 "Task failed: child agent returned no final output".to_string(),
1009 false,
1010 None,
1011 ),
1012 Ok(result) if AgentLoop::is_synthetic_failure_output(&result.text) => {
1013 (format!("Task failed: {}", result.text), false, None)
1014 }
1015 Ok(result) => {
1016 let raw_output = result
1017 .messages
1018 .last()
1019 .filter(|message| message.role == "assistant")
1020 .map(crate::llm::Message::text)
1021 .filter(|text| !text.trim().is_empty());
1022 (result.text, true, raw_output)
1023 }
1024 Err(e) if cancel_token.is_cancelled() => {
1025 (format!("Task cancelled by caller: {}", e), false, None)
1026 }
1027 Err(e) => (format!("Task failed: {}", e), false, None),
1028 }
1029 };
1030
1031 if success && !tool_free {
1032 if let Some(schema) = output_schema.as_ref() {
1033 if let Some(object) = raw_output
1034 .as_deref()
1035 .and_then(|raw| parse_validated_output(raw, schema))
1036 .or_else(|| parse_validated_output(&output, schema))
1037 {
1038 structured = Some(object);
1039 } else {
1040 let operation = agent_loop.begin_capability_operation(
1041 0,
1042 &cancel_token,
1043 "structured task coercion",
1044 )?;
1045 let llm_client = agent_loop.scoped_llm_client_for_parts(
1046 Some(&session_id),
1047 &child_llm_event_tx,
1048 operation.cancellation(),
1049 );
1050 let coercion = Self::coerce_to_schema(
1051 &*llm_client,
1052 &output,
1053 schema.clone(),
1054 operation.cancellation(),
1055 )
1056 .await;
1057 let coercion = settle_task_capability_operation(
1058 coercion,
1059 operation.close().await,
1060 "structured task coercion",
1061 );
1062 match coercion {
1063 Ok(object) => structured = Some(object),
1064 Err(error) => {
1065 success = false;
1066 output = format!("{output}\n\n[structured output failed: {error}]");
1067 }
1068 }
1069 }
1070 }
1071 }
1072 if let Some(provider) = child_security_provider.as_deref() {
1073 output = crate::security::sanitize_text(provider, &output);
1074 if let Some(value) = structured.take() {
1075 let sanitized = output_schema.as_ref().map_or_else(
1076 || sanitize_task_json(provider, &value),
1077 |schema| sanitize_task_json_with_schema(provider, &value, schema),
1078 );
1079 if output_schema
1080 .as_ref()
1081 .is_none_or(|schema| value_matches_schema(&sanitized, schema))
1082 {
1083 structured = Some(sanitized);
1084 } else {
1085 success = false;
1086 }
1087 }
1088 }
1089
1090 drop(child_event_tx);
1095 drop(child_llm_event_tx);
1096 let source_anchors = match child_event_forwarder.await {
1097 Ok(source_anchors) => source_anchors,
1098 Err(error) => {
1099 tracing::warn!(%error, task_id = %task_id, "subagent event bridge failed");
1100 Vec::new()
1101 }
1102 };
1103
1104 let end_event = AgentEvent::SubagentEnd {
1105 task_id: task_id.clone(),
1106 session_id: session_id.clone(),
1107 agent: params.agent.clone(),
1108 output: output.clone(),
1109 success,
1110 finished_ms: epoch_ms(),
1111 };
1112 if let Some(ref tracker) = self.subagent_tracker {
1113 if success {
1116 tracker
1117 .record_source_anchors(&task_id, &source_anchors)
1118 .await;
1119 }
1120 tracker.record_event(&end_event).await;
1121 tracker.clear_canceller(&task_id).await;
1122 }
1123 if let Some(ref tx) = event_tx {
1124 let _ = tx.send(end_event);
1125 }
1126 if let Some(lifecycle) = ¶llel_lifecycle {
1127 lifecycle.mark_ended(&task_id);
1131 }
1132
1133 Ok(TaskResult {
1134 output,
1135 session_id,
1136 agent: params.agent,
1137 success,
1138 task_id,
1139 structured,
1140 source_anchors,
1141 })
1142 }
1143
1144 pub fn execute_background(
1152 self: Arc<Self>,
1153 params: TaskParams,
1154 event_tx: Option<broadcast::Sender<AgentEvent>>,
1155 parent_session_id: Option<String>,
1156 ) -> String {
1157 let parent_cancellation = self.parent_cancellation.clone();
1158 self.execute_background_with_parent_cancellation(
1159 params,
1160 event_tx,
1161 parent_session_id,
1162 parent_cancellation,
1163 )
1164 }
1165
1166 fn execute_background_with_parent_cancellation(
1167 self: Arc<Self>,
1168 params: TaskParams,
1169 event_tx: Option<broadcast::Sender<AgentEvent>>,
1170 parent_session_id: Option<String>,
1171 parent_cancellation: Option<CancellationToken>,
1172 ) -> String {
1173 let task_id = format!("task-{}", uuid::Uuid::new_v4());
1174 let session_id = format!("task-run-{}", task_id);
1175 let failure_session_id = session_id.clone();
1176 let failure_agent = params.agent.clone();
1177 let start_event = AgentEvent::SubagentStart {
1178 task_id: task_id.clone(),
1179 session_id,
1180 parent_session_id: parent_session_id.clone().unwrap_or_default(),
1181 agent: params.agent.clone(),
1182 description: params.description.clone(),
1183 started_ms: epoch_ms(),
1184 };
1185 let security_provider = self
1186 .parent_context
1187 .as_ref()
1188 .and_then(|context| context.security_provider.clone());
1189 let start_event = security_provider
1190 .as_deref()
1191 .map(|provider| crate::security::sanitize_agent_event(provider, &start_event))
1192 .unwrap_or(start_event);
1193
1194 if let Some(ref tx) = event_tx {
1195 let _ = tx.send(start_event.clone());
1196 }
1197
1198 let capability_admission = self
1199 .capability_context
1200 .as_ref()
1201 .map(|context| {
1202 context
1203 .admit_subtask(task_id.clone(), true)
1204 .map(|subtask| (context.background_scope().clone(), subtask))
1205 })
1206 .transpose();
1207 let (capability_run, admitted_capability_subtask) = match capability_admission {
1208 Ok(Some((run, subtask))) => (Some(run), Some(subtask)),
1209 Ok(None) => (None, None),
1210 Err(error) => {
1211 let message = format!("Background task capability admission failed: {error}");
1212 let end_event = AgentEvent::SubagentEnd {
1213 task_id: task_id.clone(),
1214 session_id: failure_session_id,
1215 agent: failure_agent,
1216 output: message.clone(),
1217 success: false,
1218 finished_ms: epoch_ms(),
1219 };
1220 let end_event = security_provider
1221 .as_deref()
1222 .map(|provider| crate::security::sanitize_agent_event(provider, &end_event))
1223 .unwrap_or(end_event);
1224 if let Some(tx) = event_tx {
1225 let _ = tx.send(end_event);
1226 }
1227 tracing::error!(task_id = %task_id, "{message}");
1228 return task_id;
1229 }
1230 };
1231
1232 let task_id_for_spawn = task_id.clone();
1233 let task_id_for_log = task_id.clone();
1234 let admission_failure_task_id = task_id.clone();
1235 let admission_failure_session_id = failure_session_id.clone();
1236 let admission_failure_agent = failure_agent.clone();
1237 let admission_failure_events = event_tx.clone();
1238 let admission_failure_security = security_provider.clone();
1239 let background = async move {
1240 if let Some(ref tracker) = self.subagent_tracker {
1241 tracker.record_event(&start_event).await;
1242 }
1243 let failure_event_tx = event_tx.clone();
1244 if let Err(error) = self
1245 .execute_with_task_id_scoped(
1246 task_id_for_spawn,
1247 params,
1248 ScopedTaskExecution {
1249 event_tx,
1250 parent_session_id: parent_session_id.as_deref(),
1251 emit_start: false,
1252 parent_cancellation: parent_cancellation.as_ref(),
1253 admitted_capability_subtask,
1254 parallel_lifecycle: None,
1255 },
1256 )
1257 .await
1258 {
1259 let end_event = AgentEvent::SubagentEnd {
1260 task_id: task_id_for_log.clone(),
1261 session_id: failure_session_id,
1262 agent: failure_agent,
1263 output: format!("Task failed before child execution started: {error}"),
1264 success: false,
1265 finished_ms: epoch_ms(),
1266 };
1267 let end_event = security_provider
1268 .as_deref()
1269 .map(|provider| crate::security::sanitize_agent_event(provider, &end_event))
1270 .unwrap_or(end_event);
1271 if let Some(ref tracker) = self.subagent_tracker {
1272 tracker.record_event(&end_event).await;
1273 tracker.clear_canceller(&task_id_for_log).await;
1274 }
1275 if let Some(tx) = failure_event_tx {
1276 let _ = tx.send(end_event);
1277 }
1278 tracing::error!("Background task {} failed: {}", task_id_for_log, error);
1279 }
1280 };
1281 if let Some(run) = capability_run {
1282 let task_name = format!("subagent.{task_id}");
1283 if let Err(error) = run.spawn_task(task_name, async move {
1284 background.await;
1285 Ok(())
1286 }) {
1287 let message = format!("Background task capability admission failed: {error}");
1291 let end_event = AgentEvent::SubagentEnd {
1292 task_id: admission_failure_task_id.clone(),
1293 session_id: admission_failure_session_id,
1294 agent: admission_failure_agent,
1295 output: message.clone(),
1296 success: false,
1297 finished_ms: epoch_ms(),
1298 };
1299 let end_event = admission_failure_security
1300 .as_deref()
1301 .map(|provider| crate::security::sanitize_agent_event(provider, &end_event))
1302 .unwrap_or(end_event);
1303 if let Some(tx) = admission_failure_events {
1304 let _ = tx.send(end_event);
1305 }
1306 tracing::error!(task_id = %admission_failure_task_id, "{message}");
1307 }
1308 } else {
1309 tokio::spawn(background);
1310 }
1311
1312 task_id
1313 }
1314}
1315
1316async fn close_capability_subtask(
1317 subtask: Option<&crate::capability::AgentCapabilitySubtask>,
1318) -> Result<()> {
1319 let Some(subtask) = subtask else {
1320 return Ok(());
1321 };
1322 let report = subtask.close().await?;
1323 if !report.is_clean() {
1324 anyhow::bail!(
1325 "Capability Subtask close was incomplete (tasks failed: {}, tasks timed out: {}, child scopes failed: {}, child scopes timed out: {}, effects failed: {}, effects timed out: {})",
1326 report.tasks_failed,
1327 report.tasks_timed_out,
1328 report.child_scopes_failed,
1329 report.child_scopes_timed_out,
1330 report.effects_failed,
1331 report.effects_timed_out,
1332 );
1333 }
1334 Ok(())
1335}
1336
1337fn settle_task_capability_operation<T>(
1338 execution: Result<T>,
1339 close: Result<()>,
1340 label: &str,
1341) -> Result<T> {
1342 match (execution, close) {
1343 (Ok(result), Ok(())) => Ok(result),
1344 (Ok(_), Err(close_error)) => Err(close_error),
1345 (Err(error), Ok(())) => Err(error),
1346 (Err(error), Err(close_error)) => {
1347 tracing::warn!(
1348 error = %close_error,
1349 operation = label,
1350 "Capability orchestration Turn close also failed after model failure"
1351 );
1352 Err(error)
1353 }
1354 }
1355}
1356
1357fn structured_task_prompt(prompt: &str, schema: &serde_json::Value) -> String {
1358 let schema = serde_json::to_string_pretty(schema).unwrap_or_else(|_| schema.to_string());
1359 format!(
1360 "{prompt}\n\n\
1361 FINAL OUTPUT CONTRACT\n\
1362 Complete the requested investigation before answering. Your final response must contain \
1363 exactly one JSON value matching the JSON Schema below, with no Markdown fence or prose \
1364 outside the JSON. This contract applies to the final response only; use the available \
1365 tools as needed before finalizing.\n\n\
1366 {schema}"
1367 )
1368}
1369
1370fn value_matches_schema(value: &serde_json::Value, schema: &serde_json::Value) -> bool {
1371 serde_json::to_string(value)
1372 .ok()
1373 .and_then(|encoded| parse_validated_output(&encoded, schema))
1374 .is_some()
1375}
1376
1377#[derive(Debug, Clone)]
1378struct AgentCatalogEntry {
1379 name: String,
1380 description: String,
1381}
1382
1383fn agent_catalog_entries(agents: &[AgentDefinition]) -> Vec<AgentCatalogEntry> {
1384 let mut entries = agents
1385 .iter()
1386 .map(|agent| AgentCatalogEntry {
1387 name: agent.name.clone(),
1388 description: agent
1389 .description
1390 .split_whitespace()
1391 .collect::<Vec<_>>()
1392 .join(" "),
1393 })
1394 .collect::<Vec<_>>();
1395 entries.sort_by(|left, right| left.name.cmp(&right.name));
1396 entries
1397}
1398
1399fn agent_catalog_text(agents: &[AgentDefinition]) -> String {
1400 agent_catalog_entries(agents)
1401 .into_iter()
1402 .map(|entry| format!("{}: {}", entry.name, entry.description))
1403 .collect::<Vec<_>>()
1404 .join("\n")
1405}
1406
1407fn delegation_tool_description(base: &str, agents: &[AgentDefinition]) -> String {
1408 format!(
1409 "{base}\n\nAvailable agents (live catalog; use canonical names):\n{}",
1410 agent_catalog_text(agents)
1411 )
1412}
1413
1414pub(super) fn task_agent_parameter_schema(agents: &[AgentDefinition]) -> serde_json::Value {
1415 let entries = agent_catalog_entries(agents);
1416 let examples = entries
1417 .iter()
1418 .map(|entry| serde_json::Value::String(entry.name.clone()))
1419 .collect::<Vec<_>>();
1420 let catalog = entries
1421 .into_iter()
1422 .map(|entry| format!("{}: {}", entry.name, entry.description))
1423 .collect::<Vec<_>>()
1424 .join("\n");
1425 serde_json::json!({
1426 "type": "string",
1427 "description": format!(
1428 "Required. Canonical agent type to use. Always provide this exact field name: 'agent'. Live agent catalog:\n{catalog}"
1429 ),
1430 "examples": examples
1431 })
1432}
1433
1434pub fn task_params_schema() -> serde_json::Value {
1440 task_params_schema_for_agents(&AgentRegistry::new().list_visible())
1441}
1442
1443fn task_params_schema_for_agents(agents: &[AgentDefinition]) -> serde_json::Value {
1444 serde_json::json!({
1445 "oneOf": [
1446 legacy_task_params_schema_for_agents(agents),
1447 task_model_params_schema_for_agents(agents)
1448 ]
1449 })
1450}
1451
1452fn legacy_task_params_schema_for_agents(agents: &[AgentDefinition]) -> serde_json::Value {
1453 let mut schema = task_item_params_schema_for_agents(agents, true);
1454 schema["examples"] = serde_json::json!([
1455 {
1456 "agent": "explore",
1457 "description": "Find Rust files",
1458 "prompt": "Search the workspace for Rust files and summarize the layout."
1459 },
1460 {
1461 "agent": "general",
1462 "description": "Investigate test failure",
1463 "prompt": "Inspect the failing tests and explain the root cause.",
1464 "max_steps": 6
1465 }
1466 ]);
1467 schema
1468}
1469
1470fn task_model_params_schema_for_agents(agents: &[AgentDefinition]) -> serde_json::Value {
1471 parallel_params::task_tool_params_schema_for_agents(agents)
1472}
1473
1474pub(super) fn task_item_params_schema_for_agents(
1475 agents: &[AgentDefinition],
1476 include_background: bool,
1477) -> serde_json::Value {
1478 let mut properties = serde_json::Map::from_iter([
1479 ("agent".to_string(), task_agent_parameter_schema(agents)),
1480 (
1481 "description".to_string(),
1482 serde_json::json!({
1483 "type": "string",
1484 "description": "Required. Short task label for display and tracking. Always provide this exact field name: 'description'."
1485 }),
1486 ),
1487 (
1488 "prompt".to_string(),
1489 serde_json::json!({
1490 "type": "string",
1491 "description": "Required. Detailed instruction for the delegated child run. Always provide this exact field name: 'prompt'."
1492 }),
1493 ),
1494 (
1495 "max_steps".to_string(),
1496 serde_json::json!({
1497 "type": "integer",
1498 "description": "Optional. Maximum number of steps for this task."
1499 }),
1500 ),
1501 (
1502 "output_schema".to_string(),
1503 serde_json::json!({
1504 "type": "object",
1505 "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."
1506 }),
1507 ),
1508 ]);
1509 if include_background {
1510 properties.insert(
1511 "background".to_string(),
1512 serde_json::json!({
1513 "type": "boolean",
1514 "description": "Optional. Run this task in the background. Only valid when the outer tasks array contains one item. Default: false.",
1515 "default": false
1516 }),
1517 );
1518 }
1519
1520 serde_json::json!({
1521 "type": "object",
1522 "additionalProperties": false,
1523 "properties": properties,
1524 "required": ["agent", "description", "prompt"]
1525 })
1526}
1527
1528pub struct TaskTool {
1531 executor: Arc<TaskExecutor>,
1532}
1533
1534impl TaskTool {
1535 pub fn new(executor: Arc<TaskExecutor>) -> Self {
1537 Self { executor }
1538 }
1539
1540 async fn execute_single(&self, params: TaskParams, ctx: &ToolContext) -> Result<ToolOutput> {
1541 let parent_cancellation = ctx.cancellation_token();
1542 let executor = self.executor.scoped_for_invocation(ctx);
1543
1544 if params.background {
1545 let task_id = executor.execute_background_with_parent_cancellation(
1546 params,
1547 ctx.agent_event_tx.clone(),
1548 ctx.session_id.clone(),
1549 Some(parent_cancellation),
1550 );
1551 return Ok(ToolOutput::success(format!(
1552 "Task started in background. Task ID: {}",
1553 task_id
1554 )));
1555 }
1556
1557 let result = executor
1558 .execute_with_parent_cancellation(
1559 params,
1560 ctx.agent_event_tx.clone(),
1561 ctx.session_id.as_deref(),
1562 Some(&parent_cancellation),
1563 )
1564 .await?;
1565 let (content, truncated) = format_task_result_for_context(&result);
1566 let metadata = serde_json::json!({
1567 "task_id": result.task_id,
1568 "session_id": result.session_id,
1569 "agent": result.agent,
1570 "success": result.success,
1571 "output_bytes": result.output.len(),
1572 "truncated_for_context": truncated,
1573 "artifact_id": task_artifact_id(&result),
1574 "artifact_uri": task_artifact_uri(&result),
1575 "structured": result.structured,
1576 "source_anchors": result.source_anchors,
1577 });
1578
1579 if result.success {
1580 Ok(ToolOutput::success(content).with_metadata(metadata))
1581 } else {
1582 Ok(ToolOutput::error(content).with_metadata(metadata))
1583 }
1584 }
1585}
1586
1587#[async_trait]
1588impl Tool for TaskTool {
1589 fn name(&self) -> &str {
1590 "task"
1591 }
1592
1593 fn description(&self) -> &str {
1594 TASK_TOOL_DESCRIPTION
1595 }
1596
1597 fn parameters(&self) -> serde_json::Value {
1598 task_params_schema_for_agents(&self.executor.visible_agents())
1599 }
1600
1601 fn definition(&self) -> ToolDefinition {
1602 let agents = self.executor.visible_agents();
1603 ToolDefinition {
1604 name: self.name().to_string(),
1605 description: delegation_tool_description(self.description(), &agents),
1606 parameters: task_model_params_schema_for_agents(&agents),
1607 }
1608 }
1609
1610 async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> {
1611 if args.get("tasks").is_some() {
1612 let mut params: ParallelTaskParams = match serde_json::from_value(args.clone()) {
1613 Ok(params) => params,
1614 Err(error) => {
1615 return Ok(invalid_delegation_argument(format!(
1616 "Invalid task parameters: {error}"
1617 )));
1618 }
1619 };
1620 if params.tasks.is_empty() {
1621 return Ok(invalid_delegation_argument(
1622 "task requires at least 1 task".to_string(),
1623 ));
1624 }
1625
1626 let has_fanout_options = params.allow_partial_failure
1627 || params.timeout_ms.is_some()
1628 || params.min_success_count.is_some();
1629 if params.tasks.len() == 1 && !has_fanout_options {
1630 return self.execute_single(params.tasks.remove(0), ctx).await;
1631 }
1632
1633 return ParallelTaskTool::new(Arc::clone(&self.executor))
1634 .execute_params(params, ctx, "task", 1)
1635 .await;
1636 }
1637
1638 let params: TaskParams = match serde_json::from_value(args.clone()) {
1639 Ok(params) => params,
1640 Err(error) => {
1641 return Ok(invalid_delegation_argument(format!(
1642 "Invalid task parameters: {error}"
1643 )));
1644 }
1645 };
1646 self.execute_single(params, ctx).await
1647 }
1648}
1649
1650mod parallel_params;
1651pub use parallel_params::{parallel_task_params_schema, ParallelTaskParams};
1652
1653pub struct ParallelTaskTool {
1657 executor: Arc<TaskExecutor>,
1658}
1659
1660impl ParallelTaskTool {
1661 pub fn new(executor: Arc<TaskExecutor>) -> Self {
1663 Self { executor }
1664 }
1665
1666 async fn execute_params(
1667 &self,
1668 params: ParallelTaskParams,
1669 ctx: &ToolContext,
1670 tool_name: &str,
1671 min_tasks: usize,
1672 ) -> Result<ToolOutput> {
1673 let started_at = std::time::Instant::now();
1674 let parent_cancellation = ctx.cancellation_token();
1675 let executor = self.executor.scoped_for_invocation(ctx);
1676
1677 if params.tasks.len() < min_tasks {
1678 return Ok(invalid_delegation_argument(format!(
1679 "{tool_name} requires at least {min_tasks} task{}",
1680 if min_tasks == 1 { "" } else { "s" }
1681 )));
1682 }
1683 if params.tasks.len() > MAX_PARALLEL_TASKS_PER_CALL {
1684 return Ok(invalid_delegation_argument(format!(
1685 "{tool_name} accepts at most {MAX_PARALLEL_TASKS_PER_CALL} tasks"
1686 )));
1687 }
1688 if let Some((index, _)) = params
1689 .tasks
1690 .iter()
1691 .enumerate()
1692 .find(|(_, task)| task.background)
1693 {
1694 return Ok(invalid_delegation_argument(format!(
1695 "{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",
1696 index + 1
1697 )));
1698 }
1699 if params.timeout_ms == Some(0) {
1700 return Ok(invalid_delegation_argument(format!(
1701 "{tool_name} timeout_ms must be at least 1"
1702 )));
1703 }
1704 if let Some(min_success_count) = params.min_success_count {
1705 if !params.allow_partial_failure {
1706 return Ok(invalid_delegation_argument(format!(
1707 "{tool_name} min_success_count requires allow_partial_failure=true"
1708 )));
1709 }
1710 if min_success_count == 0 || min_success_count > params.tasks.len() {
1711 return Ok(invalid_delegation_argument(format!(
1712 "{tool_name} min_success_count must be between 1 and the task count ({})",
1713 params.tasks.len()
1714 )));
1715 }
1716 }
1717
1718 let task_count = params.tasks.len();
1719 let run = executor
1720 .execute_parallel_for_tool(
1721 params.tasks.clone(),
1722 ctx.agent_event_tx.clone(),
1723 parallel_execution::ParallelToolOptions {
1724 parent_session_id: ctx.session_id.as_deref(),
1725 timeout_ms: params.timeout_ms,
1726 min_success_count: params.min_success_count,
1727 allow_partial_failure: params.allow_partial_failure,
1728 parent_cancellation: Some(&parent_cancellation),
1729 },
1730 )
1731 .await;
1732 let results = run.results;
1733
1734 let mut output = format!("Executed {} tasks concurrently:\n\n", task_count);
1735 let mut metadata_results = Vec::new();
1736 let source_anchor_counts = parallel_source_anchor_counts(&results);
1737 for (i, result) in results.iter().enumerate() {
1738 let status = if result.success { "[OK]" } else { "[ERR]" };
1739 let (formatted, truncated) = format_task_result_for_context(result);
1740 let (output_excerpt, _) = compact_task_output(&result.output);
1741 let source_anchors = &result.source_anchors[..source_anchor_counts[i]];
1742 metadata_results.push(serde_json::json!({
1743 "task_id": result.task_id,
1744 "session_id": result.session_id,
1745 "agent": result.agent,
1746 "success": result.success,
1747 "error_message": (!result.success).then(|| {
1748 crate::text::truncate_utf8(&result.output, 1024).to_string()
1749 }),
1750 "output_excerpt": output_excerpt,
1751 "structured": result.structured,
1752 "source_anchors": source_anchors,
1753 "output_bytes": result.output.len(),
1754 "truncated_for_context": truncated,
1755 "artifact_id": task_artifact_id(result),
1756 "artifact_uri": task_artifact_uri(result),
1757 }));
1758 output.push_str(&format!(
1759 "--- Task {} ({}) {} ---\n{}\n\n",
1760 i + 1,
1761 result.agent,
1762 status,
1763 formatted
1764 ));
1765 }
1766
1767 let success_count = results.iter().filter(|result| result.success).count();
1768 let failed_count = results.len().saturating_sub(success_count);
1769 let all_success = failed_count == 0;
1770 let partial_failure = failed_count > 0 && success_count > 0;
1771 if params.allow_partial_failure && partial_failure {
1772 output.push_str(&format!(
1773 "Partial failure tolerated: {success_count} succeeded, {failed_count} failed.\n"
1774 ));
1775 }
1776 if run.timed_out {
1777 output.push_str(&format!(
1778 "Task fan-out timed out after {} ms; returned completed child results and marked unfinished children failed.\n",
1779 run.timeout_ms.unwrap_or_default()
1780 ));
1781 } else if run.returned_early {
1782 output.push_str(&format!(
1783 "Task fan-out returned after reaching min_success_count={}; unfinished children were marked failed.\n",
1784 run.min_success_count.unwrap_or_default()
1785 ));
1786 }
1787
1788 let tool_success = all_success || (params.allow_partial_failure && success_count > 0);
1789 let mut output = if tool_success {
1790 ToolOutput::success(output)
1791 } else {
1792 ToolOutput::error(output)
1793 };
1794 if !tool_success && failed_count > 0 {
1795 output.error_kind = Some(crate::tools::ToolErrorKind::PartialFailure {
1796 failed: failed_count,
1797 total: results.len(),
1798 });
1799 }
1800
1801 Ok(output.with_metadata(serde_json::json!({
1802 "task_count": task_count,
1803 "result_count": results.len(),
1804 "success_count": success_count,
1805 "failed_count": failed_count,
1806 "all_success": all_success,
1807 "partial_failure": partial_failure,
1808 "allow_partial_failure": params.allow_partial_failure,
1809 "timeout_ms": params.timeout_ms,
1810 "timed_out": run.timed_out,
1811 "min_success_count": params.min_success_count,
1812 "returned_early": run.returned_early,
1813 "duration_ms": started_at.elapsed().as_millis().min(u128::from(u64::MAX)) as u64,
1814 "results": metadata_results,
1815 })))
1816 }
1817}
1818
1819#[async_trait]
1820impl Tool for ParallelTaskTool {
1821 fn name(&self) -> &str {
1822 "parallel_task"
1823 }
1824
1825 fn description(&self) -> &str {
1826 PARALLEL_TASK_TOOL_DESCRIPTION
1827 }
1828
1829 fn parameters(&self) -> serde_json::Value {
1830 parallel_params::parallel_task_params_schema_for_agents(&self.executor.visible_agents())
1831 }
1832
1833 fn definition(&self) -> ToolDefinition {
1834 let agents = self.executor.visible_agents();
1835 ToolDefinition {
1836 name: self.name().to_string(),
1837 description: delegation_tool_description(self.description(), &agents),
1838 parameters: parallel_params::parallel_task_params_schema_for_agents(&agents),
1839 }
1840 }
1841
1842 fn is_model_visible(&self) -> bool {
1843 false
1844 }
1845
1846 async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> {
1847 let params: ParallelTaskParams = match serde_json::from_value(args.clone()) {
1848 Ok(params) => params,
1849 Err(error) => {
1850 return Ok(invalid_delegation_argument(format!(
1851 "Invalid parallel_task parameters: {error}"
1852 )));
1853 }
1854 };
1855 self.execute_params(params, ctx, "parallel_task", 2).await
1856 }
1857}
1858
1859fn invalid_delegation_argument(message: String) -> ToolOutput {
1860 ToolOutput::error(&message)
1861 .with_error_kind(crate::tools::ToolErrorKind::InvalidArgument { message })
1862}
1863
1864#[cfg(test)]
1865mod tests;