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::{RegistryItem, 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 /// # Errors
157 ///
158 /// Returns [`ToolError::not_found`] if no tool named `name` is registered
159 /// (carrying `error_kind = "not_registered"` metadata), or the tool's own
160 /// error if argument deserialization fails or the handler returns an error.
161 pub async fn dispatch(
162 &self,
163 name: &str,
164 args: serde_json::Value,
165 ctx: &ToolContext,
166 ) -> Result<ToolOutput, ToolError> {
167 let Some(entry) = self.tools.get(name) else {
168 return Err(ToolError::not_found(RegistryItem::Tool, name));
169 };
170 entry.erased.call_erased(args, ctx).await
171 }
172
173 /// Dispatch a tool call by name with a raw JSON string argument.
174 ///
175 /// # Errors
176 ///
177 /// Returns [`ToolError::not_found`] if no tool named `name` is registered,
178 /// or the tool's own error if JSON parsing or the handler fails.
179 pub async fn dispatch_str(
180 &self,
181 name: &str,
182 args_json: &str,
183 ctx: &ToolContext,
184 ) -> Result<ToolOutput, ToolError> {
185 let Some(entry) = self.tools.get(name) else {
186 return Err(ToolError::not_found(RegistryItem::Tool, name));
187 };
188 let args = serde_json::from_str(args_json)
189 .map_err(|e| ToolError::new(format!("Malformed JSON arguments: {e}")))?;
190 entry.erased.call_erased(args, ctx).await
191 }
192
193 /// Number of registered tools.
194 #[must_use]
195 pub fn len(&self) -> usize {
196 self.tools.len()
197 }
198
199 /// Whether the registry has no registered tools.
200 #[must_use]
201 pub fn is_empty(&self) -> bool {
202 self.tools.is_empty()
203 }
204
205 /// Whether a tool with the given name is registered.
206 #[must_use]
207 pub fn contains(&self, name: &str) -> bool {
208 self.tools.contains_key(name)
209 }
210
211 /// Borrow the cached [`ToolDefinition`] for a registered tool by name.
212 ///
213 /// Returns `None` if no tool named `name` is registered. Unlike
214 /// [`definitions`](Self::definitions), this clones nothing.
215 #[must_use]
216 pub fn definition(&self, name: &str) -> Option<&ToolDefinition> {
217 self.tools.get(name).map(|entry| &entry.definition)
218 }
219
220 /// Iterate over `(name, definition)` pairs for every registered tool.
221 ///
222 /// Yields clones of the cached definitions computed at registration time.
223 #[must_use]
224 pub fn iter(&self) -> ToolDefinitions<'_> {
225 ToolDefinitions {
226 inner: self.tools.iter(),
227 }
228 }
229}
230
231/// Borrowing iterator over `(name, definition)` pairs, yielded by
232/// [`ToolRegistry::iter`] and by `&ToolRegistry`'s [`IntoIterator`] impl.
233///
234/// Unlike a boxed trait object, this named iterator allocates nothing to
235/// construct and forwards `size_hint`/`len` from the underlying map iterator.
236/// Each cached [`ToolDefinition`] is cloned lazily as it is yielded.
237pub struct ToolDefinitions<'a> {
238 inner: crate::compat::HashMapIter<'a, &'static str, RegisteredTool>,
239}
240
241impl Iterator for ToolDefinitions<'_> {
242 type Item = (&'static str, ToolDefinition);
243
244 fn next(&mut self) -> Option<Self::Item> {
245 self.inner
246 .next()
247 .map(|(name, entry)| (*name, entry.definition.clone()))
248 }
249
250 fn size_hint(&self) -> (usize, Option<usize>) {
251 self.inner.size_hint()
252 }
253}
254
255impl ExactSizeIterator for ToolDefinitions<'_> {
256 fn len(&self) -> usize {
257 self.inner.len()
258 }
259}
260
261/// Iterate over `(name, definition)` pairs for every registered tool.
262///
263/// Yields `(&'static str, ToolDefinition)` for each tool in the registry.
264impl<'a> IntoIterator for &'a ToolRegistry {
265 type Item = (&'static str, ToolDefinition);
266 type IntoIter = ToolDefinitions<'a>;
267
268 fn into_iter(self) -> Self::IntoIter {
269 self.iter()
270 }
271}
272
273#[cfg(all(test, feature = "std"))]
274mod tests;