1mod 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#[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
101struct 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#[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(¶meters)?;
264 let #params_name { #(#param_names),* } = params;
265 #body
266 })
267 }
268 }
269
270 impl #struct_name {
271 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 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#[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
342struct 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#[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#[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#[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#[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#[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
611fn 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
650fn 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
674fn 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
702fn 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
719fn add_lifetime_a(ty: &syn::Type) -> proc_macro2::TokenStream {
724 match ty {
725 syn::Type::Reference(r) => {
726 let elem = &r.elem;
727 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}