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