@langwatch/scenario
    Preparing search index...

    Class ElevenLabsAgentAdapter

    Hosted ElevenLabs Conversational AI adapter (official-SDK transport).

    Connect (build an ElevenLabsClient + a Conversation over our AudioInterface and start the session), stream PCM16 audio chunks at real-mic cadence, and drain agent audio the SDK pushes via output().

    Hierarchy (View Summary)

    Index
    _voiceTurnContext?: Context

    Live OTel context of the CURRENT voice.turn, published by defaultVoiceCall for background-receive-loop adapters (Pipecat/Twilio) to parent their detached-callback recv spans under the turn (#774 — the reusable pattern Twilio PR5 inherits). undefined between turns, so a callback firing outside a turn skips its span rather than parenting under a closed turn. Internal (underscore) — not a public API.

    agentHungUp: boolean = false

    SET when the AGENT deliberately ended the call (e.g. an ElevenLabs hosted agent invoking the end_call system tool), as opposed to the transport dropping. A scripted turn arriving after this concludes the conversation instead of failing the run — the agent behaved as designed. Assertions and judges can read it to reason about WHO ended the call.

    agentId: string
    agentSpeakingEvent?: AgentSpeakingEvent

    Set when the adapter has emitted its first agent audio chunk for the current turn — gates timing-based barge-in. Concrete adapters expose this so scenario.interrupt can wait for real speech before firing the interruption. Optional: adapters without server-VAD-style interrupt sequencing can leave it undefined.

    audioCommitCount: number = 0

    How many user turns this adapter committed by streaming real PCM. The voice-specific assertion keys on this together with lastUserTranscript: a non-empty user_transcript after audioCommitCount turns proves EL's STT actually ran on the audio we sent (audio reached the agent) — strictly stronger than the older >=N segments check, which passed even on the old text-commit path where no PCM ever reached EL.

    capabilities: AdapterCapabilities = ...

    Declaration of what this adapter can and cannot do. Concrete subclasses MUST publish a non-default value; the base instance defaults to "nothing supported" so capability-gated steps fail safely when an adapter forgets to declare.

    lastAgentTranscript: string | null = null
    lastUserTranscript: string | null = null
    name?: string
    responseMaxDuration: number = 30.0

    Hard cap on a single agent turn's audio. Prevents runaway loops if a transport never signals end-of-stream. 30s = a long sentence.

    responseTailSilence: number = 0.6

    Tail silence: once the first agent chunk arrives, keep draining receiveAudio until no chunk shows up within this many seconds — that's how we detect the agent finished talking.

    responseTimeout: number = 60.0

    Seconds to wait for agent audio after sending user audio: the STT + LLM + TTS budget for one agent turn. Kept identical to Python's VoiceAgentAdapter.response_timeout so the same scenario passes or fails the same way in both SDKs.

    Raise it for an agent that runs a tool call or a retrieval step before it speaks:

    const agent = elevenLabsAgent({ agentId, apiKey });
    agent.responseTimeout = 180; // wait up to 3 minutes
    role: AgentRole = AgentRole.AGENT
    streamingTranscript?: string

    Incremental transcript text emitted while the agent speaks. Populated by adapters that advertise capabilities.streamingTranscripts. Read by scenario.interrupt when afterWords: N is set.

    transcriptGraceWait: number = 2.0

    Bounded grace-wait (seconds) for the agent turn's transcript AFTER audio drains (#734). Audio silence closes the turn (responseTailSilence), but a live voice agent (hosted ElevenLabs) delivers the turn's text on a SEPARATE socket event (agent_responselastAgentTranscript). When that event lands after the audio-silence boundary, snapshotting lastAgentTranscript at drain-close reads null and the turn reaches the text-only simulator as a bare [audio message] — the simulator then fabricates.

    The default call() flow (defaultVoiceCall) polls this field up to this ceiling for a pending transcript before reading it. It short-circuits the INSTANT lastAgentTranscript is already set (zero added latency on the happy path — the common case where the transcript won the race) and only elapses when the transcript genuinely never arrives, so a real ElevenLabs drop still terminates the turn. Set to 0 to disable the wait.

    • Send a first-class interrupt signal to the agent under test.

      Adapters that advertise capabilities.interruption === true override this to send the transport-native interrupt (e.g. Twilio clear, OpenAI Realtime response.cancel). The default raises UnsupportedCapabilityError; callers (scenario.interrupt()) check capabilities.interruption and fall back to timing-based barge-in when this returns false.

      Returns Promise<void>

    • Universal inbound-message hook — wired to the SDK's callbackMessageReceived, which fires for EVERY message (ping, audio, transcript, …) AFTER the SDK has routed it. Two jobs, both ours rather than the SDK's:

      • LIVENESS: any inbound frame re-arms all active receiveAudio idle deadlines so a slow-but-pinging server does not spuriously time out (the sliding idle-deadline). The SDK already auto-pongs pings; we only need the reset.
      • TERMINAL TURN: a client_tool_call is a tool-only / non-audio terminal — this adapter ships no client_tool_result path, so EL produces no spoken audio for it. Resolve the parked receiver with an empty chunk so the drain exits cleanly instead of hanging to the timeout.

      Exposed via the class (not a closure) so unit tests can drive it directly.

      Parameters

      • message: unknown

      Returns void

    • Synchronously drain every chunk currently buffered on audioQueue and return them merged (null when the queue is empty). The turn-boundary reconcile (#747) calls this at the moment a NEW user turn commits: at that instant the queue can hold ONLY leftover from the PRIOR agent turn — the user just spoke and the hosted agent has not begun its next reply, so any audio still queued is stale-by-position. Returning it here lets the runtime attribute it to the utterance that produced it instead of the next receiveAudio shifting it out as the fake first audio of the next turn (the split-utterance bleed). Called ONLY at the cursor-safe pre-user-sendAudio hook and never while a drain is in flight, so it does not race receiveAudio on the shared queue.

      Duck-typed convention (symmetric with lastAgentTranscript): the shared runtime feature-detects this method, so adapters without a buffered queue are untouched.

      Returns AudioChunk | null