agent_sdk_macros/lib.rs
1//! Ergonomic procedural and declarative macros for the Agent SDK.
2//!
3//! This crate is the **macro / ergonomics layer** (Phase 13·E) sitting on top
4//! of the hand-written tool traits in `agent-sdk-tools` (the untyped
5//! `Tool`/`SimpleTool` baseline and the typed `TypedTool` API from 13·B). It
6//! turns today's boilerplate — hand-written `name`/`description`/`input_schema`
7//! methods, `ToolName` enums, and inline tool scaffolding — into derives and a
8//! declarative macro, **without** changing or replacing the traits themselves.
9//! Everything here is additive sugar: hand-written tools keep compiling.
10//!
11//! You almost never depend on this crate directly. The macros are re-exported
12//! from the `agent-sdk` façade (and its prelude), so the generated code refers
13//! to `::agent_sdk::…` paths. The examples below are written as a user would
14//! write them, against `agent_sdk`.
15//!
16//! # `#[derive(Tool)]`
17//!
18//! Derives a `SimpleTool` impl (the untyped, `Value`-in baseline) from
19//! struct-level `#[tool(...)]` attributes. The derive wires up `name` /
20//! `display_name` / `description` / `input_schema` / `tier`; you supply the
21//! behaviour by implementing `agent_sdk::ToolLogic` (with `type Input = Value`).
22//!
23//! ```ignore
24//! use agent_sdk::{Tool, ToolContext, ToolLogic, ToolResult};
25//! use serde_json::{json, Value};
26//!
27//! #[derive(Tool)]
28//! #[tool(
29//! name = "get_weather",
30//! description = "Get the current weather for a city",
31//! schema = json!({
32//! "type": "object",
33//! "properties": { "city": { "type": "string" } },
34//! "required": ["city"]
35//! }),
36//! )]
37//! struct WeatherTool;
38//!
39//! impl ToolLogic<()> for WeatherTool {
40//! type Input = Value;
41//! async fn execute(&self, _ctx: &ToolContext<()>, input: Value) -> anyhow::Result<ToolResult> {
42//! let city = input["city"].as_str().unwrap_or("Unknown");
43//! Ok(ToolResult::success(format!("Weather in {city}: Sunny")))
44//! }
45//! }
46//! ```
47//!
48//! # `#[derive(TypedTool)]`
49//!
50//! Derives a `TypedTool` impl with a typed `Input`. The `input_schema` can
51//! either be hand-provided (`schema = <expr>`, matching 13·B's baseline) or
52//! auto-derived from the `Input` type via `schemars`
53//! (`schema = "derive"`, requires the `schema-derive` feature). Your
54//! `ToolLogic::execute` receives the already-validated, typed `Input`.
55//!
56//! ```ignore
57//! use agent_sdk::{TypedTool, ToolContext, ToolLogic, ToolResult};
58//! use serde::{Deserialize, Serialize};
59//!
60//! #[derive(Serialize, Deserialize, schemars::JsonSchema)]
61//! struct WeatherArgs { city: String }
62//!
63//! #[derive(TypedTool)]
64//! #[tool(name = "get_weather", description = "Weather", input = WeatherArgs, schema = "derive")]
65//! struct WeatherTool;
66//!
67//! impl ToolLogic<()> for WeatherTool {
68//! type Input = WeatherArgs;
69//! async fn execute(&self, _ctx: &ToolContext<()>, input: WeatherArgs) -> anyhow::Result<ToolResult> {
70//! Ok(ToolResult::success(format!("Weather in {}: Sunny", input.city)))
71//! }
72//! }
73//! ```
74//!
75//! # `#[derive(ToolName)]`
76//!
77//! Removes the `ToolName` marker-impl boilerplate: it derives the `Serialize`
78//! / `Deserialize` / `ToolName` trio (with `snake_case` renaming by default)
79//! for a plain enum, so a strongly-typed tool-name enum is a single derive.
80//!
81//! # `tool!`
82//!
83//! A declarative macro for quick *inline* tools — handy in examples, tests, and
84//! one-off scripts where defining a named struct + impl is overkill.
85
86mod attr;
87mod tool_derive;
88mod tool_name_derive;
89mod typed_tool_derive;
90
91use proc_macro::TokenStream;
92
93/// Derive an `agent_sdk::SimpleTool` impl from struct-level `#[tool(...)]`
94/// attributes.
95///
96/// See the [crate-level docs](crate) for the full attribute list and an
97/// example. Required: `name` and `description`. Optional: `display_name`,
98/// `tier`, `schema` (a `serde_json::Value` expression; defaults to
99/// `{"type":"object"}`), and `context` (the `Ctx` type; defaults to `()`).
100///
101/// The struct must implement `agent_sdk::ToolLogic<Ctx>` with
102/// `type Input = serde_json::Value` to supply the `execute` body.
103#[proc_macro_derive(Tool, attributes(tool))]
104pub fn derive_tool(input: TokenStream) -> TokenStream {
105 tool_derive::expand(input.into())
106 .unwrap_or_else(syn::Error::into_compile_error)
107 .into()
108}
109
110/// Derive an `agent_sdk::TypedTool` impl (typed `Input` + runtime validation)
111/// from struct-level `#[tool(...)]` attributes.
112///
113/// See the [crate-level docs](crate). Required: `name`, `description`, and
114/// `input` (the typed `Input` type). The `schema` attribute is either a
115/// `serde_json::Value` expression (hand-provided, the 13·B baseline) or the
116/// literal string `"derive"` to auto-generate the schema from `Input` via
117/// `schemars` (requires this crate's `schema-derive` feature).
118///
119/// The struct must implement `agent_sdk::ToolLogic<Ctx>` with
120/// `type Input = <the typed input>` to supply the `execute` body.
121#[proc_macro_derive(TypedTool, attributes(tool))]
122pub fn derive_typed_tool(input: TokenStream) -> TokenStream {
123 typed_tool_derive::expand(input.into())
124 .unwrap_or_else(syn::Error::into_compile_error)
125 .into()
126}
127
128/// Derive `agent_sdk::ToolName` (plus the `Serialize` / `Deserialize` it
129/// requires) for an enum, removing the marker-impl boilerplate.
130///
131/// By default variants serialize as `snake_case` (matching the SDK's built-in
132/// `PrimitiveToolName`). Override with `#[tool_name(rename_all = "...")]`.
133#[proc_macro_derive(ToolName, attributes(tool_name))]
134pub fn derive_tool_name(input: TokenStream) -> TokenStream {
135 tool_name_derive::expand(input.into())
136 .unwrap_or_else(syn::Error::into_compile_error)
137 .into()
138}