Skip to main content

echo_macros/
lib.rs

1//! # echo-macros
2//!
3//! Procedural macros for the [echo-agent](https://crates.io/crates/echo_agent) framework.
4//!
5//! ## Macros
6//!
7//! | Macro | Generates | Description |
8//! |-------|-----------|-------------|
9//! | [`#[tool]`](attr.tool.html) | `Tool` impl | Auto-generates params struct, JSON Schema, and `Tool` trait impl from an async fn |
10//! | [`#[callback]`](attr.callback.html) | `AgentCallback` impl | Generates lifecycle callbacks from an impl block |
11//! | [`#[guard]`](attr.guard.html) | `Guard` impl | Content filtering guard from an async fn |
12//! | [`#[handler]`](attr.handler.html) | `HumanLoopHandler` impl | Human-in-the-loop handler from an impl block |
13//! | [`#[compressor]`](attr.compressor.html) | `ContextCompressor` impl | Context compression strategy from an async fn |
14//! | [`#[permission_policy]`](attr.permission_policy.html) | `PermissionPolicy` impl | Tool permission policy from an async fn |
15//! | [`#[audit_logger]`](attr.audit_logger.html) | `AuditLogger` impl | Audit logging backend from an impl block |
16//! | [`#[derive(Tool)]`](derive.Tool.html) | `Tool` impl | Derive macro for structs — auto-generates params, JSON Schema, and `Tool` trait impl |
17//!
18//! ## Quick Example
19//!
20//! ```rust,ignore
21//! use echo_agent::tool;
22//!
23//! #[tool(name = "add", description = "Add two numbers")]
24//! async fn add(a: f64, b: f64) -> Result<ToolResult> {
25//!     Ok(ToolResult::success(format!("{}", a + b)))
26//! }
27//! ```
28//!
29//! Most users should import these macros via `echo_agent::prelude::*` or
30//! `use echo_agent::{tool, callback, guard, handler};` rather than depending
31//! on `echo_macros` directly.
32
33mod derive_tool;
34
35use proc_macro::TokenStream;
36use proc_macro_crate::{FoundCrate, crate_name};
37use proc_macro2::Span;
38use quote::{format_ident, quote};
39use syn::{
40    DeriveInput, FnArg, ImplItem, ItemFn, ItemImpl, LitStr, Pat, ReturnType, parse_macro_input,
41};
42
43fn echo_agent_crate_path() -> syn::Result<syn::Path> {
44    match crate_name("echo_agent").map_err(|e| syn::Error::new(Span::call_site(), e.to_string()))? {
45        FoundCrate::Itself => Ok(syn::parse_quote!(::echo_agent)),
46        FoundCrate::Name(name) => {
47            let ident = syn::Ident::new(&name, Span::call_site());
48            Ok(syn::parse_quote!(::#ident))
49        }
50    }
51}
52
53// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
54// #[derive(Tool)] — Generate Tool impl from struct definition
55// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
56
57/// Derive macro that generates a complete `Tool` trait implementation from a
58/// struct definition.
59///
60/// # Struct Attributes
61///
62/// - `#[tool(name = "...", description = "...")]` — required
63/// - `#[tool(risk_level = "ReadOnly|Standard|Dangerous")]` — optional
64/// - `#[tool(permissions = [Read, Write, ...])]` — optional
65///
66/// # Field Attributes
67///
68/// - `#[tool_param(skip)]` — internal state, not exposed to LLM
69/// - `#[tool_param(description = "...")]` — parameter description (also reads doc comments)
70///
71/// # Example
72///
73/// ```rust,ignore
74/// use echo_agent::prelude::*;
75///
76/// #[derive(Tool)]
77/// #[tool(name = "read_file", description = "Read file contents")]
78/// struct ReadFileTool {
79///     #[tool_param(skip)]
80///     base_dir: PathBuf,
81///     #[tool_param(description = "File path")]
82///     path: String,
83/// }
84///
85/// impl ToolRunner<ReadFileToolParams> for ReadFileTool {
86///     async fn run(&self, params: ReadFileToolParams) -> Result<ToolResult> {
87///         let content = std::fs::read_to_string(&params.path)?;
88///         Ok(ToolResult::success(content))
89///     }
90/// }
91/// ```
92#[proc_macro_derive(Tool, attributes(tool, tool_param))]
93pub fn derive_tool(input: TokenStream) -> TokenStream {
94    let input = parse_macro_input!(input as DeriveInput);
95    match derive_tool::derive_tool_impl(input) {
96        Ok(ts) => ts.into(),
97        Err(e) => e.to_compile_error().into(),
98    }
99}
100
101// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
102// #[tool] — Generate Tool impl from async fn
103// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
104
105struct ToolAttrs {
106    name: String,
107    description: String,
108    permissions: Vec<syn::Ident>,
109}
110
111impl syn::parse::Parse for ToolAttrs {
112    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
113        let mut name: Option<String> = None;
114        let mut description: Option<String> = None;
115        let mut permissions: Vec<syn::Ident> = Vec::new();
116
117        while !input.is_empty() {
118            let ident: syn::Ident = input.parse()?;
119
120            if ident == "name" {
121                let _eq: syn::Token![=] = input.parse()?;
122                let value: LitStr = input.parse()?;
123                name = Some(value.value());
124            } else if ident == "description" {
125                let _eq: syn::Token![=] = input.parse()?;
126                let value: LitStr = input.parse()?;
127                description = Some(value.value());
128            } else if ident == "permissions" {
129                let _eq: syn::Token![=] = input.parse()?;
130                let content;
131                syn::bracketed!(content in input);
132                while !content.is_empty() {
133                    let perm: syn::Ident = content.parse()?;
134                    permissions.push(perm);
135                    if !content.is_empty() {
136                        let _comma: syn::Token![,] = content.parse()?;
137                    }
138                }
139            } else {
140                return Err(syn::Error::new_spanned(
141                    ident,
142                    "unknown attribute, expected `name`, `description`, or `permissions`",
143                ));
144            }
145
146            if !input.is_empty() {
147                let _comma: syn::Token![,] = input.parse()?;
148            }
149        }
150
151        let name = name.ok_or_else(|| {
152            syn::Error::new(Span::call_site(), "#[tool] requires `name = \"...\"`")
153        })?;
154        let description = description.ok_or_else(|| {
155            syn::Error::new(
156                Span::call_site(),
157                "#[tool] requires `description = \"...\"`",
158            )
159        })?;
160
161        Ok(ToolAttrs {
162            name,
163            description,
164            permissions,
165        })
166    }
167}
168
169/// Generate a `Tool` implementation from an async function, auto-creating the
170/// parameter struct and JSON Schema.
171///
172/// # Attributes
173///
174/// - `name` (required): Tool name
175/// - `description` (required): Tool description
176/// - `permissions` (optional): Permission list, e.g. `[Execute, Network]`
177///
178/// # Example
179///
180/// ```rust,ignore
181/// #[tool(name = "add", description = "Add two numbers")]
182/// async fn add(
183///     /// First number
184///     a: f64,
185///     /// Second number
186///     b: f64,
187/// ) -> Result<ToolResult> {
188///     Ok(ToolResult::success(format!("{}", a + b)))
189/// }
190/// // Generates: AddParams struct + AddTool unit struct + impl Tool
191/// ```
192#[proc_macro_attribute]
193pub fn tool(attr: TokenStream, item: TokenStream) -> TokenStream {
194    let attrs = parse_macro_input!(attr as ToolAttrs);
195    let input_fn = parse_macro_input!(item as ItemFn);
196
197    match tool_impl(attrs, input_fn) {
198        Ok(ts) => ts.into(),
199        Err(e) => e.to_compile_error().into(),
200    }
201}
202
203fn tool_impl(attrs: ToolAttrs, func: ItemFn) -> syn::Result<proc_macro2::TokenStream> {
204    let echo_agent = echo_agent_crate_path()?;
205    let tool_name = &attrs.name;
206    let tool_desc = &attrs.description;
207    let fn_name = &func.sig.ident;
208    let fn_name_str = fn_name.to_string();
209    let struct_name = format_ident!("{}Tool", to_pascal_case(&fn_name_str));
210    let params_name = format_ident!("{}Params", to_pascal_case(&fn_name_str));
211
212    if let ReturnType::Default = &func.sig.output {
213        return Err(syn::Error::new_spanned(
214            &func.sig,
215            "#[tool] function must have a return type (e.g., Result<ToolResult>)",
216        ));
217    }
218
219    let (param_fields, param_names) = extract_fn_params(&func)?;
220    let body = &func.block;
221
222    let permissions_override = if attrs.permissions.is_empty() {
223        quote! {}
224    } else {
225        let perms = attrs.permissions.iter().map(|p| {
226            quote! { #echo_agent::tools::permission::ToolPermission::#p }
227        });
228        quote! {
229            fn permissions(&self) -> Vec<#echo_agent::tools::permission::ToolPermission> {
230                vec![#(#perms),*]
231            }
232        }
233    };
234
235    let expanded = quote! {
236        #[derive(::serde::Deserialize, ::schemars::JsonSchema)]
237        pub struct #params_name {
238            #(#param_fields),*
239        }
240
241        pub struct #struct_name;
242
243        impl #echo_agent::tools::Tool for #struct_name {
244            fn name(&self) -> &str { #tool_name }
245            fn description(&self) -> &str { #tool_desc }
246
247            fn parameters(&self) -> ::serde_json::Value {
248                let schema = ::schemars::schema_for!(#params_name);
249                ::serde_json::to_value(schema).unwrap_or_default()
250            }
251
252            #permissions_override
253
254            fn validate_parameters<'a>(&'a self, params: &'a #echo_agent::tools::ToolParameters) -> ::futures::future::BoxFuture<'a, #echo_agent::error::Result<()>> {
255                Box::pin(async move {
256                    #struct_name::deserialize_params(params)?;
257                    Ok(())
258                })
259            }
260
261            fn execute<'a>(&'a self, parameters: #echo_agent::tools::ToolParameters) -> ::futures::future::BoxFuture<'a, #echo_agent::error::Result<#echo_agent::tools::ToolResult>> {
262                Box::pin(async move {
263                    let params = #struct_name::deserialize_params(&parameters)?;
264                    let #params_name { #(#param_names),* } = params;
265                    #body
266                })
267            }
268        }
269
270        impl #struct_name {
271            /// Deserialize and validate tool parameters, returning a typed struct
272            /// or a [`ToolError::InvalidParameter`] with extracted field name.
273            fn deserialize_params(params: &#echo_agent::tools::ToolParameters) -> #echo_agent::error::Result<#params_name> {
274                let value = ::serde_json::Value::Object(params.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
275                ::serde_json::from_value(value).map_err(|e| {
276                    let msg = e.to_string();
277                    // Extract field name from common serde error patterns:
278                    // "missing field `name`" -> "name"
279                    // "invalid type: ... at `field`" -> "field"
280                    let field = msg
281                        .strip_prefix("missing field `")
282                        .and_then(|s| s.strip_suffix('`'))
283                        .or_else(|| {
284                            msg.split("at `").nth(1).and_then(|s| s.strip_suffix('`'))
285                        })
286                        .unwrap_or("(deserialization)");
287                    #echo_agent::error::ToolError::InvalidParameter {
288                        name: field.to_string(),
289                        message: msg,
290                    }.into()
291                })
292            }
293        }
294    };
295
296    Ok(expanded)
297}
298
299// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
300// #[callback] — Generate AgentCallback impl from impl block
301// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
302
303/// Generate an `AgentCallback` implementation from an impl block, overriding
304/// only the methods you define.
305///
306/// # Example
307///
308/// ```rust,ignore
309/// struct LogCallback;
310///
311/// #[callback]
312/// impl LogCallback {
313///     async fn on_tool_start(&self, _agent: &str, tool: &str, _args: &Value) {
314///         println!("Tool started: {tool}");
315///     }
316///     async fn on_final_answer(&self, _agent: &str, answer: &str) {
317///         println!("Final answer: {answer}");
318///     }
319/// }
320/// ```
321#[proc_macro_attribute]
322pub fn callback(_attr: TokenStream, item: TokenStream) -> TokenStream {
323    let input = parse_macro_input!(item as ItemImpl);
324    match callback_impl(input) {
325        Ok(ts) => ts.into(),
326        Err(e) => e.to_compile_error().into(),
327    }
328}
329
330fn callback_impl(input: ItemImpl) -> syn::Result<proc_macro2::TokenStream> {
331    let echo_agent = echo_agent_crate_path()?;
332    let self_ty = &input.self_ty;
333    let method_impls = impl_block_to_boxfuture_methods(&input, "()")?;
334
335    Ok(quote! {
336        impl #echo_agent::agent::AgentCallback for #self_ty {
337            #(#method_impls)*
338        }
339    })
340}
341
342// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
343// #[guard] — Generate Guard impl from async fn
344// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
345
346struct NameAttr {
347    name: String,
348}
349
350impl syn::parse::Parse for NameAttr {
351    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
352        let mut name: Option<String> = None;
353        while !input.is_empty() {
354            let ident: syn::Ident = input.parse()?;
355            let _eq: syn::Token![=] = input.parse()?;
356            let value: LitStr = input.parse()?;
357            if ident == "name" {
358                name = Some(value.value());
359            } else {
360                return Err(syn::Error::new_spanned(ident, "expected `name`"));
361            }
362            if !input.is_empty() {
363                let _: syn::Token![,] = input.parse()?;
364            }
365        }
366        let name =
367            name.ok_or_else(|| syn::Error::new(Span::call_site(), "requires `name = \"...\"`"))?;
368        Ok(NameAttr { name })
369    }
370}
371
372/// Generate a `Guard` implementation from an async function.
373///
374/// # Example
375///
376/// ```rust,ignore
377/// #[guard(name = "length-limit")]
378/// async fn check_length(content: &str, direction: GuardDirection) -> Result<GuardResult> {
379///     if content.len() > 10000 {
380///         Ok(GuardResult::Block { reason: "Content too long".into() })
381///     } else {
382///         Ok(GuardResult::Pass)
383///     }
384/// }
385/// // Generates: LengthLimitGuard + impl Guard
386/// ```
387#[proc_macro_attribute]
388pub fn guard(attr: TokenStream, item: TokenStream) -> TokenStream {
389    let attrs = parse_macro_input!(attr as NameAttr);
390    let input_fn = parse_macro_input!(item as ItemFn);
391    match guard_impl(attrs, input_fn) {
392        Ok(ts) => ts.into(),
393        Err(e) => e.to_compile_error().into(),
394    }
395}
396
397fn guard_impl(attrs: NameAttr, func: ItemFn) -> syn::Result<proc_macro2::TokenStream> {
398    let echo_agent = echo_agent_crate_path()?;
399    let guard_name = &attrs.name;
400    let struct_name = format_ident!("{}Guard", to_pascal_case(&guard_name.replace('-', "_")));
401    require_return_type(&func)?;
402    let body = &func.block;
403
404    Ok(quote! {
405        pub struct #struct_name;
406
407        impl #echo_agent::guard::Guard for #struct_name {
408            fn name(&self) -> &str { #guard_name }
409
410            fn check<'a>(
411                &'a self,
412                content: &'a str,
413                direction: #echo_agent::guard::GuardDirection,
414            ) -> ::futures::future::BoxFuture<'a, #echo_agent::error::Result<#echo_agent::guard::GuardResult>> {
415                Box::pin(async move #body)
416            }
417        }
418    })
419}
420
421// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
422// #[handler] — Generate HumanLoopHandler impl from impl block
423// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
424
425/// Generate a `HumanLoopHandler` implementation from an impl block.
426///
427/// # Overridable Methods
428///
429/// - `on_approval(&self, tool_name: &str, args: &Value, prompt: &str) -> ApprovalDecision`
430/// - `on_input(&self, prompt: &str) -> String`
431///
432/// # Example
433///
434/// ```rust,ignore
435/// struct AutoApproveHandler;
436///
437/// #[handler]
438/// impl AutoApproveHandler {
439///     async fn on_approval(&self, _tool: &str, _args: &Value, _prompt: &str) -> ApprovalDecision {
440///         ApprovalDecision::Approved
441///     }
442///     async fn on_input(&self, prompt: &str) -> String {
443///         println!("Agent asks: {prompt}");
444///         "default response".to_string()
445///     }
446/// }
447/// ```
448#[proc_macro_attribute]
449pub fn handler(_attr: TokenStream, item: TokenStream) -> TokenStream {
450    let input = parse_macro_input!(item as ItemImpl);
451    match handler_impl(input) {
452        Ok(ts) => ts.into(),
453        Err(e) => e.to_compile_error().into(),
454    }
455}
456
457fn handler_impl(input: ItemImpl) -> syn::Result<proc_macro2::TokenStream> {
458    let echo_agent = echo_agent_crate_path()?;
459    let self_ty = &input.self_ty;
460    let method_impls = extract_boxfuture_methods_with_return(&input)?;
461
462    Ok(quote! {
463        impl #echo_agent::human_loop::HumanLoopHandler for #self_ty {
464            #(#method_impls)*
465        }
466    })
467}
468
469// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
470// #[compressor] — Generate ContextCompressor impl from async fn
471// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
472
473/// Generate a `ContextCompressor` implementation from an async function.
474///
475/// # Example
476///
477/// ```rust,ignore
478/// #[compressor]
479/// async fn keep_recent(input: CompressionInput) -> Result<CompressionOutput> {
480///     let keep = input.messages.len().min(20);
481///     let evicted = input.messages[..input.messages.len() - keep].to_vec();
482///     let messages = input.messages[input.messages.len() - keep..].to_vec();
483///     Ok(CompressionOutput { messages, evicted })
484/// }
485/// // Generates: KeepRecentCompressor + impl ContextCompressor
486/// ```
487#[proc_macro_attribute]
488pub fn compressor(_attr: TokenStream, item: TokenStream) -> TokenStream {
489    let input_fn = parse_macro_input!(item as ItemFn);
490    match compressor_impl(input_fn) {
491        Ok(ts) => ts.into(),
492        Err(e) => e.to_compile_error().into(),
493    }
494}
495
496fn compressor_impl(func: ItemFn) -> syn::Result<proc_macro2::TokenStream> {
497    let echo_agent = echo_agent_crate_path()?;
498    let fn_name = &func.sig.ident;
499    let struct_name = format_ident!("{}Compressor", to_pascal_case(&fn_name.to_string()));
500    require_return_type(&func)?;
501    let body = &func.block;
502
503    Ok(quote! {
504        pub struct #struct_name;
505
506        impl #echo_agent::compression::ContextCompressor for #struct_name {
507            fn compress(
508                &self,
509                input: #echo_agent::compression::CompressionInput,
510            ) -> ::futures::future::BoxFuture<'_, #echo_agent::error::Result<#echo_agent::compression::CompressionOutput>> {
511                Box::pin(async move #body)
512            }
513        }
514    })
515}
516
517// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
518// #[permission_policy] — Generate PermissionPolicy impl from async fn
519// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
520
521/// Generate a `PermissionPolicy` implementation from an async function.
522///
523/// # Example
524///
525/// ```rust,ignore
526/// #[permission_policy]
527/// async fn allow_all(tool_name: &str, permissions: &[ToolPermission]) -> PermissionDecision {
528///     PermissionDecision::Allow
529/// }
530/// // Generates: AllowAllPolicy + impl PermissionPolicy
531/// ```
532#[proc_macro_attribute]
533pub fn permission_policy(_attr: TokenStream, item: TokenStream) -> TokenStream {
534    let input_fn = parse_macro_input!(item as ItemFn);
535    match permission_policy_impl(input_fn) {
536        Ok(ts) => ts.into(),
537        Err(e) => e.to_compile_error().into(),
538    }
539}
540
541fn permission_policy_impl(func: ItemFn) -> syn::Result<proc_macro2::TokenStream> {
542    let echo_agent = echo_agent_crate_path()?;
543    let fn_name = &func.sig.ident;
544    let struct_name = format_ident!("{}Policy", to_pascal_case(&fn_name.to_string()));
545    require_return_type(&func)?;
546    let body = &func.block;
547
548    Ok(quote! {
549        pub struct #struct_name;
550
551        impl #echo_agent::tools::permission::PermissionPolicy for #struct_name {
552            fn check<'a>(
553                &'a self,
554                tool_name: &'a str,
555                permissions: &'a [#echo_agent::tools::permission::ToolPermission],
556            ) -> ::futures::future::BoxFuture<'a, #echo_agent::tools::permission::PermissionDecision> {
557                Box::pin(async move #body)
558            }
559        }
560    })
561}
562
563// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
564// #[audit_logger] — Generate AuditLogger impl from impl block
565// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
566
567/// Generate an `AuditLogger` implementation from an impl block.
568///
569/// # Overridable Methods
570///
571/// - `log(&self, event: AuditEvent) -> Result<()>`
572/// - `query(&self, filter: AuditFilter) -> Result<Vec<AuditEvent>>`
573///
574/// # Example
575///
576/// ```rust,ignore
577/// struct PrintLogger;
578///
579/// #[audit_logger]
580/// impl PrintLogger {
581///     async fn log(&self, event: AuditEvent) -> Result<()> {
582///         println!("[audit] {:?}", event.event_type);
583///         Ok(())
584///     }
585///     async fn query(&self, _filter: AuditFilter) -> Result<Vec<AuditEvent>> {
586///         Ok(vec![])
587///     }
588/// }
589/// ```
590#[proc_macro_attribute]
591pub fn audit_logger(_attr: TokenStream, item: TokenStream) -> TokenStream {
592    let input = parse_macro_input!(item as ItemImpl);
593    match audit_logger_impl(input) {
594        Ok(ts) => ts.into(),
595        Err(e) => e.to_compile_error().into(),
596    }
597}
598
599fn audit_logger_impl(input: ItemImpl) -> syn::Result<proc_macro2::TokenStream> {
600    let echo_agent = echo_agent_crate_path()?;
601    let self_ty = &input.self_ty;
602    let method_impls = extract_boxfuture_methods_with_return(&input)?;
603
604    Ok(quote! {
605        impl #echo_agent::audit::AuditLogger for #self_ty {
606            #(#method_impls)*
607        }
608    })
609}
610
611// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
612// Shared helper functions
613// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
614
615fn extract_fn_params(
616    func: &ItemFn,
617) -> syn::Result<(Vec<proc_macro2::TokenStream>, Vec<syn::Ident>)> {
618    let mut param_fields = Vec::new();
619    let mut param_names = Vec::new();
620
621    for arg in func.sig.inputs.iter() {
622        if let FnArg::Typed(pat_type) = arg {
623            let pat = &pat_type.pat;
624            let ty = &pat_type.ty;
625
626            let field_name = if let Pat::Ident(pi) = pat.as_ref() {
627                pi.ident.clone()
628            } else {
629                return Err(syn::Error::new_spanned(pat, "expected identifier pattern"));
630            };
631
632            let doc_str = extract_doc_comments(&pat_type.attrs);
633            let schemars_attr = if let Some(doc) = &doc_str {
634                quote! { #[schemars(description = #doc)] }
635            } else {
636                quote! {}
637            };
638
639            param_fields.push(quote! {
640                #schemars_attr
641                pub #field_name: #ty
642            });
643            param_names.push(field_name);
644        }
645    }
646
647    Ok((param_fields, param_names))
648}
649
650/// For callback-style traits: returns `BoxFuture<'a, ()>`.
651fn impl_block_to_boxfuture_methods(
652    input: &ItemImpl,
653    _default_return: &str,
654) -> syn::Result<Vec<proc_macro2::TokenStream>> {
655    let mut methods = Vec::new();
656
657    for item in &input.items {
658        if let ImplItem::Fn(method) = item {
659            let name_ident = &method.sig.ident;
660            let body = &method.block;
661            let lifetime_params = lifetimed_params(&method.sig.inputs);
662
663            methods.push(quote! {
664                fn #name_ident<'a>(#(#lifetime_params),*) -> ::futures::future::BoxFuture<'a, ()> {
665                    Box::pin(async move #body)
666                }
667            });
668        }
669    }
670
671    Ok(methods)
672}
673
674/// For traits where user methods have explicit return types (HumanLoopHandler, AuditLogger).
675fn extract_boxfuture_methods_with_return(
676    input: &ItemImpl,
677) -> syn::Result<Vec<proc_macro2::TokenStream>> {
678    let mut methods = Vec::new();
679
680    for item in &input.items {
681        if let ImplItem::Fn(method) = item {
682            let name_ident = &method.sig.ident;
683            let body = &method.block;
684            let lifetime_params = lifetimed_params(&method.sig.inputs);
685
686            let ret_ty = match &method.sig.output {
687                ReturnType::Default => quote! { () },
688                ReturnType::Type(_, ty) => quote! { #ty },
689            };
690
691            methods.push(quote! {
692                fn #name_ident<'a>(#(#lifetime_params),*) -> ::futures::future::BoxFuture<'a, #ret_ty> {
693                    Box::pin(async move #body)
694                }
695            });
696        }
697    }
698
699    Ok(methods)
700}
701
702/// Rewrites each `FnArg` so `&self` → `&'a self` and `&T` → `&'a T`.
703fn lifetimed_params(
704    inputs: &syn::punctuated::Punctuated<FnArg, syn::token::Comma>,
705) -> Vec<proc_macro2::TokenStream> {
706    inputs
707        .iter()
708        .map(|arg| match arg {
709            FnArg::Receiver(_) => quote! { &'a self },
710            FnArg::Typed(pat_type) => {
711                let pat = &pat_type.pat;
712                let ty = add_lifetime_a(&pat_type.ty);
713                quote! { #pat: #ty }
714            }
715        })
716        .collect()
717}
718
719/// Adds `'a` to top-level references that lack an explicit lifetime.
720/// `&T` → `&'a T`, `&mut T` → `&'a mut T`.
721/// References that already have a named lifetime (e.g., `&'b T`) are left unchanged.
722/// Non-reference types pass through unchanged.
723fn add_lifetime_a(ty: &syn::Type) -> proc_macro2::TokenStream {
724    match ty {
725        syn::Type::Reference(r) => {
726            let elem = &r.elem;
727            // Preserve existing explicit lifetimes; only add 'a if none is present
728            let lifetime = r
729                .lifetime
730                .as_ref()
731                .map(|lt| quote! { #lt })
732                .unwrap_or(quote! { 'a });
733            if r.mutability.is_some() {
734                quote! { &#lifetime mut #elem }
735            } else {
736                quote! { &#lifetime #elem }
737            }
738        }
739        other => quote! { #other },
740    }
741}
742
743fn require_return_type(func: &ItemFn) -> syn::Result<()> {
744    if let ReturnType::Default = &func.sig.output {
745        return Err(syn::Error::new_spanned(
746            &func.sig,
747            "function must have an explicit return type",
748        ));
749    }
750    Ok(())
751}
752
753pub(crate) fn extract_doc_comments(attrs: &[syn::Attribute]) -> Option<String> {
754    let docs: Vec<String> = attrs
755        .iter()
756        .filter_map(|attr| {
757            if !attr.path().is_ident("doc") {
758                return None;
759            }
760            if let syn::Meta::NameValue(nv) = &attr.meta
761                && let syn::Expr::Lit(expr_lit) = &nv.value
762                && let syn::Lit::Str(s) = &expr_lit.lit
763            {
764                return Some(s.value().trim().to_string());
765            }
766            None
767        })
768        .collect();
769
770    if docs.is_empty() {
771        None
772    } else {
773        Some(docs.join(" "))
774    }
775}
776
777fn to_pascal_case(s: &str) -> String {
778    s.split('_')
779        .map(|word| {
780            let mut chars = word.chars();
781            match chars.next() {
782                None => String::new(),
783                Some(c) => {
784                    let upper: String = c.to_uppercase().collect();
785                    upper + &chars.collect::<String>()
786                }
787            }
788        })
789        .collect()
790}