> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reactor.inc/llms.txt
> Use this file to discover all available pages before exploring further.

# Model Anatomy

> Understand every piece of a Reactor model.

A model is a `ReactorModel` subclass. You declare the media it sends and receives, load your weights
once, and write the loop that produces frames. The runtime does the rest.

## The full model

Here is a complete model:

```python model.py theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
from pathlib import Path

from reactor_runtime import (
    ClientInfo, InputField, ModelMessage, Output, ReactorModel,
    Video, connected, event, session_started,
)

DEFAULT_PROMPT = "a sunny meadow"


class MyOutput(Output):
    main_video: Video


class PromptChanged(ModelMessage):
    """The scene the model is now rendering."""

    prompt: str


class MyModel(ReactorModel):
    fps = 24

    def load(self, config_path: Path | None) -> None:
        self.pipe = load_checkpoint()

    @session_started
    async def on_session_start(self) -> None:
        self.prompt = DEFAULT_PROMPT
        self._step = 0

    @connected
    async def on_connect(self, client: ClientInfo) -> None:
        await client.send(PromptChanged(prompt=self.prompt))

    @event(name="set_prompt", description="Scene the model renders")
    async def set_prompt(
        self, prompt: str = InputField(default=DEFAULT_PROMPT, max_length=500)
    ) -> PromptChanged:
        self.prompt = prompt
        return PromptChanged(prompt=self.prompt)

    async def run(self) -> None:
        while True:
            await self.connected.wait()
            while self.connected.is_set():
                frame = self.pipe.forward(prompt=self.prompt, step=self._step)
                await self.emit(MyOutput(main_video=frame))
                self._step += 1
```

That is the whole spine: media out, state that resets per session, a greeting for each arriving
client, one command with a typed reply, and the loop that produces frames. Let's break it down.

## Output tracks

```python theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
class MyOutput(Output):
    main_video: Video
```

This declares what media the model sends to clients. Each field is a **track**, a named media
channel, and the annotation is its kind. `Video` carries frames; `Audio` carries samples. Defining
the class registers the tracks, and the model sends media by emitting instances of it.

A model can send several tracks at once:

```python theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
class MyOutput(Output):
    main_video: Video
    main_audio: Audio
```

The field names (`main_video`, `main_audio`) are the track identifiers clients subscribe to, so pick
names your frontend will read well.

To receive media from the client, declare an `Input` the same way and annotate it on the model. See
[Media Input](/deploy/development/reactor-model/media-input).

## The model class

A `ReactorModel` subclass holds four kinds of member, and the rest of this page takes them in turn:

```python theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
class MyModel(ReactorModel):
    def load(self, config_path): ...            # once, at startup

    @session_started
    async def on_session_start(self): ...       # lifecycle hooks

    @connected
    async def on_connect(self, client): ...

    @event(name="set_prompt")
    async def set_prompt(self, prompt): ...     # commands clients send

    async def run(self): ...                    # the generation loop
```

The runtime reads the class to build the model's schema, which is what generates typed SDKs and
docs for your clients. Everything a client can see comes from what you declare here.

## Loading

```python theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
def load(self, config_path: Path | None) -> None:
    self.pipe = load_checkpoint()
```

Called once at startup, before any client can connect. This is where you load checkpoints, allocate
GPU memory, and warm up your pipeline.

`config_path` is the path to the file named by `runtime.config` in `reactor.yaml`, or `None` when
none is configured. The runtime hands you the path and stays out of the way — read it however you
like:

```python theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
import yaml


def load(self, config_path: Path | None) -> None:
    config = yaml.safe_load(config_path.read_text()) if config_path else {}
    self.steps = config.get("steps", 4)
    self.pipe = load_checkpoint(steps=self.steps)
```

Checkpoints are not baked into the image. Resolve them with `get_weights_path()` so the same code
runs locally and in production; see [Weights](/deploy/development/weights).

`load()` runs once for the life of the process, and one process serves one session after another.
Anything that should start fresh for each session — a step counter, a prompt, a cache of what this
audience has seen — belongs in `@session_started` instead, which is why the example splits them.

## The run loop

```python theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
async def run(self) -> None:
    while True:
        await self.connected.wait()
        while self.connected.is_set():
            frame = self.pipe.forward(prompt=self.prompt, step=self._step)
            await self.emit(MyOutput(main_video=frame))
            self._step += 1
```

This is the heart of the model — an async method the runtime starts after `load()` and keeps running
for the model's lifetime:

1. `await self.connected.wait()` blocks until a client connects, so an idle model burns no GPU.
2. The inner loop runs your forward pass and calls `emit()` to hand each result to the transport.
3. When the last client leaves, `self.connected` clears, the inner loop exits, and the outer loop
   parks until someone else arrives.

[The Run Loop](/deploy/development/reactor-model/run-loop) covers emitting, batches, and frame rates in
detail.

## Commands

```python theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
@event(name="set_prompt", description="Scene the model renders")
async def set_prompt(
    self, prompt: str = InputField(default=DEFAULT_PROMPT, max_length=500)
) -> PromptChanged:
    self.prompt = prompt
    return PromptChanged(prompt=self.prompt)
```

Each `@event` declares one command a client can send. The handler's parameters define the payload,
and `InputField` attaches the constraints the runtime enforces before your handler runs — a prompt
over 500 characters is rejected, not truncated.

Returning a `ModelMessage` makes it that command's correlated reply, so a client awaiting
`set_prompt` resolves with the state that actually took effect rather than assuming its own value
was applied. Return `None` when there is nothing to say back, and raise `CommandError` when the
command cannot be honoured.

See [Events & Messages](/deploy/development/reactor-model/events-and-messages).

## Lifecycle hooks

```python theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
@session_started
async def on_session_start(self) -> None:
    self.prompt = DEFAULT_PROMPT
    self._step = 0


@connected
async def on_connect(self, client: ClientInfo) -> None:
    await client.send(PromptChanged(prompt=self.prompt))
```

`@session_started` and `@session_ended` bracket the session as a whole and fire once each, however
many clients come and go inside it. `@connected` and `@disconnected` fire once per client.

That difference decides where a piece of setup belongs. Resetting the prompt and the step counter is
session work: it happens once, before anyone is watching, and a second viewer joining leaves the
scene as the first one left it. Greeting an arrival with the current prompt is per-client work,
because every client needs it on the way in.

Adding a `client: ClientInfo` parameter gets you a handle to the client that triggered the hook, and
`client.send()` reaches that one client rather than broadcasting. That is what lets a viewer who
joins halfway through render the right UI immediately. The runtime injects the handle, so the schema
describes only the parameters a client actually sends.

See [Sessions & Clients](/deploy/development/reactor-model/lifecycle).

## The manifest

```yaml reactor.yaml theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
model:
  name: my-model
  version: v0.0.1

runtime:
  import: model:MyModel
  config: config.yaml
```

* `model.name`: the model's identifier, used for registration and routing.
* `model.version`: the release tag, bumped on every shipped change.
* `runtime.import`: the Python import path to your class, in `module:ClassName` form.
  `model:MyModel` means the class `MyModel` in `model.py`.
* `runtime.config`: the file whose path is handed to `load()`. Omit it and `load()` receives `None`.

## Next

<CardGroup cols={2}>
  <Card title="The Run Loop" icon="play" href="/deploy/development/reactor-model/run-loop">
    Emitting frames, batches, and frame rates.
  </Card>

  <Card title="Sessions & Clients" icon="users" href="/deploy/development/reactor-model/lifecycle">
    Session hooks, multiple clients, and what resets when.
  </Card>
</CardGroup>
