use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::client::ToolDefinition;
use crate::error::AgentError;
use crate::ids::{RunId, SessionId};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolEffect {
ReadOnly,
Internal,
Write,
External,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: serde_json::Value,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolContext {
pub run_id: RunId,
pub call_id: String,
pub session_id: Option<SessionId>,
pub step: u32,
}
impl ToolContext {
#[must_use]
pub fn detached(call_id: impl Into<String>) -> Self {
Self {
run_id: RunId::new("detached"),
call_id: call_id.into(),
session_id: None,
step: 0,
}
}
}
pub trait Tool: Send + Sync + std::fmt::Debug {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn input_schema(&self) -> serde_json::Value;
fn execute<'a>(
&'a self,
input: serde_json::Value,
ctx: &'a ToolContext,
) -> Pin<Box<dyn Future<Output = Result<serde_json::Value, AgentError>> + Send + 'a>>;
fn effect(&self) -> ToolEffect {
ToolEffect::Write
}
fn definition(&self) -> ToolDefinition {
ToolDefinition {
name: self.name().to_owned(),
description: self.description().to_owned(),
input_schema: self.input_schema(),
}
}
}
type ToolFn = Box<
dyn Fn(
serde_json::Value,
ToolContext,
) -> Pin<Box<dyn Future<Output = Result<serde_json::Value, AgentError>> + Send>>
+ Send
+ Sync,
>;
pub struct FnTool {
name: String,
description: String,
schema: serde_json::Value,
effect: ToolEffect,
run: ToolFn,
}
impl std::fmt::Debug for FnTool {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FnTool")
.field("name", &self.name)
.field("description", &self.description)
.field("effect", &self.effect)
.finish_non_exhaustive()
}
}
impl FnTool {
pub fn new<F, Fut>(
name: impl Into<String>,
description: impl Into<String>,
schema: serde_json::Value,
run: F,
) -> Self
where
F: Fn(serde_json::Value) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<serde_json::Value, AgentError>> + Send + 'static,
{
Self {
name: name.into(),
description: description.into(),
schema,
effect: ToolEffect::Write,
run: Box::new(move |input, _ctx| Box::pin(run(input))),
}
}
pub fn with_context<F, Fut>(
name: impl Into<String>,
description: impl Into<String>,
schema: serde_json::Value,
run: F,
) -> Self
where
F: Fn(serde_json::Value, ToolContext) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<serde_json::Value, AgentError>> + Send + 'static,
{
Self {
name: name.into(),
description: description.into(),
schema,
effect: ToolEffect::Write,
run: Box::new(move |input, ctx| Box::pin(run(input, ctx))),
}
}
#[must_use]
pub const fn effect(mut self, effect: ToolEffect) -> Self {
self.effect = effect;
self
}
#[must_use]
pub fn shared(self) -> Arc<dyn Tool> {
Arc::new(self)
}
}
impl Tool for FnTool {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
&self.description
}
fn input_schema(&self) -> serde_json::Value {
self.schema.clone()
}
fn execute<'a>(
&'a self,
input: serde_json::Value,
ctx: &'a ToolContext,
) -> Pin<Box<dyn Future<Output = Result<serde_json::Value, AgentError>> + Send + 'a>> {
(self.run)(input, ctx.clone())
}
fn effect(&self) -> ToolEffect {
self.effect
}
}
#[cfg(test)]
mod tests;