use crate::agentic::agent_definition_attributes::{
AgentDefinitionAttributes, AgentDefinitionKind, parse_agent_definition_attributes,
};
use crate::agentic::agent_definition_http_endpoint::{
ParsedDurableStreamSlotSource, ParsedHttpEndpointDetails, extract_http_endpoints,
};
use crate::agentic::agent_definition_read_only::extract_read_only;
use crate::agentic::helpers::{
AgentConfigAttrRemover, has_agent_config_attr, is_async_trait_attr, is_constructor_method,
};
use crate::agentic::{
async_trait_in_agent_definition_error, endpoint_on_constructor_method_error,
endpoint_on_static_method_error, generic_type_in_agent_method_error,
generic_type_in_agent_return_type_error, generic_type_in_constructor_error, get_remote_client,
invalid_static_method_in_agent_error, multiple_constructor_methods_error,
no_constructor_method_error,
};
use crate::rpc_client_common::is_static_method;
use proc_macro::TokenStream;
use quote::quote;
use syn::ItemTrait;
use syn::spanned::Spanned;
use syn::visit_mut::VisitMut;
pub fn agent_definition_impl(attrs: TokenStream, item: TokenStream) -> TokenStream {
expand_agent_definition(attrs.into(), item.into(), AgentDefinitionKind::Regular).into()
}
pub(crate) fn expand_agent_definition(
attrs: proc_macro2::TokenStream,
item: proc_macro2::TokenStream,
definition_kind: AgentDefinitionKind,
) -> proc_macro2::TokenStream {
let mut agent_definition_trait = match syn::parse2::<ItemTrait>(item) {
Ok(item) => item,
Err(error) => return error.to_compile_error(),
};
let AgentDefinitionAttributes {
name,
filesystem_mount,
agent_kind,
agent_mode,
agent_is_durable,
http_mount,
snapshotting,
snapshotting_enabled,
} = match parse_agent_definition_attributes(attrs, definition_kind) {
Ok(v) => v,
Err(err) => return err.to_compile_error(),
};
if let Err(error) = validate_http_constructor(
&agent_definition_trait,
&agent_kind,
filesystem_mount.as_ref(),
) {
return error.to_compile_error();
}
let type_name = name
.map(|name| name.value())
.unwrap_or_else(|| agent_definition_trait.ident.to_string());
if !agent_is_durable && snapshotting_enabled {
return syn::Error::new_spanned(
&agent_definition_trait.ident,
format!(
"agent `{type_name}` is ephemeral and enables snapshotting, but ephemeral agents take no snapshots. Remove the snapshotting option or make the agent durable."
),
)
.to_compile_error();
}
let type_parameters = agent_definition_trait
.generics
.type_params()
.map(|tp| tp.ident.to_string())
.collect::<Vec<_>>();
let has_async_trait_attribute = agent_definition_trait.attrs.iter().any(is_async_trait_attr);
if has_async_trait_attribute {
return async_trait_in_agent_definition_error(&agent_definition_trait);
}
match get_agent_type_with_remote_client(
&agent_definition_trait,
&type_name,
agent_kind,
agent_mode,
agent_is_durable,
http_mount,
snapshotting,
&type_parameters,
) {
Ok(agent_type_with_remote_client) => {
let AgentTypeWithRemoteClient {
agent_type,
remote_client,
} = agent_type_with_remote_client;
let registration_function: syn::TraitItem = syn::parse_quote! {
fn __register_agent_type() where Self: Sized {
fn descriptor() -> golem_rust::golem_agentic::golem::agent::common::AgentType {
use golem_rust::agentic::{AgentParameterSchema as _, AgentParameterStreams as _};
let agent_type = #agent_type;
golem_rust::agentic::validate_wire_agent_http(&agent_type)
.expect("Agent HTTP configuration validation failed");
agent_type
}
golem_rust::agentic::register_wire_agent_type(
golem_rust::agentic::AgentTypeName(Self::__golem_agent_type_name().to_string()),
descriptor,
);
}
};
let load_snapshot_item = get_load_snapshot_item();
let save_snapshot_item = get_save_snapshot_item();
let auto_snapshot_item = get_auto_snapshot_item(snapshotting_enabled);
let agent_type_name_item = get_agent_type_name_item(&type_name);
let agent_implementation_annotation_item = get_agent_implementation_annotation_item();
agent_definition_trait.items.push(load_snapshot_item);
agent_definition_trait.items.push(save_snapshot_item);
agent_definition_trait.items.push(syn::parse_quote! {
async fn save_snapshot_parts(&self) -> Result<golem_rust::agentic::MultipartSnapshot, String> {
Err("save_snapshot_parts not implemented".to_string())
}
});
agent_definition_trait.items.push(syn::parse_quote! {
async fn load_snapshot_parts(
_snapshot: golem_rust::agentic::MultipartSnapshot,
_context: golem_rust::agentic::SnapshotRestoreContext,
) -> Result<Self, String> where Self: Sized {
Err("load_snapshot_parts not implemented".to_string())
}
});
agent_definition_trait.items.push(auto_snapshot_item);
agent_definition_trait.items.push(registration_function);
agent_definition_trait.items.push(agent_type_name_item);
agent_definition_trait
.items
.push(agent_implementation_annotation_item);
AgentConfigAttrRemover.visit_item_trait_mut(&mut agent_definition_trait);
let result = quote! {
#[allow(async_fn_in_trait)]
#agent_definition_trait
#remote_client
};
result
}
Err(invalid_trait_error) => invalid_trait_error.into(),
}
}
fn validate_http_constructor(
agent: &ItemTrait,
kind: &syn::Ident,
mount: Option<&syn::LitStr>,
) -> syn::Result<()> {
if kind != "HttpRouter" && mount.is_none() {
return Ok(());
}
let mut params = std::collections::HashSet::new();
for item in &agent.items {
if let syn::TraitItem::Fn(method) = item
&& is_constructor_method(&method.sig, None)
{
for input in &method.sig.inputs {
if let syn::FnArg::Typed(param) = input {
if has_agent_config_attr(param) {
continue;
}
if kind == "HttpRouter" {
return Err(syn::Error::new_spanned(
param,
"HTTP routers have no constructor identity inputs; use #[agent_config] for configuration",
));
}
let syn::Pat::Ident(name) = &*param.pat else {
return Err(syn::Error::new_spanned(
param,
"filesystem constructor parameters must be named",
));
};
params.insert(name.ident.to_string());
}
}
}
}
if let Some(mount) = mount {
let value = mount.value();
let path = value.strip_prefix('/').ok_or_else(|| {
syn::Error::new_spanned(mount, "filesystem mount must be an absolute path")
})?;
for segment in path.split('/').filter(|_| !path.is_empty()) {
if let Some(name) = segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
if matches!(name, "agent-type" | "agent-version") {
continue;
}
if !params.remove(name) {
return Err(syn::Error::new_spanned(
mount,
format!(
"filesystem mount capture {{{name}}} must bind one constructor parameter exactly once"
),
));
}
} else {
golem_schema::http::FileMapping::compile(&format!("/{segment}"), "/mount")
.map_err(|_| syn::Error::new_spanned(mount, "filesystem mount must contain safe literals and whole-segment constructor captures"))?;
}
}
if !params.is_empty() {
let mut names = params.into_iter().collect::<Vec<_>>();
names.sort();
return Err(syn::Error::new_spanned(
mount,
format!(
"filesystem mount must bind all constructor parameters exactly once; missing: {}",
names.join(", ")
),
));
}
}
Ok(())
}
fn get_load_snapshot_item() -> syn::TraitItem {
syn::parse_quote! {
async fn load_snapshot(
_bytes: Vec<u8>,
_context: golem_rust::agentic::SnapshotRestoreContext,
) -> Result<Self, String> where Self: Sized {
Err("load_snapshot not implemented".to_string())
}
}
}
fn get_auto_snapshot_item(enabled: bool) -> syn::TraitItem {
if enabled {
syn::parse_quote! {
#[doc(hidden)]
fn __golem_auto_load_snapshot(bytes: &[u8]) -> Result<Self, String>
where Self: Sized + golem_rust::serde::Serialize + golem_rust::serde::de::DeserializeOwned {
golem_rust::serde_json::from_slice::<Self>(bytes)
.map_err(|e| format!("Failed to deserialize agent snapshot: {}", e))
}
}
} else {
syn::parse_quote! {
#[doc(hidden)]
fn __golem_auto_load_snapshot(_bytes: &[u8]) -> Result<Self, String> where Self: Sized {
Err("snapshotting is disabled".to_string())
}
}
}
}
fn get_save_snapshot_item() -> syn::TraitItem {
syn::parse_quote! {
async fn save_snapshot(&self) -> Result<Vec<u8>, String> {
Err("save_snapshot not implemented".to_string())
}
}
}
fn get_agent_implementation_annotation_item() -> syn::TraitItem {
syn::parse_quote! {
#[doc(hidden)]
fn agent_implementation_annotation() where Self: Sized;
}
}
fn get_agent_type_name_item(agent_type_name: &str) -> syn::TraitItem {
syn::parse_quote! {
#[doc(hidden)]
fn __golem_agent_type_name() -> &'static str where Self: Sized {
#agent_type_name
}
}
}
struct AgentTypeWithRemoteClient {
agent_type: proc_macro2::TokenStream,
remote_client: proc_macro2::TokenStream,
}
fn get_agent_type_with_remote_client(
agent_definition_trait: &ItemTrait,
agent_trait_name: &str,
kind_value: syn::Ident,
mode_value: proc_macro2::TokenStream,
agent_is_durable: bool,
http_options: Option<proc_macro2::TokenStream>,
snapshotting_value: proc_macro2::TokenStream,
type_parameters: &[String],
) -> Result<AgentTypeWithRemoteClient, TokenStream> {
let mut constructor_methods = vec![];
for item in &agent_definition_trait.items {
if let syn::TraitItem::Fn(trait_fn) = item
&& is_constructor_method(&trait_fn.sig, None)
{
constructor_methods.push(trait_fn.clone());
}
}
let http_options = if let Some(options) = http_options {
quote! {
Some(#options)
}
} else {
quote! {
None
}
};
let methods = agent_definition_trait.items.iter().filter_map(|item| {
if let syn::TraitItem::Fn(trait_fn) = item {
let parsed_endpoint_details_result: syn::Result<Vec<ParsedHttpEndpointDetails>> =
extract_http_endpoints(&trait_fn.attrs);
let parsed_endpoint_details = match parsed_endpoint_details_result {
Ok(details) => details,
Err(err) => {
return Some(err.to_compile_error());
}
};
if kind_value == "Regular" && parsed_endpoint_details.iter().any(|endpoint| endpoint.http_method == "any") {
return Some(syn::Error::new_spanned(
&trait_fn.sig.ident,
"ANY endpoints require an HTTP router defined with #[http_router]",
).to_compile_error());
}
if !parsed_endpoint_details.is_empty() && is_constructor_method(&trait_fn.sig, None) {
return Some(
endpoint_on_constructor_method_error(
trait_fn.sig.ident.span()
)
);
}
if !parsed_endpoint_details.is_empty() && is_static_method(&trait_fn.sig) {
return Some(
endpoint_on_static_method_error(
trait_fn.sig.ident.span()
)
);
}
if is_constructor_method(&trait_fn.sig, None) {
return None;
}
if is_static_method(&trait_fn.sig) {
return Some(invalid_static_method_in_agent_error(
trait_fn.sig.ident.span(),
&trait_fn.sig.ident.to_string()
));
}
let name = &trait_fn.sig.ident;
let method_name = &name.to_string();
let method_description = extract_description(&trait_fn.attrs).unwrap_or_default();
let method_prompt_hint = extract_prompt_hint(&trait_fn.attrs).unwrap_or_default();
let parsed_read_only = match extract_read_only(&trait_fn.attrs) {
Ok(v) => v,
Err(err) => return Some(err.to_compile_error()),
};
if parsed_read_only.is_some() && !agent_is_durable {
return Some(
syn::Error::new(
trait_fn.sig.ident.span(),
"read-only methods have no effect on ephemeral agents (no shared state to read). Remove the #[read_only] attribute or make the agent durable."
).to_compile_error()
);
}
let mut uses_principal = false;
for input in &trait_fn.sig.inputs {
if let syn::FnArg::Typed(pat_type) = input
&& let syn::Type::Path(type_path) = &*pat_type.ty
&& let Some(last) = type_path.path.segments.last()
&& last.ident == "Principal"
{
uses_principal = true;
break;
}
}
let read_only_tokens = if let Some(parsed) = parsed_read_only {
let cache_policy = parsed.cache_policy;
quote! {
Some(golem_rust::golem_agentic::golem::agent::common::ReadOnlyConfig {
cache_policy: #cache_policy,
uses_principal: #uses_principal,
})
}
} else {
quote! { None }
};
let endpoint_details_tokens = parsed_endpoint_details.iter().map(|parsed| {
let method = &parsed.http_method;
let path = &parsed.path_suffix;
let auth = if let Some(auth) = parsed.auth_details {
quote! { Some(#auth) }
} else {
quote! { None }
};
let cors_options_tokens = parsed.cors_options.iter().map(|c| quote! { #c.to_string() });
let header_vars_tokens = parsed.header_vars.iter().map(|(k,v)| {
quote! { (#k.to_string(), #v.to_string()) }
});
let durable_streams = if let Some(options) = &parsed.durable_streams {
let slots = options.slots.iter().map(|slot| {
let selector = &slot.slot;
let source = match slot.source {
ParsedDurableStreamSlotSource::Input => quote! {
golem_rust::golem_agentic::golem::agent::common::DurableStreamSlotSource::Input(#selector.to_string())
},
ParsedDurableStreamSlotSource::Output => quote! {
golem_rust::golem_agentic::golem::agent::common::DurableStreamSlotSource::Output(#selector.to_string())
},
};
let name = slot.name.as_ref().map_or_else(|| quote! { None }, |value| quote! { Some(#value.to_string()) });
let content_type = slot.content_type.as_ref().map_or_else(|| quote! { None }, |value| quote! { Some(#value.to_string()) });
quote! {
golem_rust::golem_agentic::golem::agent::common::DurableStreamSlotOptions {
source: #source,
name: #name,
content_type: #content_type,
}
}
});
let bool_option = |value: Option<bool>| value.map_or_else(|| quote! { None }, |value| quote! { Some(#value) });
let u32_option = |value: Option<u32>| value.map_or_else(|| quote! { None }, |value| quote! { Some(#value) });
let allow_external_writes = bool_option(options.allow_external_writes);
let allow_stream_delete = bool_option(options.allow_stream_delete);
let allow_invocation_delete = bool_option(options.allow_invocation_delete);
let has_load = options.max_concurrent_readers_per_stream.is_some()
|| options.max_append_requests_per_second_per_stream.is_some();
let readers = u32_option(options.max_concurrent_readers_per_stream);
let appends = u32_option(options.max_append_requests_per_second_per_stream);
let load = if has_load {
quote! {
Some(golem_rust::golem_agentic::golem::agent::common::DurableStreamRouteLoadOptions {
max_concurrent_readers_per_stream: #readers,
max_append_requests_per_second_per_stream: #appends,
})
}
} else {
quote! { None }
};
quote! {
Some(golem_rust::golem_agentic::golem::agent::common::DurableStreamRouteOptions {
slots: vec![#(#slots),*],
allow_external_writes: #allow_external_writes,
allow_stream_delete: #allow_stream_delete,
allow_invocation_delete: #allow_invocation_delete,
load: #load,
})
}
} else {
quote! { None }
};
quote! {
golem_rust::agentic::get_http_endpoint_details(
#method,
#path,
#auth,
vec![#(#cors_options_tokens),*],
vec![#(#header_vars_tokens),*],
#durable_streams,
).expect("Invalid HTTP endpoint configuration")
}
});
let endpoint_details = quote! {
vec![#(#endpoint_details_tokens),*]
};
let mut input_schema_logic = vec![];
let mut output_schema_logic = vec![];
let input_schema_token = quote! {
let mut default_inputs = vec![];
};
let output_schema_token = quote! {
let mut output = golem_rust::golem_agentic::golem::agent::common::OutputSchema::Unit;
};
for input in &trait_fn.sig.inputs {
if let syn::FnArg::Typed(pat_type) = input {
let param_name = match &*pat_type.pat {
syn::Pat::Ident(pat_ident) => pat_ident.ident.to_string(),
_ => "_".to_string(), };
let type_name = match &*pat_type.ty {
syn::Type::Path(type_path) => {
type_path.path.segments.last().unwrap().ident.to_string()
},
_ => "".to_string(),
};
if type_parameters.contains(&type_name) {
return generic_type_in_agent_method_error(pat_type.pat.span(), &type_name).into();
}
if !has_agent_config_attr(pat_type) {
let ty = &pat_type.ty;
input_schema_logic.push(quote! {
default_inputs.push((&golem_rust::agentic::AgentParameterProbe::<#ty>(::std::marker::PhantomData))
.parameter_schema(#param_name, &mut __golem_schema));
});
}
}
}
match &trait_fn.sig.output {
syn::ReturnType::Default => (),
syn::ReturnType::Type(_, ty) => {
let output_type_name = match &**ty {
syn::Type::Path(type_path) => {
type_path.path.segments.last().unwrap().ident.to_string()
},
_ => "".to_string(),
};
if type_parameters.contains(&output_type_name) {
return generic_type_in_agent_return_type_error(ty.span(), &output_type_name).into();
}
output_schema_logic.push(quote! {
if !<#ty as golem_rust::WireSchema>::IS_UNIT {
output = golem_rust::golem_agentic::golem::agent::common::OutputSchema::Single(
<#ty as golem_rust::WireSchema>::append_schema(&mut __golem_schema)
);
}
});
}
};
let input_schema = quote! {
{
#input_schema_token
#(#input_schema_logic)*
golem_rust::golem_agentic::golem::agent::common::InputSchema::Parameters(default_inputs)
}
};
let output_schema = quote! {
{
#output_schema_token
#(#output_schema_logic)*
output
}
};
Some(quote! {
golem_rust::golem_agentic::golem::agent::common::AgentMethod {
name: #method_name.to_string(),
description: #method_description.to_string(),
prompt_hint: {
if #method_prompt_hint.is_empty() {
None
} else {
Some(#method_prompt_hint.to_string())
}
},
input_schema: #input_schema,
output_schema: #output_schema,
http_endpoint: #endpoint_details,
read_only: #read_only_tokens,
}
})
} else {
None
}
});
let constructor_schema_init = quote! {
let mut constructor_default_inputs = vec![];
};
let mut constructor_parameters_with_schema: Vec<proc_macro2::TokenStream> = vec![];
if constructor_methods.is_empty() {
return Err(no_constructor_method_error(agent_definition_trait).into());
}
if constructor_methods.len() > 1 {
return Err(multiple_constructor_methods_error(agent_definition_trait).into());
}
let mut constructor_description = String::new();
let mut constructor_prompt = String::new();
let mut constructor_name = String::new();
let mut agent_config_types = Vec::new();
let mut client_data_value_param_defs = Vec::new();
let mut client_data_value_param_idents = Vec::new();
let mut client_agent_config_param_defs = Vec::new();
let mut client_agent_config_param_idents = Vec::new();
let high_level_description =
extract_description(&agent_definition_trait.attrs).unwrap_or_default();
if let Some(ctor_fn) = &constructor_methods.first().as_mut() {
constructor_description = extract_description(&ctor_fn.attrs).unwrap_or_default();
constructor_prompt = extract_prompt_hint(&ctor_fn.attrs).unwrap_or_default();
constructor_name = ctor_fn.sig.ident.to_string();
for input in &ctor_fn.sig.inputs {
if let syn::FnArg::Typed(pat_type) = input {
let param_name = match &*pat_type.pat {
syn::Pat::Ident(pat_ident) => {
let param_ident = &pat_ident.ident;
let ty = &pat_type.ty;
let type_name = match &**ty {
syn::Type::Path(type_path) => {
type_path.path.segments.last().unwrap().ident.to_string()
}
_ => "".to_string(),
};
if type_parameters.contains(&type_name) {
return Err(generic_type_in_constructor_error(
pat_type.span(),
&type_name,
)
.into());
}
if has_agent_config_attr(pat_type) {
agent_config_types.push(pat_type.ty.clone());
client_agent_config_param_defs.push(quote! {
#param_ident: <<#ty as ::golem_rust::agentic::InnerTypeHelper>::Type as ::golem_rust::agentic::ConfigSchema>::RpcType
});
client_agent_config_param_idents.push(param_ident.clone());
} else if type_name != "Principal" {
client_data_value_param_defs.push(quote! {
#param_ident: #ty
});
client_data_value_param_idents.push(param_ident.clone());
}
pat_ident.ident.to_string()
}
_ => "_".to_string(),
};
if !has_agent_config_attr(pat_type) {
let ty = &pat_type.ty;
constructor_parameters_with_schema.push(quote! {
assert!(
!(&golem_rust::agentic::AgentParameterProbe::<#ty>(::std::marker::PhantomData)).parameter_contains_stream(),
"AgentStream cannot be used in an agent constructor parameter"
);
constructor_default_inputs.push((&golem_rust::agentic::AgentParameterProbe::<#ty>(::std::marker::PhantomData))
.parameter_schema(#param_name, &mut __golem_schema));
});
}
}
}
}
let agent_constructor_input_schema = quote! {
{
#constructor_schema_init
#(#constructor_parameters_with_schema)*
golem_rust::golem_agentic::golem::agent::common::InputSchema::Parameters(constructor_default_inputs)
}
};
let constructor_data_schema_token = quote! {
let constructor_data_schema = {
#agent_constructor_input_schema
};
};
let remote_client = if kind_value == "HttpRouter" {
quote! {}
} else {
get_remote_client(
agent_definition_trait,
&client_data_value_param_defs,
&client_data_value_param_idents,
&client_agent_config_param_defs,
&client_agent_config_param_idents,
type_parameters,
agent_is_durable,
)
};
let constructor_prompt_hint = if constructor_prompt.is_empty() {
quote! { None }
} else {
quote! { Some(#constructor_prompt.to_string()) }
};
let constructor_name = if constructor_name.is_empty() {
quote! { None }
} else {
quote! { Some(#constructor_name.to_string()) }
};
let agent_constructor = quote! {
{
#constructor_data_schema_token
golem_rust::golem_agentic::golem::agent::common::AgentConstructor {
name: #constructor_name,
description: #constructor_description.to_string(),
prompt_hint: #constructor_prompt_hint,
input_schema: constructor_data_schema,
}
}
};
let high_level_description_ident = if high_level_description.is_empty() {
quote! { "" }
} else {
quote! { #high_level_description }
};
let config_impl = {
let add_type_config_entries = agent_config_types.iter().map(
|ct| quote! { result.append(&mut <#ct>::wire_config_entries(&mut __golem_schema)); },
);
quote! {
{
let mut result = Vec::new();
#(#add_type_config_entries)*
result
}
}
};
Ok(AgentTypeWithRemoteClient {
agent_type: quote! {
{
let mut __golem_schema = golem_rust::schema::wit::direct::WireSchemaBuilder::default();
let methods = vec![#(#methods),*];
let constructor = #agent_constructor;
let config = #config_impl;
let root = __golem_schema.push(golem_rust::schema::wit::wire::SchemaTypeBody::RecordType(vec![]));
golem_rust::golem_agentic::golem::agent::common::AgentType {
type_name: #agent_trait_name.to_string(),
kind: golem_rust::golem_agentic::golem::agent::common::AgentTypeKind::#kind_value,
description: #high_level_description_ident.to_string(),
source_language: "rust".to_string(),
methods,
dependencies: vec![],
constructor,
mode: #mode_value,
http_mount: #http_options,
snapshotting: #snapshotting_value,
config,
schema: __golem_schema.finish(root),
}
}
},
remote_client,
})
}
fn extract_description(attrs: &[syn::Attribute]) -> Option<String> {
extract_meta(attrs, "description")
}
fn extract_prompt_hint(attrs: &[syn::Attribute]) -> Option<String> {
extract_meta(attrs, "prompt")
}
fn extract_meta(attrs: &[syn::Attribute], key: &str) -> Option<String> {
for attr in attrs {
if attr.path().is_ident(key)
&& let Ok(syn::Lit::Str(lit_str)) = attr.parse_args::<syn::Lit>()
{
return Some(lit_str.value());
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use quote::quote;
use test_r::test;
fn expand(attrs: proc_macro2::TokenStream) -> String {
expand_agent_definition(
attrs,
quote! {
trait CounterAgent {
fn new(id: String) -> Self;
fn increment(&mut self) -> u32;
}
},
AgentDefinitionKind::Regular,
)
.to_string()
}
#[test]
fn ephemeral_agent_with_enabled_snapshotting_is_rejected() {
for attrs in [
quote! { ephemeral, snapshotting = "enabled" },
quote! { mode = "ephemeral", snapshotting = "periodic(5s)" },
quote! { snapshotting = "every(3)", ephemeral },
] {
let expanded = expand(attrs.clone());
assert!(
expanded.contains("compile_error")
&& expanded.contains("agent `CounterAgent` is ephemeral"),
"{attrs}: {expanded}"
);
}
}
#[test]
fn ephemeral_agent_without_snapshotting_is_accepted() {
for attrs in [
quote! { ephemeral },
quote! { ephemeral, snapshotting = "disabled" },
quote! { snapshotting = "enabled" },
] {
let expanded = expand(attrs.clone());
assert!(!expanded.contains("compile_error"), "{attrs}: {expanded}");
}
}
}