> ## 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.

> ## Agent Instructions
> To build and serve your own model, start at /deploy/development/quickstart and /deploy/development/overview. Deploying is the default path: reactor init scaffolds a workspace, reactor auth login authenticates, and reactor model deploy registers the model, publishes the release with the weights/ folder, and activates it on Reactor's GPUs, in one command from that workspace. Docker must be running, because the publish step builds the image locally. Bump model.version in reactor.yaml before redeploying a change, because a release that already has an image is reactivated as it is. Deployment access is granted per account, so contact team@reactor.inc if a deploy is refused. Every key in reactor.yaml is documented at /deploy/platform/reactor-yaml. Model code imports reactor_runtime; Python client code imports reactor_sdk. The runtime overview explains the model interface. Running the model on your own machine with reactor run is optional and needs a GPU you attach with --gpus; /deploy/development/local-testing covers that loop and pairs a complete brightness model with a Python client test in a separate brightness-test workspace.
> Reactor hosts multiple models, each with its own connect slug (modelName) and command/event schema. The catalog of every model — slug, typed SDK package, and links to its schema — is at /model-api-reference/overview. Some models expose one slug per experience (e.g. HappyOyster); always take the slug from the model's own pages, never guess it.
> Fastest path to a working app: `npx create-reactor-app my-app --model=<slug>` scaffolds a complete app with secure auth wired up. Typed TypeScript SDKs are published as @reactor-models/<model>; Python uses the base reactor-sdk package.
> Auth: exchange an API key (rk_...) for a JWT via POST https://api.reactor.inc/tokens from your server. Never put the API key in client-side code.
> Append .md to any docs URL for clean Markdown. Search these docs via the MCP server at https://docs.reactor.inc/mcp.

# Events & Messages

> Define commands the client can call, reply to them, and send messages from the model to the client.

Besides video and audio, a model and its clients talk to each other with structured data. A client
sends a **command** to the model. The model answers, and it can also send a **message** to the
client at any time: a status, a result, an event the client should react to.

Both directions are declared in your class. `@event` marks a method as a command handler, and a
`ModelMessage` subclass declares a message. The runtime turns those declarations into the model's
schema, which is what typed client SDKs are generated from. The examples on this page come from
the runtime's Waypoint example, a world model that starts from an uploaded image.

## Defining a command

The state gives you one `set_<field>` command per field, for values a client sets and the model
keeps reading. For anything else, write a handler with `@event`. The method's parameters are the
data the client sends:

```python waypoint.py theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
from reactor_runtime import InputField, UploadedFile, event


@event(
    name="set_image",
    description="Upload the seed frame. The next step starts a new world from it.",
)
async def set_image(
    self, image: UploadedFile = InputField(description="The seed frame.")
) -> None:
    seed = await asyncio.to_thread(_fit, image.data)
    self.state._seed = seed
    self.state._seed_id += 1
```

The client calls it as `set_image` with an `image` parameter. The runtime validates the payload
against the signature before your method runs, and it runs between steps, so the state is never
changed while `generate()` is reading it. A handler can be `async def` or a plain `def`.

A command does not need parameters. This one restarts the world from the image the model already
has:

```python waypoint.py theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
@event(name="reset", description="Restart the world from the current seed frame.")
def reset(self) -> None:
    self.engine.reset()
    self.state._applied_seed_id = None
    self.output.flush()
```

### Constraining a parameter

`InputField` on a parameter carries the same rules as on a state field, and the runtime checks them
before your handler runs:

```python theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
strength: float = InputField(default=0.5, ge=0.0, le=1.0)
prompt: str = InputField(default="hello", min_length=1, max_length=500)
style: str = InputField(default="none", choices=["none", "oil_paint", "sketch"])
```

A value outside the rules is refused with `invalid_command` and your handler is not called. The
constraints also travel into the schema, so a client sees the accepted range before it sends
anything.

## Replying to a command

A client that sends a command usually wants to know what happened. Return a `ModelMessage` from
the handler and the runtime delivers it to that client as the reply to that exact command. A
client awaiting the call receives it as the result:

```python waypoint.py theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
class WaypointStatus(ModelMessage):
    has_image: bool = MessageField(description="Whether a seed frame has been accepted.")
    paused: bool = MessageField(description="Whether generation is held.")
    step_index: int = MessageField(
        description="The last completed step within the current world, or -1 before the first."
    )


@event(name="reset", description="Restart the world from the current seed frame.")
def reset(self) -> WaypointStatus:
    self.engine.reset()
    self.state._applied_seed_id = None
    self.output.flush()
    return WaypointStatus.of(self.state, -1)
```

