use std::collections::BTreeSet;
use std::fmt;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use serde_json::Value;
use crate::error::{Error, Result};
use crate::provider::ToolSpec;
pub type ToolFuture<'a> = Pin<Box<dyn Future<Output = Result<String>> + Send + 'a>>;
pub trait Tool: Send + Sync {
fn spec(&self) -> ToolSpec;
fn invoke<'a>(&'a self, arguments: &'a Value) -> ToolFuture<'a>;
}
#[derive(Clone, Default)]
pub struct Toolbox {
tools: Vec<Arc<dyn Tool>>,
}
impl Toolbox {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with(mut self, tool: impl Tool + 'static) -> Self {
self.tools.push(Arc::new(tool));
self
}
#[must_use]
pub fn with_arc(mut self, tool: Arc<dyn Tool>) -> Self {
self.tools.push(tool);
self
}
pub fn is_empty(&self) -> bool {
self.tools.is_empty()
}
pub fn len(&self) -> usize {
self.tools.len()
}
pub fn specs(&self) -> Vec<ToolSpec> {
self.tools.iter().map(|t| t.spec()).collect()
}
pub fn names(&self) -> Vec<String> {
self.tools.iter().map(|t| t.spec().name).collect()
}
pub fn owns(&self, name: &str) -> bool {
self.tools.iter().any(|t| t.spec().name == name)
}
pub fn get(&self, name: &str) -> Option<&Arc<dyn Tool>> {
self.tools.iter().find(|t| t.spec().name == name)
}
pub fn validate(&self) -> Result<()> {
let mut seen: BTreeSet<String> = BTreeSet::new();
for tool in &self.tools {
let name = tool.spec().name;
if name.trim().is_empty() {
return Err(Error::Config(
"a registered tool has an empty name; the model addresses a tool by name, so \
every Tool::spec() must return a non-empty one"
.into(),
));
}
if RESERVED_TOOL_NAMES.contains(&name.as_str()) {
return Err(Error::Config(format!(
"registered tool {name:?} takes the name of a built-in tool. Built-ins are \
{}; rename the registered tool so a caller cannot silently replace the \
harness's own file, search, spawn, or skill tools",
RESERVED_TOOL_NAMES.join(", ")
)));
}
if name.starts_with(crate::mcp::MCP_TOOL_PREFIX) {
return Err(Error::Config(format!(
"registered tool {name:?} uses the {:?} prefix, which is reserved for MCP \
server tools. An in-process tool that took it could impersonate a tool the \
operator believes came from a configured server",
crate::mcp::MCP_TOOL_PREFIX
)));
}
if !seen.insert(name.clone()) {
return Err(Error::Config(format!(
"two registered tools are both named {name:?}; which one a model call reached \
would depend on registration order, so the set is rejected instead"
)));
}
}
Ok(())
}
}
pub(crate) const RESERVED_TOOL_NAMES: &[&str] = &[
super::WRITE_FILE_TOOL,
super::GREP_TOOL,
super::FIND_TOOL,
super::READ_FILE_TOOL,
super::READ_SKILL_TOOL,
super::REMEMBER_TOOL,
crate::run::SPAWN_TOOL,
];
impl fmt::Debug for Toolbox {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("Toolbox").field(&self.names()).finish()
}
}
impl<T: Tool + 'static> FromIterator<T> for Toolbox {
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
iter.into_iter().fold(Toolbox::new(), |b, t| b.with(t))
}
}