Skip to main content

brainwires_tools/
default_executor.rs

1//! Built-in tool executor — single dispatch point for all framework tools.
2//!
3//! [`BuiltinToolExecutor`] eliminates the need for every consumer (agent-chat,
4//! gateway, etc.) to reimplement a `dispatch_tool()` match statement.
5//! Construct one with a [`ToolRegistry`] and a [`ToolContext`], then call
6//! [`execute`](BuiltinToolExecutor::execute) or use it through the
7//! [`ToolExecutor`] trait.
8
9use 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
18/// Concrete executor that dispatches tool calls to the built-in tool modules
19/// registered in a [`ToolRegistry`].
20///
21/// # Example
22///
23/// ```rust,ignore
24/// use brainwires_tools::{BuiltinToolExecutor, ToolRegistry};
25/// use brainwires_core::ToolContext;
26///
27/// let registry = ToolRegistry::with_builtins();
28/// let context = ToolContext::default();
29/// let executor = BuiltinToolExecutor::new(registry, context);
30///
31/// // Check available tools
32/// assert!(executor.has_tool("execute_command"));
33///
34/// // Execute via the ToolExecutor trait
35/// // let result = executor.execute(&tool_use, &context).await?;
36/// ```
37pub struct BuiltinToolExecutor {
38    registry: ToolRegistry,
39    context: ToolContext,
40}
41
42impl BuiltinToolExecutor {
43    /// Create a new executor backed by the given registry and default context.
44    pub fn new(registry: ToolRegistry, context: ToolContext) -> Self {
45        Self { registry, context }
46    }
47
48    /// Execute a tool by name, dispatching to the correct handler.
49    ///
50    /// This is the standalone entry-point that mirrors the old
51    /// `dispatch_tool()` function from agent-chat.
52    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    /// Get all tool definitions (for sending to the provider).
63    pub fn tools(&self) -> Vec<Tool> {
64        self.registry.get_all().to_vec()
65    }
66
67    /// Check if a tool exists in the registry.
68    pub fn has_tool(&self, name: &str) -> bool {
69        self.registry.get(name).is_some()
70    }
71
72    /// Return a reference to the underlying registry.
73    pub fn registry(&self) -> &ToolRegistry {
74        &self.registry
75    }
76
77    /// Return a reference to the default context.
78    pub fn context(&self) -> &ToolContext {
79        &self.context
80    }
81
82    /// Core dispatch logic — routes a tool call to the correct handler module.
83    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        // Always-available tools
91        if tool_name == "search_tools" {
92            return ToolSearchTool::execute(tool_use_id, tool_name, input, context, &self.registry);
93        }
94
95        // Native-only tools
96        #[cfg(feature = "native")]
97        {
98            match tool_name {
99                // Bash / shell execution
100                "bash" | "execute_command" => {
101                    return crate::BashTool::execute(tool_use_id, tool_name, input, context);
102                }
103
104                // File operations
105                "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 operations
111                "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                // Code / file search
118                "search_code" | "search_files" => {
119                    return crate::SearchTool::execute(tool_use_id, tool_name, input, context);
120                }
121
122                // Validation
123                "check_duplicates" | "verify_build" | "check_syntax" => {
124                    return crate::ValidationTool::execute(tool_use_id, tool_name, input, context)
125                        .await;
126                }
127
128                // Web fetching
129                "fetch_url" => {
130                    return crate::WebTool::execute(tool_use_id, tool_name, input, context).await;
131                }
132
133                _ => {}
134            }
135        }
136
137        // Feature-gated: orchestrator
138        #[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        // Feature-gated: code execution / interpreters
149        #[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        // Feature-gated: semantic search / RAG
157        #[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        // Feature-gated: browser automation via Thalora subprocess
179        #[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        // Unknown tool — return an error result
192        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        // search_tools is always available
317        assert!(executor.has_tool("search_tools"));
318    }
319}