refactor: route CLI through protocol dispatcher

Replace the CLI's direct ImageApi dependency with protocol commands dispatched through runa-server. Render typed protocol replies for behavior, actor, event, lifecycle, and scheduler operations, preserving the interactive workflow while exercising the same contract used by the local JSON-lines transport.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Steve Biedermann 2026-11-02 14:30:22 +01:00
parent 3146423f78
commit ce7bb5602a
7 changed files with 251 additions and 135 deletions

View File

@ -62,6 +62,8 @@ All notable changes to this project will be documented in this file.
### Changed ### Changed
- Interactive CLI commands now use the versioned protocol dispatcher instead
of calling the image API directly.
- Actor implementation IDs now identify compiled source/configuration content - Actor implementation IDs now identify compiled source/configuration content
rather than using sequential process-local numbers. rather than using sequential process-local numbers.
- Expanded the architecture specification with crate ownership and dependency - Expanded the architecture specification with crate ownership and dependency

8
Cargo.lock generated
View File

@ -392,12 +392,8 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
name = "runa" name = "runa"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"runa-actor", "runa-protocol",
"runa-code", "runa-server",
"runa-core",
"runa-image-api",
"runa-rune",
"runa-runtime",
] ]
[[package]] [[package]]

View File

@ -20,7 +20,9 @@ registry, and the CLI exposes publishing, history, rollback, safe-boundary
installation, pause/resume, and cancellation through a trusted in-process image installation, pause/resume, and cancellation through a trusted in-process image
API. A versioned transport-neutral protocol contract is now defined, but no API. A versioned transport-neutral protocol contract is now defined, but no
network listener or authentication is implemented. The dispatcher is available network listener or authentication is implemented. The dispatcher is available
through a local JSON-lines stdio process, with a bounded request size. through a local JSON-lines stdio process, with a bounded request size. The
interactive CLI now uses the versioned protocol and dispatcher rather than
calling the image API directly.
The prototype uses worker-local invocation stacks and cooperative instruction The prototype uses worker-local invocation stacks and cooperative instruction
slices, and actor snapshots report basic active-execution progress. Native slices, and actor snapshots report basic active-execution progress. Native

View File

@ -4,9 +4,5 @@ version = "0.0.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
runa-actor = { path = "../runa-actor" } runa-protocol = { path = "../runa-protocol" }
runa-code = { path = "../runa-code" } runa-server = { path = "../runa-server" }
runa-core = { path = "../runa-core" }
runa-image-api = { path = "../runa-image-api" }
runa-rune = { path = "../runa-rune" }
runa-runtime = { path = "../runa-runtime" }

View File

