Callbacks & events
Every hook VoiceConversation exposes, and when they fire.
All callbacks are optional. Pass them inside the ConversationOptions object. They're invoked synchronously on the media transport event loop — keep them fast or defer work with queueMicrotask.
ConversationStatus
type ConversationStatus = 'connecting' | 'connected' | 'disconnecting' | 'disconnected';Transitions:
connecting— the initial state, set the moment theWebRTCConnectionis constructed.connected— afterroom.connect(),createLocalAudioTrack(), andpublishTrack()all succeed.disconnecting—endSession()has been called but the room hasn't acknowledged yet.disconnected— the transport has firedDisconnected, OR an error duringconnect()(connection, mic) short-circuited to this state.
onStatusChange fires only on actual transitions; duplicate transitions are deduped.
ConversationMode
type ConversationMode = 'listening' | 'speaking';Mirrors transport active-speaker events: speaking when any remote participant is in the active-speakers set, listening otherwise. Useful for UI states like "agent talking now — show the voice animation".
Deduped on transition — onModeChange won't fire twice for the same mode.
ConversationMessage
interface ConversationMessage {
source: 'agent' | 'user';
text: string;
isFinal: boolean;
segmentId?: string;
}onMessage fires from two sources — live transcriptions (the common case when talking to a Speko agent) and custom data-channel packets:
| Inbound event | Becomes |
|---|---|
| Transcription segment | { source, text, isFinal, segmentId } — source is user for the local participant, agent otherwise |
transcript packet | { source: packet.source, text, isFinal: packet.isFinal ?? true } |
agent_message packet | { source: 'agent', text, isFinal: packet.isFinal ?? true } |
user_message_echo packet | { source: 'user', text, isFinal: true } |
Transcription updates are cumulative per segment: the same segmentId is re-delivered with growing text (the agent's transcript streams word-by-word; the user's utterance is re-published in full on every recognizer update, and the final text can arrive more than once). Render by upserting on (source, segmentId) — replace that message's text in place, and only append when you see a new segmentId. Appending every message duplicates text, and keying only by source corrupts the transcript whenever user and agent updates interleave (which is normal). Messages from custom data packets carry no segmentId; append those.
See Data channel protocol for the raw wire format.
DisconnectionDetails
interface DisconnectionDetails {
reason: DisconnectionReason;
message?: string;
}
type DisconnectionReason = 'user' | 'agent' | 'error' | 'timeout' | 'unknown';The SDK maps transport disconnect reasons into a smaller, intent-oriented set:
| Transport disconnect reason | Mapped reason |
|---|---|
| Client initiated | user |
| Participant removed / room deleted / room closed | agent |
| Join failure | error |
everything else (including undefined) | unknown |
message is the raw transport enum name when available (useful for debugging / logging).
onConnect
onConnect?: (details: { conversationId: string }) => void;Fires exactly once, after the mic is publishing and status is connected. conversationId is the transport conversation id (same value as conversation.getId()).
onError
onError?: (error: Error) => void;Non-fatal errors:
- Media device errors from the transport.
- Output device selection failures (
setSinkIdrejections).
Malformed or unrecognised inbound data packets are silently ignored — rooms carry data from other publishers (server control topics, future participants), so a packet that isn't part of the SDK protocol is not an error.
Fatal errors during create() are thrown, not routed to onError. See Errors.