runa/vscode
Steve Biedermann 19eaeb447c feat: add actor source stepping and breakpoints
Add implementation-bound line breakpoints and bounded step-into for paused active actors. Preserve invocation drafts and transactional sends, expose debugger stop reasons, and wire VS Code actor controls with exact loaded-source navigation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-03 20:53:43 +01:00
..
integration feat: add actor source stepping and breakpoints 2026-11-03 20:53:43 +01:00
resources feat: add a dedicated Runa sidebar 2026-11-03 17:49:04 +01:00
scripts feat: add a dedicated Runa sidebar 2026-11-03 17:49:04 +01:00
src feat: add actor source stepping and breakpoints 2026-11-03 20:53:43 +01:00
test feat: add actor source stepping and breakpoints 2026-11-03 20:53:43 +01:00
.prettierignore feat: add a localhost VS Code actor inspector 2026-11-03 14:52:28 +01:00
.prettierrc.json feat: add a localhost VS Code actor inspector 2026-11-03 14:52:28 +01:00
README.md feat: add actor source stepping and breakpoints 2026-11-03 20:53:43 +01:00
package-lock.json feat!: use runa-debug WebSockets with live image notifications 2026-11-03 15:36:21 +01:00
package.json feat: add actor source stepping and breakpoints 2026-11-03 20:53:43 +01:00
tsconfig.json feat: add a localhost VS Code actor inspector 2026-11-03 14:52:28 +01:00

README.md

Runa VS Code client

This is a development client for a trusted localhost Runa image. It uses the version 5 protocol over WebSockets with the runa-debug subprotocol, not Rust APIs, and never hosts actors or starts its own image.

Run from source

Requirements: VS Code 1.96+, Node.js 22+, npm, and the repository's Rust toolchain.

From the repository root:

npm --prefix vscode ci
mise run start

Open the repository in VS Code. In Run and Debug, select Run Runa Extension and press F5. The launch task compiles TypeScript before opening an Extension Development Host.

In that host:

  1. Run Runa: Connect to Local Image. The default is 127.0.0.1:3737; runa.address changes the suggested endpoint.
  2. Open Runa in the Activity Bar. Its Image Sources view automatically shows the image's source directory (see Image source files). Right-click a .rn file and choose Spawn (initial state null) or Spawn with State... (enter JSON in the input field, for example 0 for actors/counter). Runa: Spawn Actor in Actors prompts for a source path and state. The command reports and returns the new actor reference. Mailbox capacity is 64.
  3. Expand an actor directory to open its metadata, configuration, state, mailbox, or log files. Runa: Inspect Actor also opens a combined JSON snapshot.
  4. Right-click an actor to send a JSON message or a system message: pause, resume, stop, kill, restart, reload, or become another source path. These actions are also available in the Command Palette with an actor picker.
  5. Use the Advance Scheduler button in the Actors toolbar (or Runa: Advance Scheduler) to execute an explicit number of ticks (instruction budget 32 per actor). Send only queues a message; it does not execute it. For the bundled counter, send {"op":"inc"}, then advance. It defaults missing state to zero on its first message and queues further increments to itself. Pause/resume controls its loop.
  6. Run Runa: Disconnect to close only this client connection.

JSON input supports null, booleans, signed 64-bit integers, strings, arrays, and objects. Floats and out-of-range integers are rejected rather than rounded. Use {"$actor":"@<21-character-reference>"} for a typed actor reference and {"$record":{"$actor":"literal"}} to escape a marker-shaped object. REPL-local names are not shared with the extension. Input uses the same depth/node/string limits as the runtime; the full wire request must also fit 1 MiB. Cancelling a prompt sends no mutation. Changing connections during a prompt rejects the action instead of applying it to the new image.

Connection prompts accept numeric IPv4 loopback addresses (127.x.x.x) or [::1]:port. DNS names, wildcard addresses, remote addresses, and IPv4-mapped IPv6 addresses are rejected. There is no authentication: connect only to a trusted local server. In a remote VS Code window, localhost refers to the machine running the extension host, not necessarily the desktop.

