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]>
4.8 KiB
jouledesk-livekit — the LiveKit room binding
The reference room 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:
- join — the room access token is the capability grant (LiveKit
RoomAgentDispatch); - 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; - meter — an allowed turn's model call is metered at the model-call boundary (LiveKit
llm_node/ OTelgen_ai.usage.*); - receipt — one
efficiency/1seals the whole call at shutdown (LiveKitsession.usage), anchored to the room id — a verifiable record of what@jouledid, 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.
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 for a full runnable call.
The livekit feature (real SDK) — runs live
src/adapter.rs is reference wiring against the 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:
- Native-dep skew.
livekit 0.7.49requireslibwebrtc ^0.3.38, which pulls the newestwebrtc-sys(0.3.36); butlibwebrtc 0.3.38's code omits theuser_datafield / a 4th arg thatwebrtc-sys 0.3.36now requires.vendor/libwebrtcis that crate with the three additive lines restored, wired via[patch.crates-io]in the workspaceCargo.toml. (A workaround for a broken upstream release — drop it once upstream ships a consistent pair.) - macOS ObjC categories.
build.rsemits-ObjCat link so libwebrtc's Objective-C categories force-load — without it,PeerConnectionFactoryinit crashes withunrecognized selector +[NSString stringForAbslStringView:].
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 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; Joule Desk is the collaboration binding of Joule Table.