Skip to main content

llm_tool/registry/
mod.rs

1//! Tool registry: registry and concurrent dispatch of named tools.
2
3mod batch;
4
5use alloc::{boxed::Box, sync::Arc, vec::Vec};
6
7pub use batch::{
8    AllowDestructiveBatch, BatchExecutionMode, BatchToolCall, BatchToolCallOutcome,
9    BatchToolCallStr,
10};
11
12use super::{
13    rust_tool::{ErasedTool, RustTool, definition_of},
14    types::{RegistryItem, ToolContext, ToolDefinition, ToolError, ToolOutput},
15};
16use crate::{
17    compat::{HashMap, RwLock, read_lock_recover, write_lock_recover},
18    rust_tool::BoxToolFuture,
19    types::ToolEffect,
20};
21
22/// Entry holding a cached [`ToolDefinition`] alongside the type-erased tool.
23///
24/// The definition is computed once at registration time so that
25/// [`ToolRegistry::definitions`] and [`ToolRegistry::iter`] never
26/// regenerate JSON schemas.
27struct RegisteredTool {
28    definition: ToolDefinition,
29    erased: Box<dyn ErasedTool>,
30}
31
32/// A registry of named tools available for dynamic dispatch.
33///
34/// Holds type-erased tool implementations and cached [`ToolDefinition`](super::types::ToolDefinition)
35/// schemas for fast lookup and execution.
36pub struct ToolRegistry {
37    tools: HashMap<&'static str, RegisteredTool>,
38    cached_definitions: RwLock<Option<Arc<[ToolDefinition]>>>,
39}
40
41impl core::fmt::Debug for ToolRegistry {
42    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
43        let names: Vec<&str> = self
44            .tools
45            .values()
46            .map(|r| r.definition.name.as_str())
47            .collect();
48        f.debug_struct("ToolRegistry")
49            .field("tool_count", &self.tools.len())
50            .field("tool_names", &names)
51            .finish_non_exhaustive()
52    }
53}
54
55impl Default for ToolRegistry {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61impl ToolRegistry {
62    /// Create an empty registry.
63    #[must_use]
64    pub fn new() -> Self {
65        Self {
66            tools: HashMap::new(),
67            cached_definitions: RwLock::new(None),
68        }
69    }
70
71    fn invalidate_cache(&self) {
72        let mut guard = write_lock_recover(&self.cached_definitions);
73        *guard = None;
74    }
75
76    /// Register a [`RustTool`]. Returns `&mut Self` for chaining.
77    ///
78    /// The tool's [`ToolDefinition`] (including JSON schema) is computed once
79    /// here and cached for the lifetime of the registration.
80    ///
81    /// If a tool with the same name was already registered, it is replaced.
82    ///
83    /// # Panics
84    ///
85    /// Panics if the tool's JSON schema cannot be serialized. This indicates a
86    /// bug in the tool's `Params` type (e.g. a broken `JsonSchema` impl). Use
87    /// [`try_register`](Self::try_register) for the non-panicking variant.
88    pub fn register<T: RustTool + 'static>(&mut self, tool: T) -> &mut Self {
89        if let Err(e) = self.try_register(tool) {
90            panic!("Failed to build definition for tool '{}': {e}", T::NAME);
91        }
92        self
93    }
94
95    /// Register a [`RustTool`], returning an error instead of panicking if the
96    /// tool's JSON schema cannot be built.
97    ///
98    /// This is the fallible counterpart to [`register`](Self::register); prefer
99    /// it when tool types are supplied dynamically and a broken `JsonSchema`
100    /// impl should not abort the process. If a tool with the same name was
101    /// already registered, it is replaced.
102    ///
103    /// # Errors
104    ///
105    /// Returns [`ToolError`] if the tool's `Params` type fails to produce a
106    /// JSON schema.
107    pub fn try_register<T: RustTool + 'static>(&mut self, tool: T) -> Result<&mut Self, ToolError> {
108        let definition = definition_of(&tool)?;
109        self.tools.insert(
110            T::NAME,
111            RegisteredTool {
112                definition,
113                erased: Box::new(tool),
114            },
115        );
116        self.invalidate_cache();
117        Ok(self)
118    }
119
120    /// Register a [`RustTool`], consuming and returning `Self` for owned chaining.
121    #[must_use]
122    pub fn with_tool<T: RustTool + 'static>(mut self, tool: T) -> Self {
123        self.register(tool);
124        self
125    }
126
127    /// Return a shared, deterministically sorted slice of all [`ToolDefinition`]s,
128    /// computed once and cached until the registry is mutated.
129    #[must_use]
130    pub fn definitions_slice(&self) -> Arc<[ToolDefinition]> {
131        {
132            let guard = read_lock_recover(&self.cached_definitions);
133            if let Some(ref slice) = *guard {
134                return Arc::clone(slice);
135            }
136        }
137        let mut defs: Vec<ToolDefinition> = self
138            .tools
139            .values()
140            .map(|entry| entry.definition.clone())
141            .collect();
142        defs.sort_by(|a, b| a.name.cmp(&b.name));
143        let arc_slice: Arc<[ToolDefinition]> = Arc::from(defs);
144        {
145            let mut guard = write_lock_recover(&self.cached_definitions);
146            *guard = Some(Arc::clone(&arc_slice));
147        }
148        arc_slice
149    }
150
151    /// Collect [`ToolDefinition`]s for all registered tools.
152    ///
153    /// Returns clones of the cached definitions computed at registration time.
154    #[must_use]
155    pub fn definitions(&self) -> Vec<ToolDefinition> {
156        self.definitions_slice().to_vec()
157    }
158
159    /// Dispatch a tool call by name with raw JSON arguments and a context,
160    /// returning a boxed future.
161    ///
162    /// # Errors
163    ///
164    /// Returns [`ToolError::not_found`] if no tool named `name` is registered.
165    pub fn dispatch_boxed<'a>(
166        &'a self,
167        name: &str,
168        args: serde_json::Value,
169        ctx: &'a ToolContext,
170    ) -> Result<BoxToolFuture<'a>, ToolError> {
171        let Some(entry) = self.tools.get(name) else {
172            return Err(ToolError::not_found(RegistryItem::Tool, name));
173        };
174        Ok(entry.erased.call_erased(args, ctx))
175    }
176
177    /// Dispatch a tool call by name with a raw JSON arguments string,
178    /// returning a boxed future without intermediate DOM allocation.
179    ///
180    /// # Errors
181    ///
182    /// Returns [`ToolError::not_found`] if no tool named `name` is registered.
183    pub fn dispatch_boxed_str<'a>(
184        &'a self,
185        name: &str,
186        args_json: &'a str,
187        ctx: &'a ToolContext,
188    ) -> Result<BoxToolFuture<'a>, ToolError> {
189        let Some(entry) = self.tools.get(name) else {
190            return Err(ToolError::not_found(RegistryItem::Tool, name));
191        };
192        Ok(entry.erased.call_erased_str(args_json, ctx))
193    }
194
195    /// Dispatch a tool call by name with raw JSON arguments and a context.
196    ///
197    /// # Errors
198    ///
199    /// Returns [`ToolError::not_found`] if no tool named `name` is registered
200    /// (carrying `error_kind = "not_registered"` metadata), or the tool's own
201    /// error if argument deserialization fails or the handler returns an error.
202    pub async fn dispatch(
203        &self,
204        name: &str,
205        args: serde_json::Value,
206        ctx: &ToolContext,
207    ) -> Result<ToolOutput, ToolError> {
208        self.dispatch_boxed(name, args, ctx)?.await
209    }
210
211    /// Dispatch a tool call by name with a raw JSON string argument.
212    ///
213    /// # Errors
214    ///
215    /// Returns [`ToolError::not_found`] if no tool named `name` is registered,
216    /// or the tool's own error if JSON parsing or the handler fails.
217    pub async fn dispatch_str(
218        &self,
219        name: &str,
220        args_json: &str,
221        ctx: &ToolContext,
222    ) -> Result<ToolOutput, ToolError> {
223        self.dispatch_boxed_str(name, args_json, ctx)?.await
224    }
225
226    /// Return `true` if the registered tool has [`ToolEffect::Destructive`].
227    #[must_use]
228    pub fn is_destructive(&self, name: &str) -> bool {
229        self.definition(name)
230            .is_some_and(|def| matches!(def.effect, ToolEffect::Destructive))
231    }
232
233    /// Remove a tool by name, returning `true` if it was present.
234    pub fn remove(&mut self, name: &str) -> bool {
235        let removed = self.tools.remove(name).is_some();
236        if removed {
237            self.invalidate_cache();
238        }
239        removed
240    }
241
242    /// Clear all registered tools.
243    pub fn clear(&mut self) {
244        self.tools.clear();
245        self.invalidate_cache();
246    }
247
248    /// Number of registered tools.
249    #[must_use]
250    pub fn len(&self) -> usize {
251        self.tools.len()
252    }
253
254    /// Whether the registry has no registered tools.
255    #[must_use]
256    pub fn is_empty(&self) -> bool {
257        self.tools.is_empty()
258    }
259
260    /// Whether a tool with the given name is registered.
261    #[must_use]
262    pub fn contains(&self, name: &str) -> bool {
263        self.tools.contains_key(name)
264    }
265
266    /// Borrow the cached [`ToolDefinition`] for a registered tool by name.
267    ///
268    /// Returns `None` if no tool named `name` is registered. Unlike
269    /// [`definitions`](Self::definitions), this clones nothing.
270    #[must_use]
271    pub fn definition(&self, name: &str) -> Option<&ToolDefinition> {
272        self.tools.get(name).map(|entry| &entry.definition)
273    }
274
275    /// Iterate over `(name, definition)` pairs for every registered tool.
276    ///
277    /// Yields clones of the cached definitions computed at registration time.
278    #[must_use]
279    pub fn iter(&self) -> ToolDefinitions<'_> {
280        ToolDefinitions {
281            inner: self.tools.iter(),
282        }
283    }
284}
285
286/// Borrowing iterator over `(name, definition)` pairs, yielded by
287/// [`ToolRegistry::iter`] and by `&ToolRegistry`'s [`IntoIterator`] impl.
288///
289/// Unlike a boxed trait object, this named iterator allocates nothing to
290/// construct and forwards `size_hint`/`len` from the underlying map iterator.
291/// Each cached [`ToolDefinition`] is cloned lazily as it is yielded.
292pub struct ToolDefinitions<'a> {
293    inner: crate::compat::HashMapIter<'a, &'static str, RegisteredTool>,
294}
295
296impl Iterator for ToolDefinitions<'_> {
297    type Item = (&'static str, ToolDefinition);
298
299    fn next(&mut self) -> Option<Self::Item> {
300        self.inner
301            .next()
302            .map(|(name, entry)| (*name, entry.definition.clone()))
303    }
304
305    fn size_hint(&self) -> (usize, Option<usize>) {
306        self.inner.size_hint()
307    }
308}
309
310impl ExactSizeIterator for ToolDefinitions<'_> {
311    fn len(&self) -> usize {
312        self.inner.len()
313    }
314}
315
316/// Iterate over `(name, definition)` pairs for every registered tool.
317///
318/// Yields `(&'static str, ToolDefinition)` for each tool in the registry.
319impl<'a> IntoIterator for &'a ToolRegistry {
320    type Item = (&'static str, ToolDefinition);
321    type IntoIter = ToolDefinitions<'a>;
322
323    fn into_iter(self) -> Self::IntoIter {
324        self.iter()
325    }
326}
327
328#[cfg(all(test, feature = "std"))]
329mod tests;