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::{
15 OutputSanitization, RegistryItem, ToolContext, ToolDefinition, ToolEffect, ToolError,
16 ToolOutput, ToolProvenance,
17 },
18};
19use crate::{
20 compat::{HashMap, RwLock, read_lock_recover, write_lock_recover},
21 rust_tool::BoxToolFuture,
22};
23
24struct RegisteredTool {
30 definition: ToolDefinition,
31 erased: Box<dyn ErasedTool>,
32}
33
34pub struct ToolRegistry {
43 tools: HashMap<&'static str, RegisteredTool>,
44 cached_definitions: RwLock<Option<Arc<[ToolDefinition]>>>,
45 output_floor: OutputSanitization,
46}
47
48impl core::fmt::Debug for ToolRegistry {
49 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
50 let names: Vec<&str> = self
51 .tools
52 .values()
53 .map(|r| r.definition.name.as_str())
54 .collect();
55 f.debug_struct("ToolRegistry")
56 .field("tool_count", &self.tools.len())
57 .field("tool_names", &names)
58 .field("output_floor", &self.output_floor)
59 .finish_non_exhaustive()
60 }
61}
62
63impl Default for ToolRegistry {
64 fn default() -> Self {
65 Self::new()
66 }
67}
68
69impl ToolRegistry {
70 #[must_use]
72 pub fn new() -> Self {
73 Self {
74 tools: HashMap::new(),
75 cached_definitions: RwLock::new(None),
76 output_floor: OutputSanitization::default(),
77 }
78 }
79
80 #[must_use]
86 pub const fn with_output_floor(mut self, floor: OutputSanitization) -> Self {
87 self.output_floor = floor;
88 self
89 }
90
91 pub const fn set_output_floor(&mut self, floor: OutputSanitization) -> &mut Self {
93 self.output_floor = floor;
94 self
95 }
96
97 #[must_use]
99 pub const fn output_floor(&self) -> OutputSanitization {
100 self.output_floor
101 }
102
103 #[must_use]
107 pub fn effective_output_sanitization(&self, name: &str) -> Option<OutputSanitization> {
108 self.definition(name)
109 .map(|def| def.output_sanitization.at_least(self.output_floor))
110 }
111
112 fn enforce_output_floor<'a>(
114 &self,
115 tool_policy: OutputSanitization,
116 future: BoxToolFuture<'a>,
117 ) -> BoxToolFuture<'a> {
118 let effective = tool_policy.at_least(self.output_floor);
119 if effective == tool_policy {
120 return future;
121 }
122 Box::pin(async move { future.await.map(|output| effective.apply(output)) })
123 }
124
125 fn invalidate_cache(&self) {
126 let mut guard = write_lock_recover(&self.cached_definitions);
127 *guard = None;
128 }
129
130 pub fn register<T: RustTool + 'static>(&mut self, tool: T) -> &mut Self {
143 if let Err(e) = self.try_register(tool) {
144 panic!("Failed to build definition for tool '{}': {e}", T::NAME);
145 }
146 self
147 }
148
149 pub fn try_register<T: RustTool + 'static>(&mut self, tool: T) -> Result<&mut Self, ToolError> {
162 let definition = definition_of(&tool)?;
163 self.tools.insert(
164 T::NAME,
165 RegisteredTool {
166 definition,
167 erased: Box::new(tool),
168 },
169 );
170 self.invalidate_cache();
171 Ok(self)
172 }
173
174 #[must_use]
176 pub fn with_tool<T: RustTool + 'static>(mut self, tool: T) -> Self {
177 self.register(tool);
178 self
179 }
180
181 #[must_use]
184 pub fn definitions_slice(&self) -> Arc<[ToolDefinition]> {
185 {
186 let guard = read_lock_recover(&self.cached_definitions);
187 if let Some(ref slice) = *guard {
188 return Arc::clone(slice);
189 }
190 }
191 let mut defs: Vec<ToolDefinition> = self
192 .tools
193 .values()
194 .map(|entry| entry.definition.clone())
195 .collect();
196 defs.sort_by(|a, b| a.name.cmp(&b.name));
197 let arc_slice: Arc<[ToolDefinition]> = Arc::from(defs);
198 {
199 let mut guard = write_lock_recover(&self.cached_definitions);
200 *guard = Some(Arc::clone(&arc_slice));
201 }
202 arc_slice
203 }
204
205 #[must_use]
209 pub fn definitions(&self) -> Vec<ToolDefinition> {
210 self.definitions_slice().to_vec()
211 }
212
213 pub fn dispatch_boxed<'a>(
220 &'a self,
221 name: &str,
222 args: serde_json::Value,
223 ctx: &'a ToolContext,
224 ) -> Result<BoxToolFuture<'a>, ToolError> {
225 let Some(entry) = self.tools.get(name) else {
226 return Err(ToolError::not_found(RegistryItem::Tool, name));
227 };
228 let future = entry.erased.call_erased(args, ctx);
229 Ok(self.enforce_output_floor(entry.definition.output_sanitization, future))
230 }
231
232 pub fn dispatch_boxed_str<'a>(
239 &'a self,
240 name: &str,
241 args_json: &'a str,
242 ctx: &'a ToolContext,
243 ) -> Result<BoxToolFuture<'a>, ToolError> {
244 let Some(entry) = self.tools.get(name) else {
245 return Err(ToolError::not_found(RegistryItem::Tool, name));
246 };
247 let future = entry.erased.call_erased_str(args_json, ctx);
248 Ok(self.enforce_output_floor(entry.definition.output_sanitization, future))
249 }
250
251 pub async fn dispatch(
259 &self,
260 name: &str,
261 args: serde_json::Value,
262 ctx: &ToolContext,
263 ) -> Result<ToolOutput, ToolError> {
264 self.dispatch_boxed(name, args, ctx)?.await
265 }
266
267 pub async fn dispatch_str(
274 &self,
275 name: &str,
276 args_json: &str,
277 ctx: &ToolContext,
278 ) -> Result<ToolOutput, ToolError> {
279 self.dispatch_boxed_str(name, args_json, ctx)?.await
280 }
281
282 #[must_use]
284 pub fn is_destructive(&self, name: &str) -> bool {
285 self.definition(name)
286 .is_some_and(|def| matches!(def.effect, ToolEffect::Destructive))
287 }
288
289 #[must_use]
291 pub fn provenance(&self, name: &str) -> Option<ToolProvenance> {
292 self.definition(name).map(|def| def.provenance)
293 }
294
295 pub fn remove(&mut self, name: &str) -> bool {
297 let removed = self.tools.remove(name).is_some();
298 if removed {
299 self.invalidate_cache();
300 }
301 removed
302 }
303
304 pub fn clear(&mut self) {
306 self.tools.clear();
307 self.invalidate_cache();
308 }
309
310 #[must_use]
312 pub fn len(&self) -> usize {
313 self.tools.len()
314 }
315
316 #[must_use]
318 pub fn is_empty(&self) -> bool {
319 self.tools.is_empty()
320 }
321
322 #[must_use]
324 pub fn contains(&self, name: &str) -> bool {
325 self.tools.contains_key(name)
326 }
327
328 #[must_use]
333 pub fn definition(&self, name: &str) -> Option<&ToolDefinition> {
334 self.tools.get(name).map(|entry| &entry.definition)
335 }
336
337 #[must_use]
341 pub fn iter(&self) -> ToolDefinitions<'_> {
342 ToolDefinitions {
343 inner: self.tools.iter(),
344 }
345 }
346}
347
348pub struct ToolDefinitions<'a> {
355 inner: crate::compat::HashMapIter<'a, &'static str, RegisteredTool>,
356}
357
358impl Iterator for ToolDefinitions<'_> {
359 type Item = (&'static str, ToolDefinition);
360
361 fn next(&mut self) -> Option<Self::Item> {
362 self.inner
363 .next()
364 .map(|(name, entry)| (*name, entry.definition.clone()))
365 }
366
367 fn size_hint(&self) -> (usize, Option<usize>) {
368 self.inner.size_hint()
369 }
370}
371
372impl ExactSizeIterator for ToolDefinitions<'_> {
373 fn len(&self) -> usize {
374 self.inner.len()
375 }
376}
377
378impl<'a> IntoIterator for &'a ToolRegistry {
382 type Item = (&'static str, ToolDefinition);
383 type IntoIter = ToolDefinitions<'a>;
384
385 fn into_iter(self) -> Self::IntoIter {
386 self.iter()
387 }
388}
389
390#[cfg(all(test, feature = "std"))]
391mod tests;