Snapshot semantics

  • The tree shows actor directories with references, lifecycle status, and queued/reserved counts. Tooltips include implementation ID, incarnation, and failure text.
  • Actor directories contain:
    • meta.json: read-only identity, lifecycle, execution telemetry, source path (behavior_path), pending source path, and temporary-behavior flag.
    • config.json: {"log_level":"info"}, editable even while running.
    • state.json: pretty-printed natural JSON state with two-space indentation, using $actor/$record markers as needed and preserving exact 64-bit integers.
    • mailbox.json: {"reserved":[],"queued":[]}, with at most one reserved message. State and mailbox are writable only when paused or failed with no active invocation. JSON uses two-space indentation. Saving never resumes the actor.
    • actor.log: read-only retained entries, automatically refreshed on image changes, recognized by VS Code's built-in Log language for syntax highlighting. Entries use an RFC 5424-style syslog header: <134>1 - - runa <actor-ref> <sequence> - [INFO] message. Priorities use the local0 facility and mapped severity (trace/debug: debug, info: informational, warn: warning, error: error, fatal: critical). Timestamp and hostname are - because the image does not record them; sequence IDs preserve ordering without inventing timestamps. Embedded CR/LF characters are escaped so each entry occupies one line. Only the server's latest 1,000 accepted entries per actor are available.
    • behavior: present after an actor-specific temporary behavior is saved. These files use connection-scoped runa-actor: URIs. Removed/disconnected actors' open JSON files retain their last values and become read-only; already opened log/behavior files retain cached text.
  • Inspect fetches a fresh snapshot through inspect_actor. Documents show committed state, FIFO queued messages, the reserved message, status, and active-execution telemetry. Tagged values distinguish actor references from strings and preserve signed 64-bit integers exactly.
  • Documents can be edited and saved while the actor is paused or failed without an active invocation. Edit the tagged actor.state, actor.reserved, actor.queued, and actor.log_level fields. Saving applies changed writable fields; changes to identity, status, telemetry, connection metadata, and other protected fields are discarded with a warning. The editor then reloads the canonical server snapshot. Invalid writable fields reject the save. Running, idle, suspended, and disconnected actors are read-only. Unsaved edits are not replaced by automatic refreshes; saving rechecks the current actor status. State/message edits and log-level changes are separate server operations, not one transaction. The timestamp records when this client received the data, not a runtime clock. Server image_changed notifications automatically update the tree and open documents. Bursts are coalesced, and changes arriving during a fetch trigger a follow-up fetch. Refresh Image is also available on demand.
  • Open documents are retained and marked connected: false on disconnect.
  • Stop removes the actor after graceful shutdown (advance the scheduler); kill removes it immediately, including failed actors. Failure alone does not remove an actor. Removal discards state, mailbox, and retained logs. Removed actors disappear from the tree; open snapshots keep their last values with removed: true and become read-only. Restart preserves a live/failed actor's identity but cannot recover a removed actor.
  • Explicit disconnect, reconnect, and extension deactivation complete the WebSocket close handshake. Deactivation waits for outstanding closes; an unresponsive peer is forcibly disconnected after five seconds. Protocol corruption and request failures still terminate the connection without retry. Each connection gets distinct document URIs; reconnecting never replaces snapshots from an earlier connection or another image. Snapshots are kept for the extension session, not persisted across VS Code restarts.
  • There is no auto-connect, polling, automatic reconnect, or request retry. Timeout, peer disconnect, malformed data, and version/correlation failures invalidate the connection and are surfaced in a notification and the Runa output channel. Server-reported operation errors leave the connection usable.
  • The client never ticks the scheduler implicitly or drains get_events. Notifications are non-destructive invalidations, not a replayable runtime event stream; reconnect fetches the current image rather than replaying history.

Execution stack inspection

Right-click an actor and choose Inspect Execution Stack... (also available in the Command Palette). Select a frame to open the exact loaded Rune source in a read-only runa-execution: document and highlight its source span. The picker identifies the incarnation, completed VM steps, functions, and source positions. The current frame points to the next instruction, or the pending await instruction at the last safe snapshot; callers point to call sites, not their saved return addresses. At most 64 innermost frames are shown, with an explicit truncation notice. Missing debug locations are labeled unavailable.

This never advances the scheduler or pauses the actor. Pause first for a stable snapshot; running inspection is a point-in-time view. Idle/failed actors have no active stack, and removed actors cannot be inspected. Pending source/temporary behavior changes do not replace the loaded code. Open loaded documents retain their text after edits, actor removal, and disconnect; connections use distinct URIs. These are not source editors or temporary behavior documents.

