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 = "Fan out 2 or more INDEPENDENT subtasks as delegated child runs that execute concurrently; results are returned when all complete. By default any failed child makes the tool fail; evidence-gathering callers may set allow_partial_failure=true to continue when at least one child succeeds. Child output never authorizes branch replay; provider and child runtimes own any typed retry policy below this boundary. Use this only when the work genuinely splits into branches that can be investigated or implemented separately. Do not use it for trivial, conversational, single-step, or dependent work. Choose canonical worker names from the live agent catalog.";
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52#[serde(deny_unknown_fields)]
53pub struct TaskParams {
54 pub agent: String,
56 pub description: String,
58 pub prompt: String,
60 #[serde(default)]
62 pub background: bool,
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub max_steps: Option<usize>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub output_schema: Option<serde_json::Value>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct TaskResult {
74 pub output: String,
76 pub session_id: String,
78 pub agent: String,
80 pub success: bool,
82 pub task_id: String,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub structured: Option<serde_json::Value>,
87 #[serde(default, skip_serializing_if = "Vec::is_empty")]
89 pub source_anchors: Vec<ToolSourceAnchor>,
90}
91
92struct ScopedTaskExecution<'a> {
93 event_tx: Option<broadcast::Sender<AgentEvent>>,
94 parent_session_id: Option<&'a str>,
95 emit_start: bool,
96 parent_cancellation: Option<&'a CancellationToken>,
97 admitted_capability_subtask: Option<crate::capability::AgentCapabilitySubtask>,
98 parallel_lifecycle: Option<Arc<ParallelTaskLifecycle>>,
99}
100
101#[derive(Default)]
110pub(super) struct ParallelTaskLifecycle {
111 state: Mutex<ParallelTaskLifecycleState>,
112}
113
114#[derive(Default)]
115struct ParallelTaskLifecycleState {
116 started: HashSet<String>,
117 ended: HashSet<String>,
118}
119
120impl ParallelTaskLifecycle {
121 fn lock_state(&self) -> MutexGuard<'_, ParallelTaskLifecycleState> {
122 match self.state.lock() {
123 Ok(guard) => guard,
124 Err(poisoned) => poisoned.into_inner(),
128 }
129 }
130
131 fn mark_started(&self, task_id: &str) {
135 self.lock_state().started.insert(task_id.to_string());
136 }
137
138 fn is_started(&self, task_id: &str) -> bool {
139 self.lock_state().started.contains(task_id)
140 }
141
142 fn mark_ended(&self, task_id: &str) {
146 let mut state = self.lock_state();
147 if state.started.contains(task_id) {
148 state.ended.insert(task_id.to_string());
149 }
150 }
151
152 fn is_ended(&self, task_id: &str) -> bool {
153 self.lock_state().ended.contains(task_id)
154 }
155}
156
157mod result_projection;
158use result_projection::*;
159
160mod parallel_execution;
161
162const MAX_PARALLEL_TASKS_PER_CALL: usize = 32;
163
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;