From d4ab4c7885d5b1c3ae5c29efd8b6e7cfc257d31f Mon Sep 17 00:00:00 2001 From: Christian Kirchner Date: Thu, 16 Apr 2026 21:21:20 +0200 Subject: [PATCH 1/2] feat(bridge-gen): custom derive rules and optional client feature --- cli/golem-cli/src/app/build/gen_bridge.rs | 35 ++++- cli/golem-cli/src/app/context.rs | 1 + cli/golem-cli/src/bridge_gen/mod.rs | 38 ++++- cli/golem-cli/src/bridge_gen/rust/mod.rs | 141 ++++++++++++++++-- .../src/bridge_gen/typescript/mod.rs | 9 +- cli/golem-cli/src/command.rs | 5 + cli/golem-cli/src/command_handler/bridge.rs | 25 ++++ cli/golem-cli/src/command_handler/mod.rs | 9 +- cli/golem-cli/src/model/app.rs | 4 +- cli/golem-cli/src/model/app_raw.rs | 26 ++++ cli/golem-cli/tests/bridge_gen/rust.rs | 3 +- 11 files changed, 274 insertions(+), 22 deletions(-) diff --git a/cli/golem-cli/src/app/build/gen_bridge.rs b/cli/golem-cli/src/app/build/gen_bridge.rs index ef71abd380..188ec21614 100644 --- a/cli/golem-cli/src/app/build/gen_bridge.rs +++ b/cli/golem-cli/src/app/build/gen_bridge.rs @@ -4,7 +4,9 @@ use crate::app::build::up_to_date_check::new_task_up_to_date_check; use crate::app::context::BuildContext; use crate::bridge_gen::rust::RustBridgeGenerator; use crate::bridge_gen::typescript::TypeScriptBridgeGenerator; -use crate::bridge_gen::{BridgeGenerator, bridge_client_directory_name}; +use crate::bridge_gen::{ + BridgeGenerator, BridgeGeneratorConfig, DeriveRule, bridge_client_directory_name, +}; use crate::command::GolemCliCommand; use crate::error::NonSuccessfulExit; use crate::fs; @@ -103,6 +105,19 @@ async fn collect_manifest_targets(ctx: &BuildContext<'_>) -> anyhow::Result = match target.target_language { GuestLanguage::Rust => Box::new(RustBridgeGenerator::new( target.agent_type, &output_dir, false, + config, )?), - GuestLanguage::TypeScript => Box::new(TypeScriptBridgeGenerator::new( - target.agent_type, - &output_dir, - false, - )?), + GuestLanguage::TypeScript => { + Box::new(::new( + target.agent_type, + &output_dir, + false, + config, + )?) + } GuestLanguage::Scala => { bail!("Bridge generation is not yet supported for Scala") } diff --git a/cli/golem-cli/src/app/context.rs b/cli/golem-cli/src/app/context.rs index adb2c5f738..acbb2f3915 100644 --- a/cli/golem-cli/src/app/context.rs +++ b/cli/golem-cli/src/app/context.rs @@ -235,6 +235,7 @@ impl ApplicationContext { agent_type_names: Default::default(), target_language: Some(language), output_dir: Some(repl_root_bridge_sdk_dir.clone()), + derive_rules: Vec::new(), } } diff --git a/cli/golem-cli/src/bridge_gen/mod.rs b/cli/golem-cli/src/bridge_gen/mod.rs index 599ab5c94c..b0bc6ffd79 100644 --- a/cli/golem-cli/src/bridge_gen/mod.rs +++ b/cli/golem-cli/src/bridge_gen/mod.rs @@ -21,8 +21,44 @@ use camino::Utf8Path; use golem_common::model::agent::{AgentType, AgentTypeName}; use heck::ToKebabCase; +/// A rule that adds derive macros to generated types whose names match a regex pattern. +/// +/// Multiple rules can match the same type; their derives are merged and deduplicated. +/// +/// # Examples +/// +/// Add `PartialEq` to all types: +/// ```yaml +/// { pattern: ".*", derives: ["PartialEq"] } +/// ``` +/// +/// Add `Eq` and `Hash` only to `Uuid`: +/// ```yaml +/// { pattern: "^Uuid$", derives: ["Eq", "Hash"] } +/// ``` +#[derive(Debug, Clone)] +pub struct DeriveRule { + /// Regex pattern matched against the generated type name. + pub pattern: String, + /// Derive macros to add when the pattern matches (e.g., "PartialEq", "Eq", "Hash"). + pub derives: Vec, +} + +/// Configuration options for bridge SDK code generation. +#[derive(Debug, Clone, Default)] +pub struct BridgeGeneratorConfig { + /// Rules for adding derive macros to generated types. Each rule pairs a regex + /// pattern (matched against type names) with a list of derives to add. + pub derive_rules: Vec, +} + pub trait BridgeGenerator { - fn new(agent_type: AgentType, target_path: &Utf8Path, testing: bool) -> anyhow::Result + fn new( + agent_type: AgentType, + target_path: &Utf8Path, + testing: bool, + config: BridgeGeneratorConfig, + ) -> anyhow::Result where Self: Sized; fn generate(&mut self) -> anyhow::Result<()>; diff --git a/cli/golem-cli/src/bridge_gen/rust/mod.rs b/cli/golem-cli/src/bridge_gen/rust/mod.rs index 304e2f51f6..17847b4848 100644 --- a/cli/golem-cli/src/bridge_gen/rust/mod.rs +++ b/cli/golem-cli/src/bridge_gen/rust/mod.rs @@ -14,7 +14,7 @@ use crate::bridge_gen::rust::rust::to_rust_ident; use crate::bridge_gen::type_naming::TypeNaming; -use crate::bridge_gen::{BridgeGenerator, bridge_client_directory_name}; +use crate::bridge_gen::{BridgeGenerator, BridgeGeneratorConfig, bridge_client_directory_name}; use crate::fs; use crate::sdk_overrides::{sdk_overrides, workspace_root}; use anyhow::anyhow; @@ -44,6 +44,7 @@ pub struct RustBridgeGenerator { agent_type: AgentType, testing: bool, same_language: bool, + config: BridgeGeneratorConfig, type_naming: TypeNaming, // TODO: we should integrate these names with type naming to avoid collisions @@ -53,7 +54,12 @@ pub struct RustBridgeGenerator { } impl BridgeGenerator for RustBridgeGenerator { - fn new(agent_type: AgentType, target_path: &Utf8Path, testing: bool) -> anyhow::Result { + fn new( + agent_type: AgentType, + target_path: &Utf8Path, + testing: bool, + config: BridgeGeneratorConfig, + ) -> anyhow::Result { let same_language = agent_type.source_language.eq_ignore_ascii_case("rust"); let type_naming = TypeNaming::new(&agent_type, same_language)?; @@ -62,6 +68,7 @@ impl BridgeGenerator for RustBridgeGenerator { agent_type, testing, same_language, + config, type_naming, generated_language_enums: BTreeMap::new(), @@ -91,6 +98,45 @@ impl BridgeGenerator for RustBridgeGenerator { } impl RustBridgeGenerator { + /// Returns derive attributes for a generated type, conditionally including serde + /// derives when the `serde` feature is enabled in the generated crate. + /// + /// Evaluates `BridgeGeneratorConfig::derive_rules` against the type name: each rule + /// whose regex pattern matches contributes its derives. All matching derives are + /// merged and deduplicated before emission. + fn base_derive_attrs(&self, type_name: &str) -> TokenStream { + let mut derive_set = Vec::::new(); + for rule in &self.config.derive_rules { + if let Ok(re) = regex::Regex::new(&rule.pattern) + && re.is_match(type_name) + { + for d in &rule.derives { + if !derive_set.contains(d) { + derive_set.push(d.clone()); + } + } + } + } + + let additional: Vec = derive_set + .iter() + .filter_map(|d| syn::parse_str::(d).ok()) + .map(|path| quote! { #path }) + .collect(); + + if additional.is_empty() { + quote! { + #[derive(Debug, Clone)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + } + } else { + quote! { + #[derive(Debug, Clone, #(#additional),*)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + } + } + } + /// Generates the Cargo.toml manifest file fn generate_cargo_toml(&self, path: &Utf8Path) -> anyhow::Result<()> { let golem_source = if self.testing { @@ -116,15 +162,66 @@ impl RustBridgeGenerator { doc["dependencies"] = Item::Table(Table::default()); doc["dependencies"]["chrono"] = dep("0.4", &[]); - doc["dependencies"]["golem-client"] = golem_source.dep_item("golem-client", &[])?; - doc["dependencies"]["golem-common"] = golem_source.dep_item("golem-common", &["client"])?; - doc["dependencies"]["golem-wasm"] = golem_source.dep_item("golem-wasm", &["client"])?; doc["dependencies"]["nonempty-collections"] = dep("0.3.1", &[]); doc["dependencies"]["reqwest"] = dep("0.13", &["rustls"]); doc["dependencies"]["reqwest-middleware"] = dep("0.5", &[]); doc["dependencies"]["serde_json"] = dep("1", &[]); doc["dependencies"]["uuid"] = dep("1.18.1", &["v4"]); + // Client-only deps (networking, Golem SDK) — optional, behind `client` feature + fn optional_dep(version: &str, features: &[&str]) -> Item { + let mut entry = Item::Table(Table::default()); + entry["version"] = value(version); + if !features.is_empty() { + let mut feature_items = Array::default(); + for feature in features { + feature_items.push(*feature); + } + entry["default-features"] = value(false); + entry["features"] = value(feature_items); + } + entry["optional"] = value(true); + entry + } + + doc["dependencies"]["golem-client"] = + golem_source.optional_dep_item("golem-client", &[])?; + doc["dependencies"]["golem-common"] = + golem_source.optional_dep_item("golem-common", &["client"])?; + doc["dependencies"]["golem-wasm"] = + golem_source.optional_dep_item("golem-wasm", &["client"])?; + doc["dependencies"]["reqwest"] = optional_dep("0.13", &["rustls"]); + doc["dependencies"]["reqwest-middleware"] = optional_dep("0.5", &[]); + + // Optional serde dependency for JSON serialization + { + let mut serde_entry = Item::Table(Table::default()); + serde_entry["version"] = value("1"); + let mut serde_features = Array::default(); + serde_features.push("derive"); + serde_entry["features"] = value(serde_features); + serde_entry["optional"] = value(true); + doc["dependencies"]["serde"] = serde_entry; + } + + // [features] section + doc["features"] = Item::Table(Table::default()); + let mut serde_feat = Array::default(); + serde_feat.push("dep:serde"); + doc["features"]["serde"] = value(serde_feat); + + let mut client_feat = Array::default(); + client_feat.push("dep:golem-client"); + client_feat.push("dep:golem-common"); + client_feat.push("dep:golem-wasm"); + client_feat.push("dep:reqwest"); + client_feat.push("dep:reqwest-middleware"); + doc["features"]["client"] = value(client_feat); + + let mut default_feat = Array::default(); + default_feat.push("client"); + doc["features"]["default"] = value(default_feat); + std::fs::write(path, doc.to_string()) .map_err(|e| anyhow!("Failed to write Cargo.toml file: {e}"))?; @@ -253,16 +350,21 @@ impl RustBridgeGenerator { let tokens = quote! { #![allow(unused)] + #[cfg(feature = "client")] use golem_common::base_model::agent::{UnstructuredBinaryExtensions, UnstructuredTextExtensions}; + #[cfg(feature = "client")] use golem_wasm::{FromValueAndType, IntoValueAndType}; + #[cfg(feature = "client")] #multimodal_import + #[cfg(feature = "client")] pub struct #client_struct_name { constructor_parameters: golem_client::model::UntypedJsonDataValue, phantom_id: Option, agent_id: golem_common::model::AgentId, } + #[cfg(feature = "client")] impl std::fmt::Debug for #client_struct_name { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct(stringify!(#client_struct_name)) @@ -273,6 +375,7 @@ impl RustBridgeGenerator { } } + #[cfg(feature = "client")] impl #client_struct_name { pub async fn get(#(#constructor_params),*) -> Result { #constructor_params_to_data_value @@ -542,8 +645,9 @@ impl RustBridgeGenerator { cases.push(quote! { #case_ident(#inner) }); // get_type() case — include the inner type - let type_value = self.analysed_type_as_value(typ); - case_type_tokens.push(quote! { Some(#type_value) }); + case_type_tokens.push( + quote! { Some(<#inner as golem_wasm::IntoValue>::get_type()) }, + ); // IntoValue implementation into_value_cases.push(quote! { @@ -583,12 +687,14 @@ impl RustBridgeGenerator { } } + let attrs = self.base_derive_attrs(&name.to_string()); Ok(quote! { - #[derive(Debug, Clone)] + #attrs pub enum #name { #(#cases),* } + #[cfg(feature = "client")] impl golem_wasm::IntoValue for #name { fn into_value(self) -> golem_wasm::Value { match self { @@ -612,6 +718,7 @@ impl RustBridgeGenerator { } } + #[cfg(feature = "client")] impl golem_wasm::FromValue for #name { fn from_value(value: golem_wasm::Value) -> Result { match value { @@ -665,12 +772,14 @@ impl RustBridgeGenerator { }); } + let attrs = self.base_derive_attrs(&name.to_string()); Ok(quote! { - #[derive(Debug, Clone)] + #attrs pub enum #name { #(#cases),* } + #[cfg(feature = "client")] impl golem_wasm::IntoValue for #name { fn into_value(self) -> golem_wasm::Value { match self { @@ -687,6 +796,7 @@ impl RustBridgeGenerator { } } + #[cfg(feature = "client")] impl golem_wasm::FromValue for #name { fn from_value(value: golem_wasm::Value) -> Result { match value { @@ -736,12 +846,14 @@ impl RustBridgeGenerator { let field_count = field_idents.len(); + let attrs = self.base_derive_attrs(&name.to_string()); Ok(quote! { - #[derive(Debug, Clone)] + #attrs pub struct #name { #(#fields),* } + #[cfg(feature = "client")] impl golem_wasm::IntoValue for #name { fn into_value(self) -> golem_wasm::Value { golem_wasm::Value::Record(vec![ @@ -764,6 +876,7 @@ impl RustBridgeGenerator { } } + #[cfg(feature = "client")] impl golem_wasm::FromValue for #name { fn from_value(value: golem_wasm::Value) -> Result { match value { @@ -1755,8 +1868,10 @@ impl RustBridgeGenerator { fn global_config(&self) -> TokenStream { quote! { + #[cfg(feature = "client")] static CONFIG: std::sync::OnceLock = std::sync::OnceLock::new(); + #[cfg(feature = "client")] pub fn configure(server: golem_client::bridge::GolemServer, app_name: &str, env_name: &str) { CONFIG .set(golem_client::bridge::Configuration { @@ -2138,6 +2253,12 @@ impl GolemDependencySource { )), } } + + fn optional_dep_item(&self, crate_path: &str, features: &[&str]) -> anyhow::Result { + let mut item = self.dep_item(crate_path, features)?; + item["optional"] = value(true); + Ok(item) + } } fn add_features(entry: &mut InlineTable, features: &[&str]) { diff --git a/cli/golem-cli/src/bridge_gen/typescript/mod.rs b/cli/golem-cli/src/bridge_gen/typescript/mod.rs index 0279cb750c..b9febccdab 100644 --- a/cli/golem-cli/src/bridge_gen/typescript/mod.rs +++ b/cli/golem-cli/src/bridge_gen/typescript/mod.rs @@ -25,7 +25,7 @@ use crate::bridge_gen::typescript::javascript::escape_js_ident; use crate::bridge_gen::typescript::ts_writer::{ FunctionWriter, TsAnonymousFunctionWriter, TsFunctionWriter, TsWriter, indent, }; -use crate::bridge_gen::{BridgeGenerator, bridge_client_directory_name}; +use crate::bridge_gen::{BridgeGenerator, BridgeGeneratorConfig, bridge_client_directory_name}; use crate::fs; use crate::sdk_overrides::{sdk_overrides, workspace_root}; use anyhow::anyhow; @@ -51,7 +51,12 @@ pub struct TypeScriptBridgeGenerator { } impl BridgeGenerator for TypeScriptBridgeGenerator { - fn new(agent_type: AgentType, target_path: &Utf8Path, testing: bool) -> anyhow::Result { + fn new( + agent_type: AgentType, + target_path: &Utf8Path, + testing: bool, + _config: BridgeGeneratorConfig, + ) -> anyhow::Result { TypeScriptBridgeGenerator::new(agent_type, target_path, testing) } diff --git a/cli/golem-cli/src/command.rs b/cli/golem-cli/src/command.rs index ea0220664b..96569da4b1 100644 --- a/cli/golem-cli/src/command.rs +++ b/cli/golem-cli/src/command.rs @@ -635,6 +635,11 @@ pub enum GolemCliSubcommand { /// temporary directories in the application's directory #[clap(long)] output_dir: Option, + /// Derive rules for generated Rust types. Format: "REGEX=Derive1,Derive2". + /// Can be specified multiple times. Example: --derive-rule ".*=PartialEq" + /// --derive-rule "^Uuid$=Eq,Hash" + #[clap(long)] + derive_rule: Vec, }, /// Start REPL for a selected component Repl { diff --git a/cli/golem-cli/src/command_handler/bridge.rs b/cli/golem-cli/src/command_handler/bridge.rs index db2267b539..13c9a9bb2e 100644 --- a/cli/golem-cli/src/command_handler/bridge.rs +++ b/cli/golem-cli/src/command_handler/bridge.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::bridge_gen::DeriveRule; use crate::command_handler::Handlers; use crate::context::Context; use crate::model::GuestLanguage; @@ -36,7 +37,9 @@ impl BridgeCommandHandler { component_names: Vec, agent_type_names: Vec, output_dir: Option, + derive_rules_raw: Vec, ) -> anyhow::Result<()> { + let derive_rules = parse_derive_rules(derive_rules_raw)?; self.ctx .app_handler() .build( @@ -44,6 +47,7 @@ impl BridgeCommandHandler { agent_type_names: agent_type_names.into_iter().collect(), target_language: language, output_dir, + derive_rules, }), component_names, &ApplicationComponentSelectMode::CurrentDir, @@ -51,3 +55,24 @@ impl BridgeCommandHandler { .await } } + +/// Parses CLI `--derive-rule` values in the format `REGEX=Derive1,Derive2`. +fn parse_derive_rules(raw: Vec) -> anyhow::Result> { + raw.into_iter() + .map(|s| { + let (pattern, derives_str) = s.split_once('=').ok_or_else(|| { + anyhow::anyhow!( + "Invalid derive rule format: '{}'. Expected REGEX=Derive1,Derive2", + s + ) + })?; + Ok(DeriveRule { + pattern: pattern.to_string(), + derives: derives_str + .split(',') + .map(|s| s.trim().to_string()) + .collect(), + }) + }) + .collect() +} diff --git a/cli/golem-cli/src/command_handler/mod.rs b/cli/golem-cli/src/command_handler/mod.rs index e5483255c6..abe289a1e5 100644 --- a/cli/golem-cli/src/command_handler/mod.rs +++ b/cli/golem-cli/src/command_handler/mod.rs @@ -285,10 +285,17 @@ impl CommandHandler { component_name, agent_type_name, output_dir, + derive_rule, } => { self.ctx .bridge_handler() - .cmd_generate_bridge(language, component_name, agent_type_name, output_dir) + .cmd_generate_bridge( + language, + component_name, + agent_type_name, + output_dir, + derive_rule, + ) .await } GolemCliSubcommand::Repl { diff --git a/cli/golem-cli/src/model/app.rs b/cli/golem-cli/src/model/app.rs index cf75fee97a..99b2017508 100644 --- a/cli/golem-cli/src/model/app.rs +++ b/cli/golem-cli/src/model/app.rs @@ -13,7 +13,7 @@ // limitations under the License. use super::http_api::{HttpApiDeploymentDeployProperties, McpDeploymentDeployProperties}; -use crate::bridge_gen::bridge_client_directory_name; +use crate::bridge_gen::{DeriveRule, bridge_client_directory_name}; use crate::fs; use crate::log::LogColorize; use crate::model::app::app_builder::{build_application, build_environments}; @@ -276,6 +276,7 @@ pub struct BridgeSdkTarget { pub agent_type: AgentType, pub target_language: GuestLanguage, pub output_dir: PathBuf, + pub derive_rules: Vec, } #[derive(Debug, Clone)] @@ -283,6 +284,7 @@ pub struct CustomBridgeSdkTarget { pub agent_type_names: HashSet, pub target_language: Option, pub output_dir: Option, + pub derive_rules: Vec, } pub fn includes_from_yaml_file(source: &Path) -> Vec { diff --git a/cli/golem-cli/src/model/app_raw.rs b/cli/golem-cli/src/model/app_raw.rs index 627a75d646..62392daf0c 100644 --- a/cli/golem-cli/src/model/app_raw.rs +++ b/cli/golem-cli/src/model/app_raw.rs @@ -665,6 +665,28 @@ impl BridgeSdks { } } +/// A rule for adding derive macros to generated types matching a regex pattern. +/// +/// # YAML examples +/// +/// Add `PartialEq` to all types: +/// ```yaml +/// { pattern: ".*", derives: ["PartialEq"] } +/// ``` +/// +/// Add `Eq` and `Hash` only to `Uuid`: +/// ```yaml +/// { pattern: "^Uuid$", derives: ["Eq", "Hash"] } +/// ``` +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct DeriveRuleRaw { + /// Regex pattern matched against the generated type name. + pub pattern: String, + /// Derive macros to add when the pattern matches. + pub derives: Vec, +} + #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct BridgeSdkLanguageTargets { @@ -672,6 +694,10 @@ pub struct BridgeSdkLanguageTargets { pub agents: LenientTokenList, #[serde(default, skip_serializing_if = "Option::is_none")] pub output_dir: Option, + /// Rules for adding derive macros to generated types. Each rule pairs a regex + /// pattern (matched against type names) with a list of derives to add. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub additional_derives: Option>, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/cli/golem-cli/tests/bridge_gen/rust.rs b/cli/golem-cli/tests/bridge_gen/rust.rs index 1a2f3e4c35..67ba29a1c3 100644 --- a/cli/golem-cli/tests/bridge_gen/rust.rs +++ b/cli/golem-cli/tests/bridge_gen/rust.rs @@ -196,7 +196,8 @@ fn generate_and_compile(agent_type: AgentType, target_dir: &Utf8Path) { agent_type.type_name, agent_type.description, target_dir ); - let mut generator = RustBridgeGenerator::new(agent_type, target_dir, true).unwrap(); + let mut generator = + RustBridgeGenerator::new(agent_type, target_dir, true, Default::default()).unwrap(); generator .generate() .expect("Failed to generate Rust bridge"); From 5b2e53ea0c43218400d67cdb80cfbd23be47a1c1 Mon Sep 17 00:00:00 2001 From: Daniel Vigovszky Date: Fri, 8 May 2026 16:31:36 +0200 Subject: [PATCH 2/2] Feature flag fixes, skill updates --- cli/golem-cli/src/app/build/gen_bridge.rs | 10 +- .../src/app/build/task_result_marker.rs | 40 +++++++- cli/golem-cli/src/bridge_gen/mod.rs | 15 +++ cli/golem-cli/src/bridge_gen/rust/mod.rs | 80 +++++++++------- cli/golem-cli/src/model/app_raw.rs | 6 +- cli/golem-cli/tests/bridge_gen/rust.rs | 94 +++++++++++++++++-- .../common/golem-edit-manifest/SKILL.md | 7 ++ .../golem-call-from-external-rust/SKILL.md | 40 ++++++-- golem-wasm/Cargo.toml | 4 +- golem-wasm/src/agentic/unstructured_binary.rs | 1 + golem-wasm/src/agentic/unstructured_text.rs | 2 + 11 files changed, 241 insertions(+), 58 deletions(-) diff --git a/cli/golem-cli/src/app/build/gen_bridge.rs b/cli/golem-cli/src/app/build/gen_bridge.rs index a60fd9a25e..723ea23d2d 100644 --- a/cli/golem-cli/src/app/build/gen_bridge.rs +++ b/cli/golem-cli/src/app/build/gen_bridge.rs @@ -216,12 +216,15 @@ async fn gen_bridge_sdk_target( let final_wasm = component.final_wasm(); let agent_type_name = target.agent_type.type_name.clone(); let output_dir = Utf8PathBuf::try_from(target.output_dir)?; + let config = BridgeGeneratorConfig::new(target.derive_rules.clone())?; + let generator_config = config.clone(); new_task_up_to_date_check(ctx) .with_task_result_marker(GenerateBridgeSdkMarkerHash { component_name: &target.component_name, agent_type_name: &target.agent_type.type_name, language: &target.target_language, + derive_rules: &config.derive_rules, })? .with_sources(|| vec![&final_wasm]) .with_targets(|| vec![&output_dir]) @@ -238,22 +241,19 @@ async fn gen_bridge_sdk_target( ); let _indent = LogIndent::new(); - let config = BridgeGeneratorConfig { - derive_rules: target.derive_rules, - }; let mut generator: Box = match target.target_language { GuestLanguage::Rust => Box::new(RustBridgeGenerator::new( target.agent_type, &output_dir, false, - config, + generator_config, )?), GuestLanguage::TypeScript => { Box::new(::new( target.agent_type, &output_dir, false, - config, + generator_config, )?) } GuestLanguage::Scala => { diff --git a/cli/golem-cli/src/app/build/task_result_marker.rs b/cli/golem-cli/src/app/build/task_result_marker.rs index 5e0f99eca6..7e93edaec0 100644 --- a/cli/golem-cli/src/app/build/task_result_marker.rs +++ b/cli/golem-cli/src/app/build/task_result_marker.rs @@ -13,6 +13,7 @@ // limitations under the License. use crate::app::build::task_result_marker::TaskResultMarkerHashSourceKind::{Hash, HashFromString}; +use crate::bridge_gen::DeriveRule; use crate::fs; use crate::log::log_warn_action; use crate::model::app_raw::{ @@ -319,6 +320,7 @@ pub struct GenerateBridgeSdkMarkerHash<'a> { pub component_name: &'a ComponentName, pub agent_type_name: &'a AgentTypeName, pub language: &'a GuestLanguage, + pub derive_rules: &'a [DeriveRule], } impl TaskResultMarkerHashSource for GenerateBridgeSdkMarkerHash<'_> { @@ -332,12 +334,46 @@ impl TaskResultMarkerHashSource for GenerateBridgeSdkMarkerHash<'_> { fn source(&self) -> anyhow::Result { Ok(HashFromString(format!( - "{}-{}-{}", - self.component_name, self.agent_type_name, self.language + "{}-{}-{}-{:?}", + self.component_name, self.agent_type_name, self.language, self.derive_rules ))) } } +#[cfg(test)] +mod tests { + use super::*; + use test_r::test; + + #[test] + fn generate_bridge_sdk_marker_hash_includes_derive_rules() { + let component_name = ComponentName("component".to_string()); + let agent_type_name = AgentTypeName("Agent".to_string()); + let language = GuestLanguage::Rust; + let derive_rules = vec![DeriveRule { + pattern: "^Foo$".to_string(), + derives: vec!["PartialEq".to_string()], + }]; + + let source = GenerateBridgeSdkMarkerHash { + component_name: &component_name, + agent_type_name: &agent_type_name, + language: &language, + derive_rules: &derive_rules, + } + .source() + .unwrap(); + + match source { + HashFromString(input) => { + assert!(input.contains("^Foo$")); + assert!(input.contains("PartialEq")); + } + Hash(_) => panic!("expected bridge SDK marker source to hash input text"), + } + } +} + pub struct ExtractAgentTypeMarkerHash<'a> { pub component_name: &'a ComponentName, } diff --git a/cli/golem-cli/src/bridge_gen/mod.rs b/cli/golem-cli/src/bridge_gen/mod.rs index b0bc6ffd79..495f8a4a8c 100644 --- a/cli/golem-cli/src/bridge_gen/mod.rs +++ b/cli/golem-cli/src/bridge_gen/mod.rs @@ -20,6 +20,8 @@ pub mod typescript; use camino::Utf8Path; use golem_common::model::agent::{AgentType, AgentTypeName}; use heck::ToKebabCase; +use regex::Regex; +use syn::Path; /// A rule that adds derive macros to generated types whose names match a regex pattern. /// @@ -52,6 +54,19 @@ pub struct BridgeGeneratorConfig { pub derive_rules: Vec, } +impl BridgeGeneratorConfig { + pub fn new(derive_rules: Vec) -> anyhow::Result { + for rule in &derive_rules { + Regex::new(&rule.pattern)?; + for derive in &rule.derives { + syn::parse_str::(derive)?; + } + } + + Ok(Self { derive_rules }) + } +} + pub trait BridgeGenerator { fn new( agent_type: AgentType, diff --git a/cli/golem-cli/src/bridge_gen/rust/mod.rs b/cli/golem-cli/src/bridge_gen/rust/mod.rs index a25a8f4083..5bc8e26c4c 100644 --- a/cli/golem-cli/src/bridge_gen/rust/mod.rs +++ b/cli/golem-cli/src/bridge_gen/rust/mod.rs @@ -107,9 +107,8 @@ impl RustBridgeGenerator { fn base_derive_attrs(&self, type_name: &str) -> TokenStream { let mut derive_set = Vec::::new(); for rule in &self.config.derive_rules { - if let Ok(re) = regex::Regex::new(&rule.pattern) - && re.is_match(type_name) - { + let re = regex::Regex::new(&rule.pattern).expect("derive rule regex was validated"); + if re.is_match(type_name) { for d in &rule.derives { if !derive_set.contains(d) { derive_set.push(d.clone()); @@ -120,7 +119,7 @@ impl RustBridgeGenerator { let additional: Vec = derive_set .iter() - .filter_map(|d| syn::parse_str::(d).ok()) + .map(|d| syn::parse_str::(d).expect("derive path was validated")) .map(|path| quote! { #path }) .collect(); @@ -161,15 +160,9 @@ impl RustBridgeGenerator { doc["package"]["description"] = value("Generated by golem-cli"); doc["dependencies"] = Item::Table(Table::default()); - doc["dependencies"]["chrono"] = dep("0.4", &[]); - doc["dependencies"]["nonempty-collections"] = dep("0.3.1", &[]); - doc["dependencies"]["reqwest"] = dep("0.13", &["rustls"]); - doc["dependencies"]["reqwest-middleware"] = dep("0.5", &[]); - doc["dependencies"]["serde"] = dep("1", &["derive"]); - doc["dependencies"]["serde_json"] = dep("1", &[]); - doc["dependencies"]["uuid"] = dep("1.18.1", &["v4"]); - - // Client-only deps (networking, Golem SDK) — optional, behind `client` feature + + // Optional deps are split by feature so generated crates can use plain + // data types, Golem agentic types, or the full bridge client separately. fn optional_dep(version: &str, features: &[&str]) -> Item { let mut entry = Item::Table(Table::default()); entry["version"] = value(version); @@ -185,14 +178,18 @@ impl RustBridgeGenerator { entry } + doc["dependencies"]["chrono"] = optional_dep("0.4", &[]); + doc["dependencies"]["nonempty-collections"] = optional_dep("0.3.1", &[]); + doc["dependencies"]["reqwest"] = optional_dep("0.13", &["rustls"]); + doc["dependencies"]["reqwest-middleware"] = optional_dep("0.5", &[]); + doc["dependencies"]["serde_json"] = optional_dep("1", &[]); + doc["dependencies"]["uuid"] = optional_dep("1.18.1", &["v4"]); + doc["dependencies"]["golem-client"] = golem_source.optional_dep_item("golem-client", &[])?; doc["dependencies"]["golem-common"] = golem_source.optional_dep_item("golem-common", &["client"])?; - doc["dependencies"]["golem-wasm"] = - golem_source.optional_dep_item("golem-wasm", &["client"])?; - doc["dependencies"]["reqwest"] = optional_dep("0.13", &["rustls"]); - doc["dependencies"]["reqwest-middleware"] = optional_dep("0.5", &[]); + doc["dependencies"]["golem-wasm"] = golem_source.optional_dep_item("golem-wasm", &[])?; // Optional serde dependency for JSON serialization { @@ -209,14 +206,26 @@ impl RustBridgeGenerator { doc["features"] = Item::Table(Table::default()); let mut serde_feat = Array::default(); serde_feat.push("dep:serde"); + serde_feat.push("golem-wasm?/serde"); + serde_feat.push("nonempty-collections?/serde"); doc["features"]["serde"] = value(serde_feat); + let mut golem_types_feat = Array::default(); + golem_types_feat.push("dep:golem-wasm"); + golem_types_feat.push("dep:nonempty-collections"); + doc["features"]["golem-types"] = value(golem_types_feat); + let mut client_feat = Array::default(); + client_feat.push("serde"); + client_feat.push("golem-types"); + client_feat.push("golem-wasm/client"); client_feat.push("dep:golem-client"); client_feat.push("dep:golem-common"); - client_feat.push("dep:golem-wasm"); + client_feat.push("dep:chrono"); client_feat.push("dep:reqwest"); client_feat.push("dep:reqwest-middleware"); + client_feat.push("dep:serde_json"); + client_feat.push("dep:uuid"); doc["features"]["client"] = value(client_feat); let mut default_feat = Array::default(); @@ -2028,18 +2037,22 @@ impl RustBridgeGenerator { }); } + let attrs = self.base_derive_attrs(&name.to_string()); multimodal_enums.push(quote! { + #attrs pub enum #name { #(#cases),* } impl MultimodalEnum for #name { + #[cfg(feature = "client")] fn to_named_element_value(&self) -> golem_common::model::agent::NamedElementValue { match self { #(#to_named_element_value_cases)* } } + #[cfg(feature = "client")] fn from_named_element_values(named_element_values: golem_common::model::agent::NamedElementValues) -> Result, golem_client::bridge::ClientError> { let mut values = Vec::new(); for named_element_value in named_element_values.elements { @@ -2060,25 +2073,32 @@ impl RustBridgeGenerator { }); } + let multimodal_attrs = self.base_derive_attrs("Multimodal"); Ok(quote! { + #[cfg(feature = "golem-types")] pub mod multimodal { use super::*; + #[cfg(feature = "client")] use golem_common::base_model::agent::{UnstructuredBinaryExtensions, UnstructuredTextExtensions}; + #[cfg(feature = "client")] use golem_wasm::{FromValueAndType, IntoValueAndType}; - #[derive(Debug, Clone)] + #multimodal_attrs pub struct Multimodal { pub values: nonempty_collections::NEVec } impl Multimodal { + #[cfg(feature = "client")] pub fn from_named_element_values(named_element_values: golem_common::model::agent::NamedElementValues) -> Result { T::from_named_element_values(named_element_values) } } - pub trait MultimodalEnum: Sized { + pub trait MultimodalEnum: Sized + Clone { + #[cfg(feature = "client")] fn to_named_element_value(&self) -> golem_common::model::agent::NamedElementValue; + #[cfg(feature = "client")] fn from_named_element_values(named_element_values: golem_common::model::agent::NamedElementValues) -> Result, golem_client::bridge::ClientError>; } @@ -2116,8 +2136,9 @@ impl RustBridgeGenerator { } from_match_cases.push(quote! { _ => None }); + let attrs = self.base_derive_attrs(&ident.to_string()); language_enums.push(quote! { - #[derive(Debug, Clone)] + #attrs pub enum #ident { #(#cases),* } @@ -2145,6 +2166,7 @@ impl RustBridgeGenerator { } quote! { + #[cfg(feature = "golem-types")] pub mod languages { #(#language_enums)* } @@ -2179,8 +2201,9 @@ impl RustBridgeGenerator { to_match_cases.push(quote! { Self::#enum_variant_ident => #lit.to_string(), }); } + let attrs = self.base_derive_attrs(&ident.to_string()); mimetypes_enums.push(quote! { - #[derive(Debug, Clone)] + #attrs pub enum #ident { #(#cases),* } @@ -2209,6 +2232,7 @@ impl RustBridgeGenerator { } quote! { + #[cfg(feature = "golem-types")] pub mod mimetypes { #(#mimetypes_enums)* } @@ -2274,6 +2298,7 @@ impl GolemDependencySource { fn optional_dep_item(&self, crate_path: &str, features: &[&str]) -> anyhow::Result { let mut item = self.dep_item(crate_path, features)?; item["optional"] = value(true); + item["default-features"] = value(false); Ok(item) } } @@ -2289,17 +2314,6 @@ fn add_features(entry: &mut InlineTable, features: &[&str]) { } } -fn dep(version: &str, features: &[&str]) -> Item { - if features.is_empty() { - return value(version); - } - - let mut entry = InlineTable::new(); - entry.insert("version", Value::from(version)); - add_features(&mut entry, features); - Item::Value(Value::InlineTable(entry)) -} - fn git_dep(url: &str, branch: &str, features: &[&str]) -> Item { let mut entry = InlineTable::new(); entry.insert("git", Value::from(url)); diff --git a/cli/golem-cli/src/model/app_raw.rs b/cli/golem-cli/src/model/app_raw.rs index 1a46e1d3cc..a48af56ada 100644 --- a/cli/golem-cli/src/model/app_raw.rs +++ b/cli/golem-cli/src/model/app_raw.rs @@ -1590,7 +1590,11 @@ mod test { fn arb_bridge_sdk_language_targets() -> BoxedStrategy { (arb_token_list_model(), arb_opt(arb_ident())) - .prop_map(|(agents, output_dir)| BridgeSdkLanguageTargets { agents, output_dir }) + .prop_map(|(agents, output_dir)| BridgeSdkLanguageTargets { + agents, + output_dir, + additional_derives: None, + }) .boxed() } diff --git a/cli/golem-cli/tests/bridge_gen/rust.rs b/cli/golem-cli/tests/bridge_gen/rust.rs index b336ab2766..3bb3063d9b 100644 --- a/cli/golem-cli/tests/bridge_gen/rust.rs +++ b/cli/golem-cli/tests/bridge_gen/rust.rs @@ -17,8 +17,8 @@ use crate::bridge_gen::fixtures::{ }; use crate::bridge_gen::type_naming::test_type_naming; use camino::Utf8Path; -use golem_cli::bridge_gen::BridgeGenerator; use golem_cli::bridge_gen::rust::{RustBridgeGenerator, RustTypeName}; +use golem_cli::bridge_gen::{BridgeGenerator, BridgeGeneratorConfig, DeriveRule}; use golem_cli::model::GuestLanguage; use golem_common::model::Empty; use golem_common::model::agent::{ @@ -219,8 +219,13 @@ fn bridge_rust_ephemeral_agent_skips_non_phantom_constructors() { let dir = TempDir::new().unwrap(); let target_dir = Utf8Path::from_path(dir.path()).unwrap(); - let mut generator = - RustBridgeGenerator::new(ephemeral_config_agent(), target_dir, true).unwrap(); + let mut generator = RustBridgeGenerator::new( + ephemeral_config_agent(), + target_dir, + true, + Default::default(), + ) + .unwrap(); generator .generate() .expect("Failed to generate Rust bridge"); @@ -235,6 +240,58 @@ fn bridge_rust_ephemeral_agent_skips_non_phantom_constructors() { assert!(lib_rs.contains("pub async fn get_phantom_with_config(")); } +#[test] +fn bridge_rust_plain_type_only_features_compile_without_golem_client_stack() { + let dir = TempDir::new().unwrap(); + let target_dir = Utf8Path::from_path(dir.path()).unwrap(); + generate( + multi_agent_wrapper_2_types()[0].clone(), + target_dir, + BridgeGeneratorConfig::new(vec![DeriveRule { + pattern: ".*".to_string(), + derives: vec!["PartialEq".to_string()], + }]) + .unwrap(), + ); + + cargo_check_with_features(target_dir, &["serde"]); + + let lib_rs = std::fs::read_to_string(target_dir.join("src/lib.rs")).unwrap(); + assert!(lib_rs.contains("#[derive(Debug, Clone, PartialEq)]")); +} + +#[test] +fn bridge_rust_golem_type_only_features_compile_for_multimodal_types() { + let dir = TempDir::new().unwrap(); + let target_dir = Utf8Path::from_path(dir.path()).unwrap(); + generate( + multi_agent_wrapper_2_types()[1].clone(), + target_dir, + Default::default(), + ); + + cargo_check_with_features(target_dir, &["serde", "golem-types"]); +} + +#[test] +fn bridge_rust_rejects_invalid_derive_rules() { + assert!( + BridgeGeneratorConfig::new(vec![DeriveRule { + pattern: "[".to_string(), + derives: vec!["PartialEq".to_string()], + }]) + .is_err() + ); + + assert!( + BridgeGeneratorConfig::new(vec![DeriveRule { + pattern: ".*".to_string(), + derives: vec!["not a derive path".to_string()], + }]) + .is_err() + ); +} + fn ephemeral_config_agent() -> AgentType { AgentType { type_name: AgentTypeName("EphemeralBridgeAgent".to_string()), @@ -371,11 +428,7 @@ fn generate_and_compile(agent_type: AgentType, target_dir: &Utf8Path) { agent_type.type_name, agent_type.description, target_dir ); - let mut generator = - RustBridgeGenerator::new(agent_type, target_dir, true, Default::default()).unwrap(); - generator - .generate() - .expect("Failed to generate Rust bridge"); + generate(agent_type, target_dir, Default::default()); let cwd = std::env::current_dir().expect("Failed to get current directory"); let shared_target_dir = cwd.join("../../target/shared_bridge_tests"); @@ -401,6 +454,31 @@ fn generate_and_compile(agent_type: AgentType, target_dir: &Utf8Path) { assert!(status.success(), "`cargo build` failed: {:?}", status); } +fn generate(agent_type: AgentType, target_dir: &Utf8Path, config: BridgeGeneratorConfig) { + let mut generator = RustBridgeGenerator::new(agent_type, target_dir, true, config).unwrap(); + generator + .generate() + .expect("Failed to generate Rust bridge"); +} + +fn cargo_check_with_features(target_dir: &Utf8Path, features: &[&str]) { + let cwd = std::env::current_dir().expect("Failed to get current directory"); + let shared_target_dir = cwd.join("../../target/shared_bridge_tests"); + + let status = std::process::Command::new("cargo") + .arg("check") + .arg("--manifest-path") + .arg(target_dir.join("Cargo.toml").as_std_path()) + .arg("--target-dir") + .arg(&shared_target_dir) + .arg("--no-default-features") + .arg("--features") + .arg(features.join(",")) + .status() + .expect("failed to run `cargo check`"); + assert!(status.success(), "`cargo check` failed: {:?}", status); +} + #[test] fn test_rust_type_naming_rust_foo() { test_type_naming::(GuestLanguage::Rust, "FooAgent"); diff --git a/golem-skills/skills/common/golem-edit-manifest/SKILL.md b/golem-skills/skills/common/golem-edit-manifest/SKILL.md index a2dab13f01..3affbedf7d 100644 --- a/golem-skills/skills/common/golem-edit-manifest/SKILL.md +++ b/golem-skills/skills/common/golem-edit-manifest/SKILL.md @@ -395,8 +395,15 @@ bridge: - MyAgent # Agent type name - my-app:billing # Component name (all agents in that component) outputDir: ./bridge-sdk/rust + additionalDerives: # Optional, Rust bridge SDKs only + - pattern: ".*" # Regex matched against generated Rust type names + derives: [PartialEq] + - pattern: "^.*Id$" + derives: [Eq, Hash] ``` +`additionalDerives` lets generated Rust bridge types derive extra traits. Patterns must be valid regular expressions, and each derive entry must be a syntactically valid Rust derive path such as `PartialEq`, `Eq`, or `Hash`; the derive macro must still be available to the generated crate at compile time. Invalid rules fail `golem build` during bridge validation, before up-to-date generated output is reused. Do not add `Debug`, `Clone`, `serde::Serialize`, or `serde::Deserialize` here; those are already generated, and repeating them can break compilation. + ## Plugin Installations Plugins are installed at any cascade level (template, component, agent, preset). diff --git a/golem-skills/skills/rust/golem-call-from-external-rust/SKILL.md b/golem-skills/skills/rust/golem-call-from-external-rust/SKILL.md index 485841047d..f1e248a72c 100644 --- a/golem-skills/skills/rust/golem-call-from-external-rust/SKILL.md +++ b/golem-skills/skills/rust/golem-call-from-external-rust/SKILL.md @@ -22,10 +22,17 @@ bridge: # - MyAgent # - my-app:billing outputDir: ./bridge-sdk/rust # Optional custom output directory + additionalDerives: # Optional: extra derives for generated Rust types + - pattern: ".*" # Regex matched against generated type names + derives: [PartialEq] + - pattern: "^.*Id$" + derives: [Eq, Hash] ``` The `agents` field accepts `"*"` (all agents), or a list of agent type names or component names (`namespace:name`). +Use `additionalDerives` when the external Rust application needs generated bridge types to implement extra traits, for example `PartialEq` in tests or `Eq`/`Hash` for map/set keys. Patterns must be valid regular expressions, and derives must be syntactically valid Rust derive paths whose macros are available to the generated crate at compile time. Invalid derive rules fail `golem build` during bridge validation, before stale generated output is reused. Do not add `Debug`, `Clone`, `serde::Serialize`, or `serde::Deserialize` here; those are already generated in the standard derives / `serde` feature path. + ## Step 2: Generate the Bridge SDK The recommended approach is to declare the bridge in `golem.yaml` (as shown above) and let `golem build` produce the SDK as part of the normal build: @@ -50,17 +57,13 @@ my-agent-client = { path = "../path/to/bridge-sdk/rust/my-agent-client" } Then use the generated client: ```rust -use my_agent_client::{MyAgent, global_config}; -use golem_client::bridge::{Configuration, GolemServer}; +use my_agent_client::{configure, MyAgent}; +use golem_client::bridge::GolemServer; #[tokio::main] async fn main() -> Result<(), Box> { // Configure the Golem server connection - global_config(Configuration { - app_name: "my-app".to_string(), - env_name: "local".to_string(), - server: GolemServer::Local, - }); + configure(GolemServer::Local, "my-app", "local"); // Get or create an agent instance let agent = MyAgent::get("my-instance".to_string()).await?; @@ -118,7 +121,27 @@ let agent = MyAgent::get_with_config( ## Generated Crate Dependencies -The generated crate depends on `golem-client`, `golem-common`, `golem-wasm`, `reqwest`, `serde_json`, `uuid`, and `chrono`. These are resolved from crates.io or from the Golem repository depending on the SDK version. +The generated crate has feature-gated dependencies: + +- `default = ["client"]` preserves the normal fully functional client SDK. +- `client` enables the HTTP client stack and depends on `serde` and `golem-types`. +- `serde` enables serde derives on generated types. +- `golem-types` enables lightweight Golem helper types used by multimodal and unstructured text/binary schemas without pulling in the HTTP client stack. + +For normal external clients, use the default features. For type-only use cases, depend on the generated crate with `default-features = false` and enable only what you need: + +```toml +[dependencies] +my-agent-client = { path = "../path/to/bridge-sdk/rust/my-agent-client", default-features = false, features = ["serde"] } +``` + +When `client` is disabled, the crate provides generated types only; runtime client APIs such as `configure(...)`, `MyAgent::get(...)`, and method invocation helpers are not available. + +If the generated types include multimodal or unstructured text/binary fields, also enable `golem-types`: + +```toml +my-agent-client = { path = "../path/to/bridge-sdk/rust/my-agent-client", default-features = false, features = ["serde", "golem-types"] } +``` ## Key Points @@ -127,3 +150,4 @@ The generated crate depends on `golem-client`, `golem-common`, `golem-wasm`, `re - All custom types (records, variants, enums, flags) are generated as corresponding Rust types - The client uses async/await with `reqwest` for HTTP communication - Each agent type gets its own crate with a `Cargo.toml` and `src/lib.rs` +- Changing `additionalDerives` forces the next `golem build` to regenerate the Rust bridge SDK diff --git a/golem-wasm/Cargo.toml b/golem-wasm/Cargo.toml index 8ff001989a..e69d81ca9c 100644 --- a/golem-wasm/Cargo.toml +++ b/golem-wasm/Cargo.toml @@ -89,9 +89,11 @@ host = [ derive = ["dep:golem-wasm-derive"] +serde = ["dep:serde"] + guest = ["dep:wit-bindgen", "dep:wasip2"] -client = ["derive", "dep:serde", "dep:serde_json", "dep:bigdecimal", "dep:bit-vec", "dep:url"] +client = ["derive", "serde", "dep:serde_json", "dep:bigdecimal", "dep:bit-vec", "dep:url"] [[test]] name = "exports" diff --git a/golem-wasm/src/agentic/unstructured_binary.rs b/golem-wasm/src/agentic/unstructured_binary.rs index 0cad543635..661507fb40 100644 --- a/golem-wasm/src/agentic/unstructured_binary.rs +++ b/golem-wasm/src/agentic/unstructured_binary.rs @@ -18,6 +18,7 @@ use std::fmt::Debug; /// /// `MT` specifies the allowed language codes for inline text. #[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum UnstructuredBinary { Url(String), Inline { data: Vec, mime_type: MT }, diff --git a/golem-wasm/src/agentic/unstructured_text.rs b/golem-wasm/src/agentic/unstructured_text.rs index 46dc0ce728..d3082e5337 100644 --- a/golem-wasm/src/agentic/unstructured_text.rs +++ b/golem-wasm/src/agentic/unstructured_text.rs @@ -19,6 +19,7 @@ use std::fmt::Debug; /// `LC` specifies the allowed language codes for inline text. Defaults to `AnyLanguage`, /// which allows all languages. #[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum UnstructuredText { Url(String), Text { @@ -181,6 +182,7 @@ pub trait AllowedLanguages: Debug + Clone { } #[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct AnyLanguage; impl AllowedLanguages for AnyLanguage {