Cosmo Realtime SDK
Guides

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-call to its tool-result using tool_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-calltool-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:

  1. The credential is invalid or expired — agent.start() rejects with a typed session-start error; check its code and message.
  2. The base URL is wrong — the default is https://app.askcosmo.ai; the SDK composes the external session endpoints from it.
  3. 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

On this page