runa/docs/runtime.md

12 KiB

Phase 1 actor prototype

Start the interactive, in-memory runtime session with:

mise run run

The executable loads counter.rn as the default 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 accepts an optional behavior name and otherwise uses counter. All commands operate on the same process-local runtime and code store. This CLI is for local development, not a versioned external API.

Example:

runa> spawn 0
spawned ActorId(1)
runa> spawn 10
spawned ActorId(2)
runa> send 1 1 2
message queued
runa> tick 100
runa> inspect 1
runa> inspect 2

The sender commits state 1 and sends a structured message; the receiver commits state 15. tick <count> [instruction-budget] explicitly advances the scheduler. The default per-actor slice budget is 32 instructions.

Current contracts

  • runa-actor owns actor, 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 uses newline-delimited JSON over stdin/stdout with a 1 MiB request-line limit, not a network listener.
  • 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 fingerprint; actor and mailbox IDs remain process-local counters. Actor IDs and 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. Rune handlers have this deliberately restricted interface:

handle(state, kind, value) -> (new_state, [(target_actor_id, kind, value), ...])

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 ID 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 ID, 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, API server, 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 default 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 records the scheduling trade-offs. Heap profiling and source-level debugging remain open.