1use std::collections::HashMap;
7use std::sync::Arc;
8
9use crate::error::AgentError;
10use crate::query_engine::{QueryEngine, QueryEngineConfig};
11use crate::tools::types::{Tool, ToolInputSchema};
12use crate::types::ToolResult;
13use crate::types::{Message, ToolContext};
14use super::agent_tool_utils::extract_partial_result_from_engine;
15
16#[derive(Clone)]
18pub struct AgentToolConfig {
19 pub cwd: String,
20 pub api_key: Option<String>,
21 pub base_url: Option<String>,
22 pub model: String,
23 pub tool_pool: Vec<crate::types::ToolDefinition>,
24 pub abort_controller: Arc<crate::utils::AbortController>,
25 pub can_use_tool: Option<
26 Arc<dyn Fn(crate::types::ToolDefinition, serde_json::Value) -> crate::permission::PermissionResult + Send + Sync>,
27 >,
28 pub on_event: Option<Arc<dyn Fn(crate::types::AgentEvent) + Send + Sync>>,
29 pub thinking: Option<crate::types::ThinkingConfig>,
30 pub parent_messages: Vec<Message>,
32 pub parent_user_context: HashMap<String, String>,
34 pub parent_system_context: HashMap<String, String>,
36 pub parent_session_id: Option<String>,
38}
39
40pub struct AgentTool {
45 config: AgentToolConfig,
46}
47
48impl AgentTool {
49 pub fn new(config: AgentToolConfig) -> Self {
51 Self { config }
52 }
53
54 pub fn config(&self) -> &AgentToolConfig {
56 &self.config
57 }
58}
59
60impl Tool for AgentTool {
61 fn name(&self) -> &str {
62 "Agent"
63 }
64
65 fn description(&self) -> &str {
66 "Launch a new agent to handle complex, multi-step tasks autonomously. Use this tool to spawn specialized subagents."
67 }
68
69 fn input_schema(&self) -> ToolInputSchema {
70 ToolInputSchema {
71 schema_type: "object".to_string(),
72 properties: serde_json::json!({
73 "description": {
74 "type": "string",
75 "description": "A short description (3-5 words) summarizing what the agent will do"
76 },
77 "subagent_type": {
78 "type": "string",
79 "description": "The type of subagent to use. If omitted, uses the general-purpose agent."
80 },
81 "prompt": {
82 "type": "string",
83 "description": "The task prompt for the subagent to execute"
84 },
85 "model": {
86 "type": "string",
87 "description": "Optional model override for this subagent"
88 },
89 "max_turns": {
90 "type": "number",
91 "description": "Maximum number of turns for this subagent (default: 10)"
92 },
93 "run_in_background": {
94 "type": "boolean",
95 "description": "Whether to run the agent in the background (default: false)"
96 },
97 "name": {
98 "type": "string",
99 "description": "Optional name for the subagent"
100 },
101 "team_name": {
102 "type": "string",
103 "description": "Optional team name for the subagent"
104 },
105 "mode": {
106 "type": "string",
107 "description": "Optional permission mode for the subagent"
108 },
109 "cwd": {
110 "type": "string",
111 "description": "Optional working directory for the subagent"
112 },
113 "isolation": {
114 "type": "string",
115 "enum": ["worktree", "remote"],
116 "description": "Isolation mode: 'worktree' for git worktree, 'remote' for remote CCR"
117 }
118 }),
119 required: Some(vec!["description".to_string(), "prompt".to_string()]),
120 }
121 }
122
123 async fn execute(
124 &self,
125 input: serde_json::Value,
126 _ctx: &ToolContext,
127 ) -> Result<ToolResult, AgentError> {
128 let config = self.config.clone();
129
130 let description = input["description"].as_str().unwrap_or("subagent").to_string();
132 let subagent_prompt = input["prompt"].as_str().unwrap_or("").to_string();
133 let subagent_model = input["model"]
134 .as_str()
135 .map(|s| s.to_string())
136 .unwrap_or_else(|| config.model.clone());
137 let max_turns = input["max_turns"]
138 .as_u64()
139 .or_else(|| input["maxTurns"].as_u64())
140 .unwrap_or(10) as u32;
141
142 let subagent_type = input["subagent_type"]
143 .as_str()
144 .or_else(|| input["subagentType"].as_str())
145 .map(|s| s.to_string());
146
147 let run_in_background = input["run_in_background"]
148 .as_bool()
149 .or_else(|| input["runInBackground"].as_bool())
150 .unwrap_or(false);
151
152 let agent_name = input["name"].as_str().map(|s| s.to_string());
153
154 let _team_name = input["team_name"]
155 .as_str()
156 .or_else(|| input["teamName"].as_str())
157 .map(|s| s.to_string());
158
159 let _mode = input["mode"].as_str().map(|s| s.to_string());
160
161 let subagent_cwd = input["cwd"]
162 .as_str()
163 .map(|s| s.to_string())
164 .unwrap_or_else(|| config.cwd.clone());
165
166 let _isolation = input["isolation"].as_str().map(|s| s.to_string());
167
168 let system_prompt =
170 crate::agent::build_agent_system_prompt(&description, subagent_type.as_deref());
171
172 let mut sub_engine = QueryEngine::new(QueryEngineConfig {
174 cwd: subagent_cwd,
175 model: subagent_model.to_string(),
176 api_key: config.api_key.clone(),
177 base_url: config.base_url.clone(),
178 tools: config.tool_pool.clone(),
179 system_prompt: Some(system_prompt),
180 max_turns,
181 max_budget_usd: None,
182 max_tokens: crate::utils::context::get_max_output_tokens_for_model(&subagent_model) as u32,
183 fallback_model: None,
184 user_context: HashMap::new(),
185 system_context: HashMap::new(),
186 can_use_tool: config.can_use_tool.clone(),
187 on_event: config.on_event.clone(),
188 thinking: config.thinking.clone(),
189 abort_controller: Some(config.abort_controller.clone()),
190 token_budget: None,
191 agent_id: agent_name.clone().or_else(|| Some(description.to_string())),
192 session_state: None,
193 loaded_nested_memory_paths: std::collections::HashSet::new(),
194 task_budget: None,
195 orphaned_permission: None,
196 });
197
198 crate::agent::register_all_tool_executors(&mut sub_engine);
200
201 let mcp_result = {
203 let mcp_servers =
204 crate::services::mcp::agent_mcp::parse_agent_mcp_servers(&input);
205 if !mcp_servers.is_empty() {
206 let result =
207 crate::services::mcp::agent_mcp::initialize_agent_mcp_servers(
208 &mcp_servers, None,
209 )
210 .await;
211
212 let mcp_tool_count = result.tools.len();
214 let mcp_conn_count = result.connections.len();
215 if mcp_tool_count > 0 {
216 for mcp_tool in &result.tools {
217 sub_engine
218 .config
219 .tools
220 .push(mcp_tool.clone());
221
222 let mcp_registry =
224 crate::services::mcp::tool_executor::McpToolRegistry::new();
225 let executor = crate::services::mcp::tool_executor::
226 create_named_mcp_executor(
227 mcp_registry,
228 &mcp_tool.name,
229 );
230 sub_engine.register_tool(mcp_tool.name.clone(), executor);
231 }
232
233 log::info!(
234 "[Subagent: {}] Added {} MCP tools from {} server(s)",
235 description,
236 mcp_tool_count,
237 mcp_conn_count
238 );
239 }
240
241 Some(result)
242 } else {
243 None
244 }
245 };
246
247 let is_fork = subagent_type.is_none()
249 && crate::tools::agent::prompt::is_fork_subagent_enabled()
250 && !config.parent_messages.iter().any(|m| {
251 m.role == crate::types::MessageRole::User
252 && m.content.contains(crate::tools::agent::constants::FORK_BOILERPLATE_TAG)
253 });
254
255 if is_fork {
257 sub_engine.config.system_prompt = Some(String::new());
259 sub_engine.config.user_context = config.parent_user_context.clone();
261 sub_engine.config.system_context = config.parent_system_context.clone();
262 let fork_agent = crate::tools::agent::fork_subagent::fork_agent();
264 sub_engine.config.max_turns = fork_agent.max_turns.unwrap_or(200) as u32;
265 let forked_messages = crate::tools::agent::fork_subagent::build_forked_messages_from_sdk(
267 &config.parent_messages,
268 &subagent_prompt,
269 );
270 sub_engine.set_messages(forked_messages);
271 }
272
273 let result: Result<ToolResult, AgentError> = if run_in_background {
275 let task_id = uuid::Uuid::new_v4().to_string();
277 let task_id_display = task_id.clone();
278 let prompt = subagent_prompt.clone();
279 let desc = description.clone();
280 tokio::spawn(async move {
281 match sub_engine.submit_message(&prompt).await {
282 Ok((result_text, _)) => {
283 log::info!("[BackgroundAgent:{task_id}] {desc}: {result_text}");
284 }
285 Err(e) => {
286 let is_killed = matches!(e, AgentError::UserAborted);
288 if is_killed {
289 let partial = super::agent_tool_utils::extract_partial_result_from_engine(&sub_engine.messages)
290 .unwrap_or_else(|| "No output produced".to_string());
291 log::info!(
292 "[BackgroundAgent:{task_id}] {desc}: Killed - partial: {}",
293 partial
294 );
295 } else {
296 log::error!("[BackgroundAgent:{task_id}] {desc}: Failed - {e}");
297 }
298 }
299 }
300 });
301 Ok(ToolResult {
302 result_type: "text".to_string(),
303 tool_use_id: "agent_tool".to_string(),
304 content: format!(
305 "[Background subagent '{}'] Task {} started. Use TaskOutput(task_id=\"{}\") to retrieve results.",
306 description, task_id_display, task_id_display
307 ),
308 is_error: Some(false),
309 was_persisted: None,
310 })
311 } else {
312 match sub_engine.submit_message(&subagent_prompt).await {
313 Ok((result_text, _)) => {
314 let mut content = format!("[Subagent: {}]", description);
315 if let Some(ref name) = agent_name {
316 content = format!("[Subagent: {} ({})]", description, name);
317 }
318 content = format!("{}\n\n{}", content, result_text);
319 Ok(ToolResult {
320 result_type: "text".to_string(),
321 tool_use_id: "agent_tool".to_string(),
322 content,
323 is_error: Some(false),
324 was_persisted: None,
325 })
326 }
327 Err(e) => {
328 let is_killed = matches!(e, AgentError::UserAborted)
333 || config.abort_controller.is_aborted();
334
335 if is_killed {
336 let partial = extract_partial_result_from_engine(&sub_engine.messages)
337 .unwrap_or_else(|| "No output produced".to_string());
338 log::info!(
339 "[Subagent: {}] Killed - partial result: {}",
340 description, partial
341 );
342 Ok(ToolResult {
343 result_type: "text".to_string(),
344 tool_use_id: "agent_tool".to_string(),
345 content: format!(
346 "[Subagent: {}] Status: killed\nFinal output: {}",
347 description, partial
348 ),
349 is_error: Some(true),
350 was_persisted: None,
351 })
352 } else {
353 log::error!("[Subagent: {}] Failed: {}", description, e);
354 Ok(ToolResult {
355 result_type: "text".to_string(),
356 tool_use_id: "agent_tool".to_string(),
357 content: format!(
358 "[Subagent: {}] Status: failed\nError: {}",
359 description, e
360 ),
361 is_error: Some(true),
362 was_persisted: None,
363 })
364 }
365 }
366 }
367 };
368
369 if let Some(mcp_result) = mcp_result {
371 (mcp_result.cleanup)();
372 }
373
374 result
375 }
376}
377
378pub fn create_agent_tool_executor(
383 tool: Arc<AgentTool>,
384) -> impl Fn(serde_json::Value, &ToolContext) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<ToolResult, AgentError>> + Send>> + Send + Sync + 'static {
385 move |input: serde_json::Value,
386 ctx: &ToolContext|
387 -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<ToolResult, AgentError>> + Send>> {
388 let tool_clone = Arc::clone(&tool);
389 let cwd = ctx.cwd.clone();
390 let abort_signal = ctx.abort_signal.clone();
391 Box::pin(async move {
392 let ctx2 = ToolContext {
393 cwd,
394 abort_signal: abort_signal.clone(),
395 };
396 tool_clone.execute(input, &ctx2).await
397 })
398 }
399}