use std::collections::HashMap;
use std::sync::Arc;
use crate::error::AgentError;
use crate::query_engine::{QueryEngine, QueryEngineConfig};
use crate::tools::types::{Tool, ToolInputSchema};
use crate::types::ToolResult;
use crate::types::{Message, ToolContext};
use super::agent_tool_utils::extract_partial_result_from_engine;
#[derive(Clone)]
pub struct AgentToolConfig {
pub cwd: String,
pub api_key: Option<String>,
pub base_url: Option<String>,
pub model: String,
pub tool_pool: Vec<crate::types::ToolDefinition>,
pub abort_controller: Arc<crate::utils::AbortController>,
pub can_use_tool: Option<
Arc<dyn Fn(crate::types::ToolDefinition, serde_json::Value) -> crate::permission::PermissionResult + Send + Sync>,
>,
pub on_event: Option<Arc<dyn Fn(crate::types::AgentEvent) + Send + Sync>>,
pub thinking: Option<crate::types::ThinkingConfig>,
pub parent_messages: Vec<Message>,
pub parent_user_context: HashMap<String, String>,
pub parent_system_context: HashMap<String, String>,
pub parent_session_id: Option<String>,
}
pub struct AgentTool {
config: AgentToolConfig,
}
impl AgentTool {
pub fn new(config: AgentToolConfig) -> Self {
Self { config }
}
pub fn config(&self) -> &AgentToolConfig {
&self.config
}
}
impl Tool for AgentTool {
fn name(&self) -> &str {
"Agent"
}
fn description(&self) -> &str {
"Launch a new agent to handle complex, multi-step tasks autonomously. Use this tool to spawn specialized subagents."
}
fn input_schema(&self) -> ToolInputSchema {
ToolInputSchema {
schema_type: "object".to_string(),
properties: serde_json::json!({
"description": {
"type": "string",
"description": "A short description (3-5 words) summarizing what the agent will do"
},
"subagent_type": {
"type": "string",
"description": "The type of subagent to use. If omitted, uses the general-purpose agent."
},
"prompt": {
"type": "string",
"description": "The task prompt for the subagent to execute"
},
"model": {
"type": "string",
"description": "Optional model override for this subagent"
},
"max_turns": {
"type": "number",
"description": "Maximum number of turns for this subagent (default: 10)"
},
"run_in_background": {
"type": "boolean",
"description": "Whether to run the agent in the background (default: false)"
},
"name": {
"type": "string",
"description": "Optional name for the subagent"
},
"team_name": {
"type": "string",
"description": "Optional team name for the subagent"
},
"mode": {
"type": "string",
"description": "Optional permission mode for the subagent"
},
"cwd": {
"type": "string",
"description": "Optional working directory for the subagent"
},
"isolation": {
"type": "string",
"enum": ["worktree", "remote"],
"description": "Isolation mode: 'worktree' for git worktree, 'remote' for remote CCR"
}
}),
required: Some(vec!["description".to_string(), "prompt".to_string()]),
}
}
async fn execute(
&self,
input: serde_json::Value,
_ctx: &ToolContext,
) -> Result<ToolResult, AgentError> {
let config = self.config.clone();
let description = input["description"].as_str().unwrap_or("subagent").to_string();
let subagent_prompt = input["prompt"].as_str().unwrap_or("").to_string();
let subagent_model = input["model"]
.as_str()
.map(|s| s.to_string())
.unwrap_or_else(|| config.model.clone());
let max_turns = input["max_turns"]
.as_u64()
.or_else(|| input["maxTurns"].as_u64())
.unwrap_or(10) as u32;
let subagent_type = input["subagent_type"]
.as_str()
.or_else(|| input["subagentType"].as_str())
.map(|s| s.to_string());
let run_in_background = input["run_in_background"]
.as_bool()
.or_else(|| input["runInBackground"].as_bool())
.unwrap_or(false);
let agent_name = input["name"].as_str().map(|s| s.to_string());
let _team_name = input["team_name"]
.as_str()
.or_else(|| input["teamName"].as_str())
.map(|s| s.to_string());
let _mode = input["mode"].as_str().map(|s| s.to_string());
let subagent_cwd = input["cwd"]
.as_str()
.map(|s| s.to_string())
.unwrap_or_else(|| config.cwd.clone());
let _isolation = input["isolation"].as_str().map(|s| s.to_string());
let system_prompt =
crate::agent::build_agent_system_prompt(&description, subagent_type.as_deref());
let mut sub_engine = QueryEngine::new(QueryEngineConfig {
cwd: subagent_cwd,
model: subagent_model.to_string(),
api_key: config.api_key.clone(),
base_url: config.base_url.clone(),
tools: config.tool_pool.clone(),
system_prompt: Some(system_prompt),
max_turns,
max_budget_usd: None,
max_tokens: crate::utils::context::get_max_output_tokens_for_model(&subagent_model) as u32,
fallback_model: None,
user_context: HashMap::new(),
system_context: HashMap::new(),
can_use_tool: config.can_use_tool.clone(),
on_event: config.on_event.clone(),
thinking: config.thinking.clone(),
abort_controller: Some(config.abort_controller.clone()),
token_budget: None,
agent_id: agent_name.clone().or_else(|| Some(description.to_string())),
session_state: None,
loaded_nested_memory_paths: std::collections::HashSet::new(),
task_budget: None,
orphaned_permission: None,
});
crate::agent::register_all_tool_executors(&mut sub_engine);
let mcp_result = {
let mcp_servers =
crate::services::mcp::agent_mcp::parse_agent_mcp_servers(&input);
if !mcp_servers.is_empty() {
let result =
crate::services::mcp::agent_mcp::initialize_agent_mcp_servers(
&mcp_servers, None,
)
.await;
let mcp_tool_count = result.tools.len();
let mcp_conn_count = result.connections.len();
if mcp_tool_count > 0 {
for mcp_tool in &result.tools {
sub_engine
.config
.tools
.push(mcp_tool.clone());
let mcp_registry =
crate::services::mcp::tool_executor::McpToolRegistry::new();
let executor = crate::services::mcp::tool_executor::
create_named_mcp_executor(
mcp_registry,
&mcp_tool.name,
);
sub_engine.register_tool(mcp_tool.name.clone(), executor);
}
log::info!(
"[Subagent: {}] Added {} MCP tools from {} server(s)",
description,
mcp_tool_count,
mcp_conn_count
);
}
Some(result)
} else {
None
}
};
let is_fork = subagent_type.is_none()
&& crate::tools::agent::prompt::is_fork_subagent_enabled()
&& !config.parent_messages.iter().any(|m| {
m.role == crate::types::MessageRole::User
&& m.content.contains(crate::tools::agent::constants::FORK_BOILERPLATE_TAG)
});
if is_fork {
sub_engine.config.system_prompt = Some(String::new());
sub_engine.config.user_context = config.parent_user_context.clone();
sub_engine.config.system_context = config.parent_system_context.clone();
let fork_agent = crate::tools::agent::fork_subagent::fork_agent();
sub_engine.config.max_turns = fork_agent.max_turns.unwrap_or(200) as u32;
let forked_messages = crate::tools::agent::fork_subagent::build_forked_messages_from_sdk(
&config.parent_messages,
&subagent_prompt,
);
sub_engine.set_messages(forked_messages);
}
let result: Result<ToolResult, AgentError> = if run_in_background {
let task_id = uuid::Uuid::new_v4().to_string();
let task_id_display = task_id.clone();
let prompt = subagent_prompt.clone();
let desc = description.clone();
tokio::spawn(async move {
match sub_engine.submit_message(&prompt).await {
Ok((result_text, _)) => {
log::info!("[BackgroundAgent:{task_id}] {desc}: {result_text}");
}
Err(e) => {
let is_killed = matches!(e, AgentError::UserAborted);
if is_killed {
let partial = super::agent_tool_utils::extract_partial_result_from_engine(&sub_engine.messages)
.unwrap_or_else(|| "No output produced".to_string());
log::info!(
"[BackgroundAgent:{task_id}] {desc}: Killed - partial: {}",
partial
);
} else {
log::error!("[BackgroundAgent:{task_id}] {desc}: Failed - {e}");
}
}
}
});
Ok(ToolResult {
result_type: "text".to_string(),
tool_use_id: "agent_tool".to_string(),
content: format!(
"[Background subagent '{}'] Task {} started. Use TaskOutput(task_id=\"{}\") to retrieve results.",
description, task_id_display, task_id_display
),
is_error: Some(false),
was_persisted: None,
})
} else {
match sub_engine.submit_message(&subagent_prompt).await {
Ok((result_text, _)) => {
let mut content = format!("[Subagent: {}]", description);
if let Some(ref name) = agent_name {
content = format!("[Subagent: {} ({})]", description, name);
}
content = format!("{}\n\n{}", content, result_text);
Ok(ToolResult {
result_type: "text".to_string(),
tool_use_id: "agent_tool".to_string(),
content,
is_error: Some(false),
was_persisted: None,
})
}
Err(e) => {
let is_killed = matches!(e, AgentError::UserAborted)
|| config.abort_controller.is_aborted();
if is_killed {
let partial = extract_partial_result_from_engine(&sub_engine.messages)
.unwrap_or_else(|| "No output produced".to_string());
log::info!(
"[Subagent: {}] Killed - partial result: {}",
description, partial
);
Ok(ToolResult {
result_type: "text".to_string(),
tool_use_id: "agent_tool".to_string(),
content: format!(
"[Subagent: {}] Status: killed\nFinal output: {}",
description, partial
),
is_error: Some(true),
was_persisted: None,
})
} else {
log::error!("[Subagent: {}] Failed: {}", description, e);
Ok(ToolResult {
result_type: "text".to_string(),
tool_use_id: "agent_tool".to_string(),
content: format!(
"[Subagent: {}] Status: failed\nError: {}",
description, e
),
is_error: Some(true),
was_persisted: None,
})
}
}
}
};
if let Some(mcp_result) = mcp_result {
(mcp_result.cleanup)();
}
result
}
}
pub fn create_agent_tool_executor(
tool: Arc<AgentTool>,
) -> impl Fn(serde_json::Value, &ToolContext) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<ToolResult, AgentError>> + Send>> + Send + Sync + 'static {
move |input: serde_json::Value,
ctx: &ToolContext|
-> std::pin::Pin<Box<dyn std::future::Future<Output = Result<ToolResult, AgentError>> + Send>> {
let tool_clone = Arc::clone(&tool);
let cwd = ctx.cwd.clone();
let abort_signal = ctx.abort_signal.clone();
Box::pin(async move {
let ctx2 = ToolContext {
cwd,
abort_signal: abort_signal.clone(),
};
tool_clone.execute(input, &ctx2).await
})
}
}