Skip to main content

ai_agent/tools/agent/
agent_tool.rs

1// Source: ~/claudecode/openclaudecode/src/tools/AgentTool/
2
3//! AgentTool struct that implements the Tool trait from tools/types.rs.
4//! Replaces the inline closures used in agent.rs for the "Agent" tool.
5
6use 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/// Configuration for the AgentTool, held behind an Arc for cloning into closures.
17#[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    /// Parent messages for fork subagent logic. Used in the query() path.
31    pub parent_messages: Vec<Message>,
32    /// Parent user context for fork subagent. Used in the query() path.
33    pub parent_user_context: HashMap<String, String>,
34    /// Parent system context for fork subagent. Used in the query() path.
35    pub parent_system_context: HashMap<String, String>,
36    /// Parent session ID for sidechain transcript recording.
37    pub parent_session_id: Option<String>,
38}
39
40/// A tool that spawns subagents to handle complex, multi-step tasks autonomously.
41///
42/// Implements the `Tool` trait from `tools/types.rs` and mirrors the executor
43/// logic previously implemented as inline closures in `agent.rs`.
44pub struct AgentTool {
45    config: AgentToolConfig,
46}
47
48impl AgentTool {
49    /// Create a new AgentTool with the given configuration.
50    pub fn new(config: AgentToolConfig) -> Self {
51        Self { config }
52    }
53
54    /// Get a reference to the tool's configuration.
55    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        // Extract ALL parameters from input
131        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        // Build system prompt for subagent
169        let system_prompt =
170            crate::agent::build_agent_system_prompt(&description, subagent_type.as_deref());
171
172        // Create sub-agent engine with proper system prompt
173        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        // Register all tool executors on the sub-engine
199        crate::agent::register_all_tool_executors(&mut sub_engine);
200
201        // Initialize agent MCP servers — merges MCP tools into subagent engine
202        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                // Merge MCP tool definitions into sub_engine's tool list
213                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                        // Register MCP tool executor via registry
223                        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        // Fork subagent detection: when no subagent_type is specified AND fork is enabled
248        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        // Fork subagent: configure cache-safe params and forked messages
256        if is_fork {
257            // Empty system prompt for prompt cache sharing with parent
258            sub_engine.config.system_prompt = Some(String::new());
259            // Inherit parent's context for cache sharing
260            sub_engine.config.user_context = config.parent_user_context.clone();
261            sub_engine.config.system_context = config.parent_system_context.clone();
262            // Use fork agent definition's max_turns
263            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            // Build forked messages for prompt cache sharing
266            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        // Execute subagent task
274        let result: Result<ToolResult, AgentError> = if run_in_background {
275            // Spawn subagent in a tokio task and return immediately with a task ID
276            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                        // Distinguish killed vs failed for background agents too
287                        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                    // Distinguish abort/kill from other errors.
329                    // Matches TypeScript agentToolUtils.ts:638-681:
330                    // AbortError -> status 'killed' with finalMessage from extractPartialResult
331                    // Other errors -> status 'failed' with error message
332                    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        // Cleanup MCP connections after subagent completion
370        if let Some(mcp_result) = mcp_result {
371            (mcp_result.cleanup)();
372        }
373
374        result
375    }
376}
377
378/// Create a tool executor closure from an AgentTool for use with
379/// `QueryEngine::register_tool()`.
380///
381/// The executor clones the AgentTool (via Arc) and calls its `execute` method.
382pub 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}