reactor-runtime 3.6.1__tar.gz → 3.7.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 (124) hide show
  1. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/PKG-INFO +20 -3
  2. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/README.md +18 -1
  3. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/pyproject.toml +2 -2
  4. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/pyproject.toml.orig +2 -2
  5. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/__init__.py +2 -0
  6. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/core/__init__.py +19 -1
  7. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/core/model.py +15 -0
  8. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/core/service.py +32 -0
  9. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/core/session.py +8 -3
  10. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/core/transport.py +11 -0
  11. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/core/values.py +132 -1
  12. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/http/__init__.py +2 -0
  13. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/http/routes.py +148 -3
  14. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/http/server.py +2 -0
  15. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/__init__.py +2 -1
  16. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/app/__init__.py +3 -1
  17. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/app/reactor_app.py +102 -49
  18. reactor_runtime-3.7.0/src/reactor_runtime/interface/app/step_completed.py +59 -0
  19. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/client.py +6 -0
  20. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/internal/bridge.py +28 -2
  21. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/internal/reactor_core.py +93 -20
  22. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/pipeline/reactor_pipeline.py +1 -1
  23. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/manifest.py +51 -7
  24. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/message_gateway.py +5 -3
  25. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/metrics/session.py +6 -2
  26. reactor_runtime-3.7.0/src/reactor_runtime/runner/client_stats.py +110 -0
  27. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/runner/connection_manager.py +48 -12
  28. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/runner/runner.py +430 -16
  29. reactor_runtime-3.7.0/src/reactor_runtime/runner/session_start.py +162 -0
  30. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/runner/state_machine.py +39 -2
  31. reactor_runtime-3.7.0/src/reactor_runtime/runner/system_client.py +64 -0
  32. reactor_runtime-3.7.0/src/reactor_runtime/step_results/__init__.py +12 -0
  33. reactor_runtime-3.7.0/src/reactor_runtime/step_results/mp4.py +182 -0
  34. reactor_runtime-3.7.0/src/reactor_runtime/step_results/store.py +445 -0
  35. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/acceptor.py +4 -1
  36. reactor_runtime-3.7.0/src/reactor_runtime/transport/webrtc/client_stats.py +114 -0
  37. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/config.py +16 -0
  38. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/connection.py +27 -5
  39. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/peer.py +65 -12
  40. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/LICENSE +0 -0
  41. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/NOTICE +0 -0
  42. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/codes.py +0 -0
  43. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/core/fields.py +0 -0
  44. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/core/naming.py +0 -0
  45. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/core/typespec.py +0 -0
  46. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/distributed/__init__.py +0 -0
  47. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/distributed/errors.py +0 -0
  48. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/distributed/ipc.py +0 -0
  49. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/distributed/protocol.py +0 -0
  50. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/distributed/rank.py +0 -0
  51. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/distributed/runner.py +0 -0
  52. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/distributed/worker.py +0 -0
  53. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/event_stream.py +0 -0
  54. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/http/events.py +0 -0
  55. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/http/spec.py +0 -0
  56. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/app/input_state.py +0 -0
  57. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/app/outcome.py +0 -0
  58. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/events/__init__.py +0 -0
  59. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/events/decorators.py +0 -0
  60. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/events/errors.py +0 -0
  61. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/events/messages.py +0 -0
  62. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/internal/__init__.py +0 -0
  63. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/internal/aliases.py +0 -0
  64. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/internal/input_buffer.py +0 -0
  65. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/model/__init__.py +0 -0
  66. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/model/contract.py +0 -0
  67. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/model/reactor_model.py +0 -0
  68. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/model/schema.py +0 -0
  69. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/pipeline/__init__.py +0 -0
  70. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/pipeline/idle.py +0 -0
  71. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/pipeline/input_state.py +0 -0
  72. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/tracks/__init__.py +0 -0
  73. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/tracks/descriptors.py +0 -0
  74. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/tracks/input.py +0 -0
  75. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/interface/tracks/output.py +0 -0
  76. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/log.py +0 -0
  77. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/metrics/__init__.py +0 -0
  78. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/metrics/command.py +0 -0
  79. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/metrics/model.py +0 -0
  80. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/metrics/registry.py +0 -0
  81. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/paths.py +0 -0
  82. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/protocol/__init__.py +0 -0
  83. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/protocol/base.py +0 -0
  84. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/protocol/common.py +0 -0
  85. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/protocol/v0/__init__.py +0 -0
  86. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/protocol/v0/codec.py +0 -0
  87. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/protocol/v1/__init__.py +0 -0
  88. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/protocol/v1/codec.py +0 -0
  89. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/py.typed +0 -0
  90. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/recording/__init__.py +0 -0
  91. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/recording/chunk_encoder.py +0 -0
  92. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/recording/markers.py +0 -0
  93. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/recording/recorder.py +0 -0
  94. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/runner/__init__.py +0 -0
  95. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/runner/offer_epochs.py +0 -0
  96. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/runner/upload_resolution.py +0 -0
  97. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/schema.py +0 -0
  98. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/serve.py +0 -0
  99. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/service.py +0 -0
  100. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/__init__.py +0 -0
  101. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/acceptor.py +0 -0
  102. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/router.py +0 -0
  103. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/__init__.py +0 -0
  104. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/frames.py +0 -0
  105. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/metrics.py +0 -0
  106. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/pacer.py +0 -0
  107. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/router.py +0 -0
  108. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/sdp.py +0 -0
  109. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/signaling.py +0 -0
  110. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/stats.py +0 -0
  111. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/transport/webrtc/version.py +0 -0
  112. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_runtime/upload_store.py +0 -0
  113. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_wire/v1/common_pb2.py +0 -0
  114. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_wire/v1/common_pb2.pyi +0 -0
  115. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_wire/v1/control_pb2.py +0 -0
  116. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_wire/v1/control_pb2.pyi +0 -0
  117. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_wire/v1/data_pb2.py +0 -0
  118. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_wire/v1/data_pb2.pyi +0 -0
  119. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_wire/v1/model_pb2.py +0 -0
  120. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_wire/v1/model_pb2.pyi +0 -0
  121. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_wire/v1/platform_pb2.py +0 -0
  122. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_wire/v1/platform_pb2.pyi +0 -0
  123. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_wire/v1/track_pb2.py +0 -0
  124. {reactor_runtime-3.6.1 → reactor_runtime-3.7.0}/src/reactor_wire/v1/track_pb2.pyi +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reactor-runtime
