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