The actor model
omw is built around a small actor model. Each configured agent is an actor: it
owns a single inbox on a shared event bus, and runs its “brain” — the agent
runtime — for repeated iterations against that inbox. Everything the agent
touches (chat streams, tooling resources, timers, other agents) arrives at that
one inbox as a tagged event, so the brain is a pure, mostly-synchronous event
consumer.
This page explains why the model is shaped that way and how the pieces fit together. The concrete interfaces are documented in host.
The single inbox
Every agent gets exactly one inbox: a bounded channel (see inbox_bound in
tunables) on a shared message bus. Nothing is routed to the
brain directly — chat deltas, tool results, timers, and messages from other
agents all land in the same inbox as an EventEnvelope carrying:
id— the UUID of the subscribed source the event came from.event— the strongly-typed payload (message,error,timer,chat-delta,chat-end,resource-list-updated,resource-updated,endpoint-message,endpoint-session-end).
The brain consumes events with host.recv (a blocking receive with a host-side
timeout, see recv_timeout_secs in tunables) or
host.try-recv (a non-blocking poll). Because every event is tagged with a
UUID, a single inbox is enough to multiplex many concurrent sources — the brain
correlates a delta or timer to the specific handle that opened it by matching
the envelope id against the UUID returned by the call that created it.
Nothing is shared by default
Inter-agent messaging is subscription-based. An agent does not receive anything from another agent unless it explicitly subscribed:
host.subscribe-agent(agent)returns a new UUID handle for that source.host.unsubscribe-agent(uuid)removes a subscription by handle.host.send-agent(agent, payload)delivers the message only if the recipient subscribed to the sender. Each recipient’s message is tagged with the UUID of its own subscription to the sender, not a global topic, so a sender fanning out to many subscribers reaches each one through a distinct handle.
This keeps the coupling between actors explicit and auditable: an agent can only ever be contacted by the actors it chose to listen to.
I/O sources are pump tasks
Synchronous wasm brains cannot await async I/O directly. Instead, every
long-lived I/O source is driven by a pump task spawned onto the agent’s bridge
runtime (the AgentContext.rt tokio runtime), which pushes events into the
inbox:
- a
provider.chat-streamcall spawns a chat-stream pump that deliverschat-deltaevents as chunks arrive and a terminalchat-end(orerror) event when the stream closes. - a
tooling.call-toolqueues a tool-call pump that delivers atool-result(orerroron failure) event. - a
tooling.subscribe-resource-list/subscribe-resourcecall spawns a resource pump that deliversresource-list-updated/resource-updatedevents. - a
host.wait-until/wait-for/wait-croncall schedules a timer that pushes aTimerevent at the deadline.
Every pump holds a cancel signal keyed by its UUID handle: provider.cancel,
host.cancel-timer, and the tooling.unsubscribe-* calls can drop it early,
stopping further deliveries before the source naturally ends.
Pull-style calls (list-models, list-tools, list-resources) are far
shorter, so the host runs them to completion with rt.block_on instead of
spawning a pump. Both approaches run off the wasm thread — pump tasks on the
tokio runtime, blocking calls on the spawn_blocking thread the engine runs on
— so the synchronous engine never blocks a tokio worker.
Endpoint requests
Endpoint requests are different: the endpoint server
routes an inbound chat completion straight into the owning agent’s inbox as an
endpoint-message event, and the agent streams deltas back through the session
registry. Each session ends exactly once with an endpoint-session-end event.
Why it is shaped this way
Keeping a single inbox per agent means the brain’s scheduling does not live in the host. The agent decides, iteration by iteration, which events to handle and in what order — the host just guarantees that everything relevant eventually shows up, tagged, in order on one queue. That is what lets the brain (whether a hand-written wasm component, a rhai script, or a js script) be written as a plain sequential program over a stream of facts.