1use crate::agent::{AgentConfig, AgentEvent, AgentLoop};
18use crate::llm::structured::{
19 generate_blocking, parse_validated_output, StructuredMode, StructuredRequest,
20};
21use crate::llm::{LlmClient, ToolDefinition};
22use crate::mcp::manager::McpManager;
23use crate::orchestration::{AgentExecutor, AgentStepSpec, StepOutcome, ToolSourceAnchor};
24use crate::subagent::{AgentDefinition, AgentRegistry};
25use crate::tools::types::{Tool, ToolContext, ToolOutput};
26use anyhow::{Context, Result};
27use async_trait::async_trait;
28use futures::FutureExt;
29use serde::{Deserialize, Serialize};
30use std::any::Any;
31use std::panic::AssertUnwindSafe;
32use std::path::PathBuf;
33use std::sync::Arc;
34use tokio::sync::broadcast;
35use tokio::task::JoinSet;
36use tokio_util::sync::CancellationToken;
37
38const TASK_OUTPUT_CONTEXT_LIMIT: usize = 4_000;
39const TASK_OUTPUT_CONTEXT_HEAD: usize = 3_000;
40const TASK_OUTPUT_CONTEXT_TAIL: usize = 800;
41const MAX_TASK_SOURCE_ANCHORS: usize = 64;
42const MAX_TASK_SOURCE_CANDIDATES: usize = MAX_TASK_SOURCE_ANCHORS * 4;
43const MAX_TASK_SOURCE_TOOL_BYTES: usize = 64;
44const MAX_TASK_SOURCE_VALUE_BYTES: usize = 4 * 1024;
45const MAX_PARALLEL_TASK_SOURCE_ANCHORS: usize = MAX_TASK_SOURCE_ANCHORS;
46const TASK_TOOL_DESCRIPTION: &str = "Delegate a bounded task to a specialized child run. Choose the canonical worker name from the live agent catalog. Custom agents from agent_dirs and .a3s/agents are supported; .claude/agents is read for compatibility.";
47const PARALLEL_TASK_TOOL_DESCRIPTION: &str = "Fan out 2 or more INDEPENDENT subtasks as delegated child runs that execute concurrently; results are returned when all complete. By default any failed child makes the tool fail; evidence-gathering callers may set allow_partial_failure=true to continue when at least one child succeeds. Transient provider failures may be retried once only for explicitly read-only branches; successful and potentially mutating branches are never replayed. Use this only when the work genuinely splits into branches that can be investigated or implemented separately. Do not use it for trivial, conversational, single-step, or dependent work. Choose canonical worker names from the live agent catalog.";
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(deny_unknown_fields)]
52pub struct TaskParams {
53 pub agent: String,
55 pub description: String,
57 pub prompt: String,
59 #[serde(default)]
61 pub background: bool,
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub max_steps: Option<usize>,
65 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub output_schema: Option<serde_json::Value>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct TaskResult {
73 pub output: String,
75 pub session_id: String,
77 pub agent: String,
79 pub success: bool,
81 pub task_id: String,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub structured: Option<serde_json::Value>,
86 #[serde(default, skip_serializing_if = "Vec::is_empty")]
88 pub source_anchors: Vec<ToolSourceAnchor>,
89}
90
91mod result_projection;
92use result_projection::*;
93
94mod parallel_execution;
95
96const MAX_PARALLEL_TASKS_PER_CALL: usize = 32;
97
98pub struct TaskExecutor {
100 registry: Arc<AgentRegistry>,
102 llm_client: Arc<dyn LlmClient>,
104 workspace: String,
106 mcp_managers: Vec<Arc<McpManager>>,
108 parent_context: Option<crate::child_run::ChildRunContext>,
110 parent_cancellation: Option<CancellationToken>,
114 max_parallel_tasks: usize,
115 parallel_permits: Arc<tokio::sync::Semaphore>,
119 subagent_tracker: Option<Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>>,
122}
123
124impl TaskExecutor {
125 pub fn new(
127 registry: Arc<AgentRegistry>,
128 llm_client: Arc<dyn LlmClient>,
129 workspace: String,
130 ) -> Self {
131 Self {
132 registry,
133 llm_client,
134 workspace,
135 mcp_managers: Vec::new(),
136 parent_context: None,
137 parent_cancellation: None,
138 max_parallel_tasks: crate::agent::DEFAULT_MAX_PARALLEL_TASKS,
139 parallel_permits: Arc::new(tokio::sync::Semaphore::new(
140 crate::agent::DEFAULT_MAX_PARALLEL_TASKS,
141 )),
142 subagent_tracker: None,
143 }
144 }
145
146 pub fn with_mcp(
148 registry: Arc<AgentRegistry>,
149 llm_client: Arc<dyn LlmClient>,
150 workspace: String,
151 mcp_manager: Arc<McpManager>,
152 ) -> Self {
153 Self::with_mcp_managers(registry, llm_client, workspace, vec![mcp_manager])
154 }
155
156 pub fn with_mcp_managers(
158 registry: Arc<AgentRegistry>,
159 llm_client: Arc<dyn LlmClient>,
160 workspace: String,
161 mcp_managers: Vec<Arc<McpManager>>,
162 ) -> Self {
163 Self {
164 registry,
165 llm_client,
166 workspace,
167 mcp_managers,
168 parent_context: None,
169 parent_cancellation: None,
170 max_parallel_tasks: crate::agent::DEFAULT_MAX_PARALLEL_TASKS,
171 parallel_permits: Arc::new(tokio::sync::Semaphore::new(
172 crate::agent::DEFAULT_MAX_PARALLEL_TASKS,
173 )),
174 subagent_tracker: None,
175 }
176 }
177
178 pub fn with_parent_context(mut self, ctx: crate::child_run::ChildRunContext) -> Self {
180 if let Some(max_parallel_tasks) = ctx.max_parallel_tasks {
181 let max_parallel_tasks = max_parallel_tasks.max(1);
182 self.max_parallel_tasks = max_parallel_tasks;
183 self.parallel_permits = Arc::new(tokio::sync::Semaphore::new(max_parallel_tasks));
184 }
185 self.parent_context = Some(ctx);
186 self
187 }
188
189 pub fn with_parent_cancellation(mut self, cancellation: CancellationToken) -> Self {
196 self.parent_cancellation = Some(cancellation);
197 self
198 }
199
200 pub fn with_max_parallel_tasks(mut self, max_parallel_tasks: usize) -> Self {
201 let max_parallel_tasks = max_parallel_tasks.max(1);
202 self.max_parallel_tasks = max_parallel_tasks;
203 self.parallel_permits = Arc::new(tokio::sync::Semaphore::new(max_parallel_tasks));
204 self
205 }
206
207 pub fn with_subagent_tracker(
211 mut self,
212 tracker: Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>,
213 ) -> Self {
214 self.subagent_tracker = Some(tracker);
215 self
216 }
217
218 fn visible_agents(&self) -> Vec<AgentDefinition> {
219 self.registry.list_visible()
220 }
221
222 pub async fn execute(
227 &self,
228 params: TaskParams,
229 event_tx: Option<broadcast::Sender<AgentEvent>>,
230 parent_session_id: Option<&str>,
231 ) -> Result<TaskResult> {
232 self.execute_with_parent_cancellation(
233 params,
234 event_tx,
235 parent_session_id,
236 self.parent_cancellation.as_ref(),
237 )
238 .await
239 }
240
241 async fn execute_with_parent_cancellation(
242 &self,
243 params: TaskParams,
244 event_tx: Option<broadcast::Sender<AgentEvent>>,
245 parent_session_id: Option<&str>,
246 parent_cancellation: Option<&CancellationToken>,
247 ) -> Result<TaskResult> {
248 let task_id = format!("task-{}", uuid::Uuid::new_v4());
249 self.execute_with_task_id_scoped(
250 task_id,
251 params,
252 event_tx,
253 parent_session_id,
254 true,
255 parent_cancellation,
256 )
257 .await
258 }
259
260 pub async fn execute_with_task_id(
265 &self,
266 task_id: String,
267 params: TaskParams,
268 event_tx: Option<broadcast::Sender<AgentEvent>>,
269 parent_session_id: Option<&str>,
270 emit_start: bool,
271 ) -> Result<TaskResult> {
272 self.execute_with_task_id_scoped(
273 task_id,
274 params,
275 event_tx,
276 parent_session_id,
277 emit_start,
278 self.parent_cancellation.as_ref(),
279 )
280 .await
281 }
282
283 async fn execute_with_task_id_scoped(
284 &self,
285 task_id: String,
286 params: TaskParams,
287 event_tx: Option<broadcast::Sender<AgentEvent>>,
288 parent_session_id: Option<&str>,
289 emit_start: bool,
290 parent_cancellation: Option<&CancellationToken>,
291 ) -> Result<TaskResult> {
292 if parent_cancellation.is_some_and(CancellationToken::is_cancelled) {
293 anyhow::bail!("Operation cancelled by parent session");
294 }
295
296 let session_id = format!("task-run-{}", task_id);
297 let started_ms = epoch_ms();
298 let output_schema = params.output_schema.clone();
299
300 let agent = self
301 .registry
302 .get(¶ms.agent)
303 .context(format!("Unknown agent type: '{}'", params.agent))?;
304 let tool_free = agent.tool_free;
305 let tool_free_system = agent.prompt.clone();
306 let inherited_security_provider = self
307 .parent_context
308 .as_ref()
309 .and_then(|context| context.security_provider.clone());
310
311 if emit_start {
312 let event = AgentEvent::SubagentStart {
313 task_id: task_id.clone(),
314 session_id: session_id.clone(),
315 parent_session_id: parent_session_id.unwrap_or_default().to_string(),
316 agent: params.agent.clone(),
317 description: params.description.clone(),
318 started_ms,
319 };
320 let event = inherited_security_provider
321 .as_deref()
322 .map(|provider| crate::security::sanitize_agent_event(provider, &event))
323 .unwrap_or(event);
324 if let Some(ref tracker) = self.subagent_tracker {
325 tracker.record_event(&event).await;
326 }
327 if let Some(ref tx) = event_tx {
328 let _ = tx.send(event);
329 }
330 }
331
332 let child_executor = if let Some(ref parent_ctx) = self.parent_context {
335 if let Some(ref services) = parent_ctx.workspace_services {
336 crate::tools::ToolExecutor::new_with_workspace_services_and_artifact_limits(
337 self.workspace.clone(),
338 Arc::clone(services),
339 crate::tools::ArtifactStoreLimits::default(),
340 )
341 } else {
342 crate::tools::ToolExecutor::new(self.workspace.clone())
343 }
344 } else {
345 crate::tools::ToolExecutor::new(self.workspace.clone())
346 };
347
348 for mcp in &self.mcp_managers {
350 let all_tools = match parent_cancellation {
351 Some(cancellation) => {
352 tokio::select! {
353 biased;
354 _ = cancellation.cancelled() => {
355 anyhow::bail!("Operation cancelled by parent session");
356 }
357 tools = mcp.get_all_tools() => tools,
358 }
359 }
360 None => mcp.get_all_tools().await,
361 };
362 let mut by_server: std::collections::HashMap<
363 String,
364 Vec<crate::mcp::protocol::McpTool>,
365 > = std::collections::HashMap::new();
366 for (server, tool) in all_tools {
367 by_server.entry(server).or_default().push(tool);
368 }
369 for (server_name, tools) in by_server {
370 let wrappers =
371 crate::mcp::tools::create_mcp_tools(&server_name, tools, Arc::clone(mcp));
372 for wrapper in wrappers {
373 child_executor.register_dynamic_tool(wrapper);
374 }
375 }
376 }
377
378 let child_executor = Arc::new(child_executor);
379
380 let mut child_config = AgentConfig {
381 tools: child_executor.definitions(),
382 ..AgentConfig::default()
383 };
384 agent.apply_to(&mut child_config);
385 if let Some(ref parent_ctx) = self.parent_context {
386 parent_ctx.apply_to(&mut child_config);
387 }
388 child_config.planning_mode = crate::prompts::PlanningMode::Disabled;
393 if let Some(max_steps) = params.max_steps {
394 child_config.max_tool_rounds = max_steps;
395 }
396 let child_security_provider = child_config.security_provider.clone();
397 let source_security_provider = child_security_provider.clone();
398
399 let cancel_token = parent_cancellation
400 .map(CancellationToken::child_token)
401 .unwrap_or_default();
402 let mut tool_context = ToolContext::new(PathBuf::from(&self.workspace))
403 .with_session_id(session_id.clone())
404 .with_cancellation(cancel_token.clone());
405 if let Some(ref parent_ctx) = self.parent_context {
406 if let Some(ref services) = parent_ctx.workspace_services {
407 tool_context = tool_context.with_workspace_services(Arc::clone(services));
408 }
409 }
410
411 let source_context = tool_context.clone();
412 let agent_loop = AgentLoop::new(
413 Arc::clone(&self.llm_client),
414 child_executor,
415 tool_context,
416 child_config,
417 );
418
419 let (mpsc_tx, mut mpsc_rx) = tokio::sync::mpsc::channel(100);
423 let broadcast_tx = event_tx.clone();
424 let progress_task_id = task_id.clone();
425 let progress_session_id = session_id.clone();
426 let child_event_forwarder = tokio::spawn(async move {
427 let mut source_anchors = Vec::new();
428 let mut seen_source_anchors = std::collections::HashSet::new();
429 let mut scanned_source_candidates = 0usize;
430 while let Some(event) = mpsc_rx.recv().await {
431 let event = source_security_provider
432 .as_deref()
433 .map(|provider| crate::security::sanitize_agent_event(provider, &event))
434 .unwrap_or(event);
435 collect_tool_source_anchors(
436 &event,
437 &source_context,
438 &mut source_anchors,
439 &mut seen_source_anchors,
440 &mut scanned_source_candidates,
441 );
442 if let Some(ref broadcast_tx) = broadcast_tx {
443 if let Some(progress) = synthesize_subagent_progress(
444 &event,
445 &progress_task_id,
446 &progress_session_id,
447 ) {
448 let _ = broadcast_tx.send(progress);
449 }
450 let _ = broadcast_tx.send(event);
451 }
452 }
453 source_anchors
454 });
455 let child_event_tx = Some(mpsc_tx);
456 let child_llm_event_tx = child_event_tx.clone();
457
458 if let Some(ref tracker) = self.subagent_tracker {
461 tracker
462 .register_canceller(&task_id, cancel_token.clone())
463 .await;
464 }
465
466 let structured_prompt = output_schema
467 .as_ref()
468 .filter(|_| !tool_free)
469 .map(|schema| structured_task_prompt(¶ms.prompt, schema));
470 let execution_prompt = structured_prompt.as_deref().unwrap_or(¶ms.prompt);
471
472 let mut structured = None;
473 let (mut output, mut success) = if tool_free && output_schema.is_some() {
474 let llm_client = agent_loop.scoped_llm_client_for_parts(
475 Some(&session_id),
476 &child_llm_event_tx,
477 &cancel_token,
478 );
479 match Self::generate_structured_task(
480 &*llm_client,
481 ¶ms.prompt,
482 tool_free_system.as_deref(),
483 output_schema.clone().expect("schema checked above"),
484 &cancel_token,
485 )
486 .await
487 {
488 Ok(object) => {
489 let output = serde_json::to_string_pretty(&object)
490 .unwrap_or_else(|_| object.to_string());
491 structured = Some(object);
492 (output, true)
493 }
494 Err(error) if cancel_token.is_cancelled() => {
495 (format!("Task cancelled by caller: {error}"), false)
496 }
497 Err(error) => (format!("Task failed: {error}"), false),
498 }
499 } else {
500 match agent_loop
501 .execute_with_session(
502 &[],
503 execution_prompt,
504 Some(&session_id),
505 child_event_tx.clone(),
506 Some(&cancel_token),
507 )
508 .await
509 {
510 Ok(_) if cancel_token.is_cancelled() => {
511 ("Task cancelled by caller".to_string(), false)
512 }
513 Ok(result) if result.text.trim().is_empty() => (
514 "Task failed: child agent returned no final output".to_string(),
515 false,
516 ),
517 Ok(result) if AgentLoop::is_synthetic_failure_output(&result.text) => {
518 (format!("Task failed: {}", result.text), false)
519 }
520 Ok(result) => (result.text, true),
521 Err(e) if cancel_token.is_cancelled() => {
522 (format!("Task cancelled by caller: {}", e), false)
523 }
524 Err(e) => (format!("Task failed: {}", e), false),
525 }
526 };
527
528 if success && !tool_free {
529 if let Some(schema) = output_schema {
530 if let Some(object) = parse_validated_output(&output, &schema) {
531 structured = Some(object);
532 } else {
533 let llm_client = agent_loop.scoped_llm_client_for_parts(
534 Some(&session_id),
535 &child_llm_event_tx,
536 &cancel_token,
537 );
538 match Self::coerce_to_schema(&*llm_client, &output, schema, &cancel_token).await
539 {
540 Ok(object) => structured = Some(object),
541 Err(error) => {
542 success = false;
543 output = format!("{output}\n\n[structured output failed: {error}]");
544 }
545 }
546 }
547 }
548 }
549 if let Some(provider) = child_security_provider.as_deref() {
550 output = provider.sanitize_output(&output);
551 if let Some(value) = &mut structured {
552 *value = sanitize_task_json(provider, value);
553 }
554 }
555
556 drop(child_event_tx);
561 drop(child_llm_event_tx);
562 let source_anchors = match child_event_forwarder.await {
563 Ok(source_anchors) => source_anchors,
564 Err(error) => {
565 tracing::warn!(%error, task_id = %task_id, "subagent event bridge failed");
566 Vec::new()
567 }
568 };
569
570 let end_event = AgentEvent::SubagentEnd {
571 task_id: task_id.clone(),
572 session_id: session_id.clone(),
573 agent: params.agent.clone(),
574 output: output.clone(),
575 success,
576 finished_ms: epoch_ms(),
577 };
578 if let Some(ref tracker) = self.subagent_tracker {
579 if success {
582 tracker
583 .record_source_anchors(&task_id, &source_anchors)
584 .await;
585 }
586 tracker.record_event(&end_event).await;
587 tracker.clear_canceller(&task_id).await;
588 }
589 if let Some(ref tx) = event_tx {
590 let _ = tx.send(end_event);
591 }
592
593 Ok(TaskResult {
594 output,
595 session_id,
596 agent: params.agent,
597 success,
598 task_id,
599 structured,
600 source_anchors,
601 })
602 }
603
604 pub fn execute_background(
612 self: Arc<Self>,
613 params: TaskParams,
614 event_tx: Option<broadcast::Sender<AgentEvent>>,
615 parent_session_id: Option<String>,
616 ) -> String {
617 let parent_cancellation = self.parent_cancellation.clone();
618 self.execute_background_with_parent_cancellation(
619 params,
620 event_tx,
621 parent_session_id,
622 parent_cancellation,
623 )
624 }
625
626 fn execute_background_with_parent_cancellation(
627 self: Arc<Self>,
628 params: TaskParams,
629 event_tx: Option<broadcast::Sender<AgentEvent>>,
630 parent_session_id: Option<String>,
631 parent_cancellation: Option<CancellationToken>,
632 ) -> String {
633 let task_id = format!("task-{}", uuid::Uuid::new_v4());
634 let session_id = format!("task-run-{}", task_id);
635 let failure_session_id = session_id.clone();
636 let failure_agent = params.agent.clone();
637 let start_event = AgentEvent::SubagentStart {
638 task_id: task_id.clone(),
639 session_id,
640 parent_session_id: parent_session_id.clone().unwrap_or_default(),
641 agent: params.agent.clone(),
642 description: params.description.clone(),
643 started_ms: epoch_ms(),
644 };
645 let security_provider = self
646 .parent_context
647 .as_ref()
648 .and_then(|context| context.security_provider.clone());
649 let start_event = security_provider
650 .as_deref()
651 .map(|provider| crate::security::sanitize_agent_event(provider, &start_event))
652 .unwrap_or(start_event);
653
654 if let Some(ref tx) = event_tx {
655 let _ = tx.send(start_event.clone());
656 }
657
658 let task_id_for_spawn = task_id.clone();
659 let task_id_for_log = task_id.clone();
660 tokio::spawn(async move {
661 if let Some(ref tracker) = self.subagent_tracker {
662 tracker.record_event(&start_event).await;
663 }
664 let failure_event_tx = event_tx.clone();
665 if let Err(error) = self
666 .execute_with_task_id_scoped(
667 task_id_for_spawn,
668 params,
669 event_tx,
670 parent_session_id.as_deref(),
671 false,
672 parent_cancellation.as_ref(),
673 )
674 .await
675 {
676 let end_event = AgentEvent::SubagentEnd {
677 task_id: task_id_for_log.clone(),
678 session_id: failure_session_id,
679 agent: failure_agent,
680 output: format!("Task failed before child execution started: {error}"),
681 success: false,
682 finished_ms: epoch_ms(),
683 };
684 let end_event = security_provider
685 .as_deref()
686 .map(|provider| crate::security::sanitize_agent_event(provider, &end_event))
687 .unwrap_or(end_event);
688 if let Some(ref tracker) = self.subagent_tracker {
689 tracker.record_event(&end_event).await;
690 tracker.clear_canceller(&task_id_for_log).await;
691 }
692 if let Some(tx) = failure_event_tx {
693 let _ = tx.send(end_event);
694 }
695 tracing::error!("Background task {} failed: {}", task_id_for_log, error);
696 }
697 });
698
699 task_id
700 }
701}
702
703fn structured_task_prompt(prompt: &str, schema: &serde_json::Value) -> String {
704 let schema = serde_json::to_string_pretty(schema).unwrap_or_else(|_| schema.to_string());
705 format!(
706 "{prompt}\n\n\
707 FINAL OUTPUT CONTRACT\n\
708 Complete the requested investigation before answering. Your final response must contain \
709 exactly one JSON value matching the JSON Schema below, with no Markdown fence or prose \
710 outside the JSON. This contract applies to the final response only; use the available \
711 tools as needed before finalizing.\n\n\
712 {schema}"
713 )
714}
715
716#[derive(Debug, Clone)]
717struct AgentCatalogEntry {
718 name: String,
719 description: String,
720}
721
722fn agent_catalog_entries(agents: &[AgentDefinition]) -> Vec<AgentCatalogEntry> {
723 let mut entries = agents
724 .iter()
725 .map(|agent| AgentCatalogEntry {
726 name: agent.name.clone(),
727 description: agent
728 .description
729 .split_whitespace()
730 .collect::<Vec<_>>()
731 .join(" "),
732 })
733 .collect::<Vec<_>>();
734 entries.sort_by(|left, right| left.name.cmp(&right.name));
735 entries
736}
737
738fn agent_catalog_text(agents: &[AgentDefinition]) -> String {
739 agent_catalog_entries(agents)
740 .into_iter()
741 .map(|entry| format!("{}: {}", entry.name, entry.description))
742 .collect::<Vec<_>>()
743 .join("\n")
744}
745
746fn delegation_tool_description(base: &str, agents: &[AgentDefinition]) -> String {
747 format!(
748 "{base}\n\nAvailable agents (live catalog; use canonical names):\n{}",
749 agent_catalog_text(agents)
750 )
751}
752
753pub(super) fn task_agent_parameter_schema(agents: &[AgentDefinition]) -> serde_json::Value {
754 let entries = agent_catalog_entries(agents);
755 let examples = entries
756 .iter()
757 .map(|entry| serde_json::Value::String(entry.name.clone()))
758 .collect::<Vec<_>>();
759 let catalog = entries
760 .into_iter()
761 .map(|entry| format!("{}: {}", entry.name, entry.description))
762 .collect::<Vec<_>>()
763 .join("\n");
764 serde_json::json!({
765 "type": "string",
766 "description": format!(
767 "Required. Canonical agent type to use. Always provide this exact field name: 'agent'. Live agent catalog:\n{catalog}"
768 ),
769 "examples": examples
770 })
771}
772
773pub fn task_params_schema() -> serde_json::Value {
775 task_params_schema_for_agents(&AgentRegistry::new().list_visible())
776}
777
778fn task_params_schema_for_agents(agents: &[AgentDefinition]) -> serde_json::Value {
779 serde_json::json!({
780 "type": "object",
781 "additionalProperties": false,
782 "properties": {
783 "agent": task_agent_parameter_schema(agents),
784 "description": {
785 "type": "string",
786 "description": "Required. Short task label for display and tracking. Always provide this exact field name: 'description'."
787 },
788 "prompt": {
789 "type": "string",
790 "description": "Required. Detailed instruction for the delegated child run. Always provide this exact field name: 'prompt'."
791 },
792 "background": {
793 "type": "boolean",
794 "description": "Optional. Run the task in the background. Default: false.",
795 "default": false
796 },
797 "max_steps": {
798 "type": "integer",
799 "description": "Optional. Maximum number of steps for this task."
800 },
801 "output_schema": {
802 "type": "object",
803 "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."
804 }
805 },
806 "required": ["agent", "description", "prompt"],
807 "examples": [
808 {
809 "agent": "explore",
810 "description": "Find Rust files",
811 "prompt": "Search the workspace for Rust files and summarize the layout."
812 },
813 {
814 "agent": "general",
815 "description": "Investigate test failure",
816 "prompt": "Inspect the failing tests and explain the root cause.",
817 "max_steps": 6
818 }
819 ]
820 })
821}
822
823pub struct TaskTool {
826 executor: Arc<TaskExecutor>,
827}
828
829impl TaskTool {
830 pub fn new(executor: Arc<TaskExecutor>) -> Self {
832 Self { executor }
833 }
834}
835
836#[async_trait]
837impl Tool for TaskTool {
838 fn name(&self) -> &str {
839 "task"
840 }
841
842 fn description(&self) -> &str {
843 TASK_TOOL_DESCRIPTION
844 }
845
846 fn parameters(&self) -> serde_json::Value {
847 task_params_schema_for_agents(&self.executor.visible_agents())
848 }
849
850 fn definition(&self) -> ToolDefinition {
851 let agents = self.executor.visible_agents();
852 ToolDefinition {
853 name: self.name().to_string(),
854 description: delegation_tool_description(self.description(), &agents),
855 parameters: task_params_schema_for_agents(&agents),
856 }
857 }
858
859 async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> {
860 let params: TaskParams =
861 serde_json::from_value(args.clone()).context("Invalid task parameters")?;
862 let parent_cancellation = ctx.cancellation_token();
863
864 if params.background {
865 let task_id = Arc::clone(&self.executor).execute_background_with_parent_cancellation(
866 params,
867 ctx.agent_event_tx.clone(),
868 ctx.session_id.clone(),
869 Some(parent_cancellation),
870 );
871 return Ok(ToolOutput::success(format!(
872 "Task started in background. Task ID: {}",
873 task_id
874 )));
875 }
876
877 let result = self
878 .executor
879 .execute_with_parent_cancellation(
880 params,
881 ctx.agent_event_tx.clone(),
882 ctx.session_id.as_deref(),
883 Some(&parent_cancellation),
884 )
885 .await?;
886 let (content, truncated) = format_task_result_for_context(&result);
887 let metadata = serde_json::json!({
888 "task_id": result.task_id,
889 "session_id": result.session_id,
890 "agent": result.agent,
891 "success": result.success,
892 "output_bytes": result.output.len(),
893 "truncated_for_context": truncated,
894 "artifact_id": task_artifact_id(&result),
895 "artifact_uri": task_artifact_uri(&result),
896 "structured": result.structured,
897 "source_anchors": result.source_anchors,
898 });
899
900 if result.success {
901 Ok(ToolOutput::success(content).with_metadata(metadata))
902 } else {
903 Ok(ToolOutput::error(content).with_metadata(metadata))
904 }
905 }
906}
907
908mod parallel_params;
909pub use parallel_params::{parallel_task_params_schema, ParallelTaskParams};
910
911pub struct ParallelTaskTool {
915 executor: Arc<TaskExecutor>,
916}
917
918impl ParallelTaskTool {
919 pub fn new(executor: Arc<TaskExecutor>) -> Self {
921 Self { executor }
922 }
923}
924
925#[async_trait]
926impl Tool for ParallelTaskTool {
927 fn name(&self) -> &str {
928 "parallel_task"
929 }
930
931 fn description(&self) -> &str {
932 PARALLEL_TASK_TOOL_DESCRIPTION
933 }
934
935 fn parameters(&self) -> serde_json::Value {
936 parallel_params::parallel_task_params_schema_for_agents(&self.executor.visible_agents())
937 }
938
939 fn definition(&self) -> ToolDefinition {
940 let agents = self.executor.visible_agents();
941 ToolDefinition {
942 name: self.name().to_string(),
943 description: delegation_tool_description(self.description(), &agents),
944 parameters: parallel_params::parallel_task_params_schema_for_agents(&agents),
945 }
946 }
947
948 async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> {
949 let started_at = std::time::Instant::now();
950 let params: ParallelTaskParams =
951 serde_json::from_value(args.clone()).context("Invalid parallel task parameters")?;
952 let parent_cancellation = ctx.cancellation_token();
953
954 if params.tasks.is_empty() {
955 return Ok(ToolOutput::error("No tasks provided".to_string()));
956 }
957 if params.tasks.len() > MAX_PARALLEL_TASKS_PER_CALL {
958 return Ok(ToolOutput::error(format!(
959 "parallel_task accepts at most {MAX_PARALLEL_TASKS_PER_CALL} tasks"
960 )));
961 }
962
963 let task_count = params.tasks.len();
964 let tasks = params.tasks.clone();
965
966 let mut run = self
967 .executor
968 .execute_parallel_for_tool(
969 tasks.clone(),
970 ctx.agent_event_tx.clone(),
971 parallel_execution::ParallelToolOptions {
972 parent_session_id: ctx.session_id.as_deref(),
973 timeout_ms: params.timeout_ms,
974 min_success_count: params.min_success_count,
975 allow_partial_failure: params.allow_partial_failure,
976 parent_cancellation: Some(&parent_cancellation),
977 },
978 )
979 .await;
980 let retry_summary = self
981 .executor
982 .retry_transient_parallel_failures(
983 &tasks,
984 parallel_execution::ParallelRetryOptions {
985 event_tx: ctx.agent_event_tx.clone(),
986 parent_session_id: ctx.session_id.as_deref(),
987 parent_cancellation: &parent_cancellation,
988 total_timeout_ms: params.timeout_ms,
989 started_at,
990 },
991 &mut run,
992 )
993 .await;
994 let results = run.results;
995
996 let mut output = format!("Executed {} tasks in parallel:\n\n", task_count);
998 let mut metadata_results = Vec::new();
999 let source_anchor_counts = parallel_source_anchor_counts(&results);
1000 for (i, result) in results.iter().enumerate() {
1001 let status = if result.success { "[OK]" } else { "[ERR]" };
1002 let (formatted, truncated) = format_task_result_for_context(result);
1003 let (output_excerpt, _) = compact_task_output(&result.output);
1004 let source_anchors = &result.source_anchors[..source_anchor_counts[i]];
1005 metadata_results.push(serde_json::json!({
1006 "task_id": result.task_id,
1007 "session_id": result.session_id,
1008 "agent": result.agent,
1009 "success": result.success,
1010 "error_message": (!result.success).then(|| {
1011 crate::text::truncate_utf8(&result.output, 1024).to_string()
1012 }),
1013 "output_excerpt": output_excerpt,
1014 "structured": result.structured,
1015 "source_anchors": source_anchors,
1016 "output_bytes": result.output.len(),
1017 "truncated_for_context": truncated,
1018 "artifact_id": task_artifact_id(result),
1019 "artifact_uri": task_artifact_uri(result),
1020 "retry_attempts": retry_summary.attempts_by_index.get(i).copied().unwrap_or_default(),
1021 }));
1022 output.push_str(&format!(
1023 "--- Task {} ({}) {} ---\n{}\n\n",
1024 i + 1,
1025 result.agent,
1026 status,
1027 formatted
1028 ));
1029 }
1030
1031 let success_count = results.iter().filter(|result| result.success).count();
1032 let failed_count = results.len().saturating_sub(success_count);
1033 let all_success = failed_count == 0;
1034 let partial_failure = failed_count > 0 && success_count > 0;
1035 if retry_summary.recovered_task_count > 0 {
1036 output.push_str(&format!(
1037 "Recovered {} transient read-only child failure(s) by retrying only failed branches.\n",
1038 retry_summary.recovered_task_count
1039 ));
1040 }
1041 if params.allow_partial_failure && partial_failure {
1042 output.push_str(&format!(
1043 "Partial failure tolerated: {success_count} succeeded, {failed_count} failed.\n"
1044 ));
1045 }
1046 if run.timed_out {
1047 output.push_str(&format!(
1048 "Parallel task timed out after {} ms; returned completed child results and marked unfinished children failed.\n",
1049 run.timeout_ms.unwrap_or_default()
1050 ));
1051 } else if run.returned_early {
1052 output.push_str(&format!(
1053 "Parallel task returned after reaching min_success_count={}; unfinished children were marked failed.\n",
1054 run.min_success_count.unwrap_or_default()
1055 ));
1056 }
1057
1058 let tool_success = all_success || (params.allow_partial_failure && success_count > 0);
1059 let mut output = if tool_success {
1060 ToolOutput::success(output)
1061 } else {
1062 ToolOutput::error(output)
1063 };
1064 if !tool_success && failed_count > 0 {
1065 output.error_kind = Some(crate::tools::ToolErrorKind::PartialFailure {
1066 failed: failed_count,
1067 total: results.len(),
1068 });
1069 }
1070
1071 Ok(output.with_metadata(serde_json::json!({
1072 "task_count": task_count,
1073 "result_count": results.len(),
1074 "success_count": success_count,
1075 "failed_count": failed_count,
1076 "all_success": all_success,
1077 "partial_failure": partial_failure,
1078 "allow_partial_failure": params.allow_partial_failure,
1079 "timeout_ms": params.timeout_ms,
1080 "timed_out": run.timed_out,
1081 "min_success_count": params.min_success_count,
1082 "returned_early": run.returned_early,
1083 "retry_attempt_count": retry_summary.retry_attempt_count,
1084 "retried_task_count": retry_summary.retried_task_count,
1085 "recovered_task_count": retry_summary.recovered_task_count,
1086 "duration_ms": started_at.elapsed().as_millis().min(u128::from(u64::MAX)) as u64,
1087 "results": metadata_results,
1088 })))
1089 }
1090}
1091
1092#[cfg(test)]
1093mod tests;