Only the head Rune VM's visible frames are available, not native Rust frames or stacks hidden inside nested futures. There are no locals, post-mortem stacks, or Debug Adapter Protocol support yet. For automation, runa.inspectExecution accepts {actor_ref: string} and an optional zero-based frame index to navigate without the picker.

Source breakpoints and step-into

Use the actor context menu or Command Palette:

  1. Set Actor Breakpoints... accepts 1-based lines separated by commas in the actor's loaded source, not a newer working/temporary buffer. It replaces the actor's list; blank input clears it. Up to 128 lines are accepted. Lines without executable entries are reported as unverified, never moved silently.
  2. Queue a message and Advance Scheduler. A breakpoint pauses that actor before a mapped line entry, preserving draft state and buffered sends. Inspect Execution Stack... shows the breakpoint stop reason and opens the exact loaded code. Other actors continue on ordinary ticks.
  3. Step Into Source Line advances only a paused actor with an active invocation, up to 1,000 instruction polls. It stops at another mapped line, call-depth change, breakpoint, pending await, completion/failure, or budget. It automatically opens the current frame. The Runa output channel records the reason; budget/await/completion outcomes also produce a notification.
  4. Resume Actor and advance the scheduler to continue normally. Resume crosses the stopped breakpoint once; a loop can hit it again.

An idle actor cannot be stepped: set a breakpoint, send, then tick first, or pause an already active handler. A completed step commits normally and leaves the actor paused at the handler boundary, without consuming another message. Failure is surfaced with its error text; successful shutdown may remove the actor. Same-line loops are bounded and pending awaits return immediately. Installation clears breakpoints to prevent stale source mappings; pending edits do not. Breakpoints belong to the actor/image, so disconnect does not clear them. Restart on unchanged code retains them. These are command-based controls, not editor-gutter breakpoints or an F5/debug-toolbar session. Step-over/out, conditions, hit counts, and variables remain planned.

For automation, runa.setBreakpoints accepts {actor_ref: string} and optional comma-separated 1-based line text (including "" to clear). runa.stepSource accepts {actor_ref: string} and returns its stop reason.

For automation, the runa.connect command also accepts an explicit endpoint string, and runa.inspect accepts an object with an actor_ref field. Copy Actor Reference in the actor context menu or inline button copies the raw reference to the clipboard. runa.spawn accepts { path: string, state: string }, where state is natural JSON text, and resolves to the new reference. runa.spawnSource and runa.spawnSourceWithState accept a source file URI (the latter also optional JSON state text). runa.send accepts { actor_ref: string } and JSON message text. runa.pause, runa.resume, runa.stop, runa.kill, runa.restart, and runa.reload accept { actor_ref: string }; runa.become also accepts a target source path. runa.tick accepts a positive integer tick count. Command failures are reported in the Runa output channel and an error notification; commands are never retried.

Image source files

Actor context menus also offer Edit Actor State... and Edit Reserved and Queued Messages.... They prefill JSON from the current snapshot; editing requires a paused or failed actor with no active execution. Message input uses {"reserved":[],"queued":[]} with zero or one reserved message. Clearing the reservation removes the failed input that would otherwise retry on restart. Edits do not resume actors. Set Actor Log Level... selects trace/debug/info/warn/error/fatal (default info); snapshots show the level. The actor's actor.log file shows retained output; CLI logs [-f] [actor...] also supports a live event subscription.

Actor-specific behaviors

Edit Current Behavior opens the actor's loaded script, not the potentially newer working source. Saving compiles and schedules a temporary actor-specific implementation for the next safe handler boundary; it never mutates active frames, state, mailbox, shared source, or commit history. Compilation errors reject the save and keep the previous behavior. Repeating the action reuses the same document and temporary text. Other clients can read/edit the same temporary behavior through the server; stale saves fail with a source-identity conflict. Temporary behaviors survive actor restart, but not actor removal or server exit. For failed actors, the new implementation is installed without clearing failure or the reserved message; restart explicitly to retry.

Reload Actor Source clears the override and schedules current shared source. Go to Behavior opens the original image source. Update Behavior... asks before copying the temporary script into shared working source, checking both source identities and the actor's source path for concurrent changes. It does not commit or reload other actors. Unsaved temporary editor text is not copied.

The dedicated Runa Activity Bar sidebar contains Image Sources first, followed by Actors. Both views are visible by default and do not require an open workspace. The source tree uses native VS Code tree items, file icons, directory expansion, editor opening, and file context menus.

