feat: add trusted in-process image API
Introduce the runa-image-api service boundary for behavior source retrieval, publishing, history, rollback, actor lifecycle, messaging, scheduling, snapshots, and events. Route the local CLI through the API and document that it is trusted and process-local, with no network authentication. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
8d3496c621
commit
515a6439b5
|
|
@ -66,6 +66,7 @@ proposed capabilities as plans, not existing behavior.
|
|||
- `crates/runa-code/`: source identities and in-memory Rune compile cache.
|
||||
- `crates/runa-actor/`: actor value and lifecycle contracts.
|
||||
- `crates/runa-runtime/`: single-worker scheduling and in-memory transitions.
|
||||
- `crates/runa-image-api/`: trusted in-process services for operating on an image.
|
||||
- `runa-std/actors/`: default Rune actor scripts loaded by the local CLI.
|
||||
- `docs/architecture.md`: architecture, roadmap, risks, and acceptance scenario.
|
||||
- `docs/decisions/`: architectural decisions.
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ All notable changes to this project will be documented in this file.
|
|||
retry, exposed in the CLI and documented with native side-effect caveats.
|
||||
- Active actor execution telemetry in snapshots: completed VM steps,
|
||||
instruction pointer, and call-frame depth.
|
||||
- Trusted in-process `runa-image-api` service boundary for code and actor
|
||||
operations, now used by the local CLI.
|
||||
- Execution-model decision and Phase 1 runtime documentation.
|
||||
- Lifecycle tests proving failed-message retry from committed state and
|
||||
stop/restart preservation of reserved-first FIFO order.
|
||||
|
|
|
|||
|
|
@ -395,6 +395,7 @@ dependencies = [
|
|||
"runa-actor",
|
||||
"runa-code",
|
||||
"runa-core",
|
||||
"runa-image-api",
|
||||
"runa-rune",
|
||||
"runa-runtime",
|
||||
]
|
||||
|
|
@ -420,6 +421,16 @@ dependencies = [
|
|||
name = "runa-core"
|
||||
version = "0.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "runa-image-api"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"runa-actor",
|
||||
"runa-code",
|
||||
"runa-core",
|
||||
"runa-runtime",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "runa-rune"
|
||||
version = "0.0.0"
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ members = [
|
|||
"crates/runa-actor",
|
||||
"crates/runa-code",
|
||||
"crates/runa-core",
|
||||
"crates/runa-image-api",
|
||||
"crates/runa-rune",
|
||||
"crates/runa-runtime",
|
||||
]
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -13,11 +13,12 @@ experiment using Rune 0.14.2: script execution, native calls, async suspension,
|
|||
instruction budgets, stepping, and compiled-code sharing. It also runs a minimal
|
||||
single-worker actor demonstration with bounded mailboxes and explicit message
|
||||
effects. Source and compiled implementation identities are BLAKE3 content
|
||||
addresses with an in-memory compile cache. Persistence, interactive behavior
|
||||
editing, canonicalization, the graphical world, and development API are not
|
||||
addresses with an in-memory compile cache. Persistence, canonicalization, the
|
||||
network development API, graphical world, and VS Code extension are not
|
||||
implemented yet. The code library now supports an in-memory versioned behavior
|
||||
registry, and the CLI exposes publishing, history, rollback, safe-boundary
|
||||
installation, pause/resume, and cancellation.
|
||||
installation, pause/resume, and cancellation through a trusted in-process image
|
||||
API. It is not a network API and does not provide authentication.
|
||||
|
||||
The prototype uses worker-local invocation stacks and cooperative instruction
|
||||
slices, and actor snapshots report basic active-execution progress. Native
|
||||
|
|
@ -104,7 +105,8 @@ test task runs `cargo test --workspace`.
|
|||
│ ├── runa-actor/ # Actor value contracts
|
||||
│ ├── runa-code/ # Source identities and compile cache
|
||||
│ ├── runa-rune/ # Phase 0 Rune experiments
|
||||
│ └── runa-runtime/ # Single-worker actor prototype
|
||||
│ ├── runa-runtime/ # Single-worker actor prototype
|
||||
│ └── runa-image-api/ # Trusted in-process image operations
|
||||
├── docs/
|
||||
│ ├── architecture.md
|
||||
│ └── decisions/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "runa-image-api"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
runa-actor = { path = "../runa-actor" }
|
||||
runa-code = { path = "../runa-code" }
|
||||
runa-core = { path = "../runa-core" }
|
||||
runa-runtime = { path = "../runa-runtime" }
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
//! Trusted, in-process application services for a running Runa image.
|
||||
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::Arc;
|
||||
|
||||
use runa_actor::{ActorId, ActorSnapshot, Event, Message};
|
||||
use runa_code::{BehaviorSnapshot, CodeStore};
|
||||
use runa_core::ImplementationId;
|
||||
use runa_runtime::{Runtime, RuntimeResult};
|
||||
|
||||
pub struct ImageApi {
|
||||
code: CodeStore,
|
||||
runtime: Runtime,
|
||||
}
|
||||
|
||||
impl Default for ImageApi {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ImageApi {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
code: CodeStore::default(),
|
||||
runtime: Runtime::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn publish_behavior(
|
||||
&mut self,
|
||||
name: &str,
|
||||
expected_revision: Option<u64>,
|
||||
source: &str,
|
||||
) -> RuntimeResult<BehaviorSnapshot> {
|
||||
let artifact = self.code.compile(source)?;
|
||||
self.runtime.publish(Arc::clone(&artifact))?;
|
||||
self.code.publish_behavior(name, expected_revision, source)
|
||||
}
|
||||
|
||||
pub fn rollback_behavior(
|
||||
&mut self,
|
||||
name: &str,
|
||||
expected_revision: u64,
|
||||
target_revision: u64,
|
||||
) -> RuntimeResult<BehaviorSnapshot> {
|
||||
self.code
|
||||
.rollback_behavior(name, expected_revision, target_revision)
|
||||
}
|
||||
|
||||
pub fn behavior(&self, name: &str) -> Option<BehaviorSnapshot> {
|
||||
self.code.behavior(name)
|
||||
}
|
||||
|
||||
pub fn behaviors(&self) -> Vec<BehaviorSnapshot> {
|
||||
self.code.behaviors()
|
||||
}
|
||||
|
||||
pub fn behavior_source(&self, name: &str, revision: u64) -> RuntimeResult<&str> {
|
||||
let behavior = self.code.behavior(name).ok_or("unknown behavior")?;
|
||||
let version = behavior
|
||||
.history
|
||||
.iter()
|
||||
.find(|version| version.revision == revision)
|
||||
.ok_or("unknown behavior revision")?;
|
||||
self.code
|
||||
.artifact(version.implementation_id)
|
||||
.map(|artifact| artifact.original_source())
|
||||
.ok_or_else(|| "behavior artifact is unavailable".into())
|
||||
}
|
||||
|
||||
pub fn spawn(
|
||||
&mut self,
|
||||
behavior_name: &str,
|
||||
state: i64,
|
||||
capacity: NonZeroUsize,
|
||||
) -> RuntimeResult<ActorId> {
|
||||
let behavior = self
|
||||
.code
|
||||
.behavior(behavior_name)
|
||||
.ok_or("unknown behavior")?;
|
||||
self.runtime.spawn(behavior.current, state, capacity)
|
||||
}
|
||||
|
||||
pub fn send(&mut self, actor: ActorId, message: Message) -> RuntimeResult<()> {
|
||||
self.runtime.send(actor, message)
|
||||
}
|
||||
|
||||
pub fn tick(&mut self, ticks: usize, budget: NonZeroUsize) {
|
||||
for _ in 0..ticks {
|
||||
self.runtime.tick(budget);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inspect(&self, actor: ActorId) -> RuntimeResult<ActorSnapshot> {
|
||||
self.runtime.inspect(actor)
|
||||
}
|
||||
|
||||
pub fn actors(&self) -> Vec<ActorSnapshot> {
|
||||
self.runtime.actors()
|
||||
}
|
||||
|
||||
pub fn events(&mut self) -> Vec<Event> {
|
||||
self.runtime.drain_events()
|
||||
}
|
||||
|
||||
pub fn stop(&mut self, actor: ActorId) -> RuntimeResult<()> {
|
||||
self.runtime.stop(actor)
|
||||
}
|
||||
|
||||
pub fn restart(&mut self, actor: ActorId) -> RuntimeResult<()> {
|
||||
self.runtime.restart(actor)
|
||||
}
|
||||
|
||||
pub fn pause(&mut self, actor: ActorId) -> RuntimeResult<()> {
|
||||
self.runtime.pause(actor)
|
||||
}
|
||||
|
||||
pub fn resume(&mut self, actor: ActorId) -> RuntimeResult<()> {
|
||||
self.runtime.resume(actor)
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self, actor: ActorId) -> RuntimeResult<()> {
|
||||
self.runtime.cancel(actor)
|
||||
}
|
||||
|
||||
pub fn install(
|
||||
&mut self,
|
||||
actor: ActorId,
|
||||
behavior_name: &str,
|
||||
) -> RuntimeResult<ImplementationId> {
|
||||
self.install_with_migration(actor, behavior_name, Ok)
|
||||
}
|
||||
|
||||
pub fn install_with_migration(
|
||||
&mut self,
|
||||
actor: ActorId,
|
||||
behavior_name: &str,
|
||||
migrate: impl FnOnce(i64) -> RuntimeResult<i64>,
|
||||
) -> RuntimeResult<ImplementationId> {
|
||||
let behavior = self
|
||||
.code
|
||||
.behavior(behavior_name)
|
||||
.ok_or("unknown behavior")?;
|
||||
self.runtime
|
||||
.install_with_migration(actor, behavior.current, migrate)?;
|
||||
Ok(behavior.current)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn capacity(n: usize) -> NonZeroUsize {
|
||||
NonZeroUsize::new(n).expect("capacity must be positive")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_api_publishes_spawns_inspects_and_exposes_events() -> RuntimeResult<()> {
|
||||
let mut image = ImageApi::new();
|
||||
let behavior = image.publish_behavior(
|
||||
"counter",
|
||||
None,
|
||||
"pub fn handle(state, kind, value) { (state + value, []) }",
|
||||
)?;
|
||||
assert_eq!(
|
||||
image.behavior_source("counter", behavior.revision)?,
|
||||
"pub fn handle(state, kind, value) { (state + value, []) }"
|
||||
);
|
||||
let actor = image.spawn("counter", 5, capacity(2))?;
|
||||
image.send(actor, Message { kind: 0, value: 7 })?;
|
||||
image.tick(100, capacity(10));
|
||||
|
||||
assert_eq!(image.inspect(actor)?.state, 12);
|
||||
assert_eq!(image.inspect(actor)?.implementation, behavior.current);
|
||||
assert!(
|
||||
image
|
||||
.events()
|
||||
.iter()
|
||||
.any(|event| matches!(event, Event::Committed(id) if *id == actor))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_api_rejects_stale_behavior_updates_without_changing_current() -> RuntimeResult<()> {
|
||||
let mut image = ImageApi::new();
|
||||
let first = image.publish_behavior(
|
||||
"counter",
|
||||
None,
|
||||
"pub fn handle(state, kind, value) { (state + value, []) }",
|
||||
)?;
|
||||
assert!(
|
||||
image
|
||||
.publish_behavior(
|
||||
"counter",
|
||||
None,
|
||||
"pub fn handle(state, kind, value) { (state - value, []) }"
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(image.behavior("counter").unwrap().current, first.current);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_api_installs_behavior_with_atomic_state_migration() -> RuntimeResult<()> {
|
||||
let mut image = ImageApi::new();
|
||||
let first = image.publish_behavior(
|
||||
"counter",
|
||||
None,
|
||||
"pub fn handle(state, kind, value) { (state + value, []) }",
|
||||
)?;
|
||||
image.publish_behavior(
|
||||
"counter",
|
||||
Some(first.revision),
|
||||
"pub fn handle(state, kind, value) { (state * value, []) }",
|
||||
)?;
|
||||
let actor = image.spawn("counter", 5, capacity(2))?;
|
||||
let implementation = image.install_with_migration(actor, "counter", |state| {
|
||||
Ok(state.checked_mul(10).ok_or("migration overflow")?)
|
||||
})?;
|
||||
|
||||
assert_eq!(image.inspect(actor)?.state, 50);
|
||||
assert_eq!(image.inspect(actor)?.implementation, implementation);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -7,5 +7,6 @@ edition = "2024"
|
|||
runa-actor = { path = "../runa-actor" }
|
||||
runa-code = { path = "../runa-code" }
|
||||
runa-core = { path = "../runa-core" }
|
||||
runa-image-api = { path = "../runa-image-api" }
|
||||
runa-rune = { path = "../runa-rune" }
|
||||
runa-runtime = { path = "../runa-runtime" }
|
||||
|
|
|
|||
|
|
@ -2,26 +2,20 @@ use std::io::{self, BufRead, Write};
|
|||
use std::num::NonZeroUsize;
|
||||
|
||||
use runa_actor::{ActorId, Message};
|
||||
use runa_code::CodeStore;
|
||||
use runa_image_api::ImageApi;
|
||||
use runa_rune::ExperimentResult;
|
||||
use runa_runtime::Runtime;
|
||||
|
||||
const MAILBOX_CAPACITY: usize = 64;
|
||||
const DEFAULT_TICK_BUDGET: usize = 32;
|
||||
const DEFAULT_BEHAVIOR: &str = "counter";
|
||||
|
||||
fn main() -> ExperimentResult<()> {
|
||||
let mut code = CodeStore::default();
|
||||
let behavior = code.publish_behavior(
|
||||
let mut image = ImageApi::new();
|
||||
let behavior = image.publish_behavior(
|
||||
DEFAULT_BEHAVIOR,
|
||||
None,
|
||||
include_str!("../../../runa-std/actors/counter.rn"),
|
||||
)?;
|
||||
let artifact = code
|
||||
.artifact(behavior.current)
|
||||
.ok_or("published behavior artifact is missing")?;
|
||||
let mut runtime = Runtime::default();
|
||||
runtime.publish(std::sync::Arc::clone(artifact))?;
|
||||
|
||||
println!("Runa — Rune Actors");
|
||||
println!(
|
||||
|
|
@ -32,8 +26,7 @@ fn main() -> ExperimentResult<()> {
|
|||
run_repl(
|
||||
io::stdin().lock(),
|
||||
io::stdout().lock(),
|
||||
&mut code,
|
||||
&mut runtime,
|
||||
&mut image,
|
||||
DEFAULT_BEHAVIOR,
|
||||
)?;
|
||||
Ok(())
|
||||
|
|
@ -42,8 +35,7 @@ fn main() -> ExperimentResult<()> {
|
|||
fn run_repl<R: BufRead, W: Write>(
|
||||
mut input: R,
|
||||
mut output: W,
|
||||
code: &mut CodeStore,
|
||||
runtime: &mut Runtime,
|
||||
image: &mut ImageApi,
|
||||
default_behavior: &str,
|
||||
) -> io::Result<()> {
|
||||
let mut line = String::new();
|
||||
|
|
@ -62,9 +54,9 @@ fn run_repl<R: BufRead, W: Write>(
|
|||
let args: Vec<_> = words.collect();
|
||||
let result = match command {
|
||||
"help" => print_help(&mut output),
|
||||
"spawn" => spawn_command(&mut output, &args, default_behavior, code, runtime),
|
||||
"spawn" => spawn_command(&mut output, &args, default_behavior, image),
|
||||
"behaviors" if args.is_empty() => {
|
||||
for behavior in code.behaviors() {
|
||||
for behavior in image.behaviors() {
|
||||
writeln!(
|
||||
output,
|
||||
"{} revision {} ({})",
|
||||
|
|
@ -74,12 +66,12 @@ fn run_repl<R: BufRead, W: Write>(
|
|||
Ok(())
|
||||
}
|
||||
"behaviors" => writeln!(output, "error: usage: behaviors"),
|
||||
"publish" => publish_command(&mut output, &args, code, runtime),
|
||||
"history" => history_command(&mut output, &args, code),
|
||||
"rollback" => rollback_command(&mut output, &args, code),
|
||||
"install" => install_command(&mut output, &args, code, runtime),
|
||||
"publish" => publish_command(&mut output, &args, image),
|
||||
"history" => history_command(&mut output, &args, image),
|
||||
"rollback" => rollback_command(&mut output, &args, image),
|
||||
"install" => install_command(&mut output, &args, image),
|
||||
"actors" if args.is_empty() => {
|
||||
for actor in runtime.actors() {
|
||||
for actor in image.actors() {
|
||||
writeln!(output, "{actor:#?}")?;
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -87,7 +79,7 @@ fn run_repl<R: BufRead, W: Write>(
|
|||
"actors" => writeln!(output, "error: usage: actors"),
|
||||
"send" => match parse_send_args(&args) {
|
||||
Ok((actor, kind, value)) => {
|
||||
match runtime.send(ActorId(actor), Message { kind, value }) {
|
||||
match image.send(ActorId(actor), Message { kind, value }) {
|
||||
Ok(()) => writeln!(output, "message queued"),
|
||||
Err(error) => writeln!(output, "error: {error}"),
|
||||
}
|
||||
|
|
@ -96,51 +88,32 @@ fn run_repl<R: BufRead, W: Write>(
|
|||
},
|
||||
"tick" => match parse_tick_args(&args) {
|
||||
Ok((count, budget)) => {
|
||||
for _ in 0..count {
|
||||
runtime.tick(budget);
|
||||
}
|
||||
image.tick(count, budget);
|
||||
writeln!(output, "ran {count} scheduler tick(s)")
|
||||
}
|
||||
Err(error) => writeln!(output, "error: {error}"),
|
||||
},
|
||||
"inspect" => match parse_exact::<u64, 1>(&args) {
|
||||
Ok([id]) => match runtime.inspect(ActorId(id)) {
|
||||
Ok([id]) => match image.inspect(ActorId(id)) {
|
||||
Ok(actor) => writeln!(output, "{actor:#?}"),
|
||||
Err(error) => writeln!(output, "error: {error}"),
|
||||
},
|
||||
Err(error) => writeln!(output, "error: {error}"),
|
||||
},
|
||||
"events" if args.is_empty() => {
|
||||
for event in runtime.drain_events() {
|
||||
for event in image.events() {
|
||||
writeln!(output, "{event:#?}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
"events" => writeln!(output, "error: usage: events"),
|
||||
"stop" => {
|
||||
lifecycle_command(&mut output, &args, |runtime, id| runtime.stop(id), runtime)
|
||||
"stop" => lifecycle_command(&mut output, &args, |image, id| image.stop(id), image),
|
||||
"restart" => {
|
||||
lifecycle_command(&mut output, &args, |image, id| image.restart(id), image)
|
||||
}
|
||||
"restart" => lifecycle_command(
|
||||
&mut output,
|
||||
&args,
|
||||
|runtime, id| runtime.restart(id),
|
||||
runtime,
|
||||
),
|
||||
"pause" => {
|
||||
lifecycle_command(&mut output, &args, |runtime, id| runtime.pause(id), runtime)
|
||||
}
|
||||
"resume" => lifecycle_command(
|
||||
&mut output,
|
||||
&args,
|
||||
|runtime, id| runtime.resume(id),
|
||||
runtime,
|
||||
),
|
||||
"cancel" => lifecycle_command(
|
||||
&mut output,
|
||||
&args,
|
||||
|runtime, id| runtime.cancel(id),
|
||||
runtime,
|
||||
),
|
||||
"pause" => lifecycle_command(&mut output, &args, |image, id| image.pause(id), image),
|
||||
"resume" => lifecycle_command(&mut output, &args, |image, id| image.resume(id), image),
|
||||
"cancel" => lifecycle_command(&mut output, &args, |image, id| image.cancel(id), image),
|
||||
"quit" | "exit" if args.is_empty() => break,
|
||||
"quit" | "exit" => writeln!(output, "error: usage: quit"),
|
||||
_ => writeln!(output, "error: unknown command `{command}`; type `help`"),
|
||||
|
|
@ -153,11 +126,11 @@ fn run_repl<R: BufRead, W: Write>(
|
|||
fn lifecycle_command<W: Write>(
|
||||
output: &mut W,
|
||||
args: &[&str],
|
||||
operation: impl FnOnce(&mut Runtime, ActorId) -> runa_runtime::RuntimeResult<()>,
|
||||
runtime: &mut Runtime,
|
||||
operation: impl FnOnce(&mut ImageApi, ActorId) -> ExperimentResult<()>,
|
||||
image: &mut ImageApi,
|
||||
) -> io::Result<()> {
|
||||
match parse_exact::<u64, 1>(args) {
|
||||
Ok([id]) => match operation(runtime, ActorId(id)) {
|
||||
Ok([id]) => match operation(image, ActorId(id)) {
|
||||
Ok(()) => writeln!(output, "ok"),
|
||||
Err(error) => writeln!(output, "error: {error}"),
|
||||
},
|
||||
|
|
@ -169,8 +142,7 @@ fn spawn_command<W: Write>(
|
|||
output: &mut W,
|
||||
args: &[&str],
|
||||
default_behavior: &str,
|
||||
code: &CodeStore,
|
||||
runtime: &mut Runtime,
|
||||
image: &mut ImageApi,
|
||||
) -> io::Result<()> {
|
||||
let (state_arg, behavior_name) = match args {
|
||||
[state] => (*state, default_behavior),
|
||||
|
|
@ -186,18 +158,18 @@ fn spawn_command<W: Write>(
|
|||
Ok(state) => state,
|
||||
Err(_) => return writeln!(output, "error: invalid initial state: `{state_arg}`"),
|
||||
};
|
||||
let Some(behavior) = code.behavior(behavior_name) else {
|
||||
return writeln!(output, "error: unknown behavior `{behavior_name}`");
|
||||
};
|
||||
match runtime.spawn(
|
||||
behavior.current,
|
||||
match image.spawn(
|
||||
behavior_name,
|
||||
state,
|
||||
NonZeroUsize::new(MAILBOX_CAPACITY).expect("capacity is nonzero"),
|
||||
) {
|
||||
Ok(id) => writeln!(
|
||||
output,
|
||||
"spawned {id:?} with `{behavior_name}` revision {}",
|
||||
behavior.revision
|
||||
image
|
||||
.behavior(behavior_name)
|
||||
.expect("spawn resolved behavior")
|
||||
.revision
|
||||
),
|
||||
Err(error) => writeln!(output, "error: {error}"),
|
||||
}
|
||||
|
|
@ -206,8 +178,7 @@ fn spawn_command<W: Write>(
|
|||
fn publish_command<W: Write>(
|
||||
output: &mut W,
|
||||
args: &[&str],
|
||||
code: &mut CodeStore,
|
||||
runtime: &mut Runtime,
|
||||
image: &mut ImageApi,
|
||||
) -> io::Result<()> {
|
||||
let [name, expected_revision, source_path] = args else {
|
||||
return writeln!(
|
||||
|
|
@ -229,14 +200,7 @@ fn publish_command<W: Write>(
|
|||
return writeln!(output, "error: could not read `{source_path}`: {error}");
|
||||
}
|
||||
};
|
||||
let artifact = match code.compile(&source) {
|
||||
Ok(artifact) => artifact,
|
||||
Err(error) => return writeln!(output, "error: {error}"),
|
||||
};
|
||||
if let Err(error) = runtime.publish(std::sync::Arc::clone(&artifact)) {
|
||||
return writeln!(output, "error: {error}");
|
||||
}
|
||||
let behavior = match code.publish_behavior(name, expected_revision, &source) {
|
||||
let behavior = match image.publish_behavior(name, expected_revision, &source) {
|
||||
Ok(behavior) => behavior,
|
||||
Err(error) => return writeln!(output, "error: {error}"),
|
||||
};
|
||||
|
|
@ -247,11 +211,11 @@ fn publish_command<W: Write>(
|
|||
)
|
||||
}
|
||||
|
||||
fn history_command<W: Write>(output: &mut W, args: &[&str], code: &CodeStore) -> io::Result<()> {
|
||||
fn history_command<W: Write>(output: &mut W, args: &[&str], image: &ImageApi) -> io::Result<()> {
|
||||
let [name] = args else {
|
||||
return writeln!(output, "error: usage: history <behavior-name>");
|
||||
};
|
||||
let Some(behavior) = code.behavior(name) else {
|
||||
let Some(behavior) = image.behavior(name) else {
|
||||
return writeln!(output, "error: unknown behavior `{name}`");
|
||||
};
|
||||
for version in behavior.history {
|
||||
|
|
@ -272,7 +236,7 @@ fn history_command<W: Write>(output: &mut W, args: &[&str], code: &CodeStore) ->
|
|||
fn rollback_command<W: Write>(
|
||||
output: &mut W,
|
||||
args: &[&str],
|
||||
code: &mut CodeStore,
|
||||
image: &mut ImageApi,
|
||||
) -> io::Result<()> {
|
||||
let [name, expected_revision, target_revision] = args else {
|
||||
return writeln!(
|
||||
|
|
@ -288,7 +252,7 @@ fn rollback_command<W: Write>(
|
|||
Ok(revision) => revision,
|
||||
Err(_) => return writeln!(output, "error: invalid target revision"),
|
||||
};
|
||||
match code.rollback_behavior(name, expected_revision, target_revision) {
|
||||
match image.rollback_behavior(name, expected_revision, target_revision) {
|
||||
Ok(behavior) => writeln!(
|
||||
output,
|
||||
"rolled back `{}` to implementation {} as revision {}",
|
||||
|
|
@ -301,8 +265,7 @@ fn rollback_command<W: Write>(
|
|||
fn install_command<W: Write>(
|
||||
output: &mut W,
|
||||
args: &[&str],
|
||||
code: &CodeStore,
|
||||
runtime: &mut Runtime,
|
||||
image: &mut ImageApi,
|
||||
) -> io::Result<()> {
|
||||
let [actor_id, name] = args else {
|
||||
return writeln!(output, "error: usage: install <actor-id> <behavior-name>");
|
||||
|
|
@ -311,15 +274,15 @@ fn install_command<W: Write>(
|
|||
Ok(id) => ActorId(id),
|
||||
Err(_) => return writeln!(output, "error: invalid actor ID"),
|
||||
};
|
||||
let Some(behavior) = code.behavior(name) else {
|
||||
return writeln!(output, "error: unknown behavior `{name}`");
|
||||
};
|
||||
match runtime.install(actor_id, behavior.current) {
|
||||
Ok(()) => writeln!(
|
||||
match image.install(actor_id, name) {
|
||||
Ok(implementation) => {
|
||||
let behavior = image.behavior(name).expect("install resolved behavior");
|
||||
writeln!(
|
||||
output,
|
||||
"installed `{}` revision {} on actor {}",
|
||||
"installed `{}` revision {} on actor {} ({implementation})",
|
||||
behavior.name, behavior.revision, actor_id.0
|
||||
),
|
||||
)
|
||||
}
|
||||
Err(error) => writeln!(output, "error: {error}"),
|
||||
}
|
||||
}
|
||||
|
|
@ -408,30 +371,24 @@ fn print_help<W: Write>(output: &mut W) -> io::Result<()> {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn repl_drives_actor_messages_and_inspection() -> ExperimentResult<()> {
|
||||
let mut runtime = Runtime::default();
|
||||
let mut code = CodeStore::default();
|
||||
code.publish_behavior(
|
||||
fn image() -> ExperimentResult<ImageApi> {
|
||||
let mut image = ImageApi::new();
|
||||
image.publish_behavior(
|
||||
DEFAULT_BEHAVIOR,
|
||||
None,
|
||||
include_str!("../../../runa-std/actors/counter.rn"),
|
||||
)?;
|
||||
let implementation = code.behavior(DEFAULT_BEHAVIOR).unwrap().current;
|
||||
runtime.publish(std::sync::Arc::clone(
|
||||
code.artifact(implementation).unwrap(),
|
||||
))?;
|
||||
Ok(image)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repl_drives_actor_messages_and_inspection() -> ExperimentResult<()> {
|
||||
let mut image = image()?;
|
||||
let input = io::Cursor::new(
|
||||
"spawn 0\nspawn 10\nsend 1 1 2\ntick 100\ninspect 1\ninspect 2\nevents\nquit\n",
|
||||
);
|
||||
let mut output = Vec::new();
|
||||
run_repl(
|
||||
input,
|
||||
&mut output,
|
||||
&mut code,
|
||||
&mut runtime,
|
||||
DEFAULT_BEHAVIOR,
|
||||
)?;
|
||||
run_repl(input, &mut output, &mut image, DEFAULT_BEHAVIOR)?;
|
||||
let output = String::from_utf8(output)?;
|
||||
assert!(output.contains("ActorId(\n 1,"));
|
||||
assert!(output.contains("ActorId(\n 2,"));
|
||||
|
|
@ -442,26 +399,10 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn repl_reports_bad_input_and_continues() -> ExperimentResult<()> {
|
||||
let mut runtime = Runtime::default();
|
||||
let mut code = CodeStore::default();
|
||||
code.publish_behavior(
|
||||
DEFAULT_BEHAVIOR,
|
||||
None,
|
||||
include_str!("../../../runa-std/actors/counter.rn"),
|
||||
)?;
|
||||
let implementation = code.behavior(DEFAULT_BEHAVIOR).unwrap().current;
|
||||
runtime.publish(std::sync::Arc::clone(
|
||||
code.artifact(implementation).unwrap(),
|
||||
))?;
|
||||
let mut image = image()?;
|
||||
let input = io::Cursor::new("spawn nope\nsend 999 0 1\ntick 1 0\nunknown\nquit\n");
|
||||
let mut output = Vec::new();
|
||||
run_repl(
|
||||
input,
|
||||
&mut output,
|
||||
&mut code,
|
||||
&mut runtime,
|
||||
DEFAULT_BEHAVIOR,
|
||||
)?;
|
||||
run_repl(input, &mut output, &mut image, DEFAULT_BEHAVIOR)?;
|
||||
let output = String::from_utf8(output)?;
|
||||
assert!(output.contains("invalid initial state"));
|
||||
assert!(output.contains("unknown actor"));
|
||||
|
|
@ -472,17 +413,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn repl_manages_named_behavior_revisions_and_installs_them() -> ExperimentResult<()> {
|
||||
let mut code = CodeStore::default();
|
||||
code.publish_behavior(
|
||||
DEFAULT_BEHAVIOR,
|
||||
None,
|
||||
include_str!("../../../runa-std/actors/counter.rn"),
|
||||
)?;
|
||||
let implementation = code.behavior(DEFAULT_BEHAVIOR).unwrap().current;
|
||||
let mut runtime = Runtime::default();
|
||||
runtime.publish(std::sync::Arc::clone(
|
||||
code.artifact(implementation).unwrap(),
|
||||
))?;
|
||||
let mut image = image()?;
|
||||
let source_path = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../runa-std/actors/counter.rn"
|
||||
|
|
@ -491,13 +422,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"
|
||||
));
|
||||
let mut output = Vec::new();
|
||||
run_repl(
|
||||
input,
|
||||
&mut output,
|
||||
&mut code,
|
||||
&mut runtime,
|
||||
DEFAULT_BEHAVIOR,
|
||||
)?;
|
||||
run_repl(input, &mut output, &mut image, DEFAULT_BEHAVIOR)?;
|
||||
let output = String::from_utf8(output)?;
|
||||
assert!(output.contains("published `counter` revision 2"));
|
||||
assert!(output.contains("revision 1:"));
|
||||
|
|
@ -508,8 +433,11 @@ mod tests {
|
|||
assert!(output.contains("Paused("));
|
||||
assert!(output.contains("Resumed("));
|
||||
assert!(output.contains("InvocationCancelled("));
|
||||
assert_eq!(code.behavior(DEFAULT_BEHAVIOR).unwrap().revision, 3);
|
||||
assert_eq!(runtime.inspect(ActorId(1))?.implementation, implementation);
|
||||
assert_eq!(image.behavior(DEFAULT_BEHAVIOR).unwrap().revision, 3);
|
||||
assert_eq!(
|
||||
image.inspect(ActorId(1))?.implementation,
|
||||
image.behavior(DEFAULT_BEHAVIOR).unwrap().current
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -308,7 +308,7 @@ The diagram is conceptual; Cargo dependencies should be kept minimal. A dependen
|
|||
| `runa-actor` | Mailbox and state-transition semantics, message reservation, acknowledgment, effects, actor lifecycle definitions | `runa-core` |
|
||||
| `runa-rune` | Rune compilation, VM execution, suspension, interruption hooks, language-specific diagnostics | `runa-core`, Rune libraries |
|
||||
| `runa-runtime` | Scheduling, actor execution coordination, state commits, crash recovery, binding actor semantics to Rune execution | `runa-core`, `runa-actor`, `runa-code`, `runa-rune` |
|
||||
| `runa-image-api` | Authorized in-process operations for spawning, inspecting, sending, editing, upgrading, and debugging | `runa-core`, `runa-runtime` |
|
||||
| `runa-image-api` | Trusted in-process application services for spawning, inspecting, sending, editing, and upgrading | `runa-core`, `runa-actor`, `runa-code`, `runa-runtime` |
|
||||
| `runa-protocol` | Versioned transport-neutral request/response/event DTOs and validation | `runa-core` |
|
||||
| `runa-server` | WebSocket/JSON-RPC transport, sessions, authentication, subscriptions | `runa-protocol`, `runa-image-api` |
|
||||
| `runa-graphics` | Native window, rendering and input mechanisms; no actor policy | `runa-core` if necessary |
|
||||
|
|
@ -316,8 +316,17 @@ The diagram is conceptual; Cargo dependencies should be kept minimal. A dependen
|
|||
| `runa` | Startup, configuration, resource wiring, shutdown | Top-level crates it instantiates |
|
||||
|
||||
The current prototype has added `runa-code` for exact-source identity and
|
||||
in-memory compilation caching. It deliberately does not canonicalize source
|
||||
yet; canonicalization needs a semantics-preserving, source-map-aware policy.
|
||||
in-memory compilation caching, and `runa-image-api` as the trusted,
|
||||
in-process application-service boundary used by the local CLI. It deliberately
|
||||
does not canonicalize source yet; canonicalization needs a semantics-preserving,
|
||||
source-map-aware policy. The image API is not yet a network API and performs no
|
||||
authentication or authorization; callers must be trusted by the host.
|
||||
|
||||
The initial `runa-image-api` provides process-local behavior source retrieval,
|
||||
publication, history and rollback, actor lifecycle and messaging, snapshots,
|
||||
ticks, and event retrieval. The CLI uses these application services rather
|
||||
than reaching into the code store and runtime directly. Versioned wire DTOs
|
||||
and transport are the next separate layer.
|
||||
|
||||
**Important:** `runa-core` must not contain schedulers, mailbox implementations, persistence engines, Rune VM types, service traits used by only one crate, generic helper collections, or GUI abstractions. Prefer a type defined in its owning crate until two independent consumers demonstrably need a stable shared representation.
|
||||
|
||||
|
|
@ -326,7 +335,7 @@ yet; canonicalization needs a semantics-preserving, source-map-aware policy.
|
|||
- `runa-actor` defines actor rules and data contracts; it does **not** spawn threads or call Rune.
|
||||
- `runa-rune` executes Rune code but does **not** own actor scheduling, mailboxes, or image APIs.
|
||||
- `runa-runtime` coordinates both. Initially it can depend directly on `runa-rune`; introduce an execution-adapter trait only when a second implementation or test double justifies it. Place the trait at the lowest layer that actually consumes it, rather than automatically in `runa-core`.
|
||||
- `runa-image-api` is the single application-service entry point for in-world tools and external clients. Its methods enforce permissions, lifecycle invariants, and revision checks.
|
||||
- `runa-image-api` is the single application-service entry point for in-world tools and external clients. It centralizes lifecycle invariants and revision checks; host-level permissions and authentication remain required at any external boundary.
|
||||
- `runa-protocol` holds wire DTOs, **not** runtime service implementations. Translate DTOs at the server boundary; never serialize internal actor locks or VM references.
|
||||
- `runa-world` uses the same `runa-image-api` operations as the external bridge. It does not reach into runtime internals for convenience.
|
||||
- Native platform handles stay behind `runa-graphics` (or a later platform crate), not in `runa-core`.
|
||||
|
|
@ -421,6 +430,10 @@ inspection remain future work.
|
|||
- Headless server, versioned protocol, actor and mailbox operations, source editing, events.
|
||||
- CLI client that can spawn, inspect, send messages, and replace behaviors.
|
||||
|
||||
The first Phase 5 increment is an in-process trusted application API used by
|
||||
the CLI. A transport-neutral versioned protocol and network server are not yet
|
||||
implemented.
|
||||
|
||||
### Phase 6 — Graphical world
|
||||
|
||||
- Native window/rendering, input, scene manager, Rune-driven button and window.
|
||||
|
|
|
|||
|
|
@ -47,7 +47,16 @@ The default per-actor slice budget is 32 instructions.
|
|||
- `runa-rune` owns compilation and typed bounded invocation execution.
|
||||
- `runa-runtime` owns implementation registration, queues, private committed
|
||||
state, scheduling, message effects and lifecycle coordination.
|
||||
- `runa` composes the experiment and demonstration.
|
||||
- `runa-image-api` composes code publication and runtime operations behind a
|
||||
trusted in-process service boundary.
|
||||
- `runa` provides the local CLI client for that boundary.
|
||||
|
||||
The image API is process-local and intended for trusted callers. It has no
|
||||
transport, authentication, or authorization layer; do not expose it directly
|
||||
to untrusted clients.
|
||||
Its install operation also supports the runtime's explicit host-provided
|
||||
state-migration closure. `behavior_source` retrieves the exact original
|
||||
published source for a selected behavior revision.
|
||||
|
||||
An implementation is published once under an ID. Actors reference that exact
|
||||
implementation. Implementation IDs use source content and the compile
|
||||
|
|
|
|||
Loading…
Reference in New Issue