Skip to main content

bamboo_agent_core/tools/
registry.rs

1//! Tool registry for managing and executing tools.
2//!
3//! This module provides a thread-safe registry for tool management,
4//! including registration, lookup, and execution of tools.
5//!
6//! # Key Types
7//!
8//! - [`Tool`] - Trait for implementing executable tools
9//! - [`ToolRegistry`] - Thread-safe tool registry
10//! - [`RegistryError`] - Registration errors
11//! - [`SharedTool`] - Reference-counted tool pointer
12//!
13//! # Usage
14//!
15//! ```rust,ignore
16//! use bamboo_agent::agent::core::tools::registry::*;
17//!
18//! // Create a registry
19//! let registry = ToolRegistry::new();
20//!
21//! // Register a tool
22//! registry.register(MyTool::new())?;
23//!
24//! // Get tool schema for LLM
25//! let schemas = registry.list_tools();
26//!
27//! // Execute a tool
28//! let tool = registry.get("my_tool").unwrap();
29//! let result = tool.execute(args).await?;
30//! ```
31//!
32//! # Global Registry
33//!
34//! For convenience, a global singleton registry is available:
35//!
36//! ```rust,ignore
37//! let registry = global_registry();
38//! registry.register(my_tool)?;
39//! ```
40
41use std::sync::{Arc, OnceLock};
42
43use async_trait::async_trait;
44use dashmap::{mapref::entry::Entry, DashMap};
45use thiserror::Error;
46
47use crate::tools::tool_runtime::{ToolClass, ToolCtx, ToolOutcome};
48#[cfg(test)]
49use crate::tools::ToolResult;
50use crate::tools::{FunctionSchema, ToolError, ToolSchema};
51
52/// Trait for implementing executable tools.
53///
54/// All tools must implement this trait to be registered with the tool registry.
55///
56/// # Required Methods
57///
58/// - `name()` - Unique tool identifier
59/// - `description()` - Human-readable tool description
60/// - `parameters_schema()` - JSON Schema for tool parameters
61/// - `execute()` - Async tool execution logic
62///
63/// # Provided Methods
64///
65/// - `to_schema()` - Convert tool to LLM-compatible schema
66///
67/// # Example
68///
69/// ```rust,ignore
70/// struct ReadFileTool;
71///
72/// #[async_trait]
73/// impl Tool for ReadFileTool {
74///     fn name(&self) -> &str {
75///         "read_file"
76///     }
77///
78///     fn description(&self) -> &str {
79///         "Read file contents from disk"
80///     }
81///
82///     fn parameters_schema(&self) -> serde_json::Value {
83///         json!({
84///             "type": "object",
85///             "properties": {
86///                 "path": {"type": "string"}
87///             },
88///             "required": ["path"]
89///         })
90///     }
91///
92///     async fn execute(&self, args: Value) -> Result<ToolResult, ToolError> {
93///         let path = args["path"].as_str().unwrap();
94///         let content = tokio::fs::read_to_string(path).await?;
95///         Ok(ToolResult {
96///             success: true,
97///             result: content,
98///             display_preference: None,
99///         })
100///     }
101/// }
102/// ```
103#[async_trait]
104pub trait Tool: Send + Sync {
105    fn name(&self) -> &str;
106    /// Human-readable tool description for LLM.
107    fn description(&self) -> &str;
108    /// JSON Schema for tool parameters.
109    fn parameters_schema(&self) -> serde_json::Value;
110
111    /// Per-call scheduling + permission-gating class (args-aware). Folds the
112    /// former `mutability`/`call_mutability`/`concurrency_safe`/
113    /// `call_concurrency_safe` hooks into one value. Defaults to the conservative
114    /// [`ToolClass::MUTATING_SERIAL`] (mutating, serial, not promotable).
115    fn classify(&self, _args: &serde_json::Value) -> ToolClass {
116        ToolClass::MUTATING_SERIAL
117    }
118
119    /// The sole execution entry: returns the call's per-call [`ToolOutcome`]
120    /// ([`Completed`](ToolOutcome::Completed) / [`Running`](ToolOutcome::Running)
121    /// / [`NeedsHuman`](ToolOutcome::NeedsHuman)), or a [`ToolError`] for
122    /// infrastructure / argument failures (preserved from the former `execute`).
123    ///
124    /// Every tool is context-aware now — the former `execute` /
125    /// `execute_with_context` split is gone, and `ctx` is owned so an `invoke`
126    /// future can be moved into a detached drive task for latency-adaptive
127    /// promotion.
128    async fn invoke(&self, args: serde_json::Value, ctx: ToolCtx)
129        -> Result<ToolOutcome, ToolError>;
130
131    /// Convert tool to LLM-compatible schema.
132    ///
133    /// Creates a [`ToolSchema`] suitable for LLM function calling.
134    fn to_schema(&self) -> ToolSchema {
135        ToolSchema {
136            schema_type: "function".to_string(),
137            function: FunctionSchema {
138                name: self.name().to_string(),
139                description: self.description().to_string(),
140                parameters: self.parameters_schema(),
141            },
142        }
143    }
144}
145
146/// Reference-counted pointer to a tool.
147pub type SharedTool = Arc<dyn Tool>;
148
149/// Errors that can occur during tool registration.
150///
151/// # Variants
152///
153/// * `DuplicateTool` - Tool with same name already registered
154/// * `InvalidTool` - Tool validation failed (e.g., empty name)
155#[derive(Debug, Error, PartialEq, Eq)]
156pub enum RegistryError {
157    /// Tool with same name already exists in registry.
158    #[error("tool with name '{0}' already registered")]
159    DuplicateTool(String),
160
161    /// Tool validation failed.
162    #[error("invalid tool: {0}")]
163    InvalidTool(String),
164}
165
166/// Thread-safe tool registry.
167///
168/// Manages a collection of tools with concurrent access support.
169/// Uses a `DashMap` for lock-free concurrent operations.
170///
171/// # Features
172///
173/// - Thread-safe registration and lookup
174/// - Tool schema generation for LLM
175/// - Global singleton registry support
176///
177/// # Example
178///
179/// ```rust,ignore
180/// let registry = ToolRegistry::new();
181///
182/// // Register tools
183/// registry.register(ReadFileTool::new())?;
184/// registry.register(WriteFileTool::new())?;
185///
186/// // List all tool schemas
187/// let schemas = registry.list_tools();
188///
189/// // Get and execute a tool
190/// if let Some(tool) = registry.get("read_file") {
191///     let result = tool.execute(json!({"path": "test.txt"})).await?;
192/// }
193/// ```
194pub struct ToolRegistry {
195    tools: DashMap<String, SharedTool>,
196}
197
198impl Default for ToolRegistry {
199    fn default() -> Self {
200        Self::new()
201    }
202}
203
204impl ToolRegistry {
205    /// Create a new empty tool registry.
206    pub fn new() -> Self {
207        Self {
208            tools: DashMap::new(),
209        }
210    }
211
212    /// Register a tool in the registry.
213    ///
214    /// # Arguments
215    ///
216    /// * `tool` - Tool to register
217    ///
218    /// # Errors
219    ///
220    /// Returns [`RegistryError::DuplicateTool`] if tool name already exists.
221    /// Returns [`RegistryError::InvalidTool`] if tool name is empty.
222    ///
223    /// # Example
224    ///
225    /// ```rust,ignore
226    /// registry.register(MyTool::new())?;
227    /// ```
228    pub fn register<T>(&self, tool: T) -> Result<(), RegistryError>
229    where
230        T: Tool + 'static,
231    {
232        self.register_shared(Arc::new(tool))
233    }
234
235    /// Register a shared tool reference.
236    ///
237    /// # Arguments
238    ///
239    /// * `tool` - Shared tool reference
240    ///
241    /// # Errors
242    ///
243    /// Same as [`register`](Self::register).
244    pub fn register_shared(&self, tool: SharedTool) -> Result<(), RegistryError> {
245        let name = tool.name().trim();
246
247        if name.is_empty() {
248            return Err(RegistryError::InvalidTool(
249                "tool name cannot be empty".to_string(),
250            ));
251        }
252
253        match self.tools.entry(name.to_string()) {
254            Entry::Occupied(_) => Err(RegistryError::DuplicateTool(name.to_string())),
255            Entry::Vacant(entry) => {
256                entry.insert(tool);
257                Ok(())
258            }
259        }
260    }
261
262    /// Get a tool by name.
263    ///
264    /// # Arguments
265    ///
266    /// * `name` - Tool name
267    ///
268    /// # Returns
269    ///
270    /// Shared tool reference if found, `None` otherwise.
271    pub fn get(&self, name: &str) -> Option<SharedTool> {
272        self.tools.get(name).map(|entry| Arc::clone(entry.value()))
273    }
274
275    /// Check if a tool exists in the registry.
276    pub fn contains(&self, name: &str) -> bool {
277        self.tools.contains_key(name)
278    }
279
280    /// List all tool schemas.
281    ///
282    /// Returns schemas sorted alphabetically by tool name.
283    pub fn list_tools(&self) -> Vec<ToolSchema> {
284        let mut tools: Vec<ToolSchema> = self
285            .tools
286            .iter()
287            .map(|entry| entry.value().to_schema())
288            .collect();
289        tools.sort_by_key(|t| t.function.name.clone());
290        tools
291    }
292
293    /// List all tool names.
294    ///
295    /// Returns names sorted alphabetically.
296    pub fn list_tool_names(&self) -> Vec<String> {
297        let mut names: Vec<String> = self.tools.iter().map(|entry| entry.key().clone()).collect();
298        names.sort();
299        names
300    }
301
302    /// Remove a tool from the registry.
303    ///
304    /// # Returns
305    ///
306    /// `true` if tool was removed, `false` if not found.
307    pub fn unregister(&self, name: &str) -> bool {
308        self.tools.remove(name).is_some()
309    }
310
311    /// Get the number of registered tools.
312    pub fn len(&self) -> usize {
313        self.tools.len()
314    }
315
316    /// Check if registry is empty.
317    pub fn is_empty(&self) -> bool {
318        self.tools.is_empty()
319    }
320
321    /// Remove all tools from the registry.
322    pub fn clear(&self) {
323        self.tools.clear();
324    }
325}
326
327/// Global tool registry singleton.
328static GLOBAL_REGISTRY: OnceLock<ToolRegistry> = OnceLock::new();
329
330/// Get the global tool registry.
331///
332/// The global registry is a singleton that persists for the lifetime
333/// of the application. Useful for sharing tools across components.
334///
335/// # Example
336///
337/// ```rust,ignore
338/// let registry = global_registry();
339/// registry.register(my_tool)?;
340/// ```
341pub fn global_registry() -> &'static ToolRegistry {
342    GLOBAL_REGISTRY.get_or_init(ToolRegistry::new)
343}
344
345/// Normalize a tool name by removing namespace prefix.
346///
347/// # Arguments
348///
349/// * `name` - Tool name (may include `::` namespace separator)
350///
351/// # Returns
352///
353/// Tool name after the last `::`, or the original name if no separator.
354///
355/// # Example
356///
357/// ```rust,ignore
358/// assert_eq!(normalize_tool_name("bamboo::read_file"), "read_file");
359/// assert_eq!(normalize_tool_name("read_file"), "read_file");
360/// ```
361pub fn normalize_tool_name(name: &str) -> &str {
362    name.split("::").last().unwrap_or(name)
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368
369    use serde_json::json;
370
371    struct TestTool {
372        name: &'static str,
373        description: &'static str,
374    }
375
376    #[async_trait]
377    impl Tool for TestTool {
378        fn name(&self) -> &str {
379            self.name
380        }
381
382        fn description(&self) -> &str {
383            self.description
384        }
385
386        fn parameters_schema(&self) -> serde_json::Value {
387            json!({
388                "type": "object",
389                "properties": {}
390            })
391        }
392
393        async fn invoke(
394            &self,
395            _args: serde_json::Value,
396            _ctx: ToolCtx,
397        ) -> Result<ToolOutcome, ToolError> {
398            Ok(ToolOutcome::Completed(ToolResult {
399                success: true,
400                result: "ok".to_string(),
401                display_preference: None,
402                images: Vec::new(),
403            }))
404        }
405    }
406
407    #[test]
408    fn register_and_get() {
409        let registry = ToolRegistry::new();
410        let tool = TestTool {
411            name: "test_tool",
412            description: "test tool",
413        };
414
415        assert!(registry.register(tool).is_ok());
416        assert!(registry.get("test_tool").is_some());
417        assert!(registry.get("unknown").is_none());
418    }
419
420    #[test]
421    fn duplicate_tool_registration() {
422        let registry = ToolRegistry::new();
423
424        registry
425            .register(TestTool {
426                name: "dup",
427                description: "first",
428            })
429            .unwrap();
430
431        let duplicate = registry.register(TestTool {
432            name: "dup",
433            description: "second",
434        });
435
436        assert!(matches!(duplicate, Err(RegistryError::DuplicateTool(name)) if name == "dup"));
437    }
438
439    #[test]
440    fn list_tools_returns_registered_tools() {
441        let registry = ToolRegistry::new();
442
443        registry
444            .register(TestTool {
445                name: "tool_a",
446                description: "tool a",
447            })
448            .unwrap();
449        registry
450            .register(TestTool {
451                name: "tool_b",
452                description: "tool b",
453            })
454            .unwrap();
455
456        let tools = registry.list_tools();
457
458        assert_eq!(tools.len(), 2);
459        assert_eq!(tools[0].function.name, "tool_a");
460        assert_eq!(tools[1].function.name, "tool_b");
461    }
462
463    #[test]
464    fn register_rejects_empty_tool_name() {
465        let registry = ToolRegistry::new();
466
467        let result = registry.register(TestTool {
468            name: "",
469            description: "invalid",
470        });
471
472        assert!(
473            matches!(result, Err(RegistryError::InvalidTool(reason)) if reason == "tool name cannot be empty")
474        );
475    }
476
477    #[test]
478    fn normalize_tool_name_handles_namespaced_inputs() {
479        assert_eq!(normalize_tool_name("read_file"), "read_file");
480        assert_eq!(normalize_tool_name("default::read_file"), "read_file");
481        assert_eq!(normalize_tool_name("a::b::c::read_file"), "read_file");
482    }
483}