2026-11-02 12:36:32 +00:00
|
|
|
//! Versioned, transport-neutral wire contracts for Runa clients and servers.
|
|
|
|
|
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
pub use runa_core::Value;
|
2026-11-03 17:26:21 +00:00
|
|
|
pub use runa_core::{LogEntry, LogLevel};
|
2026-11-02 12:36:32 +00:00
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
2026-11-03 17:26:21 +00:00
|
|
|
pub const PROTOCOL_VERSION: u16 = 5;
|
2026-11-03 14:36:21 +00:00
|
|
|
pub const WEBSOCKET_SUBPROTOCOL: &str = "runa-debug";
|
|
|
|
|
|
|
|
|
|
/// Coalescible invalidation, not a drain of the image's runtime event queue.
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(deny_unknown_fields)]
|
|
|
|
|
pub struct Notification {
|
|
|
|
|
pub version: u16,
|
|
|
|
|
pub event: ImageEvent,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(tag = "event", content = "data", rename_all = "snake_case")]
|
|
|
|
|
pub enum ImageEvent {
|
|
|
|
|
ImageChanged { revision: u64 },
|
2026-11-03 17:26:21 +00:00
|
|
|
Log(LogEntry),
|
2026-11-03 14:36:21 +00:00
|
|
|
}
|
2026-11-02 12:36:32 +00:00
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(deny_unknown_fields)]
|
|
|
|
|
pub struct Request {
|
|
|
|
|
pub version: u16,
|
|
|
|
|
pub request_id: String,
|
|
|
|
|
pub command: Command,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Request {
|
|
|
|
|
pub fn validate(&self) -> Result<(), ProtocolError> {
|
|
|
|
|
if self.version != PROTOCOL_VERSION {
|
|
|
|
|
return Err(ProtocolError::new(
|
|
|
|
|
ErrorCode::UnsupportedVersion,
|
|
|
|
|
format!("unsupported protocol version {}", self.version),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
if self.request_id.trim().is_empty() {
|
|
|
|
|
return Err(ProtocolError::new(
|
|
|
|
|
ErrorCode::InvalidRequest,
|
|
|
|
|
"request_id must not be empty",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match &self.command {
|
2026-11-03 19:11:39 +00:00
|
|
|
Command::RenameSource { path, target, .. } => {
|
|
|
|
|
validate_name(path)?;
|
|
|
|
|
validate_name(target)?;
|
|
|
|
|
}
|
|
|
|
|
Command::DeleteSource { path, .. } => validate_name(path)?,
|
|
|
|
|
Command::ActorBehavior { actor_ref }
|
|
|
|
|
| Command::WriteActorBehavior { actor_ref, .. } => validate_actor_ref(actor_ref)?,
|
|
|
|
|
Command::UpdateBehaviorSource {
|
|
|
|
|
actor_ref, path, ..
|
|
|
|
|
} => {
|
|
|
|
|
validate_actor_ref(actor_ref)?;
|
|
|
|
|
validate_name(path)?;
|
|
|
|
|
}
|
2026-11-03 17:26:21 +00:00
|
|
|
Command::EditActor {
|
|
|
|
|
actor_ref,
|
|
|
|
|
state,
|
|
|
|
|
messages,
|
|
|
|
|
} => {
|
|
|
|
|
validate_actor_ref(actor_ref)?;
|
|
|
|
|
if state.is_none() && messages.is_none() {
|
|
|
|
|
return Err(invalid_request("edit must specify state or messages"));
|
|
|
|
|
}
|
|
|
|
|
if let Some(state) = state {
|
|
|
|
|
state.validate().map_err(invalid_request)?;
|
|
|
|
|
}
|
|
|
|
|
if let Some(messages) = messages {
|
|
|
|
|
for message in messages.reserved.iter().chain(&messages.queued) {
|
|
|
|
|
message.validate().map_err(invalid_request)?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Command::SetLogLevel { actor_ref, .. } => validate_actor_ref(actor_ref)?,
|
|
|
|
|
Command::Logs { actors, .. } => {
|
|
|
|
|
for actor in actors {
|
|
|
|
|
validate_actor_ref(actor)?;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-11-03 16:01:24 +00:00
|
|
|
Command::ListSources => {}
|
|
|
|
|
Command::ReadSource { path }
|
|
|
|
|
| Command::CommitSource { path }
|
|
|
|
|
| Command::SourceHistory { path } => validate_name(path)?,
|
|
|
|
|
Command::WriteSource { path, .. } => validate_name(path)?,
|
|
|
|
|
Command::RevisionSource { path, revision }
|
|
|
|
|
| Command::RollbackSource { path, revision } => {
|
|
|
|
|
validate_name(path)?;
|
|
|
|
|
if revision.is_empty()
|
|
|
|
|
|| revision.len() > 64
|
|
|
|
|
|| !revision.bytes().all(|byte| byte.is_ascii_alphanumeric())
|
|
|
|
|
{
|
|
|
|
|
return Err(invalid_request("invalid source revision"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Command::System { actor_ref, message } => {
|
|
|
|
|
validate_actor_ref(actor_ref)?;
|
|
|
|
|
if let SystemMessageDto::Become { path } = message {
|
|
|
|
|
validate_name(path)?;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-11-02 12:36:32 +00:00
|
|
|
Command::PublishBehavior {
|
|
|
|
|
name,
|
|
|
|
|
expected_revision,
|
|
|
|
|
..
|
|
|
|
|
} => {
|
|
|
|
|
validate_name(name)?;
|
|
|
|
|
validate_optional_revision(*expected_revision)?;
|
|
|
|
|
}
|
|
|
|
|
Command::RollbackBehavior {
|
|
|
|
|
name,
|
|
|
|
|
expected_revision,
|
|
|
|
|
target_revision,
|
|
|
|
|
} => {
|
|
|
|
|
validate_name(name)?;
|
|
|
|
|
validate_revision(*expected_revision)?;
|
|
|
|
|
validate_revision(*target_revision)?;
|
|
|
|
|
}
|
|
|
|
|
Command::GetBehavior { name } => validate_name(name)?,
|
|
|
|
|
Command::GetBehaviorSource { name, revision } => {
|
|
|
|
|
validate_name(name)?;
|
|
|
|
|
validate_revision(*revision)?;
|
|
|
|
|
}
|
|
|
|
|
Command::Spawn {
|
|
|
|
|
behavior_name,
|
|
|
|
|
mailbox_capacity,
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
state,
|
2026-11-02 12:36:32 +00:00
|
|
|
} => {
|
|
|
|
|
validate_name(behavior_name)?;
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
state.validate().map_err(invalid_request)?;
|
2026-11-02 12:36:32 +00:00
|
|
|
if *mailbox_capacity == 0 {
|
|
|
|
|
return Err(invalid_request("mailbox_capacity must be positive"));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-11-02 14:19:25 +00:00
|
|
|
Command::Send { actor_ref, message } => {
|
|
|
|
|
validate_actor_ref(actor_ref)?;
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
message.validate().map_err(invalid_request)?;
|
2026-11-02 14:19:25 +00:00
|
|
|
}
|
|
|
|
|
Command::InspectActor { actor_ref }
|
|
|
|
|
| Command::StopActor { actor_ref }
|
|
|
|
|
| Command::RestartActor { actor_ref }
|
|
|
|
|
| Command::PauseActor { actor_ref }
|
|
|
|
|
| Command::ResumeActor { actor_ref }
|
|
|
|
|
| Command::CancelActor { actor_ref } => validate_actor_ref(actor_ref)?,
|
2026-11-02 12:36:32 +00:00
|
|
|
Command::InstallBehavior {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref,
|
2026-11-02 12:36:32 +00:00
|
|
|
behavior_name,
|
|
|
|
|
} => {
|
2026-11-02 14:19:25 +00:00
|
|
|
validate_actor_ref(actor_ref)?;
|
2026-11-02 12:36:32 +00:00
|
|
|
validate_name(behavior_name)?;
|
|
|
|
|
}
|
|
|
|
|
Command::Tick {
|
|
|
|
|
instruction_budget, ..
|
|
|
|
|
} if *instruction_budget == 0 => {
|
|
|
|
|
return Err(invalid_request("instruction_budget must be positive"));
|
|
|
|
|
}
|
|
|
|
|
Command::GetBehaviors
|
|
|
|
|
| Command::ListActors
|
|
|
|
|
| Command::GetEvents
|
|
|
|
|
| Command::Tick { .. } => {}
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(tag = "operation", content = "parameters", rename_all = "snake_case")]
|
|
|
|
|
pub enum Command {
|
2026-11-03 19:11:39 +00:00
|
|
|
RenameSource {
|
|
|
|
|
path: String,
|
|
|
|
|
target: String,
|
|
|
|
|
expected_source_id: String,
|
|
|
|
|
update_actors: bool,
|
|
|
|
|
},
|
|
|
|
|
DeleteSource {
|
|
|
|
|
path: String,
|
|
|
|
|
expected_source_id: String,
|
|
|
|
|
},
|
|
|
|
|
ActorBehavior {
|
|
|
|
|
actor_ref: String,
|
|
|
|
|
},
|
|
|
|
|
WriteActorBehavior {
|
|
|
|
|
actor_ref: String,
|
|
|
|
|
expected_source_id: String,
|
|
|
|
|
source: String,
|
|
|
|
|
},
|
|
|
|
|
UpdateBehaviorSource {
|
|
|
|
|
actor_ref: String,
|
|
|
|
|
path: String,
|
|
|
|
|
expected_source_id: String,
|
|
|
|
|
temporary_source_id: String,
|
|
|
|
|
},
|
2026-11-03 17:26:21 +00:00
|
|
|
EditActor {
|
|
|
|
|
actor_ref: String,
|
|
|
|
|
state: Option<Value>,
|
|
|
|
|
messages: Option<MailboxEdit>,
|
|
|
|
|
},
|
|
|
|
|
SetLogLevel {
|
|
|
|
|
actor_ref: String,
|
|
|
|
|
level: LogLevel,
|
|
|
|
|
},
|
|
|
|
|
Logs {
|
|
|
|
|
actors: Vec<String>,
|
|
|
|
|
follow: bool,
|
|
|
|
|
after: u64,
|
|
|
|
|
},
|
2026-11-03 16:01:24 +00:00
|
|
|
ListSources,
|
|
|
|
|
ReadSource {
|
|
|
|
|
path: String,
|
|
|
|
|
},
|
|
|
|
|
WriteSource {
|
|
|
|
|
path: String,
|
|
|
|
|
expected_source_id: Option<String>,
|
|
|
|
|
source: String,
|
|
|
|
|
},
|
|
|
|
|
CommitSource {
|
|
|
|
|
path: String,
|
|
|
|
|
},
|
|
|
|
|
SourceHistory {
|
|
|
|
|
path: String,
|
|
|
|
|
},
|
|
|
|
|
RevisionSource {
|
|
|
|
|
path: String,
|
|
|
|
|
revision: String,
|
|
|
|
|
},
|
|
|
|
|
RollbackSource {
|
|
|
|
|
path: String,
|
|
|
|
|
revision: String,
|
|
|
|
|
},
|
|
|
|
|
System {
|
|
|
|
|
actor_ref: String,
|
|
|
|
|
message: SystemMessageDto,
|
|
|
|
|
},
|
|
|
|
|
#[serde(skip)]
|
2026-11-02 12:36:32 +00:00
|
|
|
PublishBehavior {
|
|
|
|
|
name: String,
|
|
|
|
|
expected_revision: Option<u64>,
|
|
|
|
|
source: String,
|
|
|
|
|
},
|
2026-11-03 16:01:24 +00:00
|
|
|
#[serde(skip)]
|
2026-11-02 12:36:32 +00:00
|
|
|
RollbackBehavior {
|
|
|
|
|
name: String,
|
|
|
|
|
expected_revision: u64,
|
|
|
|
|
target_revision: u64,
|
|
|
|
|
},
|
2026-11-03 16:01:24 +00:00
|
|
|
#[serde(skip)]
|
2026-11-02 12:36:32 +00:00
|
|
|
GetBehavior {
|
|
|
|
|
name: String,
|
|
|
|
|
},
|
2026-11-03 16:01:24 +00:00
|
|
|
#[serde(skip)]
|
2026-11-02 12:36:32 +00:00
|
|
|
GetBehaviors,
|
2026-11-03 16:01:24 +00:00
|
|
|
#[serde(skip)]
|
2026-11-02 12:36:32 +00:00
|
|
|
GetBehaviorSource {
|
|
|
|
|
name: String,
|
|
|
|
|
revision: u64,
|
|
|
|
|
},
|
|
|
|
|
Spawn {
|
2026-11-03 16:01:24 +00:00
|
|
|
#[serde(rename = "path")]
|
2026-11-02 12:36:32 +00:00
|
|
|
behavior_name: String,
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
state: Value,
|
2026-11-02 12:36:32 +00:00
|
|
|
mailbox_capacity: usize,
|
|
|
|
|
},
|
|
|
|
|
Send {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
message: MessageDto,
|
|
|
|
|
},
|
|
|
|
|
Tick {
|
|
|
|
|
ticks: usize,
|
|
|
|
|
instruction_budget: usize,
|
|
|
|
|
},
|
|
|
|
|
InspectActor {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
ListActors,
|
|
|
|
|
GetEvents,
|
|
|
|
|
StopActor {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
RestartActor {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
PauseActor {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
ResumeActor {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
CancelActor {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
2026-11-03 16:01:24 +00:00
|
|
|
#[serde(skip)]
|
2026-11-02 12:36:32 +00:00
|
|
|
InstallBehavior {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
behavior_name: String,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-03 17:26:21 +00:00
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(deny_unknown_fields)]
|
|
|
|
|
pub struct MailboxEdit {
|
|
|
|
|
pub reserved: Option<Value>,
|
|
|
|
|
pub queued: Vec<Value>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 12:36:32 +00:00
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(deny_unknown_fields)]
|
|
|
|
|
pub struct Response {
|
|
|
|
|
pub version: u16,
|
|
|
|
|
pub request_id: String,
|
|
|
|
|
pub result: Result<Reply, ProtocolError>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(tag = "kind", content = "data", rename_all = "snake_case")]
|
|
|
|
|
pub enum Reply {
|
2026-11-03 19:11:39 +00:00
|
|
|
ActorBehavior {
|
|
|
|
|
source_id: String,
|
|
|
|
|
source: String,
|
|
|
|
|
},
|
2026-11-03 17:26:21 +00:00
|
|
|
Logs {
|
|
|
|
|
entries: Vec<LogEntry>,
|
|
|
|
|
next_sequence: u64,
|
|
|
|
|
has_more: bool,
|
|
|
|
|
},
|
2026-11-03 16:01:24 +00:00
|
|
|
Sources(Vec<SourceFileDto>),
|
|
|
|
|
Source(SourceFileDto),
|
|
|
|
|
SourceCommitted(SourceCommitDto),
|
|
|
|
|
SourceHistory(Vec<SourceCommitDto>),
|
2026-11-02 12:36:32 +00:00
|
|
|
Acknowledged,
|
|
|
|
|
Behavior(BehaviorDto),
|
|
|
|
|
Behaviors(Vec<BehaviorDto>),
|
2026-11-03 17:26:21 +00:00
|
|
|
BehaviorSource {
|
|
|
|
|
source: String,
|
|
|
|
|
},
|
|
|
|
|
ActorSpawned {
|
|
|
|
|
actor_ref: String,
|
|
|
|
|
},
|
2026-11-02 12:36:32 +00:00
|
|
|
Actor(ActorSnapshotDto),
|
|
|
|
|
Actors(Vec<ActorSnapshotDto>),
|
|
|
|
|
Events(Vec<EventDto>),
|
2026-11-03 17:26:21 +00:00
|
|
|
TickCompleted {
|
|
|
|
|
ticks: usize,
|
|
|
|
|
},
|
|
|
|
|
BehaviorInstalled {
|
|
|
|
|
implementation_id: String,
|
|
|
|
|
},
|
2026-11-02 12:36:32 +00:00
|
|
|
}
|
|
|
|
|
|
2026-11-03 16:01:24 +00:00
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(deny_unknown_fields)]
|
|
|
|
|
pub struct SourceFileDto {
|
|
|
|
|
pub path: String,
|
|
|
|
|
pub source_id: String,
|
|
|
|
|
pub source: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(deny_unknown_fields)]
|
|
|
|
|
pub struct SourceCommitDto {
|
|
|
|
|
pub revision: String,
|
|
|
|
|
pub source_id: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
|
|
|
|
pub enum SystemMessageDto {
|
|
|
|
|
Stop,
|
|
|
|
|
Kill,
|
|
|
|
|
Restart,
|
|
|
|
|
Pause,
|
|
|
|
|
Resume,
|
|
|
|
|
Reload,
|
|
|
|
|
Become { path: String },
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 12:36:32 +00:00
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(deny_unknown_fields)]
|
|
|
|
|
pub struct ProtocolEvent {
|
|
|
|
|
pub version: u16,
|
|
|
|
|
pub event: EventDto,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(tag = "event", content = "data", rename_all = "snake_case")]
|
|
|
|
|
pub enum EventDto {
|
|
|
|
|
Spawned {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
Committed {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
Failed {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
message: String,
|
|
|
|
|
},
|
|
|
|
|
Stopped {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
Restarted {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
Paused {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
Resumed {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
InvocationCancelled {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
BehaviorInstalled {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
implementation_id: String,
|
|
|
|
|
},
|
|
|
|
|
StateMigrated {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
previous_state: Value,
|
|
|
|
|
current_state: Value,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(deny_unknown_fields)]
|
|
|
|
|
pub struct BehaviorDto {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub revision: u64,
|
|
|
|
|
pub current_implementation_id: String,
|
|
|
|
|
pub history: Vec<BehaviorVersionDto>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(deny_unknown_fields)]
|
|
|
|
|
pub struct BehaviorVersionDto {
|
|
|
|
|
pub revision: u64,
|
|
|
|
|
pub implementation_id: String,
|
|
|
|
|
}
|
|
|
|
|
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
pub type MessageDto = Value;
|
2026-11-02 12:36:32 +00:00
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(deny_unknown_fields)]
|
|
|
|
|
pub struct ActorSnapshotDto {
|
2026-11-03 19:11:39 +00:00
|
|
|
#[serde(default)]
|
|
|
|
|
pub pending_behavior_path: Option<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub behavior_path: Option<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub temporary_behavior: bool,
|
2026-11-03 17:26:21 +00:00
|
|
|
pub log_level: LogLevel,
|
2026-11-02 14:19:25 +00:00
|
|
|
pub actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
pub mailbox_id: u64,
|
|
|
|
|
pub implementation_id: String,
|
|
|
|
|
pub incarnation: u64,
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
pub state: Value,
|
2026-11-02 12:36:32 +00:00
|
|
|
pub status: ActorStatusDto,
|
|
|
|
|
pub execution: Option<ExecutionSnapshotDto>,
|
|
|
|
|
pub queued: Vec<MessageDto>,
|
|
|
|
|
pub reserved: Option<MessageDto>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(tag = "status", content = "message", rename_all = "snake_case")]
|
|
|
|
|
pub enum ActorStatusDto {
|
|
|
|
|
Idle,
|
|
|
|
|
Running,
|
|
|
|
|
Awaiting,
|
|
|
|
|
Paused,
|
|
|
|
|
Stopped,
|
|
|
|
|
Failed(String),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(deny_unknown_fields)]
|
|
|
|
|
pub struct ExecutionSnapshotDto {
|
|
|
|
|
pub vm_steps: u64,
|
|
|
|
|
pub instruction_pointer: usize,
|
|
|
|
|
pub call_depth: usize,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(rename_all = "snake_case")]
|
|
|
|
|
pub enum ErrorCode {
|
|
|
|
|
InvalidRequest,
|
|
|
|
|
UnsupportedVersion,
|
|
|
|
|
NotFound,
|
|
|
|
|
Conflict,
|
|
|
|
|
CapacityExceeded,
|
|
|
|
|
RuntimeFailure,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(deny_unknown_fields)]
|
|
|
|
|
pub struct ProtocolError {
|
|
|
|
|
pub code: ErrorCode,
|
|
|
|
|
pub message: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ProtocolError {
|
|
|
|
|
pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
code,
|
|
|
|
|
message: message.into(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn validate_name(name: &str) -> Result<(), ProtocolError> {
|
|
|
|
|
if name.trim().is_empty() {
|
|
|
|
|
return Err(invalid_request("behavior name must not be empty"));
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 14:19:25 +00:00
|
|
|
fn validate_actor_ref(actor_ref: &str) -> Result<(), ProtocolError> {
|
|
|
|
|
if actor_ref.len() != 21
|
|
|
|
|
|| !actor_ref
|
|
|
|
|
.bytes()
|
|
|
|
|
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
|
|
|
|
|
{
|
|
|
|
|
return Err(invalid_request(
|
|
|
|
|
"actor_ref must be a 21-character URL-safe identifier",
|
|
|
|
|
));
|
2026-11-02 12:36:32 +00:00
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn validate_revision(revision: u64) -> Result<(), ProtocolError> {
|
|
|
|
|
if revision == 0 {
|
|
|
|
|
return Err(invalid_request("revision must be positive"));
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn validate_optional_revision(revision: Option<u64>) -> Result<(), ProtocolError> {
|
|
|
|
|
if let Some(revision) = revision {
|
|
|
|
|
validate_revision(revision)?;
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn invalid_request(message: impl Into<String>) -> ProtocolError {
|
|
|
|
|
ProtocolError::new(ErrorCode::InvalidRequest, message)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
fn test_message(kind: i64, value: i64, target: Option<runa_core::ActorRef>) -> Value {
|
|
|
|
|
Value::Record(std::collections::BTreeMap::from([
|
|
|
|
|
("kind".to_owned(), Value::Integer(kind)),
|
|
|
|
|
("value".to_owned(), Value::Integer(value)),
|
|
|
|
|
(
|
|
|
|
|
"target".to_owned(),
|
|
|
|
|
target.map_or(Value::Null, Value::ActorRef),
|
|
|
|
|
),
|
|
|
|
|
]))
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 12:36:32 +00:00
|
|
|
fn request(command: Command) -> Request {
|
|
|
|
|
Request {
|
|
|
|
|
version: PROTOCOL_VERSION,
|
|
|
|
|
request_id: "req-1".to_owned(),
|
|
|
|
|
command,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn request_uses_a_stable_tagged_wire_shape() {
|
|
|
|
|
let value = serde_json::to_value(request(Command::Send {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: "xK8_pQ2-Lm5_012345678".to_owned(),
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
message: Value::Integer(9),
|
2026-11-02 12:36:32 +00:00
|
|
|
}))
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
value,
|
|
|
|
|
serde_json::json!({
|
|
|
|
|
"version": PROTOCOL_VERSION,
|
|
|
|
|
"request_id": "req-1",
|
|
|
|
|
"command": {
|
|
|
|
|
"operation": "send",
|
|
|
|
|
"parameters": {
|
2026-11-02 14:19:25 +00:00
|
|
|
"actor_ref": "xK8_pQ2-Lm5_012345678",
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
"message": { "type": "integer", "data": 9 }
|
2026-11-02 12:36:32 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn requests_validate_versions_and_required_positive_values() {
|
|
|
|
|
let mut request = request(Command::Spawn {
|
|
|
|
|
behavior_name: "counter".to_owned(),
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
state: Value::Integer(0),
|
2026-11-02 12:36:32 +00:00
|
|
|
mailbox_capacity: 1,
|
|
|
|
|
});
|
|
|
|
|
assert_eq!(request.validate(), Ok(()));
|
|
|
|
|
|
|
|
|
|
request.version += 1;
|
|
|
|
|
assert_eq!(
|
|
|
|
|
request.validate().unwrap_err().code,
|
|
|
|
|
ErrorCode::UnsupportedVersion
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
request.version = PROTOCOL_VERSION;
|
|
|
|
|
request.command = Command::Spawn {
|
|
|
|
|
behavior_name: "counter".to_owned(),
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
state: Value::Integer(0),
|
2026-11-02 12:36:32 +00:00
|
|
|
mailbox_capacity: 0,
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(
|
|
|
|
|
request.validate().unwrap_err().code,
|
|
|
|
|
ErrorCode::InvalidRequest
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-11-02 14:19:25 +00:00
|
|
|
fn requests_reject_empty_correlation_ids_and_malformed_actor_refs() {
|
2026-11-02 12:36:32 +00:00
|
|
|
let mut request = request(Command::GetEvents);
|
|
|
|
|
request.request_id = " ".to_owned();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
request.validate().unwrap_err().code,
|
|
|
|
|
ErrorCode::InvalidRequest
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
request.request_id = "req-2".to_owned();
|
2026-11-02 14:19:25 +00:00
|
|
|
request.command = Command::StopActor {
|
|
|
|
|
actor_ref: String::new(),
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(
|
|
|
|
|
request.validate().unwrap_err().code,
|
|
|
|
|
ErrorCode::InvalidRequest
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
fn nested_references_round_trip_and_malformed_references_are_rejected() {
|
|
|
|
|
let request = request(Command::Send {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: "xK8_pQ2-Lm5_012345678".to_owned(),
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
message: test_message(2, 9, runa_core::ActorRef::parse("Hj2_9sM0-pQ4_7aBcD3eF")),
|
2026-11-02 14:19:25 +00:00
|
|
|
});
|
|
|
|
|
assert_eq!(request.validate(), Ok(()));
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
let encoded = serde_json::to_string(&request).unwrap();
|
|
|
|
|
assert_eq!(serde_json::from_str::<Request>(&encoded).unwrap(), request);
|
|
|
|
|
assert!(serde_json::from_str::<Value>(r#"{"type":"actor_ref","data":"bad"}"#).is_err());
|
2026-11-02 12:36:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn response_round_trips_with_typed_error_or_reply() {
|
|
|
|
|
let response = Response {
|
|
|
|
|
version: PROTOCOL_VERSION,
|
|
|
|
|
request_id: "req-1".to_owned(),
|
|
|
|
|
result: Err(ProtocolError::new(ErrorCode::NotFound, "unknown actor")),
|
|
|
|
|
};
|
|
|
|
|
let encoded = serde_json::to_string(&response).unwrap();
|
|
|
|
|
let decoded: Response = serde_json::from_str(&encoded).unwrap();
|
|
|
|
|
assert_eq!(decoded, response);
|
|
|
|
|
}
|
|
|
|
|
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
#[test]
|
|
|
|
|
fn structured_values_have_unambiguous_wire_types_and_reject_invalid_data() {
|
|
|
|
|
let reference = runa_core::ActorRef::new();
|
|
|
|
|
let value = Value::Record(std::collections::BTreeMap::from([(
|
|
|
|
|
"values".to_owned(),
|
|
|
|
|
Value::List(vec![
|
|
|
|
|
Value::Null,
|
|
|
|
|
Value::Bool(false),
|
|
|
|
|
Value::Integer(i64::MAX),
|
|
|
|
|
Value::String(reference.to_string()),
|
|
|
|
|
Value::ActorRef(reference),
|
|
|
|
|
]),
|
|
|
|
|
)]));
|
|
|
|
|
let encoded = serde_json::to_string(&value).unwrap();
|
|
|
|
|
assert_eq!(serde_json::from_str::<Value>(&encoded).unwrap(), value);
|
|
|
|
|
for encoded in [
|
|
|
|
|
r#"{"type":"integer","data":1.5}"#,
|
|
|
|
|
r#"{"type":"integer","data":9223372036854775808}"#,
|
|
|
|
|
r#"{"type":"actor_ref","data":"invalid"}"#,
|
|
|
|
|
r#"{"type":"unknown","data":1}"#,
|
|
|
|
|
r#"{"type":"bool","data":true,"extra":1}"#,
|
|
|
|
|
] {
|
|
|
|
|
assert!(serde_json::from_str::<Value>(encoded).is_err(), "{encoded}");
|
|
|
|
|
}
|
|
|
|
|
let invalid = Value::List(vec![Value::Null; runa_core::MAX_VALUE_NODES]);
|
|
|
|
|
let spawn = request(Command::Spawn {
|
|
|
|
|
behavior_name: "echo".to_owned(),
|
|
|
|
|
state: invalid.clone(),
|
|
|
|
|
mailbox_capacity: 2,
|
|
|
|
|
});
|
|
|
|
|
assert_eq!(
|
|
|
|
|
spawn.validate().unwrap_err().code,
|
|
|
|
|
ErrorCode::InvalidRequest
|
|
|
|
|
);
|
|
|
|
|
let send = request(Command::Send {
|
|
|
|
|
actor_ref: reference.to_string(),
|
|
|
|
|
message: invalid,
|
|
|
|
|
});
|
|
|
|
|
assert_eq!(send.validate().unwrap_err().code, ErrorCode::InvalidRequest);
|
|
|
|
|
let mut old = request(Command::ListActors);
|
|
|
|
|
old.version = 2;
|
|
|
|
|
assert_eq!(
|
|
|
|
|
old.validate().unwrap_err().code,
|
|
|
|
|
ErrorCode::UnsupportedVersion
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 12:36:32 +00:00
|
|
|
#[test]
|
|
|
|
|
fn event_envelopes_are_versioned_and_serializable() {
|
|
|
|
|
let event = ProtocolEvent {
|
|
|
|
|
version: PROTOCOL_VERSION,
|
|
|
|
|
event: EventDto::StateMigrated {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: "actor_ref_000000000000".to_owned(),
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
previous_state: Value::Integer(4),
|
|
|
|
|
current_state: Value::Integer(40),
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
let encoded = serde_json::to_string(&event).unwrap();
|
|
|
|
|
let decoded: ProtocolEvent = serde_json::from_str(&encoded).unwrap();
|
|
|
|
|
assert_eq!(decoded, event);
|
|
|
|
|
}
|
|
|
|
|
}
|