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