Skip to main content

meerkat_live/
lib.rs

1//! meerkat-live — Live multimodal transport for Meerkat.
2//!
3//! Composable WebSocket bridge between browser/test clients and
4//! `LiveAdapterHost`. Any Meerkat surface (CLI, RPC, REST, MCP) can
5//! mount the axum router or start a standalone listener.
6//!
7//! Frame protocol: client sends `LiveInputChunk` JSON, receives
8//! `LiveAdapterObservation` JSON. Token-based channel auth.
9
10pub mod host;
11pub mod transport;
12#[cfg(feature = "webrtc")]
13pub mod webrtc;
14pub mod wire_input;
15
16pub use host::{
17    DEFAULT_LIVE_TOOL_TIMEOUT, DeltaIdentity, LiveAdapterHost, LiveAdapterHostError,
18    LiveChannelCloseCommitAuthority, LiveChannelCloseObservation, LiveChannelId,
19    LiveChannelOpenAuthority, LiveChannelStatusCommitAuthority, LiveChannelStatusObservation,
20    LiveCommandAcceptanceKind, LiveCommandQueueAcceptance, LiveProjectionError, LiveProjectionSink,
21    LiveRefreshQueueAcceptance, LiveToolDispatchError, LiveToolDispatchTimeout, LiveToolDispatcher,
22    LiveTranscriptIdentity, LiveTranscriptIdentityError, NoOpProjectionSink, ObservationOutcome,
23    ObservationRouting, ToolDispatchSkipReason,
24};
25pub use transport::{
26    LIVE_WS_PATH, LiveChannelCloseFeedback, LiveChannelStatusFeedback, LiveTokenString,
27    LiveWsState, LiveWsTokenAdmission, LiveWsTokenAdmissionPublicErrorClass,
28    LiveWsTokenAdmissionRejection, LiveWsTokenAuthority, LiveWsTokenIssue, live_ws_router,
29    serve_live_ws_listener,
30};
31#[cfg(feature = "webrtc")]
32pub use webrtc::{
33    LIVE_WEBRTC_ANSWER_METHOD, LIVE_WEBRTC_ANSWER_PATH, LiveWebrtcAnswerAccepted, LiveWebrtcError,
34    LiveWebrtcState, WebrtcAudioBridge, live_webrtc_router, serve_live_ws_and_webrtc_listener,
35};
36pub use wire_input::{
37    LiveInputChunkDecodeError, live_input_chunk_decode_rejection, live_input_chunk_from_wire,
38};
39
40// E26 regression: `meerkat-live` must not depend on `meerkat-runtime`. The
41// dependency direction is: `meerkat-live` owns the live-adapter host
42// (`crate::host`) and the transport (`crate::transport`); surfaces compose
43// the two with their own session-runtime backing. A previous design had
44// `LiveAdapterHost` living in `meerkat-runtime` and `meerkat-live`
45// depending on it — wrong direction (transport pulling runtime). This
46// inline test parses `Cargo.toml` at compile time and asserts the
47// `meerkat-runtime` line is absent. Anyone who re-introduces the dep will
48// fail this test before E26 silently regresses.
49#[cfg(test)]
50mod e26_dependency_direction {
51    /// E26: `meerkat-live` must not list `meerkat-runtime` as a dependency.
52    /// We read `Cargo.toml` via `include_str!` (compile-time embed, no I/O
53    /// at test runtime) and assert the line is absent. This catches both
54    /// `meerkat-runtime = { workspace = true }` and the older
55    /// `meerkat-runtime = "..."` shapes; we simply look for the bare
56    /// crate name as a TOML key.
57    #[test]
58    fn cargo_toml_does_not_depend_on_meerkat_runtime() {
59        let cargo_toml = include_str!("../Cargo.toml");
60        // Per-line scan so we don't trip on the word in a comment (none
61        // present today, but futureproof against documentation drift).
62        for line in cargo_toml.lines() {
63            let trimmed = line.trim_start();
64            assert!(
65                !trimmed.starts_with("meerkat-runtime"),
66                "E26 regression: meerkat-live must not depend on meerkat-runtime; \
67                 found Cargo.toml line: {line}"
68            );
69        }
70    }
71
72    #[test]
73    fn webrtc_media_dependencies_are_feature_gated() {
74        let cargo_toml = include_str!("../Cargo.toml");
75        assert!(
76            cargo_toml.lines().any(|line| line.trim() == "default = []"),
77            "meerkat-live default feature set must not enable WebRTC/media"
78        );
79        for dep in ["webrtc", "webrtc-media", "opus", "rubato"] {
80            let needle = format!("{dep} = ");
81            let dep_line = cargo_toml
82                .lines()
83                .find(|line| line.trim_start().starts_with(&needle))
84                .unwrap_or_else(|| panic!("missing optional {dep} dependency"));
85            assert!(
86                dep_line.contains("optional = true"),
87                "{dep} dependency must stay optional: {dep_line}"
88            );
89        }
90    }
91}