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};
48use crate::tools::{FunctionSchema, ToolError, ToolSchema};
49#[cfg(test)]
50use crate::tools::ToolResult;
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(
129 &self,
130 args: serde_json::Value,
131 ctx: ToolCtx,
132 ) -> Result<ToolOutcome, ToolError>;
133
134 /// Convert tool to LLM-compatible schema.
135 ///
136 /// Creates a [`ToolSchema`] suitable for LLM function calling.
137 fn to_schema(&self) -> ToolSchema {
138 ToolSchema {
139 schema_type: "function".to_string(),
140 function: FunctionSchema {
141 name: self.name().to_string(),
142 description: self.description().to_string(),
143 parameters: self.parameters_schema(),
144 },
145 }
146 }
147}
148
149/// Reference-counted pointer to a tool.
150pub type SharedTool = Arc<dyn Tool>;
151
152/// Errors that can occur during tool registration.
153///
154/// # Variants
155///
156/// * `DuplicateTool` - Tool with same name already registered
157/// * `InvalidTool` - Tool validation failed (e.g., empty name)
158#[derive(Debug, Error, PartialEq, Eq)]
159pub enum RegistryError {
160 /// Tool with same name already exists in registry.
161 #[error("tool with name '{0}' already registered")]
162 DuplicateTool(String),
163
164 /// Tool validation failed.
165 #[error("invalid tool: {0}")]
166 InvalidTool(String),
167}
168
169/// Thread-safe tool registry.
170///
171/// Manages a collection of tools with concurrent access support.
172/// Uses a `DashMap` for lock-free concurrent operations.
173///
174/// # Features
175///
176/// - Thread-safe registration and lookup
177/// - Tool schema generation for LLM
178/// - Global singleton registry support
179///
180/// # Example
181///
182/// ```rust,ignore
183/// let registry = ToolRegistry::new();
184///
185/// // Register tools
186/// registry.register(ReadFileTool::new())?;
187/// registry.register(WriteFileTool::new())?;
188///
189/// // List all tool schemas
190/// let schemas = registry.list_tools();
191///
192/// // Get and execute a tool
193/// if let Some(tool) = registry.get("read_file") {
194/// let result = tool.execute(json!({"path": "test.txt"})).await?;
195/// }
196/// ```
197pub struct ToolRegistry {
198 tools: DashMap<String, SharedTool>,
199}
200
201impl Default for ToolRegistry {
202 fn default() -> Self {
203 Self::new()
204 }
205}
206
207impl ToolRegistry {
208 /// Create a new empty tool registry.
209 pub fn new() -> Self {
210 Self {
211 tools: DashMap::new(),
212 }
213 }
214
215 /// Register a tool in the registry.
216 ///
217 /// # Arguments
218 ///
219 /// * `tool` - Tool to register
220 ///
221 /// # Errors
222 ///
223 /// Returns [`RegistryError::DuplicateTool`] if tool name already exists.
224 /// Returns [`RegistryError::InvalidTool`] if tool name is empty.
225 ///
226 /// # Example
227 ///
228 /// ```rust,ignore
229 /// registry.register(MyTool::new())?;
230 /// ```
231 pub fn register<T>(&self, tool: T) -> Result<(), RegistryError>
232 where
233 T: Tool + 'static,
234 {
235 self.register_shared(Arc::new(tool))
236 }
237
238 /// Register a shared tool reference.
239 ///
240 /// # Arguments
241 ///
242 /// * `tool` - Shared tool reference
243 ///
244 /// # Errors
245 ///
246 /// Same as [`register`](Self::register).
247 pub fn register_shared(&self, tool: SharedTool) -> Result<(), RegistryError> {
248 let name = tool.name().trim();
249
250 if name.is_empty() {
251 return Err(RegistryError::InvalidTool(
252 "tool name cannot be empty".to_string(),
253 ));
254 }
255
256 match self.tools.entry(name.to_string()) {
257 Entry::Occupied(_) => Err(RegistryError::DuplicateTool(name.to_string())),
258 Entry::Vacant(entry) => {
259 entry.insert(tool);
260 Ok(())
261 }
262 }
263 }
264
265 /// Get a tool by name.
266 ///
267 /// # Arguments
268 ///
269 /// * `name` - Tool name
270 ///
271 /// # Returns
272 ///
273 /// Shared tool reference if found, `None` otherwise.
274 pub fn get(&self, name: &str) -> Option<SharedTool> {
275 self.tools.get(name).map(|entry| Arc::clone(entry.value()))
276 }
277
278 /// Check if a tool exists in the registry.
279 pub fn contains(&self, name: &str) -> bool {
280 self.tools.contains_key(name)
281 }
282
283 /// List all tool schemas.
284 ///
285 /// Returns schemas sorted alphabetically by tool name.
286 pub fn list_tools(&self) -> Vec<ToolSchema> {
287 let mut tools: Vec<ToolSchema> = self
288 .tools
289 .iter()
290 .map(|entry| entry.value().to_schema())
291 .collect();
292 tools.sort_by_key(|t| t.function.name.clone());
293 tools
294 }
295
296 /// List all tool names.
297 ///
298 /// Returns names sorted alphabetically.
299 pub fn list_tool_names(&self) -> Vec<String> {
300 let mut names: Vec<String> = self.tools.iter().map(|entry| entry.key().clone()).collect();
301 names.sort();
302 names
303 }
304
305 /// Remove a tool from the registry.
306 ///
307 /// # Returns
308 ///
309 /// `true` if tool was removed, `false` if not found.
310 pub fn unregister(&self, name: &str) -> bool {
311 self.tools.remove(name).is_some()
312 }
313
314 /// Get the number of registered tools.
315 pub fn len(&self) -> usize {
316 self.tools.len()
317 }
318
319 /// Check if registry is empty.
320 pub fn is_empty(&self) -> bool {
321 self.tools.is_empty()
322 }
323
324 /// Remove all tools from the registry.
325 pub fn clear(&self) {
326 self.tools.clear();
327 }
328}
329
330/// Global tool registry singleton.
331static GLOBAL_REGISTRY: OnceLock<ToolRegistry> = OnceLock::new();
332
333/// Get the global tool registry.
334///
335/// The global registry is a singleton that persists for the lifetime
336/// of the application. Useful for sharing tools across components.
337///
338/// # Example
339///
340/// ```rust,ignore
341/// let registry = global_registry();
342/// registry.register(my_tool)?;
343/// ```
344pub fn global_registry() -> &'static ToolRegistry {
345 GLOBAL_REGISTRY.get_or_init(ToolRegistry::new)
346}
347
348/// Normalize a tool name by removing namespace prefix.
349///
350/// # Arguments
351///
352/// * `name` - Tool name (may include `::` namespace separator)
353///
354/// # Returns
355///
356/// Tool name after the last `::`, or the original name if no separator.
357///
358/// # Example
359///
360/// ```rust,ignore
361/// assert_eq!(normalize_tool_name("bamboo::read_file"), "read_file");
362/// assert_eq!(normalize_tool_name("read_file"), "read_file");
363/// ```
364pub fn normalize_tool_name(name: &str) -> &str {
365 name.split("::").last().unwrap_or(name)
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 use serde_json::json;
373
374 struct TestTool {
375 name: &'static str,
376 description: &'static str,
377 }
378
379 #[async_trait]
380 impl Tool for TestTool {
381 fn name(&self) -> &str {
382 self.name
383 }
384
385 fn description(&self) -> &str {
386 self.description
387 }
388
389 fn parameters_schema(&self) -> serde_json::Value {
390 json!({
391 "type": "object",
392 "properties": {}
393 })
394 }
395
396 async fn invoke(
397 &self,
398 _args: serde_json::Value,
399 _ctx: ToolCtx,
400 ) -> Result<ToolOutcome, ToolError> {
401 Ok(ToolOutcome::Completed(ToolResult {
402 success: true,
403 result: "ok".to_string(),
404 display_preference: None,
405 images: Vec::new(),
406 }))
407 }
408 }
409
410 #[test]
411 fn register_and_get() {
412 let registry = ToolRegistry::new();
413 let tool = TestTool {
414 name: "test_tool",
415 description: "test tool",
416 };
417
418 assert!(registry.register(tool).is_ok());
419 assert!(registry.get("test_tool").is_some());
420 assert!(registry.get("unknown").is_none());
421 }
422
423 #[test]
424 fn duplicate_tool_registration() {
425 let registry = ToolRegistry::new();
426
427 registry
428 .register(TestTool {
429 name: "dup",
430 description: "first",
431 })
432 .unwrap();
433
434 let duplicate = registry.register(TestTool {
435 name: "dup",
436 description: "second",
437 });
438
439 assert!(matches!(duplicate, Err(RegistryError::DuplicateTool(name)) if name == "dup"));
440 }
441
442 #[test]
443 fn list_tools_returns_registered_tools() {
444 let registry = ToolRegistry::new();
445
446 registry
447 .register(TestTool {
448 name: "tool_a",
449 description: "tool a",
450 })
451 .unwrap();
452 registry
453 .register(TestTool {
454 name: "tool_b",
455 description: "tool b",
456 })
457 .unwrap();
458
459 let tools = registry.list_tools();
460
461 assert_eq!(tools.len(), 2);
462 assert_eq!(tools[0].function.name, "tool_a");
463 assert_eq!(tools[1].function.name, "tool_b");
464 }
465
466 #[test]
467 fn register_rejects_empty_tool_name() {
468 let registry = ToolRegistry::new();
469
470 let result = registry.register(TestTool {
471 name: "",
472 description: "invalid",
473 });
474
475 assert!(
476 matches!(result, Err(RegistryError::InvalidTool(reason)) if reason == "tool name cannot be empty")
477 );
478 }
479
480 #[test]
481 fn normalize_tool_name_handles_namespaced_inputs() {
482 assert_eq!(normalize_tool_name("read_file"), "read_file");
483 assert_eq!(normalize_tool_name("default::read_file"), "read_file");
484 assert_eq!(normalize_tool_name("a::b::c::read_file"), "read_file");
485 }
486}