runa/docs/runtime.md

234 lines
13 KiB
Markdown

# Phase 1 actor prototype
Start the interactive, in-memory runtime session with:
```sh
mise run run
```
The executable 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 uses the versioned protocol dispatcher, also available over stdio or
the loopback-only TCP listener for local development.
Example:
```text
runa> let sender = spawn counter 0
sender = @<actor-reference>
runa> let receiver = spawn counter 10
receiver = @<actor-reference>
runa> send sender 1 5 --to 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.
## Current contracts
- `runa-actor` owns actor references, mailbox and implementation IDs, structured
messages, snapshots, statuses and lifecycle events.
- `runa-core` owns stable source and implementation identity types.
- `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` 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
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 `i64` 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. Its state contract
is still `i64`, so structured schema/version migration 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 CLI currently creates a fresh `CodeStore` when it starts.
Prototype state is an `i64`; messages contain integer `kind` and `value`
fields, plus an optional target actor reference for a handler-produced send.
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 deliberately restricted
interface:
```text
handle(state, kind, value, target_ref)
-> (new_state, [(target_ref, kind, value), ...])
```
`target_ref` is an empty string when the incoming message has no routed target.
The returned effect vector describes sends; handlers cannot access the runtime
or directly mutate committed state. General schema/versioned values and
capability-bearing contexts are 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.