15 KiB
Runa — Rune Actors
Runa (short for Rune Actors) is an experimental project to build a persistent, live programming world around isolated actors written in Rune. The long-term vision combines an inspectable graphical environment with a programmatic API and a VS Code extension, all connected to the same running image.
Status
Runa is at an early prototype stage. The executable runs a Phase 0 feasibility
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, canonicalization,
remote network access, and the graphical world are not implemented yet.
A minimal VS Code extension can connect to the local image, list actors, and
inspect state, queued/reserved messages, and execution telemetry. Inspection
JSON is editable for inactive paused/stopped/failed actors; saving restores
protected fields with a warning. The image owns mutable, directory-based Rune source files with
independent commit history. The CLI exposes source writes, commit/history/rollback,
reload/become, graceful shutdown, kill, restart, pause/resume, and cancellation through a trusted image
API. A versioned transport-neutral protocol contract is now defined, but no
authentication is implemented. The runa server provides an unauthenticated
WebSocket listener restricted to localhost, with bounded request sizes.
The runa-cli REPL and VS Code connect with
the runa-debug WebSocket subprotocol, so clients share one server-owned image.
Server change notifications automatically refresh VS Code's actor tree and inspectors.
The prototype uses worker-local invocation stacks and cooperative instruction slices, and actor snapshots report basic active-execution progress. Native callbacks are not preemptible; source-level debugging and persistent storage are not implemented.
Design goals
- Keep actor identity, mailbox, private state, behavior, and incarnation separate.
- Use message passing rather than shared mutable state for actor interaction.
- Keep behavior implementations immutable and support safe, versioned updates.
- Expose the same runtime capabilities to in-world tools and external clients.
- Be explicit about execution, delivery, and persistence guarantees.
See the architecture spec for the full vision, architecture, roadmap, and open questions.
Getting started
You need a Rust toolchain that supports the workspace's Rust 2024 edition.
Start the live image from the repository root:
mise run start
In another terminal, connect the REPL:
mise run repl
The REPL defaults to 127.0.0.1:3737. To select another loopback endpoint,
use cargo run -p runa-cli -- --connect 127.0.0.1:4000 repl and start the server with
the corresponding --listen address. Quitting the REPL disconnects only that
client; actors and source files survive until the server exits. Ctrl+C and
Ctrl+D also exit the REPL and complete the WebSocket close handshake.
Reconnect and use ps to recover their references. REPL bindings and
command history are session-local and are not restored on reconnect.
Both executables use Clap for --help, --version, and argument validation.
runa listens on 127.0.0.1:3737 without arguments. Set RUNA_LISTEN to change
the server address and RUNA_CONNECT to change the REPL address; explicit
--listen and --connect flags override their respective environment variables.
For example:
RUNA_LISTEN=127.0.0.1:4000 mise run start
RUNA_CONNECT=127.0.0.1:4000 mise run repl
runa-cli requires a subcommand. Use runa-cli repl for an interactive session,
or run an image operation directly:
cargo run -p runa-cli -- ps
actor=$(cargo run -p runa-cli -- spawn actors/counter 1)
cargo run -p runa-cli -- send "$actor" '{"op":"add","value":2}'
cargo run -p runa-cli -- tick 100
Each image operation in the REPL has a matching subcommand; use
runa-cli <command> --help for arguments. Quote JSON as a single shell argument.
One-shot commands use actor references rather than session-local bindings,
print results without a REPL banner, and exit unsuccessfully on errors.
let name = spawn ... is session-local REPL syntax. quit (alias exit)
exits either interface without stopping the image. cancel is no longer a
REPL command; the underlying runtime cancellation API remains available.
The server is WebSocket-only; stdio transport has been removed. By default it listens on
ws://127.0.0.1:3737/ and requires the runa-debug subprotocol, with up to 16
simultaneous clients sharing one live image. The listener rejects non-loopback
addresses and has no authentication; any local process that can connect has
full access to the development API. Use it only on a trusted local machine.
The runa-client crate connects over loopback
WebSockets and exchange typed protocol requests with request/response correlation.
Network connection establishment is limited to 5 seconds and response waits/writes to 30
seconds. Transport failures disconnect the client and are never automatically
retried: an operation may have committed even if its response was lost.
The VS Code extension connects over the network to this same live image; actors remain in
the image regardless of how clients connect. Remote network access requires
authentication and authorization, which are not implemented.
The server loads the bundled source/actors/counter.rn working file at startup.
Connecting a client does not modify source or create a commit:
runa> let sender = spawn actors/counter 0
sender = @<actor-reference>
runa> let receiver = spawn actors/counter 10
receiver = @<actor-reference>
runa> send sender {"op": "send", "value": 5, "target": {"$actor": "receiver"}}
message queued
runa> tick 100
runa> inspect sender
runa> inspect receiver
runa> events
Actor references are generated NanoIDs rather than sequential numbers. The
let form binds each spawned actor to a local name; references can also be
entered directly with an @ prefix. The sender commits state 1 and sends a
message to the receiver, which commits state 15. The REPL provides Tab
completion and Up/Down command history. write actors/counter <local-file>
updates the image's working source. commit actors/counter records a snapshot
with an automatically generated readable revision ID; history actors/counter
lists commits and rollback actors/counter <revision> restores a working file.
Committing is version control only: spawn always compiles the current image
file, including uncommitted changes. Invalid working code fails spawn without
creating an actor; commits can preserve invalid work in progress.
reload <actor> loads current source at its existing path, and
become <actor> <path> switches scripts, preserving state and queued messages
after the active handler completes. stop runs optional on_shutdown(state)
after the active handler; kill discards it without shutdown. restart creates
a new incarnation with the same state and loaded script, without reloading.
Pause/resume remain cooperative controls between scheduler slices. Use help in
the session for the complete command list.
State and messages support null, booleans, signed 64-bit integers, strings,
lists, records, and distinct actor-reference values. spawn <path> <json>
sets initial state; send <actor> <json> sends any supported value. JSON
objects of the form {"$actor": "name-or-@reference"} embed an actor reference,
including inside a record or list. See the runtime guide
for Rune conversions and value limits. Protocol version 5 uses
handle(ctx, state, sender, message) -> new_state; ctx.send(recipient, message)
records sends with the calling actor as sender, and
ctx.send_from(sender, recipient, message) supports forwarding.
Returning state is optional: an omitted return, return;, or () retains the
previously committed state, discarding invocation-local state mutations.
Valid sends still commit and the message is acknowledged. The same rule applies
to on_shutdown(state). To replace state, return a non-unit portable value.
Unit can no longer be used to reset state to null; use host state editing instead.
Sender metadata is separate from message data; host-originated messages have
unit/null sender. System messages are trusted runtime commands, not data an
actor can construct.
See the feasibility report for verified Rune capabilities and limitations, and the runtime guide for the runtime contract.
VS Code client
With Node.js 22+ and npm installed, run npm --prefix vscode ci. In VS Code,
open this repository and launch Run Runa Extension from Run and Debug
(F5). Start mise run start, then use Runa: Connect to Local Image
in the Extension Development Host. Open the Runa Activity Bar sidebar.
Its Actors view lists
actors created through the REPL or another client. Click an actor to inspect
it. Server notifications automatically refresh the tree and open snapshots;
Runa: Refresh Image is also available on demand.
The Image Sources view in the Runa sidebar automatically shows the image's virtual
filesystem beneath source, including actors/counter.rn, without adding a
workspace folder. Open a file and save edits to update the image without
committing. Right-click a source file to Spawn (null state) or Spawn with
State (which prompts for JSON), or to commit, browse history, and roll back. Historical revisions
remain read-only.
Use Runa: Spawn Actor to choose a source path and supply JSON initial state. Actor context menus offer structured messages and system lifecycle commands. Runa: Advance Scheduler explicitly executes ticks; sending alone only queues a message. The extension does not start or shut down the image. Disconnecting leaves actors in the server. Detailed runtime event subscriptions and structured compilation diagnostics are not implemented yet. See the extension guide for connection restrictions, snapshot semantics, and tests.
Repairing actors and reading logs
State and messages can be edited only on paused, stopped, or failed actors with no active invocation. A paused suspended handler must be killed first. State edits preserve messages; message edits preserve state. Neither restarts an actor. Reserved messages are retried first on restart; clearing the reserved array removes the failed input.
set-state @<actor-ref> 1
set-messages @<actor-ref> {"reserved":[],"queued":[{"op":"add","value":2}]}
log-level @<actor-ref> debug
logs @<actor-ref>
logs -f
These are REPL commands and runa-cli subcommands (quote JSON when using a shell).
REPL log filters also accept session-local actor names. Omitting the filter selects all
actors. logs prints retained history; logs -f additionally subscribes to
future entries until Ctrl+C, which returns to the REPL or closes a one-shot CLI.
In VS Code, actor context menus offer Edit Actor State...,
Edit Reserved and Queued Messages..., and Set Actor Log Level....
You can also edit those fields directly in inspection JSON and save. Protected
fields are restored with a warning. The Actors toolbar advances the scheduler,
and Copy Actor Reference copies a reference from the actor context menu.
Rune handlers can call ctx.log("info", "message"), or ctx.trace, ctx.debug,
ctx.info, ctx.warn, ctx.error, and ctx.fatal with a string message.
Severity order is trace < debug < info < warn < error < fatal; the default
threshold is info. Only entries at or above the actor's threshold are retained
or emitted. Logs are immediate diagnostics, not transactional effects: they
survive handler failure and may repeat on retries. Each actor keeps its latest
1,000 accepted messages in memory, each limited to 512 UTF-8 bytes. Restart
preserves logs and the threshold. Server exit loses them. Rebuild the server,
CLI, and extension together for protocol v5.
The read-only ctx.this field is the current actor's typed reference:
ctx.send(ctx.this, message) queues a normal transactional message to itself.
Development
Install mise and run mise install to install Tombi.
Rust, rustfmt, Clippy, Node.js 22+, and npm must also be available on your PATH.
Install the extension's locked dependencies with npm --prefix vscode ci.
mise run format
mise run lint
mise run test
mise run pre-commit
The pre-commit task runs formatting, linting, and tests in that order, stopping on the first failure. Run it successfully before committing and review any formatter changes. This is a task, not an automatically installed Git hook.
Tombi formats TOML files offline; rustfmt formats all workspace Rust code;
Prettier formats the extension. Clippy checks all workspace targets with
warnings treated as errors. Extension checks include strict TypeScript and
Prettier. The test task runs the Rust workspace tests, extension unit tests,
and a TypeScript-to-Rust WebSocket interoperability test.
mise run test-extension-host additionally downloads VS Code 1.96.4 into an
ignored test cache and tests the actual inspector commands in an isolated
Extension Development Host. It requires a graphical display; on headless Linux,
use xvfb-run -a mise run test-extension-host.
Project structure
.
├── Cargo.toml
├── crates/
│ ├── runa/ # Image server, protocol dispatcher, WebSocket transport
│ ├── runa-cli/ # Interactive REPL client
│ ├── runa-core/ # Shared stable identities and portable actor values
│ ├── 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-image-api/ # Trusted in-process image operations
│ ├── runa-protocol/ # Versioned transport-neutral wire DTOs
│ └── runa-client/ # Loopback WebSocket client
├── docs/
│ ├── architecture.md
│ ├── protocol.md
│ └── decisions/
├── runa-std/
│ ├── core/
│ ├── actors/
│ ├── ui/
│ └── tools/
├── scripts/
├── vscode/
├── examples/
└── tests/
runa-std/ is reserved for the default Rune scripts loaded into an image.
scripts/ is for general-purpose development automation. Root-level tests/
is reserved for system and end-to-end tests; it is not automatically discovered
by Cargo. Add an explicit runner when those tests are introduced, keeping Rust
crate integration tests within each crate.
Only the Phase 0 and minimal Phase 1 crates are present. Remaining crates will be added as their functionality is implemented.