#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![deny(unreachable_pub)]
use std::sync::{Arc, RwLockReadGuard, RwLockWriteGuard};
use async_trait::async_trait;
use behest_context::ToolContext;
use behest_core::tool_types::{ToolCall, ToolSpec};
use serde::{Deserialize, Serialize};
use serde_json::Value;
mod strategy;
pub use strategy::{ExecutionPlan, ToolExecutionStrategy};
pub use behest_core::error::ToolError;
pub type ToolResult<T = ToolOutput> = std::result::Result<T, ToolError>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolOutput {
pub value: Value,
}
impl ToolOutput {
#[must_use]
pub fn new(value: Value) -> Self {
Self { value }
}
#[must_use]
pub fn text(text: impl Into<String>) -> Self {
Self {
value: Value::String(text.into()),
}
}
#[must_use]
pub fn error(message: impl Into<String>) -> Self {
Self {
value: serde_json::json!({"error": message.into()}),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SideEffects {
pub read: bool,
pub write: bool,
pub external: bool,
pub destructive: bool,
}
impl SideEffects {
#[must_use]
pub const fn is_concurrency_safe(&self) -> bool {
!self.write && !self.destructive
}
}
#[async_trait]
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn parameters_schema(&self) -> Value;
fn response_schema(&self) -> Option<Value> {
None
}
async fn execute(&self, args: Value) -> ToolResult<ToolOutput>;
async fn execute_with_ctx(
&self,
_ctx: &dyn ToolContext,
args: Value,
) -> ToolResult<ToolOutput> {
self.execute(args).await
}
fn is_read_only(&self) -> bool {
false
}
fn is_concurrency_safe(&self) -> bool {
false
}
fn is_long_running(&self) -> bool {
false
}
fn side_effects(&self) -> SideEffects {
SideEffects::default()
}
fn requires_approval(&self) -> bool {
false
}
fn approval_reason(&self) -> Option<String> {
None
}
fn required_scopes(&self) -> &[&str] {
&[]
}
#[must_use]
fn to_spec(&self) -> ToolSpec {
ToolSpec::new(self.name(), self.description(), self.parameters_schema())
}
}
pub type ToolHandler = dyn Fn(Value) -> std::pin::Pin<Box<dyn std::future::Future<Output = ToolResult<Value>> + Send>>
+ Send
+ Sync;
pub struct FunctionTool {
name: String,
description: String,
parameters_schema: Value,
read_only: bool,
concurrency_safe: bool,
long_running: bool,
side_effects: SideEffects,
approval_required: bool,
approval_reason: Option<String>,
handler: Box<ToolHandler>,
}
impl std::fmt::Debug for FunctionTool {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FunctionTool")
.field("name", &self.name)
.field("read_only", &self.read_only)
.finish()
}
}
impl FunctionTool {
#[must_use]
pub fn new<F, Fut>(
name: impl Into<String>,
description: impl Into<String>,
parameters_schema: Value,
handler: F,
) -> Self
where
F: Fn(Value) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = ToolResult<Value>> + Send + 'static,
{
Self {
name: name.into(),
description: description.into(),
parameters_schema,
read_only: false,
concurrency_safe: false,
long_running: false,
side_effects: SideEffects::default(),
approval_required: false,
approval_reason: None,
handler: Box::new(move |args| Box::pin(handler(args))),
}
}
#[must_use]
pub fn read_only(mut self) -> Self {
self.read_only = true;
self.concurrency_safe = true;
self.side_effects.read = true;
self
}
#[must_use]
pub fn concurrency_safe(mut self) -> Self {
self.concurrency_safe = true;
self
}
#[must_use]
pub fn long_running(mut self) -> Self {
self.long_running = true;
self
}
#[must_use]
pub fn side_effects(mut self, effects: SideEffects) -> Self {
self.side_effects = effects;
self
}
#[must_use]
pub fn requires_approval(mut self, reason: impl Into<String>) -> Self {
self.approval_required = true;
self.approval_reason = Some(reason.into());
self
}
}
#[async_trait]
impl Tool for FunctionTool {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
&self.description
}
fn parameters_schema(&self) -> Value {
self.parameters_schema.clone()
}
async fn execute(&self, args: Value) -> ToolResult<ToolOutput> {
let value = (self.handler)(args).await?;
Ok(ToolOutput::new(value))
}
fn is_read_only(&self) -> bool {
self.read_only
}
fn is_concurrency_safe(&self) -> bool {
self.concurrency_safe
}
fn is_long_running(&self) -> bool {
self.long_running
}
fn side_effects(&self) -> SideEffects {
self.side_effects
}
fn requires_approval(&self) -> bool {
self.approval_required
}
fn approval_reason(&self) -> Option<String> {
self.approval_reason.clone()
}
}
pub struct ToolRegistry {
tools: std::sync::RwLock<std::collections::HashMap<String, Arc<dyn Tool>>>,
}
impl std::fmt::Debug for ToolRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let names = self.names();
f.debug_struct("ToolRegistry")
.field("tools", &names)
.finish()
}
}
impl Default for ToolRegistry {
fn default() -> Self {
Self::new()
}
}
impl Clone for ToolRegistry {
fn clone(&self) -> Self {
let map = self.read_tools().clone();
Self {
tools: std::sync::RwLock::new(map),
}
}
}
impl ToolRegistry {
#[must_use]
pub fn new() -> Self {
Self {
tools: std::sync::RwLock::new(std::collections::HashMap::new()),
}
}
pub fn register<T: Tool + 'static>(&self, tool: T) -> Option<Arc<dyn Tool>> {
self.write_tools()
.insert(tool.name().to_string(), Arc::new(tool))
}
pub fn register_arc(&self, tool: Arc<dyn Tool>) -> Option<Arc<dyn Tool>> {
self.write_tools().insert(tool.name().to_string(), tool)
}
pub fn unregister(&self, name: &str) -> Option<Arc<dyn Tool>> {
self.write_tools().remove(name)
}
#[must_use]
pub fn get(&self, name: &str) -> Option<Arc<dyn Tool>> {
self.read_tools().get(name).cloned()
}
#[must_use]
pub fn names(&self) -> Vec<String> {
self.read_tools().keys().cloned().collect()
}
#[must_use]
pub fn len(&self) -> usize {
self.read_tools().len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.read_tools().is_empty()
}
#[must_use]
pub fn specs(&self) -> Vec<ToolSpec> {
let mut specs: Vec<_> = self.read_tools().values().map(|t| t.to_spec()).collect();
specs.sort_by(|a, b| a.name.cmp(&b.name));
specs
}
pub async fn execute(&self, ctx: &dyn ToolContext, call: &ToolCall) -> ToolResult<ToolOutput> {
let tool = self.get(&call.name).ok_or_else(|| ToolError::NotFound {
name: call.name.clone(),
})?;
tool.execute_with_ctx(ctx, call.arguments.clone()).await
}
fn read_tools(&self) -> RwLockReadGuard<'_, std::collections::HashMap<String, Arc<dyn Tool>>> {
match self.tools.read() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
}
}
fn write_tools(
&self,
) -> RwLockWriteGuard<'_, std::collections::HashMap<String, Arc<dyn Tool>>> {
match self.tools.write() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_tool(name: &str, read_only: bool) -> FunctionTool {
let n = name.to_string();
FunctionTool::new(
name,
format!("Tool {name}"),
serde_json::json!({"type": "object", "properties": {}}),
move |_args| {
let n = n.clone();
Box::pin(async move { Ok(serde_json::Value::String(format!("{n} done"))) })
},
)
.let_ro(read_only)
}
impl FunctionTool {
fn let_ro(mut self, read_only: bool) -> Self {
if read_only {
self.read_only = true;
self.concurrency_safe = true;
}
self
}
}
#[test]
fn tool_registry_register_and_get() {
let reg = ToolRegistry::new();
reg.register(make_tool("echo", true));
assert!(reg.get("echo").is_some());
assert!(reg.get("nonexistent").is_none());
assert_eq!(reg.len(), 1);
}
#[test]
fn tool_registry_unregister() {
let reg = ToolRegistry::new();
reg.register(make_tool("echo", true));
assert!(reg.unregister("echo").is_some());
assert!(reg.is_empty());
}
#[test]
fn tool_specs_sorted() {
let reg = ToolRegistry::new();
reg.register(make_tool("zulu", true));
reg.register(make_tool("alpha", true));
let specs = reg.specs();
assert_eq!(specs.len(), 2);
assert_eq!(specs[0].name, "alpha");
assert_eq!(specs[1].name, "zulu");
}
#[test]
fn tool_registry_recovers_from_poisoned_lock() {
let reg = Arc::new(ToolRegistry::new());
reg.register(make_tool("before", true));
let cloned = Arc::clone(®);
let _ = std::thread::spawn(move || {
let _guard = cloned.tools.write();
panic!("poison registry");
})
.join();
reg.register(make_tool("after", true));
assert_eq!(reg.len(), 2);
assert!(reg.get("before").is_some());
assert!(reg.get("after").is_some());
}
#[test]
fn side_effects_concurrency_safety() {
let read_only = SideEffects {
read: true,
..Default::default()
};
assert!(read_only.is_concurrency_safe());
let write = SideEffects {
write: true,
..Default::default()
};
assert!(!write.is_concurrency_safe());
let destructive = SideEffects {
destructive: true,
..Default::default()
};
assert!(!destructive.is_concurrency_safe());
}
}