fix(macros): respect local feature in #[prompt] macro — omit + Send bound (#803)

* refactor(prompt): update return type handling

* fix(prompt): add omit send and test
This commit is contained in:
WeekendsuperHero 2026-04-14 06:56:19 -07:00 committed by GitHub
parent c99903a67a
commit 6603c1ff15
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -20,6 +20,9 @@ pub struct PromptAttribute {
pub icons: Option<Expr>,
/// Optional metadata for the prompt
pub meta: Option<Expr>,
/// When true, the generated future will not require `Send`. Useful for `!Send` handlers
/// (e.g. single-threaded database connections). Also enabled globally by the `local` crate feature.
pub local: bool,
}
pub struct ResolvedPromptAttribute {
@ -78,6 +81,7 @@ pub fn prompt(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream>
};
let mut fn_item = syn::parse2::<ImplItemFn>(input.clone())?;
let fn_ident = &fn_item.sig.ident;
let omit_send = cfg!(feature = "local") || attribute.local;
let prompt_attr_fn_ident = format_ident!("{}_prompt_attr", fn_ident);
@ -123,7 +127,8 @@ pub fn prompt(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream>
// Modify the input function for async support (same as tool macro)
if fn_item.sig.asyncness.is_some() {
// 1. remove asyncness from sig
// 2. make return type: `futures::future::BoxFuture<'_, #ReturnType>`
// 2. make return type: `std::pin::Pin<Box<dyn std::future::Future<Output = #ReturnType> + Send + '_>>`
// (omit `+ Send` when the `local` crate feature is active or `#[prompt(local)]` is used)
// 3. make body: { Box::pin(async move { #body }) }
let new_output = syn::parse2::<ReturnType>({
let mut lt = quote! { 'static };
@ -138,10 +143,18 @@ pub fn prompt(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream>
}
match &fn_item.sig.output {
syn::ReturnType::Default => {
quote! { -> ::std::pin::Pin<Box<dyn ::std::future::Future<Output = ()> + Send + #lt>> }
if omit_send {
quote! { -> ::std::pin::Pin<Box<dyn ::std::future::Future<Output = ()> + #lt>> }
} else {
quote! { -> ::std::pin::Pin<Box<dyn ::std::future::Future<Output = ()> + Send + #lt>> }
}
}
syn::ReturnType::Type(_, ty) => {
quote! { -> ::std::pin::Pin<Box<dyn ::std::future::Future<Output = #ty> + Send + #lt>> }
if omit_send {
quote! { -> ::std::pin::Pin<Box<dyn ::std::future::Future<Output = #ty> + #lt>> }
} else {
quote! { -> ::std::pin::Pin<Box<dyn ::std::future::Future<Output = #ty> + Send + #lt>> }
}
}
}
})?;
@ -226,4 +239,38 @@ mod test {
Ok(())
}
#[test]
fn test_async_prompt_default_send_behavior() -> syn::Result<()> {
let attr = quote! {};
let input = quote! {
async fn test_prompt_default_send(&self) -> String {
"ok".to_string()
}
};
let result = prompt(attr, input)?;
let result_str = result.to_string();
if cfg!(feature = "local") {
assert!(!result_str.contains("+ Send +"));
} else {
assert!(result_str.contains("+ Send +"));
}
Ok(())
}
#[test]
fn test_async_prompt_local_omits_send() -> syn::Result<()> {
let attr = quote! { local };
let input = quote! {
async fn test_prompt_local_no_send(&self) -> String {
"ok".to_string()
}
};
let result = prompt(attr, input)?;
let result_str = result.to_string();
assert!(!result_str.contains("+ Send +"));
Ok(())
}
}