use std::path::PathBuf;
use std::sync::Arc;
use async_trait::async_trait;
use serde::Deserialize;
use serde_json::json;
use uuid::Uuid;
use crate::agent::agent_loop::{BackgroundResult, BackgroundResultKind};
use crate::agent::events::AgentEvent;
use crate::agent::subagent::{SubAgentDef, SubAgentRegistry, SubAgentRunner};
use crate::error::{SdkError, SdkResult};
use crate::traits::llm_client::LlmClient;
use crate::traits::tool::{Tool, ToolDefinition};
pub struct SpawnSubAgentTool {
pub work_dir: PathBuf,
pub source_root: PathBuf,
pub llm_client: Arc<dyn LlmClient>,
pub event_tx: Option<tokio::sync::mpsc::UnboundedSender<AgentEvent>>,
pub registry: Arc<SubAgentRegistry>,
pub background_tx: Option<tokio::sync::mpsc::UnboundedSender<BackgroundResult>>,
}
#[derive(Debug, Deserialize)]
struct SubAgentRequest {
name: String,
prompt: String,
#[serde(default)]
system_prompt: Option<String>,
#[serde(default)]
description: Option<String>,
#[serde(default)]
allowed_tools: Vec<String>,
#[serde(default)]
disallowed_tools: Vec<String>,
#[serde(default)]
max_turns: Option<usize>,
#[serde(default)]
background: bool,
}
#[async_trait]
impl Tool for SpawnSubAgentTool {
fn definition(&self) -> ToolDefinition {
let available: Vec<String> = self
.registry
.list()
.iter()
.map(|d| format!("{}: {}", d.name, d.description))
.collect();
let available_desc = if available.is_empty() {
"No pre-registered subagents. Provide a system_prompt for inline definition.".to_string()
} else {
format!("Available subagents:\n{}", available.join("\n"))
};
ToolDefinition {
name: "spawn_subagent".to_string(),
description: format!(
"Spawn a subagent to handle a focused task in its own context window. \
The subagent works independently and returns results back to you. \
Use this to preserve your main context by delegating exploration, \
research, or self-contained tasks to a subagent.\n\n\
You can reference a registered subagent by name, or create an inline \
subagent by providing a system_prompt.\n\n\
Subagents CANNOT spawn other subagents.\n\n\
{available_desc}"
),
parameters: json!({
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of the subagent. Use a registered name (e.g. 'explore', 'plan', 'general-purpose') or a custom name with system_prompt for inline definition."
},
"prompt": {
"type": "string",
"description": "The task prompt to send to the subagent. Be specific about what you need."
},
"system_prompt": {
"type": "string",
"description": "Custom system prompt for an inline subagent definition. If omitted, uses the registered definition for the given name."
},
"description": {
"type": "string",
"description": "Optional description for inline definitions."
},
"allowed_tools": {
"type": "array",
"items": { "type": "string" },
"description": "Tool allowlist for inline definitions. Available: read_file, write_file, list_directory, search_files, web_search, run_command"
},
"disallowed_tools": {
"type": "array",
"items": { "type": "string" },
"description": "Tool denylist. Tools listed here are removed from the available set."
},
"max_turns": {
"type": "integer",
"description": "Maximum agentic turns before the subagent stops (default: 30)."
},
"background": {
"type": "boolean",
"description": "If true, run the subagent in the background (concurrent). Default: false (blocking)."
}
},
"required": ["name", "prompt"]
}),
}
}
async fn execute(&self, arguments: serde_json::Value) -> SdkResult<serde_json::Value> {
let request: SubAgentRequest =
serde_json::from_value(arguments).map_err(|e| SdkError::ToolExecution {
tool_name: "spawn_subagent".to_string(),
message: format!("Invalid arguments: {}", e),
})?;
if request.prompt.trim().is_empty() {
return Ok(json!({ "error": "prompt cannot be empty" }));
}
let def = if let Some(ref system_prompt) = request.system_prompt {
let mut def = SubAgentDef::new(
&request.name,
request.description.as_deref().unwrap_or("Inline subagent"),
system_prompt,
);
if !request.allowed_tools.is_empty() {
def = def.with_allowed_tools(request.allowed_tools.clone());
}
if !request.disallowed_tools.is_empty() {
def = def.with_disallowed_tools(request.disallowed_tools.clone());
}
if let Some(max_turns) = request.max_turns {
def = def.with_max_turns(max_turns);
}
def
} else if let Some(registered) = self.registry.get(&request.name) {
let mut def = registered.clone();
if let Some(max_turns) = request.max_turns {
def.max_turns = max_turns;
}
if !request.disallowed_tools.is_empty() {
def.disallowed_tools
.extend(request.disallowed_tools.iter().cloned());
}
def
} else {
return Ok(json!({
"error": format!(
"No subagent '{}' registered and no system_prompt provided for inline definition. \
Available: {}",
request.name,
self.registry.list().iter().map(|d| d.name.as_str()).collect::<Vec<_>>().join(", ")
)
}));
};
let runner = SubAgentRunner::new(
self.work_dir.clone(),
self.source_root.clone(),
self.llm_client.clone(),
);
let runner = if let Some(ref tx) = self.event_tx {
runner.with_event_sink(tx.clone())
} else {
runner
};
if request.background || def.background {
let agent_id = Uuid::new_v4();
let handle = runner.run_background(def.clone(), request.prompt);
let event_tx = self.event_tx.clone();
let background_tx = self.background_tx.clone();
let name = def.name.clone();
tokio::spawn(async move {
match handle.await {
Ok(Ok(result)) => {
if let Some(bg_tx) = background_tx {
let _ = bg_tx.send(BackgroundResult {
name: result.name.clone(),
kind: BackgroundResultKind::SubAgent,
content: result.final_content.clone(),
tokens_used: result.total_tokens,
});
}
if let Some(tx) = event_tx {
let _ = tx.send(AgentEvent::SubAgentCompleted {
agent_id: result.agent_id,
name: result.name,
tokens_used: result.total_tokens,
iterations: result.iterations,
tool_calls: result.tool_calls_count,
final_content: result.final_content,
});
}
}
Ok(Err(e)) => {
if let Some(tx) = event_tx {
let _ = tx.send(AgentEvent::SubAgentFailed {
agent_id,
name,
error: e.to_string(),
});
}
}
Err(e) => {
if let Some(tx) = event_tx {
let _ = tx.send(AgentEvent::SubAgentFailed {
agent_id,
name,
error: format!("Task join error: {}", e),
});
}
}
}
});
Ok(json!({
"status": "background",
"agent_id": agent_id.to_string(),
"name": def.name,
"message": "Subagent started in background. You will be notified when it completes — continue with other work."
}))
} else {
match runner.run(&def, &request.prompt).await {
Ok(result) => Ok(json!({
"status": "completed",
"name": result.name,
"agent_id": result.agent_id.to_string(),
"result": result.final_content,
"total_tokens": result.total_tokens,
"iterations": result.iterations,
"tool_calls": result.tool_calls_count,
})),
Err(e) => Ok(json!({
"status": "failed",
"name": def.name,
"error": e.to_string(),
})),
}
}
}
}