mermaid_cli/providers/tool/
mod.rs1pub mod computer_use;
19pub mod exec;
20pub mod filesystem;
21pub mod mcp;
22pub mod memory;
23pub mod policy_gate;
24pub mod subagent;
25pub mod web;
26pub mod web_client;
27
28use async_trait::async_trait;
29use std::collections::HashMap;
30use std::sync::Arc;
31
32use crate::domain::{ToolDefinition, ToolOutcome};
33
34use super::ctx::ExecContext;
35
36#[async_trait]
40pub trait ToolExecutor: Send + Sync {
41 fn name(&self) -> &'static str;
44
45 fn schema(&self) -> ToolDefinition;
51
52 fn is_internal(&self) -> bool {
58 false
59 }
60
61 async fn execute(&self, args: serde_json::Value, ctx: ExecContext) -> ToolOutcome;
65}
66
67pub struct ToolRegistry {
71 entries: HashMap<&'static str, Arc<dyn ToolExecutor>>,
72}
73
74impl ToolRegistry {
75 pub fn new() -> Self {
76 Self {
77 entries: HashMap::new(),
78 }
79 }
80
81 pub fn register(&mut self, tool: Arc<dyn ToolExecutor>) {
82 self.entries.insert(tool.name(), tool);
83 }
84
85 pub fn get(&self, name: &str) -> Option<Arc<dyn ToolExecutor>> {
86 self.entries.get(name).cloned()
87 }
88
89 pub fn len(&self) -> usize {
90 self.entries.len()
91 }
92
93 pub fn is_empty(&self) -> bool {
94 self.entries.is_empty()
95 }
96
97 pub fn names(&self) -> impl Iterator<Item = &'static str> + '_ {
98 self.entries.keys().copied()
99 }
100
101 pub fn describe_all(&self) -> Vec<ToolDefinition> {
107 self.entries
108 .values()
109 .filter(|t| !t.is_internal())
110 .map(|t| t.schema())
111 .collect()
112 }
113}
114
115impl Default for ToolRegistry {
116 fn default() -> Self {
117 let mut r = Self::new();
118 r.register(Arc::new(filesystem::ReadFileTool));
119 r.register(Arc::new(filesystem::WriteFileTool));
120 r.register(Arc::new(filesystem::EditFileTool));
121 r.register(Arc::new(filesystem::DeleteFileTool));
122 r.register(Arc::new(filesystem::CreateDirectoryTool));
123 r.register(Arc::new(exec::ExecuteCommandTool));
124 r.register(Arc::new(memory::MemoryTool));
125 r.register(Arc::new(mcp::McpToolProxy));
129 r
130 }
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum TuiMode {
140 Interactive,
141 Headless,
142}
143
144impl ToolRegistry {
145 pub fn build(
162 _config: &crate::app::Config,
163 mode: TuiMode,
164 providers: Arc<crate::providers::ProviderFactory>,
165 ) -> Arc<Self> {
166 let mut r = Self::new();
167 r.register(Arc::new(filesystem::ReadFileTool));
168 r.register(Arc::new(filesystem::WriteFileTool));
169 r.register(Arc::new(filesystem::EditFileTool));
170 r.register(Arc::new(filesystem::DeleteFileTool));
171 r.register(Arc::new(filesystem::CreateDirectoryTool));
172 r.register(Arc::new(exec::ExecuteCommandTool));
173 r.register(Arc::new(memory::MemoryTool));
174 r.register(Arc::new(mcp::McpToolProxy));
175
176 if let Some(key) = crate::utils::resolve_api_key("OLLAMA_API_KEY", None) {
177 r.register(Arc::new(web::WebSearchTool::new(key.clone())));
178 r.register(Arc::new(web::WebFetchTool::new(key)));
179 }
180
181 if mode == TuiMode::Interactive {
186 let backend = computer_use::probe();
187 if backend.is_usable() {
188 let driver = Arc::new(computer_use::ComputerUseDriver::new(backend));
189 r.register(Arc::new(computer_use::ScreenshotTool::new(driver.clone())));
190 r.register(Arc::new(computer_use::ClickTool::new(driver.clone())));
191 r.register(Arc::new(computer_use::TypeTextTool::new(driver.clone())));
192 r.register(Arc::new(computer_use::PressKeyTool::new(driver.clone())));
193 r.register(Arc::new(computer_use::ScrollTool::new(driver.clone())));
194 r.register(Arc::new(computer_use::MouseMoveTool::new(driver.clone())));
195 r.register(Arc::new(computer_use::ListWindowsTool::new(driver)));
196 }
197 }
198
199 let spawner = Arc::new(subagent::SubagentSpawner::new(providers));
204 r.register(Arc::new(subagent::SubagentTool::new(spawner)));
205
206 Arc::new(r)
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn default_registry_has_builtin_tools() {
216 let r = ToolRegistry::default();
217 for name in &[
218 "read_file",
219 "write_file",
220 "edit_file",
221 "delete_file",
222 "create_directory",
223 "execute_command",
224 "memory",
225 ] {
226 assert!(r.get(name).is_some(), "missing: {}", name);
227 }
228 assert!(r.get("not_a_tool").is_none());
229 assert!(r.len() >= 6);
230 }
231
232 #[test]
233 fn describe_all_returns_one_per_user_facing_tool() {
234 let r = ToolRegistry::default();
235 let schemas = r.describe_all();
236 let visible = r
239 .names()
240 .filter(|n| r.get(n).map(|t| !t.is_internal()).unwrap_or(false))
241 .count();
242 assert_eq!(schemas.len(), visible);
243 for schema in &schemas {
244 assert!(
245 r.get(&schema.name).is_some(),
246 "schema for unknown tool: {}",
247 schema.name
248 );
249 }
250 }
251
252 #[test]
253 fn mcp_proxy_is_registered_but_internal() {
254 let r = ToolRegistry::default();
255 let proxy = r.get("mcp_proxy").expect("mcp_proxy registered");
256 assert!(proxy.is_internal());
257 assert!(!r.describe_all().iter().any(|s| s.name == "mcp_proxy"));
258 }
259
260 #[test]
261 fn schema_name_matches_executor_name() {
262 let r = ToolRegistry::default();
263 for name in r.names() {
264 let tool = r.get(name).unwrap();
265 assert_eq!(tool.name(), tool.schema().name.as_str());
266 }
267 }
268
269 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
274
275 #[test]
276 fn build_registers_web_tools_when_key_present() {
277 let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
278 let prior = std::env::var("OLLAMA_API_KEY").ok();
279 unsafe {
280 std::env::set_var("OLLAMA_API_KEY", "test-key-build");
281 }
282 let cfg = crate::app::Config::default();
283 let providers = Arc::new(crate::providers::ProviderFactory::new(cfg.clone()));
284 let r = ToolRegistry::build(&cfg, TuiMode::Interactive, providers);
285 assert!(r.get("web_search").is_some(), "web_search registered");
286 assert!(r.get("web_fetch").is_some(), "web_fetch registered");
287 unsafe {
288 match prior {
289 Some(v) => std::env::set_var("OLLAMA_API_KEY", v),
290 None => std::env::remove_var("OLLAMA_API_KEY"),
291 }
292 }
293 }
294
295 #[test]
296 fn build_skips_web_tools_without_key() {
297 let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
298 let prior = std::env::var("OLLAMA_API_KEY").ok();
299 unsafe {
300 std::env::remove_var("OLLAMA_API_KEY");
301 }
302 let cfg = crate::app::Config::default();
303 let providers = Arc::new(crate::providers::ProviderFactory::new(cfg.clone()));
304 let r = ToolRegistry::build(&cfg, TuiMode::Headless, providers);
305 assert!(r.get("web_search").is_none(), "web_search skipped");
306 assert!(r.get("web_fetch").is_none(), "web_fetch skipped");
307 assert!(r.get("read_file").is_some());
308 assert!(r.get("execute_command").is_some());
309 unsafe {
310 if let Some(v) = prior {
311 std::env::set_var("OLLAMA_API_KEY", v);
312 }
313 }
314 }
315}