use async_trait::async_trait;
use schemars::JsonSchema;
use serde::Deserialize as _;
use serde::de::DeserializeOwned;
use std::sync::Arc;
use tokio::sync::oneshot;
use super::{AgentTool, AgentToolResult, ToolContext, ToolError, ToolExecutionMode};
#[async_trait]
pub trait TypedTool: Send + Sync + 'static {
type Args: DeserializeOwned + JsonSchema + Send + 'static;
async fn execute_typed(
&self,
tool_call_id: &str,
args: Self::Args,
signal: Option<oneshot::Receiver<()>>,
ctx: &ToolContext,
) -> Result<AgentToolResult, ToolError>;
}
pub struct TypedToolAdapter<T: TypedTool + ?Sized>(Arc<T>);
impl<T: TypedTool + AgentTool + ?Sized> std::fmt::Debug for TypedToolAdapter<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TypedToolAdapter")
.field("name", &self.0.name())
.finish()
}
}
#[async_trait]
impl<T: TypedTool + AgentTool + ?Sized> AgentTool for TypedToolAdapter<T> {
fn name(&self) -> &str {
self.0.name()
}
fn label(&self) -> &str {
self.0.label()
}
fn description(&self) -> &str {
self.0.description()
}
fn essential(&self) -> bool {
self.0.essential()
}
fn execution_mode(&self) -> ToolExecutionMode {
self.0.execution_mode()
}
fn parameters_schema(&self) -> serde_json::Value {
self.0.parameters_schema()
}
async fn execute(
&self,
tool_call_id: &str,
params: serde_json::Value,
signal: Option<oneshot::Receiver<()>>,
ctx: &ToolContext,
) -> Result<AgentToolResult, ToolError> {
let tool_name = self.0.name();
let args = <T as TypedTool>::Args::deserialize(params)
.map_err(|e| format!("invalid args for '{tool_name}': {e}"))?;
self.0.execute_typed(tool_call_id, args, signal, ctx).await
}
}
pub fn wrap_typed<T: TypedTool + AgentTool + ?Sized>(tool: Arc<T>) -> Arc<dyn AgentTool> {
Arc::new(TypedToolAdapter(tool))
}
#[cfg(test)]
mod tests {
use super::*;
use schemars::JsonSchema;
use serde::Deserialize;
#[derive(Debug, PartialEq, Deserialize, JsonSchema)]
struct EchoArgs {
message: String,
}
struct EchoTool;
#[async_trait]
impl TypedTool for EchoTool {
type Args = EchoArgs;
async fn execute_typed(
&self,
_id: &str,
args: Self::Args,
_signal: Option<oneshot::Receiver<()>>,
_ctx: &ToolContext,
) -> Result<AgentToolResult, ToolError> {
Ok(AgentToolResult::success(args.message))
}
}
#[async_trait]
impl AgentTool for EchoTool {
fn name(&self) -> &str {
"echo"
}
fn label(&self) -> &str {
"echo"
}
fn description(&self) -> &str {
"echo back the message"
}
fn parameters_schema(&self) -> serde_json::Value {
serde_json::json!({"type": "object", "properties": {"message": {"type": "string"}}, "required": ["message"]})
}
async fn execute(
&self,
_tool_call_id: &str,
params: serde_json::Value,
_signal: Option<tokio::sync::oneshot::Receiver<()>>,
_ctx: &ToolContext,
) -> Result<AgentToolResult, ToolError> {
let msg = params.get("message").and_then(|v| v.as_str()).unwrap_or("");
Ok(AgentToolResult::success(msg.to_string()))
}
}
#[tokio::test]
async fn wrap_typed_preserves_name_and_label() {
let arc = wrap_typed::<EchoTool>(Arc::new(EchoTool));
assert_eq!(arc.name(), "echo");
assert_eq!(arc.label(), "echo");
assert_eq!(arc.description(), "echo back the message");
assert!(!arc.essential());
}
#[tokio::test]
async fn parameters_schema_contains_args_field() {
let arc = wrap_typed::<EchoTool>(Arc::new(EchoTool));
let schema = arc.parameters_schema();
let obj = schema.as_object().expect("schema should be an object");
assert!(
obj.get("properties").is_some(),
"schema missing properties: {schema}"
);
}
#[tokio::test]
async fn execute_deserializes_value_into_args() {
let arc = wrap_typed::<EchoTool>(Arc::new(EchoTool));
let ctx = ToolContext {
workspace_dir: std::path::PathBuf::from("."),
..Default::default()
};
let result = arc
.execute(
"call_1",
serde_json::json!({"message": "hello"}),
None,
&ctx,
)
.await
.expect("execute should succeed");
assert!(result.success);
assert_eq!(result.output, "hello");
}
#[tokio::test]
async fn execute_returns_error_on_invalid_args() {
let arc = wrap_typed::<EchoTool>(Arc::new(EchoTool));
let ctx = ToolContext {
workspace_dir: std::path::PathBuf::from("."),
..Default::default()
};
let result = arc
.execute("call_1", serde_json::json!({}), None, &ctx)
.await;
let err = result.expect_err("should fail with missing field");
assert!(
err.contains("invalid args for 'echo'"),
"unexpected error: {err}"
);
}
}