Skip to content

Commit 4176faf

Browse files
committed
better support for component updates
1 parent ac4c600 commit 4176faf

31 files changed

+232
-82
lines changed

cli/golem-cli/wit/deps/golem-agent/host.wit

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package golem:agent@1.5.0;
22

33
interface host {
4-
use golem:core/types@1.5.0.{component-id, uuid, promise-id, wit-value};
4+
use golem:core/types@1.5.0.{component-id, uuid, promise-id, wit-value, wit-type};
55
use wasi:clocks/wall-clock@0.2.3.{datetime};
66
use wasi:io/poll@0.2.3.{pollable};
77
use common.{agent-error, agent-type, data-value, registered-agent-type};
@@ -82,10 +82,17 @@ interface host {
8282
}
8383

8484
/// Get the current value of the config key.
85+
///
86+
/// The expected type is a hint to the host what type of value is expected by the guest and can be used
87+
/// by the host to automatically migrate config values to fit the expected schema.
88+
///
8589
/// Only keys that are declared by the agent-type are allowed to be accessed. Trying
86-
/// to access an undeclared key will trap.
90+
/// to access an undeclared key will trap, unless the expected type is an option. In that case
91+
/// none is returned.
92+
///
8793
/// Getting a local key will get values defined as part of the current
8894
/// component revision + overrides declared during agent creation.
95+
///
8996
/// Getting a shared key will get the current value of the key in the environment.
90-
get-config-value: func(key: list<string>) -> wit-value;
97+
get-config-value: func(key: list<string>, expected-type: wit-type) -> wit-value;
9198
}

golem-common/wit/deps/golem-agent/host.wit

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package golem:agent@1.5.0;
22

33
interface host {
4-
use golem:core/types@1.5.0.{component-id, uuid, promise-id, wit-value};
4+
use golem:core/types@1.5.0.{component-id, uuid, promise-id, wit-value, wit-type};
55
use wasi:clocks/wall-clock@0.2.3.{datetime};
66
use wasi:io/poll@0.2.3.{pollable};
77
use common.{agent-error, agent-type, data-value, registered-agent-type};
@@ -82,10 +82,17 @@ interface host {
8282
}
8383

8484
/// Get the current value of the config key.
85+
///
86+
/// The expected type is a hint to the host what type of value is expected by the guest and can be used
87+
/// by the host to automatically migrate config values to fit the expected schema.
88+
///
8589
/// Only keys that are declared by the agent-type are allowed to be accessed. Trying
86-
/// to access an undeclared key will trap.
90+
/// to access an undeclared key will trap, unless the expected type is an option. In that case
91+
/// none is returned.
92+
///
8793
/// Getting a local key will get values defined as part of the current
8894
/// component revision + overrides declared during agent creation.
95+
///
8996
/// Getting a shared key will get the current value of the key in the environment.
90-
get-config-value: func(key: list<string>) -> wit-value;
97+
get-config-value: func(key: list<string>, expected-type: wit-type) -> wit-value;
9198
}

golem-worker-executor/src/durable_host/golem/agent.rs

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use golem_common::model::agent::bindings::golem::agent::common::{
2020
AgentError, DataValue, RegisteredAgentType,
2121
};
2222
use golem_common::model::agent::wit_naming::ToWitNaming;
23-
use golem_common::model::agent::{AgentId, AgentTypeName, ConfigKeyValueType, ConfigValueType};
23+
use golem_common::model::agent::{AgentId, AgentTypeName, ConfigValueType};
2424
use golem_common::model::oplog::host_functions::{
2525
GolemAgentCreateWebhook, GolemAgentGetAgentType, GolemAgentGetAllAgentTypes,
2626
};
@@ -31,7 +31,7 @@ use golem_common::model::oplog::{
3131
};
3232
use golem_common::model::PromiseId;
3333
use golem_wasm::analysis::AnalysedType;
34-
use golem_wasm::{NodeBuilder, WitValue, WitValueBuilderExtensions};
34+
use golem_wasm::{NodeBuilder, WitType, WitValue, WitValueBuilderExtensions};
3535

