1mod 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::{OutputSanitization, 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
22struct RegisteredTool {
28 definition: ToolDefinition,
29 erased: Box<dyn ErasedTool>,
30}
31
32pub struct ToolRegistry {
41 tools: HashMap<&'static str, RegisteredTool>,
42 cached_definitions: RwLock<Option<Arc<[ToolDefinition]>>>,
43 output_floor: OutputSanitization,
44}
45
46impl core::fmt::Debug for ToolRegistry {
47 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
48 let names: Vec<&str> = self
49 .tools
50 .values()
51 .map(|r| r.definition.name.as_str())
52 .collect();
53 f.debug_struct("ToolRegistry")
54 .field("tool_count", &self.tools.len())
55 .field("tool_names", &names)
56 .field("output_floor", &self.output_floor)
57 .finish_non_exhaustive()
58 }
59}
60
61impl Default for ToolRegistry {
62 fn default() -> Self {
63 Self::new()
64 }
65}
66
67impl ToolRegistry {
68 #[must_use]
70 pub fn new() -> Self {
71 Self {
72 tools: HashMap::new(),
73 cached_definitions: RwLock::new(None),
74 output_floor: OutputSanitization::default(),
75 }
76 }
77
78 #[must_use]
84 pub const fn with_output_floor(mut self, floor: OutputSanitization) -> Self {
85 self.output_floor = floor;
86 self
87 }
88
89 pub const fn set_output_floor(&mut self, floor: OutputSanitization) -> &mut Self {
91 self.output_floor = floor;
92 self
93 }
94
95 #[must_use]
97 pub const fn output_floor(&self) -> OutputSanitization {
98 self.output_floor
99 }
100
101 #[must_use]
105 pub fn effective_output_sanitization(&self, name: &str) -> Option<OutputSanitization> {
106 self.definition(name)
107 .map(|def| def.output_sanitization.at_least(self.output_floor))
108 }
109
110 fn enforce_output_floor<'a>(
112 &self,
113 tool_policy: OutputSanitization,
114 future: BoxToolFuture<'a>,
115 ) -> BoxToolFuture<'a> {
116 let effective = tool_policy.at_least(self.output_floor);
117 if effective == tool_policy {
118 return future;
119 }
120 Box::pin(async move { future.await.map(|output| effective.apply(output)) })
121 }
122
123 fn invalidate_cache(&self) {
124 let mut guard = write_lock_recover(&self.cached_definitions);
125 *guard = None;
126 }
127
128 pub fn register<T: RustTool + 'static>(&mut self, tool: T) -> &mut Self {
141 if let Err(e) = self.try_register(tool) {
142 panic!("Failed to build definition for tool '{}': {e}", T::NAME);
143 }
144 self
145 }
146
147 pub fn try_register<T: RustTool + 'static>(&mut self, tool: T) -> Result<&mut Self, ToolError> {
160 let definition = definition_of(&tool)?;
161 self.tools.insert(
162 T::NAME,
163 RegisteredTool {
164 definition,
165 erased: Box::new(tool),
166 },
167 );
168 self.invalidate_cache();
169 Ok(self)
170 }
171
172 #[must_use]
174 pub fn with_tool<T: RustTool + 'static>(mut self, tool: T) -> Self {
175 self.register(tool);
176 self
177 }
178
179 #[must_use]
182 pub fn definitions_slice(&self) -> Arc<[ToolDefinition]> {
183 {
184 let guard = read_lock_recover(&self.cached_definitions);
185 if let Some(ref slice) = *guard {
186 return Arc::clone(slice);
187 }
188 }
189 let mut defs: Vec<ToolDefinition> = self
190 .tools
191 .values()
192 .map(|entry| entry.definition.clone())
193 .collect();
194 defs.sort_by(|a, b| a.name.cmp(&b.name));
195 let arc_slice: Arc<[ToolDefinition]> = Arc::from(defs);
196 {
197 let mut guard = write_lock_recover(&self.cached_definitions);
198 *guard = Some(Arc::clone(&arc_slice));
199 }
200 arc_slice
201 }
202
203 #[must_use]
207 pub fn definitions(&self) -> Vec<ToolDefinition> {
208 self.definitions_slice().to_vec()
209 }
210
211 pub fn dispatch_boxed<'a>(
218 &'a self,
219 name: &str,
220 args: serde_json::Value,
221 ctx: &'a ToolContext,
222 ) -> Result<BoxToolFuture<'a>, ToolError> {
223 let Some(entry) = self.tools.get(name) else {
224 return Err(ToolError::not_found(RegistryItem::Tool, name));
225 };
226 let future = entry.erased.call_erased(args, ctx);
227 Ok(self.enforce_output_floor(entry.definition.output_sanitization, future))
228 }
229
230 pub fn dispatch_boxed_str<'a>(
237 &'a self,
238 name: &str,
239 args_json: &'a str,
240 ctx: &'a ToolContext,
241 ) -> Result<BoxToolFuture<'a>, ToolError> {
242 let Some(entry) = self.tools.get(name) else {
243 return Err(ToolError::not_found(RegistryItem::Tool, name));
244 };
245 let future = entry.erased.call_erased_str(args_json, ctx);
246 Ok(self.enforce_output_floor(entry.definition.output_sanitization, future))
247 }
248
249 pub async fn dispatch(
257 &self,
258 name: &str,
259 args: serde_json::Value,
260 ctx: &ToolContext,
261 ) -> Result<ToolOutput, ToolError> {
262 self.dispatch_boxed(name, args, ctx)?.await
263 }
264
265 pub async fn dispatch_str(
272 &self,
273 name: &str,
274 args_json: &str,
275 ctx: &ToolContext,
276 ) -> Result<ToolOutput, ToolError> {
277 self.dispatch_boxed_str(name, args_json, ctx)?.await
278 }
279
280 #[must_use]
282 pub fn is_destructive(&self, name: &str) -> bool {
283 self.definition(name)
284 .is_some_and(|def| matches!(def.effect, ToolEffect::Destructive))
285 }
286
287 pub fn remove(&mut self, name: &str) -> bool {
289 let removed = self.tools.remove(name).is_some();
290 if removed {
291 self.invalidate_cache();
292 }
293 removed
294 }
295
296 pub fn clear(&mut self) {
298 self.tools.clear();
299 self.invalidate_cache();
300 }
301
302 #[must_use]
304 pub fn len(&self) -> usize {
305 self.tools.len()
306 }
307
308 #[must_use]
310 pub fn is_empty(&self) -> bool {
311 self.tools.is_empty()
312 }
313
314 #[must_use]
316 pub fn contains(&self, name: &str) -> bool {
317 self.tools.contains_key(name)
318 }
319
320 #[must_use]
325 pub fn definition(&self, name: &str) -> Option<&ToolDefinition> {
326 self.tools.get(name).map(|entry| &entry.definition)
327 }
328
329 #[must_use]
333 pub fn iter(&self) -> ToolDefinitions<'_> {
334 ToolDefinitions {
335 inner: self.tools.iter(),
336 }
337 }
338}
339
340pub struct ToolDefinitions<'a> {
347 inner: crate::compat::HashMapIter<'a, &'static str, RegisteredTool>,
348}
349
350impl Iterator for ToolDefinitions<'_> {
351 type Item = (&'static str, ToolDefinition);
352
353 fn next(&mut self) -> Option<Self::Item> {
354 self.inner
355 .next()
356 .map(|(name, entry)| (*name, entry.definition.clone()))
357 }
358
359 fn size_hint(&self) -> (usize, Option<usize>) {
360 self.inner.size_hint()
361 }
362}
363
364impl ExactSizeIterator for ToolDefinitions<'_> {
365 fn len(&self) -> usize {
366 self.inner.len()
367 }
368}
369
370impl<'a> IntoIterator for &'a ToolRegistry {
374 type Item = (&'static str, ToolDefinition);
375 type IntoIter = ToolDefinitions<'a>;
376
377 fn into_iter(self) -> Self::IntoIter {
378 self.iter()
379 }
380}
381
382#[cfg(all(test, feature = "std"))]
383mod tests;