use std::fmt;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use async_trait::async_trait;
use everruns_core::capabilities::Capability;
use everruns_core::tools::{Tool as CoreTool, ToolExecutionResult};
use serde_json::Value;
pub struct ToolResponse(ToolExecutionResult);
impl ToolResponse {
pub fn json(value: impl Into<Value>) -> Self {
Self(ToolExecutionResult::success(value))
}
pub fn text(text: impl Into<String>) -> Self {
Self(ToolExecutionResult::success(Value::String(text.into())))
}
pub fn error(message: impl Into<String>) -> Self {
Self(ToolExecutionResult::tool_error(message))
}
}
impl fmt::Debug for ToolResponse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("ToolResponse").field(&self.0).finish()
}
}
pub trait IntoToolResult {
fn into_tool_result(self) -> ToolExecutionResult;
}
impl IntoToolResult for Value {
fn into_tool_result(self) -> ToolExecutionResult {
ToolExecutionResult::Success(self)
}
}
impl IntoToolResult for String {
fn into_tool_result(self) -> ToolExecutionResult {
ToolExecutionResult::Success(Value::String(self))
}
}
impl IntoToolResult for ToolResponse {
fn into_tool_result(self) -> ToolExecutionResult {
self.0
}
}
type HandlerFn =
Arc<dyn Fn(Value) -> Pin<Box<dyn Future<Output = ToolExecutionResult> + Send>> + Send + Sync>;
#[derive(Clone)]
pub struct FunctionTool {
name: String,
description: String,
schema: Value,
handler: HandlerFn,
}
impl FunctionTool {
pub fn new<F, Fut, T, E>(
name: impl Into<String>,
description: impl Into<String>,
json_schema: Value,
handler: F,
) -> Self
where
F: Fn(Value) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<T, E>> + Send + 'static,
T: IntoToolResult + 'static,
E: fmt::Display + 'static,
{
let handler: HandlerFn = Arc::new(move |args| {
let fut = handler(args);
Box::pin(async move {
match fut.await {
Ok(value) => value.into_tool_result(),
Err(err) => ToolExecutionResult::tool_error(err.to_string()),
}
})
});
Self {
name: name.into(),
description: description.into(),
schema: json_schema,
handler,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub(crate) fn schema(&self) -> &Value {
&self.schema
}
pub(crate) fn into_capability(self) -> FunctionCapability {
FunctionCapability { tool: self }
}
}
impl fmt::Debug for FunctionTool {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FunctionTool")
.field("name", &self.name)
.field("description", &self.description)
.field("schema", &self.schema)
.finish_non_exhaustive()
}
}
#[async_trait]
impl CoreTool for FunctionTool {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
&self.description
}
fn parameters_schema(&self) -> Value {
self.schema.clone()
}
async fn execute(&self, arguments: Value) -> ToolExecutionResult {
(self.handler)(arguments).await
}
}
pub(crate) struct FunctionCapability {
tool: FunctionTool,
}
impl Capability for FunctionCapability {
fn id(&self) -> &str {
&self.tool.name
}
fn name(&self) -> &str {
&self.tool.name
}
fn description(&self) -> &str {
&self.tool.description
}
fn tools(&self) -> Vec<Box<dyn CoreTool>> {
vec![Box::new(self.tool.clone())]
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum Tool {
Function(FunctionTool),
}
impl Tool {
pub(crate) fn name(&self) -> &str {
match self {
Tool::Function(tool) => &tool.name,
}
}
pub(crate) fn into_function(self) -> FunctionTool {
match self {
Self::Function(tool) => tool,
}
}
}
pub trait IntoTool {
fn into_tool(self) -> Tool;
}
impl IntoTool for Tool {
fn into_tool(self) -> Tool {
self
}
}
impl IntoTool for FunctionTool {
fn into_tool(self) -> Tool {
Tool::Function(self)
}
}
pub(crate) fn validate_tool_name(name: &str) -> Result<(), String> {
if name.is_empty() {
return Err("tool name must not be empty".to_string());
}
if name.len() > 64 {
return Err(format!(
"tool name must be at most 64 characters (got {})",
name.len()
));
}
let mut chars = name.chars();
let first = chars.next().expect("non-empty checked above");
if !(first.is_ascii_alphabetic() || first == '_') {
return Err(format!(
"tool name must start with a letter or underscore (got {first:?})"
));
}
if let Some(bad) = name
.chars()
.find(|c| !(c.is_ascii_alphanumeric() || *c == '_' || *c == '-'))
{
return Err(format!(
"tool name may only contain letters, digits, '_' or '-' (got {bad:?})"
));
}
Ok(())
}
pub(crate) fn validate_tool_schema(schema: &Value) -> Result<(), String> {
let Some(object) = schema.as_object() else {
return Err("JSON schema must be a JSON object".to_string());
};
if let Some(type_value) = object.get("type") {
match type_value.as_str() {
Some("object") => {}
Some(other) => {
return Err(format!(
"JSON schema top-level \"type\" must be \"object\" (got {other:?})"
));
}
None => {
return Err("JSON schema \"type\" must be a string".to_string());
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn obj_schema() -> Value {
json!({ "type": "object", "properties": {}, "additionalProperties": false })
}
#[tokio::test]
async fn handler_receives_args_and_returns_json() {
let tool = FunctionTool::new(
"echo",
"Echo the input back.",
obj_schema(),
|args: Value| async move { Ok::<_, String>(json!({ "echoed": args })) },
);
let result = tool.execute(json!({ "a": 1, "b": "two" })).await;
match result {
ToolExecutionResult::Success(value) => {
assert_eq!(value["echoed"]["a"], json!(1));
assert_eq!(value["echoed"]["b"], json!("two"));
}
other => panic!("expected success, got {other:?}"),
}
}
#[tokio::test]
async fn string_result_maps_to_success() {
let tool = FunctionTool::new(
"shout",
"Return text.",
obj_schema(),
|_args: Value| async move { Ok::<_, String>("hello".to_string()) },
);
let result = tool.execute(json!({})).await;
match result {
ToolExecutionResult::Success(Value::String(s)) => assert_eq!(s, "hello"),
other => panic!("expected string success, got {other:?}"),
}
}
#[tokio::test]
async fn handler_error_maps_to_tool_error_without_panicking() {
let tool = FunctionTool::new(
"boom",
"Always fails.",
obj_schema(),
|_args: Value| async move { Err::<Value, String>("kaboom".to_string()) },
);
let result = tool.execute(json!({})).await;
match result {
ToolExecutionResult::ToolError(message) => assert_eq!(message, "kaboom"),
other => panic!("expected tool error, got {other:?}"),
}
}
#[tokio::test]
async fn tool_response_adapter_can_return_error_from_ok() {
let tool = FunctionTool::new(
"structured",
"Returns a structured tool error from an Ok path.",
obj_schema(),
|_args: Value| async move { Ok::<_, String>(ToolResponse::error("not found")) },
);
match tool.execute(json!({})).await {
ToolExecutionResult::ToolError(message) => assert_eq!(message, "not found"),
other => panic!("expected tool error, got {other:?}"),
}
}
#[tokio::test]
async fn function_tool_executes_concurrently() {
let tool = Arc::new(FunctionTool::new(
"double",
"Double a number.",
obj_schema(),
|args: Value| async move {
let n = args["n"].as_i64().unwrap_or(0);
Ok::<_, String>(json!({ "result": n * 2 }))
},
));
let mut handles = Vec::new();
for n in 0..16i64 {
let tool = tool.clone();
handles.push(tokio::spawn(async move {
let result = tool.execute(json!({ "n": n })).await;
match result {
ToolExecutionResult::Success(value) => value["result"].as_i64().unwrap(),
other => panic!("expected success, got {other:?}"),
}
}));
}
for (n, handle) in handles.into_iter().enumerate() {
assert_eq!(handle.await.unwrap(), n as i64 * 2);
}
}
#[test]
fn into_tool_maps_function_tool() {
let tool = FunctionTool::new("noop", "no-op", obj_schema(), |_: Value| async move {
Ok::<_, String>(json!({}))
});
let Tool::Function(function) = tool.into_tool();
assert_eq!(function.name(), "noop");
}
#[test]
fn validate_tool_name_accepts_and_rejects() {
assert!(validate_tool_name("get_weather").is_ok());
assert!(validate_tool_name("Add-2").is_ok());
assert!(validate_tool_name("_x").is_ok());
assert!(validate_tool_name("").is_err());
assert!(validate_tool_name("2fast").is_err());
assert!(validate_tool_name("has space").is_err());
assert!(validate_tool_name("dot.name").is_err());
assert!(validate_tool_name(&"x".repeat(65)).is_err());
}
#[test]
fn validate_tool_schema_accepts_and_rejects() {
assert!(validate_tool_schema(&json!({ "type": "object" })).is_ok());
assert!(validate_tool_schema(&json!({ "properties": {} })).is_ok());
assert!(validate_tool_schema(&json!({ "type": "array" })).is_err());
assert!(validate_tool_schema(&json!("nope")).is_err());
assert!(validate_tool_schema(&json!(42)).is_err());
}
}