From ec791fa54a0852271d384446a340af36d5161e66 Mon Sep 17 00:00:00 2001 From: Daniel Vigovszky Date: Wed, 6 May 2026 19:45:06 +0200 Subject: [PATCH] Compile-time error when forgetting the agent_implementation annotation, and fix for colliding import aliases --- sdks/rust/AGENTS.md | 5 + sdks/rust/Cargo.lock | 101 ++++++++- sdks/rust/README.md | 9 + .../src/agentic/agent_definition_impl.rs | 23 +++ .../src/agentic/agent_implementation_impl.rs | 82 +++++--- .../golem-rust-macro/src/agentic/helpers.rs | 10 - sdks/rust/golem-rust/Cargo.toml | 5 + sdks/rust/golem-rust/tests/agent.rs | 49 +++++ sdks/rust/golem-rust/tests/ui.rs | 195 ++++++++++++++++++ .../ui/fail/aliased_import_plain_impl.rs | 27 +++ .../ui/fail/aliased_import_plain_impl.stderr | 8 + .../tests/ui/fail/generic_plain_impl.rs | 31 +++ .../tests/ui/fail/generic_plain_impl.stderr | 10 + .../tests/ui/fail/multiple_plain_impls.rs | 32 +++ .../tests/ui/fail/multiple_plain_impls.stderr | 17 ++ .../tests/ui/fail/plain_import_plain_impl.rs | 27 +++ .../ui/fail/plain_import_plain_impl.stderr | 8 + .../tests/ui/fail/reexport_plain_impl.rs | 29 +++ .../tests/ui/fail/reexport_plain_impl.stderr | 8 + .../tests/ui/fail/same_crate_plain_impl.rs | 23 +++ .../ui/fail/same_crate_plain_impl.stderr | 8 + .../aliased_import_agent_implementation.rs | 28 +++ .../ui/pass/generic_agent_implementation.rs | 26 +++ .../ui/pass/multiple_agent_implementations.rs | 45 ++++ .../pass/plain_import_agent_implementation.rs | 28 +++ .../ui/pass/reexport_agent_implementation.rs | 32 +++ .../pass/same_crate_agent_implementation.rs | 24 +++ 27 files changed, 842 insertions(+), 48 deletions(-) create mode 100644 sdks/rust/golem-rust/tests/ui.rs create mode 100644 sdks/rust/golem-rust/tests/ui/fail/aliased_import_plain_impl.rs create mode 100644 sdks/rust/golem-rust/tests/ui/fail/aliased_import_plain_impl.stderr create mode 100644 sdks/rust/golem-rust/tests/ui/fail/generic_plain_impl.rs create mode 100644 sdks/rust/golem-rust/tests/ui/fail/generic_plain_impl.stderr create mode 100644 sdks/rust/golem-rust/tests/ui/fail/multiple_plain_impls.rs create mode 100644 sdks/rust/golem-rust/tests/ui/fail/multiple_plain_impls.stderr create mode 100644 sdks/rust/golem-rust/tests/ui/fail/plain_import_plain_impl.rs create mode 100644 sdks/rust/golem-rust/tests/ui/fail/plain_import_plain_impl.stderr create mode 100644 sdks/rust/golem-rust/tests/ui/fail/reexport_plain_impl.rs create mode 100644 sdks/rust/golem-rust/tests/ui/fail/reexport_plain_impl.stderr create mode 100644 sdks/rust/golem-rust/tests/ui/fail/same_crate_plain_impl.rs create mode 100644 sdks/rust/golem-rust/tests/ui/fail/same_crate_plain_impl.stderr create mode 100644 sdks/rust/golem-rust/tests/ui/pass/aliased_import_agent_implementation.rs create mode 100644 sdks/rust/golem-rust/tests/ui/pass/generic_agent_implementation.rs create mode 100644 sdks/rust/golem-rust/tests/ui/pass/multiple_agent_implementations.rs create mode 100644 sdks/rust/golem-rust/tests/ui/pass/plain_import_agent_implementation.rs create mode 100644 sdks/rust/golem-rust/tests/ui/pass/reexport_agent_implementation.rs create mode 100644 sdks/rust/golem-rust/tests/ui/pass/same_crate_agent_implementation.rs diff --git a/sdks/rust/AGENTS.md b/sdks/rust/AGENTS.md index a4eb4a79fe..389fcb4e29 100644 --- a/sdks/rust/AGENTS.md +++ b/sdks/rust/AGENTS.md @@ -124,6 +124,11 @@ impl MyAgent for MyAgentImpl { } ``` +Every implementation of an `#[agent_definition]` trait must be annotated with +`#[agent_implementation]`, including test or mock implementations. If the +attribute is forgotten, the compiler reports a missing hidden trait item named +`agent_implementation_annotation`; add `#[agent_implementation]` to the impl. + ## Integration with Main Repository This SDK is part of the main Golem repository but is **not built by `cargo make build`**. When changes affect core functionality, test with the full Golem test suite: diff --git a/sdks/rust/Cargo.lock b/sdks/rust/Cargo.lock index caf9067fcb..b8c9f7e01f 100644 --- a/sdks/rust/Cargo.lock +++ b/sdks/rust/Cargo.lock @@ -621,6 +621,12 @@ dependencies = [ "wasip2", ] +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "golem-rust" version = "0.0.0" @@ -643,6 +649,7 @@ dependencies = [ "serde", "serde_json", "test-r", + "trybuild", "unicode-segmentation", "url", "uuid", @@ -1510,6 +1517,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "sha1_smol" version = "1.0.1" @@ -1620,6 +1636,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "target-triple" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" + [[package]] name = "tempfile" version = "3.24.0" @@ -1633,6 +1655,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "test-r" version = "2.3.1" @@ -1737,6 +1768,21 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.2", +] + [[package]] name = "toml_datetime" version = "0.7.5+spec-1.1.0" @@ -1746,6 +1792,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.23.10+spec-1.0.0" @@ -1753,26 +1808,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ "indexmap", - "toml_datetime", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", - "winnow", + "winnow 0.7.14", ] [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow", + "winnow 1.0.2", ] +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + [[package]] name = "topological-sort" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" +[[package]] +name = "trybuild" +version = "1.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c635f0191bd3a2941013e5062667100969f8c4e9cd787c14f977265d73616e" +dependencies = [ + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml", +] + [[package]] name = "unarray" version = "0.1.4" @@ -2063,6 +2139,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -2293,6 +2378,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" + [[package]] name = "wit-bindgen" version = "0.24.0" diff --git a/sdks/rust/README.md b/sdks/rust/README.md index 722af750f7..c8c387272a 100644 --- a/sdks/rust/README.md +++ b/sdks/rust/README.md @@ -10,3 +10,12 @@ the [transaction API](https://learn.golem.cloud/docs/transaction-api). ## golem-rust-macro The `golem-rust-macro` crate contains Rust derivation macros for working with Golem's `ValueAndType` types. + +## Agent implementations + +Traits annotated with `#[agent_definition]` must be implemented with +`#[agent_implementation]`. A plain `impl AgentTrait for Type` now fails during +`cargo check` with a missing hidden item named +`agent_implementation_annotation`, which points to the forgotten annotation. +The post-build `discover-agent-types` check remains the fallback for detecting +agent definitions that have no implementation anywhere. diff --git a/sdks/rust/golem-rust-macro/src/agentic/agent_definition_impl.rs b/sdks/rust/golem-rust-macro/src/agentic/agent_definition_impl.rs index 5e0a80b5de..f42c59a0a9 100644 --- a/sdks/rust/golem-rust-macro/src/agentic/agent_definition_impl.rs +++ b/sdks/rust/golem-rust-macro/src/agentic/agent_definition_impl.rs @@ -105,10 +105,17 @@ pub fn agent_definition_impl(attrs: TokenStream, item: TokenStream) -> TokenStre let load_snapshot_item = get_load_snapshot_item(); let save_snapshot_item = get_save_snapshot_item(); + let agent_type_name_item = + get_agent_type_name_item(&agent_definition_trait.ident.to_string()); + 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(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); @@ -141,6 +148,22 @@ fn get_save_snapshot_item() -> syn::TraitItem { } } +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, diff --git a/sdks/rust/golem-rust-macro/src/agentic/agent_implementation_impl.rs b/sdks/rust/golem-rust-macro/src/agentic/agent_implementation_impl.rs index 718c854239..53e7393b77 100644 --- a/sdks/rust/golem-rust-macro/src/agentic/agent_implementation_impl.rs +++ b/sdks/rust/golem-rust-macro/src/agentic/agent_implementation_impl.rs @@ -18,7 +18,7 @@ use syn::ItemImpl; use crate::agentic::helpers::{ AgentConfigAttrRemover, Asyncness, FunctionOutputInfo, get_asyncness, has_agent_config_attr, - has_async_trait_attr, is_constructor_method, is_static_method, trim_type_parameter, + has_async_trait_attr, is_constructor_method, is_static_method, }; use syn::visit_mut::VisitMut; @@ -43,10 +43,12 @@ pub fn agent_implementation_impl(_attrs: TokenStream, item: TokenStream) -> Toke let self_ty = &impl_block.self_ty; - let (trait_name_ident, trait_name_str_raw) = extract_trait_name(&impl_block); + let (trait_name_ident, trait_path) = extract_trait(&impl_block); + let agent_type_name = quote! { + <#self_ty as #trait_path>::__golem_agent_type_name() + }; - let (match_arms, constructor_method) = - build_match_arms(&impl_block, trait_name_str_raw.to_string()); + let (match_arms, constructor_method) = build_match_arms(&impl_block, agent_type_name.clone()); let constructor_method = match constructor_method { Some(m) => m, @@ -98,7 +100,7 @@ pub fn agent_implementation_impl(_attrs: TokenStream, item: TokenStream) -> Toke let base_agent_impl = generate_base_agent_impl( &impl_block, &match_arms, - &trait_name_str_raw, + agent_type_name.clone(), &impl_generics, &ty_generics, where_clause, @@ -133,7 +135,7 @@ pub fn agent_implementation_impl(_attrs: TokenStream, item: TokenStream) -> Toke let constructor_param_extraction = generate_constructor_extraction( &ctor_param_idents_and_types, - &trait_name_str_raw, + agent_type_name.clone(), constructor_param_extraction_call_back, ); @@ -142,10 +144,17 @@ pub fn agent_implementation_impl(_attrs: TokenStream, item: TokenStream) -> Toke let base_initiator_impl = generate_initiator_impl(&initiator_ident, &constructor_param_extraction); - let register_initiator_fn = - generate_register_initiator_fn(&impl_block.self_ty, &trait_name_ident, &initiator_ident); + let register_initiator_fn = generate_register_initiator_fn( + &impl_block.self_ty, + &trait_path, + &trait_name_ident, + &initiator_ident, + ); AgentConfigAttrRemover.visit_item_impl_mut(&mut impl_block); + impl_block + .items + .push(get_agent_implementation_annotation_item()); quote! { #impl_block @@ -160,15 +169,22 @@ fn parse_impl_block(item: &TokenStream) -> syn::Result { syn::parse::(item.clone()) } -fn extract_trait_name(impl_block: &syn::ItemImpl) -> (syn::Ident, String) { - let trait_name = if let Some((_bang, path, _for_token)) = &impl_block.trait_ { - path.segments.last().unwrap().ident.clone() +fn get_agent_implementation_annotation_item() -> syn::ImplItem { + syn::parse_quote! { + #[doc(hidden)] + fn agent_implementation_annotation() where Self: Sized {} + } +} + +fn extract_trait(impl_block: &syn::ItemImpl) -> (syn::Ident, syn::Path) { + let trait_path = if let Some((_bang, path, _for_token)) = &impl_block.trait_ { + path.clone() } else { panic!("Expected trait implementation, found none"); }; - let trait_name_str_raw = trait_name.to_string(); - (trait_name, trait_name_str_raw) + let trait_name = trait_path.segments.last().unwrap().ident.clone(); + (trait_name, trait_path) } // This will include all auto injected parameters too @@ -193,7 +209,7 @@ fn extract_param_idents(method: &syn::ImplItemFn) -> Vec<(syn::Ident, syn::PatTy fn build_match_arms( impl_block: &ItemImpl, - agent_type_name: String, + agent_type_name: proc_macro2::TokenStream, ) -> (Vec, Option<&syn::ImplItemFn>) { let mut match_arms = Vec::new(); let mut constructor_method = None; @@ -317,7 +333,7 @@ fn build_match_arms( let method_param_extraction = generate_method_param_extraction( param_idents, - &agent_type_name, + agent_type_name.clone(), method_name.as_str(), sorted_method_index, post_method_param_extraction_logic, @@ -335,21 +351,22 @@ fn build_match_arms( fn generate_method_param_extraction( param_idents: &[syn::Ident], - agent_type_name: &str, + agent_type_name: proc_macro2::TokenStream, method_name: &str, sorted_method_index: usize, post_method_param_extraction_logic: proc_macro2::TokenStream, ) -> proc_macro2::TokenStream { let input_param_index_init = quote! { let mut input_param_index: usize = 0; - let __agent_type_name = golem_rust::agentic::AgentTypeName(#agent_type_name.to_string()); + let __agent_type_name_raw = #agent_type_name; + let __agent_type_name = golem_rust::agentic::AgentTypeName(__agent_type_name_raw.to_string()); let __param_schemas = golem_rust::agentic::get_method_parameter_types_by_index( &__agent_type_name, #sorted_method_index ).ok_or_else(|| { golem_rust::agentic::custom_error(format!( "Internal Error: Parameter schemas not found for agent: {}, method index: {}", - #agent_type_name, #sorted_method_index + __agent_type_name_raw, #sorted_method_index )) })?; }; @@ -364,7 +381,7 @@ fn generate_method_param_extraction( .ok_or_else(|| { golem_rust::agentic::custom_error(format!( "Internal Error: Parameter schema not found for agent: {}, method: {}, parameter index: {}", - #agent_type_name, #method_name, #original_method_param_idx + __agent_type_name_raw, #method_name, #original_method_param_idx )) })?; @@ -434,7 +451,7 @@ fn generate_method_param_extraction( fn generate_base_agent_impl( impl_block: &syn::ItemImpl, match_arms: &[proc_macro2::TokenStream], - trait_name_str: &str, + agent_type_name: proc_macro2::TokenStream, impl_generics: &syn::ImplGenerics<'_>, ty_generics: &syn::TypeGenerics<'_>, where_clause: Option<&syn::WhereClause>, @@ -489,7 +506,8 @@ fn generate_base_agent_impl( fn get_definition(&self) -> golem_rust::golem_agentic::golem::agent::common::AgentType { - golem_rust::agentic::get_agent_type_by_name(&golem_rust::agentic::AgentTypeName(#trait_name_str.to_string())) + let __agent_type_name = #agent_type_name; + golem_rust::agentic::get_agent_type_by_name(&golem_rust::agentic::AgentTypeName(__agent_type_name.to_string())) .expect("Agent definition not found") } @@ -500,7 +518,7 @@ fn generate_base_agent_impl( fn generate_constructor_extraction( ctor_params: &[(syn::Ident, syn::PatType)], - agent_type_name: &str, + agent_type_name: proc_macro2::TokenStream, call_back: proc_macro2::TokenStream, ) -> proc_macro2::TokenStream { let mut config_extractions = Vec::new(); @@ -527,7 +545,7 @@ fn generate_constructor_extraction( .ok_or_else(|| { golem_rust::agentic::internal_error(format!( "Constructor parameter schema not found for agent: {}, parameter index: {}", - #agent_type_name, #idx + __agent_type_name_raw, #idx )) })?; @@ -545,10 +563,10 @@ fn generate_constructor_extraction( golem_rust::agentic::EnrichedElementSchema::ElementSchema(element_schema) => { let element_value = if input_param_index < values.len() { values[input_param_index].take().ok_or_else(|| { - golem_rust::agentic::invalid_input_error(format!("Constructor argument already consumed for agent {}", #agent_type_name)) + golem_rust::agentic::invalid_input_error(format!("Constructor argument already consumed for agent {}", __agent_type_name_raw)) })? } else { - return Err(golem_rust::agentic::invalid_input_error(format!("Missing constructor arguments for agent {}", #agent_type_name))); + return Err(golem_rust::agentic::invalid_input_error(format!("Missing constructor arguments for agent {}", __agent_type_name_raw))); }; input_param_index += 1; @@ -565,7 +583,8 @@ fn generate_constructor_extraction( } quote! { - let __agent_type_name = golem_rust::agentic::AgentTypeName(#agent_type_name.to_string()); + let __agent_type_name_raw = #agent_type_name; + let __agent_type_name = golem_rust::agentic::AgentTypeName(__agent_type_name_raw.to_string()); #(#config_extractions)* #(#predecls)* @@ -580,7 +599,7 @@ fn generate_constructor_extraction( ).ok_or_else(|| { golem_rust::agentic::internal_error(format!( "Constructor parameter schemas not found for agent: {}", - #agent_type_name + __agent_type_name_raw )) })?; @@ -614,13 +633,10 @@ fn generate_initiator_impl( fn generate_register_initiator_fn( self_ty: &syn::Type, + agent_trait_path: &syn::Path, agent_trait_ident: &syn::Ident, initiator_ident: &syn::Ident, ) -> proc_macro2::TokenStream { - let agent_impl_type_trimmed = trim_type_parameter(self_ty); - let agent_impl_type_trimmed_ident = format_ident!("{}", agent_impl_type_trimmed); - let agent_trait_name = agent_trait_ident.to_string(); - let register_initiator_fn_name = format_ident!( "__register_agent_initiator_{}", agent_trait_ident.to_string().to_lowercase() @@ -632,10 +648,10 @@ fn generate_register_initiator_fn( quote! { ::golem_rust::ctor::__support::ctor_parse!( #[ctor] fn #register_initiator_fn_name() { - #agent_impl_type_trimmed_ident::__register_agent_type(); + <#self_ty as #agent_trait_path>::__register_agent_type(); golem_rust::agentic::register_agent_initiator( - &#agent_trait_name, + <#self_ty as #agent_trait_path>::__golem_agent_type_name(), std::sync::Arc::new(#initiator_ident) ); } diff --git a/sdks/rust/golem-rust-macro/src/agentic/helpers.rs b/sdks/rust/golem-rust-macro/src/agentic/helpers.rs index 157c8a07a5..5f0b304a70 100644 --- a/sdks/rust/golem-rust-macro/src/agentic/helpers.rs +++ b/sdks/rust/golem-rust-macro/src/agentic/helpers.rs @@ -66,16 +66,6 @@ pub fn is_static_method(sig: &syn::Signature) -> bool { sig.receiver().is_none() } -pub fn trim_type_parameter(self_ty: &syn::Type) -> String { - match self_ty { - syn::Type::Path(type_path) => { - let ident = &type_path.path.segments.last().unwrap().ident; - ident.to_string() - } - _ => String::new(), - } -} - pub fn get_asyncness(sig: &syn::Signature) -> Asyncness { if sig.asyncness.is_some() { Asyncness::Future diff --git a/sdks/rust/golem-rust/Cargo.toml b/sdks/rust/golem-rust/Cargo.toml index 051af3fd2c..f6dfd889af 100644 --- a/sdks/rust/golem-rust/Cargo.toml +++ b/sdks/rust/golem-rust/Cargo.toml @@ -16,6 +16,10 @@ harness = false name = "agent" harness = false +[[test]] +name = "ui" +harness = false + [[test]] name = "agent_registry_bench" path = "benches/agent_registry_bench.rs" @@ -81,3 +85,4 @@ url = ["dep:url"] [dev-dependencies] test-r = "2.3.1" proptest = "1.4.0" +trybuild = "1.0.116" diff --git a/sdks/rust/golem-rust/tests/agent.rs b/sdks/rust/golem-rust/tests/agent.rs index 10ced82523..fa7fd8cba2 100644 --- a/sdks/rust/golem-rust/tests/agent.rs +++ b/sdks/rust/golem-rust/tests/agent.rs @@ -17,6 +17,7 @@ test_r::enable!(); #[cfg(test)] #[cfg(feature = "export_golem_agentic")] #[test_r::sequential] +#[allow(clippy::disallowed_names)] mod tests { use golem_rust::agentic::{ AgentTypeName, Multimodal, MultimodalAdvanced, MultimodalCustom, Schema, @@ -531,6 +532,33 @@ mod tests { } } + mod aliased_agent_api { + use super::agent_definition; + + #[agent_definition] + pub trait CanonicallyNamedAgent { + fn new(id: String) -> Self; + fn ping(&self) -> String; + } + } + + use aliased_agent_api::CanonicallyNamedAgent as RenamedAgent; + + struct AliasedAgentImplementation { + id: String, + } + + #[agent_implementation] + impl RenamedAgent for AliasedAgentImplementation { + fn new(id: String) -> Self { + Self { id } + } + + fn ping(&self) -> String { + self.id.clone() + } + } + #[derive(Schema, MultimodalSchema)] enum TextOrImage { Text(String), @@ -548,6 +576,26 @@ mod tests { assert!(true); } + #[test] + fn test_aliased_agent_implementation_uses_canonical_agent_name() { + use golem_rust::agentic::{get_agent_type_by_name, with_agent_initiator}; + + AliasedAgentImplementation::__register_agent_type(); + + let canonical_name = AgentTypeName("CanonicallyNamedAgent".to_string()); + let alias_name = AgentTypeName("RenamedAgent".to_string()); + + let agent = AliasedAgentImplementation::new("id".to_string()); + + assert!(get_agent_type_by_name(&canonical_name).is_some()); + assert!(get_agent_type_by_name(&alias_name).is_none()); + assert_eq!(agent.get_definition().type_name, canonical_name.0); + + let initiator_registered_under_canonical_name = + with_agent_initiator(|_| async { true }, &canonical_name); + assert!(initiator_registered_under_canonical_name); + } + #[agent_definition] #[description("a descriptive agent")] pub trait DescriptiveAgent { @@ -562,6 +610,7 @@ mod tests { struct DescriptiveAgentImpl {} + #[agent_implementation] impl DescriptiveAgent for DescriptiveAgentImpl { fn new(_name: String) -> Self { DescriptiveAgentImpl {} diff --git a/sdks/rust/golem-rust/tests/ui.rs b/sdks/rust/golem-rust/tests/ui.rs new file mode 100644 index 0000000000..28f963aef0 --- /dev/null +++ b/sdks/rust/golem-rust/tests/ui.rs @@ -0,0 +1,195 @@ +// Copyright 2024-2026 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +test_r::enable!(); + +#[cfg(feature = "export_golem_agentic")] +mod tests { + use std::fs; + use std::path::{Path, PathBuf}; + use std::process::Command; + use test_r::test; + + #[test] + fn agent_implementation_annotation_ui_tests() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/ui/fail/*.rs"); + t.pass("tests/ui/pass/*.rs"); + } + + #[test] + fn agent_definition_implementation_and_client_can_live_in_separate_crates() { + let workspace = create_cross_crate_workspace(); + let target_dir = Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("golem-rust crate should have an SDK workspace parent") + .join("target"); + + let output = Command::new("cargo") + .arg("check") + .arg("--workspace") + .arg("--quiet") + .env("CARGO_TARGET_DIR", target_dir) + .current_dir(&workspace) + .output() + .expect("failed to run cargo check for cross-crate agent workspace"); + + fs::remove_dir_all(&workspace).unwrap_or_else(|error| { + panic!( + "failed to remove temporary workspace {}: {error}", + workspace.display() + ) + }); + + if !output.status.success() { + panic!( + "cross-crate agent workspace failed to compile\nstatus: {}\nstdout:\n{}\nstderr:\n{}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + } + } + + fn create_cross_crate_workspace() -> PathBuf { + let root = std::env::temp_dir().join(format!( + "golem-rust-agent-ui-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after UNIX_EPOCH") + .as_nanos() + )); + + fs::create_dir_all(root.join("agent-api/src")).unwrap(); + fs::create_dir_all(root.join("agent-impl/src")).unwrap(); + fs::create_dir_all(root.join("agent-client/src")).unwrap(); + + let golem_rust_path = Path::new(env!("CARGO_MANIFEST_DIR")); + + fs::write( + root.join("Cargo.toml"), + r#" +[workspace] +resolver = "2" +members = ["agent-api", "agent-impl", "agent-client"] +"#, + ) + .unwrap(); + + write_crate_manifest( + &root, + "agent-api", + &format!( + r#" +[dependencies] +golem-rust = {{ path = {}, features = ["export_golem_agentic"] }} +"#, + toml_string(golem_rust_path) + ), + ); + fs::write( + root.join("agent-api/src/lib.rs"), + r#" +use golem_rust::agent_definition; + +#[agent_definition] +pub trait CrossCrateAgent { + fn new(id: String) -> Self; + fn ping(&self) -> String; +} +"#, + ) + .unwrap(); + + write_crate_manifest( + &root, + "agent-impl", + &format!( + r#" +[dependencies] +agent-api = {{ path = "../agent-api" }} +golem-rust = {{ path = {}, features = ["export_golem_agentic"] }} +"#, + toml_string(golem_rust_path) + ), + ); + fs::write( + root.join("agent-impl/src/lib.rs"), + r#" +use agent_api::CrossCrateAgent; +use golem_rust::agent_implementation; + +pub struct CrossCrateAgentImpl { + id: String, +} + +#[agent_implementation] +impl CrossCrateAgent for CrossCrateAgentImpl { + fn new(id: String) -> Self { + Self { id } + } + + fn ping(&self) -> String { + self.id.clone() + } +} +"#, + ) + .unwrap(); + + write_crate_manifest( + &root, + "agent-client", + r#" +[dependencies] +agent-api = { path = "../agent-api" } +"#, + ); + fs::write( + root.join("agent-client/src/lib.rs"), + r#" +use agent_api::CrossCrateAgentClient; + +pub fn generated_client_type_is_available() -> usize { + std::mem::size_of::() +} +"#, + ) + .unwrap(); + + root + } + + fn write_crate_manifest(root: &Path, name: &str, dependencies: &str) { + fs::write( + root.join(name).join("Cargo.toml"), + format!( + r#" +[package] +name = "{name}" +version = "0.0.0" +edition = "2024" + +{dependencies} +"# + ), + ) + .unwrap(); + } + + fn toml_string(path: &Path) -> String { + format!("{:?}", path.display().to_string()) + } +} diff --git a/sdks/rust/golem-rust/tests/ui/fail/aliased_import_plain_impl.rs b/sdks/rust/golem-rust/tests/ui/fail/aliased_import_plain_impl.rs new file mode 100644 index 0000000000..8201e51a07 --- /dev/null +++ b/sdks/rust/golem-rust/tests/ui/fail/aliased_import_plain_impl.rs @@ -0,0 +1,27 @@ +use golem_rust::agent_definition; + +mod api { + use super::agent_definition; + + #[agent_definition] + pub trait AliasedAgent { + fn new(id: String) -> Self; + fn ping(&self) -> String; + } +} + +use api::AliasedAgent as AgentAlias; + +struct AliasedAgentImpl; + +impl AgentAlias for AliasedAgentImpl { + fn new(_id: String) -> Self { + Self + } + + fn ping(&self) -> String { + "ok".to_string() + } +} + +fn main() {} diff --git a/sdks/rust/golem-rust/tests/ui/fail/aliased_import_plain_impl.stderr b/sdks/rust/golem-rust/tests/ui/fail/aliased_import_plain_impl.stderr new file mode 100644 index 0000000000..accec38355 --- /dev/null +++ b/sdks/rust/golem-rust/tests/ui/fail/aliased_import_plain_impl.stderr @@ -0,0 +1,8 @@ +error[E0046]: not all trait items implemented, missing: `agent_implementation_annotation` + --> tests/ui/fail/aliased_import_plain_impl.rs:17:1 + | + 6 | #[agent_definition] + | ------------------- `agent_implementation_annotation` from trait +... +17 | impl AgentAlias for AliasedAgentImpl { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `agent_implementation_annotation` in implementation diff --git a/sdks/rust/golem-rust/tests/ui/fail/generic_plain_impl.rs b/sdks/rust/golem-rust/tests/ui/fail/generic_plain_impl.rs new file mode 100644 index 0000000000..25851fa00b --- /dev/null +++ b/sdks/rust/golem-rust/tests/ui/fail/generic_plain_impl.rs @@ -0,0 +1,31 @@ +use golem_rust::agent_definition; +use std::marker::PhantomData; + +#[agent_definition] +trait GenericAgent { + fn new(id: String) -> Self; + fn len(&self) -> usize; +} + +struct GenericAgentImpl<'a, T, const N: usize> { + id: String, + _marker: PhantomData<&'a [T; N]>, +} + +impl<'a, T, const N: usize> GenericAgent for GenericAgentImpl<'a, T, N> +where + T: Clone, +{ + fn new(id: String) -> Self { + Self { + id, + _marker: PhantomData, + } + } + + fn len(&self) -> usize { + self.id.len() + N + } +} + +fn main() {} diff --git a/sdks/rust/golem-rust/tests/ui/fail/generic_plain_impl.stderr b/sdks/rust/golem-rust/tests/ui/fail/generic_plain_impl.stderr new file mode 100644 index 0000000000..aae596a22f --- /dev/null +++ b/sdks/rust/golem-rust/tests/ui/fail/generic_plain_impl.stderr @@ -0,0 +1,10 @@ +error[E0046]: not all trait items implemented, missing: `agent_implementation_annotation` + --> tests/ui/fail/generic_plain_impl.rs:15:1 + | + 4 | #[agent_definition] + | ------------------- `agent_implementation_annotation` from trait +... +15 | / impl<'a, T, const N: usize> GenericAgent for GenericAgentImpl<'a, T, N> +16 | | where +17 | | T: Clone, + | |_____________^ missing `agent_implementation_annotation` in implementation diff --git a/sdks/rust/golem-rust/tests/ui/fail/multiple_plain_impls.rs b/sdks/rust/golem-rust/tests/ui/fail/multiple_plain_impls.rs new file mode 100644 index 0000000000..fc33d4f867 --- /dev/null +++ b/sdks/rust/golem-rust/tests/ui/fail/multiple_plain_impls.rs @@ -0,0 +1,32 @@ +use golem_rust::agent_definition; + +#[agent_definition] +trait MultiAgent { + fn new(id: String) -> Self; + fn ping(&self) -> String; +} + +struct FirstImpl; +struct SecondImpl; + +impl MultiAgent for FirstImpl { + fn new(_id: String) -> Self { + Self + } + + fn ping(&self) -> String { + "first".to_string() + } +} + +impl MultiAgent for SecondImpl { + fn new(_id: String) -> Self { + Self + } + + fn ping(&self) -> String { + "second".to_string() + } +} + +fn main() {} diff --git a/sdks/rust/golem-rust/tests/ui/fail/multiple_plain_impls.stderr b/sdks/rust/golem-rust/tests/ui/fail/multiple_plain_impls.stderr new file mode 100644 index 0000000000..6b91ad3db1 --- /dev/null +++ b/sdks/rust/golem-rust/tests/ui/fail/multiple_plain_impls.stderr @@ -0,0 +1,17 @@ +error[E0046]: not all trait items implemented, missing: `agent_implementation_annotation` + --> tests/ui/fail/multiple_plain_impls.rs:12:1 + | + 3 | #[agent_definition] + | ------------------- `agent_implementation_annotation` from trait +... +12 | impl MultiAgent for FirstImpl { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `agent_implementation_annotation` in implementation + +error[E0046]: not all trait items implemented, missing: `agent_implementation_annotation` + --> tests/ui/fail/multiple_plain_impls.rs:22:1 + | + 3 | #[agent_definition] + | ------------------- `agent_implementation_annotation` from trait +... +22 | impl MultiAgent for SecondImpl { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `agent_implementation_annotation` in implementation diff --git a/sdks/rust/golem-rust/tests/ui/fail/plain_import_plain_impl.rs b/sdks/rust/golem-rust/tests/ui/fail/plain_import_plain_impl.rs new file mode 100644 index 0000000000..7a60e4360c --- /dev/null +++ b/sdks/rust/golem-rust/tests/ui/fail/plain_import_plain_impl.rs @@ -0,0 +1,27 @@ +use golem_rust::agent_definition; + +mod api { + use super::agent_definition; + + #[agent_definition] + pub trait ImportedAgent { + fn new(id: String) -> Self; + fn ping(&self) -> String; + } +} + +use api::ImportedAgent; + +struct ImportedAgentImpl; + +impl ImportedAgent for ImportedAgentImpl { + fn new(_id: String) -> Self { + Self + } + + fn ping(&self) -> String { + "ok".to_string() + } +} + +fn main() {} diff --git a/sdks/rust/golem-rust/tests/ui/fail/plain_import_plain_impl.stderr b/sdks/rust/golem-rust/tests/ui/fail/plain_import_plain_impl.stderr new file mode 100644 index 0000000000..9b6259ab67 --- /dev/null +++ b/sdks/rust/golem-rust/tests/ui/fail/plain_import_plain_impl.stderr @@ -0,0 +1,8 @@ +error[E0046]: not all trait items implemented, missing: `agent_implementation_annotation` + --> tests/ui/fail/plain_import_plain_impl.rs:17:1 + | + 6 | #[agent_definition] + | ------------------- `agent_implementation_annotation` from trait +... +17 | impl ImportedAgent for ImportedAgentImpl { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `agent_implementation_annotation` in implementation diff --git a/sdks/rust/golem-rust/tests/ui/fail/reexport_plain_impl.rs b/sdks/rust/golem-rust/tests/ui/fail/reexport_plain_impl.rs new file mode 100644 index 0000000000..ceb0436522 --- /dev/null +++ b/sdks/rust/golem-rust/tests/ui/fail/reexport_plain_impl.rs @@ -0,0 +1,29 @@ +use golem_rust::agent_definition; + +mod api { + use super::agent_definition; + + #[agent_definition] + pub trait ReexportedAgent { + fn new(id: String) -> Self; + fn ping(&self) -> String; + } +} + +mod prelude { + pub use super::api::ReexportedAgent; +} + +struct ReexportedAgentImpl; + +impl prelude::ReexportedAgent for ReexportedAgentImpl { + fn new(_id: String) -> Self { + Self + } + + fn ping(&self) -> String { + "ok".to_string() + } +} + +fn main() {} diff --git a/sdks/rust/golem-rust/tests/ui/fail/reexport_plain_impl.stderr b/sdks/rust/golem-rust/tests/ui/fail/reexport_plain_impl.stderr new file mode 100644 index 0000000000..9d9f9b73bf --- /dev/null +++ b/sdks/rust/golem-rust/tests/ui/fail/reexport_plain_impl.stderr @@ -0,0 +1,8 @@ +error[E0046]: not all trait items implemented, missing: `agent_implementation_annotation` + --> tests/ui/fail/reexport_plain_impl.rs:19:1 + | + 6 | #[agent_definition] + | ------------------- `agent_implementation_annotation` from trait +... +19 | impl prelude::ReexportedAgent for ReexportedAgentImpl { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `agent_implementation_annotation` in implementation diff --git a/sdks/rust/golem-rust/tests/ui/fail/same_crate_plain_impl.rs b/sdks/rust/golem-rust/tests/ui/fail/same_crate_plain_impl.rs new file mode 100644 index 0000000000..f85be8ad6a --- /dev/null +++ b/sdks/rust/golem-rust/tests/ui/fail/same_crate_plain_impl.rs @@ -0,0 +1,23 @@ +use golem_rust::agent_definition; + +#[agent_definition] +trait SameCrateAgent { + fn new(id: String) -> Self; + fn ping(&self) -> String; +} + +struct SameCrateAgentImpl { + id: String, +} + +impl SameCrateAgent for SameCrateAgentImpl { + fn new(id: String) -> Self { + Self { id } + } + + fn ping(&self) -> String { + self.id.clone() + } +} + +fn main() {} diff --git a/sdks/rust/golem-rust/tests/ui/fail/same_crate_plain_impl.stderr b/sdks/rust/golem-rust/tests/ui/fail/same_crate_plain_impl.stderr new file mode 100644 index 0000000000..5663288538 --- /dev/null +++ b/sdks/rust/golem-rust/tests/ui/fail/same_crate_plain_impl.stderr @@ -0,0 +1,8 @@ +error[E0046]: not all trait items implemented, missing: `agent_implementation_annotation` + --> tests/ui/fail/same_crate_plain_impl.rs:13:1 + | + 3 | #[agent_definition] + | ------------------- `agent_implementation_annotation` from trait +... +13 | impl SameCrateAgent for SameCrateAgentImpl { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `agent_implementation_annotation` in implementation diff --git a/sdks/rust/golem-rust/tests/ui/pass/aliased_import_agent_implementation.rs b/sdks/rust/golem-rust/tests/ui/pass/aliased_import_agent_implementation.rs new file mode 100644 index 0000000000..7603dfea57 --- /dev/null +++ b/sdks/rust/golem-rust/tests/ui/pass/aliased_import_agent_implementation.rs @@ -0,0 +1,28 @@ +use golem_rust::{agent_definition, agent_implementation}; + +mod api { + use super::agent_definition; + + #[agent_definition] + pub trait AliasedAgent { + fn new(id: String) -> Self; + fn ping(&self) -> String; + } +} + +use api::AliasedAgent as AgentAlias; + +struct AliasedAgentImpl; + +#[agent_implementation] +impl AgentAlias for AliasedAgentImpl { + fn new(_id: String) -> Self { + Self + } + + fn ping(&self) -> String { + "ok".to_string() + } +} + +fn main() {} diff --git a/sdks/rust/golem-rust/tests/ui/pass/generic_agent_implementation.rs b/sdks/rust/golem-rust/tests/ui/pass/generic_agent_implementation.rs new file mode 100644 index 0000000000..bb6949f862 --- /dev/null +++ b/sdks/rust/golem-rust/tests/ui/pass/generic_agent_implementation.rs @@ -0,0 +1,26 @@ +use golem_rust::agentic::Schema; +use golem_rust::{agent_definition, agent_implementation}; +use std::fmt::Debug; + +#[agent_definition] +trait GenericAgent { + fn new(id: String) -> Self; + fn len(&self) -> usize; +} + +struct GenericAgentImpl { + id: String, +} + +#[agent_implementation] +impl GenericAgent for GenericAgentImpl { + fn new(id: String) -> Self { + Self { id } + } + + fn len(&self) -> usize { + self.id.len() + } +} + +fn main() {} diff --git a/sdks/rust/golem-rust/tests/ui/pass/multiple_agent_implementations.rs b/sdks/rust/golem-rust/tests/ui/pass/multiple_agent_implementations.rs new file mode 100644 index 0000000000..00c6f61ad7 --- /dev/null +++ b/sdks/rust/golem-rust/tests/ui/pass/multiple_agent_implementations.rs @@ -0,0 +1,45 @@ +use golem_rust::agent_definition; + +#[agent_definition] +trait MultiAgent { + fn new(id: String) -> Self; + fn ping(&self) -> String; +} + +mod first { + use super::MultiAgent; + use golem_rust::agent_implementation; + + pub struct FirstImpl; + + #[agent_implementation] + impl MultiAgent for FirstImpl { + fn new(_id: String) -> Self { + Self + } + + fn ping(&self) -> String { + "first".to_string() + } + } +} + +mod second { + use super::MultiAgent; + use golem_rust::agent_implementation; + + pub struct SecondImpl; + + #[agent_implementation] + impl MultiAgent for SecondImpl { + fn new(_id: String) -> Self { + Self + } + + fn ping(&self) -> String { + "second".to_string() + } + } +} + +fn main() {} diff --git a/sdks/rust/golem-rust/tests/ui/pass/plain_import_agent_implementation.rs b/sdks/rust/golem-rust/tests/ui/pass/plain_import_agent_implementation.rs new file mode 100644 index 0000000000..489fe85171 --- /dev/null +++ b/sdks/rust/golem-rust/tests/ui/pass/plain_import_agent_implementation.rs @@ -0,0 +1,28 @@ +use golem_rust::{agent_definition, agent_implementation}; + +mod api { + use super::agent_definition; + + #[agent_definition] + pub trait ImportedAgent { + fn new(id: String) -> Self; + fn ping(&self) -> String; + } +} + +use api::ImportedAgent; + +struct ImportedAgentImpl; + +#[agent_implementation] +impl ImportedAgent for ImportedAgentImpl { + fn new(_id: String) -> Self { + Self + } + + fn ping(&self) -> String { + "ok".to_string() + } +} + +fn main() {} diff --git a/sdks/rust/golem-rust/tests/ui/pass/reexport_agent_implementation.rs b/sdks/rust/golem-rust/tests/ui/pass/reexport_agent_implementation.rs new file mode 100644 index 0000000000..5407c94321 --- /dev/null +++ b/sdks/rust/golem-rust/tests/ui/pass/reexport_agent_implementation.rs @@ -0,0 +1,32 @@ +use golem_rust::{agent_definition, agent_implementation}; + +mod api { + use super::agent_definition; + + #[agent_definition] + pub trait ReexportedAgent { + fn new(id: String) -> Self; + fn ping(&self) -> String; + } +} + +mod prelude { + pub use super::api::ReexportedAgent; +} + +use api::ReexportedAgent; + +struct ReexportedAgentImpl; + +#[agent_implementation] +impl prelude::ReexportedAgent for ReexportedAgentImpl { + fn new(_id: String) -> Self { + Self + } + + fn ping(&self) -> String { + "ok".to_string() + } +} + +fn main() {} diff --git a/sdks/rust/golem-rust/tests/ui/pass/same_crate_agent_implementation.rs b/sdks/rust/golem-rust/tests/ui/pass/same_crate_agent_implementation.rs new file mode 100644 index 0000000000..90989951cc --- /dev/null +++ b/sdks/rust/golem-rust/tests/ui/pass/same_crate_agent_implementation.rs @@ -0,0 +1,24 @@ +use golem_rust::{agent_definition, agent_implementation}; + +#[agent_definition] +trait SameCrateAgent { + fn new(id: String) -> Self; + fn ping(&self) -> String; +} + +struct SameCrateAgentImpl { + id: String, +} + +#[agent_implementation] +impl SameCrateAgent for SameCrateAgentImpl { + fn new(id: String) -> Self { + Self { id } + } + + fn ping(&self) -> String { + self.id.clone() + } +} + +fn main() {}