open-standards/jouledesk/jouledesk-rs/crates/jouledesk-livekit/README.md
Transaction Science c8db7f4efa jouledesk-livekit: prove the room binding live — real LiveKit run
Drives all four seams over a real WebRTC room against a live livekit-server:
join under a minted token, gate each turn OUTSIDE the model (served vs refused
for unheld-capability / over-ceiling), meter the model calls, seal one
efficiency/1 that verifies under the steward key. examples/live_room.rs is the
runnable proof (data-plane turns → gate/meter/seal, receipt verified).

Clears two upstream blockers so `--features livekit` builds + runs on macOS:
- vendor/libwebrtc: libwebrtc 0.3.38 omits user_data that webrtc-sys 0.3.36
  requires (broken upstream release pair); patched additively + [patch.crates-io].
- build.rs: emit -ObjC so libwebrtc's ObjC categories force-load, else
  PeerConnectionFactory init crashes on an unrecognized selector.

Default build + tests are unaffected (the feature stays off by default).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-06 04:13:40 -04:00

82 lines
4.8 KiB
Markdown

# jouledesk-livekit — the LiveKit `room` binding
The reference [`room`](../../SPEC.md) binding class of Joule Desk: `@joule` joins a **live
voice/video room** and drives the four real-time seams over the call's lifecycle.
A live room is a *session*, not a mention. So the loop is not task-per-message — it is:
1. **join** — the room access token *is* the capability grant (LiveKit `RoomAgentDispatch`);
2. **turn** — each user turn is gated **outside the model** (LiveKit `on_user_turn_completed`
→ `StopResponse`) — an unheld capability or an over-ceiling turn never reaches generation;
3. **meter** — an allowed turn's model call is metered at the model-call boundary (LiveKit
`llm_node` / OTel `gen_ai.usage.*`);
4. **receipt** — one `efficiency/1` seals the whole call at shutdown (LiveKit `session.usage`),
anchored to the room id — a verifiable record of what `@joule` did, under which capability,
at what metered energy.
The **joule ceiling is shared across the session**, so it holds whether `@joule` is typing or
talking: the same meter that bounds a text session bounds the room.
## Two layers, on purpose
| Layer | What it is | Build |
|---|---|---|
| **`RoomAgent`** (crate root) | The four seams as a **transport-agnostic** driver over `RealtimeSession`. Gate-before-generate, meter, seal — no SDK. | Default. **Fully unit-tested** (`cargo test`). |
| **`adapter`** (`src/adapter.rs`) | Wires `RoomAgent` onto the **real LiveKit Rust SDK**: `Room::connect` with the token, the room event loop → `on_turn` / `shutdown`. | Behind `--features livekit`. Needs libwebrtc + a live server. |
The driver is where the standard lives, and it is verifiable without a network, a GPU, or a
running media server. The adapter is thin glue on top.
```rust
let mut agent = RoomAgent::join("standup", &["answer", "summarize"], 5_000_000);
// A turn: gated outside the model; run_model runs ONLY if allowed, then is metered.
agent.on_turn(&turn, |t| my_stt_llm_tts_pipeline(t)); // → Served
agent.on_turn(&delete_turn, |t| unreachable(t)); // → Refused (unheld cap): model never runs
let receipt = agent.shutdown(); // one efficiency/1, verifiable by the whole room
```
See [`../jouledesk/examples/room.rs`](../jouledesk/examples/room.rs) for a full runnable call.
## The `livekit` feature (real SDK) — runs live
`src/adapter.rs` is reference wiring against the [`livekit`](https://crates.io/crates/livekit)
Rust SDK — the lower-level client (Room, tracks, data). The high-level `AgentSession` /
`llm_node` framework is Python/Node, so this crate owns the turn/gate/meter/seal loop itself
and hands the transport the four call-points; `adapter::run` maps the room event loop onto them.
The feature is **off by default** (the SDK is heavy — it pulls libwebrtc), but it **builds and
runs end-to-end**. Two upstream blockers are worked around in-tree:
1. **Native-dep skew.** `livekit 0.7.49` requires `libwebrtc ^0.3.38`, which pulls the newest
`webrtc-sys` (0.3.36); but `libwebrtc 0.3.38`'s code omits the `user_data` field / a 4th arg
that `webrtc-sys 0.3.36` now requires. [`vendor/libwebrtc`](../../vendor/libwebrtc) is that
crate with the three additive lines restored, wired via `[patch.crates-io]` in the workspace
`Cargo.toml`. (A workaround for a broken upstream release — drop it once upstream ships a
consistent pair.)
2. **macOS ObjC categories.** [`build.rs`](build.rs) emits `-ObjC` at link so libwebrtc's
Objective-C categories force-load — without it, `PeerConnectionFactory` init crashes with
`unrecognized selector +[NSString stringForAbslStringView:]`.
```bash
cargo test # the tested core (default) — no SDK, no network
cargo build -p jouledesk-livekit --features livekit # builds the real adapter (+ vendored patch)
# a real end-to-end run against a live server:
brew install livekit && livekit-server --dev & # devkey/secret on ws://127.0.0.1:7880
cargo run -p jouledesk-livekit --features livekit --example live_room
```
[`examples/live_room.rs`](examples/live_room.rs) mints a real access token, JOINs a real WebRTC
room as `@joule`, has a second participant publish turns on the room's data plane, gates each one
**outside the model** (served vs refused for unheld-capability / over-ceiling), meters the model
calls, and seals one `efficiency/1` that verifies under the steward key — the four seams over real
transport (ICE + DTLS + SCTP). Bring your own `parse_turn` and `run_model` (STT → LLM → reply →
`Usage`) closures; the seam logic is identical to the fully-tested core.
## Licence
Apache-2.0 for reference code; CC-BY-4.0 for the specification text. Part of the
[Transaction Science open standards](../../../README.md); Joule Desk is the collaboration
binding of [Joule Table](../../../jouletable).