Cosmo Realtime SDK
ReferenceTypescript

RealtimeClient

Core classes of the TypeScript SDK — RealtimeClient, RealtimeAgent, and RealtimeSession.

RealtimeClient

RealtimeClient owns one logical realtime session: the transport (mic + data channel), normalized state, and the typed event stream. Two instances can coexist without interfering — each owns its own transport, analyser nodes, and emitter.

Sessions open through the agent surface: build a persona with client.agent({...}) (or client.catalogAgent(name)), then agent.start() returns a RealtimeSession. For React apps, CosmoRealtimeProvider wraps this class.

import { RealtimeClient } from 'cosmo-ai';

const client = new RealtimeClient({
  baseUrl: 'https://app.askcosmo.ai',
  token: mintedJwt, // from mintToken() on your backend
});

const agent = client.agent({
  instructions: 'You are a concise support agent.',
  voice: 'Puck',
});
const session = await agent.start();
await session.waitUntilReady();
await session.sendText('Hello');

Constructor

new RealtimeClient(options?: RealtimeClientOptions)

RealtimeClientOptions

FieldTypeDescription
baseUrlstringOrigin of the Cosmo external API (e.g. https://app.askcosmo.ai). Must be https:// (http:// allowed only for localhost). Omit in a browser to use the page's own origin.
apiKeystringWorkspace-scoped API key — a server-side secret. Can mint end-user tokens (mintToken) and open sessions. Requires baseUrl.
tokenstringA minted end-user JWT (from mintToken), scoped to one external user. Safe for a browser/device; can open sessions but cannot mint.
getAuthHeaders() => Record<string,string> | Promise<Record<string,string>>Resolve auth headers for every Cosmo API request. Sync or async. When apiKey/token is also set, the credential's Authorization header wins.
transportFactory() => RealtimeTransportOverride the underlying transport, constructed per session start. Defaults to LiveKitTransport.
screenInteractionScreenInteractionOpt into grounded screen control: capture/activate/highlight for this platform. Omit and those tools are never offered. See /multimodal/screen-interaction.

Provide at most one of apiKey / token — supplying both throws RealtimeCredentialError, as does apiKey without baseUrl.


Auth

mintToken(externalUserId: string): Promise<MintedToken>

Mint a short-lived end-user token. Run this on your backend with an apiKey client; hand the returned jwt to the end user's browser, which constructs new RealtimeClient({ token: jwt }). Idempotent per (workspace, externalUserId).

Throws RealtimeMintTokenError if this client has no apiKey, or the server rejects the mint.

type MintedToken = { jwt: string; expiresAt: Date };

See /production/end-user-credentials.

verify(): Promise<CredentialInfo>

Check the credential without starting a session — free, and works with either credential. Resolving at all means the credential authenticated; canStartSessions reports whether it carries realtime:use, and realtimeVoiceAvailable whether this deployment has the default voice stack configured.

Throws RealtimeVerifyError when the server rejects the credential, or with code: 'no_origin' when there is no baseUrl and no page origin. An under-scoped credential does not throw.

type CredentialInfo = {
  credential: 'api_key' | 'user_token';
  /** Null for a minted token — an end user is not told the workspace. */
  workspace: { name: string; slug: string } | null;
  scopes: string[];
  canStartSessions: boolean;
  realtimeVoiceAvailable: boolean;
  externalUserId: string | null;
};

See /auth/api-keys.


Building agents

agent(config?: AgentConfig): RealtimeAgent

Build a reusable persona — immutable; derive variants with agent.with({...}) and open runs with agent.start(). Fields left unset fall through to the server-side protocol defaults.

catalogAgent(name: string, opts?: CatalogAgentOptions): RealtimeAgent

Build an agent that runs a workspace catalog agent by machine handle; the stored config runs verbatim. Only per-run ride-alongs are accepted — no persona parameters except voice, the one cosmetic override; anything else is a type error.

See RealtimeAgent below for both config shapes.


Lifecycle

isActive(): boolean

true when a transport connection is open.

getSessionId(): string | null

Server-minted session id, available the instant the session-start POST returns (before ready); null when no session started.

getLifecycleState(): SessionLifecycleState

Formal connection lifecycle (idle → connecting → connected ↔ reconnecting → disconnected) with typed end reasons. See /concepts/lifecycle.

getSnapshot(): RealtimeSnapshot

Shallow clone of the current normalized state:

type RealtimeSnapshot = {
  transportState: TransportState;
  agentState: AgentState;
  mediaState: MediaState;
  error: RealtimeError | null;
};

waitUntilReady(): Promise<void>

Resolves when the current session reaches transportState === 'ready'. Resolves immediately if already ready; rejects if the session ends before becoming ready. Use between agent.start() and the first sendText() to avoid the RealtimeNotReadyError race.

disconnect(): Promise<void>

Gracefully end the session: sends the wire end frame and leaves the room. The session stream finishes with reason client ended. Idempotent.

close(): Promise<void>

Abrupt local teardown without the graceful wire end frame; the stream finishes with reason client closed. Idempotent.

on<E>(event: E, handler: (payload: RealtimeEventMap[E]) => void): Unsubscribe

Subscribe to a typed event. Returns an unsubscribe function.

const unsub = client.on('transcript', (event) => {
  console.log(event.role, event.text, event.isFinal);
});

Event map

EventPayloadWhen
transport_stateTransportStateWire-connectivity transition
agent_stateAgentStateAgent activity transition
media_stateMediaStateMic / screen / output change
lifecycleSessionLifecycleStateFormal session lifecycle transition
transcriptTranscriptEventStreaming transcript delta
model_textModelTextEventModel's text-channel output (not spoken audio)
tool_callToolCallEventModel decided to invoke a tool
tool_dispatch_startedToolDispatchStartedEventServer began dispatching a tool
tool_resultToolResultEventTool completed
session_stateSessionStateEventDurable session state changed (server-side set_state)
volumeVolumeEvent{ mic, output } RMS levels, per animation frame
errorRealtimeError | nullError emitted or cleared
readyReadyEventOnce per session when the server sends ready
session_started{ sessionId: string }Once per session, the instant the session-start POST returns — before ready
reconnectingReconnectingEventServer is rotating the upstream model; session stays live
session_ending_soonSessionEndingSoonEventServer will end the session shortly (e.g. max-duration cap)
session_endedSessionEndedEventServer ended the session on purpose — clean terminal, not a failure
turn_completeTurnCompleteEventEnd-of-turn marker
pongPongEventReply to sendPing()
user_speech_timeoutUserSpeechTimeoutEventA server-runtime silence timeout fired

See /concepts/events for payload shapes.


Messaging

sendText(content: string, options?: { transcript?: boolean }): Promise<void>

Send a text turn — the agent answers it. Requires transportState === 'ready'; throws RealtimeNotReadyError otherwise. Empty strings are silently dropped.

transcript: false skips the optimistic transcript event (role: 'user') the SDK otherwise emits after delivery, so context notes the user never typed stay out of the UI. The turn still happens — to hand the agent information without provoking a reply, use sendContext.

sendContext(content: string): Promise<void>

Give the agent context without asking it anything. Requires transportState === 'ready'; throws RealtimeNotReadyError otherwise. Empty strings are silently dropped, and the server rejects a note longer than 4096 characters rather than truncating it.

The note rides the provider's pre-turn channel (turn_complete=false), so the model is never asked for a response: no speech, no assistant turn, no interruption of what it is currently saying — it simply knows this the next time it answers. Nothing is added to the transcript either. Use it for live application state (scroll position, selection, current record, form values); a note that arrives while the agent is speaking is delivered when it stops, and if several arrive in that window only the newest is — the latest state is the true one.

sendImage(args: { data: string; mimeType?: string; streamId?: string }): Promise<void>

Send a single image frame as base64 JSON (data is base64-encoded bytes, not raw bytes). mimeType defaults to 'image/jpeg', streamId to 'video.input.default'. Use for one-shot captures; for continuous streams prefer startScreenShare / video tracks. See /multimodal/image-input.

sendPing(): Promise<void>

Send a keep-alive ping. The server replies with a pong event.

activityEnd(): Promise<void>

Signal end-of-turn for manual-VAD turn-taking: the agent stops waiting for further user audio and responds to what it has. Distinct from ending the session. See /concepts/turn-taking.


Telephony

dial(phoneNumber: string, callerNumber?: string): Promise<DialResult>

Place an outbound phone call into this live session's room. The dialed party joins as a SIP participant and the agent converses with them. phoneNumber (and optional callerNumber) must be E.164 (+ then 8–15 digits). Resolves once the dial is queued (DialResult = { dialId: string }); the call rings asynchronously.

Throws RealtimeDialError for a malformed number, an unresolvable API origin (not_dialable), or a server rejection (phone calls disabled, over the minute limit, …); RealtimeNotReadyError when no session has started. See /telephony.


Microphone and audio output

setMicMuted(muted: boolean): Promise<void>

Toggle the mic — the local track and the server-side gate. Requires ready. Optimistic: state updates first and rolls back on failure, emitting a server_error.

acquireMic(): Promise<void>

Open the mic and hold it without publishing. Rejects if permission is denied or no device is available, leaving the room untouched. Requires ready.

publishHeldMic(): Promise<void>

Publish the held mic track. Requires ready.

releaseHeldMic(): Promise<void>

Drop a held, unpublished mic track. No-op when not connected.

setOutputBlocked(blocked: boolean): void

Mark the remote-audio output as blocked (browser refused autoplay) or unblocked. Called by <RealtimeAudio /> from its play/pause listeners; drives mediaState.output.

resumeAudioPlayback(): Promise<void>

Replay all remote audio from a user gesture (the <StartAudio /> affordance) to clear a browser autoplay block. No-op when no session is connected.

setListenMuted(muted: boolean): void

Mute/unmute the remote audio the listener hears (supervisor "speaker off"). No-op when not connected.

attachAudioElement(el: HTMLAudioElement | null): void

Hand the client a host-owned <audio> element for the remote bot audio; re-applied on reconnect. Pass null to detach. Idempotent.


Screen share and video

startScreenShare(): Promise<void>

Prompt for display capture, then publish the video track. Sets mediaState.screen through requesting → active. Throws on denial (screen_denied) or capture failure (screen_start_failed). Requires ready.

stopScreenShare(): Promise<void>

Unpublish the screen track and stop capture. Sets mediaState.screen to inactive. Idempotent.

isScreenSharing(): boolean

true when mediaState.screen.kind === 'active'.

getScreenShareState(): ScreenShareState

The current ScreenShareState discriminated union.

getVisionInputStatus(): { captured: boolean; message: string }

Snapshot whether fresh frames are flowing into the model's vision input, across every published video track (screen-share and camera). The returned shape mirrors the desktop get_current_screen tool output.

addVideoStream(stream: MediaStream, options?: VideoStreamOptions): Promise<VideoStreamHandle>

Publish an arbitrary MediaStream as a video track — webcam, canvas, or screen capture. Returns a handle for later removal. Requires ready; throws if the transport doesn't support video. See /multimodal/video.

removeVideoStream(streamId: VideoStreamHandle): Promise<void>

Unpublish a previously added video stream. Idempotent.


Monitoring

connectAsMonitor(opts): Promise<MonitorSession>

connectAsMonitor(opts: {
  livekitUrl: string;
  token: string;
  roomName: string;
  sessionId: string;
}): Promise<MonitorSession>

Join an in-progress session's room as a silent supervisor. Any prior connection on this client is torn down first. The returned MonitorSession accumulates the monitored session's transcript:

MemberDescription
sessionIdThe monitored session's id.
closedtrue once the monitoring connection ended or was replaced.
getTranscript(): RealtimeTranscriptItem[]Folded transcript bubbles; reference-stable between events.
subscribe(listener: () => void): UnsubscribeChange notifications, useSyncExternalStore-compatible.

Advanced

registerRpcMethod(name: string, handler: (payload: string) => Promise<string>): Unsubscribe

Register a transport-level RPC method the server invokes for client-tool execution. Forwarded to the active transport; throws RealtimeNotReadyError if no session is connected. Prefer declared client tools with handlers (/capabilities/tools) — this is the low-level escape hatch.


RealtimeAgent

The reusable persona. Build once via client.agent({...}) or client.catalogAgent(name, {...}), start any number of sessions from it, derive variants immutably. Validation (duplicate skill names, malformed hooks) throws when the agent is built, not at start().

AgentConfig

FieldTypeDescription
instructionsstringSystem instructions. Replaces the server's neutral default.
modelstringConcrete model within the provider named by modelOptions. Unknown values are rejected at session start.
modelOptionsModelOptionsProvider-scoped knobs, discriminated on provider ('gemini' | 'openai' | 'cosmo_voice_ultravox' | 'cosmo_voice_personaplex'). Gemini: temperature, maxOutputTokens, thinkingLevel; Ultravox: temperature, turnEndpointDelaySeconds. An illegal pairing is a type error.
voicestring | VoiceConfigHow the agent sounds: the provider voice id as a plain string, or { name?, speakingStyle? } when a "how to speak" instruction rides along.
toolsRealtimeTool[]Client-executed specs, server-tool opt-ins, and transfer-call tools. Unset → no tools. See /capabilities/tools.
interruptionSensitivityInterruptionSensitivityHow readily user audio barges in over the assistant.
greetingstringOpening line spoken as soon as the model session opens. A resumed session never re-greets.
audioAudioConfigThe audio pipeline: output (false runs text-only), noiseCancellation, and ambience ({ track?, gainDb? } — presence enables the bed).
skillsSkill[]Skill menu folded into instructions at start(); a load_skill client tool serves bodies on demand. See /capabilities/skills.
hooks(Hook | ServerHook)[]In-process client hooks (seam factories: sessionStart(fn), preToolUse(fn, {matcher}), …) plus declarative server hooks (SilenceTimeout). See /capabilities/hooks.

CatalogAgentOptions

Per-run ride-alongs for client.catalogAgent(name, {...}) — the stored config runs verbatim, so there are no persona fields here.

FieldTypeDescription
inputsRecord<string, string>Values for the agent's declared input fields ({{key}} placeholders).
toolsRealtimeTool[]Client tool declarations merged with the agent's attached tools.
voicestring | VoiceConfigPer-run voice override — the one cosmetic exception to "stored config runs verbatim". Pass { speakingStyle } to append delivery guidance after the stored persona.
hooksHook[]In-process client hooks only. Server hooks are stored config.

with(overrides: AgentConfig): RealtimeAgent

Derive an immutable variant: fields set here override this agent's; undefined keeps the current value. There is deliberately no way to unset a field back to the server default.

const base = client.agent({ instructions: 'You are a support agent.' });
const spanish = base.with({ voice: { speakingStyle: 'Respond in Spanish.' } });

start(opts?: SessionStartOptions): Promise<RealtimeSession>

Open one session from this persona. Resolves once the transport is connected (room joined); wait for ready before the first send. Rejects on handshake or transport failure — you never receive a session for a run that failed to start.

OptionTypeDescription
resumeSessionIdstringResume the named prior session — natively when the resumption handle is still warm, otherwise by seeding the new session with the prior transcript.
storeRecordingbooleanPersist recording artifacts server-side. Unset keeps the server default: the session records. See /production/recording-and-privacy.
publishMicrophonebooleanPublish the local mic track. Default true. Set false to join as a silent observer (e.g. an operator watching an outbound call). Client-side only.

RealtimeSession

One live run of an agent, returned by RealtimeAgent.start(). Wraps the same engine as RealtimeClient, so the browser surface (media state, React hooks) keeps working unchanged.

const session = await agent.start();
await session.waitUntilReady();
await session.sendText('Hi');
await session.end();

Properties

MemberTypeDescription
stateSessionLifecycleStateFormal lifecycle with typed end reason. Latches disconnected forever once this session ends, even though the client resets to idle.
sessionIdstring | nullServer-minted session id.

Methods

MethodDescription
end(): Promise<void>Graceful end — wire end frame; stream finishes with reason client ended. Idempotent.
close(): Promise<void>Abrupt local teardown, no wire end frame; reason client closed. Idempotent.
waitUntilReady(): Promise<void>Same as client.waitUntilReady().
getSnapshot(): RealtimeSnapshotSame as client.getSnapshot().
sendText(content, options?)Same as the client method.
sendImage(args)Same as the client method.
ping(): Promise<void>Same as client.sendPing().
activityEnd(): Promise<void>Signal end-of-turn for manual-VAD turn-taking.
dial(phoneNumber): Promise<DialResult>Same as the client method (no callerNumber override on the session surface).
setMuted(muted): Promise<void>Same as client.setMicMuted()setMuted is the cross-SDK session name.
startScreenShare() / stopScreenShare() / getScreenShareState()Same as the client methods.
addVideoStream(stream, options?) / removeVideoStream(streamId)Same as the client methods.
registerRpcMethod(name, handler)Same as the client method.
attachAudioElement(el) / resumeAudioPlayback()Same as the client methods.

Events — callbacks

session.on(event, handler) subscribes to the same typed event map as client.on (UI-normalized payloads).

Events — async iteration

for await (const event of session) yields the wire-level event stream:

for await (const event of session) {
  if (event.type === 'transcript') console.log(event.role, event.text);
  if (event.type === 'session-ended') console.log('ended:', event.reason);
}
  • Items are external wire frames verbatim (ready, transcript, tool-call, …), { type: 'unknown', rawType, payload } for unrecognized frame types (never terminal), and the SDK-local terminal { type: 'session-ended', reason }.
  • A session-ended item is always the final one, even when the server's notice races later frames.
  • Single consumer: a second concurrent next() rejects.
  • The queue is bounded at 1024 events; a consumer that stops pulling drops overflow events (logged). Terminal items evict a buffered event rather than being lost.

Handshake failures throw from agent.start() — the stream a caller never received ends empty. Wrap start() in try/catch rather than watching the stream for startup errors.

On this page

RealtimeClientConstructorRealtimeClientOptionsAuthmintToken(externalUserId: string): Promise<MintedToken>verify(): Promise<CredentialInfo>Building agentsagent(config?: AgentConfig): RealtimeAgentcatalogAgent(name: string, opts?: CatalogAgentOptions): RealtimeAgentLifecycleisActive(): booleangetSessionId(): string | nullgetLifecycleState(): SessionLifecycleStategetSnapshot(): RealtimeSnapshotwaitUntilReady(): Promise<void>disconnect(): Promise<void>close(): Promise<void>on<E>(event: E, handler: (payload: RealtimeEventMap[E]) => void): UnsubscribeEvent mapMessagingsendText(content: string, options?: { transcript?: boolean }): Promise<void>sendContext(content: string): Promise<void>sendImage(args: { data: string; mimeType?: string; streamId?: string }): Promise<void>sendPing(): Promise<void>activityEnd(): Promise<void>Telephonydial(phoneNumber: string, callerNumber?: string): Promise<DialResult>Microphone and audio outputsetMicMuted(muted: boolean): Promise<void>acquireMic(): Promise<void>publishHeldMic(): Promise<void>releaseHeldMic(): Promise<void>setOutputBlocked(blocked: boolean): voidresumeAudioPlayback(): Promise<void>setListenMuted(muted: boolean): voidattachAudioElement(el: HTMLAudioElement | null): voidScreen share and videostartScreenShare(): Promise<void>stopScreenShare(): Promise<void>isScreenSharing(): booleangetScreenShareState(): ScreenShareStategetVisionInputStatus(): { captured: boolean; message: string }addVideoStream(stream: MediaStream, options?: VideoStreamOptions): Promise<VideoStreamHandle>removeVideoStream(streamId: VideoStreamHandle): Promise<void>MonitoringconnectAsMonitor(opts): Promise<MonitorSession>AdvancedregisterRpcMethod(name: string, handler: (payload: string) => Promise<string>): UnsubscribeRealtimeAgentAgentConfigCatalogAgentOptionswith(overrides: AgentConfig): RealtimeAgentstart(opts?: SessionStartOptions): Promise<RealtimeSession>RealtimeSessionPropertiesMethodsEvents — callbacksEvents — async iteration