3636
impl<Ctx: WorkerCtx> Host for DurableWorkerCtx<Ctx> {
3737
async fn get_all_agent_types(&mut self) -> anyhow::Result<Vec<RegisteredAgentType>> {
@@ -220,42 +220,69 @@ impl<Ctx: WorkerCtx> Host for DurableWorkerCtx<Ctx> {
220220
}
221221
}
222222

223-
async fn get_config_value(&mut self, key: Vec<String>) -> anyhow::Result<WitValue> {
224-
tracing::debug!("Agent getting config value for key {}", key.join("."));
223+
async fn get_config_value(
224+
&mut self,
225+
key: Vec<String>,
226+
expected_type: WitType,
227+
) -> anyhow::Result<WitValue> {
228+
let key_str = key.join(".");
229+
tracing::debug!("Agent getting config value for key {}", key_str);
225230

226231
let agent_id = self
227232
.agent_id()
228233
.ok_or_else(|| anyhow!("only agentic workers can access agent config"))?;
229234

235+
let expected_type = AnalysedType::from(expected_type);
236+
230237
let declaration = self
231238
.component_metadata()
232239
.metadata
233240
.agent_types()
234241
.iter()
235242
.find(|at| at.type_name == agent_id.agent_type)
236-
.unwrap()
243+
.expect("Active agent type of agent was not declared in component metadata")
237244
.config
238245
.iter()
239-
.find(|c| c.key == key);
246+
.find_map(|c| (c.key == key).then(|| c.value.clone()));
247+
248+
let declaration = match declaration {
249+
None if matches!(expected_type, AnalysedType::Option(_)) => {
250+
// Allow optional undeclared config for schema evolution
251+
return Ok(WitValue::builder().option_none());
252+
}
253+
None => {
254+
return Err(anyhow!("No config declared for key {}", key_str));
255+
}
256+
Some(d) => d,
257+
};
240258

241259
match declaration {
242-
Some(ConfigKeyValueType {
243-
value: ConfigValueType::Local(config_declaration),
244-
..
245-
}) => match self.state.local_agent_config.get(&key) {
246-
Some(config_value) => Ok(config_value.value.clone().into()),
247-
None if matches!(config_declaration.value, AnalysedType::Option(_)) => {
248-
Ok(WitValue::builder().option_none())
260+
ConfigValueType::Local(local_decl) => {
261+
let config_value = self.state.local_agent_config.get(&key);
262+
263+
match (&local_decl.value, &expected_type, config_value) {
264+
// Declared optional, expected optional, value missing
265+
(AnalysedType::Option(declared), AnalysedType::Option(expected), None)
266+
if declared == expected =>
267+
{
268+
Ok(WitValue::builder().option_none())
269+
}
270+
271+
// Types match and value exists
272+
(declared, expected, Some(value)) if declared == expected => {
273+
Ok(value.value.clone().into())
274+
}
275+
276+
_ => Err(anyhow!(
277+
"declared and expected type for config key {} are not compatible",
278+
key_str
279+
)),
249280
}
250-
None => Err(anyhow!("No config declared for key {}", key.join("."))),
251-
},
252-
Some(ConfigKeyValueType {
253-
value: ConfigValueType::Shared(_),
254-
..
255-
}) => {
281+
}
282+
283+
ConfigValueType::Shared(_) => {
256284
unimplemented!()
257285
}
258-
None => Err(anyhow!("No config declared for key {}", key.join("."))),
259286
}
260287
}
261288
}

golem-worker-executor/src/workerctx/default.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ use golem_service_base::error::worker_executor::{
6565
use golem_service_base::model::component::Component;
6666
use golem_service_base::model::GetFileSystemNodeResult;
6767
use golem_wasm::wasmtime::{ResourceStore, ResourceTypeId};
68-
use golem_wasm::Uri;
68+
use golem_wasm::{Uri, WitType};
6969
use std::collections::HashSet;
7070
use std::future::Future;
7171
use std::sync::{Arc, Weak};
@@ -539,8 +539,12 @@ impl AgentHost for Context {
539539
AgentHost::create_webhook(&mut self.durable_ctx, promise_id).await
540540
}
541541

542-
async fn get_config_value(&mut self, key: Vec<String>) -> anyhow::Result<golem_wasm::WitValue> {
543-
AgentHost::get_config_value(&mut self.durable_ctx, key).await
542+
async fn get_config_value(
543+
&mut self,
544+
key: Vec<String>,
545+
expected_type: WitType,
546+
) -> anyhow::Result<golem_wasm::WitValue> {
547+
AgentHost::get_config_value(&mut self.durable_ctx, key, expected_type).await
544548
}
545549
}
546550

integration-tests/tests/worker_local_agent_config.rs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,6 @@ async fn agent_with_mixed_local_agent_config(
264264
#[test]
265265
#[tracing::instrument]
266266
#[timeout("4m")]
267-
#[ignore]
268267
async fn agent_with_mixed_local_agent_config_update(
269268
deps: &EnvBasedTestDependencies,
270269
) -> anyhow::Result<()> {
@@ -478,3 +477,71 @@ async fn mistyped_local_agent_config_key(deps: &EnvBasedTestDependencies) -> any
478477

479478
Ok(())
480479
}
480+
481+
#[test]
482+
#[tracing::instrument]
483+
#[timeout("4m")]
484+
async fn optional_local_agent_config_does_not_need_to_be_provided(
485+
deps: &EnvBasedTestDependencies,
486+
) -> anyhow::Result<()> {
487+
let user = deps.user().await?;
488+
let (_, env) = user.app_and_env().await?;
489+
490+
let component = user
491+
.component(&env.id, "golem_it_agent_sdk_ts")
492+
.name("golem-it:agent-sdk-ts")
493+
.store()
494+
.await?;
495+
496+
let agent_id = agent_id!("config-agent", "test-agent");
497+
user.start_agent_with(
498+
&component.id,
499+
agent_id.clone(),
500+
HashMap::new(),
501+
HashMap::new(),
502+
vec![
503+
WorkerCreationLocalAgentConfigEntry {
504+
key: vec!["foo".to_string()],
505+
value: json!(1),
506+
},
507+
WorkerCreationLocalAgentConfigEntry {
508+
key: vec!["bar".to_string()],
509+
value: json!("bar"),
510+
},
511+
WorkerCreationLocalAgentConfigEntry {
512+
key: vec!["nested".to_string(), "a".to_string()],
513+
value: json!(true),
514+
},
515+
WorkerCreationLocalAgentConfigEntry {
516+
key: vec!["nested".to_string(), "b".to_string()],
517+
value: json!([1, 2]),
518+
},
519+
],
520+
)
521+
.await?;
522+
523+
let response = user
524+
.invoke_and_await_agent(&component, &agent_id, "echoLocalConfig", data_value!())
525+
.await?
526+
.into_return_value()
527+
.ok_or_else(|| anyhow!("expected return value"))?;
528+
529+
let_assert!(Value::String(agent_config) = response);
530+
531+
let parsed_agent_config: serde_json::Value = serde_json::from_str(&agent_config)?;
532+
533+
assert_eq!(
534+
parsed_agent_config,
535+
json!({
536+
"foo": 1,
537+
"bar": "bar",
538+
"nested": {
539+
"a": true,
540+
"b": [1, 2]
541+
},
542+
"aliasedNested": { }
543+
})
544+
);
545+
546+
Ok(())
547+
}

sdks/rust/golem-rust/src/agentic/agent_config.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,11 @@ impl<T> Secret<T> {
5353
where
5454
T: FromValueAndType + IntoValue,
5555
{
56-
let value = get_config_value(&self.path);
56+
let typ = T::get_type();
57+
let value = get_config_value(&self.path, &typ);
5758
T::from_value_and_type(ValueAndType {
5859
value,
59-
typ: T::get_type(),
60+
typ,
6061
})
6162
}
6263
}
@@ -126,10 +127,11 @@ impl<T: ComponentModelConfigLeaf> ConfigField for T {
126127
}
127128

128129
fn load(path: &[String]) -> Result<Self, String> {
129-
let value = get_config_value(path);
130+
let typ = <Self as IntoValue>::get_type();
131+
let value = get_config_value(path, &typ);
130132
<Self as FromValueAndType>::from_value_and_type(ValueAndType {
131133
value,
132-
typ: <Self as IntoValue>::get_type(),
134+
typ,
133135
})
134136
}
135137
}

sdks/rust/golem-rust/wit/deps/golem-agent/host.wit

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package golem:agent@1.5.0;
22

33
interface host {
4-
use golem:core/types@1.5.0.{component-id, uuid, promise-id, wit-value};
4+
use golem:core/types@1.5.0.{component-id, uuid, promise-id, wit-value, wit-type};
55
use wasi:clocks/wall-clock@0.2.3.{datetime};
66
use wasi:io/poll@0.2.3.{pollable};
77
use common.{agent-error, agent-type, data-value, registered-agent-type};
@@ -82,10 +82,17 @@ interface host {
8282
}
8383

8484
/// Get the current value of the config key.
85+
///
86+
/// The expected type is a hint to the host what type of value is expected by the guest and can be used
87+
/// by the host to automatically migrate config values to fit the expected schema.
88+
///
8589
/// Only keys that are declared by the agent-type are allowed to be accessed. Trying
86-
/// to access an undeclared key will trap.
90+
/// to access an undeclared key will trap, unless the expected type is an option. In that case
91+
/// none is returned.
92+
///
8793
/// Getting a local key will get values defined as part of the current
8894
/// component revision + overrides declared during agent creation.
95+
///
8996
/// Getting a shared key will get the current value of the key in the environment.
90-
get-config-value: func(key: list<string>) -> wit-value;
97+
get-config-value: func(key: list<string>, expected-type: wit-type) -> wit-value;
9198
}

sdks/ts/packages/golem-ts-sdk/src/index.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,9 +249,46 @@ export class Secret<T> {
249249
}
250250

251251
export class Config<T> {
252-
readonly value: T;
252+
private _value: T
253253

254-
constructor(value: T) {
255-
this.value = value;
254+
constructor(readonly properties: Type.ConfigProperty[]) {
255+
this._value = this.loadConfig()
256+
}
257+
258+
get value(): T {
259+
return this._value
260+
}
261+
262+
reload(): void {
263+
this._value = this.loadConfig()
264+
}
265+
266+
private loadConfig(): T {
267+
const root: Record<string, any> = {};
268+
269+
for (const prop of this.properties) {
270+
const { path } = prop;
271+
if (path.length === 0) continue;
272+
273+
let current = root;
274+
275+
for (let i = 0; i < path.length - 1; i++) {
276+
const key = path[i];
277+
if (!(key in current)) current[key] = {};
278+
current = current[key];
279+
}
280+
281+
const leafKey = path[path.length - 1];
282+
let leafValue;
283+
if (prop.secret) {
284+
leafValue = new Secret(path, prop.type);
285+
} else {
286+
leafValue = loadConfigKey(path, prop.type);
287+
}
288+
289+
current[leafKey] = leafValue;
290+
}
291+
292+
return root as T
256293
}
257294
}

0 commit comments

Comments
 (0)