Skip to main content
Releases, breaking changes, and notable improvements to the Python runtime partners use to build and ship models on Reactor. Subscribe to updates in Discord.
reactor-runtime 3.2.0 · Opt-in moderation · Audio on the wall clock

Breaking

  • Marking a field for moderation is opt-in. InputField(moderate=True) marks a field whose text a deployment should have moderated, and the default is now False. Earlier releases marked every field unless it opted out, so a model that wants a field moderated has to say so. Mark only the fields a client writes free text into — a mark on an enum, a number, or a file changes nothing, since only free text is eligible — and only on a command that arrives occasionally, like a prompt or a script. A check runs one command at a time and admits anything still waiting after two seconds, so marking a per-frame command neither gets it moderated nor leaves room for the prompts that matter. The runtime moderates nothing itself: the mark is a preference a deployment reads off the published schema, where every field now states it either way.

New

  • A model can send more than one audio track. Outbound audio is keyed by track name, the way video always was, so two Audio fields on one Output reach the client as two tracks. Declaring two previously concatenated them into one buffer that played out through whichever track negotiated last, at twice the rate, with nothing rejecting or logging it.
  • Clients are told why a session ended. A session the platform ends can carry a human-readable reason, delivered to every connected client before its connection closes, in place of a bare disconnect. Nothing in the model changes.

Fixed

  • Audio no longer drifts against video. An outbound audio track’s clock advances only with the samples pushed onto it, so a tick the model did not fill was time the stream never accounted for — and because the packets either side of it stayed contiguous, the client read the whole stream as arriving late, grew its jitter buffer, and time-stretched audio to refill it. That stretching was the artefact and the swinging buffer was the drift. The runtime now pushes a frame on every tick, sending silence for one the model did not fill, and warns with the track’s name when it has had to manufacture a meaningful share of the last second.
  • Pausing an audio track stops the audio. A paused track is skipped before anything is read from it, so a client that pauses stops receiving packets instead of roughly fifty a second of digital silence — and the pause no longer counts against the under-production warning, which had been blaming the model for audio the client declined.
reactor-runtime 3.1.2 · Per-frame metadata
Rolls up 3.1.0 through 3.1.2. The two patch releases pin the transport dependency and change nothing you write.

New

  • Send metadata with a frame. Wrap a track’s payload in TrackPayload to tag what it emits: TrackPayload(frame, metadata={"seed": seed}). A mapping travels as JSON and bytes travel as they are, and a batch carries either one value for the whole batch or one per frame. A bare array still works everywhere it did.
  • Read the metadata a client attached. InputFrame.metadata holds the bytes the sender sent with that frame, or None when it sent none. Decoding them is the model’s business — the transport treats them as opaque. See Media Input.
reactor-runtime 3.0.2 · Output backpressure · The playout handle

Breaking

  • emit() waits for downstream room. A model that generates faster than its playout rate is throttled to that rate rather than piling up latency, so a hand-rolled rate limiter is now redundant and should come out. The wait runs off the model loop, so commands and lifecycle hooks keep dispatching while it holds. Pass drop=True on a producer that would rather skip a frame than wait.
  • output is the playout handle, not an annotation. Remove output: MyOutput from your model class — outbound tracks register when the Output subclass is defined, and the name now belongs to self.output. A class that re-annotates it contradicts the real attribute.
  • buffer_size must be positive when declared. Zero or less fails at startup instead of silently falling back to the runtime default.

New

  • self.output controls playout. self.output.fps = n re-paces frames that are already queued rather than waiting for the next emit, so a speed command takes effect immediately. self.output.flush() drops what is queued and cuts the client to black, which is what a scene reset wants so none of the old content plays afterwards. Both fan out to every connection, including ones that join later. See The Run Loop.
  • buffer_size bounds buffered latency. It declares how many frames may sit between the model and each client, and is never applied below one emitted chunk, so a batching model always fits a whole batch.
  • A quiet wire on underrun. When no frame is ready for a tick nothing is sent and the client holds what it has, instead of the stream spending bandwidth repeating it. One black frame marks each boundary — a connection opening, or a flush.
reactor-runtime 3.0.1 · Command failures · Recording in process

Breaking

  • An @event return annotation must name one message type, or nothing. A handler annotates a single ModelMessage subclass for a typed reply, or None for a bodyless acknowledgement. A union — Reply | None included — is now rejected when the class is declared, because the schema publishes one response shape and a client generated from it would expect no body and receive one. Annotate the message and raise CommandError for the failure case. See Events & Messages.

