reactor-runtime 3.3.2__tar.gz → 3.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. reactor_runtime-3.5.0/PKG-INFO +180 -0
  2. reactor_runtime-3.5.0/README.md +154 -0
  3. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/pyproject.toml +5 -2
  4. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/pyproject.toml.orig +13 -4
  5. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/__init__.py +41 -9
  6. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/core/transport.py +7 -5
  7. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/interface/__init__.py +40 -7
  8. reactor_runtime-3.5.0/src/reactor_runtime/interface/app/__init__.py +14 -0
  9. {reactor_runtime-3.3.2/src/reactor_runtime/interface/pipeline → reactor_runtime-3.5.0/src/reactor_runtime/interface/app}/input_state.py +7 -6
  10. reactor_runtime-3.5.0/src/reactor_runtime/interface/app/outcome.py +73 -0
  11. reactor_runtime-3.5.0/src/reactor_runtime/interface/app/reactor_app.py +719 -0
  12. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/interface/events/__init__.py +2 -1
  13. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/interface/events/decorators.py +6 -1
  14. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/interface/events/errors.py +23 -1
  15. reactor_runtime-3.5.0/src/reactor_runtime/interface/internal/aliases.py +56 -0
  16. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/interface/internal/input_buffer.py +2 -2
  17. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/interface/internal/reactor_core.py +5 -5
  18. reactor_runtime-3.5.0/src/reactor_runtime/interface/model/__init__.py +42 -0
  19. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/interface/model/contract.py +1 -1
  20. reactor_runtime-3.5.0/src/reactor_runtime/interface/model/reactor_model.py +25 -0
  21. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/interface/pipeline/__init__.py +2 -2
  22. reactor_runtime-3.5.0/src/reactor_runtime/interface/pipeline/input_state.py +11 -0
  23. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/interface/pipeline/reactor_pipeline.py +24 -211
  24. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/interface/tracks/__init__.py +17 -5
  25. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/interface/tracks/descriptors.py +1 -1
  26. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/interface/tracks/input.py +17 -7
  27. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/recording/recorder.py +29 -16
  28. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/runner/runner.py +1 -1
  29. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/transport/webrtc/config.py +13 -0
  30. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/transport/webrtc/connection.py +5 -1
  31. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/transport/webrtc/peer.py +84 -12
  32. reactor_runtime-3.3.2/PKG-INFO +0 -140
  33. reactor_runtime-3.3.2/README.md +0 -114
  34. reactor_runtime-3.3.2/src/reactor_runtime/interface/model/__init__.py +0 -33
  35. reactor_runtime-3.3.2/src/reactor_runtime/interface/model/reactor_model.py +0 -294
  36. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/LICENSE +0 -0
  37. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/NOTICE +0 -0
  38. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/codes.py +0 -0
  39. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/core/__init__.py +0 -0
  40. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/core/fields.py +0 -0
  41. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/core/model.py +0 -0
  42. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/core/naming.py +0 -0
  43. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/core/service.py +0 -0
  44. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/core/session.py +0 -0
  45. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/core/typespec.py +0 -0
  46. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/core/values.py +0 -0
  47. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/event_stream.py +0 -0
  48. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/http/__init__.py +0 -0
  49. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/http/events.py +0 -0
  50. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/http/routes.py +0 -0
  51. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/http/server.py +0 -0
  52. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/http/spec.py +0 -0
  53. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/interface/client.py +0 -0
  54. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/interface/events/messages.py +0 -0
  55. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/interface/internal/__init__.py +0 -0
  56. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/interface/internal/bridge.py +0 -0
  57. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/interface/model/schema.py +0 -0
  58. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/interface/pipeline/idle.py +0 -0
  59. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/interface/tracks/output.py +0 -0
  60. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/log.py +0 -0
  61. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/manifest.py +0 -0
  62. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/message_gateway.py +0 -0
  63. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/metrics/__init__.py +0 -0
  64. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/metrics/command.py +0 -0
  65. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/metrics/model.py +0 -0
  66. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/metrics/registry.py +0 -0
  67. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/metrics/session.py +0 -0
  68. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/paths.py +0 -0
  69. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/protocol/__init__.py +0 -0
  70. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/protocol/base.py +0 -0
  71. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/protocol/common.py +0 -0
  72. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/protocol/v0/__init__.py +0 -0
  73. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/protocol/v0/codec.py +0 -0
  74. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/protocol/v1/__init__.py +0 -0
  75. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/protocol/v1/codec.py +0 -0
  76. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/py.typed +0 -0
  77. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/recording/__init__.py +0 -0
  78. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/recording/chunk_encoder.py +0 -0
  79. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/recording/markers.py +0 -0
  80. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/runner/__init__.py +0 -0
  81. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/runner/connection_manager.py +0 -0
  82. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/runner/offer_epochs.py +0 -0
  83. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/runner/state_machine.py +0 -0
  84. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/runner/upload_resolution.py +0 -0
  85. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/schema.py +0 -0
  86. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/serve.py +0 -0
  87. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/service.py +0 -0
  88. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/transport/__init__.py +0 -0
  89. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/transport/acceptor.py +0 -0
  90. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/transport/router.py +0 -0
  91. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/transport/webrtc/__init__.py +0 -0
  92. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/transport/webrtc/acceptor.py +0 -0
  93. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/transport/webrtc/frames.py +0 -0
  94. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/transport/webrtc/metrics.py +0 -0
  95. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/transport/webrtc/pacer.py +0 -0
  96. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/transport/webrtc/router.py +0 -0
  97. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/transport/webrtc/sdp.py +0 -0
  98. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/transport/webrtc/signaling.py +0 -0
  99. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/transport/webrtc/stats.py +0 -0
  100. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/transport/webrtc/version.py +0 -0
  101. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_runtime/upload_store.py +0 -0
  102. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_wire/v1/common_pb2.py +0 -0
  103. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_wire/v1/common_pb2.pyi +0 -0
  104. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_wire/v1/control_pb2.py +0 -0
  105. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_wire/v1/control_pb2.pyi +0 -0
  106. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_wire/v1/data_pb2.py +0 -0
  107. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_wire/v1/data_pb2.pyi +0 -0
  108. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_wire/v1/model_pb2.py +0 -0
  109. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_wire/v1/model_pb2.pyi +0 -0
  110. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_wire/v1/platform_pb2.py +0 -0
  111. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_wire/v1/platform_pb2.pyi +0 -0
  112. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_wire/v1/track_pb2.py +0 -0
  113. {reactor_runtime-3.3.2 → reactor_runtime-3.5.0}/src/reactor_wire/v1/track_pb2.pyi +0 -0
