- A voice agent that gets its replies from Dialog-RSN-1 as text and speaks them through ElevenLabs.
- Text streamed into ElevenLabs as it arrives, so the first words play before the reply is finished.
- Interruptions that stop ElevenLabs and drop the audio still queued to play.
- Playback reports, so after an interruption the model remembers only what you heard.
- Your coding agent, set up as in the quickstart’s Set up your agent: skill installed,
POLY_API_KEYset, connection check passing. - The Python
voice_agent.pyfrom the quickstart for coding agents: mic in, server voice out, interruptions, and the spacheck_availabilitytool. - An ElevenLabs API key. In ElevenLabs, open Developers, then API Keys, and create one with access to text to speech. The free tier works.
- An ElevenLabs voice id. Open Voices, pick a voice, and copy its id. On the free tier the API can use the default voices, such as Sarah (
EXAVITQu4vr4xnSDxMaL), but not voices from the Voice Library.
.env file in your folder, next to the code, as ELEVENLABS_API_KEY=... and ELEVENLABS_VOICE_ID=.... As with your PolyAI key, don’t paste them into the chat.
If you’d rather write the code yourself, follow the Streaming to your TTS guide instead.
Step 1: speak with your ElevenLabs voice
Prompt
session.update sets output_modalities: ["text"], so the server stops speaking and the reply arrives as response.output_text.delta. Each delta goes to ElevenLabs the moment it arrives. The code sends the new piece, not the reply so far, and tells ElevenLabs there’s no more text on response.output_text.done. Waiting for .done before sending anything would add the whole generation to the silence before the first word.
ElevenLabs runs over its own WebSocket. The voice id goes in the URL path and the key in the xi-api-key header. Ask for pcm_24000 output, so the audio plays on the same 24 kHz speaker as before. Each reply gets its own ElevenLabs context, keyed on the response_id.
An interruption is two separate steps. When input_audio_buffer.speech_started arrives while a reply is playing, the code closes that reply’s ElevenLabs context, so it stops synthesizing, and then empties the speaker’s queue, so the audio ElevenLabs already sent doesn’t play. A response.done with status cancelled for the same reply is the same signal, not a second one.
A tool call’s response carries no text, so nothing reaches ElevenLabs until the model answers with the result. Empty or punctuation-only text never reaches ElevenLabs.
For the test without speakers, the agent swaps the speaker for one that plays to a WAV file at real speed. Time to first audio counts from the end of your turn to the first sample played, not to the first text delta.
Check it. Run it with a typed question, such as --say "Is the spa free on Friday?", and play the file it saved. You should hear your ElevenLabs voice read the Friday slots. Then talk to it with headphones on and interrupt it mid-sentence: it should stop at once and answer what you said.
If it goes wrong:
Step 2: tell it what you heard
Prompt
response_id of the reply being played:
poly.output_audio.startedwhen the first sample of the reply plays.poly.output_audio.deltaabout every 250 ms, withaudio_ms(the milliseconds played so far) anddelta(the words spoken since the last report).poly.output_audio.stoppedwhen the reply finishes or you interrupt it, withaudio_ms,dropped_ms(the queued audio that never played) andspoken_text(the words you heard).
sync_alignment=true on the WebSocket URL. They count from the start of the context, not from each audio chunk. The code counts what the speaker played, not what ElevenLabs sent: a reply can be synthesized seconds ahead of playback.
Send reports for every reply. Once a session sends one, the server stops estimating for it.
Check it. Run the interruption test. The stopped report should show dropped_ms above zero and a spoken_text that ends where you cut in. The next reply should pick up from there, not from the end of the reply you didn’t hear.
If it goes wrong:
Troubleshooting
For key, connection and session limit errors from Dialog-RSN-1, see the quickstart’s Troubleshooting.