@ -1,21 +1,26 @@
use std::io::{self, BufRead, Write}; use std::io::{self, BufRead, Write};
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use runa_actor::{ActorId, Message}; use runa_protocol::{Command, MessageDto, PROTOCOL_VERSION, Reply, Request, Response};
use runa_image_api::ImageApi; use runa_server::Server;
use runa_rune::ExperimentResult;
const MAILBOX_CAPACITY: usize = 64; const MAILBOX_CAPACITY: usize = 64;
const DEFAULT_TICK_BUDGET: usize = 32; const DEFAULT_TICK_BUDGET: usize = 32;
const DEFAULT_BEHAVIOR: &str = "counter"; const DEFAULT_BEHAVIOR: &str = "counter";
fn main() -> ExperimentResult<()> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut image = ImageApi::new(); let mut server = Server::new();
let behavior = image.publish_behavior( let behavior = match dispatch(
DEFAULT_BEHAVIOR, &mut server,
None, Command::PublishBehavior {
include_str!("../../../runa-std/actors/counter.rn"), name: DEFAULT_BEHAVIOR.to_owned(),
)?; expected_revision: None,
source: include_str!("../../../runa-std/actors/counter.rn").to_owned(),
},
)? {
Reply::Behavior(behavior) => behavior,
other => return Err(format!("unexpected publish response: {other:?}").into()),
};
println!("Runa — Rune Actors"); println!("Runa — Rune Actors");
println!( println!(
@ -26,7 +31,7 @@ fn main() -> ExperimentResult<()> {
run_repl( run_repl(
io::stdin().lock(), io::stdin().lock(),
io::stdout().lock(), io::stdout().lock(),
&mut image, &mut server,
DEFAULT_BEHAVIOR, DEFAULT_BEHAVIOR,
)?; )?;
Ok(()) Ok(())
@ -35,7 +40,7 @@ fn main() -> ExperimentResult<()> {
fn run_repl<R: BufRead, W: Write>( fn run_repl<R: BufRead, W: Write>(
mut input: R, mut input: R,
mut output: W, mut output: W,
image: &mut ImageApi, server: &mut Server,
default_behavior: &str, default_behavior: &str,
) -> io::Result<()> { ) -> io::Result<()> {
let mut line = String::new(); let mut line = String::new();
@ -54,33 +59,48 @@ fn run_repl<R: BufRead, W: Write>(
let args: Vec<_> = words.collect(); let args: Vec<_> = words.collect();
let result = match command { let result = match command {
"help" => print_help(&mut output), "help" => print_help(&mut output),
"spawn" => spawn_command(&mut output, &args, default_behavior, image), "spawn" => spawn_command(&mut output, &args, default_behavior, server),
"behaviors" if args.is_empty() => { "behaviors" if args.is_empty() => match dispatch(server, Command::GetBehaviors) {
for behavior in image.behaviors() { Ok(Reply::Behaviors(behaviors)) => {
writeln!( for behavior in behaviors {
output, writeln!(
"{} revision {} ({})", output,
behavior.name, behavior.revision, behavior.current "{} revision {} ({})",
)?; behavior.name, behavior.revision, behavior.current_implementation_id
)?;
}
Ok(())
} }
Ok(()) Ok(other) => unexpected_reply(&mut output, other),
} Err(error) => writeln!(output, "error: {error}"),
},
"behaviors" => writeln!(output, "error: usage: behaviors"), "behaviors" => writeln!(output, "error: usage: behaviors"),
"publish" => publish_command(&mut output, &args, image), "publish" => publish_command(&mut output, &args, server),
"history" => history_command(&mut output, &args, image), "history" => history_command(&mut output, &args, server),
"rollback" => rollback_command(&mut output, &args, image), "rollback" => rollback_command(&mut output, &args, server),
"install" => install_command(&mut output, &args, image), "install" => install_command(&mut output, &args, server),
"actors" if args.is_empty() => { "actors" if args.is_empty() => match dispatch(server, Command::ListActors) {
for actor in image.actors() { Ok(Reply::Actors(actors)) => {
writeln!(output, "{actor:#?}")?; for actor in actors {
writeln!(output, "{actor:#?}")?;
}
Ok(())
} }
Ok(()) Ok(other) => unexpected_reply(&mut output, other),
} Err(error) => writeln!(output, "error: {error}"),
},
"actors" => writeln!(output, "error: usage: actors"), "actors" => writeln!(output, "error: usage: actors"),
"send" => match parse_send_args(&args) { "send" => match parse_send_args(&args) {
Ok((actor, kind, value)) => { Ok((actor, kind, value)) => {
match image.send(ActorId(actor), Message { kind, value }) { match dispatch(
Ok(()) => writeln!(output, "message queued"), server,
Command::Send {
actor_id: actor,
message: MessageDto { kind, value },
},
) {
Ok(Reply::Acknowledged) => writeln!(output, "message queued"),
Ok(other) => unexpected_reply(&mut output, other),
Err(error) => writeln!(output, "error: {error}"), Err(error) => writeln!(output, "error: {error}"),
} }
} }
@ -88,32 +108,56 @@ fn run_repl<R: BufRead, W: Write>(
}, },
"tick" => match parse_tick_args(&args) { "tick" => match parse_tick_args(&args) {
Ok((count, budget)) => { Ok((count, budget)) => {
image.tick(count, budget); match dispatch(
writeln!(output, "ran {count} scheduler tick(s)") server,
Command::Tick {
ticks: count,
instruction_budget: budget.get(),
},
) {
Ok(Reply::TickCompleted { ticks }) => {
writeln!(output, "ran {ticks} scheduler tick(s)")
}
Ok(other) => unexpected_reply(&mut output, other),
Err(error) => writeln!(output, "error: {error}"),
}
} }
Err(error) => writeln!(output, "error: {error}"), Err(error) => writeln!(output, "error: {error}"),
}, },
"inspect" => match parse_exact::<u64, 1>(&args) { "inspect" => match parse_exact::<u64, 1>(&args) {
Ok([id]) => match image.inspect(ActorId(id)) { Ok([id]) => match dispatch(server, Command::InspectActor { actor_id: id }) {
Ok(actor) => writeln!(output, "{actor:#?}"), Ok(Reply::Actor(actor)) => writeln!(output, "{actor:#?}"),
Ok(other) => unexpected_reply(&mut output, other),
Err(error) => writeln!(output, "error: {error}"), Err(error) => writeln!(output, "error: {error}"),
}, },
Err(error) => writeln!(output, "error: {error}"), Err(error) => writeln!(output, "error: {error}"),
}, },
"events" if args.is_empty() => { "events" if args.is_empty() => match dispatch(server, Command::GetEvents) {
for event in image.events() { Ok(Reply::Events(events)) => {
writeln!(output, "{event:#?}")?; for event in events {
writeln!(output, "{event:#?}")?;
}
Ok(())
} }
Ok(()) Ok(other) => unexpected_reply(&mut output, other),
} Err(error) => writeln!(output, "error: {error}"),
},
"events" => writeln!(output, "error: usage: events"), "events" => writeln!(output, "error: usage: events"),
"stop" => lifecycle_command(&mut output, &args, |image, id| image.stop(id), image), "stop" => lifecycle_command(&mut output, &args, server, |actor_id| {
"restart" => { Command::StopActor { actor_id }
lifecycle_command(&mut output, &args, |image, id| image.restart(id), image) }),
} "restart" => lifecycle_command(&mut output, &args, server, |actor_id| {
"pause" => lifecycle_command(&mut output, &args, |image, id| image.pause(id), image), Command::RestartActor { actor_id }
"resume" => lifecycle_command(&mut output, &args, |image, id| image.resume(id), image), }),
"cancel" => lifecycle_command(&mut output, &args, |image, id| image.cancel(id), image), "pause" => lifecycle_command(&mut output, &args, server, |actor_id| {
Command::PauseActor { actor_id }
}),
"resume" => lifecycle_command(&mut output, &args, server, |actor_id| {
Command::ResumeActor { actor_id }
}),
"cancel" => lifecycle_command(&mut output, &args, server, |actor_id| {
Command::CancelActor { actor_id }
}),
"quit" | "exit" if args.is_empty() => break, "quit" | "exit" if args.is_empty() => break,
"quit" | "exit" => writeln!(output, "error: usage: quit"), "quit" | "exit" => writeln!(output, "error: usage: quit"),
_ => writeln!(output, "error: unknown command `{command}`; type `help`"), _ => writeln!(output, "error: unknown command `{command}`; type `help`"),
@ -126,12 +170,13 @@ fn run_repl<R: BufRead, W: Write>(
fn lifecycle_command<W: Write>( fn lifecycle_command<W: Write>(
output: &mut W, output: &mut W,
args: &[&str], args: &[&str],
operation: impl FnOnce(&mut ImageApi, ActorId) -> ExperimentResult<()>, server: &mut Server,
image: &mut ImageApi, operation: impl FnOnce(u64) -> Command,
) -> io::Result<()> { ) -> io::Result<()> {
match parse_exact::<u64, 1>(args) { match parse_exact::<u64, 1>(args) {
Ok([id]) => match operation(image, ActorId(id)) { Ok([id]) => match dispatch(server, operation(id)) {
Ok(()) => writeln!(output, "ok"), Ok(Reply::Acknowledged) => writeln!(output, "ok"),
Ok(other) => unexpected_reply(output, other),
Err(error) => writeln!(output, "error: {error}"), Err(error) => writeln!(output, "error: {error}"),
}, },
Err(error) => writeln!(output, "error: {error}"), Err(error) => writeln!(output, "error: {error}"),
@ -142,7 +187,7 @@ fn spawn_command<W: Write>(
output: &mut W, output: &mut W,
args: &[&str], args: &[&str],
default_behavior: &str, default_behavior: &str,
image: &mut ImageApi, server: &mut Server,
) -> io::Result<()> { ) -> io::Result<()> {
let (state_arg, behavior_name) = match args { let (state_arg, behavior_name) = match args {
[state] => (*state, default_behavior), [state] => (*state, default_behavior),
@ -158,28 +203,37 @@ fn spawn_command<W: Write>(
Ok(state) => state, Ok(state) => state,
Err(_) => return writeln!(output, "error: invalid initial state: `{state_arg}`"), Err(_) => return writeln!(output, "error: invalid initial state: `{state_arg}`"),
}; };
match image.spawn( let response = dispatch(
behavior_name, server,
state, Command::Spawn {
NonZeroUsize::new(MAILBOX_CAPACITY).expect("capacity is nonzero"), behavior_name: behavior_name.to_owned(),
) { state,
Ok(id) => writeln!( mailbox_capacity: MAILBOX_CAPACITY,
output, },
"spawned {id:?} with `{behavior_name}` revision {}", );
image match response {
.behavior(behavior_name) Ok(Reply::ActorSpawned { actor_id }) => {
.expect("spawn resolved behavior") let revision = match dispatch(
.revision server,
), Command::GetBehavior {
name: behavior_name.to_owned(),
},
) {
Ok(Reply::Behavior(behavior)) => behavior.revision,
Ok(other) => return unexpected_reply(output, other),
Err(error) => return writeln!(output, "error: {error}"),
};
writeln!(
output,
"spawned ActorId({actor_id}) with `{behavior_name}` revision {revision}"
)
}
Ok(other) => unexpected_reply(output, other),
Err(error) => writeln!(output, "error: {error}"), Err(error) => writeln!(output, "error: {error}"),
} }
} }
fn publish_command<W: Write>( fn publish_command<W: Write>(output: &mut W, args: &[&str], server: &mut Server) -> io::Result<()> {
output: &mut W,
args: &[&str],
image: &mut ImageApi,
) -> io::Result<()> {
let [name, expected_revision, source_path] = args else { let [name, expected_revision, source_path] = args else {
return writeln!( return writeln!(
output, output,
@ -200,23 +254,38 @@ fn publish_command<W: Write>(
return writeln!(output, "error: could not read `{source_path}`: {error}"); return writeln!(output, "error: could not read `{source_path}`: {error}");
} }
}; };
let behavior = match image.publish_behavior(name, expected_revision, &source) { let behavior = match dispatch(
Ok(behavior) => behavior, server,
Command::PublishBehavior {
name: (*name).to_owned(),
expected_revision,
source,
},
) {
Ok(Reply::Behavior(behavior)) => behavior,
Ok(other) => return unexpected_reply(output, other),
Err(error) => return writeln!(output, "error: {error}"), Err(error) => return writeln!(output, "error: {error}"),
}; };
writeln!( writeln!(
output, output,
"published `{}` revision {} ({})", "published `{}` revision {} ({})",
behavior.name, behavior.revision, behavior.current behavior.name, behavior.revision, behavior.current_implementation_id
) )
} }
fn history_command<W: Write>(output: &mut W, args: &[&str], image: &ImageApi) -> io::Result<()> { fn history_command<W: Write>(output: &mut W, args: &[&str], server: &mut Server) -> io::Result<()> {
let [name] = args else { let [name] = args else {
return writeln!(output, "error: usage: history <behavior-name>"); return writeln!(output, "error: usage: history <behavior-name>");
}; };
let Some(behavior) = image.behavior(name) else { let behavior = match dispatch(
return writeln!(output, "error: unknown behavior `{name}`"); server,
Command::GetBehavior {
name: (*name).to_owned(),
},
) {
Ok(Reply::Behavior(behavior)) => behavior,
Ok(other) => return unexpected_reply(output, other),
Err(error) => return writeln!(output, "error: {error}"),
}; };
for version in behavior.history { for version in behavior.history {
let current = if version.revision == behavior.revision { let current = if version.revision == behavior.revision {
@ -236,7 +305,7 @@ fn history_command<W: Write>(output: &mut W, args: &[&str], image: &ImageApi) ->
fn rollback_command<W: Write>( fn rollback_command<W: Write>(
output: &mut W, output: &mut W,
args: &[&str], args: &[&str],
image: &mut ImageApi, server: &mut Server,
) -> io::Result<()> { ) -> io::Result<()> {
let [name, expected_revision, target_revision] = args else { let [name, expected_revision, target_revision] = args else {
return writeln!( return writeln!(
@ -252,41 +321,74 @@ fn rollback_command<W: Write>(
Ok(revision) => revision, Ok(revision) => revision,
Err(_) => return writeln!(output, "error: invalid target revision"), Err(_) => return writeln!(output, "error: invalid target revision"),
}; };
match image.rollback_behavior(name, expected_revision, target_revision) { match dispatch(
Ok(behavior) => writeln!( server,
Command::RollbackBehavior {
name: (*name).to_owned(),
expected_revision,
target_revision,
},
) {
Ok(Reply::Behavior(behavior)) => writeln!(
output, output,
"rolled back `{}` to implementation {} as revision {}", "rolled back `{}` to implementation {} as revision {}",
behavior.name, behavior.current, behavior.revision behavior.name, behavior.current_implementation_id, behavior.revision
), ),
Ok(other) => unexpected_reply(output, other),
Err(error) => writeln!(output, "error: {error}"), Err(error) => writeln!(output, "error: {error}"),
} }
} }
fn install_command<W: Write>( fn install_command<W: Write>(output: &mut W, args: &[&str], server: &mut Server) -> io::Result<()> {
output: &mut W,
args: &[&str],
image: &mut ImageApi,
) -> io::Result<()> {
let [actor_id, name] = args else { let [actor_id, name] = args else {
return writeln!(output, "error: usage: install <actor-id> <behavior-name>"); return writeln!(output, "error: usage: install <actor-id> <behavior-name>");
}; };
let actor_id = match actor_id.parse::<u64>() { let actor_id = match actor_id.parse::<u64>() {
Ok(id) => ActorId(id), Ok(id) => id,
Err(_) => return writeln!(output, "error: invalid actor ID"), Err(_) => return writeln!(output, "error: invalid actor ID"),
}; };
match image.install(actor_id, name) { let implementation = match dispatch(
Ok(implementation) => { server,
let behavior = image.behavior(name).expect("install resolved behavior"); Command::InstallBehavior {
actor_id,
behavior_name: (*name).to_owned(),
},
) {
Ok(Reply::BehaviorInstalled { implementation_id }) => implementation_id,
Ok(other) => return unexpected_reply(output, other),
Err(error) => return writeln!(output, "error: {error}"),
};
match dispatch(
server,
Command::GetBehavior {
name: (*name).to_owned(),
},
) {
Ok(Reply::Behavior(behavior)) => {
writeln!( writeln!(
output, output,
"installed `{}` revision {} on actor {} ({implementation})", "installed `{}` revision {} on actor {} ({implementation})",
behavior.name, behavior.revision, actor_id.0 behavior.name, behavior.revision, actor_id
) )
} }
Ok(other) => unexpected_reply(output, other),
Err(error) => writeln!(output, "error: {error}"), Err(error) => writeln!(output, "error: {error}"),
} }
} }
fn dispatch(server: &mut Server, command: Command) -> Result<Reply, String> {
let response: Response = server.dispatch(Request {
version: PROTOCOL_VERSION,
request_id: "runa-cli".to_owned(),
command,
});
response.result.map_err(|error| error.message)
}
fn unexpected_reply<W: Write>(output: &mut W, reply: Reply) -> io::Result<()> {
writeln!(output, "error: unexpected protocol response: {reply:?}")
}
fn parse_exact<T: std::str::FromStr, const N: usize>(args: &[&str]) -> Result<[T; N], String> { fn parse_exact<T: std::str::FromStr, const N: usize>(args: &[&str]) -> Result<[T; N], String> {
if args.len() != N { if args.len() != N {
return Err(format!("expected {N} argument(s), got {}", args.len())); return Err(format!("expected {N} argument(s), got {}", args.len()));
@ -371,38 +473,43 @@ fn print_help<W: Write>(output: &mut W) -> io::Result<()> {
mod tests { mod tests {
use super::*; use super::*;
fn image() -> ExperimentResult<ImageApi> { fn server() -> Server {
let mut image = ImageApi::new(); let mut server = Server::new();
image.publish_behavior( dispatch(
DEFAULT_BEHAVIOR, &mut server,
None, Command::PublishBehavior {
include_str!("../../../runa-std/actors/counter.rn"), name: DEFAULT_BEHAVIOR.to_owned(),
)?; expected_revision: None,
Ok(image) source: include_str!("../../../runa-std/actors/counter.rn").to_owned(),
},
)
.expect("default behavior publishes");
server
} }
#[test] #[test]
fn repl_drives_actor_messages_and_inspection() -> ExperimentResult<()> { fn repl_drives_actor_messages_and_inspection() -> Result<(), Box<dyn std::error::Error>> {
let mut image = image()?; let mut server = server();
let input = io::Cursor::new( let input = io::Cursor::new(
"spawn 0\nspawn 10\nsend 1 1 2\ntick 100\ninspect 1\ninspect 2\nevents\nquit\n", "spawn 0\nspawn 10\nsend 1 1 2\ntick 100\ninspect 1\ninspect 2\nevents\nquit\n",
); );
let mut output = Vec::new(); let mut output = Vec::new();
run_repl(input, &mut output, &mut image, DEFAULT_BEHAVIOR)?; run_repl(input, &mut output, &mut server, DEFAULT_BEHAVIOR)?;
let output = String::from_utf8(output)?; let output = String::from_utf8(output)?;
assert!(output.contains("ActorId(\n 1,")); assert!(output.contains("actor_id: 1"));
assert!(output.contains("ActorId(\n 2,")); assert!(output.contains("actor_id: 2"));
assert!(output.contains("state: 15")); assert!(output.contains("state: 15"));
assert!(output.contains("Committed")); assert!(output.contains("Committed {"));
assert!(output.contains("actor_id: 2"));
Ok(()) Ok(())
} }
#[test] #[test]
fn repl_reports_bad_input_and_continues() -> ExperimentResult<()> { fn repl_reports_bad_input_and_continues() -> Result<(), Box<dyn std::error::Error>> {
let mut image = image()?; let mut server = server();
let input = io::Cursor::new("spawn nope\nsend 999 0 1\ntick 1 0\nunknown\nquit\n"); let input = io::Cursor::new("spawn nope\nsend 999 0 1\ntick 1 0\nunknown\nquit\n");
let mut output = Vec::new(); let mut output = Vec::new();
run_repl(input, &mut output, &mut image, DEFAULT_BEHAVIOR)?; run_repl(input, &mut output, &mut server, DEFAULT_BEHAVIOR)?;
let output = String::from_utf8(output)?; let output = String::from_utf8(output)?;
assert!(output.contains("invalid initial state")); assert!(output.contains("invalid initial state"));
assert!(output.contains("unknown actor")); assert!(output.contains("unknown actor"));
@ -412,8 +519,9 @@ mod tests {
} }
#[test] #[test]
fn repl_manages_named_behavior_revisions_and_installs_them() -> ExperimentResult<()> { fn repl_manages_named_behavior_revisions_and_installs_them()
let mut image = image()?; -> Result<(), Box<dyn std::error::Error>> {
let mut server = server();
let source_path = concat!( let source_path = concat!(
env!("CARGO_MANIFEST_DIR"), env!("CARGO_MANIFEST_DIR"),
"/../../runa-std/actors/counter.rn" "/../../runa-std/actors/counter.rn"
@ -422,7 +530,7 @@ mod tests {
"behaviors\npublish counter 1 {source_path}\nhistory counter\nrollback counter 2 1\nspawn 0 counter\ninstall 1 counter\nsend 1 0 1\ntick 1 1\ncancel 1\npause 1\nresume 1\ninspect 1\nevents\nquit\n" "behaviors\npublish counter 1 {source_path}\nhistory counter\nrollback counter 2 1\nspawn 0 counter\ninstall 1 counter\nsend 1 0 1\ntick 1 1\ncancel 1\npause 1\nresume 1\ninspect 1\nevents\nquit\n"
)); ));
let mut output = Vec::new(); let mut output = Vec::new();
run_repl(input, &mut output, &mut image, DEFAULT_BEHAVIOR)?; run_repl(input, &mut output, &mut server, DEFAULT_BEHAVIOR)?;
let output = String::from_utf8(output)?; let output = String::from_utf8(output)?;
assert!(output.contains("published `counter` revision 2")); assert!(output.contains("published `counter` revision 2"));
assert!(output.contains("revision 1:")); assert!(output.contains("revision 1:"));
@ -430,14 +538,24 @@ mod tests {
assert!(output.contains("rolled back `counter`")); assert!(output.contains("rolled back `counter`"));
assert!(output.contains("spawned ActorId(1) with `counter` revision 3")); assert!(output.contains("spawned ActorId(1) with `counter` revision 3"));
assert!(output.contains("installed `counter` revision 3 on actor 1")); assert!(output.contains("installed `counter` revision 3 on actor 1"));
assert!(output.contains("Paused(")); assert!(output.contains("Paused {"));
assert!(output.contains("Resumed(")); assert!(output.contains("Resumed {"));
assert!(output.contains("InvocationCancelled(")); assert!(output.contains("InvocationCancelled {"));
assert_eq!(image.behavior(DEFAULT_BEHAVIOR).unwrap().revision, 3); let behavior = match dispatch(
assert_eq!( &mut server,
image.inspect(ActorId(1))?.implementation, Command::GetBehavior {
image.behavior(DEFAULT_BEHAVIOR).unwrap().current name: DEFAULT_BEHAVIOR.to_owned(),
); },
)? {
Reply::Behavior(behavior) => behavior,
other => return Err(format!("unexpected response: {other:?}").into()),
};
assert_eq!(behavior.revision, 3);
let actor = match dispatch(&mut server, Command::InspectActor { actor_id: 1 })? {
Reply::Actor(actor) => actor,
other => return Err(format!("unexpected response: {other:?}").into()),
};
assert_eq!(actor.implementation_id, behavior.current_implementation_id);
Ok(()) Ok(())
} }
} }

View File

@ -438,7 +438,8 @@ inspection remain future work.
The first Phase 5 increments are a trusted in-process application API used by The first Phase 5 increments are a trusted in-process application API used by
the CLI, a transport-neutral versioned protocol contract, and a dispatcher the CLI, a transport-neutral versioned protocol contract, and a dispatcher
exposed through bounded local JSON-lines stdio. A network server, sessions, exposed through bounded local JSON-lines stdio. The interactive CLI uses the
same protocol commands and dispatcher in-process. A network server, sessions,
authentication, and authorization are not yet implemented. authentication, and authorization are not yet implemented.
### Phase 6 — Graphical world ### Phase 6 — Graphical world

View File

@ -54,7 +54,8 @@ The default per-actor slice budget is 32 instructions.
- `runa-server` dispatches those requests through `runa-image-api` and maps - `runa-server` dispatches those requests through `runa-image-api` and maps
results to protocol replies; its executable uses newline-delimited JSON over results to protocol replies; its executable uses newline-delimited JSON over
stdin/stdout with a 1 MiB request-line limit, not a network listener. stdin/stdout with a 1 MiB request-line limit, not a network listener.
- `runa` provides the local CLI client for that boundary. - `runa` provides an interactive CLI client using protocol requests through the
dispatcher.
The image API is process-local and intended for trusted callers. It has no The image API is process-local and intended for trusted callers. It has no
transport, authentication, or authorization layer; do not expose it directly transport, authentication, or authorization layer; do not expose it directly