3
- Version: 3.6.1
3
+ Version: 3.7.0
4
4
  Summary: A Python framework for building real-time, interactive video models
5
5
  Author: Reactor
6
6
  Author-email: Reactor <team@reactor.inc>
@@ -19,7 +19,7 @@ Requires-Dist: numpy>=2.1
19
19
  Requires-Dist: prometheus-client>=0.26.0
20
20
  Requires-Dist: protobuf>=7.35.1
21
21
  Requires-Dist: pyyaml>=6.0.3
22
- Requires-Dist: reactor-webrtc==0.18.0
22
+ Requires-Dist: reactor-webrtc==0.21.0
23
23
  Requires-Dist: uvicorn>=0.49.0
24
24
  Requires-Python: >=3.12
25
25
  Project-URL: Source, https://github.com/reactor-team/reactor-runtime
@@ -122,7 +122,24 @@ class MyModel(ReactorApp):
122
122
  return outcome.to_output()
123
123
  ```
124
124
 
125
- Command handlers and lifecycle hooks run between steps, never during one. A model has one method per lifecycle hook kind (`@session_started`, `@session_ended`, `@connected`, `@disconnected`, `@file_uploaded`). A subclass extends an inherited hook by overriding that method and calling `super()`; the override runs whether or not it repeats the decorator. Marking a second method with the same kind, in one class or in a subclass, fails when the class is defined, so a hook is never dropped in silence. 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.
125
+ Command handlers and lifecycle hooks run between steps, never during one. A model has one method per lifecycle hook kind (`@session_started`, `@session_ended`, `@connected`, `@disconnected`, `@file_uploaded`). A subclass extends an inherited hook by overriding that method and calling `super()`; the override runs whether or not it repeats the decorator. Marking a second method with the same kind, in one class or in a subclass, fails when the class is defined, so a hook is never dropped in silence. The default `run()` is the loop that drives the three hooks, and it reports each step that runs with `complete_step()`, which is what the runtime counts a session's steps by. To write that report yourself, for example to keep extra files with the step, return a `StepCompleted(output=..., files=...)` from `process_output()` instead of the `Output`. Override `run()` to write your own loop against `emit()`, `send()`, `complete_step()`, `@event`, `self.connected`, and the tracks; `process_input()`, `generate()`, and `process_output()` are then not called, and your loop calls `complete_step(StepCompleted(output=...))` each time it finishes a unit of work. 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.
126
+
127
+ A session can start with its setup already in hand: `POST /start_session` takes a `starting_input` of state values and commands, which the runtime applies on the normal command path before any client connects. A system client sends them, a connection the runtime opens itself, so a handler that takes `client` gets a `ClientInfo` whose `system` is true. The step loop takes its first step only once every starting command has landed, uploads included, so no step runs on the defaults the starting input replaces. In a live session the system client then leaves and the loop waits for a viewer, as it always does; a model with its own `run()` sees the system client connect like any client. A live session stays `waiting` while its starting input applies, because the system client does not occupy it, and `/events` journals the system client's `connection_opened` and `connection_closed` as `waiting -> waiting`. A session with `steps` is occupied by the system client until it ends, so it streams with no other client. The runtime ends it itself once the model has reported that many steps, with a normal stop whose close reason is "Session ended: the requested steps are complete."
128
+
129
+ A model can keep each step it reports as a folder of files, turned on in `reactor.yaml`:
130
+
131
+ ```yaml
132
+ runtime:
133
+ step_results:
134
+ enabled: true
135
+ video: {codec: h264, preset: veryfast, crf: 23} # or codec: h265
136
+ audio: {codec: aac, bitrate_kbps: 128}
137
+ queue: 8
138
+ ```
139
+
140
+ Every track of the step's output becomes its own stream in one `output.mp4`, beside the extra `files` the step carried and a `result.json` that lists them. `queue` bounds how many steps may wait to be saved; a step that finds it full is not saved, so saving never holds the model up. Each step is its own encode, so this suits a model that finishes a step every few seconds rather than every frame.
141
+
142
+ When step results are on, `GET /sessions/{id}/steps` lists a session's saved folders by the session's own id; each stays readable for five minutes after it is written, after the session ends too.
126
143
 
127
144
  Scaffold, build, and run it with the CLI:
128
145
 
@@ -95,7 +95,24 @@ class MyModel(ReactorApp):
95
95
  return outcome.to_output()
96
96
  ```
