Member Junction
    Preparing search index...

    Real-time, full-duplex driver for the ElevenLabs Agents Platform, implementing the Core BaseRealtimeModel primitive. Registers as ElevenLabsRealtime and is resolved for MJ: AI Models typed Realtime (API-key env alias: AI_VENDOR_API_KEY__ElevenLabsRealtime).

    Why an "agent", not a model: ElevenLabs realtime is an orchestrated STT→LLM→TTS stack exposed only through pre-configured server-side agents — there is no bare-model realtime socket. The driver hides that behind the standard realtime contract with a managed-agent strategy (ensureAgent):

    • params.Model starting with agent_ → used VERBATIM as a deployment-managed agent id.
    • any other value → the NAME of the driver-managed agent: find-by-name; create-if-missing (with the session's client-tool set and the override enablement that lets each session supply its own system prompt, voice and opening utterance); PATCH when the order-insensitive tool fingerprint differs or any required override is not enabled. Results are instance-cached per name+tools.

    Per-session prompt, voice AND opening-utterance authority stays with MJ: the managed agent stores only a placeholder prompt and explicitly enables the overrides in buildRequiredOverrideEnablement (agent.prompt.prompt + agent.first_message + tts.voice_id); every session (server-bridged or client-direct) sends the real system prompt — plus its voice and first message, when configured — in its conversation_initiation_client_data frame. All three are per-session overrides, so sessions differing in any of them still share ONE managed agent.

    The agent can speak first only because agent.first_message is among those overrides: with none set, ElevenLabs waits for user audio before producing any, and no wording of the persona prompt changes that (issue #3557).

    Topologies:

    No usage events: the Agents websocket reports no token usage; sessions never emit IRealtimeSession.OnUsage. Usage accounting for ElevenLabs realtime lives in the platform's own conversation dashboard / billing exports.

    Hierarchy (View Summary)

    Index

    Constructors

    Properties

    SupportsDynamicToolSet?: boolean

    Whether this driver subclass supports dynamic, multi-tool sets projected into the realtime session (e.g. direct action invocation on realtime co-agents).

    Static capability descriptor read before a session instance exists. Default: absent / false. Subclasses that support dynamic toolsets declare public static readonly SupportsDynamicToolSet = true;.

    Accessors

    • get apiKey(): string

      Only sub-classes can access the API key

      Returns string

    • get SupportsClientDirect(): boolean

      ElevenLabs supports the client-direct topology natively: the signed websocket URL is a short-lived, agent-scoped credential the browser can open directly.

      Returns boolean

    • get SupportsVideo(): boolean

      Whether this driver's sessions carry a video track in addition to audio — i.e. the model accepts video input (it can "see" the user's camera) and/or emits video output (a talking-head avatar / generated video), in sync with audio.

      Defaults to false (audio-only — today's realtime models). Video-capable drivers (a native multimodal realtime model, or an avatar provider) override this to true. The session's media plane is media-tagged (IRealtimeSession.SendInput takes a RealtimeMediaKind; IRealtimeSession.OnVideoOutput delivers video-out), so a video session reuses the entire realtime contract — only the media frames gain a video kind. Resolution prefers a video-capable model when an agent requests video, and degrades to audio-only otherwise.

      Returns boolean

      true if sessions can carry video; false (audio-only) otherwise.

    Methods

    • Builds the create/update body for the managed agent: the placeholder base prompt, the session tool set mapped to inline CLIENT tools (expects_response: true so the agent blocks on — and then speaks — each result; max response timeout because MJ client tools delegate to long-running agents), and the platform-settings enablement of every per-session override the driver sends (buildRequiredOverrideEnablement).

      params.Config.llm (when a string) selects the agent's underlying LLM; all other provider-level agent settings are deployment concerns (use a verbatim agent id for full control).

      Parameters

      Returns BodyCreateAgentV1ConvaiAgentsCreatePost

    • REST seam: creates an agent, returning its id.

      Parameters

      • body: BodyCreateAgentV1ConvaiAgentsCreatePost

      Returns Promise<string>

    • Resolves params.Model to a concrete ElevenLabs agent id.

      • A verbatim agent id (agent_…) is returned as-is — the deployment owns that agent's configuration (including its tool set and override enablement).
      • Anything else is the MANAGED agent name: find-by-name → create-if-missing → PATCH when the order-insensitive client-tool fingerprint differs or any required per-session override is not enabled. The resolution is instance-cached per name + fingerprint — deliberately NOT per voice, which is a per-session override rather than agent state.

      Parameters

      Returns Promise<string>

    • Optional WebRTC SDP exchange seam for drivers that support server-brokered WebRTC topologies.

      Parameters

      • _offerSdp: string

        The local SDP offer generated by the browser.

      • Optional_sessionConfig: Record<string, unknown>

        The session configuration to initialize the session with.

      Returns Promise<{ answerSdp: string; prebillSeconds: number; sessionId: string }>

      The answer SDP, provider session ID, and any prebill accounting.

    • Resolves the managed agent by NAME, retrying a miss up to MAX_AGENT_LOOKUP_ATTEMPTS times.

      ElevenLabs' agent search is EVENTUALLY CONSISTENT: an agent created moments ago — by us or by a concurrent process — is briefly invisible to find-by-name. Treating one miss as "does not exist" makes the ensure flow CREATE, forking a duplicate managed agent that then competes for the same name forever.

      Cost falls entirely on the FIRST-EVER provision of a name. A name that resolves pays nothing (it hits on attempt 1); a name that genuinely does not exist yet exhausts every attempt before falling through to create, so it pays the whole backoff ladder — 500ms + 1000ms ≈ 1.5s at the current constants, once, before the create call. Every later session for that name is served from agentCache without any of this.

      Parameters

      • name: string

        The managed agent name to resolve.

      Returns Promise<AgentSummaryResponseModel>

      The adopted agent summary, or undefined once the attempts are exhausted.

    • REST seam: mints the signed websocket URL for an agent (GET /v1/convai/conversation/get-signed-url?agent_id=… with the xi-api-key header — the SDK call below is that exact request).

      Parameters

      • agentId: string

      Returns Promise<string>

    • Builds the RAW-WIRE conversation_config_override for one session: the server-authored system prompt, plus the per-session TTS voice and opening utterance when the config bag carries them.

      Mind the casing split. This object is forwarded VERBATIM onto the conversation websocket (by this driver server-bridged, by the 'elevenlabs' client driver client-direct), so it is snake_case — tts.voice_id, agent.first_message. The matching ENABLEMENT in buildRequiredOverrideEnablement goes out through the SDK's serializer instead and is therefore camelCase — tts.voiceId, agent.firstMessage. Only agent.prompt.prompt reads the same in both, so every override added after it has to be spelled twice, differently.

      Each optional key is omitted ENTIRELY when unconfigured, so a session that configures neither is byte-for-byte the frame it was before either existed. That is what preserves the platform's wait-for-the-user default: an ENABLED-but-empty first_message means exactly "no opening utterance", so omitting the key and sending it blank are the same behavior — and omitting keeps the frame honest.

      Parameters

      • systemPrompt: string

        The per-session system prompt (the standing prompt override).

      • Optionalconfig: JSONObject

        The session's open config bag (realtime.voice.providers.elevenlabs merged in).

      Returns JSONObject

      The wire-shaped override object.

    • The desired-model-settings half of the ensure cache key (#3859): the config bag's llm and temperature in one stable string. Unconfigured values serialize as absent, so a bag that says nothing produces the same fingerprint forever.

      Parameters

      Returns string

    • Whether the agent's OWN configuration matches the model settings the config bag asks for — the drift the ensure flow repairs for settings that are AGENT STATE rather than per-session overrides (#3859).

      temperature lives here because the platform offers no per-session override flag for it (PromptAgentApiModelOverrideConfig has prompt/llm/nativeMcpServerIds only), so the only honest delivery is: set it on the managed agent's body and re-PATCH when it changes. llm is checked here TOO, even though it is also per-session overridable, so an agent whose configured default model changed is repaired even for callers that send no override.

      An UNCONFIGURED desire matches anything: half the point of the managed agent is that the deployment may tune it, and a config bag that says nothing must not stampede a PATCH war against a hand-tuned value.

      Parameters

      • agent: GetAgentResponseModel
      • Optionalconfig: JSONObject

      Returns boolean

    • Whether the agent's platform settings enable EVERY per-session override this driver sends — the drift condition the ensure flow repairs. Checking the whole required set (rather than the prompt override alone) is what lets an agent provisioned by an older MJ version pick up a newly-added override: it still matches on tools, so the enablement gap is the only signal that it needs a PATCH.

      This is the READ half of the contract buildRequiredOverrideEnablement WRITES. The two are deliberately spelled out separately rather than derived from one another: the write side is the SDK's …Input shape and this reads the …Output shape, so a generic walker over either would have to erase both types and lose exactly the compile-time checking that catches a mistake here. They are kept honest instead by the round-trip test "considers an agent this driver just provisioned already satisfied" — add an override to one half only and that test fails (as a permanent re-PATCH loop).

      Parameters

      • agent: GetAgentResponseModel

        The agent configuration as fetched from the REST API.

      Returns boolean

      true when every required override is enabled.

    • Chooses ONE agent when the search returns several with the requested name — which happens whenever a duplicate was forked before this guard existed. The OLDEST wins (ties broken by agent id), so every process and every session converges on the SAME agent instead of picking whichever the API happened to list first and PATCHing them alternately.

      Parameters

      • summaries: AgentSummaryResponseModel[]

        The search results.

      • name: string

        The exact name being resolved.

      Returns AgentSummaryResponseModel

      The canonical match, or undefined when none has that exact name.

    • Whether the agent's platform settings enable the per-session system-prompt override.

      Parameters

      • agent: GetAgentResponseModel

      Returns boolean

      This is only ONE of the overrides the driver requires, so a true here does NOT mean the agent is up to date — an agent predating per-session voice passes this while still dropping the voice. Retained for API compatibility only; the ensure flow uses ElevenLabsRealtime.OverridesSatisfied, and so should any caller.

    • Reads the session's opening utterance out of the config bag — the firstMessage key, which maps to ElevenLabs' agent.first_message override.

      This is spoken VERBATIM, not interpreted. It is the literal first thing the agent says, not an instruction to the model about how to open — which is also why it is immune to the ordering of ElevenLabsRealtimeSession.SendInitiation's deferred initial-context injection: there is no generation to race.

      A missing, blank, or non-string value yields undefined so the override is omitted rather than sent empty. An empty first_message IS the platform's wait-for-the-user default, so this is a no-op either way — omitting simply keeps the frame free of meaningless keys.

      Parameters

      • Optionalconfig: JSONObject

        The session's open config bag.

      Returns string

      The trimmed opening utterance, or undefined when none is configured.

    • Reads the DRIVER-NEUTRAL voice key out of the session's config bag — the same key the AssemblyAI and Inworld realtime drivers read, so a persona's configured voice is authored identically whichever provider ends up speaking it. Reached in practice via the effective config's per-provider bag, realtime.voice.providers.elevenlabs.voice.

      A missing, blank, or non-string value yields undefined so the override is omitted rather than sent empty (ElevenLabs would reject an empty voice_id, killing the whole session over a misconfigured persona).

      Parameters

      • Optionalconfig: JSONObject

        The session's open config bag.

      Returns string

      The trimmed ElevenLabs voice id, or undefined when none is configured.