use crate::tool_context::ToolContext;
use crate::tool_types::ToolCall;
use crate::tools::{Tool, ToolExecutionResult};
use async_trait::async_trait;
use super::*;
pub struct DelegationTargetProvider {
pub target_type: &'static str,
pub tool: Box<dyn Tool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SpawnMode {
Background,
Foreground,
}
impl SpawnMode {
pub fn parse(value: &str) -> Option<Self> {
match value {
"background" => Some(Self::Background),
"foreground" => Some(Self::Foreground),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Background => "background",
Self::Foreground => "foreground",
}
}
}
pub(crate) struct UnifiedSpawnAgentTool {
pub(crate) providers: Vec<DelegationTargetProvider>,
}
pub(crate) fn validate_spawn_agent_target_fields(
arguments: &serde_json::Value,
target_type: &str,
) -> Result<(), String> {
for field in ["blueprint", "config"] {
if target_type != "subagent" && arguments.get(field).is_some_and(|value| !value.is_null()) {
return Err(format!(
"{field} is only valid for subagent targets, not {target_type}."
));
}
}
Ok(())
}
impl UnifiedSpawnAgentTool {
pub(crate) fn new(providers: Vec<DelegationTargetProvider>) -> Self {
Self { providers }
}
pub(crate) fn provider_for(&self, target_type: &str) -> Option<&dyn Tool> {
self.providers
.iter()
.find(|provider| provider.target_type == target_type)
.map(|provider| provider.tool.as_ref())
}
pub(crate) fn target_types(&self) -> Vec<&'static str> {
["subagent", "agent", "external_a2a"]
.into_iter()
.filter(|target_type| {
self.providers
.iter()
.any(|provider| provider.target_type == *target_type)
})
.collect()
}
pub(crate) fn target_constraint_branches(&self) -> Vec<serde_json::Value> {
self.target_types()
.into_iter()
.filter_map(|target_type| match target_type {
"subagent" => Some(serde_json::json!({
"properties": {
"type": {"const": "subagent"}
}
})),
"agent" => Some(serde_json::json!({
"properties": {
"type": {"const": "agent"}
},
"required": ["type", "id"]
})),
"external_a2a" => Some(serde_json::json!({
"properties": {
"type": {"const": "external_a2a"}
},
"anyOf": [
{"required": ["id"]},
{"required": ["external_agent_id"]}
]
})),
_ => None,
})
.collect()
}
}
#[async_trait]
impl Tool for UnifiedSpawnAgentTool {
fn narrate(
&self,
tool_call: &ToolCall,
phase: crate::tool_narration::ToolNarrationPhase,
locale: Option<&str>,
ctx: crate::tool_narration::ToolNarrationContext<'_>,
) -> Option<String> {
let from_provider = tool_call
.arguments
.get("target")
.and_then(|target| target.get("type"))
.and_then(serde_json::Value::as_str)
.and_then(|target_type| self.provider_for(target_type))
.and_then(|tool| tool.narrate(tool_call, phase, locale, ctx));
Some(from_provider.unwrap_or_else(|| {
crate::tool_narration::narrate_subagent_spawn(&tool_call.arguments, phase, locale)
}))
}
fn name(&self) -> &str {
"spawn_agent"
}
fn display_name(&self) -> Option<&str> {
Some("Spawn Agent")
}
fn description(&self) -> &str {
"Delegate work to another agent target. Set target.type to one of the advertised target types; background returns a task_id for generic task tools, and foreground waits for the result."
}
fn parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Human-readable name for the delegated run (subagent, first-party handoff, or external delegation). Used as the task label."
},
"instructions": {
"type": "string",
"description": "Instructions for the delegated agent. Do not include credentials or bearer tokens."
},
"goal": {
"type": "string",
"description": "Optional objective stored on the spawned session and made visible at system-prompt level."
},
"lifetime": {
"type": "string",
"enum": ["linked", "detached"],
"default": "linked",
"description": "linked creates a lifecycle child; detached creates an independent top-level peer session. Not valid for external_a2a."
},
"seed": {
"type": "string",
"enum": ["fresh", "fork", "workspace"],
"default": "fresh",
"description": "Detached-session seed mode: fresh starts blank, fork copies history/workspace/session storage, workspace copies workspace files only."
},
"target": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": self.target_types(),
"description": "Delegation target type. Use subagent for same-agent child sessions, agent for configured first-party handoffs, or external_a2a for configured remote A2A agents."
},
"id": {
"type": "string",
"description": "Configured target id for first-party handoffs or external A2A agents."
},
"external_agent_id": {
"type": "string",
"description": "Configured external A2A agent id."
}
},
"required": ["type"],
"oneOf": self.target_constraint_branches(),
"additionalProperties": false
},
"mode": {
"type": "string",
"enum": ["background", "foreground"],
"description": "Execution mode. Use background to return immediately with a task_id, or foreground to block until the delegated work reaches a terminal state or timeout."
},
"blueprint": {
"type": "string",
"description": "Subagent-only blueprint ID to spawn a specialist agent with its own tools and model."
},
"config": {
"type": "object",
"description": "Subagent-only blueprint configuration. Only valid when blueprint is set."
},
"result_schema": {
"type": "object",
"description": "JSON Schema for a required final structured result. Local child agents must call report_result; external A2A agents must return a structured data artifact."
},
"message_schema": {
"type": "object",
"description": "JSON Schema for structured progress messages from local child agents. When set, the child receives report_task_progress. External A2A targets reject this option explicitly."
},
"public_context": {
"type": "object",
"description": "Agent-handoff-only non-secret structured context to include with the instructions."
},
"wait_timeout_secs": {
"type": "integer",
"minimum": 1,
"maximum": 86400,
"description": "External-A2A-only foreground timeout."
},
"wake_on_completion": {
"type": "boolean",
"description": "External-A2A-only control for background completion wake-ups."
}
},
"required": ["name", "instructions", "target"],
"additionalProperties": false
})
}
fn hints(&self) -> crate::tool_types::ToolHints {
let mut hints = crate::tool_types::ToolHints::default()
.with_long_running(true)
.with_concurrency_class(SPAWN_AGENT_CONCURRENCY_CLASS);
if self.provider_for("external_a2a").is_some() {
hints = hints.with_open_world(true);
}
hints
}
async fn execute(&self, _arguments: serde_json::Value) -> ToolExecutionResult {
ToolExecutionResult::tool_error(
"spawn_agent requires context. This tool must be executed with session context.",
)
}
async fn execute_with_context(
&self,
arguments: serde_json::Value,
context: &ToolContext,
) -> ToolExecutionResult {
let target_type = match arguments
.get("target")
.and_then(|target| target.get("type"))
.and_then(serde_json::Value::as_str)
{
Some(target_type) => target_type,
None => {
return ToolExecutionResult::tool_error("Missing required parameter: target.type");
}
};
let Some(provider) = self.provider_for(target_type) else {
let supported = self.target_types().join(", ");
return ToolExecutionResult::tool_error(format!(
"Unsupported spawn_agent target.type: \"{target_type}\". Supported target types: {supported}"
));
};
if let Err(error) = validate_spawn_agent_target_fields(&arguments, target_type) {
return ToolExecutionResult::tool_error(error);
}
if target_type == "external_a2a"
&& arguments
.get("lifetime")
.and_then(serde_json::Value::as_str)
.is_some_and(|value| value == "detached")
{
return ToolExecutionResult::tool_error(
"lifetime=\"detached\" is only valid for local session targets (subagent or agent), not external_a2a.",
);
}
if target_type == "external_a2a"
&& arguments
.get("message_schema")
.is_some_and(|schema| !schema.is_null())
{
return ToolExecutionResult::tool_error(
"message_schema is not supported for external_a2a targets because remote agents cannot receive report_task_progress.",
);
}
provider.execute_with_context(arguments, context).await
}
fn requires_context(&self) -> bool {
true
}
}