Skip to main content
Nothing on this page is enforced. A ReactorApp with the weights loaded in load() and the inference written straight into generate() is a complete model, and for a small one that is the right shape. This page is about what to do when the model grows: the pattern the runtime’s own examples follow, and the reasons behind it. The idea is a split into two halves. The model is a black box: give it an input, it produces a result. The application is everything around it that a client can see or touch.
An Application box holding state and commands, tracks and messages, process_input() and process_output(). Inside it sits a Model box with load(), generate(), and reset(), described as owning the weights, cache, and its own step count. Client input enters the application, an input goes into the model, a result comes back, and the output leaves to the clients.

The application owns everything a client can observe. The model owns the weights and produces a result from an input.

Kept apart, the model half can be tested on its own, with no runtime and no client, by constructing it and calling generate() in a loop. The application half can be read without opening the model, since everything a client can observe is in one place. And because the two share nothing but two dataclasses, either can change without touching the other. The examples are the runtime’s Waypoint: waypoint_model.py is the model half, waypoint.py is the application half.

The model half

A plain Python class with three methods. It imports nothing from reactor_runtime, knows nothing about clients, tracks, or commands, and runs in a notebook with Reactor uninstalled.
waypoint_model.py
The model keeps its own state, the world and the step count, and nobody else writes it. It never invents input: with no seed to start from, it raises rather than making one up. And it never resets itself: when it cannot continue, it raises, and the application decides what to do about it.

The application half

The ReactorApp constructs the model in load(), holds it under an attribute, and its generate() is one line that forwards to it. Everything else on the class is about the client.
waypoint.py

Where the halves meet

The two halves talk through two dataclasses you define: the input process_input() builds and the result generate() returns. The Step Loop covers how they travel.
An Application box with process_input() and process_output() on the left, a Model box with generate() on the right. A WaypointInput crosses from process_input() into the model, and a WaypointResult crosses from the model back into process_output().

The only things that cross between the halves are the two dataclasses.

Going in, everything the model needs to know rides in the input: a control, a prompt, a seed frame. The application never writes a model attribute. When a handler needs the model to change, it calls a method the model wrote, such as reset(), and since handlers run between steps that call is safe. Coming back, everything the application needs to know rides in the result. Waypoint reads result.seed_id to know which seed the world holds and result.index to tag frames, and never touches self.engine.index. Design the result as the model’s public face.

Refusing is the application’s, failing is the model’s

process_input() refuses a step with ApplicationError for a fact about the client: paused, no prompt yet, waiting for frames. The model is not called and the loop asks again shortly. generate() fails a step by raising the model’s own exception for a fact about the model: it cannot continue from the state it holds. The runtime delivers it to process_output() as outcome.error, and the application decides whether to recover or let the session end. Never raise ApplicationError from the model half. It does not import the runtime, and whether to refuse is not its call.

Next

The Step Loop

How the input and the result travel between the three calls.

Session Recording

Record sessions and let clients capture clips.