use std::collections::HashMap;
use std::sync::Arc;
use std::sync::Mutex as StdMutex;
use async_trait::async_trait;
use meerkat::AgentFactory;
use meerkat::session_runtime::LiveOpenPrecheckError;
use meerkat::session_runtime::live_orchestration::{
precheck_identity, realtime_projection_messages, realtime_projection_root_system_message,
realtime_projection_runtime_system_context,
};
use meerkat::session_runtime::realtime_credentials::RealtimeCurrentConfigSource;
use meerkat_client::realtime_session::{RealtimeSessionFactory, RealtimeSessionOpenConfig};
use meerkat_contracts::{
LiveChannelParams, LiveCloseResult, LiveCommitInputParams, LiveCommitInputResult,
LiveInterruptResult, LiveOpenResult, LiveOpenTransport, LiveRefreshResult, LiveSendInputParams,
LiveSendInputResult, LiveStatusResult, RealtimeCapabilities, RealtimeTurningMode,
WireLiveAdapterStatus, WireLiveDegradationReason,
};
use meerkat_core::live_adapter::{
LiveAdapterCommand, LiveAdapterErrorCode, LiveAudioConfig, LiveChannelCapabilities,
LiveContinuityMode, LiveProjectionSnapshot, LiveTransportBootstrap,
};
use meerkat_core::service::SessionService as _;
use meerkat_core::types::{AssistantBlock, ContentInput, Message, SessionId, StopReason, Usage};
use meerkat_core::{Config, ConfigError, RealtimeTranscriptEvent};
use meerkat_live::{
LiveAdapterHost, LiveAdapterHostError, LiveChannelCloseFeedback, LiveChannelCloseObservation,
LiveChannelId, LiveChannelStatusFeedback, LiveChannelStatusObservation, LiveProjectionError,
LiveProjectionSink, LiveTokenString, LiveToolDispatcher, LiveTranscriptIdentity,
LiveTranscriptIdentityError, LiveWsState, LiveWsTokenAdmission,
LiveWsTokenAdmissionPublicErrorClass, LiveWsTokenAdmissionRejection, LiveWsTokenAuthority,
LiveWsTokenIssue, live_input_chunk_from_wire,
};
use meerkat_runtime::MeerkatMachine;
use meerkat_session::{PersistentSessionService, SessionAgentBuilder};
use serde::Deserialize;
use serde::de::DeserializeOwned;
use serde_json::Value;
use crate::rpc::{JSONRPC_VERSION, JsonRpcError, JsonRpcResponse};
pub const LIVE_UNAVAILABLE_CODE: i64 = -32050;
pub const LIVE_UNAVAILABLE_KIND: &str = "live_unavailable";
const INVALID_PARAMS_CODE: i64 = -32602;
const METHOD_NOT_FOUND_CODE: i64 = -32601;
const INTERNAL_ERROR_CODE: i64 = -32000;
#[derive(Debug, Clone)]
enum PendingAssistantContent {
Text(String),
}
#[derive(Debug, Default)]
struct PendingTurn {
blocks: Vec<PendingAssistantContent>,
}
#[derive(Default)]
struct PendingTurnLedger {
slots: StdMutex<HashMap<(SessionId, Option<String>), PendingTurn>>,
}
impl PendingTurnLedger {
fn buffer(
&self,
session_id: &SessionId,
response_id: Option<&str>,
content: PendingAssistantContent,
) {
let Ok(mut slots) = self.slots.lock() else {
return;
};
slots
.entry((session_id.clone(), response_id.map(ToString::to_string)))
.or_default()
.blocks
.push(content);
}
fn drain(&self, session_id: &SessionId, response_id: Option<&str>) -> PendingTurn {
let Ok(mut slots) = self.slots.lock() else {
return PendingTurn::default();
};
slots
.remove(&(session_id.clone(), response_id.map(ToString::to_string)))
.unwrap_or_default()
}
fn drain_all(&self, session_id: &SessionId) {
let Ok(mut slots) = self.slots.lock() else {
return;
};
slots.retain(|(sid, _resp), _| sid != session_id);
}
}
pub struct GatewayLiveProjectionSink<B: SessionAgentBuilder + 'static> {
service: Arc<PersistentSessionService<B>>,
machine: Arc<MeerkatMachine>,
pending_turns: PendingTurnLedger,
}
impl<B: SessionAgentBuilder + 'static> GatewayLiveProjectionSink<B> {
pub fn new(service: Arc<PersistentSessionService<B>>, machine: Arc<MeerkatMachine>) -> Self {
Self {
service,
machine,
pending_turns: PendingTurnLedger::default(),
}
}
}
fn build_assistant_text_delta_event(
delta: &str,
identity: LiveTranscriptIdentity<'_>,
) -> Result<RealtimeTranscriptEvent, LiveTranscriptIdentityError> {
let resolved = identity.require_delta_identity()?;
Ok(RealtimeTranscriptEvent::AssistantTextDelta {
response_id: resolved.response_id.to_string(),
delta_id: resolved.delta_id.to_string(),
item_id: resolved.item_id.to_string(),
previous_item_id: resolved.previous_item_id.map(ToString::to_string),
content_index: resolved.content_index.unwrap_or(0),
delta: delta.to_string(),
})
}
fn build_assistant_transcript_delta_event(
delta: &str,
identity: LiveTranscriptIdentity<'_>,
) -> Result<RealtimeTranscriptEvent, LiveTranscriptIdentityError> {
let resolved = identity.require_delta_identity()?;
Ok(RealtimeTranscriptEvent::AssistantTranscriptDelta {
response_id: resolved.response_id.to_string(),
delta_id: resolved.delta_id.to_string(),
item_id: resolved.item_id.to_string(),
previous_item_id: resolved.previous_item_id.map(ToString::to_string),
content_index: resolved.content_index.unwrap_or(0),
delta: delta.to_string(),
})
}
fn identity_error_to_projection(err: LiveTranscriptIdentityError) -> LiveProjectionError {
LiveProjectionError::Rejected(err.to_string())
}
fn collapse_pending_blocks(buffered: Vec<PendingAssistantContent>) -> Vec<AssistantBlock> {
if buffered.is_empty() {
return Vec::new();
}
let mut acc = String::new();
for PendingAssistantContent::Text(fragment) in buffered {
acc.push_str(&fragment);
}
vec![AssistantBlock::Text {
text: acc,
meta: None,
}]
}
fn session_error_to_projection(
err: meerkat_core::SessionError,
id: &SessionId,
) -> LiveProjectionError {
LiveProjectionError::from_session_error(id, err)
}
#[async_trait]
impl<B: SessionAgentBuilder + 'static> LiveChannelCloseFeedback for GatewayLiveProjectionSink<B> {
async fn record_live_channel_closed(
&self,
channel_id: &LiveChannelId,
observation: &LiveChannelCloseObservation,
) -> Result<meerkat_live::LiveChannelCloseCommitAuthority, String> {
let session_id = self
.machine
.live_session_for_active_channel(channel_id)
.await
.ok_or_else(|| {
format!("generated live active-channel authority absent for channel {channel_id}")
})?;
self.machine
.resolve_live_close_result(&session_id, observation)
.await
.map_err(|err| err.to_string())?
.into_channel_close_commit_authority()
.ok_or_else(|| {
format!(
"generated live close authority omitted host commit handoff for channel {channel_id}"
)
})
}
}
#[async_trait]
impl<B: SessionAgentBuilder + 'static> LiveChannelStatusFeedback for GatewayLiveProjectionSink<B> {
async fn record_live_channel_status(
&self,
channel_id: &LiveChannelId,
observation: &LiveChannelStatusObservation,
) -> Result<meerkat_live::LiveChannelStatusCommitAuthority, String> {
if observation.channel_id() != channel_id.as_str() {
return Err(format!(
"generated live status observation channel mismatch: observed {}, requested {}",
observation.channel_id(),
channel_id
));
}
let session_id = self
.machine
.live_session_for_status_channel(channel_id)
.await
.ok_or_else(|| {
format!("generated live status-channel authority absent for channel {channel_id}")
})?;
self.machine
.resolve_live_channel_status_result(&session_id, observation)
.await
.map_err(|err| err.to_string())?
.into_channel_status_commit_authority()
.ok_or_else(|| {
format!(
"generated live status authority omitted host commit handoff for channel {channel_id}"
)
})
}
}
#[async_trait]
impl<B: SessionAgentBuilder + 'static> LiveWsTokenAuthority for GatewayLiveProjectionSink<B> {
async fn record_live_ws_token_issued(
&self,
session_id: &SessionId,
channel_id: &LiveChannelId,
token: &LiveTokenString,
issued_at_ms: u64,
ttl_ms: u64,
) -> Result<LiveWsTokenIssue, String> {
let authority = self
.machine
.record_live_websocket_token_issued(
session_id,
channel_id,
token.as_str(),
issued_at_ms,
ttl_ms,
)
.await
.map_err(|err| err.to_string())?;
let token = LiveTokenString::new(authority.token).map_err(|err| err.to_string())?;
Ok(LiveWsTokenIssue {
token,
expires_at_ms: authority.expires_at_ms,
sequence: authority.sequence,
})
}
async fn resolve_live_ws_token_admission(
&self,
channel_id: &LiveChannelId,
token: &str,
observed_at_ms: u64,
) -> Result<LiveWsTokenAdmission, String> {
let token_owner = self.machine.live_session_for_websocket_token(token).await;
let authority = match token_owner {
Some(session_id) => {
self.machine
.resolve_live_websocket_token_admission(
&session_id,
channel_id,
token,
observed_at_ms,
)
.await
}
None => match self
.machine
.live_session_for_active_channel(channel_id)
.await
{
Some(session_id) => {
self.machine
.resolve_live_websocket_token_admission(
&session_id,
channel_id,
token,
observed_at_ms,
)
.await
}
None => {
self.machine
.resolve_unbound_live_websocket_token_admission(
channel_id,
token,
observed_at_ms,
)
.await
}
},
}
.map_err(|err| err.to_string())?;
Ok(LiveWsTokenAdmission {
channel_id: channel_id.clone(),
admitted: authority.admitted,
rejection: authority
.rejection
.map(live_ws_token_admission_rejection_from_machine),
public_error_class: authority
.public_error_class
.map(live_ws_token_public_error_class_from_machine),
sequence: authority.sequence,
})
}
}
fn live_ws_token_admission_rejection_from_machine(
rejection: meerkat_runtime::meerkat_machine::dsl::LiveWebsocketTokenAdmissionRejection,
) -> LiveWsTokenAdmissionRejection {
use meerkat_runtime::meerkat_machine::dsl::LiveWebsocketTokenAdmissionRejection as Dsl;
match rejection {
Dsl::TokenNotFound => LiveWsTokenAdmissionRejection::TokenNotFound,
Dsl::TokenExpired => LiveWsTokenAdmissionRejection::TokenExpired,
Dsl::TokenChannelMismatch => LiveWsTokenAdmissionRejection::TokenChannelMismatch,
Dsl::TokenAlreadyConsumed => LiveWsTokenAdmissionRejection::TokenAlreadyConsumed,
Dsl::ChannelNotBound => LiveWsTokenAdmissionRejection::ChannelNotBound,
}
}
fn live_ws_token_public_error_class_from_machine(
public_error_class: meerkat_runtime::meerkat_machine::dsl::LiveWebsocketTokenAdmissionPublicErrorClass,
) -> LiveWsTokenAdmissionPublicErrorClass {
use meerkat_runtime::meerkat_machine::dsl::LiveWebsocketTokenAdmissionPublicErrorClass as Dsl;
match public_error_class {
Dsl::InvalidToken => LiveWsTokenAdmissionPublicErrorClass::InvalidToken,
}
}
#[async_trait]
impl<B: SessionAgentBuilder + 'static> LiveProjectionSink for GatewayLiveProjectionSink<B> {
async fn append_user_transcript(
&self,
session_id: &SessionId,
text: &str,
identity: LiveTranscriptIdentity<'_>,
) -> Result<(), LiveProjectionError> {
if let Some(item_id) = identity.provider_item_id {
let event = RealtimeTranscriptEvent::UserTranscriptFinal {
item_id: item_id.to_string(),
previous_item_id: identity.previous_item_id.map(ToString::to_string),
content_index: identity.content_index.unwrap_or(0),
text: text.to_string(),
};
return self
.service
.append_realtime_transcript_event(session_id, event)
.await
.map(|_outcome| ())
.map_err(|err| session_error_to_projection(err, session_id));
}
self.service
.append_external_user_content(session_id, ContentInput::Text(text.to_string()))
.await
.map_err(|err| session_error_to_projection(err, session_id))
}
async fn append_assistant_text_delta(
&self,
session_id: &SessionId,
delta: &str,
identity: LiveTranscriptIdentity<'_>,
) -> Result<(), LiveProjectionError> {
let event = build_assistant_text_delta_event(delta, identity)
.map_err(identity_error_to_projection)?;
self.service
.append_realtime_transcript_event(session_id, event)
.await
.map(|_outcome| ())
.map_err(|err| session_error_to_projection(err, session_id))
}
async fn append_assistant_transcript_delta(
&self,
session_id: &SessionId,
delta: &str,
identity: LiveTranscriptIdentity<'_>,
) -> Result<(), LiveProjectionError> {
let event = build_assistant_transcript_delta_event(delta, identity)
.map_err(identity_error_to_projection)?;
self.service
.append_realtime_transcript_event(session_id, event)
.await
.map(|_outcome| ())
.map_err(|err| session_error_to_projection(err, session_id))
}
async fn append_assistant_text_final(
&self,
session_id: &SessionId,
text: &str,
_identity: LiveTranscriptIdentity<'_>,
_stop_reason: StopReason,
_usage: Usage,
response_id: Option<&str>,
) -> Result<(), LiveProjectionError> {
self.pending_turns.buffer(
session_id,
response_id,
PendingAssistantContent::Text(text.to_string()),
);
Ok(())
}
async fn append_assistant_transcript_final(
&self,
session_id: &SessionId,
text: &str,
identity: LiveTranscriptIdentity<'_>,
_stop_reason: StopReason,
_usage: Usage,
response_id: Option<&str>,
) -> Result<(), LiveProjectionError> {
let response_id = identity
.response_id
.map(ToString::to_string)
.or_else(|| response_id.map(ToString::to_string))
.unwrap_or_default();
let event = RealtimeTranscriptEvent::AssistantTranscriptFinalText {
response_id,
item_id: identity
.provider_item_id
.map(ToString::to_string)
.unwrap_or_default(),
content_index: identity.content_index.unwrap_or(0),
text: text.to_string(),
};
self.service
.append_realtime_transcript_event(session_id, event)
.await
.map(|_outcome| ())
.map_err(|err| session_error_to_projection(err, session_id))
}
async fn truncate_assistant_transcript(
&self,
session_id: &SessionId,
provider_item_id: Option<&str>,
_previous_item_id: Option<&str>,
content_index: Option<u32>,
response_id: Option<&str>,
text: Option<&str>,
) -> Result<(), LiveProjectionError> {
let Some(response_id) = response_id else {
return Err(LiveProjectionError::Rejected(
"AssistantTranscriptTruncated missing response_id from adapter".to_string(),
));
};
let Some(item_id) = provider_item_id else {
return Err(LiveProjectionError::Rejected(
"AssistantTranscriptTruncated missing provider_item_id from adapter".to_string(),
));
};
let event = RealtimeTranscriptEvent::AssistantTranscriptTruncated {
response_id: response_id.to_string(),
item_id: item_id.to_string(),
content_index: content_index.unwrap_or(0),
text: text.unwrap_or_default().to_string(),
};
self.service
.append_realtime_transcript_event(session_id, event)
.await
.map(|_outcome| ())
.map_err(|err| session_error_to_projection(err, session_id))
}
async fn signal_turn_interrupt(
&self,
session_id: &SessionId,
response_id: Option<&str>,
) -> Result<(), LiveProjectionError> {
let mut response_ids: Vec<String> = Vec::new();
if let Some(rid) = response_id.filter(|s| !s.is_empty()) {
response_ids.push(rid.to_string());
}
match self.service.load_authoritative_session(session_id).await {
Ok(Some(session)) => {
for id in session.in_flight_realtime_assistant_response_ids() {
if !response_ids.contains(&id) {
response_ids.push(id);
}
}
}
Ok(None) => {}
Err(
meerkat_core::SessionError::NotFound { .. }
| meerkat_core::SessionError::Unsupported(_),
) => {}
Err(err) => return Err(session_error_to_projection(err, session_id)),
}
for rid in response_ids {
let event = RealtimeTranscriptEvent::AssistantTurnInterrupted { response_id: rid };
match self
.service
.append_realtime_transcript_event(session_id, event)
.await
{
Ok(_) => {}
Err(
meerkat_core::SessionError::NotFound { .. }
| meerkat_core::SessionError::Unsupported(_),
) => {}
Err(err) => return Err(session_error_to_projection(err, session_id)),
}
}
match self
.service
.interrupt_with_machine_authority(session_id, self.machine.session_control_authority())
.await
{
Ok(()) => Ok(()),
Err(meerkat_core::SessionError::NotRunning { .. }) => Ok(()),
Err(err) => Err(session_error_to_projection(err, session_id)),
}
}
async fn signal_turn_completed(
&self,
session_id: &SessionId,
stop_reason: StopReason,
usage: Usage,
response_id: Option<&str>,
) -> Result<(), LiveProjectionError> {
let mut realtime_materialized = false;
if let Some(rid) = response_id.filter(|s| !s.is_empty()) {
let event = RealtimeTranscriptEvent::AssistantTurnCompleted {
response_id: rid.to_string(),
stop_reason,
usage: usage.clone(),
};
match self
.service
.append_realtime_transcript_event(session_id, event)
.await
{
Ok(outcome) => {
realtime_materialized = !outcome.is_inert();
}
Err(
meerkat_core::SessionError::NotFound { .. }
| meerkat_core::SessionError::Unsupported(_),
) => {}
Err(err) => return Err(session_error_to_projection(err, session_id)),
}
}
let pending = self.pending_turns.drain(session_id, response_id);
let blocks = collapse_pending_blocks(pending.blocks);
if realtime_materialized && blocks.is_empty() {
return Ok(());
}
let usage_for_drain = if realtime_materialized {
Usage::default()
} else {
usage
};
self.service
.append_external_assistant_output(session_id, blocks, stop_reason, usage_for_drain)
.await
.map_err(|err| session_error_to_projection(err, session_id))
}
async fn signal_terminal_error(
&self,
session_id: &SessionId,
code: LiveAdapterErrorCode,
message: &str,
) -> Result<(), LiveProjectionError> {
self.pending_turns.drain_all(session_id);
tracing::warn!(
target: "meerkat_mobkit::live_wiring",
session_id = %session_id,
?code,
message,
"live adapter terminal error",
);
self.service
.record_live_terminal_error(session_id, code)
.await
.map_err(|err| session_error_to_projection(err, session_id))
}
async fn signal_output_audio_degraded(
&self,
session_id: &SessionId,
dropped: u64,
) -> Result<(), LiveProjectionError> {
self.service
.record_live_output_audio_degraded(session_id, dropped)
.await
.map_err(|err| session_error_to_projection(err, session_id))
}
async fn append_realtime_transcript(
&self,
session_id: &SessionId,
event: &RealtimeTranscriptEvent,
) -> Result<(), LiveProjectionError> {
self.service
.append_realtime_transcript_event(session_id, event.clone())
.await
.map(|_outcome| ())
.map_err(|err| session_error_to_projection(err, session_id))
}
}
pub struct GatewayLiveToolDispatcher<B: SessionAgentBuilder + 'static> {
service: Arc<PersistentSessionService<B>>,
}
impl<B: SessionAgentBuilder + 'static> GatewayLiveToolDispatcher<B> {
pub fn new(service: Arc<PersistentSessionService<B>>) -> Self {
Self { service }
}
}
#[async_trait]
impl<B: SessionAgentBuilder + 'static> LiveToolDispatcher for GatewayLiveToolDispatcher<B> {
async fn dispatch_live_tool_call(
&self,
session_id: &SessionId,
call: meerkat_core::ToolCall,
) -> Result<meerkat_core::ops::ToolDispatchOutcome, meerkat_live::LiveToolDispatchError> {
self.service
.dispatch_external_tool_call(session_id, call)
.await
.map_err(|err| meerkat_live::LiveToolDispatchError::from_session_error(session_id, err))
}
}
pub struct EnvRealtimeConfigSource {
config: Config,
}
impl EnvRealtimeConfigSource {
pub fn new(config: Config) -> Self {
Self { config }
}
}
#[async_trait]
impl RealtimeCurrentConfigSource for EnvRealtimeConfigSource {
async fn current_config(&self) -> Result<Config, ConfigError> {
Ok(self.config.clone())
}
}
#[derive(Clone)]
pub struct GatewayLiveContext {
pub host: Arc<LiveAdapterHost>,
pub ws_state: Arc<LiveWsState>,
pub session_factory: Arc<dyn RealtimeSessionFactory>,
pub ws_base_url: String,
}
pub fn attach_live<B: SessionAgentBuilder + 'static>(
service: Arc<PersistentSessionService<B>>,
machine: Arc<MeerkatMachine>,
factory: &AgentFactory,
config: Config,
ws_base_url: String,
) -> GatewayLiveContext {
let sink = Arc::new(GatewayLiveProjectionSink::new(
Arc::clone(&service),
machine,
));
let dispatcher: Arc<dyn LiveToolDispatcher> = Arc::new(GatewayLiveToolDispatcher::new(service));
let host = Arc::new(
LiveAdapterHost::new(Arc::clone(&sink) as Arc<dyn LiveProjectionSink>)
.with_live_tool_dispatcher(dispatcher),
);
let ws_state = Arc::new(LiveWsState::new(
Arc::clone(&host),
Arc::clone(&sink) as Arc<dyn LiveChannelCloseFeedback>,
Arc::clone(&sink) as Arc<dyn LiveChannelStatusFeedback>,
sink as Arc<dyn LiveWsTokenAuthority>,
));
let session_factory = factory
.build_openai_realtime_session_factory(Arc::new(EnvRealtimeConfigSource::new(config)));
GatewayLiveContext {
host,
ws_state,
session_factory,
ws_base_url,
}
}
#[derive(Debug, Deserialize)]
struct GatewayLiveOpenParams {
#[serde(default)]
turning_mode: Option<RealtimeTurningMode>,
#[serde(default)]
transport: Option<LiveOpenTransport>,
#[serde(default)]
model: Option<String>,
}
fn live_success(rpc_id: Value, result: Value) -> JsonRpcResponse {
JsonRpcResponse {
jsonrpc: JSONRPC_VERSION.to_string(),
id: rpc_id,
result: Some(result),
error: None,
}
}
fn live_error(rpc_id: Value, code: i64, message: impl Into<String>) -> JsonRpcResponse {
JsonRpcResponse {
jsonrpc: JSONRPC_VERSION.to_string(),
id: rpc_id,
result: None,
error: Some(JsonRpcError::new(code, message)),
}
}
pub type LiveRpcHandler = Arc<
dyn Fn(
Option<meerkat_core::types::SessionId>,
String,
Value,
Value,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = JsonRpcResponse> + Send>>
+ Send
+ Sync,
>;
pub fn live_rpc_handler<B: SessionAgentBuilder + 'static>(
ctx: Arc<GatewayLiveContext>,
service: Arc<PersistentSessionService<B>>,
machine: Arc<MeerkatMachine>,
) -> LiveRpcHandler {
Arc::new(move |resolved_session, method, params, rpc_id| {
let ctx = Arc::clone(&ctx);
let service = Arc::clone(&service);
let machine = Arc::clone(&machine);
Box::pin(async move {
handle_live_method(
&ctx,
&service,
&machine,
resolved_session,
&method,
¶ms,
rpc_id,
)
.await
})
})
}
#[must_use]
pub fn live_unavailable_response(rpc_id: Value) -> JsonRpcResponse {
JsonRpcResponse {
jsonrpc: JSONRPC_VERSION.to_string(),
id: rpc_id,
result: None,
error: Some(
JsonRpcError::new(
LIVE_UNAVAILABLE_CODE,
"live sessions are not available on this gateway",
)
.with_data(serde_json::json!({ "kind": LIVE_UNAVAILABLE_KIND })),
),
}
}
fn parse_live_params<T: DeserializeOwned>(
params: &Value,
rpc_id: &Value,
) -> Result<T, Box<JsonRpcResponse>> {
serde_json::from_value(params.clone()).map_err(|err| {
Box::new(live_error(
rpc_id.clone(),
INVALID_PARAMS_CODE,
format!("invalid params: {err}"),
))
})
}
pub async fn handle_live_method<B: SessionAgentBuilder + 'static>(
ctx: &GatewayLiveContext,
service: &Arc<PersistentSessionService<B>>,
machine: &Arc<MeerkatMachine>,
resolved_session: Option<SessionId>,
method: &str,
params: &Value,
rpc_id: Value,
) -> JsonRpcResponse {
match method {
"mobkit/live/open" => {
let Some(session_id) = resolved_session else {
return live_error(
rpc_id,
INVALID_PARAMS_CODE,
"live/open requires a resolvable member target \
(identity, member_id, or session_id)",
);
};
handle_live_open(ctx, service, machine, &session_id, params, rpc_id).await
}
"mobkit/live/status" => handle_live_status(ctx, machine, params, rpc_id).await,
"mobkit/live/close" => handle_live_close(ctx, machine, params, rpc_id).await,
"mobkit/live/refresh" => handle_live_refresh(ctx, service, machine, params, rpc_id).await,
"mobkit/live/send_input" => handle_live_send_input(ctx, machine, params, rpc_id).await,
"mobkit/live/commit_input" => handle_live_commit_input(ctx, machine, params, rpc_id).await,
"mobkit/live/interrupt" => handle_live_interrupt(ctx, machine, params, rpc_id).await,
other => live_error(
rpc_id,
METHOD_NOT_FOUND_CODE,
format!("unknown live method {other}"),
),
}
}
async fn live_open_config_for_session<B: SessionAgentBuilder + 'static>(
service: &PersistentSessionService<B>,
session_id: &SessionId,
turning_mode: RealtimeTurningMode,
) -> Result<RealtimeSessionOpenConfig, meerkat_core::SessionError> {
let session = service
.export_realtime_open_session_snapshot(session_id)
.await?;
let llm_identity = service.live_session_llm_identity(session_id).await?;
let visible_tools = service.live_visible_tool_defs(session_id).await?;
Ok(RealtimeSessionOpenConfig::new(
turning_mode,
llm_identity,
visible_tools,
realtime_projection_messages(&session)?,
)
.with_runtime_system_context(realtime_projection_runtime_system_context(&session)?)
.with_system_prompt(match realtime_projection_root_system_message(&session)? {
Some(Message::System(system)) => Some(system.content),
_ => None,
}))
}
fn live_audio_config_from_capabilities(
capabilities: &RealtimeCapabilities,
) -> Option<LiveAudioConfig> {
let input = capabilities.audio_input_format.as_ref()?;
let output = capabilities.audio_output_format.as_ref()?;
Some(LiveAudioConfig {
input_sample_rate_hz: input.sample_rate_hz,
input_channels: u16::from(input.channels),
output_sample_rate_hz: output.sample_rate_hz,
output_channels: u16::from(output.channels),
})
}
fn live_ws_audio_format_param(audio: &LiveAudioConfig) -> Option<&'static str> {
const PCM_24K_MONO_RATE_HZ: u32 = 24_000;
const PCM_24K_MONO_CHANNELS: u16 = 1;
if audio.input_sample_rate_hz == PCM_24K_MONO_RATE_HZ
&& audio.input_channels == PCM_24K_MONO_CHANNELS
{
Some("pcm_24k_mono")
} else {
None
}
}
fn build_live_projection_snapshot(
session_id: &SessionId,
open_config: &RealtimeSessionOpenConfig,
audio_config: Option<LiveAudioConfig>,
) -> LiveProjectionSnapshot {
LiveProjectionSnapshot {
session_id: session_id.clone(),
snapshot_version: 0,
seed_messages: open_config.seed_messages.clone(),
visible_tools: open_config.visible_tools.clone(),
system_prompt: open_config.system_prompt.clone(),
model_id: open_config.llm_identity.model.clone(),
provider_id: open_config.llm_identity.provider,
audio_config,
runtime_system_context: open_config.runtime_system_context.clone(),
}
}
fn continuity_from_snapshot(snapshot: &LiveProjectionSnapshot) -> LiveContinuityMode {
if snapshot.seed_messages.is_empty() {
LiveContinuityMode::Fresh
} else {
LiveContinuityMode::TranscriptOnly
}
}
async fn abandon_live_open_admission(
machine: &Arc<MeerkatMachine>,
session_id: &SessionId,
channel_id: &LiveChannelId,
) {
if let Err(err) = machine
.abandon_live_open_admission(session_id, channel_id)
.await
{
tracing::warn!(
target: "meerkat_mobkit::live_wiring",
?channel_id,
?session_id,
?err,
"generated live-open admission abandonment failed"
);
}
}
async fn close_live_channel_after_open_failure(
host: &LiveAdapterHost,
machine: &Arc<MeerkatMachine>,
session_id: &SessionId,
channel_id: &LiveChannelId,
) {
match host.reserve_channel_close_observation(channel_id).await {
Ok(observation) => {
let committed = commit_live_close_for_open_failure(
host,
machine,
session_id,
channel_id,
&observation,
)
.await;
if !committed {
abandon_live_open_admission(machine, session_id, channel_id).await;
}
}
Err(LiveAdapterHostError::ChannelNotFound(_)) => {
abandon_live_open_admission(machine, session_id, channel_id).await;
}
Err(err) => {
tracing::warn!(
target: "meerkat_mobkit::live_wiring",
?channel_id,
?session_id,
?err,
"failed to close live channel after open failure; evicting admission"
);
abandon_live_open_admission(machine, session_id, channel_id).await;
}
}
}
async fn commit_live_close_for_open_failure(
host: &LiveAdapterHost,
machine: &Arc<MeerkatMachine>,
session_id: &SessionId,
channel_id: &LiveChannelId,
observation: &LiveChannelCloseObservation,
) -> bool {
let authority = match machine
.resolve_live_close_result(session_id, observation)
.await
{
Ok(authority) => authority,
Err(err) => {
tracing::warn!(
target: "meerkat_mobkit::live_wiring",
?channel_id,
?session_id,
?err,
"generated live-close authority rejected open-failure cleanup; evicting admission"
);
return false;
}
};
let Some(close_commit_authority) = authority.channel_close_commit_authority() else {
tracing::warn!(
target: "meerkat_mobkit::live_wiring",
?channel_id,
?session_id,
"generated live-close result omitted host commit authority; evicting admission"
);
return false;
};
if let Err(err) = host
.commit_channel_close_observation(observation, close_commit_authority)
.await
{
tracing::warn!(
target: "meerkat_mobkit::live_wiring",
?channel_id,
?session_id,
?err,
"host live-close commit failed after generated open-failure cleanup; evicting admission"
);
return false;
}
true
}
#[allow(clippy::too_many_lines)]
async fn handle_live_open<B: SessionAgentBuilder + 'static>(
ctx: &GatewayLiveContext,
service: &Arc<PersistentSessionService<B>>,
machine: &Arc<MeerkatMachine>,
session_id: &SessionId,
params: &Value,
rpc_id: Value,
) -> JsonRpcResponse {
let parsed: GatewayLiveOpenParams = match parse_live_params(params, &rpc_id) {
Ok(p) => p,
Err(resp) => return *resp,
};
match parsed.transport {
None | Some(LiveOpenTransport::Websocket) => {}
Some(_) => {
return live_error(
rpc_id,
INVALID_PARAMS_CODE,
"only the websocket live transport is supported by this gateway",
);
}
}
let turning_mode = parsed
.turning_mode
.unwrap_or(RealtimeTurningMode::ProviderManaged);
let mut open_config =
match live_open_config_for_session(service, session_id, turning_mode).await {
Ok(config) => config,
Err(meerkat_core::SessionError::NotFound { .. }) => {
return live_error(
rpc_id,
INVALID_PARAMS_CODE,
format!("session {session_id} not found"),
);
}
Err(err) => {
return live_error(
rpc_id,
INTERNAL_ERROR_CODE,
format!("failed to build session config: {err}"),
);
}
};
if let Some(model) = parsed.model {
open_config.llm_identity.model = model;
}
if let Err(precheck_err) = precheck_identity(&open_config.llm_identity) {
let (code, message) = match &precheck_err {
LiveOpenPrecheckError::ModelNotRealtime { .. } => {
(INVALID_PARAMS_CODE, precheck_err.to_string())
}
_ => (INTERNAL_ERROR_CODE, precheck_err.to_string()),
};
return live_error(rpc_id, code, message);
}
if !ctx
.session_factory
.supports_provider(open_config.llm_identity.provider)
{
return live_error(
rpc_id,
INTERNAL_ERROR_CODE,
format!(
"provider {} has no live adapter wired in this build",
open_config.llm_identity.provider.as_str()
),
);
}
let live_open_identity = open_config.llm_identity.clone();
let candidate_channel_id = LiveChannelId::random_uuid();
let open_authority = match machine
.resolve_live_open_admission(session_id, &candidate_channel_id, &live_open_identity)
.await
{
Ok(authority) => authority,
Err(err) => {
return live_error(
rpc_id,
INTERNAL_ERROR_CODE,
format!("live open authority rejected admission: {err}"),
);
}
};
if !open_authority.admitted() {
use meerkat_runtime::meerkat_machine::dsl::LiveOpenAdmissionRejection;
return match open_authority.rejection() {
Some(LiveOpenAdmissionRejection::AlreadyBound) => live_error(
rpc_id,
INVALID_PARAMS_CODE,
format!("session {session_id} already has an active live channel"),
),
Some(LiveOpenAdmissionRejection::ChannelAlreadyBound) => live_error(
rpc_id,
INTERNAL_ERROR_CODE,
format!("generated duplicate live channel id {candidate_channel_id}"),
),
None => live_error(
rpc_id,
INTERNAL_ERROR_CODE,
"live open authority rejected admission without a reason",
),
};
}
let Some(channel_open_authority) = open_authority.channel_open_authority() else {
abandon_live_open_admission(machine, session_id, &candidate_channel_id).await;
return live_error(
rpc_id,
INTERNAL_ERROR_CODE,
"live open admission was accepted without a generated host handoff",
);
};
let channel_id = match ctx
.host
.open_channel_with_authority(channel_open_authority)
.await
{
Ok(ch) => ch,
Err(LiveAdapterHostError::SessionAlreadyBound(sid)) => {
abandon_live_open_admission(machine, session_id, &candidate_channel_id).await;
return live_error(
rpc_id,
INTERNAL_ERROR_CODE,
format!(
"live host transport cache still has active channel for session {sid} \
after generated admission"
),
);
}
Err(err) => {
abandon_live_open_admission(machine, session_id, &candidate_channel_id).await;
return live_error(rpc_id, INTERNAL_ERROR_CODE, err.to_string());
}
};
let resolved_audio_config =
live_audio_config_from_capabilities(&ctx.session_factory.capabilities());
let capabilities: LiveChannelCapabilities;
let continuity: LiveContinuityMode;
match ctx.session_factory.open_live_adapter(&open_config).await {
Ok(adapter) => {
capabilities = adapter.capabilities();
if let Err(err) = ctx.host.attach_adapter(&channel_id, adapter).await {
close_live_channel_after_open_failure(&ctx.host, machine, session_id, &channel_id)
.await;
return live_error(
rpc_id,
INTERNAL_ERROR_CODE,
format!("failed to attach adapter: {err}"),
);
}
let snapshot = build_live_projection_snapshot(
session_id,
&open_config,
resolved_audio_config.clone(),
);
continuity = continuity_from_snapshot(&snapshot);
}
Err(err) => {
close_live_channel_after_open_failure(&ctx.host, machine, session_id, &channel_id)
.await;
return live_error(
rpc_id,
INTERNAL_ERROR_CODE,
format!("failed to open provider session: {err}"),
);
}
}
let token = match ctx
.ws_state
.mint_token(session_id, channel_id.clone())
.await
{
Ok(token) => token,
Err(err) => {
close_live_channel_after_open_failure(&ctx.host, machine, session_id, &channel_id)
.await;
return live_error(
rpc_id,
INTERNAL_ERROR_CODE,
format!("live WebSocket token authority rejected issue: {err}"),
);
}
};
let token_str = token.to_string();
let Some(audio_config) = resolved_audio_config.as_ref() else {
close_live_channel_after_open_failure(&ctx.host, machine, session_id, &channel_id).await;
return live_error(
rpc_id,
INTERNAL_ERROR_CODE,
"live websocket transport requires a resolved audio policy; no realtime \
factory audio format was available",
);
};
let Some(format_param) = live_ws_audio_format_param(audio_config) else {
close_live_channel_after_open_failure(&ctx.host, machine, session_id, &channel_id).await;
return live_error(
rpc_id,
INTERNAL_ERROR_CODE,
format!(
"resolved live audio policy (input {}Hz/{}ch) has no websocket binary \
format the transport can negotiate",
audio_config.input_sample_rate_hz, audio_config.input_channels,
),
);
};
let transport = LiveTransportBootstrap::Websocket {
url: format!(
"{base_url}{path}?token={token_str}&channel={channel_id}&format={format_param}",
base_url = ctx.ws_base_url,
path = meerkat_live::LIVE_WS_PATH,
),
token: token_str,
};
let result = LiveOpenResult {
channel_id: channel_id.to_string(),
transport: transport.into(),
capabilities: capabilities.into(),
continuity: continuity.into(),
};
match serde_json::to_value(result) {
Ok(value) => live_success(rpc_id, value),
Err(err) => live_error(
rpc_id,
INTERNAL_ERROR_CODE,
format!("failed to serialize LiveOpenResult: {err}"),
),
}
}
fn live_refresh_result_from_machine_authority(
authority: &meerkat_runtime::meerkat_machine::LiveRefreshResultAuthority,
) -> LiveRefreshResult {
match authority.status {
meerkat_runtime::meerkat_machine::dsl::LiveRefreshPublicStatus::Queued => {
LiveRefreshResult::queued()
}
}
}
fn live_close_result_from_machine_authority(
authority: &meerkat_runtime::meerkat_machine::LiveCloseResultAuthority,
) -> LiveCloseResult {
match authority.status {
meerkat_runtime::meerkat_machine::dsl::LiveClosePublicStatus::Closed => {
LiveCloseResult::closed()
}
}
}
fn live_command_result_from_machine_authority(
authority: &meerkat_runtime::meerkat_machine::LiveCommandResultAuthority,
expected: meerkat_runtime::meerkat_machine::dsl::LiveCommandPublicKind,
) -> Result<Value, String> {
use meerkat_runtime::meerkat_machine::dsl::LiveCommandPublicKind;
if authority.command != expected {
return Err(format!(
"LiveCommandResultResolved emitted command {:?} for expected {:?}",
authority.command, expected
));
}
match expected {
LiveCommandPublicKind::SendInput => serde_json::to_value(LiveSendInputResult::sent())
.map_err(|err| format!("failed to serialize LiveSendInputResult: {err}")),
LiveCommandPublicKind::CommitInput => {
serde_json::to_value(LiveCommitInputResult::committed())
.map_err(|err| format!("failed to serialize LiveCommitInputResult: {err}"))
}
LiveCommandPublicKind::Interrupt => {
serde_json::to_value(LiveInterruptResult::interrupted())
.map_err(|err| format!("failed to serialize LiveInterruptResult: {err}"))
}
LiveCommandPublicKind::TruncateAssistantOutput => {
Err("live/truncate is not surfaced by the mobkit gateway".to_string())
}
}
}
fn live_status_result_from_machine_authority(
channel_id: String,
authority: &meerkat_runtime::meerkat_machine::LiveChannelStatusAuthority,
) -> Result<LiveStatusResult, String> {
Ok(LiveStatusResult {
channel_id,
status: wire_live_status_from_machine_authority(authority)?,
})
}
fn wire_live_status_from_machine_authority(
authority: &meerkat_runtime::meerkat_machine::LiveChannelStatusAuthority,
) -> Result<WireLiveAdapterStatus, String> {
use meerkat_runtime::meerkat_machine::dsl::LiveChannelPublicStatus;
match authority.status {
LiveChannelPublicStatus::Idle => Ok(WireLiveAdapterStatus::Idle),
LiveChannelPublicStatus::Opening => Ok(WireLiveAdapterStatus::Opening),
LiveChannelPublicStatus::Ready => Ok(WireLiveAdapterStatus::Ready),
LiveChannelPublicStatus::Closing => Ok(WireLiveAdapterStatus::Closing),
LiveChannelPublicStatus::Closed => Ok(WireLiveAdapterStatus::Closed),
LiveChannelPublicStatus::Degraded => {
let reason = authority.degradation_reason.ok_or_else(|| {
"LiveChannelStatusResolved emitted degraded status without reason".to_string()
})?;
Ok(WireLiveAdapterStatus::Degraded {
reason: wire_live_degradation_reason_from_machine_authority(
reason,
authority.degradation_detail.as_deref(),
),
})
}
}
}
fn wire_live_degradation_reason_from_machine_authority(
reason: meerkat_runtime::meerkat_machine::dsl::LiveChannelDegradationReason,
detail: Option<&str>,
) -> WireLiveDegradationReason {
use meerkat_runtime::meerkat_machine::dsl::LiveChannelDegradationReason;
match reason {
LiveChannelDegradationReason::RateLimited => WireLiveDegradationReason::RateLimited,
LiveChannelDegradationReason::ProviderThrottled => {
WireLiveDegradationReason::ProviderThrottled
}
LiveChannelDegradationReason::NetworkUnstable => WireLiveDegradationReason::NetworkUnstable,
LiveChannelDegradationReason::Other => WireLiveDegradationReason::Other {
detail: detail.unwrap_or_default().to_string(),
},
LiveChannelDegradationReason::Unknown => WireLiveDegradationReason::Unknown {
debug: detail
.unwrap_or("unknown live channel degradation")
.to_string(),
},
}
}
fn live_command_rejection_response_from_machine_authority(
rpc_id: Value,
authority: &meerkat_runtime::meerkat_machine::LiveCommandRejectionAuthority,
expected: meerkat_runtime::meerkat_machine::dsl::LiveCommandPublicKind,
channel_id: &LiveChannelId,
host_error: &LiveAdapterHostError,
) -> JsonRpcResponse {
use meerkat_runtime::meerkat_machine::dsl::{
LiveCommandRejectionPublicErrorClass, LiveCommandRejectionReason,
};
if authority.command != expected {
return live_error(
rpc_id,
INTERNAL_ERROR_CODE,
format!(
"LiveCommandRejectionResolved emitted command {:?} for expected {:?}",
authority.command, expected
),
);
}
let code = match authority.public_error_class {
LiveCommandRejectionPublicErrorClass::InvalidParams => INVALID_PARAMS_CODE,
LiveCommandRejectionPublicErrorClass::InternalError => INTERNAL_ERROR_CODE,
};
let message = match authority.rejection {
LiveCommandRejectionReason::ChannelNotFound => format!("channel {channel_id} not found"),
LiveCommandRejectionReason::NoAdapter => {
format!("channel {channel_id} has no adapter attached")
}
LiveCommandRejectionReason::ChannelNotReady
| LiveCommandRejectionReason::UnsupportedCommand
| LiveCommandRejectionReason::AdapterError
| LiveCommandRejectionReason::InternalHostError => host_error.to_string(),
};
live_error(rpc_id, code, message)
}
async fn live_command_error_response(
rpc_id: Value,
machine: &Arc<MeerkatMachine>,
session_id: &SessionId,
channel_id: &LiveChannelId,
command: meerkat_runtime::meerkat_machine::dsl::LiveCommandPublicKind,
host_error: &LiveAdapterHostError,
) -> JsonRpcResponse {
let authority = match machine
.resolve_live_command_rejection_result(session_id, channel_id, command, host_error)
.await
{
Ok(authority) => authority,
Err(error) => {
return live_error(
rpc_id,
INTERNAL_ERROR_CODE,
format!("live command rejection authority rejected result: {error}"),
);
}
};
live_command_rejection_response_from_machine_authority(
rpc_id, &authority, command, channel_id, host_error,
)
}
async fn live_unbound_command_error_response(
rpc_id: Value,
machine: &Arc<MeerkatMachine>,
channel_id: &LiveChannelId,
command: meerkat_runtime::meerkat_machine::dsl::LiveCommandPublicKind,
) -> JsonRpcResponse {
let authority = match machine
.resolve_unbound_live_command_rejection_result(channel_id, command)
.await
{
Ok(authority) => authority,
Err(error) => {
return live_error(
rpc_id,
INTERNAL_ERROR_CODE,
format!("unbound live command rejection authority rejected result: {error}"),
);
}
};
let host_error = LiveAdapterHostError::ChannelNotFound(channel_id.clone());
live_command_rejection_response_from_machine_authority(
rpc_id,
&authority,
command,
channel_id,
&host_error,
)
}
fn live_channel_request_rejection_response_from_machine_authority(
rpc_id: Value,
authority: &meerkat_runtime::meerkat_machine::LiveChannelRequestRejectionAuthority,
expected: meerkat_runtime::meerkat_machine::dsl::LiveChannelRequestPublicKind,
channel_id: &LiveChannelId,
detail: Option<String>,
) -> JsonRpcResponse {
use meerkat_runtime::meerkat_machine::dsl::{
LiveChannelRequestRejectionPublicErrorClass, LiveChannelRequestRejectionReason,
};
if authority.request != expected {
return live_error(
rpc_id,
INTERNAL_ERROR_CODE,
format!(
"LiveChannelRequestRejectionResolved emitted request {:?} for expected {:?}",
authority.request, expected
),
);
}
let code = match authority.public_error_class {
LiveChannelRequestRejectionPublicErrorClass::InvalidParams => INVALID_PARAMS_CODE,
LiveChannelRequestRejectionPublicErrorClass::InternalError => INTERNAL_ERROR_CODE,
};
let message = match authority.rejection {
LiveChannelRequestRejectionReason::ChannelNotFound => {
format!("channel {channel_id} not found")
}
LiveChannelRequestRejectionReason::NoAdapter => {
format!("channel {channel_id} has no adapter attached")
}
LiveChannelRequestRejectionReason::InvalidToken
| LiveChannelRequestRejectionReason::InvalidPayload
| LiveChannelRequestRejectionReason::WebrtcAnswerError
| LiveChannelRequestRejectionReason::InternalHostError => detail.unwrap_or_else(|| {
format!(
"live channel request {:?} rejected for channel {}",
authority.request, channel_id
)
}),
};
live_error(rpc_id, code, message)
}
async fn live_channel_request_error_response(
rpc_id: Value,
machine: &Arc<MeerkatMachine>,
session_id: &SessionId,
channel_id: &LiveChannelId,
request: meerkat_runtime::meerkat_machine::dsl::LiveChannelRequestPublicKind,
host_error: &LiveAdapterHostError,
) -> JsonRpcResponse {
let authority = match machine
.resolve_live_channel_request_rejection_result(session_id, channel_id, request, host_error)
.await
{
Ok(authority) => authority,
Err(error) => {
return live_error(
rpc_id,
INTERNAL_ERROR_CODE,
format!("live channel request rejection authority rejected result: {error}"),
);
}
};
live_channel_request_rejection_response_from_machine_authority(
rpc_id,
&authority,
request,
channel_id,
Some(host_error.to_string()),
)
}
async fn live_unbound_channel_request_error_response(
rpc_id: Value,
machine: &Arc<MeerkatMachine>,
channel_id: &LiveChannelId,
request: meerkat_runtime::meerkat_machine::dsl::LiveChannelRequestPublicKind,
) -> JsonRpcResponse {
let authority = match machine
.resolve_unbound_live_channel_request_rejection_result(channel_id, request)
.await
{
Ok(authority) => authority,
Err(error) => {
return live_error(
rpc_id,
INTERNAL_ERROR_CODE,
format!(
"unbound live channel request rejection authority rejected result: {error}"
),
);
}
};
let host_error = LiveAdapterHostError::ChannelNotFound(channel_id.clone());
live_channel_request_rejection_response_from_machine_authority(
rpc_id,
&authority,
request,
channel_id,
Some(host_error.to_string()),
)
}
async fn handle_live_status(
ctx: &GatewayLiveContext,
machine: &Arc<MeerkatMachine>,
params: &Value,
rpc_id: Value,
) -> JsonRpcResponse {
let parsed: LiveChannelParams = match parse_live_params(params, &rpc_id) {
Ok(p) => p,
Err(resp) => return *resp,
};
let channel_id = LiveChannelId::new(&parsed.channel_id);
let request_kind = meerkat_runtime::meerkat_machine::dsl::LiveChannelRequestPublicKind::Status;
let Some(session_id) = machine.live_session_for_status_channel(&channel_id).await else {
return live_unbound_channel_request_error_response(
rpc_id,
machine,
&channel_id,
request_kind,
)
.await;
};
match ctx.host.channel_status_observation(&channel_id).await {
Ok(observation) => {
let authority = match machine
.resolve_live_channel_status_result(&session_id, &observation)
.await
{
Ok(authority) => authority,
Err(error) => {
return live_error(
rpc_id,
INTERNAL_ERROR_CODE,
format!("live status authority rejected result: {error}"),
);
}
};
let result =
match live_status_result_from_machine_authority(parsed.channel_id, &authority) {
Ok(result) => result,
Err(error) => return live_error(rpc_id, INTERNAL_ERROR_CODE, error),
};
match serde_json::to_value(result) {
Ok(value) => live_success(rpc_id, value),
Err(err) => live_error(
rpc_id,
INTERNAL_ERROR_CODE,
format!("failed to serialize LiveStatusResult: {err}"),
),
}
}
Err(err) => {
live_channel_request_error_response(
rpc_id,
machine,
&session_id,
&channel_id,
request_kind,
&err,
)
.await
}
}
}
async fn handle_live_close(
ctx: &GatewayLiveContext,
machine: &Arc<MeerkatMachine>,
params: &Value,
rpc_id: Value,
) -> JsonRpcResponse {
let parsed: LiveChannelParams = match parse_live_params(params, &rpc_id) {
Ok(p) => p,
Err(resp) => return *resp,
};
let channel_id = LiveChannelId::new(&parsed.channel_id);
let request_kind = meerkat_runtime::meerkat_machine::dsl::LiveChannelRequestPublicKind::Close;
let Some(session_id) = machine.live_session_for_active_channel(&channel_id).await else {
return live_unbound_channel_request_error_response(
rpc_id,
machine,
&channel_id,
request_kind,
)
.await;
};
match ctx
.host
.reserve_channel_close_observation(&channel_id)
.await
{
Ok(observation) => {
let authority = match machine
.resolve_live_close_result(&session_id, &observation)
.await
{
Ok(authority) => authority,
Err(error) => {
return live_error(
rpc_id,
INTERNAL_ERROR_CODE,
format!("live close authority rejected result: {error}"),
);
}
};
let Some(close_commit_authority) = authority.channel_close_commit_authority() else {
return live_error(
rpc_id,
INTERNAL_ERROR_CODE,
"live close authority omitted host commit handoff",
);
};
if let Err(error) = ctx
.host
.commit_channel_close_observation(&observation, close_commit_authority)
.await
{
return live_error(
rpc_id,
INTERNAL_ERROR_CODE,
format!("live close host commit failed after generated authority: {error}"),
);
}
let result = live_close_result_from_machine_authority(&authority);
match serde_json::to_value(result) {
Ok(body) => live_success(rpc_id, body),
Err(error) => live_error(
rpc_id,
INTERNAL_ERROR_CODE,
format!("live close authority projection failed: {error}"),
),
}
}
Err(err) => {
live_channel_request_error_response(
rpc_id,
machine,
&session_id,
&channel_id,
request_kind,
&err,
)
.await
}
}
}
async fn handle_live_refresh<B: SessionAgentBuilder + 'static>(
ctx: &GatewayLiveContext,
service: &Arc<PersistentSessionService<B>>,
machine: &Arc<MeerkatMachine>,
params: &Value,
rpc_id: Value,
) -> JsonRpcResponse {
let parsed: LiveChannelParams = match parse_live_params(params, &rpc_id) {
Ok(p) => p,
Err(resp) => return *resp,
};
let channel_id = LiveChannelId::new(&parsed.channel_id);
let request_kind = meerkat_runtime::meerkat_machine::dsl::LiveChannelRequestPublicKind::Refresh;
let Some(session_id) = machine.live_session_for_active_channel(&channel_id).await else {
return live_unbound_channel_request_error_response(
rpc_id,
machine,
&channel_id,
request_kind,
)
.await;
};
let open_config = match live_open_config_for_session(
service,
&session_id,
RealtimeTurningMode::ProviderManaged,
)
.await
{
Ok(config) => config,
Err(err) => {
return live_error(
rpc_id,
INTERNAL_ERROR_CODE,
format!("failed to build session config: {err}"),
);
}
};
let mut snapshot = build_live_projection_snapshot(&session_id, &open_config, None);
match ctx.host.next_snapshot_version(&channel_id).await {
Ok(v) => snapshot.snapshot_version = v,
Err(err) => {
return live_channel_request_error_response(
rpc_id,
machine,
&session_id,
&channel_id,
request_kind,
&err,
)
.await;
}
}
match ctx.host.enqueue_refresh(&channel_id, snapshot).await {
Ok(acceptance) => {
let authority = match machine
.resolve_live_refresh_queued_result(&session_id, &acceptance)
.await
{
Ok(authority) => authority,
Err(error) => {
return live_error(
rpc_id,
INTERNAL_ERROR_CODE,
format!("live refresh queued authority rejected result: {error}"),
);
}
};
let result = live_refresh_result_from_machine_authority(&authority);
match serde_json::to_value(result) {
Ok(body) => live_success(rpc_id, body),
Err(error) => live_error(
rpc_id,
INTERNAL_ERROR_CODE,
format!("live refresh queued authority projection failed: {error}"),
),
}
}
Err(err) => {
live_channel_request_error_response(
rpc_id,
machine,
&session_id,
&channel_id,
request_kind,
&err,
)
.await
}
}
}
async fn handle_live_send_input(
ctx: &GatewayLiveContext,
machine: &Arc<MeerkatMachine>,
params: &Value,
rpc_id: Value,
) -> JsonRpcResponse {
let parsed: LiveSendInputParams = match parse_live_params(params, &rpc_id) {
Ok(p) => p,
Err(resp) => return *resp,
};
let channel_id = LiveChannelId::new(&parsed.channel_id);
let chunk = match live_input_chunk_from_wire(parsed.chunk) {
Ok(chunk) => chunk,
Err(err) => {
return live_error(rpc_id, INVALID_PARAMS_CODE, err.to_string());
}
};
let command_kind = meerkat_runtime::meerkat_machine::dsl::LiveCommandPublicKind::SendInput;
let Some(session_id) = machine.live_session_for_active_channel(&channel_id).await else {
return live_unbound_command_error_response(rpc_id, machine, &channel_id, command_kind)
.await;
};
match ctx.host.send_input_observed(&channel_id, chunk).await {
Ok(acceptance) => {
let authority = match machine
.resolve_live_command_result(&session_id, &acceptance)
.await
{
Ok(authority) => authority,
Err(error) => {
return live_error(
rpc_id,
INTERNAL_ERROR_CODE,
format!("live send_input authority rejected result: {error}"),
);
}
};
match live_command_result_from_machine_authority(&authority, command_kind) {
Ok(value) => live_success(rpc_id, value),
Err(error) => live_error(rpc_id, INTERNAL_ERROR_CODE, error),
}
}
Err(err) => {
live_command_error_response(
rpc_id,
machine,
&session_id,
&channel_id,
command_kind,
&err,
)
.await
}
}
}
async fn handle_live_commit_input(
ctx: &GatewayLiveContext,
machine: &Arc<MeerkatMachine>,
params: &Value,
rpc_id: Value,
) -> JsonRpcResponse {
let parsed: LiveCommitInputParams = match parse_live_params(params, &rpc_id) {
Ok(p) => p,
Err(resp) => return *resp,
};
let channel_id = LiveChannelId::new(&parsed.channel_id);
let response_modality = match parsed.response_modality.map(TryInto::try_into) {
Some(Ok(modality)) => Some(modality),
Some(Err(err)) => {
return live_error(
rpc_id,
INVALID_PARAMS_CODE,
format!("invalid response_modality: {err}"),
);
}
None => None,
};
let command_kind = meerkat_runtime::meerkat_machine::dsl::LiveCommandPublicKind::CommitInput;
let Some(session_id) = machine.live_session_for_active_channel(&channel_id).await else {
return live_unbound_command_error_response(rpc_id, machine, &channel_id, command_kind)
.await;
};
match ctx
.host
.send_command_observed(
&channel_id,
LiveAdapterCommand::CommitInput { response_modality },
)
.await
{
Ok(acceptance) => {
let authority = match machine
.resolve_live_command_result(&session_id, &acceptance)
.await
{
Ok(authority) => authority,
Err(error) => {
return live_error(
rpc_id,
INTERNAL_ERROR_CODE,
format!("live commit_input authority rejected result: {error}"),
);
}
};
match live_command_result_from_machine_authority(&authority, command_kind) {
Ok(value) => live_success(rpc_id, value),
Err(error) => live_error(rpc_id, INTERNAL_ERROR_CODE, error),
}
}
Err(err) => {
live_command_error_response(
rpc_id,
machine,
&session_id,
&channel_id,
command_kind,
&err,
)
.await
}
}
}
async fn handle_live_interrupt(
ctx: &GatewayLiveContext,
machine: &Arc<MeerkatMachine>,
params: &Value,
rpc_id: Value,
) -> JsonRpcResponse {
let parsed: LiveChannelParams = match parse_live_params(params, &rpc_id) {
Ok(p) => p,
Err(resp) => return *resp,
};
let channel_id = LiveChannelId::new(&parsed.channel_id);
let command_kind = meerkat_runtime::meerkat_machine::dsl::LiveCommandPublicKind::Interrupt;
let Some(session_id) = machine.live_session_for_active_channel(&channel_id).await else {
return live_unbound_command_error_response(rpc_id, machine, &channel_id, command_kind)
.await;
};
match ctx
.host
.send_command_observed(&channel_id, LiveAdapterCommand::Interrupt)
.await
{
Ok(acceptance) => {
let authority = match machine
.resolve_live_command_result(&session_id, &acceptance)
.await
{
Ok(authority) => authority,
Err(error) => {
return live_error(
rpc_id,
INTERNAL_ERROR_CODE,
format!("live interrupt authority rejected result: {error}"),
);
}
};
match live_command_result_from_machine_authority(&authority, command_kind) {
Ok(value) => live_success(rpc_id, value),
Err(error) => live_error(rpc_id, INTERNAL_ERROR_CODE, error),
}
}
Err(err) => {
live_command_error_response(
rpc_id,
machine,
&session_id,
&channel_id,
command_kind,
&err,
)
.await
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
fn test_session_id() -> SessionId {
SessionId::parse("00000000-0000-0000-0000-000000000001").unwrap()
}
fn other_session_id() -> SessionId {
SessionId::parse("00000000-0000-0000-0000-000000000002").unwrap()
}
#[test]
fn session_error_maps_to_distinct_typed_projection_variants() {
let id = SessionId::new();
assert!(matches!(
session_error_to_projection(
meerkat_core::SessionError::NotFound { id: id.clone() },
&id,
),
LiveProjectionError::SessionNotFound(_)
));
assert!(matches!(
session_error_to_projection(
meerkat_core::SessionError::Unsupported("nope".to_string()),
&id,
),
LiveProjectionError::Rejected(_)
));
assert!(matches!(
session_error_to_projection(meerkat_core::SessionError::Busy { id: id.clone() }, &id),
LiveProjectionError::SessionBusy(_)
));
assert!(matches!(
session_error_to_projection(
meerkat_core::SessionError::NotRunning { id: id.clone() },
&id,
),
LiveProjectionError::SessionNotRunning(_)
));
assert!(matches!(
session_error_to_projection(meerkat_core::SessionError::PersistenceDisabled, &id),
LiveProjectionError::CapabilityDisabled { .. }
));
}
#[test]
fn assistant_text_delta_helper_builds_text_delta_event() {
let identity = LiveTranscriptIdentity {
provider_item_id: Some("item_text"),
previous_item_id: Some("item_prev"),
content_index: Some(2),
response_id: Some("resp_text"),
delta_id: Some("delta_text"),
};
let event = build_assistant_text_delta_event("display fragment", identity)
.expect("complete identity must build a typed delta event");
match event {
RealtimeTranscriptEvent::AssistantTextDelta {
response_id,
delta_id,
item_id,
previous_item_id,
content_index,
delta,
} => {
assert_eq!(response_id, "resp_text");
assert_eq!(delta_id, "delta_text");
assert_eq!(item_id, "item_text");
assert_eq!(previous_item_id.as_deref(), Some("item_prev"));
assert_eq!(content_index, 2);
assert_eq!(delta, "display fragment");
}
other => panic!("display-text delta path must build AssistantTextDelta, got {other:?}"),
}
}
#[test]
fn assistant_transcript_delta_helper_builds_transcript_delta_event() {
let identity = LiveTranscriptIdentity {
provider_item_id: Some("item_tx"),
previous_item_id: Some("item_prev"),
content_index: Some(0),
response_id: Some("resp_tx"),
delta_id: Some("delta_tx"),
};
let event = build_assistant_transcript_delta_event("spoken fragment", identity)
.expect("complete identity must build a typed transcript delta event");
match event {
RealtimeTranscriptEvent::AssistantTranscriptDelta {
response_id,
delta_id,
item_id,
previous_item_id,
content_index,
delta,
} => {
assert_eq!(response_id, "resp_tx");
assert_eq!(delta_id, "delta_tx");
assert_eq!(item_id, "item_tx");
assert_eq!(previous_item_id.as_deref(), Some("item_prev"));
assert_eq!(content_index, 0);
assert_eq!(delta, "spoken fragment");
}
other => panic!(
"spoken-transcript delta path must build AssistantTranscriptDelta, got {other:?}"
),
}
}
#[test]
fn missing_delta_identity_fails_closed_typed() {
let missing_response = LiveTranscriptIdentity {
provider_item_id: Some("item"),
previous_item_id: None,
content_index: Some(0),
response_id: None,
delta_id: Some("delta"),
};
assert_eq!(
build_assistant_text_delta_event("fragment", missing_response),
Err(LiveTranscriptIdentityError::MissingResponseId)
);
let missing_delta = LiveTranscriptIdentity {
provider_item_id: Some("item"),
previous_item_id: None,
content_index: Some(0),
response_id: Some("resp"),
delta_id: None,
};
assert_eq!(
build_assistant_transcript_delta_event("fragment", missing_delta),
Err(LiveTranscriptIdentityError::MissingDeltaId)
);
let missing_item = LiveTranscriptIdentity {
provider_item_id: None,
previous_item_id: None,
content_index: Some(0),
response_id: Some("resp"),
delta_id: Some("delta"),
};
assert_eq!(
build_assistant_text_delta_event("fragment", missing_item),
Err(LiveTranscriptIdentityError::MissingItemId)
);
}
#[test]
fn r6_pending_turn_ledger_keys_on_response_id() {
let ledger = PendingTurnLedger::default();
let session_id = test_session_id();
ledger.buffer(
&session_id,
Some("resp_a"),
PendingAssistantContent::Text("from resp_a".to_string()),
);
ledger.buffer(
&session_id,
Some("resp_b"),
PendingAssistantContent::Text("from resp_b".to_string()),
);
assert!(
ledger
.drain(&session_id, Some("resp_stale"))
.blocks
.is_empty()
);
let drained_a = collapse_pending_blocks(ledger.drain(&session_id, Some("resp_a")).blocks);
assert_eq!(drained_a.len(), 1);
match &drained_a[0] {
AssistantBlock::Text { text, .. } => assert_eq!(text, "from resp_a"),
other => panic!("expected text block, got {other:?}"),
}
let drained_b = collapse_pending_blocks(ledger.drain(&session_id, Some("resp_b")).blocks);
assert_eq!(drained_b.len(), 1);
match &drained_b[0] {
AssistantBlock::Text { text, .. } => assert_eq!(text, "from resp_b"),
other => panic!("expected text block, got {other:?}"),
}
assert!(ledger.drain(&session_id, Some("resp_a")).blocks.is_empty());
assert!(ledger.drain(&session_id, Some("resp_b")).blocks.is_empty());
}
#[test]
fn terminal_error_drains_all_response_slots_for_session_only() {
let ledger = PendingTurnLedger::default();
let session_id = test_session_id();
let other = other_session_id();
ledger.buffer(
&session_id,
Some("resp_a"),
PendingAssistantContent::Text("a".to_string()),
);
ledger.buffer(
&session_id,
None,
PendingAssistantContent::Text("orphan".to_string()),
);
ledger.buffer(
&other,
Some("resp_x"),
PendingAssistantContent::Text("x".to_string()),
);
ledger.drain_all(&session_id);
assert!(ledger.drain(&session_id, Some("resp_a")).blocks.is_empty());
assert!(ledger.drain(&session_id, None).blocks.is_empty());
let survived = ledger.drain(&other, Some("resp_x"));
assert_eq!(survived.blocks.len(), 1);
}
#[test]
fn collapse_pending_blocks_coalesces_fragments_in_arrival_order() {
let blocks = collapse_pending_blocks(vec![
PendingAssistantContent::Text("part one ".to_string()),
PendingAssistantContent::Text("part two".to_string()),
]);
assert_eq!(blocks.len(), 1);
match &blocks[0] {
AssistantBlock::Text { text, .. } => assert_eq!(text, "part one part two"),
other => panic!("expected Text block, got {other:?}"),
}
assert!(collapse_pending_blocks(Vec::new()).is_empty());
}
#[test]
fn ws_token_admission_mapping_covers_all_machine_variants() {
use meerkat_runtime::meerkat_machine::dsl::{
LiveWebsocketTokenAdmissionPublicErrorClass as DslClass,
LiveWebsocketTokenAdmissionRejection as Dsl,
};
let cases = [
(
Dsl::TokenNotFound,
LiveWsTokenAdmissionRejection::TokenNotFound,
),
(
Dsl::TokenExpired,
LiveWsTokenAdmissionRejection::TokenExpired,
),
(
Dsl::TokenChannelMismatch,
LiveWsTokenAdmissionRejection::TokenChannelMismatch,
),
(
Dsl::TokenAlreadyConsumed,
LiveWsTokenAdmissionRejection::TokenAlreadyConsumed,
),
(
Dsl::ChannelNotBound,
LiveWsTokenAdmissionRejection::ChannelNotBound,
),
];
for (machine, transport) in cases {
assert_eq!(
live_ws_token_admission_rejection_from_machine(machine),
transport
);
}
assert_eq!(
live_ws_token_public_error_class_from_machine(DslClass::InvalidToken),
LiveWsTokenAdmissionPublicErrorClass::InvalidToken
);
}
#[tokio::test]
async fn env_realtime_config_source_returns_given_config() {
let mut config = Config::default();
config.realm.insert(
"live-wiring-test".to_string(),
meerkat_core::RealmConfigSection::default(),
);
let source = EnvRealtimeConfigSource::new(config);
let served = source.current_config().await.expect("static config source");
assert!(served.realm.contains_key("live-wiring-test"));
let served_again = source.current_config().await.expect("static config source");
assert!(served_again.realm.contains_key("live-wiring-test"));
}
}