1mod prompt_macro;
12mod resource_macro;
13#[cfg(feature = "md-tmpl")]
14mod response_struct_gen;
15
16use convert_case::{Case, Casing};
17use proc_macro::TokenStream;
18use quote::{format_ident, quote};
19#[cfg(feature = "md-tmpl")]
20use syn::Ident;
21use syn::{ItemFn, LitStr, parse_macro_input};
22
23#[proc_macro_attribute]
131pub fn llm_tool(attr: TokenStream, item: TokenStream) -> TokenStream {
132 let func = parse_macro_input!(item as ItemFn);
133 let tool_attr = if attr.is_empty() {
134 None
135 } else {
136 match syn::parse::<ToolAttr>(attr) {
137 Ok(parsed) => Some(parsed),
138 Err(err) => return err.to_compile_error().into(),
139 }
140 };
141 match tool_impl(&func, tool_attr.as_ref()) {
142 Ok(tokens) => tokens.into(),
143 Err(err) => err.to_compile_error().into(),
144 }
145}
146
147#[proc_macro_attribute]
149pub fn llm_prompt(attr: TokenStream, item: TokenStream) -> TokenStream {
150 let func = parse_macro_input!(item as ItemFn);
151 let tool_attr = if attr.is_empty() {
152 None
153 } else {
154 match syn::parse::<ToolAttr>(attr) {
155 Ok(parsed) => Some(parsed),
156 Err(err) => return err.to_compile_error().into(),
157 }
158 };
159 match prompt_macro::prompt_impl(&func, tool_attr.as_ref()) {
160 Ok(tokens) => tokens.into(),
161 Err(err) => err.to_compile_error().into(),
162 }
163}
164
165#[proc_macro_attribute]
167pub fn llm_resource(attr: TokenStream, item: TokenStream) -> TokenStream {
168 let func = parse_macro_input!(item as ItemFn);
169 let res_attr = match syn::parse::<resource_macro::ResourceAttr>(attr) {
170 Ok(parsed) => parsed,
171 Err(err) => return err.to_compile_error().into(),
172 };
173 match resource_macro::resource_impl(&func, &res_attr) {
174 Ok(tokens) => tokens.into(),
175 Err(err) => err.to_compile_error().into(),
176 }
177}
178
179struct ToolAttr {
191 description_inline: Option<LitStr>,
193 description_file_path: Option<LitStr>,
195 response_file_path: Option<LitStr>,
197 response_inline: Option<LitStr>,
199 #[cfg(feature = "md-tmpl")]
202 inline_params: Vec<(Ident, LitStr)>,
203 #[cfg(feature = "md-tmpl")]
205 env_vars: Vec<(Ident, syn::Lit)>,
206 #[cfg(feature = "md-tmpl")]
208 context_fn: Option<syn::Path>,
209 has_inline_params: bool,
210 has_context_fn: bool,
211 idempotent: bool,
212 effect: MacroToolEffect,
213}
214
215#[derive(Copy, Clone, PartialEq, Eq, Debug)]
216pub(crate) enum MacroToolEffect {
217 ReadOnly,
218 Mutating,
219 Destructive,
220}
221
222fn parse_effect_str(s: &str, span: proc_macro2::Span) -> syn::Result<MacroToolEffect> {
223 match s {
224 "read_only" | "ReadOnly" => Ok(MacroToolEffect::ReadOnly),
225 "mutating" | "Mutating" => Ok(MacroToolEffect::Mutating),
226 "destructive" | "Destructive" => Ok(MacroToolEffect::Destructive),
227 _ => Err(syn::Error::new(
228 span,
229 "expected `read_only`, `mutating`, or `destructive`",
230 )),
231 }
232}
233
234pub(crate) const MACRO_LLM_TOOL: &str = "llm_tool";
235pub(crate) const MACRO_LLM_PROMPT: &str = "llm_prompt";
236pub(crate) const MACRO_LLM_RESOURCE: &str = "llm_resource";
237
238pub(crate) const ATTR_DESCRIPTION: &str = "description";
239pub(crate) const ATTR_DESCRIPTION_FILE: &str = "description_file";
240pub(crate) const ATTR_RESPONSE_FILE: &str = "response_file";
241pub(crate) const ATTR_RESPONSE: &str = "response";
242pub(crate) const ATTR_PARAMS: &str = "params";
243pub(crate) const ATTR_CONTEXT: &str = "context";
244pub(crate) const ATTR_ENV: &str = "env";
245pub(crate) const ATTR_IDEMPOTENT: &str = "idempotent";
246pub(crate) const ATTR_EFFECT: &str = "effect";
247pub(crate) const ATTR_DOC: &str = "doc";
248
249pub(crate) const TYPE_OPTION: &str = "Option";
250pub(crate) const TYPE_TOOL_CONTEXT: &str = "ToolContext";
251pub(crate) const TYPE_STR: &str = "str";
252pub(crate) const TYPE_RESULT: &str = "Result";
253
254#[derive(Copy, Clone, PartialEq, Eq, Debug)]
255pub(crate) enum ToolAttrKey {
256 Description,
257 DescriptionFile,
258 ResponseFile,
259 Response,
260 Params,
261 Env,
262 Context,
263 Idempotent,
264 Effect,
265}
266
267impl ToolAttrKey {
268 pub(crate) const ALL: &'static [Self] = &[
269 Self::Description,
270 Self::DescriptionFile,
271 Self::Response,
272 Self::ResponseFile,
273 Self::Params,
274 Self::Env,
275 Self::Context,
276 Self::Idempotent,
277 Self::Effect,
278 ];
279
280 pub(crate) const fn as_str(self) -> &'static str {
281 match self {
282 Self::Description => ATTR_DESCRIPTION,
283 Self::DescriptionFile => ATTR_DESCRIPTION_FILE,
284 Self::ResponseFile => ATTR_RESPONSE_FILE,
285 Self::Response => ATTR_RESPONSE,
286 Self::Params => ATTR_PARAMS,
287 Self::Env => ATTR_ENV,
288 Self::Context => ATTR_CONTEXT,
289 Self::Idempotent => ATTR_IDEMPOTENT,
290 Self::Effect => ATTR_EFFECT,
291 }
292 }
293
294 pub(crate) fn expected_keys_error(span: proc_macro2::Span) -> syn::Error {
295 let mut parts: Vec<String> = Self::ALL
296 .iter()
297 .map(|k| format!("`{}`", k.as_str()))
298 .collect();
299 let last = parts.pop().expect("ToolAttrKey::ALL is non-empty");
300 let formatted = if parts.is_empty() {
301 last
302 } else {
303 format!("{}, or {last}", parts.join(", "))
304 };
305 syn::Error::new(span, format!("expected {formatted}"))
306 }
307}
308
309impl TryFrom<&syn::Ident> for ToolAttrKey {
310 type Error = syn::Error;
311
312 fn try_from(ident: &syn::Ident) -> Result<Self, Self::Error> {
313 let s = ident.to_string();
314 for &variant in Self::ALL {
315 if s == variant.as_str() {
316 return Ok(variant);
317 }
318 }
319 Err(Self::expected_keys_error(ident.span()))
320 }
321}
322
323#[derive(Default)]
324pub(crate) struct ToolAttrBuilder {
325 description_inline: Option<syn::LitStr>,
326 description_file_path: Option<syn::LitStr>,
327 response_file_path: Option<syn::LitStr>,
328 response_inline: Option<syn::LitStr>,
329 idempotent: Option<syn::LitBool>,
330 effect: Option<(MacroToolEffect, proc_macro2::Span)>,
331 #[cfg(feature = "md-tmpl")]
332 inline_params: Vec<(syn::Ident, syn::LitStr)>,
333 #[cfg(feature = "md-tmpl")]
334 env_vars: Vec<(syn::Ident, syn::Lit)>,
335 #[cfg(feature = "md-tmpl")]
336 context_fn: Option<syn::Path>,
337 #[cfg(not(feature = "md-tmpl"))]
338 has_inline_params: bool,
339 #[cfg(not(feature = "md-tmpl"))]
340 has_context_fn: bool,
341 #[cfg(not(feature = "md-tmpl"))]
342 has_env: bool,
343}
344
345impl ToolAttrBuilder {
346 fn parse_params_attr(&mut self, input: syn::parse::ParseStream) -> syn::Result<()> {
347 let content;
348 syn::parenthesized!(content in input);
349 while !content.is_empty() {
350 let key: syn::Ident = content.parse()?;
351 let _: syn::Token![=] = content.parse()?;
352 let value: syn::LitStr = content.parse()?;
353 #[cfg(feature = "md-tmpl")]
354 self.inline_params.push((key, value));
355 #[cfg(not(feature = "md-tmpl"))]
356 {
357 drop(key);
358 drop(value);
359 }
360 if !content.is_empty() {
361 let _: syn::Token![,] = content.parse()?;
362 }
363 }
364 #[cfg(not(feature = "md-tmpl"))]
365 {
366 self.has_inline_params = true;
367 }
368 Ok(())
369 }
370
371 fn parse_env_attr(&mut self, input: syn::parse::ParseStream) -> syn::Result<()> {
372 let content;
373 syn::parenthesized!(content in input);
374 while !content.is_empty() {
375 let key: syn::Ident = content.parse()?;
376 let _: syn::Token![=] = content.parse()?;
377 let value: syn::Lit = content.parse()?;
378 match &value {
379 syn::Lit::Str(_) | syn::Lit::Int(_) | syn::Lit::Float(_) | syn::Lit::Bool(_) => {}
380 other => {
381 return Err(syn::Error::new(
382 other.span(),
383 "env values must be string, integer, float, or bool literals",
384 ));
385 }
386 }
387 #[cfg(feature = "md-tmpl")]
388 self.env_vars.push((key, value));
389 #[cfg(not(feature = "md-tmpl"))]
390 {
391 drop(key);
392 drop(value);
393 }
394 if !content.is_empty() {
395 let _: syn::Token![,] = content.parse()?;
396 }
397 }
398 #[cfg(not(feature = "md-tmpl"))]
399 {
400 self.has_env = true;
401 }
402 Ok(())
403 }
404
405 fn parse_single(&mut self, input: syn::parse::ParseStream) -> syn::Result<()> {
406 let ident: syn::Ident = input.parse()?;
407 let key = ToolAttrKey::try_from(&ident)?;
408 self.parse_by_key(&ident, key, input)
409 }
410
411 fn parse_lit_str_attr(
412 field: &mut Option<syn::LitStr>,
413 ident: &syn::Ident,
414 key: ToolAttrKey,
415 input: syn::parse::ParseStream,
416 ) -> syn::Result<()> {
417 let _: syn::Token![=] = input.parse()?;
418 if field.is_some() {
419 return Err(syn::Error::new(
420 ident.span(),
421 format!("duplicate `{}` attribute", key.as_str()),
422 ));
423 }
424 *field = Some(input.parse::<syn::LitStr>()?);
425 Ok(())
426 }
427
428 fn parse_context_attr(
429 &mut self,
430 ident: &syn::Ident,
431 key: ToolAttrKey,
432 input: syn::parse::ParseStream,
433 ) -> syn::Result<()> {
434 let _: syn::Token![=] = input.parse()?;
435 #[cfg(feature = "md-tmpl")]
436 {
437 if self.context_fn.is_some() {
438 return Err(syn::Error::new(
439 ident.span(),
440 format!("duplicate `{}` attribute", key.as_str()),
441 ));
442 }
443 self.context_fn = Some(input.parse::<syn::Path>()?);
444 }
445 #[cfg(not(feature = "md-tmpl"))]
446 {
447 let _path: syn::Path = input.parse()?;
448 if self.has_context_fn {
449 return Err(syn::Error::new(
450 ident.span(),
451 format!("duplicate `{}` attribute", key.as_str()),
452 ));
453 }
454 self.has_context_fn = true;
455 }
456 Ok(())
457 }
458
459 fn parse_effect_attr(
460 &mut self,
461 ident: &syn::Ident,
462 key: ToolAttrKey,
463 input: syn::parse::ParseStream,
464 ) -> syn::Result<()> {
465 let _: syn::Token![=] = input.parse()?;
466 if self.effect.is_some() {
467 return Err(syn::Error::new(
468 ident.span(),
469 format!("duplicate `{}` attribute", key.as_str()),
470 ));
471 }
472 let span = input.span();
473 let eff = if input.peek(syn::Ident) {
474 let id: syn::Ident = input.parse()?;
475 parse_effect_str(id.to_string().as_str(), id.span())?
476 } else if input.peek(syn::LitStr) {
477 let lit: syn::LitStr = input.parse()?;
478 parse_effect_str(lit.value().as_str(), lit.span())?
479 } else {
480 return Err(syn::Error::new(
481 span,
482 "expected identifier or string literal (`read_only`, `mutating`, or `destructive`)",
483 ));
484 };
485 self.effect = Some((eff, span));
486 Ok(())
487 }
488
489 pub(crate) fn parse_by_key(
490 &mut self,
491 ident: &syn::Ident,
492 key: ToolAttrKey,
493 input: syn::parse::ParseStream,
494 ) -> syn::Result<()> {
495 match key {
496 ToolAttrKey::Description => {
497 Self::parse_lit_str_attr(&mut self.description_inline, ident, key, input)?;
498 }
499 ToolAttrKey::DescriptionFile => {
500 Self::parse_lit_str_attr(&mut self.description_file_path, ident, key, input)?;
501 }
502 ToolAttrKey::ResponseFile => {
503 Self::parse_lit_str_attr(&mut self.response_file_path, ident, key, input)?;
504 }
505 ToolAttrKey::Response => {
506 Self::parse_lit_str_attr(&mut self.response_inline, ident, key, input)?;
507 }
508 ToolAttrKey::Params => {
509 self.parse_params_attr(input)?;
510 }
511 ToolAttrKey::Env => {
512 self.parse_env_attr(input)?;
513 }
514 ToolAttrKey::Context => {
515 self.parse_context_attr(ident, key, input)?;
516 }
517 ToolAttrKey::Idempotent => {
518 let _: syn::Token![=] = input.parse()?;
519 if self.idempotent.is_some() {
520 return Err(syn::Error::new(
521 ident.span(),
522 format!("duplicate `{}` attribute", key.as_str()),
523 ));
524 }
525 self.idempotent = Some(input.parse::<syn::LitBool>()?);
526 }
527 ToolAttrKey::Effect => {
528 self.parse_effect_attr(ident, key, input)?;
529 }
530 }
531 Ok(())
532 }
533
534 pub(crate) fn build(self) -> syn::Result<ToolAttr> {
535 #[cfg(feature = "md-tmpl")]
536 let has_inline_params = !self.inline_params.is_empty();
537 #[cfg(not(feature = "md-tmpl"))]
538 let has_inline_params = self.has_inline_params;
539
540 #[cfg(feature = "md-tmpl")]
541 let has_context_fn = self.context_fn.is_some();
542 #[cfg(not(feature = "md-tmpl"))]
543 let has_context_fn = self.has_context_fn;
544
545 validate_tool_attr(&self)?;
546
547 let effect = match (self.effect, self.idempotent) {
548 (Some((eff, _)), _) => eff,
549 (None, Some(idemp)) => {
550 if idemp.value() {
551 MacroToolEffect::ReadOnly
552 } else {
553 MacroToolEffect::Mutating
554 }
555 }
556 (None, None) => MacroToolEffect::Mutating,
557 };
558 let idempotent = matches!(effect, MacroToolEffect::ReadOnly);
559
560 Ok(ToolAttr {
561 description_inline: self.description_inline,
562 description_file_path: self.description_file_path,
563 response_file_path: self.response_file_path,
564 response_inline: self.response_inline,
565 #[cfg(feature = "md-tmpl")]
566 inline_params: self.inline_params,
567 #[cfg(feature = "md-tmpl")]
568 env_vars: self.env_vars,
569 #[cfg(feature = "md-tmpl")]
570 context_fn: self.context_fn,
571 has_inline_params,
572 has_context_fn,
573 idempotent,
574 effect,
575 })
576 }
577}
578
579impl syn::parse::Parse for ToolAttr {
580 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
581 let mut builder = ToolAttrBuilder::default();
582
583 while !input.is_empty() {
584 builder.parse_single(input)?;
585 if !input.is_empty() {
586 let _: syn::Token![,] = input.parse()?;
587 }
588 }
589
590 builder.build()
591 }
592}
593
594fn validate_tool_attr(builder: &ToolAttrBuilder) -> syn::Result<()> {
595 if builder.description_inline.is_some() && builder.description_file_path.is_some() {
596 return Err(syn::Error::new(
597 proc_macro2::Span::call_site(),
598 "`description` and `description_file` are mutually exclusive",
599 ));
600 }
601
602 if builder.response_file_path.is_some() && builder.response_inline.is_some() {
603 return Err(syn::Error::new(
604 proc_macro2::Span::call_site(),
605 "`response` and `response_file` are mutually exclusive",
606 ));
607 }
608
609 #[cfg(feature = "md-tmpl")]
610 let has_inline_params = !builder.inline_params.is_empty();
611 #[cfg(not(feature = "md-tmpl"))]
612 let has_inline_params = builder.has_inline_params;
613
614 #[cfg(feature = "md-tmpl")]
615 let has_context_fn = builder.context_fn.is_some();
616 #[cfg(not(feature = "md-tmpl"))]
617 let has_context_fn = builder.has_context_fn;
618
619 #[cfg(feature = "md-tmpl")]
620 let has_env = !builder.env_vars.is_empty();
621 #[cfg(not(feature = "md-tmpl"))]
622 let has_env = builder.has_env;
623
624 if has_inline_params && has_context_fn {
625 return Err(syn::Error::new(
626 proc_macro2::Span::call_site(),
627 "`params(...)` and `context = ...` are mutually exclusive; \
628 use `params` for compile-time values or `context` for runtime values",
629 ));
630 }
631
632 if has_inline_params
633 && builder.description_file_path.is_none()
634 && builder.description_inline.is_none()
635 {
636 return Err(syn::Error::new(
637 proc_macro2::Span::call_site(),
638 "`params(...)` requires `description_file = \"...\"` or `description = \"...\"`",
639 ));
640 }
641
642 if has_context_fn
643 && builder.description_file_path.is_none()
644 && builder.description_inline.is_none()
645 {
646 return Err(syn::Error::new(
647 proc_macro2::Span::call_site(),
648 "`context = ...` requires `description_file = \"...\"` or `description = \"...\"`",
649 ));
650 }
651
652 if has_env && builder.description_file_path.is_none() && builder.description_inline.is_none() {
653 return Err(syn::Error::new(
654 proc_macro2::Span::call_site(),
655 "`env(...)` requires `description_file = \"...\"` or `description = \"...\"`",
656 ));
657 }
658
659 #[cfg(not(feature = "md-tmpl"))]
660 if builder.description_file_path.is_some() {
661 return Err(syn::Error::new(
662 proc_macro2::Span::call_site(),
663 "`description_file` requires the `md-tmpl` feature of `llm-tool`",
664 ));
665 }
666
667 #[cfg(not(feature = "md-tmpl"))]
668 if builder.response_file_path.is_some() {
669 return Err(syn::Error::new(
670 proc_macro2::Span::call_site(),
671 "`response_file` requires the `md-tmpl` feature of `llm-tool`",
672 ));
673 }
674
675 #[cfg(not(feature = "md-tmpl"))]
676 if builder.response_inline.is_some() {
677 return Err(syn::Error::new(
678 proc_macro2::Span::call_site(),
679 "`response` requires the `md-tmpl` feature of `llm-tool`",
680 ));
681 }
682
683 if let (Some(idemp), Some((eff, span))) = (&builder.idempotent, &builder.effect) {
684 let idemp_val = idemp.value();
685 let eff_is_read_only = matches!(eff, MacroToolEffect::ReadOnly);
686 if idemp_val != eff_is_read_only {
687 return Err(syn::Error::new(
688 *span,
689 "conflicting `idempotent` and `effect` attributes: `idempotent = true` requires `effect = read_only`",
690 ));
691 }
692 }
693
694 Ok(())
695}
696
697struct ParamInfo {
701 name: syn::Ident,
702 ty: Box<syn::Type>,
703 doc_attrs: Vec<syn::Attribute>,
704 is_context: bool,
705 is_mut: bool,
706}
707
708enum ReturnInfo {
710 ResultType {
712 ok_type: Box<syn::Type>,
713 err_type: Box<syn::Type>,
714 },
715 BareType,
717}
718
719fn validate_params(all_params: &[ParamInfo], sig: &syn::Signature) -> syn::Result<()> {
720 let ctx_count = all_params.iter().filter(|p| p.is_context).count();
721 if ctx_count > 1 {
722 return Err(syn::Error::new_spanned(
723 sig,
724 "#[llm_tool] functions can accept at most one ToolContext parameter",
725 ));
726 }
727 for param in all_params.iter().filter(|p| !p.is_context) {
728 if param.doc_attrs.is_empty() {
729 return Err(syn::Error::new_spanned(
730 ¶m.name,
731 format!(
732 "#[llm_tool] parameter `{}` must have a doc comment \
733 (used as the parameter description in the JSON schema)",
734 param.name
735 ),
736 ));
737 }
738 }
739 Ok(())
740}
741
742fn resolve_effect_tokens(
743 attr: Option<&ToolAttr>,
744 crate_path: &proc_macro2::TokenStream,
745) -> (proc_macro2::TokenStream, bool) {
746 let effect_val = attr.map_or(MacroToolEffect::Mutating, |a| a.effect);
747 let effect_tokens = match effect_val {
748 MacroToolEffect::ReadOnly => quote!(#crate_path::ToolEffect::ReadOnly),
749 MacroToolEffect::Destructive => quote!(#crate_path::ToolEffect::Destructive),
750 MacroToolEffect::Mutating => quote!(#crate_path::ToolEffect::Mutating),
751 };
752 let idempotent_bool = attr.is_some_and(|a| a.idempotent);
753 (effect_tokens, idempotent_bool)
754}
755
756fn tool_impl(func: &ItemFn, attr: Option<&ToolAttr>) -> syn::Result<proc_macro2::TokenStream> {
757 let crate_path = quote! { ::llm_tool };
758 let fn_name = &func.sig.ident;
759 reject_generic_signature(func, MACRO_LLM_TOOL)?;
760 let tool_name_str = fn_name.to_string();
761 let struct_name = format_ident!("{}", tool_name_str.to_case(Case::Pascal));
762 let params_name = format_ident!("{}Params", struct_name);
763
764 let DescriptionInfo {
766 static_description,
767 helper_tokens,
768 description_method,
769 dep_tracking,
770 } = resolve_description(func, attr)?;
771
772 let response_info = resolve_response_template(attr, &struct_name, fn_name)?;
774
775 let all_params = extract_params(func, MACRO_LLM_TOOL)?;
777 validate_params(&all_params, &func.sig)?;
778 let ctx_param = all_params.iter().find(|p| p.is_context);
779 let params: Vec<&ParamInfo> = all_params.iter().filter(|p| !p.is_context).collect();
780
781 let return_info = parse_return_type(func, MACRO_LLM_TOOL)?;
783
784 let param_names: Vec<_> = params.iter().map(|p| &p.name).collect();
785 let param_descriptions: Vec<String> = params
786 .iter()
787 .map(|p| extract_doc_string(&p.doc_attrs))
788 .collect();
789
790 let (param_struct_types, borrow_bindings) = build_param_types_and_borrows(¶ms);
791 let serde_defaults = build_serde_defaults(¶ms);
792 let body_tokens = build_body_tokens(func, &return_info, &crate_path, &response_info);
793
794 let vis = &func.vis;
795
796 let params_doc = format!("Auto-generated parameters for the [`{struct_name}`] tool.");
797 let struct_doc = format!(
798 "Auto-generated tool struct. See the `#[llm_tool]`-annotated function `{fn_name}` for the implementation."
799 );
800
801 let ctx_binding = if let Some(cp) = ctx_param {
804 let ctx_name = &cp.name;
805 quote! { let #ctx_name = _ctx; }
806 } else {
807 quote! {}
808 };
809
810 let mut_tokens: Vec<proc_macro2::TokenStream> = params
811 .iter()
812 .map(|p| {
813 if p.is_mut {
814 quote! { mut }
815 } else {
816 quote! {}
817 }
818 })
819 .collect();
820
821 let response_dep_tracking = &response_info.dep_tracking;
822 let response_helper_tokens = &response_info.helper_tokens;
823 let (effect_tokens, idempotent_bool) = resolve_effect_tokens(attr, &crate_path);
824
825 Ok(quote! {
826 #dep_tracking
827 #response_dep_tracking
828 #helper_tokens
829 #response_helper_tokens
830
831 #[doc = #params_doc]
832 #[derive(::serde::Deserialize, ::schemars::JsonSchema)]
833 #vis struct #params_name {
834 #(
835 #[schemars(description = #param_descriptions)]
836 #serde_defaults
837 pub #param_names: #param_struct_types,
838 )*
839 }
840
841 #[doc = #struct_doc]
842 #vis struct #struct_name;
843
844 impl #crate_path::RustTool for #struct_name {
845 type Params = #params_name;
846 const NAME: &'static str = #tool_name_str;
847 const DESCRIPTION: &'static str = #static_description;
848 const IDEMPOTENT: bool = #idempotent_bool;
849 const EFFECT: #crate_path::ToolEffect = #effect_tokens;
850
851 #description_method
852
853
854 #[allow(unknown_lints, clippy::unused_async_trait_impl)]
856 async fn call(&self, params: Self::Params, _ctx: &#crate_path::ToolContext) -> ::core::result::Result<#crate_path::ToolOutput, #crate_path::ToolError> {
857
858 use #crate_path::__private::SerializeFallback as _;
861 let #params_name { #( #mut_tokens #param_names, )* } = params;
864 #( #borrow_bindings )*
866 #ctx_binding
867 #body_tokens
868 }
869 }
870 })
871}
872
873struct DescriptionInfo {
877 static_description: String,
879 helper_tokens: proc_macro2::TokenStream,
881 description_method: Option<proc_macro2::TokenStream>,
883 dep_tracking: proc_macro2::TokenStream,
885}
886
887pub(crate) mod desc;
888pub(crate) mod helpers;
889pub(crate) use desc::resolve_description;
890pub(crate) use helpers::{
891 build_body_tokens, build_param_types_and_borrows, build_serde_defaults, extract_doc_string,
892 extract_params, parse_return_type, reject_generic_signature, resolve_response_template,
893};