Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The rhai runtime

The rhai runtime (runtime::rhai, kind rhai) lets an agent’s brain be a rhai script instead of a hand-written wasm component. It evaluates the script on the bundled rhai interpreter (a wasm component compiled into the host at build time whose omw.* host imports route to the very same global provider / tooling / bus as every other runtime).

The interpreter ships in the omw-rhai package/binary (nix run .#omw-rhai or the omw-rhai-<arch>.tar.gz release tarball). The default omw binary doesn’t include it. With the --features runtime-rhai build flag it is compiled into the host at build time instead.

Configuration

The rhai runtime takes no required parameters beyond the shared WASI sandbox:

[runtime.rhai]
kind = "rhai"

[[agents]]
name = "alice"
runtime = "rhai"
script = "brain.rhai"

A custom interpreter component can be substituted via the runtime’s interpreter parameter; otherwise the interpreter compiled into the binary is used. The same WASI sandbox keys as the wasm runtime apply, flattened alongside interpreter:

[runtime.rhai]
kind = "rhai"
inherit_env = true
env = { FOO = "bar" }

[[runtime.rhai.preopens]]
host_path = "./data"
guest_path = "/data"
perms = "read_only"

The interpreter and the WIT bindings

The bundled guest (omw-wasm-rhai-interpreter) exports the runtime interface (kind returns rhai, run(script) evaluates the script) and imports the omw world. On startup it registers an omw static module with three sub-modules that expose the WIT interfaces to the script:

  • omw::provider::get(name) — returns a provider handle map whose blocking chat, streaming chat_stream, is-open, cancel, list_models, and kind entries are methods. chat/chat_stream take an optional trailing params argument: either a map of generation settings (#{ temperature: 0.2, reasoning_effort: "high" }) or an already-JSON string (e.g. an endpoint-message’s params), merged over the provider’s configured defaults.
  • omw::tooling::get(name) — returns a tooling handle map whose list-tools, call-tool, is-open, cancel, call-tool-blocking, list-resources, read-resource, subscribe-resource-list, subscribe-resource, unsubscribe-resource-list, unsubscribe-resource, and kind entries are methods.
  • omw::host::* — the host helpers: log, time_now, time_format, wait_until, wait_for, wait_cron, cancel_timer, subscribe_agent, unsubscribe_agent, subscribe_lifecycle, unsubscribe_lifecycle, subscribe_endpoint, unsubscribe_endpoint, stream_endpoint, send_agent, recv, try_recv, new_uuid, base64_encode, base64_decode, memory_get, memory_set, memory_remove, sleep_for, sleep_until, and sleep_cron.

Handles are Rhai maps. Methods are FnPtrs stored on them, so scripts call them method-style (provider.chat_stream(...), tooling.call-tool(...)). The time functions take plain integer literals: the interpreter converts rhai’s i64 integers to the WIT u64 tick type at the boundary (rejecting negatives).

Values in rhai

Events come back as maps shaped #{ id, kind, payload }:

  • id — the envelope’s UUID;
  • kind — one of message, error, timer, chat-delta, chat-end, tool-result, resource-list-updated, resource-updated, endpoint-message, endpoint-session-end;
  • payload — the text for message/error, a map for chat-delta (with content, reasoning, tool_call { id, name, arguments }, finish_reason, and usage { prompt_tokens?, completion_tokens?, total_tokens? }), a map for tool-result ({ name, arguments, value }), a list of resource maps ({ uri, name, description?, mime_type? }) for resource-list-updated, a resource-content map ({ uri, mime_type?, content }) for resource-updated, a map for endpoint-message ({ session, messages, tools, params? }, with messages a list of { role, content?, reasoning?, tool_call? } maps, tools a list of { name, description?, input_schema }, and params the opaque JSON string the client submitted), a map for endpoint-session-end ({ session, error? }), and unit otherwise. The content field holds actual text for textual formats and base64 for anything else — match on mime_type to tell which. Decode binary payloads with omw::host::base64_decode (which returns a blob) and encode back with omw::host::base64_encode.

Example brain

let p = omw::provider::get("openai");
p.chat_stream("gpt-4o", [
  #{ role: "user", content: "say hi" },
], []);

let out = "";
loop {
  let ev = omw::host::recv();
  if ev.kind == "chat-delta" { out += ev.payload.content }
  if ev.kind == "chat-end" { break }
  if ev.kind == "error" { throw ev.payload }
}
out

The script’s final value becomes its terminal message when it is not unit.

Memory

memory_get returns the value or unit when absent; memory_set stores; memory_remove returns true when a value was present:

omw::host::memory_set("timer", omw::host::wait_for(1000));
// ... after a reload, the same context still has it:
let timer = omw::host::memory_get("timer");