303 lines
15 KiB
Markdown
303 lines
15 KiB
Markdown
# 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](docs/architecture.md) 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:
|
|
|
|
```sh
|
|
mise run start
|
|
```
|
|
|
|
In another terminal, connect the REPL:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```text
|
|
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](docs/runtime.md)
|
|
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](docs/rune-feasibility.md) for verified Rune
|
|
capabilities and limitations, and [the runtime guide](docs/runtime.md) 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](vscode/README.md)
|
|
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.
|
|
|
|
```text
|
|
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](https://mise.jdx.dev/) 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`.
|
|
|
|
```sh
|
|
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
|
|
|
|
```text
|
|
.
|
|
├── 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.
|