#[cfg(feature = "tool_health")]
pub mod health;
#[cfg(feature = "tool_shield")]
pub mod shield;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::fmt;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use crate::message::ToolContent as MessageToolContent;
pub mod permission;
pub mod registry;
pub use permission::PermissionCheck;
pub use registry::{FnTool, ToolRegistry};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolSchema {
pub tool: String,
pub description: String,
pub input_schema: Value,
}
#[derive(Debug, Clone)]
pub struct ToolOutput {
pub payload: MessageToolContent,
pub is_error: bool,
}
impl ToolOutput {
pub fn success(payload: impl Into<MessageToolContent>) -> Self {
Self {
payload: payload.into(),
is_error: false,
}
}
pub fn error(payload: impl Into<MessageToolContent>) -> Self {
Self {
payload: payload.into(),
is_error: true,
}
}
pub fn text(text: impl Into<String>) -> Self {
Self::success(text.into())
}
pub fn error_text(text: impl Into<String>) -> Self {
Self::error(text.into())
}
#[must_use]
pub fn text_content(&self) -> String {
match &self.payload {
MessageToolContent::Text(s) => s.clone(),
MessageToolContent::Multipart(parts) => {
use crate::message::ToolContentPart;
parts
.iter()
.filter_map(|p| match p {
ToolContentPart::Text { text } => Some(text.clone()),
ToolContentPart::Image { .. } => None,
})
.collect::<Vec<_>>()
.join("\n")
}
}
}
}
impl From<String> for ToolOutput {
fn from(s: String) -> Self {
Self::text(s)
}
}
impl From<&str> for ToolOutput {
fn from(s: &str) -> Self {
Self::text(s)
}
}
#[derive(Debug, Clone)]
pub struct ToolDispatchResult {
pub tool_call_id: String,
pub output: crate::message::ToolContent,
pub is_error: bool,
pub duration: Duration,
pub resolved_tool_name: String,
}
impl ToolDispatchResult {
#[must_use]
pub fn ok(tool_name: &str, output: String, duration: Duration) -> Self {
Self {
tool_call_id: String::new(),
output: crate::message::ToolContent::Text(output),
is_error: false,
duration,
resolved_tool_name: tool_name.to_string(),
}
}
#[must_use]
pub fn err(tool_name: &str, message: String, duration: Duration) -> Self {
Self {
tool_call_id: String::new(),
output: crate::message::ToolContent::Text(message),
is_error: true,
duration,
resolved_tool_name: tool_name.to_string(),
}
}
#[must_use]
pub fn from_tool_output(tool_name: &str, output: ToolOutput, duration: Duration) -> Self {
Self::from(output)
.with_tool_name(tool_name)
.with_duration(duration)
}
#[must_use]
pub fn with_call_id(mut self, id: impl Into<String>) -> Self {
self.tool_call_id = id.into();
self
}
#[must_use]
pub fn with_tool_name(mut self, name: &str) -> Self {
name.clone_into(&mut self.resolved_tool_name);
self
}
#[must_use]
pub fn with_duration(mut self, dur: Duration) -> Self {
self.duration = dur;
self
}
#[must_use]
pub fn from_tool_error(tool_name: &str, error: &ToolError, duration: Duration) -> Self {
Self {
tool_call_id: String::new(),
output: crate::message::ToolContent::Text(error.to_string()),
is_error: true,
duration,
resolved_tool_name: tool_name.to_string(),
}
}
#[must_use]
pub fn from_result(
tool_name: &str,
result: Result<ToolOutput, ToolError>,
duration: Duration,
) -> Self {
match result {
Ok(output) => Self::from_tool_output(tool_name, output, duration),
Err(e) => Self::from_tool_error(tool_name, &e, duration),
}
}
}
impl From<ToolOutput> for ToolDispatchResult {
fn from(output: ToolOutput) -> Self {
Self {
tool_call_id: String::new(),
output: output.payload,
is_error: output.is_error,
duration: Duration::ZERO,
resolved_tool_name: String::new(),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ToolError {
#[error("Tool not found: {0}. Available: {1}")]
NotFound(String, String),
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Execution error: {0}")]
Execution(String),
#[error("Permission denied: {0}")]
Permission(String),
#[error("File not found: {0}")]
FileNotFound(String),
#[error("Timeout after {0}s")]
Timeout(u64),
#[error("Cancelled")]
Cancelled,
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
}
impl ToolError {
pub fn not_found(tool: impl Into<String>, available: &[&str]) -> Self {
let tool = tool.into();
let available_str = if available.is_empty() {
"none registered".to_string()
} else if available.len() <= 10 {
available.join(", ")
} else {
let count = available.len().saturating_sub(10);
format!(
"{}... (and {count} more)",
available
.iter()
.take(10)
.copied()
.collect::<Vec<_>>()
.join(", "),
)
};
Self::NotFound(tool, available_str)
}
}
#[derive(Clone)]
pub struct ToolContext {
pub cwd: String,
pub session_id: uuid::Uuid,
pub temp_dir: String,
pub is_non_interactive: bool,
pub user_context: HashMap<String, String>,
pub extensions: HashMap<std::any::TypeId, Arc<dyn std::any::Any + Send + Sync>>,
}
impl ToolContext {
pub fn set_extension<T: 'static + Send + Sync>(&mut self, val: T) {
self.extensions
.insert(std::any::TypeId::of::<T>(), Arc::new(val));
}
#[must_use]
pub fn get_extension<T: 'static>(&self) -> Option<&T> {
self.extensions
.get(&std::any::TypeId::of::<T>())
.and_then(|arc| arc.downcast_ref::<T>())
}
}
impl fmt::Debug for ToolContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ToolContext")
.field("cwd", &self.cwd)
.field("session_id", &self.session_id)
.field("temp_dir", &self.temp_dir)
.field("is_non_interactive", &self.is_non_interactive)
.field("user_context", &self.user_context)
.field("extensions", &format!("{} entries", self.extensions.len()))
.finish()
}
}
impl Default for ToolContext {
fn default() -> Self {
Self {
cwd: ".".to_string(),
session_id: uuid::Uuid::new_v4(),
temp_dir: std::env::temp_dir().to_string_lossy().to_string(),
is_non_interactive: false,
user_context: HashMap::new(),
extensions: HashMap::new(),
}
}
}
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn schema(&self) -> ToolSchema;
fn call(
&self,
input: Value,
context: &ToolContext,
) -> Pin<Box<dyn Future<Output = Result<ToolOutput, ToolError>> + Send + '_>>;
fn is_concurrency_safe(&self) -> bool {
false
}
fn is_safe_for_concurrent_execution(&self, _input: &Value) -> bool {
self.is_concurrency_safe()
}
fn is_read_only(&self) -> bool {
false
}
fn system_prompt(&self) -> Option<String> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
struct EchoTool;
impl Tool for EchoTool {
fn name(&self) -> &'static str {
"echo"
}
fn description(&self) -> &'static str {
"Echoes back the input"
}
fn schema(&self) -> ToolSchema {
ToolSchema {
tool: "echo".into(),
description: "Echoes back the input".into(),
input_schema: json!({
"type": "object",
"properties": { "message": { "type": "string" } },
"required": ["message"]
}),
}
}
fn call(
&self,
input: Value,
_context: &ToolContext,
) -> Pin<Box<dyn Future<Output = Result<ToolOutput, ToolError>> + Send + '_>> {
let msg = input
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
Box::pin(async move { Ok(ToolOutput::text(msg)) })
}
fn is_concurrency_safe(&self) -> bool {
true
}
fn is_read_only(&self) -> bool {
true
}
}
struct FailTool;
impl Tool for FailTool {
fn name(&self) -> &'static str {
"fail"
}
fn description(&self) -> &'static str {
"Always fails"
}
fn schema(&self) -> ToolSchema {
ToolSchema {
tool: "fail".into(),
description: "Always fails".into(),
input_schema: json!({ "type": "object", "properties": {} }),
}
}
fn call(
&self,
_input: Value,
_context: &ToolContext,
) -> Pin<Box<dyn Future<Output = Result<ToolOutput, ToolError>> + Send + '_>> {
Box::pin(async { Err(ToolError::Execution("always fails".into())) })
}
}
#[test]
fn test_tool_result_success() {
let result = ToolOutput::text("hello");
assert!(!result.is_error);
assert_eq!(result.text_content(), "hello");
}
#[test]
fn test_tool_result_error() {
let result = ToolOutput::error_text("something went wrong");
assert!(result.is_error);
assert_eq!(result.text_content(), "something went wrong");
}
#[test]
fn test_tool_result_from_string() {
let result: ToolOutput = "hello".into();
assert!(!result.is_error);
}
#[test]
fn test_tool_result_from_str() {
let result: ToolOutput = "hello".into();
assert!(!result.is_error);
}
#[test]
fn test_tool_error_not_found() {
let err = ToolError::not_found("missing", &["tool_a", "tool_b"]);
assert!(err.to_string().contains("missing"));
assert!(err.to_string().contains("tool_a"));
}
#[test]
fn test_tool_error_not_found_empty() {
let err = ToolError::not_found("missing", &[]);
assert!(err.to_string().contains("none registered"));
}
#[test]
fn test_tool_error_not_found_many() {
let tools: Vec<&str> = (0..15)
.map(|i| Box::leak(format!("tool_{i}").into_boxed_str()) as &str)
.collect();
let err = ToolError::not_found("missing", &tools);
assert!(err.to_string().contains("and 5 more"));
}
#[test]
fn test_tool_context_default() {
let ctx = ToolContext::default();
assert_eq!(ctx.cwd, ".");
assert!(!ctx.is_non_interactive);
}
#[test]
fn test_permission_check_allow() {
let check = PermissionCheck::allow();
assert!(check.is_allow());
assert!(!check.is_deny());
}
#[test]
fn test_permission_check_deny() {
let check = PermissionCheck::deny("unsafe");
assert!(check.is_deny());
assert!(!check.is_allow());
}
#[test]
fn test_permission_check_ask() {
let check = PermissionCheck::ask("Run this?");
assert!(check.is_ask());
}
#[test]
fn test_permission_check_modify() {
let check = PermissionCheck::modify(json!({"safe": true}));
assert!(check.is_modify());
}
#[test]
fn test_tool_schema_serialization() {
let schema = ToolSchema {
tool: "test".into(),
description: "A test tool".into(),
input_schema: json!({"type": "object"}),
};
let json = serde_json::to_string(&schema).unwrap();
let back: ToolSchema = serde_json::from_str(&json).unwrap();
assert_eq!(schema.tool, back.tool);
}
#[test]
fn test_registry_register_and_get() {
let mut registry = ToolRegistry::new();
assert!(registry.is_empty());
registry.register(EchoTool);
assert_eq!(registry.len(), 1);
assert!(registry.contains("echo"));
let tool = registry.get("echo").unwrap();
assert_eq!(tool.name(), "echo");
}
#[test]
fn test_registry_get_missing() {
let registry = ToolRegistry::new();
assert!(registry.get("missing").is_none());
}
#[test]
fn test_registry_all_schemas() {
let mut registry = ToolRegistry::new();
registry.register(EchoTool);
let schemas = registry.all_schemas();
assert_eq!(schemas.len(), 1);
assert_eq!(schemas[0].tool, "echo");
}
#[test]
fn test_registry_tool_names() {
let mut registry = ToolRegistry::new();
registry.register(FailTool);
registry.register(EchoTool);
let names = registry.tool_names();
assert_eq!(names, vec!["echo", "fail"]);
}
#[test]
fn test_registry_concurrent_safe_tools() {
let mut registry = ToolRegistry::new();
registry.register(EchoTool);
registry.register(FailTool);
let safe = registry.concurrent_safe_tools();
assert_eq!(safe.len(), 1);
assert_eq!(safe[0].name(), "echo");
}
#[tokio::test]
async fn test_echo_tool_call() {
let tool = EchoTool;
let ctx = ToolContext::default();
let result = tool.call(json!({"message": "hello"}), &ctx).await;
assert!(result.is_ok());
let result = result.unwrap();
assert_eq!(result.text_content(), "hello");
}
#[tokio::test]
async fn test_fail_tool_call() {
let tool = FailTool;
let ctx = ToolContext::default();
let result = tool.call(json!({}), &ctx).await;
assert!(result.is_err());
}
#[test]
fn test_tool_trait_concurrency_default() {
let tool = FailTool;
assert!(!tool.is_concurrency_safe());
assert!(!tool.is_safe_for_concurrent_execution(&json!({})));
}
#[test]
fn test_tool_trait_read_only_default() {
let tool = FailTool;
assert!(!tool.is_read_only());
}
#[test]
fn test_tool_trait_system_prompt_default() {
let tool = EchoTool;
assert!(tool.system_prompt().is_none());
}
}