generate() again and
again, and each call is one step that produces the next piece: a frame, a chunk of frames, a
stretch of audio. A step takes the current state, calls generate(), and streams what comes back.
Two optional hooks sit around generate(), one to prepare what goes in and one to shape what comes
out. This page builds the loop up one piece at a time.
The examples continue the SANA-WM world model from
Managing State: a prompt, camera controls, and a chunk of
frames per step.
The default step
Without the hooks, a step is one call. The runtime handsgenerate() the state and streams the
Output it returns:
Without the hooks: the state goes into generate(), and what it returns goes out to the clients.
sana_wm.py
run(). A model that has to
drive itself, to emit several times per step or block on an input before it can produce anything,
can replace it; Controlling the loop shows how.
Handlers run between steps
Every@event handler and lifecycle hook waits for the step in flight to finish before it runs.
A command that arrives while generate() is busy is not lost and is not applied halfway through;
it runs in the gap before the next step.
A command that arrives mid-step runs once the step is over. The next step sees what it changed.
generate().
Steps run only while a session is live and at least one client is connected. When the last client
leaves, the loop stops at the next step boundary and waits.
Preparing the input
process_input() runs before generate(). Override it when the state is not quite what the model
wants, or when there are moments the model should not run at all.
process_input() takes the state and decides what generate() receives.
self.state, and the media tracks if the model declares any, and
returns whatever generate() should receive. That return type is yours. A small dataclass is the
usual choice, because it gives generate() a signature that says exactly what one step needs:
sana_wm.py
ApplicationError skips the step. The model is not called, the reason is logged once
rather than on every turn, and the runtime asks again a few milliseconds later. This is how a
paused model costs nothing, and how a model that needs an upload waits for it without special
cases inside generate().
Shaping the output
process_output() runs after generate(). Override it when the model’s result is not already an
Output, when a step should also send a message, or when the model can raise an error you want to
recover from.
process_output() takes the result and decides what the clients receive.
StepOutcome, which is the runtime’s envelope around what generate() did. Inside
it, outcome.result is exactly the object generate() returned, in your type. It returns the
Output to stream, or None to stream nothing this step:
sana_wm.py
StepOutcome carries result and error, exactly one of which is set, plus elapsed, the
seconds the step took.
StepOutcome
The three fields, and
to_output(), which is what the default process_output() calls.When generate() raises
An exception out ofgenerate() does not end anything by itself. It arrives in
process_output() as outcome.error, and you decide. If it is an error the model is known to
raise, recover: put the model back into a good state, cut playout if a stale frame must not
follow, tell the client, and return None. The loop carries on with the next step.
process_output() ends the session with
an error the client can see, which is better than streaming a broken world in silence.
Controlling the loop
Everything on this page so far is the runtime’s defaultrun(). Once load() has finished, the
runtime calls run() exactly once, and that call is the model’s whole life: as long as it is
running, the model is up. The default implementation is the loop you have seen, waiting for a
client, taking a step, streaming the result, and asking again.
You can replace it. Override run() and you own the loop: when to wait, when to call the model,
when to emit. The three hooks are not called anymore, and nothing reads the state for you. Commands
and lifecycle hooks still run, and so do self.connected, self.send(), and the tracks.
self.connected is set while at least one client is connected and cleared when the last one
leaves, so the inner loop ends on its own. emit() puts an Output on the tracks and waits while
the frames already handed over are still playing, which is what paces a loop like this. If run()
returns or raises, the runtime ends the session with an error, so a loop that should live for the
whole session is wrapped in while True.
Write your own run() when the default does not fit: a model that emits several times per step, or
one that has to block on an input before it can produce anything. A model whose whole job lives in
@event handlers can simply park:
run(), keep your values in your own attributes and write @event handlers
for them, as the example above does. Declaring state: still generates the set_<field> commands,
but nothing reads the state on your behalf, and nothing bounds when a write lands relative to your
loop’s reads.
Next
Application and Model
How far to take the split between the code that answers clients and the code that runs weights.
Video & Audio Tracks
Read the client’s camera and microphone in process_input().