use crate::tool::ToolDef;
use serde::de::DeserializeOwned;
use serde_json::Value;
#[derive(Debug, Clone)]
pub struct ToolOutput {
pub content: String,
pub done: bool,
pub waiting: bool,
}
impl ToolOutput {
pub fn text(content: impl Into<String>) -> Self {
Self {
content: content.into(),
done: false,
waiting: false,
}
}
pub fn done(content: impl Into<String>) -> Self {
Self {
content: content.into(),
done: true,
waiting: false,
}
}
pub fn waiting(question: impl Into<String>) -> Self {
Self {
content: question.into(),
done: false,
waiting: true,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ToolError {
#[error("{0}")]
Execution(String),
#[error("invalid args: {0}")]
InvalidArgs(String),
}
pub fn parse_args<T: DeserializeOwned>(args: &Value) -> Result<T, ToolError> {
serde_json::from_value(args.clone()).map_err(|e| ToolError::InvalidArgs(e.to_string()))
}
#[async_trait::async_trait]
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn is_system(&self) -> bool {
false
}
fn is_read_only(&self) -> bool {
false
}
fn parameters_schema(&self) -> Value;
async fn execute(
&self,
args: Value,
ctx: &mut super::context::AgentContext,
) -> Result<ToolOutput, ToolError>;
async fn execute_readonly(&self, args: Value) -> Result<ToolOutput, ToolError> {
let _ = args;
panic!("execute_readonly called on tool that doesn't implement it")
}
fn to_def(&self) -> ToolDef {
ToolDef {
name: self.name().to_string(),
description: self.description().to_string(),
parameters: self.parameters_schema(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::AgentContext;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct EchoArgs {
message: String,
}
struct EchoTool;
#[async_trait::async_trait]
impl Tool for EchoTool {
fn name(&self) -> &str {
"echo"
}
fn description(&self) -> &str {
"Echo a message back"
}
fn parameters_schema(&self) -> Value {
serde_json::json!({
"type": "object",
"properties": {
"message": { "type": "string" }
},
"required": ["message"]
})
}
async fn execute(
&self,
args: Value,
_ctx: &mut AgentContext,
) -> Result<ToolOutput, ToolError> {
let a: EchoArgs = parse_args(&args)?;
Ok(ToolOutput::text(a.message))
}
}
#[test]
fn parse_args_valid() {
let args = serde_json::json!({"message": "hello"});
let parsed: EchoArgs = parse_args(&args).unwrap();
assert_eq!(parsed.message, "hello");
}
#[test]
fn parse_args_invalid() {
let args = serde_json::json!({"wrong_field": 42});
let result = parse_args::<EchoArgs>(&args);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), ToolError::InvalidArgs(_)));
}
#[test]
fn tool_to_def() {
let tool = EchoTool;
let def = tool.to_def();
assert_eq!(def.name, "echo");
assert_eq!(def.description, "Echo a message back");
assert!(def.parameters["properties"]["message"].is_object());
}
#[tokio::test]
async fn tool_execute() {
let tool = EchoTool;
let mut ctx = AgentContext::new();
let args = serde_json::json!({"message": "world"});
let output = tool.execute(args, &mut ctx).await.unwrap();
assert_eq!(output.content, "world");
assert!(!output.done);
}
#[test]
fn tool_output_done() {
let out = ToolOutput::done("finished");
assert!(out.done);
assert_eq!(out.content, "finished");
}
}