use darling::FromMeta;
use syn::{FnArg, ItemFn, Pat, PatType, Type};
#[derive(Debug)]
pub struct ToolInfo {
#[allow(dead_code)]
pub func: ItemFn,
pub tool_name: String,
pub fn_ident: syn::Ident,
pub description: String,
pub is_async: bool,
pub has_context: bool,
pub metadata_type: Option<Type>,
pub args: Vec<ToolArg>,
pub struct_args: Option<Type>,
pub read_only: bool,
pub idempotent: bool,
pub destructive: bool,
pub streaming: bool,
pub timeout_ms: Option<u64>,
}
#[derive(Debug)]
pub struct ToolArg {
pub name: String,
pub ty: Type,
pub doc: Option<String>,
}
#[derive(Debug, Default, FromMeta)]
pub struct ToolAttrs {
#[darling(default)]
pub description: Option<String>,
#[darling(default)]
pub read_only: bool,
#[darling(default)]
pub idempotent: bool,
#[darling(default)]
pub destructive: bool,
#[darling(default)]
pub streaming: bool,
#[darling(default)]
pub timeout_ms: Option<u64>,
}
fn is_act_context(ty: &Type) -> bool {
if let Type::Reference(r) = ty {
return is_act_context(&r.elem);
}
if let Type::Path(tp) = ty {
let last = tp.path.segments.last();
if let Some(seg) = last {
return seg.ident == "ActContext";
}
}
false
}
fn extract_metadata_type(ty: &Type) -> Option<Type> {
let inner = match ty {
Type::Reference(r) => &*r.elem,
other => other,
};
if let Type::Path(tp) = inner
&& let Some(seg) = tp.path.segments.last()
&& let syn::PathArguments::AngleBracketed(args) = &seg.arguments
&& let Some(syn::GenericArgument::Type(t)) = args.args.first()
{
if let Type::Tuple(tuple) = t
&& tuple.elems.is_empty()
{
return None;
}
return Some(t.clone());
}
None
}
pub fn parse_tool_fn(func: &ItemFn, attrs: ToolAttrs) -> syn::Result<ToolInfo> {
let fn_ident = func.sig.ident.clone();
let tool_name = fn_ident.to_string();
let is_async = func.sig.asyncness.is_some();
let mut args = Vec::new();
let mut has_context = false;
let mut metadata_type = None;
let mut struct_args = None;
for input in &func.sig.inputs {
if let FnArg::Typed(PatType { pat, ty, attrs, .. }) = input {
if is_act_context(ty) {
has_context = true;
metadata_type = extract_metadata_type(ty);
continue;
}
let is_args_param = attrs.iter().any(|a| a.path().is_ident("args"));
if is_args_param {
struct_args = Some(ty.as_ref().clone());
continue;
}
let param_name = if let Pat::Ident(pi) = pat.as_ref() {
pi.ident.to_string()
} else {
return Err(syn::Error::new_spanned(pat, "expected identifier pattern"));
};
let doc = attrs
.iter()
.find(|a| a.path().is_ident("doc"))
.and_then(|a| {
if let syn::Meta::NameValue(nv) = &a.meta
&& let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) = &nv.value
{
return Some(s.value().trim().to_string());
}
None
});
args.push(ToolArg {
name: param_name,
ty: ty.as_ref().clone(),
doc,
});
}
}
let streaming = attrs.streaming;
Ok(ToolInfo {
func: func.clone(),
tool_name,
fn_ident,
description: attrs.description.unwrap_or_default(),
is_async,
has_context,
metadata_type,
args,
struct_args,
read_only: attrs.read_only,
idempotent: attrs.idempotent,
destructive: attrs.destructive,
streaming,
timeout_ms: attrs.timeout_ms,
})
}