Skip to main content

agent_code_lib/tools/
tool_search.rs

1//! ToolSearch tool: discover deferred tools by keyword.
2//!
3//! Allows the agent to search for tools that aren't loaded by default.
4//! Supports direct selection (`select:ToolName`) and keyword search.
5
6use 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        // For now, search against the currently registered tools.
67        // In a full implementation, this would also search deferred/MCP tools.
68        let query_lower = query.to_lowercase();
69
70        // Direct selection: select:Name,Name2
71        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        // Keyword search against tool names and descriptions.
85        let terms: Vec<&str> = query_lower.split_whitespace().collect();
86        let mut matches: Vec<(String, String, usize)> = Vec::new();
87
88        // Search built-in tools (in a full implementation, this would
89        // also search deferred tools that aren't currently loaded).
90        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}