openrtc 0.2.2__tar.gz → 0.2.3__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 (152) hide show
  1. {openrtc-0.2.2 → openrtc-0.2.3}/PKG-INFO +71 -1
  2. {openrtc-0.2.2 → openrtc-0.2.3}/README.md +70 -0
  3. {openrtc-0.2.2 → openrtc-0.2.3}/docs/changelog.md +19 -4
  4. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/execution/prewarm.py +18 -0
  5. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/observability/metrics.py +37 -0
  6. openrtc-0.2.3/tests/test_savings_readout.py +76 -0
  7. {openrtc-0.2.2 → openrtc-0.2.3}/.coderabbit.yaml +0 -0
  8. {openrtc-0.2.2 → openrtc-0.2.3}/.editorconfig +0 -0
  9. {openrtc-0.2.2 → openrtc-0.2.3}/.env.example +0 -0
  10. {openrtc-0.2.2 → openrtc-0.2.3}/.github/FUNDING.yml +0 -0
  11. {openrtc-0.2.2 → openrtc-0.2.3}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  12. {openrtc-0.2.2 → openrtc-0.2.3}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  13. {openrtc-0.2.2 → openrtc-0.2.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  14. {openrtc-0.2.2 → openrtc-0.2.3}/.github/dependabot.yml +0 -0
  15. {openrtc-0.2.2 → openrtc-0.2.3}/.github/workflows/audit.yml +0 -0
  16. {openrtc-0.2.2 → openrtc-0.2.3}/.github/workflows/bench.yml +0 -0
  17. {openrtc-0.2.2 → openrtc-0.2.3}/.github/workflows/build.yml +0 -0
  18. {openrtc-0.2.2 → openrtc-0.2.3}/.github/workflows/canary.yml +0 -0
  19. {openrtc-0.2.2 → openrtc-0.2.3}/.github/workflows/deploy-docs.yml +0 -0
  20. {openrtc-0.2.2 → openrtc-0.2.3}/.github/workflows/docs.yml +0 -0
  21. {openrtc-0.2.2 → openrtc-0.2.3}/.github/workflows/integration.yml +0 -0
  22. {openrtc-0.2.2 → openrtc-0.2.3}/.github/workflows/lint.yml +0 -0
  23. {openrtc-0.2.2 → openrtc-0.2.3}/.github/workflows/publish.yml +0 -0
  24. {openrtc-0.2.2 → openrtc-0.2.3}/.github/workflows/test.yml +0 -0
  25. {openrtc-0.2.2 → openrtc-0.2.3}/.gitignore +0 -0
  26. {openrtc-0.2.2 → openrtc-0.2.3}/.pre-commit-config.yaml +0 -0
  27. {openrtc-0.2.2 → openrtc-0.2.3}/AGENTS.md +0 -0
  28. {openrtc-0.2.2 → openrtc-0.2.3}/CLAUDE.md +0 -0
  29. {openrtc-0.2.2 → openrtc-0.2.3}/CONTRIBUTING.md +0 -0
  30. {openrtc-0.2.2 → openrtc-0.2.3}/LICENSE +0 -0
  31. {openrtc-0.2.2 → openrtc-0.2.3}/Makefile +0 -0
  32. {openrtc-0.2.2 → openrtc-0.2.3}/assets/banner.png +0 -0
  33. {openrtc-0.2.2 → openrtc-0.2.3}/assets/logo.png +0 -0
  34. {openrtc-0.2.2 → openrtc-0.2.3}/codecov.yml +0 -0
  35. {openrtc-0.2.2 → openrtc-0.2.3}/docker-compose.test.yml +0 -0
  36. {openrtc-0.2.2 → openrtc-0.2.3}/docs/.vitepress/config.ts +0 -0
  37. {openrtc-0.2.2 → openrtc-0.2.3}/docs/.vitepress/theme/custom.css +0 -0
  38. {openrtc-0.2.2 → openrtc-0.2.3}/docs/.vitepress/theme/index.ts +0 -0
  39. {openrtc-0.2.2 → openrtc-0.2.3}/docs/api/pool.md +0 -0
  40. {openrtc-0.2.2 → openrtc-0.2.3}/docs/audit-2026-05-02.md +0 -0
  41. {openrtc-0.2.2 → openrtc-0.2.3}/docs/benchmarks/density-v0.1.md +0 -0
  42. {openrtc-0.2.2 → openrtc-0.2.3}/docs/cli.md +0 -0
  43. {openrtc-0.2.2 → openrtc-0.2.3}/docs/concepts/architecture.md +0 -0
  44. {openrtc-0.2.2 → openrtc-0.2.3}/docs/deployment/github-pages.md +0 -0
  45. {openrtc-0.2.2 → openrtc-0.2.3}/docs/design/agent-server-integration.md +0 -0
  46. {openrtc-0.2.2 → openrtc-0.2.3}/docs/design/job-executor-protocol.md +0 -0
  47. {openrtc-0.2.2 → openrtc-0.2.3}/docs/design/proc-pool-surface.md +0 -0
  48. {openrtc-0.2.2 → openrtc-0.2.3}/docs/design/v0.1.md +0 -0
  49. {openrtc-0.2.2 → openrtc-0.2.3}/docs/examples.md +0 -0
  50. {openrtc-0.2.2 → openrtc-0.2.3}/docs/getting-started.md +0 -0
  51. {openrtc-0.2.2 → openrtc-0.2.3}/docs/index.md +0 -0
  52. {openrtc-0.2.2 → openrtc-0.2.3}/docs/package-lock.json +0 -0
  53. {openrtc-0.2.2 → openrtc-0.2.3}/docs/package.json +0 -0
  54. {openrtc-0.2.2 → openrtc-0.2.3}/docs/public/banner.png +0 -0
  55. {openrtc-0.2.2 → openrtc-0.2.3}/docs/public/logo.png +0 -0
  56. {openrtc-0.2.2 → openrtc-0.2.3}/docs/public/logo.svg +0 -0
  57. {openrtc-0.2.2 → openrtc-0.2.3}/docs/release-v0.1.md +0 -0
  58. {openrtc-0.2.2 → openrtc-0.2.3}/examples/agents/dental.py +0 -0
  59. {openrtc-0.2.2 → openrtc-0.2.3}/examples/agents/restaurant.py +0 -0
  60. {openrtc-0.2.2 → openrtc-0.2.3}/examples/density_demo.py +0 -0
  61. {openrtc-0.2.2 → openrtc-0.2.3}/examples/frontend/.dockerignore +0 -0
  62. {openrtc-0.2.2 → openrtc-0.2.3}/examples/frontend/.env.example +0 -0
  63. {openrtc-0.2.2 → openrtc-0.2.3}/examples/frontend/.gitignore +0 -0
  64. {openrtc-0.2.2 → openrtc-0.2.3}/examples/frontend/Dockerfile +0 -0
  65. {openrtc-0.2.2 → openrtc-0.2.3}/examples/frontend/README.md +0 -0
  66. {openrtc-0.2.2 → openrtc-0.2.3}/examples/frontend/app/app.css +0 -0
  67. {openrtc-0.2.2 → openrtc-0.2.3}/examples/frontend/app/components/agents-ui/agent-audio-visualizer-wave.tsx +0 -0
  68. {openrtc-0.2.2 → openrtc-0.2.3}/examples/frontend/app/components/agents-ui/agent-chat-transcript.tsx +0 -0
  69. {openrtc-0.2.2 → openrtc-0.2.3}/examples/frontend/app/components/agents-ui/agent-session-provider.tsx +0 -0
  70. {openrtc-0.2.2 → openrtc-0.2.3}/examples/frontend/app/components/demo-call-page.tsx +0 -0
  71. {openrtc-0.2.2 → openrtc-0.2.3}/examples/frontend/app/root.tsx +0 -0
  72. {openrtc-0.2.2 → openrtc-0.2.3}/examples/frontend/app/routes/api.token.ts +0 -0
  73. {openrtc-0.2.2 → openrtc-0.2.3}/examples/frontend/app/routes/dentist.tsx +0 -0
  74. {openrtc-0.2.2 → openrtc-0.2.3}/examples/frontend/app/routes/home.tsx +0 -0
  75. {openrtc-0.2.2 → openrtc-0.2.3}/examples/frontend/app/routes/restaurant.tsx +0 -0
  76. {openrtc-0.2.2 → openrtc-0.2.3}/examples/frontend/app/routes.ts +0 -0
  77. {openrtc-0.2.2 → openrtc-0.2.3}/examples/frontend/app/welcome/logo-dark.svg +0 -0
  78. {openrtc-0.2.2 → openrtc-0.2.3}/examples/frontend/app/welcome/logo-light.svg +0 -0
  79. {openrtc-0.2.2 → openrtc-0.2.3}/examples/frontend/app/welcome/welcome.tsx +0 -0
  80. {openrtc-0.2.2 → openrtc-0.2.3}/examples/frontend/package-lock.json +0 -0
  81. {openrtc-0.2.2 → openrtc-0.2.3}/examples/frontend/package.json +0 -0
  82. {openrtc-0.2.2 → openrtc-0.2.3}/examples/frontend/public/favicon.ico +0 -0
  83. {openrtc-0.2.2 → openrtc-0.2.3}/examples/frontend/react-router.config.ts +0 -0
  84. {openrtc-0.2.2 → openrtc-0.2.3}/examples/frontend/tsconfig.json +0 -0
  85. {openrtc-0.2.2 → openrtc-0.2.3}/examples/frontend/vite.config.ts +0 -0
  86. {openrtc-0.2.2 → openrtc-0.2.3}/examples/main.py +0 -0
  87. {openrtc-0.2.2 → openrtc-0.2.3}/pyproject.toml +0 -0
  88. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/__init__.py +0 -0
  89. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/cli/__init__.py +0 -0
  90. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/cli/commands.py +0 -0
  91. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/cli/dashboard.py +0 -0
  92. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/cli/entry.py +0 -0
  93. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/cli/livekit.py +0 -0
  94. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/cli/params.py +0 -0
  95. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/cli/reporter.py +0 -0
  96. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/cli/types.py +0 -0
  97. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/core/__init__.py +0 -0
  98. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/core/config.py +0 -0
  99. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/core/discovery.py +0 -0
  100. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/core/pool.py +0 -0
  101. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/core/routing.py +0 -0
  102. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/core/serialization.py +0 -0
  103. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/core/turn_handling.py +0 -0
  104. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/execution/__init__.py +0 -0
  105. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/execution/coroutine.py +0 -0
  106. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/execution/coroutine_server.py +0 -0
  107. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/execution/file_watcher.py +0 -0
  108. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/observability/__init__.py +0 -0
  109. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/observability/snapshot.py +0 -0
  110. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/observability/stream.py +0 -0
  111. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/py.typed +0 -0
  112. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/tui/__init__.py +0 -0
  113. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/tui/app.py +0 -0
  114. {openrtc-0.2.2 → openrtc-0.2.3}/src/openrtc/types.py +0 -0
  115. {openrtc-0.2.2 → openrtc-0.2.3}/tests/benchmarks/__init__.py +0 -0
  116. {openrtc-0.2.2 → openrtc-0.2.3}/tests/benchmarks/density.py +0 -0
  117. {openrtc-0.2.2 → openrtc-0.2.3}/tests/benchmarks/throughput.py +0 -0
  118. {openrtc-0.2.2 → openrtc-0.2.3}/tests/conftest.py +0 -0
  119. {openrtc-0.2.2 → openrtc-0.2.3}/tests/execution/__init__.py +0 -0
  120. {openrtc-0.2.2 → openrtc-0.2.3}/tests/execution/test_file_watcher.py +0 -0
  121. {openrtc-0.2.2 → openrtc-0.2.3}/tests/execution/test_file_watcher_smoke.py +0 -0
  122. {openrtc-0.2.2 → openrtc-0.2.3}/tests/integration/README.md +0 -0
  123. {openrtc-0.2.2 → openrtc-0.2.3}/tests/integration/__init__.py +0 -0
  124. {openrtc-0.2.2 → openrtc-0.2.3}/tests/integration/conftest.py +0 -0
  125. {openrtc-0.2.2 → openrtc-0.2.3}/tests/integration/test_concurrent_real_calls.py +0 -0
  126. {openrtc-0.2.2 → openrtc-0.2.3}/tests/integration/test_coroutine_realroom.py +0 -0
  127. {openrtc-0.2.2 → openrtc-0.2.3}/tests/integration/test_dev_server_fixture.py +0 -0
  128. {openrtc-0.2.2 → openrtc-0.2.3}/tests/test_cli.py +0 -0
  129. {openrtc-0.2.2 → openrtc-0.2.3}/tests/test_cli_optional_extra_integration.py +0 -0
  130. {openrtc-0.2.2 → openrtc-0.2.3}/tests/test_cli_params.py +0 -0
  131. {openrtc-0.2.2 → openrtc-0.2.3}/tests/test_config.py +0 -0
  132. {openrtc-0.2.2 → openrtc-0.2.3}/tests/test_coroutine_backpressure.py +0 -0
  133. {openrtc-0.2.2 → openrtc-0.2.3}/tests/test_coroutine_coverage.py +0 -0
  134. {openrtc-0.2.2 → openrtc-0.2.3}/tests/test_coroutine_drain.py +0 -0
  135. {openrtc-0.2.2 → openrtc-0.2.3}/tests/test_coroutine_isolation.py +0 -0
  136. {openrtc-0.2.2 → openrtc-0.2.3}/tests/test_coroutine_job_context.py +0 -0
  137. {openrtc-0.2.2 → openrtc-0.2.3}/tests/test_coroutine_lifecycle.py +0 -0
  138. {openrtc-0.2.2 → openrtc-0.2.3}/tests/test_coroutine_server.py +0 -0
  139. {openrtc-0.2.2 → openrtc-0.2.3}/tests/test_coroutine_skeleton.py +0 -0
  140. {openrtc-0.2.2 → openrtc-0.2.3}/tests/test_coroutine_smoke.py +0 -0
  141. {openrtc-0.2.2 → openrtc-0.2.3}/tests/test_dashboard.py +0 -0
  142. {openrtc-0.2.2 → openrtc-0.2.3}/tests/test_discovery.py +0 -0
  143. {openrtc-0.2.2 → openrtc-0.2.3}/tests/test_isolation_process_parity.py +0 -0
  144. {openrtc-0.2.2 → openrtc-0.2.3}/tests/test_metrics_stream.py +0 -0
  145. {openrtc-0.2.2 → openrtc-0.2.3}/tests/test_pool.py +0 -0
  146. {openrtc-0.2.2 → openrtc-0.2.3}/tests/test_resources.py +0 -0
  147. {openrtc-0.2.2 → openrtc-0.2.3}/tests/test_routing.py +0 -0
  148. {openrtc-0.2.2 → openrtc-0.2.3}/tests/test_serialization.py +0 -0
  149. {openrtc-0.2.2 → openrtc-0.2.3}/tests/test_throughput_bench.py +0 -0
  150. {openrtc-0.2.2 → openrtc-0.2.3}/tests/test_tui_app.py +0 -0
  151. {openrtc-0.2.2 → openrtc-0.2.3}/tests/test_turn_handling.py +0 -0
  152. {openrtc-0.2.2 → openrtc-0.2.3}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openrtc
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Run multiple LiveKit voice agents in a single shared worker process.
5
5
  Project-URL: Homepage, https://github.com/mahimailabs/openrtc
6
6
  Project-URL: Repository, https://github.com/mahimailabs/openrtc
@@ -203,6 +203,76 @@ If a module has no `@agent_config`, the agent name defaults to the filename stem
203
203
 
204
204
  Discovered agents work with `livekit dev` and spawn-based workers on macOS. For `add()`, define agent classes at module scope so worker reload can import them.
205
205
 
206
+ ## Migrating from livekit-agents
207
+
208
+ Already running one or more `livekit-agents` workers? Each is its own process that
209
+ loads the same VAD and turn-detector models. Collapse them into one `AgentPool`
210
+ worker without changing your agents.
211
+
212
+ **Before** (one worker per agent, N processes):
213
+
214
+ ```python
215
+ # restaurant_worker.py (plus a near-identical dental_worker.py, support_worker.py, ...)
216
+ from livekit import agents
217
+ from livekit.agents import Agent, AgentSession
218
+ from livekit.plugins import openai, silero
219
+
220
+
221
+ class RestaurantAgent(Agent):
222
+ def __init__(self) -> None:
223
+ super().__init__(instructions="You help callers book tables.")
224
+
225
+
226
+ async def entrypoint(ctx: agents.JobContext) -> None:
227
+ session = AgentSession(
228
+ stt=openai.STT(), llm=openai.LLM(), tts=openai.TTS(), vad=silero.VAD.load()
229
+ )
230
+ await session.start(agent=RestaurantAgent(), room=ctx.room)
231
+ await ctx.connect()
232
+
233
+
234
+ if __name__ == "__main__":
235
+ agents.cli.run_app(agents.WorkerOptions(entrypoint_fnc=entrypoint))
236
+ ```
237
+
238
+ **After** (one worker, N agents, one shared prewarm):
239
+
240
+ ```python
241
+ # worker.py
242
+ from livekit.agents import Agent
243
+ from livekit.plugins import openai
244
+ from openrtc import AgentPool
245
+
246
+
247
+ class RestaurantAgent(Agent): # unchanged
248
+ def __init__(self) -> None:
249
+ super().__init__(instructions="You help callers book tables.")
250
+
251
+
252
+ class DentalAgent(Agent): # unchanged
253
+ def __init__(self) -> None:
254
+ super().__init__(instructions="You help callers manage appointments.")
255
+
256
+
257
+ pool = AgentPool(default_stt=openai.STT(), default_llm=openai.LLM(), default_tts=openai.TTS())
258
+ pool.add("restaurant", RestaurantAgent)
259
+ pool.add("dental", DentalAgent)
260
+ pool.run()
261
+ ```
262
+
263
+ Your `Agent` subclasses, tools, and provider objects are unchanged. You delete the
264
+ per-worker boilerplate (`entrypoint`, `AgentSession` wiring, `cli.run_app`) and
265
+ register the agents on one pool; OpenRTC owns prewarm, routing, and per-call
266
+ session construction. On the first run the worker logs the win, for example:
267
+
268
+ ```text
269
+ OpenRTC: 2 agents in 1 worker (baseline ~410 MB). 2 separate livekit-agents
270
+ workers would cost ~820 MB; sharing one worker saves ~410 MB of idle baseline
271
+ (assumes equal per-worker baselines).
272
+ ```
273
+
274
+ See [Routing](#routing) for how each incoming call resolves to one registered agent.
275
+
206
276
  ## Memory: before and after
207
277
 
208
278
  Assume an illustrative **~400 MB** idle baseline per worker for the shared stack (VAD, turn detector, and similar). Your measured RSS will differ by provider, model, and OS.
@@ -171,6 +171,76 @@ If a module has no `@agent_config`, the agent name defaults to the filename stem
171
171
 
172
172
  Discovered agents work with `livekit dev` and spawn-based workers on macOS. For `add()`, define agent classes at module scope so worker reload can import them.
173
173
 
174
+ ## Migrating from livekit-agents
175
+
176
+ Already running one or more `livekit-agents` workers? Each is its own process that
177
+ loads the same VAD and turn-detector models. Collapse them into one `AgentPool`
178
+ worker without changing your agents.
179
+
180
+ **Before** (one worker per agent, N processes):
181
+
182
+ ```python
183
+ # restaurant_worker.py (plus a near-identical dental_worker.py, support_worker.py, ...)
184
+ from livekit import agents
185
+ from livekit.agents import Agent, AgentSession
186
+ from livekit.plugins import openai, silero
187
+
188
+
189
+ class RestaurantAgent(Agent):
190
+ def __init__(self) -> None:
191
+ super().__init__(instructions="You help callers book tables.")
192
+
193
+
194
+ async def entrypoint(ctx: agents.JobContext) -> None:
195
+ session = AgentSession(
196
+ stt=openai.STT(), llm=openai.LLM(), tts=openai.TTS(), vad=silero.VAD.load()
197
+ )
198
+ await session.start(agent=RestaurantAgent(), room=ctx.room)
199
+ await ctx.connect()
200
+
201
+
202
+ if __name__ == "__main__":
203
+ agents.cli.run_app(agents.WorkerOptions(entrypoint_fnc=entrypoint))
204
+ ```
205
+
206
+ **After** (one worker, N agents, one shared prewarm):
207
+
208
+ ```python
209
+ # worker.py
210
+ from livekit.agents import Agent
211
+ from livekit.plugins import openai
212
+ from openrtc import AgentPool
213
+
214
+
215
+ class RestaurantAgent(Agent): # unchanged
216
+ def __init__(self) -> None:
217
+ super().__init__(instructions="You help callers book tables.")
218
+
219
+
220
+ class DentalAgent(Agent): # unchanged
221
+ def __init__(self) -> None:
222
+ super().__init__(instructions="You help callers manage appointments.")
223
+
224
+
225
+ pool = AgentPool(default_stt=openai.STT(), default_llm=openai.LLM(), default_tts=openai.TTS())
226
+ pool.add("restaurant", RestaurantAgent)
227
+ pool.add("dental", DentalAgent)
228
+ pool.run()
229
+ ```
230
+
231
+ Your `Agent` subclasses, tools, and provider objects are unchanged. You delete the
232
+ per-worker boilerplate (`entrypoint`, `AgentSession` wiring, `cli.run_app`) and
233
+ register the agents on one pool; OpenRTC owns prewarm, routing, and per-call
234
+ session construction. On the first run the worker logs the win, for example:
235
+
236
+ ```text
237
+ OpenRTC: 2 agents in 1 worker (baseline ~410 MB). 2 separate livekit-agents
238
+ workers would cost ~820 MB; sharing one worker saves ~410 MB of idle baseline
239
+ (assumes equal per-worker baselines).
240
+ ```
241
+
242
+ See [Routing](#routing) for how each incoming call resolves to one registered agent.
243
+
174
244
  ## Memory: before and after
175
245
 
176
246
  Assume an illustrative **~400 MB** idle baseline per worker for the shared stack (VAD, turn detector, and similar). Your measured RSS will differ by provider, model, and OS.
@@ -147,12 +147,27 @@ contributor onboarding matches what's in the repo.
147
147
 
148
148
  <!-- releases -->
149
149
 
150
+ ## [0.2.2] - 2026-05-30
151
+
152
+ ### Fixed
153
+ - Coroutine mode now establishes the LiveKit job context for the session duration, so `get_job_context()` works inside agents and sessions and shutdown callbacks run (MAH-158).
154
+ - Coroutine sessions are held open until the call ends (room disconnect or `ctx.shutdown()`) instead of being marked SUCCESS when the entrypoint returns, so `max_concurrent_sessions` backpressure and runtime session counts are accurate (MAH-160).
155
+
156
+ ### Added
157
+ - Real-audio throughput benchmark (`tests/benchmarks/throughput.py`) reporting steady-state event-loop p99 vs session count, separating startup from steady state (MAH-163).
158
+ - `examples/density_demo.py`: a no-server demo comparing process-per-session vs coroutine-pool resident memory.
159
+
160
+ ### Changed
161
+ - The coroutine real-room integration test is now a correctness gate (job context plus no-failure); throughput moved to the dedicated benchmark.
162
+
163
+ ---
164
+
150
165
  ## [0.2.1] - 2026-05-06
151
166
 
152
- ## What's Changed
153
- * [v0.2.1] File watcher infrastructure for agent code (MAH-80) by @mahimairaja in https://github.com/mahimailabs/openrtc-runtime/pull/39
154
-
155
-
167
+ ## What's Changed
168
+ * [v0.2.1] File watcher infrastructure for agent code (MAH-80) by @mahimairaja in https://github.com/mahimailabs/openrtc-runtime/pull/39
169
+
170
+
156
171
  **Full Changelog**: https://github.com/mahimailabs/openrtc-runtime/compare/v0.1.0...v0.2.1
157
172
 
158
173
  ---
@@ -10,13 +10,21 @@ Adding a new shared resource means adding it to ``_prewarm_worker``.
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
+ import logging
13
14
  from typing import TYPE_CHECKING, Any
14
15
 
15
16
  from livekit.agents import JobProcess
16
17
 
18
+ from openrtc.observability.metrics import (
19
+ format_prewarm_savings,
20
+ process_resident_set_bytes,
21
+ )
22
+
17
23
  if TYPE_CHECKING:
18
24
  from openrtc.core.pool import _PoolRuntimeState
19
25
 
26
+ logger = logging.getLogger("openrtc.execution.prewarm")
27
+
20
28
 
21
29
  def _prewarm_worker(
22
30
  runtime_state: _PoolRuntimeState,
@@ -29,6 +37,16 @@ def _prewarm_worker(
29
37
  proc.userdata["vad"] = silero_module.VAD.load()
30
38
  proc.userdata["turn_detection_factory"] = turn_detector_model
31
39
 
40
+ # Day-one payoff: surface the fleet-collapse idle-baseline saving once per
41
+ # worker, post-prewarm (RSS is now the per-worker baseline), so existing
42
+ # livekit-agents users see the win on first run without --dashboard.
43
+ logger.info(
44
+ format_prewarm_savings(
45
+ agent_count=len(runtime_state.agents),
46
+ shared_worker_bytes=process_resident_set_bytes(),
47
+ )
48
+ )
49
+
32
50
 
33
51
  def _load_shared_runtime_dependencies() -> tuple[Any, type[Any]]:
34
52
  """Load the optional LiveKit runtime dependencies used during prewarm."""
@@ -352,6 +352,43 @@ def estimate_shared_worker_savings(
352
352
  )
353
353
 
354
354
 
355
+ def format_prewarm_savings(*, agent_count: int, shared_worker_bytes: int | None) -> str:
356
+ """One honest, human-readable line about the shared-worker idle-baseline win.
357
+
358
+ Emitted once per worker at prewarm so the fleet-collapse saving is visible on
359
+ first run. It claims only idle-baseline memory saved by hosting N per-agent
360
+ workers as one shared worker; it never implies per-session density or a speed
361
+ multiple, and it names its equal-baseline assumption whenever it shows a
362
+ number. Stays graceful when RSS is unavailable (e.g. Windows).
363
+ """
364
+ estimate = estimate_shared_worker_savings(
365
+ agent_count=agent_count, shared_worker_bytes=shared_worker_bytes
366
+ )
367
+ agents = "1 agent" if agent_count == 1 else f"{agent_count} agents"
368
+
369
+ if estimate.shared_worker_bytes is None or estimate.estimated_saved_bytes is None:
370
+ return (
371
+ f"OpenRTC: {agents} in 1 worker; per-worker memory estimate "
372
+ "unavailable on this platform."
373
+ )
374
+
375
+ baseline_mb = estimate.shared_worker_bytes / (1024 * 1024)
376
+ if agent_count <= 1:
377
+ return (
378
+ f"OpenRTC: {agents} in this worker (baseline ~{baseline_mb:.0f} MB). "
379
+ "Register more agents on the pool to amortize the shared prewarm."
380
+ )
381
+
382
+ separate_mb = (estimate.estimated_separate_workers_bytes or 0) / (1024 * 1024)
383
+ saved_mb = estimate.estimated_saved_bytes / (1024 * 1024)
384
+ return (
385
+ f"OpenRTC: {agents} in 1 worker (baseline ~{baseline_mb:.0f} MB). "
386
+ f"{agent_count} separate livekit-agents workers would cost "
387
+ f"~{separate_mb:.0f} MB; sharing one worker saves ~{saved_mb:.0f} MB "
388
+ "of idle baseline (assumes equal per-worker baselines)."
389
+ )
390
+
391
+
355
392
  def _linux_rss_bytes() -> int | None:
356
393
  """Read VmRSS (kiB in procfs) and convert to bytes."""
357
394
  try:
@@ -0,0 +1,76 @@
1
+ """Day-one savings readout: the format_prewarm_savings line and its prewarm emit.
2
+
3
+ The readout makes the fleet-collapse idle-baseline win visible on first run. It
4
+ claims only idle-baseline memory saved (never per-session density or a speed
5
+ multiple) and stays graceful when RSS is unavailable.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from types import SimpleNamespace
12
+
13
+ import pytest
14
+
15
+ from openrtc.execution import prewarm as prewarm_module
16
+ from openrtc.execution.prewarm import _prewarm_worker
17
+ from openrtc.observability.metrics import format_prewarm_savings
18
+
19
+ _MB = 1024 * 1024
20
+
21
+
22
+ def test_savings_line_for_multiple_agents() -> None:
23
+ line = format_prewarm_savings(agent_count=3, shared_worker_bytes=400 * _MB)
24
+ assert "3 agents" in line
25
+ assert "400 MB" in line # shared baseline
26
+ assert "1200 MB" in line # 3 separate workers
27
+ assert "800 MB" in line # idle baseline saved: (3 - 1) * 400
28
+ assert "assumes" in line.lower()
29
+ assert "saves" in line.lower()
30
+
31
+
32
+ def test_neutral_line_for_single_agent() -> None:
33
+ line = format_prewarm_savings(agent_count=1, shared_worker_bytes=400 * _MB)
34
+ assert "1 agent" in line
35
+ assert "saves" not in line.lower() # no boast for a single agent
36
+ assert "amortize" in line.lower()
37
+
38
+
39
+ def test_graceful_when_rss_unavailable() -> None:
40
+ line = format_prewarm_savings(agent_count=3, shared_worker_bytes=None)
41
+ assert "3 agents" in line
42
+ assert "unavailable" in line.lower()
43
+ assert "MB" not in line # no fabricated number
44
+
45
+
46
+ def test_prewarm_emits_one_savings_line(
47
+ monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
48
+ ) -> None:
49
+ class _FakeVAD:
50
+ @staticmethod
51
+ def load() -> str:
52
+ return "vad"
53
+
54
+ class _FakeSilero:
55
+ VAD = _FakeVAD
56
+
57
+ class _FakeTurnDetector:
58
+ pass
59
+
60
+ monkeypatch.setattr(
61
+ prewarm_module,
62
+ "_load_shared_runtime_dependencies",
63
+ lambda: (_FakeSilero, _FakeTurnDetector),
64
+ )
65
+ runtime_state = SimpleNamespace(agents={"a": object(), "b": object()})
66
+ proc = SimpleNamespace(userdata={}, inference_executor=None)
67
+
68
+ with caplog.at_level(logging.INFO, logger="openrtc.execution.prewarm"):
69
+ _prewarm_worker(runtime_state, proc) # type: ignore[arg-type]
70
+
71
+ records = [r for r in caplog.records if r.name == "openrtc.execution.prewarm"]
72
+ assert len(records) == 1
73
+ message = records[0].getMessage()
74
+ assert message.startswith("OpenRTC:")
75
+ assert "2 agents" in message
76
+ assert "saves" in message.lower() # RSS is available in-process, so a number
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes