Skip to main content

aquaregia_macros/
lib.rs

1//! Procedural macros for the Aquaregia crate.
2//!
3//! This crate provides the `#[tool]` procedural macro for concise tool definitions
4//! in Aquaregia agents.
5//!
6//! ## The `#[tool]` Macro
7//!
8//! The `#[tool]` macro generates a function that returns a [`aquaregia::Tool`] with
9//! automatic JSON Schema derivation and typed argument handling.
10//!
11//! ### Usage
12//!
13//! ```rust,no_run
14//! use aquaregia::tool;
15//! use serde_json::{Value, json};
16//!
17//! /// Get weather information by city
18//! #[tool(description = "Get weather by city")]
19//! async fn get_weather(city: String) -> Result<Value, String> {
20//!     Ok(json!({ "city": city, "temp_c": 23, "condition": "sunny" }))
21//! }
22//!
23//! // The macro generates:
24//! // - A struct `get_weather_args` with Deserialize and JsonSchema derives
25//! // - A function `get_weather()` that returns a `Tool`
26//! // - Automatic schema validation for arguments
27//! ```
28//!
29//! ### Macro Requirements
30//!
31//! - Must be an `async fn`
32//! - Must return `Result<Value, String>` or similar error type
33//! - Parameters must be simple identifiers with types (no patterns)
34//! - No generic parameters or where clauses (currently)
35//! - No `self` receivers (must be free functions)
36//!
37//! ### Generated Code
38//!
39//! For a function like:
40//!
41//! ```rust,ignore
42//! #[tool(description = "Example tool")]
43//! async fn example(x: String, y: i32) -> Result<Value, String> {
44//!     // body
45//! }
46//! ```
47//!
48//! The macro generates:
49//!
50//! ```rust,ignore
51//! #[derive(Deserialize, JsonSchema)]
52//! struct __AquaregiaToolArgs_example {
53//!     x: String,
54//!     y: i32,
55//! }
56//!
57//! fn example() -> Tool {
58//!     tool("example")
59//!         .description("Example tool")
60//!         .execute(|args: __AquaregiaToolArgs_example| async move {
61//!             __aquaregia_tool_handler_example(args.x, args.y)
62//!                 .await
63//!                 .map_err(|err| ToolExecError::Execution(err.to_string()))
64//!         })
65//! }
66//!
67//! async fn __aquaregia_tool_handler_example(x: String, y: i32) -> Result<Value, String> {
68//!     // original body
69//! }
70//! ```
71
72use proc_macro::TokenStream;
73use quote::{format_ident, quote};
74use syn::parse::Parser;
75use syn::spanned::Spanned;
76use syn::{
77    Expr, FnArg, ItemFn, Lit, LitStr, Meta, Pat, Token, punctuated::Punctuated, parse_macro_input,
78};
79
80#[proc_macro_attribute]
81pub fn tool(attr: TokenStream, item: TokenStream) -> TokenStream {
82    let metas = match Punctuated::<Meta, Token![,]>::parse_terminated.parse(attr) {
83        Ok(metas) => metas,
84        Err(err) => return err.to_compile_error().into(),
85    };
86
87    let mut description: Option<LitStr> = None;
88    for meta in metas {
89        match meta {
90            Meta::NameValue(name_value) if name_value.path.is_ident("description") => {
91                if description.is_some() {
92                    return syn::Error::new(
93                        name_value.span(),
94                        "duplicate `description` argument in #[tool(...)]",
95                    )
96                    .to_compile_error()
97                    .into();
98                }
99                match name_value.value {
100                    Expr::Lit(expr_lit) => match expr_lit.lit {
101                        Lit::Str(lit) => description = Some(lit),
102                        _ => {
103                            return syn::Error::new(
104                                expr_lit.span(),
105                                "`description` must be a string literal",
106                            )
107                            .to_compile_error()
108                            .into();
109                        }
110                    },
111                    _ => {
112                        return syn::Error::new(
113                            name_value.value.span(),
114                            "`description` must be a string literal",
115                        )
116                        .to_compile_error()
117                        .into();
118                    }
119                }
120            }
121            other => {
122                return syn::Error::new(
123                    other.span(),
124                    "unsupported #[tool(...)] argument; expected `description = \"...\"`",
125                )
126                .to_compile_error()
127                .into();
128            }
129        }
130    }
131
132    let input = parse_macro_input!(item as ItemFn);
133    if input.sig.asyncness.is_none() {
134        return syn::Error::new(
135            input.sig.fn_token.span(),
136            "#[tool] requires an `async fn` handler",
137        )
138        .to_compile_error()
139        .into();
140    }
141    if !input.sig.generics.params.is_empty() || input.sig.generics.where_clause.is_some() {
142        return syn::Error::new(
143            input.sig.generics.span(),
144            "#[tool] does not support generic parameters yet",
145        )
146        .to_compile_error()
147        .into();
148    }
149
150    let vis = input.vis;
151    let attrs = input.attrs;
152    let fn_name = input.sig.ident;
153    let output = input.sig.output;
154    let body = input.block;
155
156    let mut arg_idents = Vec::new();
157    let mut arg_tys = Vec::new();
158    for arg in input.sig.inputs {
159        match arg {
160            FnArg::Receiver(receiver) => {
161                return syn::Error::new(
162                    receiver.span(),
163                    "#[tool] does not support methods with `self`",
164                )
165                .to_compile_error()
166                .into();
167            }
168            FnArg::Typed(pat_type) => {
169                let ident = match *pat_type.pat {
170                    Pat::Ident(pat_ident)
171                        if pat_ident.by_ref.is_none()
172                            && pat_ident.mutability.is_none()
173                            && pat_ident.subpat.is_none() =>
174                    {
175                        pat_ident.ident
176                    }
177                    other => {
178                        return syn::Error::new(
179                            other.span(),
180                            "#[tool] parameters must be simple identifiers, e.g. `city: String`",
181                        )
182                        .to_compile_error()
183                        .into();
184                    }
185                };
186                arg_idents.push(ident);
187                arg_tys.push(*pat_type.ty);
188            }
189        }
190    }
191
192    let description_lit =
193        description.unwrap_or_else(|| LitStr::new("", proc_macro2::Span::call_site()));
194    let args_ident = format_ident!("__AquaregiaToolArgs_{}", fn_name);
195    let handler_ident = format_ident!("__aquaregia_tool_handler_{}", fn_name);
196    let arg_extracts = arg_idents.iter().map(|ident| quote! { args.#ident });
197
198    quote! {
199        #(#attrs)*
200        #vis fn #fn_name() -> ::aquaregia::Tool {
201            #[allow(non_camel_case_types)]
202            #[derive(::aquaregia::__aquaregia_serde::Deserialize, ::aquaregia::__aquaregia_schemars::JsonSchema)]
203            struct #args_ident {
204                #( #arg_idents: #arg_tys, )*
205            }
206
207            ::aquaregia::tool(stringify!(#fn_name))
208                .description(#description_lit)
209                .execute(|args: #args_ident| async move {
210                    #handler_ident( #( #arg_extracts ),* )
211                        .await
212                        .map_err(|err| ::aquaregia::ToolExecError::Execution(err.to_string()))
213                })
214        }
215
216        async fn #handler_ident( #( #arg_idents: #arg_tys ),* ) #output {
217            #body
218        }
219    }
220    .into()
221}