1use 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#[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
20pub 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 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 let parsed_query = parse_tool_search_query(query);
93
94 let matches = match &parsed_query {
95 ToolSearchQuery::Select(requested) => {
96 let mut found = Vec::new();
98 let mut missing = Vec::new();
99
100 for tool_name in requested {
101 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 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 let output = ToolSearchOutput {
156 matches: matches.clone(),
157 query: query.to_string(),
158 total_deferred_tools: total_deferred,
159 pending_mcp_servers: None, };
161
162 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 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 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}