pub use registry::{MissingTools, RegistryError, ToolRegistry};
pub use shared_state::SharedState;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolSchema {
pub name: String,
pub description: String,
pub parameters: serde_json::Value,
}
#[async_trait::async_trait]
pub trait Tool: Send + Sync {
fn schema(&self) -> ToolSchema;
async fn call(
&self,
arguments: serde_json::Value,
state: &SharedState,
) -> Result<String, ToolError>;
fn protected_output(&self) -> bool {
false
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ToolError {
#[error("invalid arguments: {0}")]
InvalidArguments(String),
#[error("execution failed: {0}")]
Execution(String),
}
impl From<serde_json::Error> for ToolError {
fn from(err: serde_json::Error) -> Self {
ToolError::InvalidArguments(err.to_string())
}
}
mod registry;
mod shared_state;
#[cfg(test)]
mod macro_tests {
use super::{SharedState, ToolError, ToolRegistry};
use schemars::JsonSchema;
use serde::Deserialize;
#[derive(Debug, Deserialize, JsonSchema)]
struct CalcArgs {
#[schemars(description = "The math expression to evaluate, e.g. \"1 + 2 * 3\"")]
expression: String,
}
#[molo::tool(description = "Evaluate a math expression")]
async fn calculator(args: CalcArgs) -> Result<String, ToolError> {
Ok(format!("calc:{}", args.expression))
}
#[molo::tool(description = "Say hello")]
async fn hello(name: String) -> Result<String, ToolError> {
Ok(format!("hello,{name}"))
}
#[molo::tool(description = "Return the current Unix timestamp")]
async fn now() -> Result<String, ToolError> {
Ok("now".into())
}
#[molo::tool(description = "Count calls, returning the current count")]
async fn counter(state: &SharedState) -> Result<String, ToolError> {
state.with_mut::<usize>(|n| *n += 1);
Ok(format!("count={}", state.get::<usize>().unwrap_or(0)))
}
#[molo::tool(description = "Record an action, returning the current count")]
async fn record(action: String, state: &SharedState) -> Result<String, ToolError> {
state.with_mut::<usize>(|n| *n += 1);
Ok(format!(
"recorded {action}, count={}",
state.get::<usize>().unwrap_or(0)
))
}
#[molo::tool(name = "renamed", description = "Rename demo")]
async fn original_name() -> Result<String, ToolError> {
Ok("renamed".into())
}
fn registry() -> ToolRegistry {
let mut registry = ToolRegistry::new();
registry
.register(Calculator)
.register(Hello)
.register(Now)
.register(Counter)
.register(Record)
.register(OriginalName);
registry
}
#[test]
fn macro_generates_registrable_tool() {
let registry = registry();
assert_eq!(
registry.names(),
vec!["calculator", "hello", "now", "counter", "record", "renamed"]
);
}
#[test]
fn macro_generates_correct_schema() {
let registry = registry();
let calc_schema = registry
.get("calculator")
.expect("calculator registered")
.schema();
assert_eq!(calc_schema.name, "calculator");
assert_eq!(calc_schema.description, "Evaluate a math expression");
let params = calc_schema.parameters;
assert_eq!(params["type"], "object");
assert_eq!(
params["properties"]["expression"]["description"],
"The math expression to evaluate, e.g. \"1 + 2 * 3\""
);
assert!(params.get("$schema").is_none());
assert!(params.get("title").is_none());
let hello_schema = registry.get("hello").expect("hello registered").schema();
assert_eq!(
hello_schema.parameters["properties"]["name"]["type"],
"string"
);
let now_schema = registry.get("now").expect("now registered").schema();
assert_eq!(
now_schema.parameters,
serde_json::json!({ "type": "object", "properties": {} })
);
}
#[tokio::test]
async fn macro_generates_working_call() {
let registry = registry();
let state = SharedState::new();
assert_eq!(
registry
.call("calculator", r#"{"expression":"1+1"}"#, &state)
.await
.unwrap(),
"calc:1+1"
);
assert_eq!(
registry
.call("hello", r#"{"name":"molo"}"#, &state)
.await
.unwrap(),
"hello,molo"
);
assert_eq!(registry.call("now", "{}", &state).await.unwrap(), "now");
let err = registry
.call("calculator", "not-json", &state)
.await
.unwrap_err();
assert!(err.to_string().starts_with("invalid arguments:"));
state.insert(0usize);
assert_eq!(
registry.call("counter", "{}", &state).await.unwrap(),
"count=1"
);
assert_eq!(
registry.call("counter", "{}", &state).await.unwrap(),
"count=2"
);
assert_eq!(
registry
.call("record", r#"{"action":"x"}"#, &state)
.await
.unwrap(),
"recorded x, count=3"
);
}
#[tokio::test]
async fn macro_tool_works_in_agent() {
use crate::agent::Agent;
use crate::{FakeProvider, FakeReply, ToolCall};
let fake = FakeProvider::new([
FakeReply::ToolCalls {
content: "".into(),
calls: vec![ToolCall {
id: "c1".into(),
name: "hello".into(),
arguments: r#"{"name":"molo"}"#.into(),
}],
},
FakeReply::Text("done".into()),
]);
let mut agent = crate::react_agent!(fake, [Hello], "You are an assistant");
assert_eq!(agent.run("hi").await.unwrap(), "done");
}
}