mod batch;
use alloc::{boxed::Box, sync::Arc, vec::Vec};
pub use batch::{
AllowDestructiveBatch, BatchExecutionMode, BatchToolCall, BatchToolCallOutcome,
BatchToolCallStr,
};
use super::{
rust_tool::{ErasedTool, RustTool, definition_of},
types::{OutputSanitization, RegistryItem, ToolContext, ToolDefinition, ToolError, ToolOutput},
};
use crate::{
compat::{HashMap, RwLock, read_lock_recover, write_lock_recover},
rust_tool::BoxToolFuture,
types::ToolEffect,
};
struct RegisteredTool {
definition: ToolDefinition,
erased: Box<dyn ErasedTool>,
}
pub struct ToolRegistry {
tools: HashMap<&'static str, RegisteredTool>,
cached_definitions: RwLock<Option<Arc<[ToolDefinition]>>>,
output_floor: OutputSanitization,
}
impl core::fmt::Debug for ToolRegistry {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let names: Vec<&str> = self
.tools
.values()
.map(|r| r.definition.name.as_str())
.collect();
f.debug_struct("ToolRegistry")
.field("tool_count", &self.tools.len())
.field("tool_names", &names)
.field("output_floor", &self.output_floor)
.finish_non_exhaustive()
}
}
impl Default for ToolRegistry {
fn default() -> Self {
Self::new()
}
}
impl ToolRegistry {
#[must_use]
pub fn new() -> Self {
Self {
tools: HashMap::new(),
cached_definitions: RwLock::new(None),
output_floor: OutputSanitization::default(),
}
}
#[must_use]
pub const fn with_output_floor(mut self, floor: OutputSanitization) -> Self {
self.output_floor = floor;
self
}
pub const fn set_output_floor(&mut self, floor: OutputSanitization) -> &mut Self {
self.output_floor = floor;
self
}
#[must_use]
pub const fn output_floor(&self) -> OutputSanitization {
self.output_floor
}
#[must_use]
pub fn effective_output_sanitization(&self, name: &str) -> Option<OutputSanitization> {
self.definition(name)
.map(|def| def.output_sanitization.at_least(self.output_floor))
}
fn enforce_output_floor<'a>(
&self,
tool_policy: OutputSanitization,
future: BoxToolFuture<'a>,
) -> BoxToolFuture<'a> {
let effective = tool_policy.at_least(self.output_floor);
if effective == tool_policy {
return future;
}
Box::pin(async move { future.await.map(|output| effective.apply(output)) })
}
fn invalidate_cache(&self) {
let mut guard = write_lock_recover(&self.cached_definitions);
*guard = None;
}
pub fn register<T: RustTool + 'static>(&mut self, tool: T) -> &mut Self {
if let Err(e) = self.try_register(tool) {
panic!("Failed to build definition for tool '{}': {e}", T::NAME);
}
self
}
pub fn try_register<T: RustTool + 'static>(&mut self, tool: T) -> Result<&mut Self, ToolError> {
let definition = definition_of(&tool)?;
self.tools.insert(
T::NAME,
RegisteredTool {
definition,
erased: Box::new(tool),
},
);
self.invalidate_cache();
Ok(self)
}
#[must_use]
pub fn with_tool<T: RustTool + 'static>(mut self, tool: T) -> Self {
self.register(tool);
self
}
#[must_use]
pub fn definitions_slice(&self) -> Arc<[ToolDefinition]> {
{
let guard = read_lock_recover(&self.cached_definitions);
if let Some(ref slice) = *guard {
return Arc::clone(slice);
}
}
let mut defs: Vec<ToolDefinition> = self
.tools
.values()
.map(|entry| entry.definition.clone())
.collect();
defs.sort_by(|a, b| a.name.cmp(&b.name));
let arc_slice: Arc<[ToolDefinition]> = Arc::from(defs);
{
let mut guard = write_lock_recover(&self.cached_definitions);
*guard = Some(Arc::clone(&arc_slice));
}
arc_slice
}
#[must_use]
pub fn definitions(&self) -> Vec<ToolDefinition> {
self.definitions_slice().to_vec()
}
pub fn dispatch_boxed<'a>(
&'a self,
name: &str,
args: serde_json::Value,
ctx: &'a ToolContext,
) -> Result<BoxToolFuture<'a>, ToolError> {
let Some(entry) = self.tools.get(name) else {
return Err(ToolError::not_found(RegistryItem::Tool, name));
};
let future = entry.erased.call_erased(args, ctx);
Ok(self.enforce_output_floor(entry.definition.output_sanitization, future))
}
pub fn dispatch_boxed_str<'a>(
&'a self,
name: &str,
args_json: &'a str,
ctx: &'a ToolContext,
) -> Result<BoxToolFuture<'a>, ToolError> {
let Some(entry) = self.tools.get(name) else {
return Err(ToolError::not_found(RegistryItem::Tool, name));
};
let future = entry.erased.call_erased_str(args_json, ctx);
Ok(self.enforce_output_floor(entry.definition.output_sanitization, future))
}
pub async fn dispatch(
&self,
name: &str,
args: serde_json::Value,
ctx: &ToolContext,
) -> Result<ToolOutput, ToolError> {
self.dispatch_boxed(name, args, ctx)?.await
}
pub async fn dispatch_str(
&self,
name: &str,
args_json: &str,
ctx: &ToolContext,
) -> Result<ToolOutput, ToolError> {
self.dispatch_boxed_str(name, args_json, ctx)?.await
}
#[must_use]
pub fn is_destructive(&self, name: &str) -> bool {
self.definition(name)
.is_some_and(|def| matches!(def.effect, ToolEffect::Destructive))
}
pub fn remove(&mut self, name: &str) -> bool {
let removed = self.tools.remove(name).is_some();
if removed {
self.invalidate_cache();
}
removed
}
pub fn clear(&mut self) {
self.tools.clear();
self.invalidate_cache();
}
#[must_use]
pub fn len(&self) -> usize {
self.tools.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.tools.is_empty()
}
#[must_use]
pub fn contains(&self, name: &str) -> bool {
self.tools.contains_key(name)
}
#[must_use]
pub fn definition(&self, name: &str) -> Option<&ToolDefinition> {
self.tools.get(name).map(|entry| &entry.definition)
}
#[must_use]
pub fn iter(&self) -> ToolDefinitions<'_> {
ToolDefinitions {
inner: self.tools.iter(),
}
}
}
pub struct ToolDefinitions<'a> {
inner: crate::compat::HashMapIter<'a, &'static str, RegisteredTool>,
}
impl Iterator for ToolDefinitions<'_> {
type Item = (&'static str, ToolDefinition);
fn next(&mut self) -> Option<Self::Item> {
self.inner
.next()
.map(|(name, entry)| (*name, entry.definition.clone()))
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl ExactSizeIterator for ToolDefinitions<'_> {
fn len(&self) -> usize {
self.inner.len()
}
}
impl<'a> IntoIterator for &'a ToolRegistry {
type Item = (&'static str, ToolDefinition);
type IntoIter = ToolDefinitions<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
#[cfg(all(test, feature = "std"))]
mod tests;