Debugging
Read session IDs from the ready event, correlate per-message IDs across the control plane, and trace a session end-to-end.
Every session and every message carries a stable identifier. Use them to correlate events across browser devtools, server logs, and the Cosmo dashboard.
Session ID
The server-minted session ID arrives on the ready event once the agent is live. It is the primary key for filtering server logs.
TypeScript
import { RealtimeClient } from 'cosmo-ai';
const client = new RealtimeClient({ baseUrl: 'https://app.askcosmo.ai', token });
const session = await client.agent({ /* config */ }).start();
session.on('ready', ({ sessionId, rejectedTools }) => {
console.log('session_id:', sessionId);
// Store this if you want to correlate with server-side logs or
// file a support ticket.
});If you need the ID before ready (e.g. to poll per-session REST endpoints), it is available the instant the session-start POST returns: subscribe to session_started, or read session.sessionId after agent.start() resolves.
In a React app, the ready event fires once per session. Subscribe from an effect:
import { useEffect } from 'react';
import { useRealtimeClient } from 'cosmo-ai';
function SessionLogger() {
const client = useRealtimeClient();
useEffect(() => {
const unsub = client.on('ready', (event) => {
console.log('[session]', {
sessionId: event.sessionId,
rejectedTools: event.rejectedTools,
maxSessionSeconds: event.maxSessionSeconds,
});
});
return unsub;
}, [client]);
return null;
}Python
from cosmo_ai import CosmoRealtime, RealtimeReady
import structlog
logger = structlog.get_logger(__name__)
async with CosmoRealtime(api_key="cosmo_...") as client:
async with client.agent().start() as session:
async for event in session:
if isinstance(event, RealtimeReady):
logger.info(
"session.ready",
session_id=event.session_id,
rejected_tools=[t.name for t in event.rejected_tools],
)session.session_id is also available directly as soon as agent.start() returns.
Swift
for try await event in session.events {
if case .ready(let ready) = event {
print("session_id=\(ready.sessionId)")
}
}session.sessionId (optional, set once the start succeeds) is available without waiting for the event.
Message IDs
Every wire message carries an id field — a UUID4 hex string generated by the sender. The IDs are unique per message, not per session. Use them to:
- Match a
tool-callto itstool-resultusingtool_call_id. - Confirm that a specific request was received by the server (search the server-side structured logs for the
id). - Debug dropped messages in high-throughput image-streaming sessions.
The normalized callback events (TS session.on('transcript', …)) carry UI-shaped payloads; to see raw wire frames with their ids, use the stream form — for await (const event of session) in TypeScript, async for event in session in Python (each Pydantic event model exposes id directly), for try await event in session.events in Swift.
from cosmo_ai import RealtimeTranscriptDelta
async for event in session:
if isinstance(event, RealtimeTranscriptDelta):
logger.info(
"transcript.delta",
message_id=event.id,
role=event.role,
text=event.text,
is_final=event.is_final,
)Catch-all event logger
Python
The session stream already carries every server event — log them all with one loop:
async for event in session:
logger.debug("realtime.event", event_type=getattr(event, "type", type(event).__name__))Unrecognized frames surface as UnknownEvent (with raw_type and the raw payload), so a catch-all logger also shows anything a newer server sends.
TypeScript
Iterate the session for the wire-level stream:
for await (const event of session) {
console.debug('[realtime]', event.type, event);
}Or approximate a catch-all on the callback surface by subscribing to the known event names:
const ALL_EVENTS = [
'transport_state', 'agent_state', 'media_state', 'lifecycle',
'transcript', 'model_text', 'tool_call', 'tool_dispatch_started', 'tool_result',
'volume', 'error', 'ready', 'session_started', 'reconnecting',
'session_ending_soon', 'session_ended', 'turn_complete', 'pong',
] as const;
for (const event of ALL_EVENTS) {
session.on(event, (payload) => {
console.debug(`[realtime:${event}]`, payload);
});
}Correlating tool calls
Each tool-call → tool-result pair shares a tool_call_id distinct from the per-message id:
from cosmo_ai import RealtimeToolCall, RealtimeToolResult
active_calls: dict[str, str] = {} # tool_call_id → name
async for event in session:
match event:
case RealtimeToolCall():
active_calls[event.tool_call_id] = event.name
logger.info("tool.call", name=event.name, call_id=event.tool_call_id)
case RealtimeToolResult():
name = active_calls.pop(event.tool_call_id, "unknown")
logger.info(
"tool.result",
name=name,
call_id=event.tool_call_id,
ok=event.ok,
summary=event.summary,
)Common issues
transportState stuck at 'connecting'
The ready event has not arrived. Common causes:
- The credential is invalid or expired —
agent.start()rejects with a typed session-start error; check its code and message. - The base URL is wrong — the default is
https://app.askcosmo.ai; the SDK composes the external session endpoints from it. - A firewall or corporate proxy is blocking the WebRTC connection.
session.on('error', (err) => {
if (!err) return;
console.error('session error', err.code, err.message);
});mic_denied error
The browser rejected getUserMedia. The user must grant microphone permission in browser settings. The error code 'mic_denied' is set on RealtimeError.code.
rejected_tools in the ready event
Tool specs you passed on the agent's tools list were refused by the server — an unknown server-tool name, a sanitized schema, or a cap. Each entry carries the name and a reason. Double-check names against the server-side tools table; the session runs without the refused tools.
Next steps
- Reconnects — the session ID survives an upstream rotation; log it once per session
- Handle tool calls — tool_call_id correlation in detail
- Server-side tools — verify tool names before the session starts
Reconnects
How the server transparently rotates the upstream model session, what the reconnecting wire event means, and how to surface reconnecting state in each SDK.
Coach
A real-time presentation coach that watches the user via webcam and gives spoken feedback. Shows how to stream camera frames via addVideoStream and the Python equivalent.