283 lines
16 KiB
Markdown
283 lines
16 KiB
Markdown
# Phase 1 actor prototype
|
|
|
|
Start the server-owned, in-memory image:
|
|
|
|
```sh
|
|
mise run serve-network
|
|
```
|
|
|
|
Then connect from another terminal with `cargo run -p runa`, or use
|
|
`cargo run -p runa -- --connect <loopback-address>` for a custom listener.
|
|
Quitting the REPL leaves the image running. Reconnecting clients can list and
|
|
inspect the same actors; local bindings and history do not survive a REPL
|
|
restart. Server exit still loses the entire image.
|
|
|
|
The TCP server publishes [counter.rn](../runa-std/actors/counter.rn) as the
|
|
`counter` behavior. Commands include `spawn`, `send`, `tick`, `inspect`, `actors`,
|
|
`events`, `stop`, `restart`, `pause`, `resume`, `cancel`, `behaviors`,
|
|
`publish`, `history`, `rollback`, and `install`; enter `help` to see usage.
|
|
`publish` reads a Rune source file and takes `new` for a first revision or the
|
|
expected current revision for an update. `rollback` requires the expected
|
|
current revision and a target historical revision. `install` selects a named
|
|
behavior's current implementation and fails if the actor is not at an idle
|
|
handler boundary. `spawn` requires a behavior name and initial state, in that
|
|
order. All commands operate on the same process-local runtime and code store.
|
|
The CLI sends versioned protocol requests to the loopback-only TCP listener.
|
|
The separate stdio endpoint starts an empty image for child-process clients.
|
|
|
|
Example:
|
|
|
|
```text
|
|
runa> let sender = spawn counter 0
|
|
sender = @<actor-reference>
|
|
runa> let receiver = spawn 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
|
|
```
|
|
|
|
The sender commits state 1 and sends a structured message; the receiver commits
|
|
state 15. Actor references are 21-character NanoIDs; names are local REPL
|
|
bindings. Tab completes commands, behaviors, and bound actor references, while
|
|
Up/Down navigate command history. `tick <count> [instruction-budget]`
|
|
explicitly advances the scheduler. The default per-actor slice budget is 32
|
|
instructions.
|
|
|
|
State and messages use JSON literals in the REPL, preserving whitespace inside
|
|
strings. `spawn <behavior> <json>` and `let <name> = spawn <behavior> <json>`
|
|
require explicit initial state; `send <actor> <json>` takes one message value.
|
|
For example, `send receiver {"op": "add", "value": 7}` adds to the bundled
|
|
counter. Its supported operations are `add` and `send`; unknown operations
|
|
fail explicitly.
|
|
|
|
The sole-key object `{"$actor": "receiver"}` resolves a local binding to a
|
|
distinct actor-reference value. A literal `@<NanoID>` or raw NanoID string
|
|
can be used in place of the binding. Ordinary strings are never implicitly
|
|
converted to references. To encode a literal record that would otherwise
|
|
match a marker, wrap it in `{"$record": {...}}`. The wrapper escapes only that
|
|
record; nested values still use the normal conversion rules. Tab completion
|
|
also offers references inside a `"$actor"` string.
|
|
|
|
## Current contracts
|
|
|
|
- `runa-actor` owns actor references, mailbox and implementation IDs, structured
|
|
messages, snapshots, statuses and lifecycle events.
|
|
- `runa-core` owns stable source/implementation identities, actor references,
|
|
and the portable owned value model shared by the runtime and protocol.
|
|
- `runa-code` preserves original source, derives identities, caches compiled
|
|
artifacts, and provides an in-memory named behavior registry with revisions.
|
|
- `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-image-api` composes code publication and runtime operations behind a
|
|
trusted in-process service boundary.
|
|
- `runa-protocol` defines versioned transport-neutral request, response, and
|
|
event DTOs; it does not dispatch requests or access runtime state.
|
|
- `runa-server` dispatches those requests through `runa-image-api` and maps
|
|
results to protocol replies; its executable accepts newline-delimited JSON
|
|
over stdin/stdout or TCP bound to loopback, with a 1 MiB request-line limit.
|
|
- `runa-client` provides correlated, bounded stdio and loopback TCP clients.
|
|
- `runa` provides an interactive CLI using the TCP client, without an embedded
|
|
runtime. Connection errors are explicit; requests are never auto-retried.
|
|
|
|
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
|
|
fingerprint; actor references are random NanoIDs and mailbox IDs remain
|
|
process-local counters. Incarnation counters use checked increments.
|
|
|
|
`CodeStore::publish_behavior` compiles source before appending a behavior
|
|
revision. Creation expects no existing revision; updates require the caller's
|
|
expected current revision, so stale edits fail without changing the behavior
|
|
reference. Failed compilation also leaves the reference and history unchanged
|
|
(the exact source remains available for diagnostics). Rollback points the
|
|
behavior at an earlier immutable implementation and appends a new monotonically
|
|
increasing revision; it does not rewrite history. Behavior names are
|
|
case-sensitive paths limited to ASCII letters, digits, `_`, `-`, `.`, and `/`,
|
|
with empty, `.` and `..` path components rejected.
|
|
|
|
The runtime's `install` operation switches an actor's implementation only
|
|
when it is idle, has no active invocation, and has no reserved input. Queued
|
|
messages may remain; they will use the newly installed implementation. Active
|
|
or awaiting handlers keep their original implementation, and installation
|
|
attempts during execution fail explicitly so the caller can retry at the next
|
|
handler boundary. `install` preserves the current structured state unchanged.
|
|
|
|
Runtime callers that need a state transformation can use
|
|
`install_with_migration`, which receives the committed state and computes its
|
|
replacement before changing either state or implementation. The operation
|
|
requires the same idle safe point; if the migration returns an error, neither
|
|
value changes and no installation event is emitted. A successful changed
|
|
state emits `StateMigrated` as well as `BehaviorInstalled`. This prototype
|
|
accepts a host-provided migration closure only: it does not infer schema
|
|
compatibility or execute migration code from Rune source. The closure receives
|
|
an owned copy of the structured state; an invalid result also leaves the
|
|
original state and implementation unchanged. Schema/version policy remains
|
|
future work.
|
|
|
|
`pause` prevents an actor from being scheduled on subsequent worker ticks and
|
|
retains its active invocation, reserved message, and prior `Running`, `Awaiting`,
|
|
or `Idle` status. `resume` restores that status; an awaiting invocation resumes
|
|
only if its future has been woken. These operations take effect between calls
|
|
to `tick` and cannot interrupt a currently executing Rune slice or native
|
|
callback. They provide cooperative scheduling control, not preemption.
|
|
|
|
Actor snapshots include basic execution telemetry while an invocation is
|
|
active: completed VM instruction-step count, Rune instruction pointer, and
|
|
call-frame depth. Paused invocations retain the same telemetry. These values
|
|
are execution-local diagnostics only; they do not map to source locations or
|
|
expose locals, and disappear when the invocation completes or is cancelled.
|
|
|
|
`cancel` is distinct from `stop`: it drops an actor's active invocation, moves
|
|
its reserved message to the front of the mailbox, and returns the actor to
|
|
`Idle`. The input is therefore eligible for retry on the next tick, ahead of
|
|
messages that were already queued. Committed state is unchanged and no
|
|
transition effects are applied. Cancellation is also available for paused
|
|
invocations. It does not undo external side effects already performed by
|
|
native callbacks, so retries may repeat such effects.
|
|
|
|
The source identity hashes the exact original UTF-8 bytes with the versioned
|
|
`runa-source:v1` domain. Whitespace/comment canonicalization is intentionally
|
|
not attempted; textually different source has a different identity. The
|
|
implementation identity includes the source ID, pinned Rune version, compiler
|
|
settings/features, registered native-module manifest, runtime ABI version, and
|
|
Cargo target triple. The native-module manifest and runtime ABI identifier must
|
|
be updated when those compile inputs change.
|
|
|
|
The cache is in-memory only. Invalid source is retained in the current
|
|
`CodeStore` for diagnostics, but does not produce a compiled artifact. Cache
|
|
identity is not a claim that Rune bytecode is portable or safe to persist.
|
|
`CodeStore::compile_with_module` requires an explicit stable module identity;
|
|
callers must change it when registered native functions or their contracts
|
|
change. The server creates a fresh `CodeStore` when its image starts.
|
|
|
|
State and messages are owned `Value` trees: null, boolean, signed 64-bit
|
|
integer, UTF-8 string, list, string-keyed record, or actor reference. Public
|
|
actor references are 21-character URL-safe NanoIDs. They are generated when
|
|
actors are spawned and remain stable across stop/restart; mailbox IDs are
|
|
separate internal counters. Rune handlers have this interface:
|
|
|
|
```text
|
|
handle(state, message) -> (new_state, [(target_actor_ref, message), ...])
|
|
```
|
|
|
|
The former `kind`, `value`, and `target_ref` envelope is removed. Actors choose
|
|
their own message records. For example:
|
|
|
|
```rune
|
|
pub fn handle(state, message) {
|
|
state.items.push(message.text);
|
|
(state, [(message.reply_to, #{accepted: true})])
|
|
}
|
|
```
|
|
|
|
The runtime allocates a fresh Rune object graph for each state and message
|
|
input, and copies portable data back out before committing. A Rune handler can
|
|
mutate its invocation-local draft but cannot mutate committed state, the
|
|
reserved input, or another actor's data. Copies made for separate effect
|
|
messages do not share mutable Rune objects.
|
|
|
|
Null maps to Rune unit `()`, lists to Rune vectors, records to anonymous
|
|
objects (`#{...}`), and references to an opaque native `runa::ActorReference`.
|
|
Effect targets must be references, not lookalike strings. References support
|
|
equality and cloning in Rune and can be retained and forwarded through
|
|
state/messages; this is not authentication.
|
|
Floats, nonempty tuples, options, closures, futures, custom Rune structs, and
|
|
other native objects are not portable actor data and fail conversion. Cyclic
|
|
or excessively deep Rune output also fails without committing.
|
|
|
|
Each state/message is limited to depth 32 (root depth 0), 4,096 value nodes
|
|
(including the root), and 1 MiB of UTF-8 string/key bytes. A transition permits
|
|
at most 4,096 send effects. Transport line-size limits apply separately.
|
|
These are conversion limits, not a Rune heap quota or hostile-code sandbox.
|
|
Schema/version policy and capability-bearing contexts remain future work.
|
|
|
|
## Scheduling and lifecycle
|
|
|
|
`tick(nonzero_budget)` gives each runnable actor one bounded slice in actor
|
|
reference order.
|
|
FIFO queues are bounded, including the reserved message in capacity accounting.
|
|
A queued message is reserved when its invocation starts. Subsequent messages
|
|
stay queued, including while the actor awaits. A wake marks an awaiting actor
|
|
runnable; the caller still needs to drive ticks.
|
|
|
|
Successful handlers validate every send target and its aggregate queue
|
|
capacity before updating state, acknowledging the reserved message, and
|
|
enqueueing effects. Invalid effects fail the actor without partial sends or a
|
|
state commit. This is a **logical, single-worker in-memory atomicity guarantee**:
|
|
observers using the runtime see either the transition before commit or after
|
|
commit, not an intermediate state/effect set. It is not crash-safe or durable.
|
|
External side effects performed by native callbacks are outside this boundary
|
|
and cannot be rolled back.
|
|
|
|
Runtime errors retain the last committed state and reserved message, set an
|
|
observable failed status, and emit a failure event. The failed message is not
|
|
silently acknowledged or automatically retried. External sends return errors
|
|
for unknown, unavailable, or full actors.
|
|
|
|
Stopping drops the invocation, retaining committed state, mailbox and reserved
|
|
message. Restarting a stopped or failed actor preserves its actor reference,
|
|
mailbox ID, implementation ID, committed state, and queued-message order; it
|
|
increments its incarnation and puts the reserved message at the front of the
|
|
queue.
|
|
Restart is explicit. Repeated failure of the same reserved message can therefore
|
|
block later messages until an operator changes the implementation or otherwise
|
|
resolves it; there is no automatic poison-message policy or supervision.
|
|
|
|
### Transition and retry guarantees
|
|
|
|
| Situation | State | Reserved input | Effects | Status |
|
|
| --- | --- | --- | --- | --- |
|
|
| Handler returns a valid transition | New state commits | Acknowledged | All validated sends enqueue | `Idle` |
|
|
| Rune execution or transition decoding fails | Last committed state retained | Retained | None from the returned transition | `Failed` |
|
|
| Any target is unknown, stopped, failed, or over capacity | Last committed state retained | Retained | No sends from the transition enqueue | `Failed` |
|
|
| Stop during an invocation | Last committed state retained | Retained | None from the incomplete transition | `Stopped` |
|
|
| Restart after stop or failure | Retained until retried | Moved to queue front | None until retry completes | `Idle` |
|
|
| Pause between ticks | Last committed state retained | Retained | None while paused | `Paused` |
|
|
| Resume a paused invocation | Retained until invocation completes | Retained | None until completion | Prior runnable/awaiting status |
|
|
| Cancel an invocation | Last committed state retained | Moved to queue front | None from incomplete transition | `Idle` |
|
|
|
|
Mailbox capacity counts queued plus reserved messages. Before applying a
|
|
transition, the runtime validates all effects, including aggregate sends to a
|
|
single target. For a self-send, the completing input is acknowledged as part
|
|
of the same transition, freeing its reserved slot before the new effects are
|
|
enqueued. An invalid target or over-capacity effect rejects the entire returned
|
|
transition.
|
|
|
|
These are process-local guarantees only. A host crash loses state, mailboxes,
|
|
reservations and events. External/native callback effects may already have
|
|
occurred even when the Rune invocation later fails or is stopped.
|
|
|
|
Lifecycle tests verify that failed and cancelled reservations retry from the
|
|
last committed state, that stop/restart preserves reserved-first FIFO order,
|
|
and that cancellation followed by installation retries queued input in order
|
|
with the new implementation. Tests also cover that a running actor's queued
|
|
messages are not processed until the reservation finishes. Transition tests
|
|
cover malformed results, runtime failures, invalid targets, aggregate
|
|
capacity, and successful message effects.
|
|
|
|
## Limits
|
|
|
|
This is not yet a full image: no persistent behavior registry or mailboxes,
|
|
transactional outbox, supervisors, GUI, or automatic image bootstrap. The
|
|
behavior registry and implementation installation are process-local;
|
|
installation requires an idle handler boundary and does not upgrade active
|
|
execution. The example counter script is bundled through `include_str!`.
|
|
Messages are not accepted by stopped/failed actors.
|
|
|
|
Native functions must be nonblocking. Dropping a future proves cleanup only
|
|
for the controlled test operation, not rollback of arbitrary external effects.
|
|
Instruction slices cannot interrupt a native callback.
|
|
|
|
The [execution decision](decisions/0001-worker-local-execution.md) records the
|
|
scheduling trade-offs. Heap profiling and source-level debugging remain open.
|