Skip to main content

ai_agent/tools/
search.rs

1// Source: ~/claudecode/openclaudecode/src/tools/ToolSearchTool/ToolSearchTool.ts
2use crate::error::AgentError;
3use crate::tools::config_tools::TOOL_SEARCH_TOOL_NAME;
4use crate::tools::deferred_tools::{
5    ToolSearchQuery, extract_discovered_tool_names, get_deferred_tool_names, is_deferred_tool,
6    parse_tool_search_query, search_tools_with_keywords,
7};
8use crate::types::*;
9
10/// ToolSearchTool result output
11#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
12pub struct ToolSearchOutput {
13    pub matches: Vec<String>,
14    pub query: String,
15    pub total_deferred_tools: usize,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub pending_mcp_servers: Option<Vec<String>>,
18}
19
20/// ToolSearchTool - discovers deferred tools via search
21pub struct ToolSearchTool;
22
23impl ToolSearchTool {
24    pub fn new() -> Self {
25        Self
26    }
27
28    pub fn name(&self) -> &str {
29        TOOL_SEARCH_TOOL_NAME
30    }
31
32    pub fn description(&self) -> &str {
33        "Fetches full schema definitions for deferred tools so they can be called. \
34         Deferred tools appear by name in <available-deferred-tools> messages. \
35         Until fetched, only the name is known — there is no parameter schema, so the tool cannot be invoked. \
36         This tool takes a query, matches it against the deferred tool list, and returns the matched tools' \
37         complete JSONSchema definitions inside a <functions> block. \
38         Query forms: \
39         - \"select:Read,Edit,Grep\" — fetch these exact tools by name \
40         - \"notebook jupyter\" — keyword search, up to max_results best matches \
41         - \"+slack send\" — require \"slack\" in the name, rank by remaining terms"
42    }
43
44    pub fn user_facing_name(&self, _input: Option<&serde_json::Value>) -> String {
45        "ToolSearch".to_string()
46    }
47
48    pub fn get_tool_use_summary(&self, input: Option<&serde_json::Value>) -> Option<String> {
49        input.and_then(|inp| inp["query"].as_str().map(String::from))
50    }
51
52    pub fn render_tool_result_message(
53        &self,
54        content: &serde_json::Value,
55    ) -> Option<String> {
56        content["content"].as_str().map(|s| s.to_string())
57    }
58
59    pub fn input_schema(&self) -> ToolInputSchema {
60        ToolInputSchema {
61            schema_type: "object".to_string(),
62            properties: serde_json::json!({
63                "query": {
64                    "type": "string",
65                    "description": "Query to find deferred tools. Use \"select:<tool_name>\" for direct selection, or keywords to search."
66                },
67                "max_results": {
68                    "type": "number",
69                    "description": "Maximum number of results to return (default: 5)"
70                }
71            }),
72            required: Some(vec!["query".to_string()]),
73        }
74    }
75
76    pub async fn execute(
77        &self,
78        input: serde_json::Value,
79        context: &ToolContext,
80    ) -> Result<ToolResult, AgentError> {
81        let query = input["query"].as_str().unwrap_or("");
82        let max_results = input["max_results"].as_u64().unwrap_or(5) as usize;
83
84        // Get all base tools to identify deferred subset
85        let all_tools = crate::tools::get_all_base_tools();
86        let deferred_tools: Vec<&ToolDefinition> =
87            all_tools.iter().filter(|t| is_deferred_tool(t)).collect();
88
89        let total_deferred = deferred_tools.len();
90
91        // Parse the query
92        let parsed_query = parse_tool_search_query(query);
93
94        let matches = match &parsed_query {
95            ToolSearchQuery::Select(requested) => {
96                // Direct tool selection
97                let mut found = Vec::new();
98                let mut missing = Vec::new();
99
100                for tool_name in requested {
101                    // Check deferred tools first, then all tools
102                    if let Some(tool) = deferred_tools.iter().find(|t| t.name == *tool_name) {
103                        if !found.contains(&tool.name) {
104                            found.push(tool.name.clone());
105                        }
106                    } else if let Some(tool) = all_tools.iter().find(|t| t.name == *tool_name) {
107                        // Tool is already loaded (not deferred) — still return it so model can proceed
108                        if !found.contains(&tool.name) {
109                            found.push(tool.name.clone());
110                        }
111                    } else {
112                        missing.push(tool_name.clone());
113                    }
114                }
115
116                if found.is_empty() {
117                    log::debug!(
118                        "ToolSearchTool: select failed — none found: {}",
119                        missing.join(", ")
120                    );
121                } else if !missing.is_empty() {
122                    log::debug!(
123                        "ToolSearchTool: partial select — found: {}, missing: {}",
124                        found.join(", "),
125                        missing.join(", ")
126                    );
127                } else {
128                    log::debug!("ToolSearchTool: selected {}", found.join(", "));
129                }
130                found
131            }
132            ToolSearchQuery::Keyword(q) => {
133                let results = search_tools_with_keywords(q, &deferred_tools, max_results);
134                log::debug!(
135                    "ToolSearchTool: keyword search for \"{}\", found {} matches",
136                    q,
137                    results.len()
138                );
139                results
140            }
141            ToolSearchQuery::KeywordWithRequired { .. } => {
142                let results = search_tools_with_keywords(query, &deferred_tools, max_results);
143                log::debug!(
144                    "ToolSearchTool: keyword search with required terms for \"{}\", found {} matches",
145                    query,
146                    results.len()
147                );
148                results
149            }
150        };
151
152        // Build result
153        // When matches exist, we return tool_reference blocks for API expansion.
154        // When no matches, we return plain text.
155        let output = ToolSearchOutput {
156            matches: matches.clone(),
157            query: query.to_string(),
158            total_deferred_tools: total_deferred,
159            pending_mcp_servers: None, // No MCP in Rust SDK yet
160        };
161
162        // Serialize to the structured content format
163        let content_value = if matches.is_empty() {
164            let deferred_names: Vec<&str> =
165                deferred_tools.iter().map(|t| t.name.as_str()).collect();
166            let names_str = deferred_names.join(", ");
167            serde_json::json!({
168                "type": "text",
169                "text": format!("No matching deferred tools found for query: \"{}\". Available deferred tools: {}", query, names_str)
170            })
171        } else {
172            // Return tool_reference blocks for API expansion
173            serde_json::json!(
174                matches
175                    .iter()
176                    .map(|name| {
177                        serde_json::json!({
178                            "type": "tool_reference",
179                            "tool_name": name
180                        })
181                    })
182                    .collect::<Vec<_>>()
183            )
184        };
185
186        Ok(ToolResult {
187            result_type: "text".to_string(),
188            tool_use_id: "".to_string(),
189            content: serde_json::to_string(&content_value).unwrap_or_default(),
190            is_error: Some(false),
191            was_persisted: None,
192        })
193    }
194
195    /// Build a tool_result with tool_reference blocks for the API
196    pub fn build_tool_reference_result(matches: &[String], tool_use_id: &str) -> serde_json::Value {
197        if matches.is_empty() {
198            serde_json::json!({
199                "type": "tool_result",
200                "tool_use_id": tool_use_id,
201                "content": "No matching deferred tools found."
202            })
203        } else {
204            serde_json::json!({
205                "type": "tool_result",
206                "tool_use_id": tool_use_id,
207                "content": matches.iter().map(|name| {
208                    serde_json::json!({
209                        "type": "tool_reference",
210                        "tool_name": name
211                    })
212                }).collect::<Vec<_>>()
213            })
214        }
215    }
216}
217
218impl Default for ToolSearchTool {
219    fn default() -> Self {
220        Self::new()
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn test_tool_search_tool_name() {
230        let tool = ToolSearchTool::new();
231        assert_eq!(tool.name(), TOOL_SEARCH_TOOL_NAME);
232    }
233
234    #[test]
235    fn test_tool_search_tool_schema() {
236        let tool = ToolSearchTool::new();
237        let schema = tool.input_schema();
238        assert_eq!(schema.schema_type, "object");
239        assert!(schema.required.is_some());
240        assert!(
241            schema
242                .required
243                .as_ref()
244                .unwrap()
245                .contains(&"query".to_string())
246        );
247    }
248
249    #[test]
250    fn test_build_tool_reference_result() {
251        let result = ToolSearchTool::build_tool_reference_result(
252            &["WebSearch".to_string(), "WebFetch".to_string()],
253            "tool_123",
254        );
255        assert_eq!(result["type"], "tool_result");
256        assert_eq!(result["tool_use_id"], "tool_123");
257        assert!(result["content"].is_array());
258        assert_eq!(result["content"].as_array().unwrap().len(), 2);
259        assert_eq!(result["content"][0]["type"], "tool_reference");
260        assert_eq!(result["content"][0]["tool_name"], "WebSearch");
261    }
262
263    #[test]
264    fn test_build_tool_reference_result_empty() {
265        let result = ToolSearchTool::build_tool_reference_result(&[], "tool_123");
266        assert_eq!(result["type"], "tool_result");
267        assert!(result["content"].is_string());
268    }
269
270    #[test]
271    fn test_extract_discovered_tool_names() {
272        let messages = vec![serde_json::json!({
273            "role": "user",
274            "content": [{
275                "type": "tool_result",
276                "content": [
277                    {"type": "tool_reference", "tool_name": "WebSearch"},
278                    {"type": "tool_reference", "tool_name": "WebFetch"}
279                ]
280            }]
281        })];
282        let discovered = extract_discovered_tool_names(&messages);
283        assert!(discovered.contains("WebSearch"));
284        assert!(discovered.contains("WebFetch"));
285    }
286}