@@ -0,0 +1,180 @@
1
+ Metadata-Version: 2.4
2
+ Name: reactor-runtime
3
+ Version: 3.5.0
4
+ Summary: A Python framework for building real-time, interactive video models
5
+ Author: Reactor
6
+ Author-email: Reactor <team@reactor.inc>
7
+ License-Expression: Apache-2.0
8
+ License-File: LICENSE
9
+ License-File: NOTICE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Typing :: Typed
15
+ Requires-Dist: av>=18.0.0
16
+ Requires-Dist: fastapi>=0.139.0
17
+ Requires-Dist: numpy>=2.1
18
+ Requires-Dist: prometheus-client>=0.26.0
19
+ Requires-Dist: protobuf>=7.35.1
20
+ Requires-Dist: pyyaml>=6.0.3
21
+ Requires-Dist: reactor-webrtc==0.18.0
22
+ Requires-Dist: uvicorn>=0.49.0
23
+ Requires-Python: >=3.12
24
+ Project-URL: Source, https://github.com/reactor-team/reactor-runtime
25
+ Description-Content-Type: text/markdown
26
+
27
+ <div align="center">
28
+
29
+ <img src="assets/banner.png" alt="Reactor Runtime" width="100%" />
30
+
31
+ **Build real-time AI models in Python.**
32
+
33
+ [📖 Documentation](https://docs.reactor.inc/deploy/overview) · [🚀 Quickstart](https://docs.reactor.inc/deploy/development/overview) · [🌐 Reactor](https://reactor.inc)
34
+
35
+ </div>
36
+
37
+ ---
38
+
39
+ Reactor Runtime turns an inference pipeline into a real-time, interactive media and data stream. You write `load()` and `generate()`; the runtime drives them one step at a time and handles the session lifecycle, the WebRTC media transport, and the wire protocol that connects clients to your model. Viewers watch frames as they are generated and change what the model is doing mid-stream, with no restart and no re-queue.
40
+
41
+ ## Highlights
42
+
43
+ - 📡 **Real-time streaming.** Frames reach clients over WebRTC as your model produces them, not after a whole video is done. The runtime paces playout from the time each step took, so a model needs no rate limiter of its own.
44
+ - 🎮 **Live interaction.** Clients send commands mid-generation: change a prompt, move a camera, adjust a parameter. The next frame reflects it.
45
+ - 🔌 **No transport code.** You never import a WebRTC library, manage a WebSocket, or encode video. The runtime ships its own media engine as a wheel, so a plain Python container is all a model needs.
46
+ - ✅ **Typed, validated commands.** Declare the commands your model accepts with standard Python types and constraints. The runtime validates every payload before your handler runs and compiles the surface into an OpenAPI schema that drives typed client SDKs.
47
+ - 🔎 **Traceable logs.** `get_logger()` writes structured records — readable `key=value` in a terminal, JSON for a log pipeline. Every record a session writes carries that session's id automatically, so one filter recovers everything a single run logged.
48
+ - 📦 **One container, anywhere.** The `reactor` CLI scaffolds a workspace, builds a small image, and runs it locally. The same image deploys to [Reactor](https://reactor.inc)'s GPU cloud unchanged.
49
+
50
+ ## How it works
51
+
52
+ You ship one `ReactorApp` subclass: the application the runtime drives and the client talks to. Declare the media it sends and the state a client can set, load your weights once, and write what one step of generation does:
53
+
54
+ ```python
55
+ from pathlib import Path
56
+
57
+ from reactor_runtime import InputField, InputState, Output, ReactorApp, Video
58
+
59
+
60
+ class MyState(InputState):
61
+ prompt: str = InputField(default="a sunny meadow", moderate=True, description="Scene to render.")
62
+ paused: bool = InputField(default=False, description="Hold generation on the last frame.")
63
+
64
+
65
+ class MyOutput(Output):
66
+ main_video: Video
67
+
68
+
69
+ class MyModel(ReactorApp):
70
+ state: MyState
71
+
72
+ def load(self, config_path: Path | None) -> None:
73
+ self.pipe = load_my_pipeline()
74
+
75
+ def generate(self, input: MyState) -> MyOutput:
76
+ return MyOutput(main_video=self.pipe.forward(prompt=input.prompt))
77
+ ```
78
+
79
+ That is a complete application. The runtime calls `generate()` in a loop for as long as someone is watching and emits what it returns. Every public field on `MyState` is a command the client can send: here `set_prompt` and `set_paused`, validated from the fields, and the next step reads the new values.
80
+
81
+ A step is three calls, and `generate()` is the one you must write. `process_input()` runs before it, reading `self.state` and the media tracks, and decides whether a step can happen: return the input `generate()` gets, or raise `ApplicationError("reason")` to skip the step without touching the model. `process_output(outcome)` runs after it, with the result or the error, and returns the media to emit; send a message from there with `await self.send()` and it reaches the client before the step's frames. Both have defaults, so the model above writes neither.
82
+
83
+ ```python
84
+ from reactor_runtime import ApplicationError, MessageField, ModelMessage, StepOutcome
85
+
86
+
87
+ class FrameReady(ModelMessage):
88
+ prompt: str = MessageField(description="The prompt this frame was rendered from.")
89
+
90
+
91
+ class MyModel(ReactorApp):
92
+ state: MyState
93
+
94
+ async def process_input(self) -> MyState:
95
+ if self.state.paused:
96
+ raise ApplicationError("paused")
97
+ return self.state
98
+
99
+ def generate(self, input: MyState) -> MyOutput: ...
100
+
101
+ async def process_output(self, outcome: StepOutcome) -> Output | None:
102
+ if outcome.error is not None:
103
+ raise outcome.error
104
+ await self.send(FrameReady(prompt=self.state.prompt))
105
+ return outcome.to_output()
106
+ ```
107
+
108
+ `generate()` fails by raising, and the runtime hands the exception to `process_output()` as `outcome.error` rather than letting it escape. That is where you decide. Recover an error you expect from your model: reset it, send a message, return `None`, and the loop goes on to the next step. Re-raise anything else, as the example does: a raise out of `process_output()` is a crash of the model, not of the step. The runtime logs the traceback, stops dispatching commands, ends the session with an error the client sees, and does not restart the loop, which is what an uncaught exception in a hand-written `run()` does too. The default `process_output()` re-raises, so a failing model ends loudly instead of serving nothing in silence. A refusal from `process_input()` is not a failure and never reaches `process_output()`.
109
+
110
+ ```python
111
+ async def process_output(self, outcome: StepOutcome) -> Output | None:
112
+ if isinstance(outcome.error, RolloutExhausted): # an error the model is known to raise
113
+ self.engine.reset()
114
+ self.output.flush()
115
+ await self.send(WorldRestarted(reason="rollout window reached"))
116
+ return None # nothing to show; the next step starts over
117
+ if outcome.error is not None:
118
+ raise outcome.error # anything else is a bug: end the loop
119
+ return outcome.to_output()
120
+ ```
121
+
122
+ Command handlers and lifecycle hooks run between steps, never during one. The default `run()` is the loop that drives the three hooks. Override it to write your own loop against `emit()`, `send()`, `@event`, `self.connected`, and the tracks; `process_input()`, `generate()`, and `process_output()` are then not called. Do that for a loop that is not one step per emit, such as a renderer that emits several times per step or a model that must block on an input.
123
+
124
+ Scaffold, build, and run it with the CLI:
125
+
126
+ ```sh
127
+ reactor init my-model
128
+ cd my-model
129
+ reactor run
130
+ ```
131
+
132
+ `reactor run` builds a container with the runtime inside and serves WebRTC signaling on port 8080. Point a browser at it with the [JS SDK](https://docs.reactor.inc), or connect from the [Reactor Sandbox](https://reactor-sandbox.vercel.app/) and watch frames stream immediately.
133
+
134
+ Log from the same import, passing context as keyword arguments:
135
+
136
+ ```python
137
+ from reactor_runtime import get_logger
138
+
139
+ logger = get_logger(__name__)
140
+
141
+ logger.info("scene changed", prompt=self.prompt)
142
+ ```
143
+
144
+ Records render as `key=value` text by default, or as one JSON object per line under `REACTOR_LOG_FORMAT=json`. While a session is live, its id is stamped on every record, so tracing one run's logs never requires threading an id through your call sites. Every record also carries the lifecycle phase it was written in, at both granularities: `state`, the session state machine's word, and `runtime_state`, the coarse word the health endpoint serves — so the logs of one phase — loading weights, a live session, teardown — are filterable by whichever vocabulary you are reading off another surface. The stamp is applied where records are written rather than where they are made, so a plain `logging.getLogger(__name__)` and the libraries your model imports are covered too.
145
+
146
+ ## Install
147
+
148
+ Everything runs through the [`reactor` CLI](https://docs.reactor.inc/deploy/platform/installation). There is nothing to install on your host but the CLI and Docker; the runtime ships inside the image the CLI builds for your workspace.
149
+
150
+ ```sh
151
+ brew install reactor-team/tools/reactor-cli
152
+ ```
153
+
154
+ Not on macOS, or pinning a release in CI? See [Install the CLI](https://docs.reactor.inc/deploy/platform/installation).
155
+
156
+ ## Learn more
157
+
158
+ - [Quickstart](https://docs.reactor.inc/deploy/development/overview): from zero to a streaming model in 2 minutes
159
+ - [Model anatomy](https://docs.reactor.inc/deploy/development/reactor-app/model-anatomy): every member of a `ReactorApp`, line by line
160
+ - [The step loop](https://docs.reactor.inc/deploy/development/reactor-app/step-loop): `process_input()`, `generate()`, `process_output()`, and the frame rate
161
+ - [Starter example](./examples/starter/README.md): the model `reactor init` scaffolds: one class, `generate()` alone, the smallest complete `ReactorApp`
162
+ - [Echo example](./examples/echo/README.md): the client's webcam and microphone in, an effect applied, both sent back, in batches
163
+ - [Waypoint example](./examples/waypoint/README.md): a world model on a GPU, seeded from an upload and steered live
164
+
165
+ ## Development
166
+
167
+ This repository holds the runtime package itself: the authoring interface, the session runner, the media transport, and the wire protocol. To work on it, use [mise](https://mise.jdx.dev/), which pins the toolchain and forwards every task through a thin `make` shim:
168
+
169
+ ```sh
170
+ mise run install # install deps, generate wire bindings, and git hooks
171
+ mise run lint # ruff check, ruff format --check, and mise.lock drift
172
+ mise run format # apply ruff formatting
173
+ mise run typecheck # ty (strict)
174
+ mise run test # unit tests on the floor Python
175
+ mise run test-matrix # unit tests on every supported Python
176
+ ```
177
+
178
+ ## License
179
+
180
+ Licensed under the [Apache License, Version 2.0](./LICENSE).
@@ -0,0 +1,154 @@
1
+ <div align="center">
2
+
3
+ <img src="assets/banner.png" alt="Reactor Runtime" width="100%" />
4
+
5
+ **Build real-time AI models in Python.**
6
+
7
+ [📖 Documentation](https://docs.reactor.inc/deploy/overview) · [🚀 Quickstart](https://docs.reactor.inc/deploy/development/overview) · [🌐 Reactor](https://reactor.inc)
8
+
9
+ </div>
10
+
11
+ ---
12
+
13
+ Reactor Runtime turns an inference pipeline into a real-time, interactive media and data stream. You write `load()` and `generate()`; the runtime drives them one step at a time and handles the session lifecycle, the WebRTC media transport, and the wire protocol that connects clients to your model. Viewers watch frames as they are generated and change what the model is doing mid-stream, with no restart and no re-queue.
14
+
15
+ ## Highlights
16
+
17
+ - 📡 **Real-time streaming.** Frames reach clients over WebRTC as your model produces them, not after a whole video is done. The runtime paces playout from the time each step took, so a model needs no rate limiter of its own.
18
+ - 🎮 **Live interaction.** Clients send commands mid-generation: change a prompt, move a camera, adjust a parameter. The next frame reflects it.
19
+ - 🔌 **No transport code.** You never import a WebRTC library, manage a WebSocket, or encode video. The runtime ships its own media engine as a wheel, so a plain Python container is all a model needs.
20
+ - ✅ **Typed, validated commands.** Declare the commands your model accepts with standard Python types and constraints. The runtime validates every payload before your handler runs and compiles the surface into an OpenAPI schema that drives typed client SDKs.
21
+ - 🔎 **Traceable logs.** `get_logger()` writes structured records — readable `key=value` in a terminal, JSON for a log pipeline. Every record a session writes carries that session's id automatically, so one filter recovers everything a single run logged.
22
+ - 📦 **One container, anywhere.** The `reactor` CLI scaffolds a workspace, builds a small image, and runs it locally. The same image deploys to [Reactor](https://reactor.inc)'s GPU cloud unchanged.
23
+
24
+ ## How it works
25
+
26
+ You ship one `ReactorApp` subclass: the application the runtime drives and the client talks to. Declare the media it sends and the state a client can set, load your weights once, and write what one step of generation does:
27
+
28
+ ```python
29
+ from pathlib import Path
30
+
31
+ from reactor_runtime import InputField, InputState, Output, ReactorApp, Video
32
+
33
+
34
+ class MyState(InputState):
35
+ prompt: str = InputField(default="a sunny meadow", moderate=True, description="Scene to render.")
36
+ paused: bool = InputField(default=False, description="Hold generation on the last frame.")
37
+
38
+
39
+ class MyOutput(Output):
40
+ main_video: Video
41
+
42
+
43
+ class MyModel(ReactorApp):
44
+ state: MyState
45
+
46
+ def load(self, config_path: Path | None) -> None:
47
+ self.pipe = load_my_pipeline()
48
+
49
+ def generate(self, input: MyState) -> MyOutput:
50
+ return MyOutput(main_video=self.pipe.forward(prompt=input.prompt))
51
+ ```
52
+
53
+ That is a complete application. The runtime calls `generate()` in a loop for as long as someone is watching and emits what it returns. Every public field on `MyState` is a command the client can send: here `set_prompt` and `set_paused`, validated from the fields, and the next step reads the new values.
54
+
55
+ A step is three calls, and `generate()` is the one you must write. `process_input()` runs before it, reading `self.state` and the media tracks, and decides whether a step can happen: return the input `generate()` gets, or raise `ApplicationError("reason")` to skip the step without touching the model. `process_output(outcome)` runs after it, with the result or the error, and returns the media to emit; send a message from there with `await self.send()` and it reaches the client before the step's frames. Both have defaults, so the model above writes neither.
56
+
57
+ ```python
58
+ from reactor_runtime import ApplicationError, MessageField, ModelMessage, StepOutcome
59
+
60
+
61
+ class FrameReady(ModelMessage):
62
+ prompt: str = MessageField(description="The prompt this frame was rendered from.")
63
+
64
+
65
+ class MyModel(ReactorApp):
66
+ state: MyState
67
+
68
+ async def process_input(self) -> MyState:
69
+ if self.state.paused:
70
+ raise ApplicationError("paused")
71
+ return self.state
72
+
73
+ def generate(self, input: MyState) -> MyOutput: ...
74
+
75
+ async def process_output(self, outcome: StepOutcome) -> Output | None:
76
+ if outcome.error is not None:
77
+ raise outcome.error
78
+ await self.send(FrameReady(prompt=self.state.prompt))
79
+ return outcome.to_output()
80
+ ```
81
+
82
+ `generate()` fails by raising, and the runtime hands the exception to `process_output()` as `outcome.error` rather than letting it escape. That is where you decide. Recover an error you expect from your model: reset it, send a message, return `None`, and the loop goes on to the next step. Re-raise anything else, as the example does: a raise out of `process_output()` is a crash of the model, not of the step. The runtime logs the traceback, stops dispatching commands, ends the session with an error the client sees, and does not restart the loop, which is what an uncaught exception in a hand-written `run()` does too. The default `process_output()` re-raises, so a failing model ends loudly instead of serving nothing in silence. A refusal from `process_input()` is not a failure and never reaches `process_output()`.
83
+
84
+ ```python
85
+ async def process_output(self, outcome: StepOutcome) -> Output | None:
86
+ if isinstance(outcome.error, RolloutExhausted): # an error the model is known to raise
87
+ self.engine.reset()
88
+ self.output.flush()
89
+ await self.send(WorldRestarted(reason="rollout window reached"))
90
+ return None # nothing to show; the next step starts over
91
+ if outcome.error is not None:
92
+ raise outcome.error # anything else is a bug: end the loop
93
+ return outcome.to_output()
94
+ ```
95
+
96
+ Command handlers and lifecycle hooks run between steps, never during one. The default `run()` is the loop that drives the three hooks. Override it to write your own loop against `emit()`, `send()`, `@event`, `self.connected`, and the tracks; `process_input()`, `generate()`, and `process_output()` are then not called. Do that for a loop that is not one step per emit, such as a renderer that emits several times per step or a model that must block on an input.
97
+
98
+ Scaffold, build, and run it with the CLI:
99
+
100
+ ```sh
101
+ reactor init my-model
102
+ cd my-model
103
+ reactor run
104
+ ```
105
+
106
+ `reactor run` builds a container with the runtime inside and serves WebRTC signaling on port 8080. Point a browser at it with the [JS SDK](https://docs.reactor.inc), or connect from the [Reactor Sandbox](https://reactor-sandbox.vercel.app/) and watch frames stream immediately.
107
+
108
+ Log from the same import, passing context as keyword arguments:
109
+
110
+ ```python
111
+ from reactor_runtime import get_logger
112
+
113
+ logger = get_logger(__name__)
114
+
115
+ logger.info("scene changed", prompt=self.prompt)
116
+ ```
117
+
118
+ Records render as `key=value` text by default, or as one JSON object per line under `REACTOR_LOG_FORMAT=json`. While a session is live, its id is stamped on every record, so tracing one run's logs never requires threading an id through your call sites. Every record also carries the lifecycle phase it was written in, at both granularities: `state`, the session state machine's word, and `runtime_state`, the coarse word the health endpoint serves — so the logs of one phase — loading weights, a live session, teardown — are filterable by whichever vocabulary you are reading off another surface. The stamp is applied where records are written rather than where they are made, so a plain `logging.getLogger(__name__)` and the libraries your model imports are covered too.
119
+
120
+ ## Install
121
+
122
+ Everything runs through the [`reactor` CLI](https://docs.reactor.inc/deploy/platform/installation). There is nothing to install on your host but the CLI and Docker; the runtime ships inside the image the CLI builds for your workspace.
123
+
124
+ ```sh
125
+ brew install reactor-team/tools/reactor-cli
126
+ ```
127
+
128
+ Not on macOS, or pinning a release in CI? See [Install the CLI](https://docs.reactor.inc/deploy/platform/installation).
129
+
130
+ ## Learn more
131
+
132
+ - [Quickstart](https://docs.reactor.inc/deploy/development/overview): from zero to a streaming model in 2 minutes
133
+ - [Model anatomy](https://docs.reactor.inc/deploy/development/reactor-app/model-anatomy): every member of a `ReactorApp`, line by line
134
+ - [The step loop](https://docs.reactor.inc/deploy/development/reactor-app/step-loop): `process_input()`, `generate()`, `process_output()`, and the frame rate
135
+ - [Starter example](./examples/starter/README.md): the model `reactor init` scaffolds: one class, `generate()` alone, the smallest complete `ReactorApp`
136
+ - [Echo example](./examples/echo/README.md): the client's webcam and microphone in, an effect applied, both sent back, in batches
137
+ - [Waypoint example](./examples/waypoint/README.md): a world model on a GPU, seeded from an upload and steered live
138
+
139
+ ## Development
140
+
141
+ This repository holds the runtime package itself: the authoring interface, the session runner, the media transport, and the wire protocol. To work on it, use [mise](https://mise.jdx.dev/), which pins the toolchain and forwards every task through a thin `make` shim:
142
+
143
+ ```sh
144
+ mise run install # install deps, generate wire bindings, and git hooks
145
+ mise run lint # ruff check, ruff format --check, and mise.lock drift
146
+ mise run format # apply ruff formatting
147
+ mise run typecheck # ty (strict)
148
+ mise run test # unit tests on the floor Python
149
+ mise run test-matrix # unit tests on every supported Python
150
+ ```
151
+
152
+ ## License
153
+
154
+ Licensed under the [Apache License, Version 2.0](./LICENSE).
@@ -70,6 +70,9 @@ python = ".venv"
70
70
  python-version = "3.12"
71
71
  extra-paths = [
72
72
  ".",
73
+ "examples/starter",
74
+ "examples/echo",
75
+ "examples/waypoint",
73
76
  "tests/unit/transport/webrtc",
74
77
  "tests/contract",
75
78
  ]
@@ -92,7 +95,7 @@ pythonpath = ["."]
92
95
 
93
96
  [project]
94
97
  name = "reactor-runtime"
95
- version = "3.3.2"
98
+ version = "3.5.0"
96
99
  description = "A Python framework for building real-time, interactive video models"
97
100
  readme = "README.md"
98
101
  license = "Apache-2.0"
@@ -115,7 +118,7 @@ dependencies = [
115
118
  "prometheus-client>=0.26.0",
116
119
  "protobuf>=7.35.1",
117
120
  "pyyaml>=6.0.3",
118
- "reactor-webrtc==0.17.0",
121
+ "reactor-webrtc==0.18.0",
119
122
  "uvicorn>=0.49.0",
120
123
  ]
121
124
 
@@ -20,7 +20,7 @@ version = "1.20260814.7"
20
20
 
21
21
  [project]
22
22
  name = "reactor-runtime"
23
- version = "3.3.2"
23
+ version = "3.5.0"
24
24
  description = "A Python framework for building real-time, interactive video models"
25
25
  readme = "README.md"
26
26
  license = "Apache-2.0"
@@ -41,7 +41,7 @@ dependencies = [
41
41
  "prometheus-client>=0.26.0",
42
42
  "protobuf>=7.35.1",
43
43
  "pyyaml>=6.0.3",
44
- "reactor-webrtc==0.17.0",
44
+ "reactor-webrtc==0.18.0",
45
45
  "uvicorn>=0.49.0",
46
46
  ]
47
47
 
@@ -128,8 +128,17 @@ python-version = "3.12"
128
128
  # models (`examples`, first-party but outside the package) resolve the way
129
129
  # pytest's pythonpath makes them importable; the WebRTC test directory, so
130
130
  # its tests can import their sibling `conftest` for the shared peer fakes; and
131
- # the contract suite, so its tests can import their sibling `contract_helpers`.
132
- extra-paths = [".", "tests/unit/transport/webrtc", "tests/contract"]
131
+ # the contract suite, so its tests can import their sibling `contract_helpers`;
132
+ # and each example workspace, whose two modules import each other as top-level
133
+ # names, the way the runtime imports a workspace.
134
+ extra-paths = [
135
+ ".",
136
+ "examples/starter",
137
+ "examples/echo",
138
+ "examples/waypoint",
139
+ "tests/unit/transport/webrtc",
140
+ "tests/contract",
141
+ ]
133
142
 
134
143
  [tool.ty.src]
135
144
  # Check the package, its tests, and the example models; scripts/ ships developer
@@ -3,39 +3,47 @@
3
3
  The authoring surface is re-exported here so a model author imports from one
4
4
  obvious place::
5
5
 
6
- from reactor_runtime import ReactorModel, Output, Video, event
6
+ from reactor_runtime import ReactorApp, Output, Video, event
7
7
 
8
8
  The same names are available under :mod:`reactor_runtime.interface`; importing
9
9
  from the top-level package is the preferred path.
10
+
11
+ Four names are deprecated and import with a :class:`DeprecationWarning`.
12
+ ``ReactorModel`` is :class:`ReactorApp` and ``Input`` is :class:`MediaInput`,
13
+ renames that resolve to the new class. ``ReactorPipeline`` and ``Idle`` still
14
+ resolve to themselves, so a model on the generator pattern keeps working, but
15
+ the pattern is retired in favour of :class:`ReactorApp` and ``generate()``.
16
+ All four are removed in the next major.
10
17
  """
11
18
 
12
19
  from importlib.metadata import version
20
+ from typing import Any
13
21
 
14
22
  from reactor_runtime.interface import (
15
23
  EVENT_REGISTRY,
16
24
  INPUT_REGISTRY,
17
25
  MESSAGE_REGISTRY,
18
26
  OUTPUT_REGISTRY,
27
+ ApplicationError,
19
28
  Audio,
20
29
  BufferClosed,
21
30
  ClientInfo,
22
31
  Command,
23
32
  CommandError,
24
33
  FieldInfo,
25
- Idle,
26
- Input,
27
34
  InputBuffer,
28
35
  InputField,
29
36
  InputFrame,
30
37
  InputState,
38
+ MediaInput,
31
39
  MessageField,
32
40
  Metadata,
33
41
  ModelMessage,
34
42
  Output,
35
43
  OutputStream,
36
- ReactorModel,
37
- ReactorPipeline,
44
+ ReactorApp,
38
45
  ReadMode,
46
+ StepOutcome,
39
47
  Track,
40
48
  TrackPayload,
41
49
  UploadedFile,
@@ -49,6 +57,16 @@ from reactor_runtime.interface import (
49
57
  session_ended,
50
58
  session_started,
51
59
  )
60
+ from reactor_runtime.interface.internal.aliases import (
61
+ IDLE_DEPRECATION,
62
+ PIPELINE_DEPRECATION,
63
+ deprecated_alias,
64
+ )
65
+
66
+ # Bound under private names so the public ones stay out of the module's
67
+ # namespace and resolve through __getattr__, which is where the warning lives.
68
+ from reactor_runtime.interface.pipeline import Idle as _Idle
69
+ from reactor_runtime.interface.pipeline import ReactorPipeline as _ReactorPipeline
52
70
  from reactor_runtime.log import get_logger
53
71
  from reactor_runtime.paths import get_weights_path
54
72
 
@@ -59,26 +77,26 @@ __all__ = [
59
77
  "INPUT_REGISTRY",
60
78
  "MESSAGE_REGISTRY",
61
79
  "OUTPUT_REGISTRY",
80
+ "ApplicationError",
62
81
  "Audio",
63
82
  "BufferClosed",
64
83
  "ClientInfo",
65
84
  "Command",
66
85
  "CommandError",
67
86
  "FieldInfo",
68
- "Idle",
69
- "Input",
70
87
  "InputBuffer",
71
88
  "InputField",
72
89
  "InputFrame",
73
90
  "InputState",
91
+ "MediaInput",
74
92
  "MessageField",
75
93
  "Metadata",
76
94
  "ModelMessage",
77
95
  "Output",
78
96
  "OutputStream",
79
- "ReactorModel",
80
- "ReactorPipeline",
97
+ "ReactorApp",
81
98
  "ReadMode",
99
+ "StepOutcome",
82
100
  "Track",
83
101
  "TrackPayload",
84
102
  "UploadedFile",
@@ -95,3 +113,17 @@ __all__ = [
95
113
  "session_ended",
96
114
  "session_started",
97
115
  ]
116
+
117
+ _DEPRECATED: dict[str, tuple[str, object, str | None]] = {
118
+ "ReactorModel": ("ReactorApp", ReactorApp, None),
119
+ "Input": ("MediaInput", MediaInput, None),
120
+ "ReactorPipeline": ("ReactorApp", _ReactorPipeline, PIPELINE_DEPRECATION),
121
+ "Idle": ("ApplicationError", _Idle, IDLE_DEPRECATION),
122
+ }
123
+
124
+
125
+ def __getattr__(name: str) -> Any:
126
+ if name in _DEPRECATED:
127
+ new, target, message = _DEPRECATED[name]
128
+ return deprecated_alias(name, new, target, message)
129
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -105,13 +105,15 @@ class ConnectionSink(Protocol):
105
105
  """
106
106
 
107
107
  def connection_opened(self, conn: Connection) -> None:
108
- """Register a connection whose wire has reached its connected state.
108
+ """Register a connection whose wire can carry frames to the client.
109
109
 
110
110
  Fired when the transport is actually connected — for WebRTC, when the
111
- peer connection reaches its connected state — never at handshake or
112
- offer time. A transport that builds a connection during signaling holds
113
- it itself until then, so a client that offers but never connects never
114
- advances the session.
111
+ peer connection is connected and both data channels are open — never at
112
+ handshake or offer time. A transport that builds a connection during
113
+ signaling holds it itself until then, so a client that offers but never
114
+ connects never advances the session. Because the wire is open by the
115
+ time this fires, a message the model sends from its ``@connected`` hook
116
+ reaches the joining client.
115
117
  """
116
118
 
117
119
  def connection_closed(self, conn_id: ConnId) -> None:
@@ -8,8 +8,17 @@ resolve a model's contract.
8
8
 
9
9
  Everything re-exported here is also available directly on the top-level
10
10
  ``reactor_runtime`` package, which is the preferred import path.
11
+
12
+ Four names are deprecated and import with a :class:`DeprecationWarning`.
13
+ ``ReactorModel`` is :class:`ReactorApp` and ``Input`` is :class:`MediaInput`,
14
+ renames that resolve to the new class. ``ReactorPipeline`` and ``Idle`` still
15
+ resolve to themselves, so a model on the generator pattern keeps working, but
16
+ the pattern is retired in favour of :class:`ReactorApp` and ``generate()``.
17
+ All four are removed in the next major.
11
18
  """
12
19
 
20
+ from typing import Any
21
+
13
22
  from reactor_runtime.core import (
14
23
  Command,
15
24
  FieldInfo,
@@ -17,10 +26,12 @@ from reactor_runtime.core import (
17
26
  InputFrame,
18
27
  UploadedFile,
19
28
  )
29
+ from reactor_runtime.interface.app import InputState, ReactorApp, StepOutcome
20
30
  from reactor_runtime.interface.client import ClientInfo
21
31
  from reactor_runtime.interface.events import (
22
32
  EVENT_REGISTRY,
23
33
  MESSAGE_REGISTRY,
34
+ ApplicationError,
24
35
  CommandError,
25
36
  MessageField,
26
37
  ModelMessage,
@@ -31,19 +42,27 @@ from reactor_runtime.interface.events import (
31
42
  session_ended,
32
43
  session_started,
33
44
  )
45
+ from reactor_runtime.interface.internal.aliases import (
46
+ IDLE_DEPRECATION,
47
+ PIPELINE_DEPRECATION,
48
+ deprecated_alias,
49
+ )
34
50
  from reactor_runtime.interface.internal.input_buffer import (
35
51
  BufferClosed,
36
52
  InputBuffer,
37
53
  ReadMode,
38
54
  )
39
55
  from reactor_runtime.interface.internal.reactor_core import OutputStream
40
- from reactor_runtime.interface.model import ReactorModel
41
- from reactor_runtime.interface.pipeline import Idle, InputState, ReactorPipeline
56
+
57
+ # Bound under private names so the public ones stay out of the module's
58
+ # namespace and resolve through __getattr__, which is where the warning lives.
59
+ from reactor_runtime.interface.pipeline import Idle as _Idle
60
+ from reactor_runtime.interface.pipeline import ReactorPipeline as _ReactorPipeline
42
61
  from reactor_runtime.interface.tracks import (
43
62
  INPUT_REGISTRY,
44
63
  OUTPUT_REGISTRY,
45
64
  Audio,
46
- Input,
65
+ MediaInput,
47
66
  Metadata,
48
67
  Output,
49
68
  Track,
@@ -58,26 +77,26 @@ __all__ = [
58
77
  "INPUT_REGISTRY",
59
78
  "MESSAGE_REGISTRY",
60
79
  "OUTPUT_REGISTRY",
80
+ "ApplicationError",
61
81
  "Audio",
62
82
  "BufferClosed",
63
83
  "ClientInfo",
64
84
  "Command",
65
85
  "CommandError",
66
86
  "FieldInfo",
67
- "Idle",
68
- "Input",
69
87
  "InputBuffer",
70
88
  "InputField",
71
89
  "InputFrame",
72
90
  "InputState",
91
+ "MediaInput",
73
92
  "MessageField",
74
93
  "Metadata",
75
94
  "ModelMessage",
76
95
  "Output",
77
96
  "OutputStream",
78
- "ReactorModel",
79
- "ReactorPipeline",
97
+ "ReactorApp",
80
98
  "ReadMode",
99
+ "StepOutcome",
81
100
  "Track",
82
101
  "TrackPayload",
83
102
  "UploadedFile",
@@ -91,3 +110,17 @@ __all__ = [
91
110
  "session_ended",
92
111
  "session_started",
93
112
  ]
113
+
114
+ _DEPRECATED: dict[str, tuple[str, object, str | None]] = {
115
+ "ReactorModel": ("ReactorApp", ReactorApp, None),
116
+ "Input": ("MediaInput", MediaInput, None),
117
+ "ReactorPipeline": ("ReactorApp", _ReactorPipeline, PIPELINE_DEPRECATION),
118
+ "Idle": ("ApplicationError", _Idle, IDLE_DEPRECATION),
119
+ }
120
+
121
+
122
+ def __getattr__(name: str) -> Any:
123
+ if name in _DEPRECATED:
124
+ new, target, message = _DEPRECATED[name]
125
+ return deprecated_alias(name, new, target, message)
126
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,14 @@
1
+ """The application authoring base, :class:`ReactorApp`, its typed state, and the step types.
2
+
3
+ What an author subclasses. Declaring the subclass assembles its
4
+ :class:`ModelContract` from one traversal of the class; declaring
5
+ ``state: MyState`` on it turns every public :class:`InputState` field into a
6
+ ``set_<field>`` command. :class:`StepOutcome` is what one call to
7
+ ``generate()`` did, built by the runtime and read by the application.
8
+ """
9
+
10
+ from reactor_runtime.interface.app.input_state import InputState
11
+ from reactor_runtime.interface.app.outcome import StepOutcome
12
+ from reactor_runtime.interface.app.reactor_app import ReactorApp
13
+
14
+ __all__ = ["InputState", "ReactorApp", "StepOutcome"]