Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile.toml
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ install_crate = "clippy"
command = "cargo"
args = [
"clippy",
"--all-targets",
"--fix",
"--allow-dirty",
"--allow-staged",
Expand Down
24 changes: 16 additions & 8 deletions cli/golem-cli/wit/deps/golem-1.x/golem-host.wit
Original file line number Diff line number Diff line change
Expand Up @@ -196,12 +196,19 @@ interface host {
revert-last-invocations(u64)
}

/// Indicates which agent the code is running on after `fork`
enum fork-result {
/// Details about the fork result
record fork-details {
forked-phantom-id: uuid,
}

/// Indicates which agent the code is running on after `fork`.
/// The parameter contains details about the fork result, such as the phantom-ID of the newly
/// created agent.
variant fork-result {
/// The original agent that called `fork`
original,
original(fork-details),
/// The new agent
forked
forked(fork-details)
}

resource get-promise-result {
Expand Down Expand Up @@ -305,10 +312,11 @@ interface host {
/// Returns none when no component for the specified component-reference or no agent with the specified agent-name exists.
resolve-agent-id-strict: func(component-reference: string, agent-name: string) -> option<agent-id>;

/// Forks the current agent at the current execution point. The new agent gets the `new-name` agent ID,
/// and this agent continues running as well. The return value is going to be different in this agent and
/// the forked agent.
fork: func(new-name: string) -> fork-result;
/// Forks the current agent at the current execution point. The new agent gets the same base agent ID but
/// with a new unique phantom ID. The phantom ID of the forked agent is returned in `fork-result` on
/// both sides. The newly created agent continues running from the same point, but the return value is
/// going to be different in this agent and the forked agent.
fork: func() -> fork-result;
}

/// Interface providing user-defined snapshotting capability. This can be used to perform manual update of agents
Expand Down
8 changes: 5 additions & 3 deletions cli/golem-cli/wit/deps/golem-agent/host.wit
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package golem:agent;

interface host {
use golem:rpc/[email protected].{component-id};
use golem:rpc/[email protected].{component-id, uuid};
use common.{agent-error, agent-type, data-value};

/// Associates an agent type with a component that implements it
Expand All @@ -17,8 +17,10 @@ interface host {
get-agent-type: func(agent-type-name: string) -> option<registered-agent-type>;

/// Constructs a string agent-id from the agent type and its constructor parameters
make-agent-id: func(agent-type-name: string, input: data-value) -> result<string, agent-error>;
/// and an optional phantom ID
make-agent-id: func(agent-type-name: string, input: data-value, phantom-id: option<uuid>) -> result<string, agent-error>;

/// Parses an agent-id (created by `make-agent-id`) into an agent type name and its constructor parameters
parse-agent-id: func(agent-id: string) -> result<tuple<string, data-value>, agent-error>;
/// and an optional phantom ID
parse-agent-id: func(agent-id: string) -> result<tuple<string, data-value, option<uuid>>, agent-error>;
}
2 changes: 1 addition & 1 deletion cli/golem-templates/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ static APP_MANIFEST_HEADER: &str = indoc! {"
"};

static GOLEM_RUST_VERSION: &str = "1.9.0";
static GOLEM_TS_VERSION: &str = "0.0.60";
static GOLEM_TS_VERSION: &str = "0.0.63";

fn all_templates(dev_mode: bool) -> Vec<Template> {
let mut result: Vec<Template> = vec![];
Expand Down
24 changes: 16 additions & 8 deletions cli/golem-templates/wit/deps/golem-1.x/golem-host.wit
Original file line number Diff line number Diff line change
Expand Up @@ -196,12 +196,19 @@ interface host {
revert-last-invocations(u64)
}

/// Indicates which agent the code is running on after `fork`
enum fork-result {
/// Details about the fork result
record fork-details {
forked-phantom-id: uuid,
}

/// Indicates which agent the code is running on after `fork`.
/// The parameter contains details about the fork result, such as the phantom-ID of the newly
/// created agent.
variant fork-result {
/// The original agent that called `fork`
original,
original(fork-details),
/// The new agent
forked
forked(fork-details)
}

resource get-promise-result {
Expand Down Expand Up @@ -305,10 +312,11 @@ interface host {
/// Returns none when no component for the specified component-reference or no agent with the specified agent-name exists.
resolve-agent-id-strict: func(component-reference: string, agent-name: string) -> option<agent-id>;

/// Forks the current agent at the current execution point. The new agent gets the `new-name` agent ID,
/// and this agent continues running as well. The return value is going to be different in this agent and
/// the forked agent.
fork: func(new-name: string) -> fork-result;
/// Forks the current agent at the current execution point. The new agent gets the same base agent ID but
/// with a new unique phantom ID. The phantom ID of the forked agent is returned in `fork-result` on
/// both sides. The newly created agent continues running from the same point, but the return value is
/// going to be different in this agent and the forked agent.
fork: func() -> fork-result;
}

/// Interface providing user-defined snapshotting capability. This can be used to perform manual update of agents
Expand Down
8 changes: 5 additions & 3 deletions cli/golem-templates/wit/deps/golem-agent/host.wit
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package golem:agent;

interface host {
use golem:rpc/[email protected].{component-id};
use golem:rpc/[email protected].{component-id, uuid};
use common.{agent-error, agent-type, data-value};

/// Associates an agent type with a component that implements it
Expand All @@ -17,8 +17,10 @@ interface host {
get-agent-type: func(agent-type-name: string) -> option<registered-agent-type>;

/// Constructs a string agent-id from the agent type and its constructor parameters
make-agent-id: func(agent-type-name: string, input: data-value) -> result<string, agent-error>;
/// and an optional phantom ID
make-agent-id: func(agent-type-name: string, input: data-value, phantom-id: option<uuid>) -> result<string, agent-error>;

/// Parses an agent-id (created by `make-agent-id`) into an agent type name and its constructor parameters
parse-agent-id: func(agent-id: string) -> result<tuple<string, data-value>, agent-error>;
/// and an optional phantom ID
parse-agent-id: func(agent-id: string) -> result<tuple<string, data-value, option<uuid>>, agent-error>;
}
63 changes: 39 additions & 24 deletions golem-common/src/model/agent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,12 @@ use golem_wasm::{
parse_value_and_type, print_value_and_type, IntoValue, IntoValueAndType, Value, ValueAndType,
};
use golem_wasm_derive::{FromValue, IntoValue};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use std::sync::LazyLock;
use uuid::Uuid;

#[derive(
Debug,
Expand Down Expand Up @@ -950,15 +953,17 @@ pub struct RegisteredAgentType {
pub struct AgentId {
pub agent_type: String,
pub parameters: DataValue,
pub phantom_id: Option<Uuid>,
wrapper_agent_type: String,
}

impl AgentId {
pub fn new(agent_type: String, parameters: DataValue) -> Self {
pub fn new(agent_type: String, parameters: DataValue, phantom_id: Option<Uuid>) -> Self {
let wrapper_agent_type = agent_type.to_wit_naming();
Self {
agent_type,
parameters,
phantom_id,
wrapper_agent_type,
}
}
Expand All @@ -971,30 +976,36 @@ impl AgentId {
s: impl AsRef<str>,
resolver: impl AgentTypeResolver,
) -> Result<(Self, AgentType), String> {
static AGENT_ID_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^([^(]+)\((.*)\)(?:\[([^\]]+)\])?$").expect("Invalid agent ID regex")
});

let s = s.as_ref();

if let Some((agent_type, param_list)) = s.split_once('(') {
if let Some(param_list) = param_list.strip_suffix(')') {
let agent_type = resolver.resolve_agent_type_by_wrapper_name(agent_type)?;
let value = DataValue::parse(param_list, &agent_type.constructor.input_schema)?;
Ok((
AgentId {
agent_type: agent_type.type_name.clone(),
wrapper_agent_type: agent_type.type_name.to_wit_naming(),
parameters: value,
},
agent_type,
))
} else {
Err(format!(
"Unexpected agent-id format - missing closing ')', got: {s}"
))
}
} else {
Err(format!(
"Unexpected agent-id format - must be 'agent-type(...)', got: {s}"
))
}
let captures = AGENT_ID_REGEX.captures(s).ok_or_else(|| {
format!("Unexpected agent-id format - must be 'agent-type(...)' or 'agent-type(...)[uuid]', got: {s}")
})?;

let agent_type_name = captures.get(1).unwrap().as_str();
let param_list = captures.get(2).unwrap().as_str();
let phantom_id = captures
.get(3)
.map(|m| Uuid::parse_str(m.as_str()))
.transpose()
.map_err(|e| format!("Invalid UUID in phantom ID: {e}"))?;

let agent_type = resolver.resolve_agent_type_by_wrapper_name(agent_type_name)?;
let value = DataValue::parse(param_list, &agent_type.constructor.input_schema)?;

Ok((
AgentId {
agent_type: agent_type.type_name.clone(),
wrapper_agent_type: agent_type.type_name.to_wit_naming(),
parameters: value,
phantom_id,
},
agent_type,
))
}

pub fn wrapper_agent_type(&self) -> &str {
Expand All @@ -1009,7 +1020,11 @@ impl Display for AgentId {
"{}({})",
self.wrapper_agent_type,
self.parameters.to_compact_string()
)
)?;
if let Some(phantom_id) = &self.phantom_id {
write!(f, "[{phantom_id}]")?;
}
Ok(())
}
}

Expand Down
78 changes: 74 additions & 4 deletions golem-common/src/model/agent/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use proptest::string::string_regex;
use proptest::{prop_assert_eq, prop_oneof, proptest};
use std::collections::HashMap;
use test_r::test;
use uuid::Uuid;

#[test]
fn agent_id_wave_normalization() {
Expand Down Expand Up @@ -163,7 +164,7 @@ proptest! {
]
}
);
let id = AgentId::new("agent-6".to_string(), parameters);
let id = AgentId::new("agent-6".to_string(), parameters, None);
let s = id.to_string();
println!("{s}");
let id2 = AgentId::parse(s, TestAgentTypes::new()).unwrap();
Expand All @@ -186,7 +187,7 @@ proptest! {
]
}
);
let id = AgentId::new("agent-6".to_string(), parameters);
let id = AgentId::new("agent-6".to_string(), parameters, None);
let s = id.to_string();
println!("{s}");
let id2 = AgentId::parse(s, TestAgentTypes::new()).unwrap();
Expand Down Expand Up @@ -309,21 +310,90 @@ fn invalid_text_url() {
)
}

#[test]
fn roundtrip_test_with_phantom_id() {
let phantom_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
roundtrip_test_with_id(
"agent-1",
DataValue::Tuple(ElementValues { elements: vec![] }),
Some(phantom_id),
)
}

#[test]
fn roundtrip_test_phantom_id_complex() {
let phantom_id = Uuid::parse_str("f47ac10b-58cc-4372-a567-0e02b2c3d479").unwrap();
roundtrip_test_with_id(
"agent-3",
DataValue::Tuple(ElementValues {
elements: vec![
ElementValue::ComponentModel(12u32.into_value_and_type()),
ElementValue::ComponentModel(ValueAndType::new(
Value::Record(vec![
Value::U32(1),
Value::U32(2),
Value::Flags(vec![true, false, true]),
]),
record(vec![
field("x", u32()),
field("y", u32()),
field("properties", flags(&["a", "b", "c"])),
]),
)),
],
}),
Some(phantom_id),
)
}

#[test]
fn invalid_phantom_id() {
failure_test_with_string(
"agent-1()[not-a-uuid]",
"Invalid UUID in phantom ID: invalid character: expected an optional prefix of `urn:uuid:` followed by [0-9a-fA-F-], found `n` at 1",
)
}

#[test]
fn roundtrip_without_phantom_id_maintains_none() {
roundtrip_test_with_id(
"agent-1",
DataValue::Tuple(ElementValues { elements: vec![] }),
None,
)
}

fn roundtrip_test(agent_type: &str, parameters: DataValue) {
let id = AgentId::new(agent_type.to_string(), parameters);
let id = AgentId::new(agent_type.to_string(), parameters, None);
let s = id.to_string();
println!("{s}");
let id2 = AgentId::parse(s, TestAgentTypes::new()).unwrap();
assert_eq!(id, id2);
}

fn roundtrip_test_with_id(agent_type: &str, parameters: DataValue, phantom_id: Option<Uuid>) {
let id = AgentId::new(agent_type.to_string(), parameters, phantom_id);
let s = id.to_string();
println!("{s}");
let id2 = AgentId::parse(s, TestAgentTypes::new()).unwrap();
assert_eq!(id, id2);
assert_eq!(id.phantom_id, phantom_id);
}

fn failure_test(agent_type: &str, parameters: DataValue, expected_failure: &str) {
let id = AgentId::new(agent_type.to_string(), parameters);
let id = AgentId::new(agent_type.to_string(), parameters, None);
let s = id.to_string();
let id2 = AgentId::parse(s, TestAgentTypes::new()).err().unwrap();
assert_eq!(id2, expected_failure.to_string());
}

fn failure_test_with_string(agent_id_str: &str, expected_failure: &str) {
let id2 = AgentId::parse(agent_id_str, TestAgentTypes::new())
.err()
.unwrap();
assert_eq!(id2, expected_failure.to_string());
}

struct TestAgentTypes {
types: HashMap<String, AgentType>,
}
Expand Down
6 changes: 2 additions & 4 deletions golem-common/src/model/oplog/payload/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,6 @@ oplog_payload! {
component_slug: String,
agent_name: String
},
GolemApiFork {
name: String,
},
GolemApiForkAgent {
source_agent_id: WorkerId,
target_agent_id: WorkerId,
Expand Down Expand Up @@ -231,6 +228,7 @@ oplog_payload! {
result: Result<Option<ComponentId>, String>
},
GolemApiFork {
forked_phantom_id: Uuid,
result: Result<ForkResult, String>,
},
GolemApiIdempotencyKey {
Expand Down Expand Up @@ -421,7 +419,7 @@ pub mod host_functions {
(GolemApiRevertWorker => "golem::api", "revert_worker", GolemApiRevertAgent, GolemApiUnit),
(GolemApiResolveComponentId => "golem::api", "resolve_component_id", GolemApiComponentSlug, GolemApiComponentId),
(GolemApiResolveWorkerIdStrict => "golem::api", "resolve_worker_id_strict", GolemApiComponentSlugAndAgentName, GolemApiAgentId),
(GolemApiFork => "golem::api", "fork", GolemApiFork, GolemApiFork)
(GolemApiFork => "golem::api", "fork", NoInput, GolemApiFork)
}
}

Expand Down
Loading