use agent_sdk::{
SimpleTool, ToolContext, ToolLogic, ToolName, ToolRegistry, ToolResult, ToolTier, TypedTool,
tool,
};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
struct HandWrittenWeather;
impl SimpleTool<()> for HandWrittenWeather {
fn name(&self) -> &'static str {
"get_weather"
}
fn display_name(&self) -> &'static str {
"Weather"
}
fn description(&self) -> &'static str {
"Get the current weather for a city"
}
fn input_schema(&self) -> Value {
json!({
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
})
}
fn tier(&self) -> ToolTier {
ToolTier::Observe
}
async fn execute(&self, _ctx: &ToolContext<()>, input: Value) -> Result<ToolResult> {
let city = input["city"].as_str().unwrap_or("Unknown");
Ok(ToolResult::success(format!("Weather in {city}: Sunny")))
}
}
#[derive(agent_sdk::Tool)]
#[tool(
name = "get_weather",
display_name = "Weather",
description = "Get the current weather for a city",
schema = json!({
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}),
)]
struct DerivedWeather;
impl ToolLogic<()> for DerivedWeather {
type Input = Value;
async fn execute(&self, _ctx: &ToolContext<()>, input: Value) -> Result<ToolResult> {
let city = input["city"].as_str().unwrap_or("Unknown");
Ok(ToolResult::success(format!("Weather in {city}: Sunny")))
}
}
#[tokio::test]
async fn derived_tool_metadata_matches_hand_written() -> Result<()> {
let mut hand = ToolRegistry::<()>::new();
hand.register_simple(HandWrittenWeather);
let mut derived = ToolRegistry::<()>::new();
derived.register_simple(DerivedWeather);
let h = hand.get("get_weather").context("hand tool registered")?;
let d = derived
.get("get_weather")
.context("derived tool registered")?;
assert_eq!(d.name_str(), h.name_str());
assert_eq!(d.display_name(), h.display_name());
assert_eq!(d.description(), h.description());
assert_eq!(d.input_schema(), h.input_schema());
assert_eq!(d.tier(), h.tier());
Ok(())
}
#[tokio::test]
async fn derived_tool_execute_matches_hand_written() -> Result<()> {
let ctx = ToolContext::new(());
let hand = HandWrittenWeather;
let derived = DerivedWeather;
let args = json!({ "city": "Lisbon" });
let h = SimpleTool::execute(&hand, &ctx, args.clone()).await?;
let d = SimpleTool::execute(&derived, &ctx, args).await?;
assert_eq!(d.success, h.success);
assert_eq!(d.output, h.output);
assert_eq!(d.output, "Weather in Lisbon: Sunny");
Ok(())
}
#[tokio::test]
async fn derived_tool_defaults_schema_and_tier() -> Result<()> {
#[derive(agent_sdk::Tool)]
#[tool(name = "ping", description = "Ping")]
struct Ping;
impl ToolLogic<()> for Ping {
type Input = Value;
async fn execute(&self, _ctx: &ToolContext<()>, _input: Value) -> Result<ToolResult> {
Ok(ToolResult::success("pong"))
}
}
let ping = Ping;
assert_eq!(SimpleTool::input_schema(&ping), json!({ "type": "object" }));
assert_eq!(SimpleTool::tier(&ping), ToolTier::Observe);
assert_eq!(SimpleTool::display_name(&ping), "");
Ok(())
}
#[derive(Debug, Serialize, Deserialize)]
struct GreetArgs {
name: String,
greeting: String,
}
struct HandWrittenGreet;
impl TypedTool<()> for HandWrittenGreet {
type Input = GreetArgs;
fn name(&self) -> &'static str {
"greet"
}
fn description(&self) -> &'static str {
"Greet someone by name"
}
fn input_schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"name": { "type": "string" },
"greeting": { "type": "string" }
},
"required": ["name", "greeting"]
})
}
async fn execute(&self, _ctx: &ToolContext<()>, input: GreetArgs) -> Result<ToolResult> {
Ok(ToolResult::success(format!(
"{}, {}!",
input.greeting, input.name
)))
}
}
#[derive(agent_sdk::TypedTool)]
#[tool(
name = "greet",
description = "Greet someone by name",
input = GreetArgs,
schema = json!({
"type": "object",
"properties": {
"name": { "type": "string" },
"greeting": { "type": "string" }
},
"required": ["name", "greeting"]
}),
)]
struct DerivedGreet;
impl ToolLogic<()> for DerivedGreet {
type Input = GreetArgs;
async fn execute(&self, _ctx: &ToolContext<()>, input: GreetArgs) -> Result<ToolResult> {
Ok(ToolResult::success(format!(
"{}, {}!",
input.greeting, input.name
)))
}
}
#[tokio::test]
async fn derived_typed_tool_metadata_matches_hand_written() -> Result<()> {
assert_eq!(
TypedTool::name(&DerivedGreet),
TypedTool::name(&HandWrittenGreet)
);
assert_eq!(
TypedTool::description(&DerivedGreet),
TypedTool::description(&HandWrittenGreet)
);
assert_eq!(
TypedTool::input_schema(&DerivedGreet),
TypedTool::input_schema(&HandWrittenGreet)
);
Ok(())
}
#[tokio::test]
async fn derived_typed_tool_validates_args_through_registry() -> Result<()> {
let mut registry = ToolRegistry::<()>::new();
registry.register_typed(DerivedGreet);
let tool = registry.get("greet").context("typed tool registered")?;
let ctx = ToolContext::new(());
let ok = tool
.execute(&ctx, json!({ "name": "Ada", "greeting": "Hello" }))
.await?;
assert!(ok.success);
assert_eq!(ok.output, "Hello, Ada!");
let bad = tool.execute(&ctx, json!({ "name": "Ada" })).await?;
assert!(!bad.success, "validation failure is an error result");
assert!(
bad.output.contains("Invalid arguments for tool `greet`"),
"must identify the tool: {}",
bad.output
);
assert!(
bad.output.contains("greeting"),
"must surface the missing field: {}",
bad.output
);
Ok(())
}
#[derive(agent_sdk::TypedTool)]
#[tool(name = "echo", description = "Echo any JSON", input = Value)]
struct DerivedEcho;
impl ToolLogic<()> for DerivedEcho {
type Input = Value;
async fn execute(&self, _ctx: &ToolContext<()>, input: Value) -> Result<ToolResult> {
Ok(ToolResult::success(input.to_string()))
}
}
#[tokio::test]
async fn derived_typed_tool_value_input_is_identity_passthrough() -> Result<()> {
let mut registry = ToolRegistry::<()>::new();
registry.register_typed(DerivedEcho);
let tool = registry.get("echo").context("echo registered")?;
let ctx = ToolContext::new(());
let res = tool
.execute(
&ctx,
json!({ "anything": [1, 2, 3], "nested": { "ok": true } }),
)
.await?;
assert!(res.success);
assert_eq!(tool.input_schema(), json!({ "type": "object" }));
Ok(())
}
#[tokio::test]
async fn declarative_tool_macro_round_trips() -> Result<()> {
let weather = tool! {
name: "get_weather",
description: "Get the current weather for a city",
schema: json!({
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"],
}),
|_ctx, input| async move {
let city = input["city"].as_str().unwrap_or("Unknown");
Ok(ToolResult::success(format!("Weather in {city}: Sunny")))
}
};
let mut registry = ToolRegistry::<()>::new();
registry.register_simple(weather);
let tool = registry
.get("get_weather")
.context("inline tool registered")?;
let ctx = ToolContext::new(());
let res = tool.execute(&ctx, json!({ "city": "Porto" })).await?;
assert!(res.success);
assert_eq!(res.output, "Weather in Porto: Sunny");
assert_eq!(tool.input_schema()["required"][0], json!("city"));
Ok(())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
enum HandName {
ReadFile,
WriteFile,
}
impl ToolName for HandName {}
#[derive(Clone, Copy, Debug, PartialEq, Eq, agent_sdk::ToolName)]
enum DerivedName {
ReadFile,
WriteFile,
}
#[test]
fn derived_tool_name_serializes_identically() -> Result<()> {
let hand = serde_json::to_string(&HandName::ReadFile)?;
let derived = serde_json::to_string(&DerivedName::ReadFile)?;
assert_eq!(hand, derived);
assert_eq!(derived, "\"read_file\"");
let parsed: DerivedName = serde_json::from_str("\"write_file\"")?;
assert_eq!(parsed, DerivedName::WriteFile);
Ok(())
}
#[test]
fn derived_tool_name_is_usable_as_tool_name() {
fn assert_tool_name<N: ToolName>() {}
assert_tool_name::<DerivedName>();
}
#[cfg(feature = "macros-schema")]
mod schema_derive {
use super::*;
#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
struct SearchArgs {
query: String,
#[serde(default)]
limit: u32,
}
#[derive(agent_sdk::TypedTool)]
#[tool(
name = "search",
description = "Search the corpus",
input = SearchArgs,
schema = "derive"
)]
struct SearchTool;
impl ToolLogic<()> for SearchTool {
type Input = SearchArgs;
async fn execute(&self, _ctx: &ToolContext<()>, input: SearchArgs) -> Result<ToolResult> {
Ok(ToolResult::success(format!(
"searched {} (limit {})",
input.query, input.limit
)))
}
}
#[tokio::test]
async fn schema_is_derived_from_input_type() -> Result<()> {
let mut registry = ToolRegistry::<()>::new();
registry.register_typed(SearchTool);
let tool = registry.get("search").context("search registered")?;
let schema = tool.input_schema();
assert_eq!(schema["type"], json!("object"));
assert!(
schema["properties"]["query"].is_object(),
"derived schema must describe `query`: {schema}"
);
assert!(
schema["properties"]["limit"].is_object(),
"derived schema must describe `limit`: {schema}"
);
let required = schema["required"]
.as_array()
.context("derived schema must have a `required` array")?;
assert!(
required.iter().any(|v| v == "query"),
"`query` must be required (no serde default): {schema}"
);
let ctx = ToolContext::new(());
let ok = tool.execute(&ctx, json!({ "query": "rust" })).await?;
assert!(ok.success);
assert_eq!(ok.output, "searched rust (limit 0)");
Ok(())
}
}