New

  • Command handlers can fail out loud. raise CommandError(code, message) answers the calling client with a failure it can branch on, correlated with its command, so an awaiting caller rejects with a reason instead of hanging. Any other exception answers with internal_error and keeps its detail in the log.
  • UploadedFile.size reports the byte length of an upload.
  • Recording encodes in process. No external encoder binary is involved. A finished recording stays fetchable for five minutes after its session ends and is then deleted; the clip endpoints answer 410 Gone past that. recording_dir (or REACTOR_RECORDINGS_DIR) chooses where chunks are written. See Session Recording.
  • Render a schema without serving one. python -m reactor_runtime.schema prints the OpenAPI contract of the model in a directory, so a build step can publish it without booting the runtime. The schema is titled with the name the model publishes.
  • GET /metrics serves the process’s metrics in Prometheus text format.
reactor-runtime 3.0.0 · New authoring surface
A rebuilt runtime with a smaller, sharper authoring surface. Every change below is source-level and mechanical; the shape of a model — declare tracks, load(), run(), emit — is unchanged.

Breaking

  • Import from reactor_runtime directly. Everything author-facing is exported from the top-level package. Replace from reactor_runtime.interface import ... with from reactor_runtime import ....
  • load() receives a path, not a dict. The signature is now load(self, config_path: Path | None), and the runtime no longer parses the file. Read it however you like: yaml.safe_load(config_path.read_text()) if config_path else {}.
  • output_buffer is now self.output. There is no single buffer any more — each connection paces its own playback — so the handle is named for what it does. output_buffer.flush() becomes self.output.flush() and output_buffer.set_fps(n) becomes self.output.fps = n, both fanning out to every connection. buffer_size and emit(drop=...) keep their meaning. See The Run Loop.
  • Drop the output: MyOutput class annotation. Outbound tracks register when the Output subclass is defined, so the annotation was never read — and the name now belongs to the playout handle, so leaving it in contradicts the real attribute. Delete the line; nothing replaces it.
  • Output and Input subclasses drop @dataclass. Declare the track annotations and nothing else. Output.__init__ now validates that you supplied exactly the declared tracks, so a missing track fails at the emit() call rather than silently streaming nothing.
  • @event(dedupe=True) is removed. Every command is delivered. A handler that must collapse a burst should track the latest value itself.
  • runtime.weights_path is no longer read by the runtime. get_weights_path() resolves $REACTOR_WEIGHTS_PATH, falling back to ~/.cache/reactor_registry. reactor run still reads runtime.weights_path to decide what to mount and sets the variable for you, so most workspaces need no change. See Weights.
  • serve has no subcommands. The entry point is python -m reactor_runtime.serve, run from the directory holding reactor.yaml. Fetch a model’s schema from GET /schema on the running server instead of serve schema.
  • Removed from the public API: get_profiler(), ReactorConfig, ReactorCore, and the Event / Connected / Disconnected classes. Use the @event, @connected, and @disconnected decorators.
  • Python 3.12 or newer is required.
  • No media libraries on the host. The runtime carries its own WebRTC stack as a wheel, so a model image needs nothing from the system package manager and a workspace Dockerfile can start from a plain Python base. PREFERRED_TRANSPORT and the GST_-prefixed variables are gone with it.

New

  • Session lifecycle hooks. @session_started and @session_ended bracket the session as a whole and fire once each, so a model can tell a new viewer joining apart from the session itself beginning. Note that a session end tears its connections down wholesale without firing the per-client @disconnected hooks. See Sessions & Clients.
  • Command replies. An @event handler that returns a ModelMessage sends it as that command’s correlated reply, so a client awaiting the command resolves with the state that actually took effect — useful when a value is clamped or a default is resolved server-side.
  • Read order per track. Reads take a mode: ReadMode.LATEST returns the newest frames and clears the backlog, ReadMode.FIFO consumes in arrival order. Video wants the former, audio the latter. See Media Input.
  • Rate from measured throughput. Passing compute_time to emit() plays the chunk back at the rate you actually produced it, so a model that warms up or slows under load stays in sync without touching fps.
  • Defaults validated at import. An InputField default that violates its own constraints now fails when the class is defined rather than on the first request, and mutable defaults are rejected outright.
reactor-runtime 2.9.4 · High-res codecs · Network resilience
Rolls up the patch releases since 2.8.0.

New

  • Clients are told when the network is struggling. The runtime periodically sends a mediaStats message on the data channel carrying an aggregate video quality score (0–10), so frontends can show a “connection is unstable” notice when reduced stream quality comes from network conditions rather than the model. Already surfaced by the JS SDK.