97
97
 
98
- Command handlers and lifecycle hooks run between steps, never during one. A model has one method per lifecycle hook kind (`@session_started`, `@session_ended`, `@connected`, `@disconnected`, `@file_uploaded`). A subclass extends an inherited hook by overriding that method and calling `super()`; the override runs whether or not it repeats the decorator. Marking a second method with the same kind, in one class or in a subclass, fails when the class is defined, so a hook is never dropped in silence. 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.
98
+ Command handlers and lifecycle hooks run between steps, never during one. A model has one method per lifecycle hook kind (`@session_started`, `@session_ended`, `@connected`, `@disconnected`, `@file_uploaded`). A subclass extends an inherited hook by overriding that method and calling `super()`; the override runs whether or not it repeats the decorator. Marking a second method with the same kind, in one class or in a subclass, fails when the class is defined, so a hook is never dropped in silence. The default `run()` is the loop that drives the three hooks, and it reports each step that runs with `complete_step()`, which is what the runtime counts a session's steps by. To write that report yourself, for example to keep extra files with the step, return a `StepCompleted(output=..., files=...)` from `process_output()` instead of the `Output`. Override `run()` to write your own loop against `emit()`, `send()`, `complete_step()`, `@event`, `self.connected`, and the tracks; `process_input()`, `generate()`, and `process_output()` are then not called, and your loop calls `complete_step(StepCompleted(output=...))` each time it finishes a unit of work. 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.
99
+
100
+ A session can start with its setup already in hand: `POST /start_session` takes a `starting_input` of state values and commands, which the runtime applies on the normal command path before any client connects. A system client sends them, a connection the runtime opens itself, so a handler that takes `client` gets a `ClientInfo` whose `system` is true. The step loop takes its first step only once every starting command has landed, uploads included, so no step runs on the defaults the starting input replaces. In a live session the system client then leaves and the loop waits for a viewer, as it always does; a model with its own `run()` sees the system client connect like any client. A live session stays `waiting` while its starting input applies, because the system client does not occupy it, and `/events` journals the system client's `connection_opened` and `connection_closed` as `waiting -> waiting`. A session with `steps` is occupied by the system client until it ends, so it streams with no other client. The runtime ends it itself once the model has reported that many steps, with a normal stop whose close reason is "Session ended: the requested steps are complete."
101
+
102
+ A model can keep each step it reports as a folder of files, turned on in `reactor.yaml`:
103
+
104
+ ```yaml
105
+ runtime:
106
+ step_results:
107
+ enabled: true
108
+ video: {codec: h264, preset: veryfast, crf: 23} # or codec: h265
109
+ audio: {codec: aac, bitrate_kbps: 128}
110
+ queue: 8
111
+ ```
112
+
113
+ Every track of the step's output becomes its own stream in one `output.mp4`, beside the extra `files` the step carried and a `result.json` that lists them. `queue` bounds how many steps may wait to be saved; a step that finds it full is not saved, so saving never holds the model up. Each step is its own encode, so this suits a model that finishes a step every few seconds rather than every frame.
114
+
115
+ When step results are on, `GET /sessions/{id}/steps` lists a session's saved folders by the session's own id; each stays readable for five minutes after it is written, after the session ends too.
99
116
 
