Skip to main content

llm_tool/registry/
mod.rs

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