Technical docs

Realtime Voice

Overview

Support reference for the current realtime voice server and client flows.

Runtime entrypoints

  • HTTP/WebSocket runtime:
  • `backend/src/realtimeServer.ts`
  • WebSocket orchestration:
  • `backend/src/components/realtime/RealtimeProxy.ts`
  • Provider/runtime implementations:
  • `backend/src/components/realtime/agents/realtime/OpenAIRealtimeAgent.ts`
  • `backend/src/components/realtime/agents/realtime/GoogleAIRealtimeAgent.ts`
  • `backend/src/components/realtime/agents/tts/TtsAgent.ts`
  • WebSocket path:
  • `/chat/realtime?session={sessionId}`
  • Health endpoint:
  • `GET /chat/realtime/health`

Session creation endpoints

  • Public webchat voice:
  • `POST /chat/realtime/create-session/{employeeId}`
  • The widget currently passes its configured `eventId` in that path slot.
  • Internal workflow voice:
  • `POST /chat/realtime/workflow/create-session/{organization_id}/{workflow_id}`
  • Internal AI teammate and Ally voice:
  • `POST /chat/realtime/employee/create-session/{organization_id}/{employee_id}`
  • The frontend first resolves the current user's personal Ally employee, then passes that employee ID to this shared endpoint.

Request rules and responses

  • Internal employee session creation requires exactly one of:
  • `conversation_id`
  • `create_new_conversation`
  • Public webchat session creation can include:
  • `contact`
  • `multi_conversations`
  • Successful create-session responses include:
  • `sessionId`
  • `isAudioAutoTurnOn`
  • Internal employee responses also include `conversation_id`.
  • Session IDs are one-time use and currently match the created workflow run ID.
  • Pending session configs are stored in Redis with a 20-minute TTL and deleted on the first websocket connect.
  • All realtime session routes reject `structured` workflows. The published starting step must resolve to an `agent` step with a configured TTS-capable employee.
  • Supported `locale` values are currently:
  • `en-US`
  • `ru-RU`
  • `es-ES`
  • default fallback is `en-US`

Provider routing

  • If the selected TTS model is realtime and the provider is Google:
  • `GoogleAIRealtimeAgent`
  • `GeminiLiveVoice`
  • If the selected TTS model is realtime and not Google:
  • `OpenAIRealtimeAgent`
  • If the TTS model is non-realtime but still TTS-capable:
  • `TtsAgent`
  • OpenAI realtime STT is used when an STT model is configured
  • The websocket runtime rebuilds a `RequestContext` from `workflowRun.state.runtimeContext` and adds `organizationId`, `employeeId`, and `runId` before the session starts.

Realtime tool contracts

  • Gemini Live sends each registered tool through `parametersJsonSchema`; OpenAI Realtime sends it through `parameters`.
  • Existing JSON Schema inputs pass through unchanged. Zod inputs are converted to JSON Schema without the top-level `$schema` marker.
  • Provider declarations preserve nested object fields, array item schemas, required fields, optional fields, enums, nullable values, descriptions, and numeric constraints used by Alloy tools.
  • OpenAI Realtime adapts the voice package execution envelope before invoking a current Mastra tool: the tool receives flat validated input, while runtime metadata including `RequestContext` is supplied separately.
  • Gemini Live also invokes registered tools with the decoded argument object and the session request context.

Audio and message flow

  • Clients send binary microphone audio over the websocket.
  • The websocket wire format expects mono signed `16-bit PCM` audio at `24kHz`.
  • The internal frontend explicitly resamples to `24kHz` before sending.
  • Clients can also send JSON:
  • `type: "user_message"`
  • `data.text`
  • optional `data.attachments`
  • The server validates attachments for websocket `user_message` payloads before processing them.
  • Attachment guardrails on websocket messages:
  • maximum 5 files
  • maximum 10 MB each
  • allowed MIME families: images, PDF, plain text, XML, and SQLite
  • For internal voice conversations, the client requests `session.capabilities` with `session.capabilities.request`; attachment sends require `data.attachments` and a `request_id` on `user_message`. The server advertises `data.attachment_messages` only when that session supports this path. The client does not submit attachments unless support is confirmed.
  • The internal attachment path validates and saves the message before replying with `user_message.accepted` and the matching `request_id` (and a `message_id` when available). Before-save failures return `user_message.rejected`; if delivery to the AI fails after the save, the server sends `user_message.delivery_failed` instead. Repeating an accepted request ID returns the prior acknowledgement; simultaneous attachment requests are rejected. Text-only voice messages retain their existing path.
  • In OpenAI realtime sessions, saved attachments are added to the provider conversation and a reply is requested when appropriate. Image content is limited to PNG, JPEG, WebP, and GIF in that path. The TTS path also saves attachments before invoking its text/voice response. If automation is off, internal voice attachment sends are unavailable. The ordinary webchat voice attachment path is separate.

Server -> client events

  • The realtime websocket currently sends JSON events such as:
  • `session.created`
  • `speech_started`
  • `speech_stopped`
  • `response.cancelled`
  • `error`
  • Audio responses are streamed back as binary websocket frames.
  • `TtsAgent` additionally sends text-sidecar events such as:
  • `text` with `role: user|assistant`
  • `speaking.done`
  • Internal chat also listens to org websocket events from the TTS path:
  • `voice_thinking_started`
  • `voice_thinking_ended`

Automation and takeover behavior

  • Realtime agents refresh automation state from the conversation while the session is running.
  • If automation is disabled on the conversation, the voice session stays connected and still saves user messages, but skips AI response generation.
  • If the organization is out of tokens or the resolved AI teammate has already reached its daily spending limit when voice starts, the server sets an eligible webchat conversation to `waiting_for_operator` and rejects the session.
  • Native realtime and TTS paths record attributable usage and re-check the shared AI execution gate after each completed response.
  • If a completed response crosses the organization token balance or employee daily spending limit, the response remains recorded, an eligible webchat conversation moves to `waiting_for_operator`, and the websocket closes with code `4008` and the canonical resource-limit message.
  • A realtime close code at or above `4000` is mapped to a failed workflow run.

Start building your AI team