100
117
  Scaffold, build, and run it with the CLI:
101
118
 
@@ -95,7 +95,7 @@ pythonpath = ["."]
95
95
 
96
96
  [project]
97
97
  name = "reactor-runtime"
98
- version = "3.6.1"
98
+ version = "3.7.0"
99
99
  description = "A Python framework for building real-time, interactive video models"
100
100
  readme = "README.md"
101
101
  license = "Apache-2.0"
@@ -119,7 +119,7 @@ dependencies = [
119
119
  "prometheus-client>=0.26.0",
120
120
  "protobuf>=7.35.1",
121
121
  "pyyaml>=6.0.3",
122
- "reactor-webrtc==0.18.0",
122
+ "reactor-webrtc==0.21.0",
123
123
  "uvicorn>=0.49.0",
124
124
  ]
125
125
 
@@ -20,7 +20,7 @@ version = "1.20260929.8"
20
20
 
21
21
  [project]
22
22
  name = "reactor-runtime"
23
- version = "3.6.1"
23
+ version = "3.7.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"
@@ -42,7 +42,7 @@ dependencies = [
42
42
  "prometheus-client>=0.26.0",
43
43
  "protobuf>=7.35.1",
44
44
  "pyyaml>=6.0.3",
45
- "reactor-webrtc==0.18.0",
45
+ "reactor-webrtc==0.21.0",
46
46
  "uvicorn>=0.49.0",
47
47
  ]
48
48
 
