1use anyhow::Result;
10use async_trait::async_trait;
11
12use brainwires_core::{Tool, ToolContext, ToolResult, ToolUse};
13
14use crate::ToolSearchTool;
15use crate::executor::ToolExecutor;
16use crate::registry::ToolRegistry;
17
18pub struct BuiltinToolExecutor {
38 registry: ToolRegistry,
39 context: ToolContext,
40}
41
42impl BuiltinToolExecutor {
43 pub fn new(registry: ToolRegistry, context: ToolContext) -> Self {
45 Self { registry, context }
46 }
47
48 pub async fn execute_tool(
53 &self,
54 tool_name: &str,
55 tool_use_id: &str,
56 input: &serde_json::Value,
57 ) -> ToolResult {
58 self.dispatch(tool_use_id, tool_name, input, &self.context)
59 .await
60 }
61
62 pub fn tools(&self) -> Vec<Tool> {
64 self.registry.get_all().to_vec()
65 }
66
67 pub fn has_tool(&self, name: &str) -> bool {
69 self.registry.get(name).is_some()
70 }
71
72 pub fn registry(&self) -> &ToolRegistry {
74 &self.registry
75 }
76
77 pub fn context(&self) -> &ToolContext {
79 &self.context
80 }
81
82 async fn dispatch(
84 &self,
85 tool_use_id: &str,
86 tool_name: &str,
87 input: &serde_json::Value,
88 context: &ToolContext,
89 ) -> ToolResult {
90 if tool_name == "search_tools" {
92 return ToolSearchTool::execute(tool_use_id, tool_name, input, context, &self.registry);
93 }
94
95 #[cfg(feature = "native")]
97 {
98 match tool_name {
99 "bash" | "execute_command" => {
101 return crate::BashTool::execute(tool_use_id, tool_name, input, context);
102 }
103
104 "read_file" | "write_file" | "edit_file" | "patch_file" | "list_directory"
106 | "delete_file" | "create_directory" | "file_search" => {
107 return crate::FileOpsTool::execute(tool_use_id, tool_name, input, context);
108 }
109
110 "git_status" | "git_diff" | "git_log" | "git_stage" | "git_commit" | "git_push"
112 | "git_pull" | "git_branch" | "git_checkout" | "git_stash" | "git_reset"
113 | "git_show" | "git_blame" => {
114 return crate::GitTool::execute(tool_use_id, tool_name, input, context);
115 }
116
117 "search_code" | "search_files" => {
119 return crate::SearchTool::execute(tool_use_id, tool_name, input, context);
120 }
121
122 "check_duplicates" | "verify_build" | "check_syntax" => {
124 return crate::ValidationTool::execute(tool_use_id, tool_name, input, context)
125 .await;
126 }
127
128 "fetch_url" => {
130 return crate::WebTool::execute(tool_use_id, tool_name, input, context).await;
131 }
132
133 _ => {}
134 }
135 }
136
137 #[cfg(any(feature = "orchestrator", feature = "orchestrator-wasm"))]
139 {
140 if tool_name == "execute_script" {
141 let orchestrator = crate::OrchestratorTool::new();
142 return orchestrator
143 .execute(tool_use_id, tool_name, input, context)
144 .await;
145 }
146 }
147
148 #[cfg(feature = "interpreters")]
150 {
151 if tool_name == "execute_code" {
152 return crate::CodeExecTool::execute(tool_use_id, tool_name, input, context).await;
153 }
154 }
155
156 #[cfg(feature = "rag")]
158 {
159 match tool_name {
160 "index_codebase"
161 | "query_codebase"
162 | "search_with_filters"
163 | "get_rag_statistics"
164 | "clear_rag_index"
165 | "search_git_history" => {
166 return crate::SemanticSearchTool::execute(
167 tool_use_id,
168 tool_name,
169 input,
170 context,
171 )
172 .await;
173 }
174 _ => {}
175 }
176 }
177
178 #[cfg(feature = "browser")]
180 {
181 match tool_name {
182 "browser_read_url" | "browser_navigate" | "browser_click" | "browser_fill"
183 | "browser_eval" | "browser_screenshot" | "browser_search" => {
184 return crate::BrowserTool::execute(tool_use_id, tool_name, input, context)
185 .await;
186 }
187 _ => {}
188 }
189 }
190
191 ToolResult::error(
193 tool_use_id.to_string(),
194 format!("Unknown tool: {tool_name}"),
195 )
196 }
197}
198
199#[async_trait]
200impl ToolExecutor for BuiltinToolExecutor {
201 async fn execute(&self, tool_use: &ToolUse, context: &ToolContext) -> Result<ToolResult> {
202 Ok(self
203 .dispatch(&tool_use.id, &tool_use.name, &tool_use.input, context)
204 .await)
205 }
206
207 fn available_tools(&self) -> Vec<Tool> {
208 self.tools()
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use brainwires_core::ToolInputSchema;
216 use std::collections::HashMap;
217
218 fn make_tool(name: &str) -> Tool {
219 Tool {
220 name: name.to_string(),
221 description: format!("A {} tool", name),
222 input_schema: ToolInputSchema::object(HashMap::new(), vec![]),
223 ..Default::default()
224 }
225 }
226
227 fn make_executor_with(names: &[&str]) -> BuiltinToolExecutor {
228 let mut registry = ToolRegistry::new();
229 for name in names {
230 registry.register(make_tool(name));
231 }
232 let context = ToolContext::default();
233 BuiltinToolExecutor::new(registry, context)
234 }
235
236 #[test]
237 fn test_new_creates_successfully() {
238 let executor = make_executor_with(&["read_file", "execute_command"]);
239 assert_eq!(executor.tools().len(), 2);
240 }
241
242 #[test]
243 fn test_has_tool_returns_true_for_registered() {
244 let executor = make_executor_with(&["read_file", "execute_command"]);
245 assert!(executor.has_tool("read_file"));
246 assert!(executor.has_tool("execute_command"));
247 }
248
249 #[test]
250 fn test_has_tool_returns_false_for_unknown() {
251 let executor = make_executor_with(&["read_file"]);
252 assert!(!executor.has_tool("nonexistent_tool"));
253 assert!(!executor.has_tool(""));
254 }
255
256 #[test]
257 fn test_tools_returns_registered_tools() {
258 let executor = make_executor_with(&["read_file", "write_file", "git_status"]);
259 let tools = executor.tools();
260 assert_eq!(tools.len(), 3);
261 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
262 assert!(names.contains(&"read_file"));
263 assert!(names.contains(&"write_file"));
264 assert!(names.contains(&"git_status"));
265 }
266
267 #[test]
268 fn test_available_tools_matches_tools() {
269 let executor = make_executor_with(&["read_file", "execute_command"]);
270 let tools = executor.tools();
271 let available = executor.available_tools();
272 assert_eq!(tools.len(), available.len());
273 }
274
275 #[tokio::test]
276 async fn test_unknown_tool_returns_error() {
277 let executor = make_executor_with(&["read_file"]);
278 let result = executor
279 .execute_tool("totally_fake_tool", "test-id-1", &serde_json::json!({}))
280 .await;
281 assert!(result.is_error);
282 assert!(result.content.contains("Unknown tool"));
283 assert!(result.content.contains("totally_fake_tool"));
284 }
285
286 #[tokio::test]
287 async fn test_unknown_tool_via_trait() {
288 let executor = make_executor_with(&["read_file"]);
289 let tool_use = ToolUse {
290 id: "test-id-2".to_string(),
291 name: "nonexistent".to_string(),
292 input: serde_json::json!({}),
293 };
294 let result = executor
295 .execute(&tool_use, &executor.context)
296 .await
297 .unwrap();
298 assert!(result.is_error);
299 assert!(result.content.contains("Unknown tool"));
300 }
301
302 #[test]
303 fn test_empty_registry() {
304 let executor = make_executor_with(&[]);
305 assert_eq!(executor.tools().len(), 0);
306 assert!(!executor.has_tool("anything"));
307 }
308
309 #[test]
310 fn test_with_builtins_registry() {
311 let registry = ToolRegistry::with_builtins();
312 let tool_count = registry.len();
313 let context = ToolContext::default();
314 let executor = BuiltinToolExecutor::new(registry, context);
315 assert_eq!(executor.tools().len(), tool_count);
316 assert!(executor.has_tool("search_tools"));
318 }
319}