#![cfg(feature = "tool")]
use std::{collections::HashMap, fmt::Display, sync::Arc};
use anyhow::Result;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ToolType {
Function,
}
impl Display for ToolType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "function")
}
}
#[async_trait]
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
async fn execute(&self, args: JsonValue) -> Result<String, ToolCallError>;
fn get_type(&self) -> ToolType {
ToolType::Function
}
fn schema(&self) -> JsonValue;
}
#[derive(Debug, Error)]
pub enum ToolCallError {
#[error("parameter mismatch: {0}")]
ParameterMismatch(JsonValue),
#[error("tool execution error: {0}")]
ToolExecutionError(#[from] anyhow::Error),
#[error("tool unavailable: {0}")]
ToolUnavailable(String),
#[error("tool not found: {0}")]
ToolNotFound(String),
#[error("approval required for {tool_name}: {reason}")]
ApprovalRequired {
call_id: String,
tool_name: String,
reason: String,
},
#[error("tool call rejected: {reason}")]
Rejected { reason: String },
}
#[derive(Clone)]
pub struct ToolRegistryEntry {
pub tool: Arc<dyn Tool>,
pub available: bool,
}
impl ToolRegistryEntry {
pub fn new(tool: Arc<dyn Tool>, available: bool) -> Self {
Self { tool, available }
}
pub fn is_available(&self) -> bool {
self.available
}
pub fn new_available(tool: Arc<dyn Tool>) -> Self {
Self::new(tool, true)
}
pub fn new_unavailable(tool: Arc<dyn Tool>) -> Self {
Self::new(tool, false)
}
}
#[doc(hidden)]
#[derive(Clone)]
pub struct RawToolRegistry {
tools: HashMap<String, ToolRegistryEntry>,
}
impl Default for RawToolRegistry {
fn default() -> Self {
Self::new()
}
}
impl RawToolRegistry {
pub fn new() -> Self {
Self {
tools: HashMap::new(),
}
}
pub fn add_tool(&mut self, tool: Arc<dyn Tool>) {
self.tools.insert(
tool.name().to_string(),
ToolRegistryEntry::new_available(tool),
);
}
pub fn get_tool(&self, name: &str) -> Option<&ToolRegistryEntry> {
self.tools.get(name)
}
pub fn get_tool_arc(&self, name: &str) -> Option<Arc<dyn Tool>> {
self.get_tool(name)
.filter(|entry| entry.is_available())
.map(|entry| entry.tool.clone())
}
pub fn remove_tool(&mut self, name: &str) {
self.tools.remove(name);
}
pub fn remove_tool_if_same(&mut self, name: &str, tool: &Arc<dyn Tool>) -> bool {
match self.tools.get(name) {
Some(entry) if Arc::ptr_eq(&entry.tool, tool) => {
self.tools.remove(name);
true
}
_ => false,
}
}
pub fn tool_exists(&self, name: &str) -> bool {
self.tools.contains_key(name)
}
pub fn tool_count(&self) -> usize {
self.tools.len()
}
pub fn get_all_tools(&self) -> &HashMap<String, ToolRegistryEntry> {
&self.tools
}
pub fn available_tools_json(&self) -> JsonValue {
self.tools
.values()
.filter_map(|tool| {
if tool.is_available() {
Some(tool.tool.schema())
} else {
None
}
})
.collect::<Vec<_>>()
.into()
}
pub async fn call_tool(&self, name: &str, args: JsonValue) -> Result<String, ToolCallError> {
if let Some(tool) = self.get_tool(name) {
if tool.is_available() {
tool.tool.execute(args).await
} else {
Err(ToolCallError::ToolUnavailable(name.to_string()))
}
} else {
Err(ToolCallError::ToolNotFound(name.to_string()))
}
}
}
#[cfg(feature = "security")]
pub use crate::security::registry::GuardedToolRegistry as ToolRegistry;
#[cfg(not(feature = "security"))]
pub use RawToolRegistry as ToolRegistry;
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn rejected_error_display() {
let e = ToolCallError::Rejected {
reason: "access denied".into(),
};
let msg = format!("{e}");
assert!(msg.contains("access denied"), "msg: {msg}");
}
#[test]
fn approval_required_error_display() {
let e = ToolCallError::ApprovalRequired {
call_id: "c1".into(),
tool_name: "shell".into(),
reason: "needs approval".into(),
};
let msg = format!("{e}");
assert!(msg.contains("shell"), "msg: {msg}");
assert!(msg.contains("approval"), "msg: {msg}");
}
struct MockTool;
#[async_trait]
impl Tool for MockTool {
fn name(&self) -> &str {
"mock"
}
fn description(&self) -> &str {
"mock tool"
}
fn schema(&self) -> JsonValue {
json!({"type": "function", "function": {"name": "mock"}})
}
async fn execute(&self, _args: JsonValue) -> Result<String, ToolCallError> {
Ok("done".into())
}
}
#[test]
fn get_tool_arc_returns_available_tool_or_none() {
let mut reg = RawToolRegistry::new();
assert!(reg.get_tool_arc("mock").is_none());
reg.add_tool(Arc::new(MockTool));
let tool = reg.get_tool_arc("mock");
assert!(
tool.is_some(),
"registered tool must be clonable via get_tool_arc"
);
assert_eq!(tool.unwrap().name(), "mock");
assert!(reg.get_tool_arc("missing").is_none());
}
#[test]
fn remove_tool_if_same_only_removes_matching_arc() {
let mut reg = RawToolRegistry::new();
let original: Arc<dyn Tool> = Arc::new(MockTool);
reg.add_tool(Arc::clone(&original));
let other: Arc<dyn Tool> = Arc::new(MockTool);
assert!(!reg.remove_tool_if_same("mock", &other));
assert!(reg.tool_exists("mock"));
assert!(reg.remove_tool_if_same("mock", &original));
assert!(!reg.tool_exists("mock"));
}
}