@@ -43,6 +43,7 @@ from reactor_runtime.interface import (
43
43
  OutputStream,
44
44
  ReactorApp,
45
45
  ReadMode,
46
+ StepCompleted,
46
47
  StepOutcome,
47
48
  Track,
48
49
  TrackPayload,
@@ -96,6 +97,7 @@ __all__ = [
96
97
  "OutputStream",
97
98
  "ReactorApp",
98
99
  "ReadMode",
100
+ "StepCompleted",
99
101
  "StepOutcome",
100
102
  "Track",
101
103
  "TrackPayload",
@@ -17,10 +17,16 @@ from reactor_runtime.core.model import (
17
17
  ReactorEvent,
18
18
  SessionEnded,
19
19
  SessionStarted,
20
+ StartingInputApplied,
20
21
  TransitionEvent,
21
22
  UploadedFile,
22
23
  )
23
- from reactor_runtime.core.service import RecordingConfig, RuntimeConfig, ServiceComponent
24
+ from reactor_runtime.core.service import (
25
+ RecordingConfig,
26
+ RuntimeConfig,
27
+ ServiceComponent,
28
+ StepResultsConfig,
29
+ )
24
30
  from reactor_runtime.core.session import (
25
31
  JOURNAL_EVENTS,
26
32
  SessionEvent,
@@ -30,7 +36,12 @@ from reactor_runtime.core.session import (
30
36
  from reactor_runtime.core.transport import Connection, ConnectionSink
31
37
  from reactor_runtime.core.typespec import TypeSpec
32
38
  from reactor_runtime.core.values import (
39
+ ClientConnectionStat,
40
+ ClientStatsBatch,
41
+ ClientTrackDirection,
42
+ ClientTrackStat,
33
43
  CommandFailure,
44
+ CompletedStep,
34
45
  ConnectionCapabilities,
35
46
  ConnId,
36
47
  Health,
@@ -48,10 +59,15 @@ from reactor_runtime.core.values import (
48
59
  __all__ = [
49
60
  "JOURNAL_EVENTS",
50
61
  "ClientConnected",
62
+ "ClientConnectionStat",
51
63
  "ClientDisconnected",
64
+ "ClientStatsBatch",
65
+ "ClientTrackDirection",
66
+ "ClientTrackStat",
52
67
  "Command",
53
68
  "CommandFailure",
54
69
  "CommandField",
70
+ "CompletedStep",
55
71
  "ConnId",
56
72
  "Connection",
57
73
  "ConnectionCapabilities",
@@ -74,6 +90,8 @@ __all__ = [
74
90
  "SessionEvent",
75
91
  "SessionStarted",
76
92
  "SessionState",
93
+ "StartingInputApplied",
94
+ "StepResultsConfig",
77
95
  "TrackData",
78
96
  "TrackDirection",
79
97
  "TrackInfo",
@@ -243,9 +243,22 @@ class SessionStarted(ReactorEvent):
243
243
 
244
244
  Attributes:
245
245
  session_id: Identifier for the session that started.
246
+ starting_input: Whether a starting input follows. When one does, the
247
+ step loop takes no step until :class:`StartingInputApplied`
248
+ arrives, so its first step sees the state the starting input set.
246
249
  """
247
250
 
248
251
  session_id: str
252
+ starting_input: bool = False
253
+
254
+
255
+ @dataclass(frozen=True)
256
+ class StartingInputApplied(ReactorEvent):
257
+ """Every command of the session's starting input has been handed to the model.
258
+
259
+ It follows the last starting command on the same ordered queue, so every
260
+ starting handler has returned by the time the model handles it.
261
+ """
249
262
 
250
263
 
251
264
  @dataclass(frozen=True)
@@ -274,10 +287,12 @@ class ClientConnected(ReactorEvent):
274
287
  Attributes:
275
288
  conn_id: The connection that opened.
276
289
  total: Live connection count after the open.
290
+ system: Whether the connection is the runtime's own system client.
277
291
  """
278
292
 
279
293
  conn_id: ConnId
280
294
  total: int
295
+ system: bool = False
281
296
 
282
297
 
283
298
  @dataclass(frozen=True)
@@ -92,6 +92,36 @@ class RecordingConfig:
92
92
  recording_dir: str | None = None
93
93
 
94
94
 
95
+ @dataclass(frozen=True)
96
+ class StepResultsConfig:
97
+ """How each finished step is kept, drawn from the manifest's ``step_results:`` block.
98
+
99
+ A model with no ``step_results:`` block keeps nothing (``enabled`` is
100
+ ``False``). When enabled, every step the model reports is saved as a
101
+ folder: its output in one ``output.mp4``, with each track of the step's
102
+ ``Output`` as its own stream, beside any extra files and a ``result.json``.
103
+ Turning it on is the model's choice, not a session option.
104
+
105
+ Attributes:
106
+ enabled: Whether the runtime saves each finished step.
107
+ video_codec: ``"h264"`` or ``"h265"``.
108
+ video_preset: The libx264/libx265 preset.
109
+ video_crf: The constant-rate-factor quality target.
110
+ audio_codec: The audio codec, e.g. ``"aac"``.
111
+ audio_bitrate_kbps: The audio bitrate in kilobits per second.
112
+ queue: How many steps may wait to be saved. A step that finds the
113
+ queue full is not saved, so the model never waits on saving.
114
+ """
115
+
116
+ enabled: bool = False
117
+ video_codec: str = "h264"
118
+ video_preset: str = "veryfast"
119
+ video_crf: int = 23
120
+ audio_codec: str = "aac"
121
+ audio_bitrate_kbps: int = 128
122
+ queue: int = 8
123
+
124
+
95
125
  @dataclass(frozen=True)
96
126
  class RuntimeConfig:
97
127
  """The single configuration object threaded through ``serve``.
@@ -116,6 +146,7 @@ class RuntimeConfig:
116
146
  session drain it includes, before the process is forced to exit.
117
147
  orphan_timeout: Seconds a session may stay client-less before it closes.
118
148
  recording: The recorder's configuration; disabled by default.
149
+ step_results: How each finished step is saved; disabled by default.
119
150
  """
120
151
 
121
152
  model_ref: str
@@ -127,3 +158,4 @@ class RuntimeConfig:
127
158
  exit_timeout: float = 10.0
128
159
  orphan_timeout: float = 60.0
129
160
  recording: RecordingConfig = field(default_factory=RecordingConfig)
161
+ step_results: StepResultsConfig = field(default_factory=StepResultsConfig)
@@ -75,9 +75,10 @@ class SessionEvent(Enum):
75
75
  one phase otherwise silent, since the runner emits nothing until it leaves
76
76
  ``CREATED`` on ``INITIALIZATION_SUCCESS``.
77
77
 
78
- ``CHUNK_READY``, ``CLIP_READY``, ``COMMAND``, ``ERROR``, and ``METRIC`` are
79
- the journal-only events (:data:`JOURNAL_EVENTS`): facts recorded for an
80
- external consumer rather than moves of the lifecycle. Each is a pure
78
+ ``CHUNK_READY``, ``CLIP_READY``, ``COMMAND``, ``ERROR``, ``METRIC``,
79
+ ``STEP_COMPLETED``, and ``STEP_RESULT_READY`` are the journal-only events
80
+ (:data:`JOURNAL_EVENTS`): facts recorded for an external consumer rather
81
+ than moves of the lifecycle. Each is a pure
81
82
  self-loop legal in **every** state — a journal fact must never be silently
82
83
  dropped by the transition table (the final ``CHUNK_READY`` fires during
83
84
  ``CLOSING``; an ``ERROR`` can fire while ``ORPHANED``) — that changes no
@@ -101,6 +102,8 @@ class SessionEvent(Enum):
101
102
  COMMAND = auto()
102
103
  ERROR = auto()
103
104
  METRIC = auto()
105
+ STEP_COMPLETED = auto()
106
+ STEP_RESULT_READY = auto()
104
107
 
105
108
 
106
109
  JOURNAL_EVENTS: frozenset[SessionEvent] = frozenset(
@@ -110,6 +113,8 @@ JOURNAL_EVENTS: frozenset[SessionEvent] = frozenset(
110
113
  SessionEvent.COMMAND,
111
114
  SessionEvent.ERROR,
112
115
  SessionEvent.METRIC,
116
+ SessionEvent.STEP_COMPLETED,
117
+ SessionEvent.STEP_RESULT_READY,
113
118
  }
114
119
  )
115
120
  """The journal-only events: pure self-loops in every state, payload in ``detail``."""
@@ -13,6 +13,7 @@ from collections.abc import Mapping
13
13
  from typing import Protocol, runtime_checkable
14
14
 
15
15
  from reactor_runtime.core.values import (
16
+ ClientStatsBatch,
16
17
  ConnectionCapabilities,
17
18
  ConnId,
18
19
  InputFrame,
@@ -193,6 +194,16 @@ class ConnectionSink(Protocol):
193
194
  timeline, correlated by *request_id*.
194
195
  """
195
196
 
197
+ def client_stats_received(self, conn_id: ConnId, batch: ClientStatsBatch) -> None:
198
+ """Hand up a batch of client-observed quality samples for one connection.
199
+
200
+ The client is the only vantage point onto its own receive-side
201
+ quality, so a transport reports the batch as decoded rather than
202
+ validating it. The connection identity comes from *conn_id*, which the
203
+ transport already holds — never from anything the payload itself
204
+ might claim, because the payload carries none.
205
+ """
206
+
196
207
  def connection_answered(self, conn_id: ConnId, answer: Mapping[str, str]) -> None:
197
208
  """Relay the transport's negotiation answer for one connection.
198
209
 
@@ -9,7 +9,7 @@ the import graph.
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
- from collections.abc import Iterable
12
+ from collections.abc import Iterable, Mapping, Sequence
13
13
  from dataclasses import dataclass, field
14
14
  from enum import Enum, StrEnum
15
15
  from typing import Any, NewType
@@ -44,6 +44,95 @@ class CommandFailure:
44
44
  message: str
45
45
 
46
46
 
47
+ @dataclass(frozen=True)
48
+ class ClientTrackStat:
49
+ """One WebRTC quality reading a client took for one of its tracks.
50
+
51
+ Carries the client's own view of its receive-side quality — bitrate, frame
52
+ rate, packet loss, jitter, round-trip time — sourced from the browser's own
53
+ ``getStats()``, which is the only vantage point onto that side of the
54
+ connection. Values are taken as reported; the runtime attaches the
55
+ connection identity itself rather than trusting anything the payload might
56
+ claim, because the payload carries none.
57
+
58
+ ``kind``/``direction``/``codec`` are fixed, structural facts about the
59
+ track — never a new value at runtime — so they stay their own fields;
60
+ :attr:`metrics` is where the actual measurements live, because that set
61
+ grows as new stats become worth reporting, without a schema change.
62
+
63
+ Attributes:
64
+ timestamp: When the client took this reading, on its own clock, in
65
+ epoch milliseconds.
66
+ track_name: The track the reading is for.
67
+ kind: The track's kind; ``None`` when the client did not say.
68
+ direction: The track's direction from the client's own perspective;
69
+ ``None`` when the client did not say.
70
+ codec: The negotiated codec, e.g. ``"VP9"``, ``"opus"`` — empty when
71
+ the browser hasn't reported it yet.
72
+ paused: Whether the client has this track paused right now.
73
+ metrics: The reading's measurements, by name — e.g.
74
+ ``"bitrate_bps"``, ``"frames_per_second"``, ``"packets_lost"``,
75
+ ``"packets_received"``, ``"jitter_ms"``, ``"round_trip_time_ms"``,
76
+ ``"frames_decoded"``, ``"frames_dropped"``, ``"frame_width"``,
77
+ ``"frame_height"``, ``"nack_count"``, ``"keyframe_requests"``.
78
+ A cumulative counter (e.g. ``packets_lost``) rides here as a raw
79
+ count, not a ratio computed once elsewhere — a ratio from two
80
+ readings with different totals cannot be re-aggregated correctly
81
+ afterward; a raw count can always be summed and divided once, at
82
+ query time.
83
+ """
84
+
85
+ timestamp: int
86
+ track_name: str
87
+ kind: TrackKind | None
88
+ direction: ClientTrackDirection | None
89
+ codec: str
90
+ paused: bool
91
+ metrics: Mapping[str, float]
92
+
93
+
94
+ @dataclass(frozen=True)
95
+ class ClientConnectionStat:
96
+ """A connection-wide reading, not tied to any one track.
97
+
98
+ e.g. ``"available_outgoing_bitrate_bps"`` (the client engine's own
99
+ estimate of outgoing headroom — the encoder's own belief, not a
100
+ measurement of the user's internet) or ``"time_to_connect_ms"`` (how long
101
+ this connection waited to establish, start to "ready") — the latter a
102
+ one-time fact a client reports once rather than repeating on every batch,
103
+ which is why :class:`ClientStatsBatch` carries this as optional.
104
+
105
+ Attributes:
106
+ timestamp: When the client took this reading, on its own clock, in
107
+ epoch milliseconds.
108
+ metrics: The reading's measurements, by name.
109
+ """
110
+
111
+ timestamp: int
112
+ metrics: Mapping[str, float]
113
+
114
+
115
+ @dataclass(frozen=True)
116
+ class ClientStatsBatch:
117
+ """A batch of client-observed quality readings, plus connection-wide facts.
118
+
119
+ Mirrors the wire's ``ClientStats`` message: per-track readings alongside
120
+ a fact set that applies to the whole connection rather than any one
121
+ track. A client typically sends ``connection_stat`` once, on its first
122
+ batch after connecting, and omits it on every batch after — most batches
123
+ therefore carry ``track_stats`` only.
124
+
125
+ Attributes:
126
+ track_stats: One reading per track this batch reports on. Empty on a
127
+ batch that carries only ``connection_stat``.
128
+ connection_stat: This batch's connection-wide reading, or ``None`` on
129
+ a batch that doesn't carry one — the common case.
130
+ """
131
+
132
+ track_stats: Sequence[ClientTrackStat]
133
+ connection_stat: ClientConnectionStat | None
134
+
135
+
47
136
  @dataclass(frozen=True, eq=False)
48
137
  class InputFrame:
49
138
  """A single inbound media frame with its presentation timestamp.
@@ -103,6 +192,18 @@ class TrackDirection(StrEnum):
103
192
  OUT = "out"
104
193
 
105
194
 
195
+ class ClientTrackDirection(StrEnum):
196
+ """Direction of flow for a track, from the client's perspective.
197
+
198
+ The opposite polarity of :class:`TrackDirection`: a model output track
199
+ (``OUT``) is one the client receives (``RECVONLY``), and a model input
200
+ track (``IN``) is one the client sends (``SENDONLY``).
201
+ """
202
+
203
+ RECVONLY = "recvonly"
204
+ SENDONLY = "sendonly"
205
+
206
+
106
207
  @dataclass(frozen=True)
107
208
  class TrackInfo:
108
209
  """Immutable metadata describing a single media track.
@@ -321,6 +422,36 @@ class MediaChunk:
321
422
  return split_batch(self.bundle)
322
423
 
323
424
 
425
+ @dataclass(frozen=True)
426
+ class CompletedStep:
427
+ """One finished step, as the model reports it to the runtime.
428
+
429
+ The neutral form of a model's step report: the media as a bundle rather
430
+ than the model's own output class, and the extra files as bytes, so the
431
+ runtime can count, journal, and save the step without touching model code.
432
+
433
+ Attributes:
434
+ bundle: The media the step produced, or ``None`` when it produced none.
435
+ files: Extra files the model keeps with the step, keyed by file name.
436
+ error: Why the step failed, or ``None`` when it worked.
437
+ elapsed: Wall-clock seconds the step took, when measured.
438
+ session: The session the step belongs to, as the number of session
439
+ starts the model had received when the step began. The runtime
440
+ counts the session starts it posts the same way, so it can tell a
441
+ late report from an earlier session apart from the current one.
442
+ fps: The rate the step's media was emitted at, which a saved step's
443
+ video plays at: the measured throughput for a model that does not
444
+ pin its rate.
445
+ """
446
+
447
+ bundle: MediaBundle | None
448
+ files: Mapping[str, bytes] = field(default_factory=dict)
449
+ error: str | None = None
450
+ elapsed: float | None = None
451
+ session: int = 0
452
+ fps: float = 30.0
453
+
454
+
324
455
  @dataclass(frozen=True)
325
456
  class ConnectionCapabilities:
326
457
  """What media a connection's wire can carry.
@@ -11,6 +11,7 @@ from reactor_runtime.http.routes import (
11
11
  EgressRoutes,
12
12
  RecordingRoutes,
13
13
  SessionRoutes,
14
+ StepRoutes,
14
15
  UploadRoutes,
15
16
  )
16
17
  from reactor_runtime.http.server import HttpServer
@@ -20,5 +21,6 @@ __all__ = [
20
21
  "HttpServer",
21
22
  "RecordingRoutes",
22
23
  "SessionRoutes",
24
+ "StepRoutes",
23
25
  "UploadRoutes",
24
26
  ]