The image owns actor source text. On connect, Image Sources automatically shows its virtual filesystem beneath source (runa-image://<endpoint>/source). No workspace changes, folder prompt, or extension restart is required. Opening a file reads it from the image; saving writes back to the image. The extension never adds image folders to the workspace. VS Code's public API does not support overlaying virtual files onto the built-in workspace file tree, so image files appear in the Runa sidebar's source tree backed by the filesystem provider. Directories are derived from source paths: actors/counter appears as source/actors/counter.rn. Source paths are canonical and exclude the source/ prefix and .rn suffix.

  • Save writes the image's working source with compare-and-swap against the last observed source_id. If another client changed the file, the save fails instead of overwriting; reopen or revert to load the image's text. Saving never commits. New Source File... in the toolbar or a directory context menu creates a file at a relative path (including nested directories).
  • Rename or Move Source... changes a file's path and moves its commit history. If installed or pending actor behaviors use it, a warning asks to update their paths atomically. Declining cancels the operation. Loaded code does not change; future reloads follow the new path. Existing destinations are not overwritten.
  • Delete Source confirms deletion of a file and its history. The server rejects deletion if any nonfailed actor still uses its installed or pending path, including paused actors: stop them first. Failed actors may retain loaded code after deletion, but reload/go-to-source then fail until the path is recreated. Directory rename/delete and cross-image moves are not supported.
  • Spawn uses the latest saved working source, committed or not. If the editor has unsaved changes, spawn and commit ask to save first; cancelling does nothing. Unsaved editor text is never used for spawning/committing; automatic diagnostics do send it to the connected trusted image for validation.
  • Spawn with State... opens a JSON input field; cancelling the field does not spawn an actor.
  • Runa: Commit Source records the saved working source as a new revision and reports its revision string.
  • Runa: Source History lists revisions for the file and opens the selected one as a read-only runa-source: document. Revision documents are cached per connection session, never change, and remain open after disconnect.
  • Runa: Roll Back Source asks for confirmation and replaces the working source with the selected revision. It does not change running actors; use Reload or Become to install source into an actor.

Server notifications refresh the source tree and clean open editors. For automation, runa.commitSource accepts a source URI and returns the revision; runa.sourceHistory accepts a URI and optional revision and returns the opened document URI; runa.rollbackSource accepts a URI and optional revision and returns the new source_id after confirmation.

Compilation diagnostics

Open image sources and actor-specific behavior documents are checked after a 300 ms edit debounce. Rune errors and warnings appear inline and in Problems, with zero-based UTF-16 ranges converted from Rune's UTF-8 byte spans. Non-ASCII text and CRLF line endings retain correct editor locations. Diagnostics without a source location are displayed at the start of the document.

Validation compiles the current editor text through the connected image's native context; it never executes the script, saves a source, commits, installs an implementation, or changes actor state/mailboxes. Validation artifacts are discarded rather than filling the image's compile cache on every keystroke. Shared source may still be saved/committed with compilation errors; spawn and reload require successful compilation. Temporary behavior saves still reject invalid code without replacing the previously installed implementation.

Checks are serialized and edits coalesced. Responses for old document versions, closed editors, or disconnected sessions are discarded. Closing a document or disconnecting clears its diagnostics; reconnect validates current open image sources but not old-session actor documents. Compiler errors are normal validation results, not connection failures. Transport and compiler-service failures remain explicit notifications in the Runa output channel.

Compilation checking is single-source, not an LSP: completion, cross-file import resolution through image source paths, and quick fixes remain planned. Debugger controls are described separately above; variable inspection is not implemented.

Development and validation

From the repository root:

mise run format
mise run lint
mise run test
mise run test-extension-host

The normal test task covers portable-value validation, framing and UTF-8 fragmentation, subprotocol negotiation, interleaved notifications, refresh coalescing, timeout/disconnect behavior, exact 64-bit integer display, and interoperation with the real Rust server on an ephemeral loopback port.

The separate host test downloads VS Code 1.96.4 to the ignored .vscode-test/ cache in a temporary multi-root workspace and runs source folder save/spawn/ commit/history/rollback, spawn/send/system-message/tick, connection, inspection, disconnect, and reconnect commands against a test-owned Rust server. It verifies automatic document updates after another client sends messages, pauses, and advances an actor into failure, as well as session isolation. On headless Linux:

xvfb-run -a mise run test-extension-host

Detailed runtime event subscriptions, packaging for distribution, and debugging remain future work.