Fixed

  • H.264 and H.265 now stream above 720p. The senders pinned the level offered by the client onto the encoder, and browser offers advertise a ~720p level — so any higher-resolution model produced zero video on those codecs while the session otherwise looked healthy. The encoder now derives a level that fits the actual resolution (only the negotiated profile is enforced), and 2K+ output flows on both codecs, verified against real Chrome (H.264) and Safari (H.265) clients.
  • Video survives low-MTU networks. RTP packets are now capped at 1200 bytes, matching mainstream WebRTC stacks. Sessions on VPNs, WireGuard/Tailscale tunnels, PPPoE ISPs, and similar paths previously connected fine (data channel, audio) but silently black-holed every full-size video packet. Overridable per deployment with GST_RTP_PAYLOAD_MTU.
  • Every frame width works over WebRTC. Widths that are not a multiple of 4 no longer come in as a grayscale, sheared smear or get dropped by the encoder on the way out — the transport now honours GStreamer’s row padding in both directions. Widths that already worked keep the exact same fast path.
  • Native macOS runs. The GStreamer send pipeline no longer fails to build on Homebrew PyGObject, so running the runtime directly on a Mac (outside reactor run) works for local development.
reactor-runtime 2.8.0 · Multi-client sessions

Breaking

  • The aiortc WebRTC fallback has been removed. Running the runtime directly on a host now requires a working GStreamer installation. Stay inside the container with reactor run, which bundles GStreamer and every other dependency automatically.

New

  • Multiple clients can share one session. @connected and @disconnected now run once per client as each one joins and leaves, and self.connected stays set while at least one client is connected. Single-client models keep working unchanged: self.send() still reaches the one client connected.
  • Per-client messaging with ClientInfo. Any handler (@event, @connected, @disconnected, @file_uploaded) can accept a client: ClientInfo parameter for the client that triggered it. Call client.send() to reply to that single client or self.send() to broadcast to everyone, and store the handle to message a client later. Runtime responses (requestClip, requestRecording, schema) now address only the requesting client instead of broadcasting. See Events and Messages.
reactor-runtime 2.7.0 · Session recording

New

  • Session recording, configured from reactor.yaml. Models can now record every session continuously and let clients request snap clips or full recordings of the live stream. Flip recording.enabled: true in reactor.yaml and the runtime hooks the same buffer that feeds the wire, encodes fMP4 chunks in the background, and exposes a requestClip / requestRecording API to clients. Already wired in the JS SDK and the Demo Frontend’s Capture panel. See the new Session Recording page for the full configuration reference, encoder knobs (chunk_seconds, crf, target_width, …), and multi-track disambiguation.
reactor-runtime 2.6.0 · Host CLI · Docker

Breaking

  • Host CLI is now the Go reactor binary. The runtime no longer publishes a console script. Replace pip install reactor-runtime + reactor-runtime init|run with the Go reactor CLI; the runtime itself ships only inside reactor-runtime-base.
  • Workspace runtime version is now pinned in the Dockerfile. reactor init substitutes a fully-qualified FROM reactortechnologies/reactor-runtime-base:<X.Y.Z-N> line. Existing workspaces using ARG RUNTIME_VERSION need a one-line edit; everything else keeps working unchanged.
  • reactor run no longer accepts build-time flags. Build-phase concerns (-f, --no-cache, --build-secret) moved to reactor build; run-phase concerns (--port, --gpus, --tty, -e, --env-file) stay on reactor run. Both share the same image tag, so reactor build && reactor run always boots the freshly built image.

New

  • Scaffolded workspaces double as plain Docker projects. reactor init emits ENTRYPOINT [..., "python", "-m", "reactor_runtime.serve"] and CMD ["run"], so docker build && docker run -p 8080:8080 . works without the reactor CLI installed. reactor run itself remains opinionated about the run subcommand.
reactor-runtime 2.5.0 · CLI · reactor.yaml · Weights

Breaking

  • CLI renamed: reactorreactor-runtime. The bundled console script is now reactor-runtime; update reactor run|init|schema call sites accordingly. When the Go reactor CLI is installed, reactor run keeps working by delegating to reactor-runtime run under the hood.

New

  • Modern nested shape for reactor.yaml. Identity and runtime entrypoint are now split under model: and runtime: sections. The legacy flat shape keeps working but logs a one-shot deprecation warning per process; new scaffolds emit the modern shape. See Model Anatomy.
  • get_weights_path() helper. New import from reactor_runtime returns a pathlib.Path to the resolved weights root, with resolution order $REACTOR_WEIGHTS_PATHruntime.weights_path~/.cache/reactor_registry. See Weights.
  • runtime.weights_path in reactor.yaml. Optional field for committing a workspace-relative weights root (the env var still wins, so production deployments override committed values).
  • reactor-runtime init improvements. Name argument is optional (scaffolds into the current empty folder when omitted), model name is substituted into reactor.yaml automatically, a Dockerfile is part of the scaffold (python:3.12-slim + GStreamer + uv), and requirements.txt ships reactor-runtime so the workspace .venv has the import target available immediately.

Improved

  • Multi-line ModelMessage docstrings render correctly. reactor-runtime schema and downstream SDK / docs generation now preserve the full docstring of each ModelMessage subclass. Wrapped one-sentence summaries are no longer truncated at the first newline, and undocumented @dataclass messages no longer leak their constructor signature into the description.