openrtc 0.2.2__tar.gz → 0.3.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 (157) hide show
  1. {openrtc-0.2.2 → openrtc-0.3.0}/PKG-INFO +92 -1
  2. {openrtc-0.2.2 → openrtc-0.3.0}/README.md +91 -0
  3. {openrtc-0.2.2 → openrtc-0.3.0}/docs/changelog.md +36 -5
  4. openrtc-0.3.0/docs/superpowers/plans/2026-06-08-session-observer-protocol.md +954 -0
  5. openrtc-0.3.0/docs/superpowers/specs/2026-06-08-session-observer-protocol-design.md +252 -0
  6. {openrtc-0.2.2 → openrtc-0.3.0}/src/openrtc/__init__.py +10 -0
  7. {openrtc-0.2.2 → openrtc-0.3.0}/src/openrtc/core/pool.py +63 -2
  8. {openrtc-0.2.2 → openrtc-0.3.0}/src/openrtc/execution/prewarm.py +18 -0
  9. openrtc-0.3.0/src/openrtc/observability/__init__.py +13 -0
  10. {openrtc-0.2.2 → openrtc-0.3.0}/src/openrtc/observability/metrics.py +37 -0
  11. openrtc-0.3.0/src/openrtc/observability/observer.py +224 -0
  12. openrtc-0.3.0/tests/test_savings_readout.py +76 -0
  13. openrtc-0.3.0/tests/test_session_observer.py +421 -0
  14. openrtc-0.2.2/tests/integration/__init__.py +0 -0
  15. {openrtc-0.2.2 → openrtc-0.3.0}/.coderabbit.yaml +0 -0
  16. {openrtc-0.2.2 → openrtc-0.3.0}/.editorconfig +0 -0
  17. {openrtc-0.2.2 → openrtc-0.3.0}/.env.example +0 -0
  18. {openrtc-0.2.2 → openrtc-0.3.0}/.github/FUNDING.yml +0 -0
  19. {openrtc-0.2.2 → openrtc-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  20. {openrtc-0.2.2 → openrtc-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  21. {openrtc-0.2.2 → openrtc-0.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  22. {openrtc-0.2.2 → openrtc-0.3.0}/.github/dependabot.yml +0 -0
  23. {openrtc-0.2.2 → openrtc-0.3.0}/.github/workflows/audit.yml +0 -0
  24. {openrtc-0.2.2 → openrtc-0.3.0}/.github/workflows/bench.yml +0 -0
  25. {openrtc-0.2.2 → openrtc-0.3.0}/.github/workflows/build.yml +0 -0
  26. {openrtc-0.2.2 → openrtc-0.3.0}/.github/workflows/canary.yml +0 -0
  27. {openrtc-0.2.2 → openrtc-0.3.0}/.github/workflows/deploy-docs.yml +0 -0
  28. {openrtc-0.2.2 → openrtc-0.3.0}/.github/workflows/docs.yml +0 -0
  29. {openrtc-0.2.2 → openrtc-0.3.0}/.github/workflows/integration.yml +0 -0
  30. {openrtc-0.2.2 → openrtc-0.3.0}/.github/workflows/lint.yml +0 -0
  31. {openrtc-0.2.2 → openrtc-0.3.0}/.github/workflows/publish.yml +0 -0
  32. {openrtc-0.2.2 → openrtc-0.3.0}/.github/workflows/test.yml +0 -0
  33. {openrtc-0.2.2 → openrtc-0.3.0}/.gitignore +0 -0
  34. {openrtc-0.2.2 → openrtc-0.3.0}/.pre-commit-config.yaml +0 -0
  35. {openrtc-0.2.2 → openrtc-0.3.0}/AGENTS.md +0 -0
  36. {openrtc-0.2.2 → openrtc-0.3.0}/CLAUDE.md +0 -0
  37. {openrtc-0.2.2 → openrtc-0.3.0}/CONTRIBUTING.md +0 -0
  38. {openrtc-0.2.2 → openrtc-0.3.0}/LICENSE +0 -0
  39. {openrtc-0.2.2 → openrtc-0.3.0}/Makefile +0 -0
  40. {openrtc-0.2.2 → openrtc-0.3.0}/assets/banner.png +0 -0
  41. {openrtc-0.2.2 → openrtc-0.3.0}/assets/logo.png +0 -0
  42. {openrtc-0.2.2 → openrtc-0.3.0}/codecov.yml +0 -0
  43. {openrtc-0.2.2 → openrtc-0.3.0}/docker-compose.test.yml +0 -0
  44. {openrtc-0.2.2 → openrtc-0.3.0}/docs/.vitepress/config.ts +0 -0
  45. {openrtc-0.2.2 → openrtc-0.3.0}/docs/.vitepress/theme/custom.css +0 -0
  46. {openrtc-0.2.2 → openrtc-0.3.0}/docs/.vitepress/theme/index.ts +0 -0
  47. {openrtc-0.2.2 → openrtc-0.3.0}/docs/api/pool.md +0 -0
  48. {openrtc-0.2.2 → openrtc-0.3.0}/docs/audit-2026-05-02.md +0 -0
  49. {openrtc-0.2.2 → openrtc-0.3.0}/docs/benchmarks/density-v0.1.md +0 -0
  50. {openrtc-0.2.2 → openrtc-0.3.0}/docs/cli.md +0 -0
  51. {openrtc-0.2.2 → openrtc-0.3.0}/docs/concepts/architecture.md +0 -0
  52. {openrtc-0.2.2 → openrtc-0.3.0}/docs/deployment/github-pages.md +0 -0
  53. {openrtc-0.2.2 → openrtc-0.3.0}/docs/design/agent-server-integration.md +0 -0
  54. {openrtc-0.2.2 → openrtc-0.3.0}/docs/design/job-executor-protocol.md +0 -0
  55. {openrtc-0.2.2 → openrtc-0.3.0}/docs/design/proc-pool-surface.md +0 -0
  56. {openrtc-0.2.2 → openrtc-0.3.0}/docs/design/v0.1.md +0 -0
  57. {openrtc-0.2.2 → openrtc-0.3.0}/docs/examples.md +0 -0
  58. {openrtc-0.2.2 → openrtc-0.3.0}/docs/getting-started.md +0 -0
  59. {openrtc-0.2.2 → openrtc-0.3.0}/docs/index.md +0 -0
  60. {openrtc-0.2.2 → openrtc-0.3.0}/docs/package-lock.json +0 -0
  61. {openrtc-0.2.2 → openrtc-0.3.0}/docs/package.json +0 -0
  62. {openrtc-0.2.2 → openrtc-0.3.0}/docs/public/banner.png +0 -0
  63. {openrtc-0.2.2 → openrtc-0.3.0}/docs/public/logo.png +0 -0
  64. {openrtc-0.2.2 → openrtc-0.3.0}/docs/public/logo.svg +0 -0
  65. {openrtc-0.2.2 → openrtc-0.3.0}/docs/release-v0.1.md +0 -0
  66. {openrtc-0.2.2 → openrtc-0.3.0}/examples/agents/dental.py +0 -0
  67. {openrtc-0.2.2 → openrtc-0.3.0}/examples/agents/restaurant.py +0 -0
  68. {openrtc-0.2.2 → openrtc-0.3.0}/examples/density_demo.py +0 -0
  69. {openrtc-0.2.2 → openrtc-0.3.0}/examples/frontend/.dockerignore +0 -0
  70. {openrtc-0.2.2 → openrtc-0.3.0}/examples/frontend/.env.example +0 -0
  71. {openrtc-0.2.2 → openrtc-0.3.0}/examples/frontend/.gitignore +0 -0
  72. {openrtc-0.2.2 → openrtc-0.3.0}/examples/frontend/Dockerfile +0 -0
  73. {openrtc-0.2.2 → openrtc-0.3.0}/examples/frontend/README.md +0 -0
  74. {openrtc-0.2.2 → openrtc-0.3.0}/examples/frontend/app/app.css +0 -0
  75. {openrtc-0.2.2 → openrtc-0.3.0}/examples/frontend/app/components/agents-ui/agent-audio-visualizer-wave.tsx +0 -0
  76. {openrtc-0.2.2 → openrtc-0.3.0}/examples/frontend/app/components/agents-ui/agent-chat-transcript.tsx +0 -0
  77. {openrtc-0.2.2 → openrtc-0.3.0}/examples/frontend/app/components/agents-ui/agent-session-provider.tsx +0 -0
  78. {openrtc-0.2.2 → openrtc-0.3.0}/examples/frontend/app/components/demo-call-page.tsx +0 -0
  79. {openrtc-0.2.2 → openrtc-0.3.0}/examples/frontend/app/root.tsx +0 -0
  80. {openrtc-0.2.2 → openrtc-0.3.0}/examples/frontend/app/routes/api.token.ts +0 -0
  81. {openrtc-0.2.2 → openrtc-0.3.0}/examples/frontend/app/routes/dentist.tsx +0 -0
  82. {openrtc-0.2.2 → openrtc-0.3.0}/examples/frontend/app/routes/home.tsx +0 -0
  83. {openrtc-0.2.2 → openrtc-0.3.0}/examples/frontend/app/routes/restaurant.tsx +0 -0
  84. {openrtc-0.2.2 → openrtc-0.3.0}/examples/frontend/app/routes.ts +0 -0
  85. {openrtc-0.2.2 → openrtc-0.3.0}/examples/frontend/app/welcome/logo-dark.svg +0 -0
  86. {openrtc-0.2.2 → openrtc-0.3.0}/examples/frontend/app/welcome/logo-light.svg +0 -0
  87. {openrtc-0.2.2 → openrtc-0.3.0}/examples/frontend/app/welcome/welcome.tsx +0 -0
  88. {openrtc-0.2.2 → openrtc-0.3.0}/examples/frontend/package-lock.json +0 -0
  89. {openrtc-0.2.2 → openrtc-0.3.0}/examples/frontend/package.json +0 -0
  90. {openrtc-0.2.2 → openrtc-0.3.0}/examples/frontend/public/favicon.ico +0 -0
  91. {openrtc-0.2.2 → openrtc-0.3.0}/examples/frontend/react-router.config.ts +0 -0
  92. {openrtc-0.2.2 → openrtc-0.3.0}/examples/frontend/tsconfig.json +0 -0
  93. {openrtc-0.2.2 → openrtc-0.3.0}/examples/frontend/vite.config.ts +0 -0
  94. {openrtc-0.2.2 → openrtc-0.3.0}/examples/main.py +0 -0
  95. {openrtc-0.2.2 → openrtc-0.3.0}/pyproject.toml +0 -0
  96. {openrtc-0.2.2 → openrtc-0.3.0}/src/openrtc/cli/__init__.py +0 -0
  97. {openrtc-0.2.2 → openrtc-0.3.0}/src/openrtc/cli/commands.py +0 -0
  98. {openrtc-0.2.2 → openrtc-0.3.0}/src/openrtc/cli/dashboard.py +0 -0
  99. {openrtc-0.2.2 → openrtc-0.3.0}/src/openrtc/cli/entry.py +0 -0
  100. {openrtc-0.2.2 → openrtc-0.3.0}/src/openrtc/cli/livekit.py +0 -0
  101. {openrtc-0.2.2 → openrtc-0.3.0}/src/openrtc/cli/params.py +0 -0
  102. {openrtc-0.2.2 → openrtc-0.3.0}/src/openrtc/cli/reporter.py +0 -0
  103. {openrtc-0.2.2 → openrtc-0.3.0}/src/openrtc/cli/types.py +0 -0
  104. {openrtc-0.2.2 → openrtc-0.3.0}/src/openrtc/core/__init__.py +0 -0
  105. {openrtc-0.2.2 → openrtc-0.3.0}/src/openrtc/core/config.py +0 -0
  106. {openrtc-0.2.2 → openrtc-0.3.0}/src/openrtc/core/discovery.py +0 -0
  107. {openrtc-0.2.2 → openrtc-0.3.0}/src/openrtc/core/routing.py +0 -0
  108. {openrtc-0.2.2 → openrtc-0.3.0}/src/openrtc/core/serialization.py +0 -0
  109. {openrtc-0.2.2 → openrtc-0.3.0}/src/openrtc/core/turn_handling.py +0 -0
  110. {openrtc-0.2.2 → openrtc-0.3.0}/src/openrtc/execution/__init__.py +0 -0
  111. {openrtc-0.2.2 → openrtc-0.3.0}/src/openrtc/execution/coroutine.py +0 -0
  112. {openrtc-0.2.2 → openrtc-0.3.0}/src/openrtc/execution/coroutine_server.py +0 -0
  113. {openrtc-0.2.2 → openrtc-0.3.0}/src/openrtc/execution/file_watcher.py +0 -0
  114. {openrtc-0.2.2 → openrtc-0.3.0}/src/openrtc/observability/snapshot.py +0 -0
  115. {openrtc-0.2.2 → openrtc-0.3.0}/src/openrtc/observability/stream.py +0 -0
  116. {openrtc-0.2.2 → openrtc-0.3.0}/src/openrtc/py.typed +0 -0
  117. {openrtc-0.2.2/src/openrtc/observability → openrtc-0.3.0/src/openrtc/tui}/__init__.py +0 -0
  118. {openrtc-0.2.2 → openrtc-0.3.0}/src/openrtc/tui/app.py +0 -0
  119. {openrtc-0.2.2 → openrtc-0.3.0}/src/openrtc/types.py +0 -0
  120. {openrtc-0.2.2/src/openrtc/tui → openrtc-0.3.0/tests/benchmarks}/__init__.py +0 -0
  121. {openrtc-0.2.2 → openrtc-0.3.0}/tests/benchmarks/density.py +0 -0
  122. {openrtc-0.2.2 → openrtc-0.3.0}/tests/benchmarks/throughput.py +0 -0
  123. {openrtc-0.2.2 → openrtc-0.3.0}/tests/conftest.py +0 -0
  124. {openrtc-0.2.2/tests/benchmarks → openrtc-0.3.0/tests/execution}/__init__.py +0 -0
  125. {openrtc-0.2.2 → openrtc-0.3.0}/tests/execution/test_file_watcher.py +0 -0
  126. {openrtc-0.2.2 → openrtc-0.3.0}/tests/execution/test_file_watcher_smoke.py +0 -0
  127. {openrtc-0.2.2 → openrtc-0.3.0}/tests/integration/README.md +0 -0
  128. {openrtc-0.2.2/tests/execution → openrtc-0.3.0/tests/integration}/__init__.py +0 -0
  129. {openrtc-0.2.2 → openrtc-0.3.0}/tests/integration/conftest.py +0 -0
  130. {openrtc-0.2.2 → openrtc-0.3.0}/tests/integration/test_concurrent_real_calls.py +0 -0
  131. {openrtc-0.2.2 → openrtc-0.3.0}/tests/integration/test_coroutine_realroom.py +0 -0
  132. {openrtc-0.2.2 → openrtc-0.3.0}/tests/integration/test_dev_server_fixture.py +0 -0
  133. {openrtc-0.2.2 → openrtc-0.3.0}/tests/test_cli.py +0 -0
  134. {openrtc-0.2.2 → openrtc-0.3.0}/tests/test_cli_optional_extra_integration.py +0 -0
  135. {openrtc-0.2.2 → openrtc-0.3.0}/tests/test_cli_params.py +0 -0
  136. {openrtc-0.2.2 → openrtc-0.3.0}/tests/test_config.py +0 -0
  137. {openrtc-0.2.2 → openrtc-0.3.0}/tests/test_coroutine_backpressure.py +0 -0
  138. {openrtc-0.2.2 → openrtc-0.3.0}/tests/test_coroutine_coverage.py +0 -0
  139. {openrtc-0.2.2 → openrtc-0.3.0}/tests/test_coroutine_drain.py +0 -0
  140. {openrtc-0.2.2 → openrtc-0.3.0}/tests/test_coroutine_isolation.py +0 -0
  141. {openrtc-0.2.2 → openrtc-0.3.0}/tests/test_coroutine_job_context.py +0 -0
  142. {openrtc-0.2.2 → openrtc-0.3.0}/tests/test_coroutine_lifecycle.py +0 -0
  143. {openrtc-0.2.2 → openrtc-0.3.0}/tests/test_coroutine_server.py +0 -0
  144. {openrtc-0.2.2 → openrtc-0.3.0}/tests/test_coroutine_skeleton.py +0 -0
  145. {openrtc-0.2.2 → openrtc-0.3.0}/tests/test_coroutine_smoke.py +0 -0
  146. {openrtc-0.2.2 → openrtc-0.3.0}/tests/test_dashboard.py +0 -0
  147. {openrtc-0.2.2 → openrtc-0.3.0}/tests/test_discovery.py +0 -0
  148. {openrtc-0.2.2 → openrtc-0.3.0}/tests/test_isolation_process_parity.py +0 -0
  149. {openrtc-0.2.2 → openrtc-0.3.0}/tests/test_metrics_stream.py +0 -0
  150. {openrtc-0.2.2 → openrtc-0.3.0}/tests/test_pool.py +0 -0
  151. {openrtc-0.2.2 → openrtc-0.3.0}/tests/test_resources.py +0 -0
  152. {openrtc-0.2.2 → openrtc-0.3.0}/tests/test_routing.py +0 -0
  153. {openrtc-0.2.2 → openrtc-0.3.0}/tests/test_serialization.py +0 -0
  154. {openrtc-0.2.2 → openrtc-0.3.0}/tests/test_throughput_bench.py +0 -0
  155. {openrtc-0.2.2 → openrtc-0.3.0}/tests/test_tui_app.py +0 -0
  156. {openrtc-0.2.2 → openrtc-0.3.0}/tests/test_turn_handling.py +0 -0
  157. {openrtc-0.2.2 → openrtc-0.3.0}/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.3.0
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.
@@ -373,6 +443,27 @@ Pass instantiated provider objects through to `livekit-agents` unchanged, for ex
373
443
 
374
444
  If you pass strings such as `openai/gpt-4.1-mini`, OpenRTC leaves them as-is and the LiveKit runtime interprets them for your deployment.
375
445
 
446
+ ## Session observers
447
+
448
+ Attach external telemetry to every session without subclassing or touching OpenRTC internals. Any object with two async methods is a `SessionObserver` (structural typing, no base class):
449
+
450
+ ```python
451
+ from openrtc import AgentPool, SessionInfo, SessionOutcome
452
+
453
+
454
+ class LoggingObserver:
455
+ async def on_session_start(self, info: SessionInfo, session: object) -> None:
456
+ print(f"live: {info.agent_name} in {info.room_name}")
457
+
458
+ async def on_session_end(self, info: SessionInfo, outcome: SessionOutcome) -> None:
459
+ print(f"done: {info.agent_name} -> {outcome.status.value}")
460
+
461
+
462
+ pool = AgentPool(observers=[LoggingObserver()]) # or pool.add_observer(...)
463
+ ```
464
+
465
+ `on_session_start` receives the live `AgentSession`, which is where you subscribe to its metrics. `on_session_end` receives the terminal outcome (`SUCCESS`, `FAILED`, or `CANCELLED`). Observer calls are isolated: a raising or slow observer is logged and skipped, never crashing the session. In `process` isolation mode an observer must be picklable, so build live resources lazily on the first `on_session_start`.
466
+
376
467
  ## CLI and TUI
377
468
 
378
469
  Install `openrtc[cli]` to get `openrtc` on your PATH. Subcommands follow the LiveKit Agents CLI shape (`dev`, `start`, `console`, `connect`, `download-files`), plus `list` and `tui`. For most commands you can pass the agents directory (or, for `tui`, the metrics JSONL file) as the first path argument instead of `--agents-dir` / `--watch`.
@@ -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.
@@ -341,6 +411,27 @@ Pass instantiated provider objects through to `livekit-agents` unchanged, for ex
341
411
 
342
412
  If you pass strings such as `openai/gpt-4.1-mini`, OpenRTC leaves them as-is and the LiveKit runtime interprets them for your deployment.
343
413
 
414
+ ## Session observers
415
+
416
+ Attach external telemetry to every session without subclassing or touching OpenRTC internals. Any object with two async methods is a `SessionObserver` (structural typing, no base class):
417
+
418
+ ```python
419
+ from openrtc import AgentPool, SessionInfo, SessionOutcome
420
+
421
+
422
+ class LoggingObserver:
423
+ async def on_session_start(self, info: SessionInfo, session: object) -> None:
424
+ print(f"live: {info.agent_name} in {info.room_name}")
425
+
426
+ async def on_session_end(self, info: SessionInfo, outcome: SessionOutcome) -> None:
427
+ print(f"done: {info.agent_name} -> {outcome.status.value}")
428
+
429
+
430
+ pool = AgentPool(observers=[LoggingObserver()]) # or pool.add_observer(...)
431
+ ```
432
+
433
+ `on_session_start` receives the live `AgentSession`, which is where you subscribe to its metrics. `on_session_end` receives the terminal outcome (`SUCCESS`, `FAILED`, or `CANCELLED`). Observer calls are isolated: a raising or slow observer is logged and skipped, never crashing the session. In `process` isolation mode an observer must be picklable, so build live resources lazily on the first `on_session_start`.
434
+
344
435
  ## CLI and TUI
345
436
 
346
437
  Install `openrtc[cli]` to get `openrtc` on your PATH. Subcommands follow the LiveKit Agents CLI shape (`dev`, `start`, `console`, `connect`, `download-files`), plus `list` and `tui`. For most commands you can pass the agents directory (or, for `tui`, the metrics JSONL file) as the first path argument instead of `--agents-dir` / `--watch`.
@@ -59,6 +59,14 @@ Changes that have landed on `main` but have not yet been tagged for release.
59
59
  `livekit-agents` and is allowed to fail.
60
60
  - New `docker-compose.test.yml` + `tests/integration/conftest.py`
61
61
  fixture harness for local and CI integration runs.
62
+ - Public `SessionObserver` protocol (`openrtc.SessionObserver`,
63
+ `SessionInfo`, `SessionOutcome`, `SessionStatus`) plus
64
+ `AgentPool(observers=[...])` and `AgentPool.add_observer(...)`. External
65
+ telemetry attaches to each live session through the pool:
66
+ `on_session_start` hands the live `AgentSession`, `on_session_end` the
67
+ terminal outcome. Observer faults are isolated (logged and skipped,
68
+ bounded by a timeout) and never crash the session. Additive and backward
69
+ compatible; the built-in metrics store is unchanged.
62
70
 
63
71
  **Changed**
64
72
 
@@ -147,12 +155,35 @@ contributor onboarding matches what's in the repo.
147
155
 
148
156
  <!-- releases -->
149
157
 
158
+ ## [0.2.3] - 2026-05-30
159
+
160
+ ### Added
161
+ - Day-one savings readout: each worker logs the fleet-collapse idle-baseline memory saved (N agents in one shared-prewarm worker vs N separate livekit-agents workers) once at startup, for both `pool.run()` and `openrtc dev/start`, with no `--dashboard` flag. The line claims only idle baseline saved (not per-session density), stays neutral for a single agent, degrades gracefully when RSS is unavailable, and names its equal-baseline assumption.
162
+ - README: a one-screen "Migrating from livekit-agents" recipe (N per-agent workers to one AgentPool).
163
+
164
+ ---
165
+
166
+ ## [0.2.2] - 2026-05-30
167
+
168
+ ### Fixed
169
+ - 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).
170
+ - 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).
171
+
172
+ ### Added
173
+ - Real-audio throughput benchmark (`tests/benchmarks/throughput.py`) reporting steady-state event-loop p99 vs session count, separating startup from steady state (MAH-163).
174
+ - `examples/density_demo.py`: a no-server demo comparing process-per-session vs coroutine-pool resident memory.
175
+
176
+ ### Changed
177
+ - The coroutine real-room integration test is now a correctness gate (job context plus no-failure); throughput moved to the dedicated benchmark.
178
+
179
+ ---
180
+
150
181
  ## [0.2.1] - 2026-05-06
151
182
 
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
-
183
+ ## What's Changed
184
+ * [v0.2.1] File watcher infrastructure for agent code (MAH-80) by @mahimairaja in https://github.com/mahimailabs/openrtc-runtime/pull/39
185
+
186
+
156
187
  **Full Changelog**: https://github.com/mahimailabs/openrtc-runtime/compare/v0.1.0...v0.2.1
157
188
 
158
189
  ---
@@ -162,7 +193,7 @@ contributor onboarding matches what's in the repo.
162
193
  ## What's Changed
163
194
  * Feat: light websocket by @mahimairaja in https://github.com/mahimailabs/openrtc-runtime/pull/30
164
195
  * docs: bring docs/ in sync with v0.1 surface by @mahimairaja in https://github.com/mahimailabs/openrtc-runtime/pull/35
165
- * Feat: struc refac by @mahimairaja in https://github.com/mahimailabs/openrtc-runtime/pull/36
196
+ * Feat: structural refactor by @mahimairaja in https://github.com/mahimailabs/openrtc-runtime/pull/36
166
197
  * Feat/coroutine pool by @mahimairaja in https://github.com/mahimailabs/openrtc-runtime/pull/37
167
198
  * Feat/coroutine pool prod by @mahimairaja in https://github.com/mahimailabs/openrtc-runtime/pull/38
168
199