agent_code_lib/tools/
tool_search.rs1use async_trait::async_trait;
7use serde_json::json;
8
9use super::{Tool, ToolContext, ToolResult};
10use crate::error::ToolError;
11
12pub struct ToolSearchTool;
13
14#[async_trait]
15impl Tool for ToolSearchTool {
16 fn name(&self) -> &'static str {
17 "ToolSearch"
18 }
19
20 fn description(&self) -> &'static str {
21 "Search for available tools by name or keyword. Use 'select:Name' \
22 for direct lookup, or keywords to search descriptions."
23 }
24
25 fn input_schema(&self) -> serde_json::Value {
26 json!({
27 "type": "object",
28 "required": ["query"],
29 "properties": {
30 "query": {
31 "type": "string",
32 "description": "Query: 'select:ToolName' for direct lookup, or keywords"
33 },
34 "max_results": {
35 "type": "integer",
36 "description": "Maximum results to return (default: 5)",
37 "default": 5
38 }
39 }
40 })
41 }
42
43 fn is_read_only(&self) -> bool {
44 true
45 }
46
47 fn is_concurrency_safe(&self) -> bool {
48 true
49 }
50
51 async fn call(
52 &self,
53 input: serde_json::Value,
54 _ctx: &ToolContext,
55 ) -> Result<ToolResult, ToolError> {
56 let query = input
57 .get("query")
58 .and_then(|v| v.as_str())
59 .ok_or_else(|| ToolError::InvalidInput("'query' is required".into()))?;
60
61 let max_results = input
62 .get("max_results")
63 .and_then(|v| v.as_u64())
64 .unwrap_or(5) as usize;
65
66 let query_lower = query.to_lowercase();
69
70 if let Some(names) = query_lower.strip_prefix("select:") {
72 let requested: Vec<&str> = names.split(',').map(|s| s.trim()).collect();
73 let mut results = Vec::new();
74 for name in &requested {
75 results.push(format!("Tool '{}' — use it directly by name", name));
76 }
77 return Ok(ToolResult::success(format!(
78 "Selected {} tool(s):\n{}",
79 results.len(),
80 results.join("\n")
81 )));
82 }
83
84 let terms: Vec<&str> = query_lower.split_whitespace().collect();
86 let mut matches: Vec<(String, String, usize)> = Vec::new();
87
88 let tool_info = vec![
91 ("Bash", "Execute shell commands"),
92 ("FileRead", "Read file contents with line numbers"),
93 ("FileWrite", "Write or create files"),
94 ("FileEdit", "Search-and-replace editing in files"),
95 ("Grep", "Regex content search powered by ripgrep"),
96 ("Glob", "Find files matching glob patterns"),
97 ("Agent", "Spawn subagents for parallel tasks"),
98 ("WebFetch", "Fetch content from URLs"),
99 ("AskUserQuestion", "Ask the user interactive questions"),
100 ("NotebookEdit", "Edit Jupyter notebook cells"),
101 ("ToolSearch", "Search for available tools"),
102 ];
103
104 for (name, desc) in &tool_info {
105 let name_lower = name.to_lowercase();
106 let desc_lower = desc.to_lowercase();
107 let mut score = 0usize;
108
109 for term in &terms {
110 if name_lower.contains(term) {
111 score += 10;
112 }
113 if desc_lower.contains(term) {
114 score += 2;
115 }
116 }
117
118 if score > 0 {
119 matches.push((name.to_string(), desc.to_string(), score));
120 }
121 }
122
123 matches.sort_by_key(|m| std::cmp::Reverse(m.2));
124 matches.truncate(max_results);
125
126 if matches.is_empty() {
127 Ok(ToolResult::success(format!(
128 "No tools found matching '{query}'. Available tools: {}",
129 tool_info
130 .iter()
131 .map(|(n, _)| *n)
132 .collect::<Vec<_>>()
133 .join(", ")
134 )))
135 } else {
136 let results: Vec<String> = matches
137 .iter()
138 .map(|(name, desc, _)| format!("- {name}: {desc}"))
139 .collect();
140 Ok(ToolResult::success(format!(
141 "Found {} tool(s):\n{}",
142 results.len(),
143 results.join("\n")
144 )))
145 }
146 }
147}