<CodeGroup>
  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
  status = await reactor.send_command("reset", {})
  # {"type": "waypoint_status", "data": {"has_image": true, "paused": false, "step_index": -1}}
  ```

  ```typescript JavaScript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
  const status = await reactor.sendCommand("reset", {});
  ```
</CodeGroup>

Return `None` when there is nothing to say. Reply when the client should confirm what took effect:
a clamped value, a resolved default, the state after a change.

The return annotation has to be one `ModelMessage` subclass or `None`. A union such as
`WaypointStatus | None` is rejected when the class is declared, because the schema publishes one
reply shape per command.

## Reporting a failure

When a command cannot be carried out, raise `CommandError` with a code and a message. Both reach
the client as the reply to that command, so an awaiting caller gets an error instead of waiting
forever:

```python waypoint.py theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
from reactor_runtime import CommandError


@event(name="set_image", description="Upload the seed frame.")
async def set_image(
    self, image: UploadedFile = InputField(description="The seed frame.")
) -> WaypointStatus:
    if not image.mime_type.startswith("image/"):
        raise CommandError("unsupported_media", f"{image.name} is not an image.")
    ...
```

Write the message for the client, not for a log. Any other exception is treated as a bug: the
runtime logs it with its traceback and answers the client with a generic `internal_error`, so
paths, queries, or credentials in the exception text never leave the container.

The runtime uses the same channel for its own refusals: `invalid_command` when a payload fails
validation, and `unresolved_upload` when a command references a file that cannot be fetched.

## Sending messages

A `ModelMessage` subclass declares a typed message. Send one with `self.send()` from anywhere in
your class, and every connected client receives it:

```python waypoint.py theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
class WaypointStatus(ModelMessage):
    has_image: bool = MessageField(description="Whether a seed frame has been accepted.")
    paused: bool = MessageField(description="Whether generation is held.")
    step_index: int = MessageField(description="The last completed step, or -1 before the first.")


async def process_output(self, outcome: StepOutcome) -> WaypointOutput | None:
    ...
    if result.index % 50 == 0:
        await self.send(WaypointStatus.of(self.state, result.index))
    return WaypointOutput(main_video=TrackPayload(result.frames, metadata=metadata))
```

The client receives `{"type": "waypoint_status", "data": {...}}`. The wire `type` is the
snake\_case form of the class name. `MessageField` is optional; it attaches a description or a
default that shows up in the schema, and the class docstring documents the message itself. A
message with no fields is valid and is the right shape for a pure signal.

A message sent from `process_output()` goes on the wire before that step's frames, so a client that
reads the status and then sees the frame gets them in that order.

### Messages to one client

`self.send()` goes to everyone. To reach the client a handler or hook is running for, add a
`client: ClientInfo` parameter. The runtime fills it in, and `client.send()` delivers to that
client alone:

```python waypoint.py theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
@connected
async def on_connect(self, client: ClientInfo) -> None:
    await client.send(WaypointStatus.of(self.state, self.last_index))
```

This is how a client that joins a running session gets a snapshot without every other client
receiving it too. [Sessions & Clients](/deploy/development/reactor-app/sessions-and-clients) covers the client
handle and serving several clients at once.

## File uploads

To receive a file with a command, annotate a parameter as `UploadedFile`. The runtime fetches the
upload and hands your handler the bytes:

```python theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
@event(name="set_reference_image", description="Image the style is drawn from")
async def set_reference_image(
    self,
    reference_image: UploadedFile,
    strength: float = InputField(default=0.5, ge=0.0, le=1.0),
) -> None:
    if not reference_image.mime_type.startswith("image/"):
        raise CommandError("unsupported_media", "Expected an image.")
    self.state._reference = decode(reference_image.data)
```

`UploadedFile` carries `name`, `mime_type`, `data` (the raw bytes), and `size`. Check `mime_type`
before decoding, because a client can upload anything. A parameter can also be a
`list[UploadedFile]`, and an upload nested inside a `dict` or a dataclass field resolves the same
way.

A client can also upload a file on its own, outside any command. Declare a `@file_uploaded` hook
to receive those:

```python theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
from reactor_runtime import ClientInfo, UploadedFile, file_uploaded


@file_uploaded
async def on_file(self, uploaded_file: UploadedFile, client: ClientInfo) -> None:
    if uploaded_file.mime_type.startswith("image/"):
        self.state._reference = decode(uploaded_file.data)
```

The handler takes one parameter named `uploaded_file`, plus the optional `client`. Prefer putting
a file on the command it belongs to, so the schema tells the client what the file is for. Use
`@file_uploaded` for a drop zone or a client that uploads first and decides later.

## Next

<CardGroup cols={2}>
  <Card title="Sessions & Clients" icon="users" href="/deploy/development/reactor-app/sessions-and-clients">
    Session and connection hooks, the client handle, and serving several clients.
  </Card>

  <Card title="Managing State" icon="sliders-horizontal" href="/deploy/development/reactor-app/state">
    The state object and the commands it generates.
  </Card>
</CardGroup>
