Skip to main content
State is plain instance attributes. Handlers write them, run() reads them. The whole subject is when that happens, and the answer comes from one fact: your model runs on a single event loop.

Write in the handler, read in the loop

Every parameter a client can change has the same three parts:
load() sets a starting value, so the first frame renders before any client sends a command. The handler stores the new value on self, and run() reads the attribute again on every pass, so the change applies to the next frame.

One loop, one thing at a time

run(), your command handlers, and your lifecycle hooks are three tasks on the same event loop. The runtime starts them together after load(), and exactly one of them holds the loop at any moment. A command that arrives while run() is working waits on a queue. It is dispatched at the next point run() awaits:
Two things follow, and together they are why models on this runtime need no locks. A blocking call is safe. A synchronous forward pass holds the loop for its whole duration, so no handler can land in the middle of it. self.prompt is the same value on the first line of the pass as on the last, and a half-applied command is not a state your model can reach. emit() is your yield point. It always awaits, so a loop that emits every pass always picks up whatever queued up during the pass. While emit() waits for downstream room, the loop is free and handlers dispatch there too, which is why backpressure slows your generation without freezing your commands.
A loop that never awaits never picks up commands. If a code path can run for a while without emitting — a warm-up, a retry, a long wait on something external — put await asyncio.sleep(0) in it so handlers get their turn.

Snapshot across awaits

The guarantee holds for one uninterrupted stretch, so a pass with several awaits in it can read two different values of the same attribute. Copy what the pass depends on into locals first:
Without the snapshot, encode() can use one prompt while the brightness step uses a value from a command that arrived after it, and one frame mixes two states. The same applies when you move a slow pass onto a worker thread with asyncio.to_thread — that frees the loop, so handlers run during the pass.

Keep a handler shorter than a frame

A handler can do real work, and when that work depends only on the new value it belongs there: the handler runs once per change, while run() runs continuously.
run() then reads self._embedding and never encodes again. The underscore marks a value the model derives for itself rather than one a client sets. The budget is the same one run() spends: a handler holds the loop while it runs, so a slow one delays the next frame. When the work takes longer than a frame, store the input and let run() pick it up.

State that resets

load() runs once for the life of the process, and one process serves session after session. State that belongs to a session — a step counter, a conversation, a cache of what this audience has seen — is set up in @session_started and released in @session_ended:
That is also the reliable place for it. A session can end with clients still attached, and the runtime tears those connections down without firing @disconnected, so cleanup hung off the per-client hook is skipped. See Sessions & Clients.

Next

Media Input

Read the client’s camera and microphone.

The Run Loop

Emitting frames, batches, and frame rates.