Skip to main content
A voice agent has to sound like a conversation and finish the caller’s task. Either can fail while the other looks fine: a reply that reads perfectly can confirm a booking that was never written, and a correct booking can arrive three seconds late to a caller who has already hung up. So follow one request the whole way through: the audio you send, the turn boundary the server confirmed, the text Dialog-RSN-1 returned, the tools your client ran, the state your application ended up in, and the words your text-to-speech said out loud. Grade the final transcript alone and you miss most of the ways that chain breaks.

Where the system splits

Dialog-RSN-1 writes each reply before it’s spoken, and an evaluation harness asks for it as text with output_modalities: ["text"]. That puts the seam in an unusual place: The middle two layers are text and JSON. You can assert on a reply’s exact characters before a caller hears it, with no transcription step in the way and no audio quality to confound the result. Checking whether a bulleted list snuck into something meant to be spoken is a regex here, not a judge. The cost is that no metric on this API describes what a caller heard. Latency measured to the first response.output_text.delta isn’t the caller’s experience, because your text-to-speech hasn’t started yet. Add your own time-to-first-audio before comparing that number against anything.
Hold your text-to-speech fixed. It sits in the middle of both the latency your caller feels and the turn boundaries the server sees. Changing voice, provider, or buffering between runs moves numbers that have nothing to do with the model.

What the wire gives you

Most metrics come off the event stream. The events below are the ones a text session sends, so set output_modalities: ["text"] in your session.update rather than relying on the default. Record every frame you send with a monotonic timestamp and every event you receive with another, and you have the evidence for all of the below. Two properties of that stream shape the collector. The end of a turn is a judgment, not a threshold. speech_stopped is sent only once Dialog-RSN-1 has confirmed the boundary voice activity detection proposed, so a caller pausing mid-sentence produces no stop at all. speech_started is confirmed the same way only while a reply is playing. With nothing playing, it goes out on the detector’s onset, and a sound the model then judges to be noise is dropped with no speech_stopped after it. That makes turn segmentation directly checkable: count speech_stopped and committed pairs, compare against the turns your fixture contains, and treat a lone speech_started as a sound the model ignored. The reference has the full sequence. The transcript is a by-product of the reply, not a stage before it. It’s the last thing the generation writes, so it arrives after the reply text, and conversation.item.input_audio_transcription.completed comes only with a completed reply. A reply cancelled by a barge-in, or one that fails, usually leaves its turn with no transcript at all. A collector that waits for the transcript before recording the reply will hang on exactly those turns. Record both, join them by item id afterwards.

Telling the server what the caller heard

Dialog-RSN-1 owns hearing and you own speaking, so nothing in the standard protocol tells the server whether a reply reached the caller or how much of it they got through. Three client events carry that back:
you send: playback reports
They change behavior. Barge-in confirmation runs for as long as a reply is audible. With no reports, Dialog-RSN-1 guesses how long that is from a typical speaking rate of 15 characters a second, counted from the reply’s first words. Your text-to-speech rarely runs at exactly that rate, so the guess ends early on a slow voice, and a sound after that point is forwarded as an unconfirmed interruption. Reporting playback makes the window match the real speaker, so a “mm-hmm” two seconds into a slow reply gets checked rather than acted on. They are evidence. spoken_text on poly.output_audio.stopped is the client’s own account of what the caller actually heard, and dropped_ms is what your synthesizer threw away on a barge-in. Dialog-RSN-1 cuts the reply in history back to that text, so the model knows what the caller missed. Without reports, history holds the speaking-rate estimate instead, which is why your record of what was played is the ground truth to grade against. Three details to get right:
  • audio_ms is cumulative, not incremental, so a dropped report leaves a gap rather than desyncing the count.
  • A report naming a response that is no longer the one playing is ignored rather than applied to whatever is playing now. Reports and responses race, so carry the response_id from response.created through every report about it.
  • Always send stopped. A generation clears the open report, so a started with no stopped won’t wedge the session, but it does leave a window where the server believes a finished reply is still playing.
Report playback in your harness exactly as your production client does. These reports move the turn boundaries the server confirms, so a harness that reports nothing while your product reports everything is measuring a different agent. If you don’t know which regime you’re in, run the turn-taking scenarios both ways and compare.

Metrics

Grouped by which failure they catch. All of them come from the event log plus your own fixtures, and none needs an LLM judge except where noted.

Task and correctness

Turn-taking

Latency

Define a start and an end event for every timer, and never mix populations.

Transcription and text quality

Usage

Transcription usage is reported as audio duration rather than a token count, so report seconds of audio and keep it separate from your own text-to-speech, judge, and simulated-caller costs. Those are your bill, not the agent’s behavior, and folding them together makes a prompt change look like a cost regression.
Run every scenario more than once. Generation isn’t deterministic, and neither is a boundary decision made on live audio. Report P50 and P90 for every latency and duration metric alongside the completion rate and the sample count. A single run tells you almost nothing about the tail.

Crawl, walk, run

Three harnesses, each adding one variable. Same scenarios, same expected outcomes, same graders throughout; only the caller input and the interaction complexity change.

Crawl: one synthesized request

Write the caller’s request as text, synthesize it once, and reuse that WAV forever. The audio is now a fixture: identical bytes, identical pacing, every run. What changes between runs is your prompt, your tools, or your application, which is what you want when you’re chasing a regression. For each scenario the harness:
  1. Loads the written request, the context and tools the agent gets, and the expected outcome, which the agent never sees.
  2. Streams the synthesized audio in paced PCM frames, at roughly the rate a real caller’s audio would arrive.
  3. Lets the agent answer, ask, refuse, or call a tool, running the tool against a fixture application.
  4. Records the event timeline, the reply text, the tool calls, and the final application state.
  5. Grades all of it against the expected outcome and saves the artifacts.
Pace those frames. Dumping a ten-second WAV in one input_audio_buffer.append hands voice activity detection ten seconds at once, and the turn boundary you measure afterwards is a property of your harness rather than the model. You don’t have to start from an empty conversation. To test a specific point mid-call, seed the earlier turns as text with conversation.item.create and stream only the turn under test as audio. That gives you real conversational context without the variance of replaying an entire call.
seeding history, then testing one turn
Representative failure modes, using a restaurant booking line as the running example:

Walk: the same task, on real audio

Walk keeps everything constant except the recording. Recordings of real callers, cleared for use, are worth far more here than synthetic ones: real voices, accents, microphones, and rooms are what you’re trying to survive. Where you can’t collect them, applying controlled effects to clean recordings is a usable fallback, as long as the effects are seeded and reproducible. Cover the conditions that reach a phone line: background noise across a range of signal-to-noise ratios, telephony band-limiting and codec artifacts, room reverberation, packet loss with concealment, and speakerphone echo where the caller’s channel carries your own reply back. Echo is the one to test first. Dialog-RSN-1 checks that sound arriving during generation is really the caller before treating it as a barge-in, and a speakerphone scenario, where your reply bleeds back into the caller’s microphone, is the direct test of that check. Walk can’t test clarification, recovery, or anything else that needs a second turn. That’s Run.

Run: a full conversation

Run connects your agent to an independent simulated caller and lets both sides talk until the task is done or clearly failed. Multi-turn memory, mid-conversation corrections, barge-in, and recovery from an unavailable option are only observable here. The reply has to be spoken on a real clock. Dialog-RSN-1 returns text far faster than your text-to-speech can say it, and during those seconds the caller is listening. Anything they do lands in that window. Feed the reply to the simulated caller as text and the window doesn’t exist, so you’ll never reproduce the barge-in, the backchannel, or the impatient interruption a real caller produces. Synthesize the reply, play it into the caller’s input on the conversation’s own timeline, and let the caller decide when to speak against audio that takes as long to hear as it really would. That timeline is where playback reporting earns its place, and Run is how you size what it buys you. Two things to grade:
  • Whether a sound during playback gets confirmed. Without reports it does only while the speaking-rate estimate says the reply is still playing. How often that misses depends on your voice’s real pace and your reply lengths.
  • What the caller heard, which comes from your side. poly.output_audio.stopped is where you say so, and Dialog-RSN-1 writes it into history. Without reports, history holds an estimate. Grade the confirmation the caller actually got against spoken_text, and keep the full reply text for grading the model.
For each scenario the harness:
  1. Loads the caller’s goal, persona, and opening line, plus the agent’s own context and the expected outcome it never sees.
  2. Runs both sides on one advancing timeline: caller audio into the socket, reply text out, reply audio back into the caller, and playback reports back into the socket as that audio plays.
  3. Lets the agent answer, clarify, refuse, or call tools while the caller reacts.
  4. Detects that the conversation has resolved, then drains remaining playback and any in-flight tool work.
  5. Grades the conversation, the tool calls, and the final application state, and saves the recording, the transcript, and the event log.
Give the cut-off row its own scenario if your agent collects digits at all. It’s the failure the turn-taking recipes exist to prevent, and the event log makes it unambiguous: two committed events for one spoken number means the caller was cut off.
Manual turns test generation, not turn-taking. With create_response: false, Dialog-RSN-1 skips its own end-of-turn check. A turn commits at your input_audio_buffer.commit, or at the first silence voice activity detection hears, whichever comes first, and nothing confirms it. That’s useful for determinism in Crawl, but any turn-taking metric from that run describes your harness and a raw detector rather than the model. Anything measuring boundaries needs server-driven turns.

Collecting the evidence

One collector serves all three stages. What matters is a single monotonic timeline with both directions on it.
timeline collector
If your harness plays replies aloud, report that back on the same socket and record what you sent alongside what you received:
reporting playback
Record the rest of the outbound side too: every input_audio_buffer.append with the timestamp you sent it and the number of samples it carried. Without it you can’t compute turn confirmation time, and you can’t tell a slow model from a harness that stalled for 400 milliseconds between frames. Keep, per run: the scenario id and version, the session configuration, the exact audio sent, the full event log in both directions, the reply text, the scope verdict, the tool calls and their results, the application state before and after, what your text-to-speech actually played, and the grader’s input and verdict. Anything you didn’t record, you can’t diagnose later.

Diagnosing a failure

Trace the request forwards and stop at the first seam where it went wrong. Each seam has a different fix.
  • The audio never arrived as intended. Frames sent too fast, at the wrong rate, or with a gap your harness introduced. Fix the sender, and invalidate the run rather than scoring it.
  • The transcript is wrong. An audio or acoustic problem, not a reasoning one. Reproduce it in Walk with the same recording.
  • The transcript is right and the tool arguments are wrong. A prompt or tool-description problem. Reproduce it in Crawl with the transcript seeded as text, which removes audio from the picture entirely.
  • The tool ran and the state is wrong. Your application, not the agent. The event log will show the correct call.
  • Everything is right and the reply is wrong. Response style or grounding. The reply text is right there; assert on it directly.
  • Everything is right and the caller heard it late. Your text-to-speech, your buffering, or your playback loop. Compare caller-perceived latency against time to first token to see which side of the seam the time went.
Before treating any of these as an agent failure, decide whether the run was valid at all. A run that sent the wrong file, stalled mid-stream, lost the socket, or lost its grader isn’t a failure; it’s a missing measurement. Report attempted runs, valid runs, infrastructure failures, passes, and failures as separate counts, and compute latency percentiles only over the valid population. Mixing broken sessions into a latency distribution invents regressions that aren’t there and hides the ones that are.

Making it yours

Start small. A handful of scenarios covering the behaviors your product depends on beats a hundred generated ones. Useful shapes: a question answerable from context that must not call a tool; an action that must reach the application exactly once with exact arguments; a correction mid-request; a missing detail that must be asked for rather than guessed; a forbidden action that must be refused; and a caller who talks over you. Change one variable at a time. Prompt, model, tool schema, audio condition, text-to-speech. Two at once and you can’t attribute the delta. Give every scenario a contract. A stable id and version, the fixture and its initial state, the caller behavior, the required outcome, the forbidden outcomes, the evidence that proves each, and the run policy. The same contract holds across all three stages: change how the caller’s audio is delivered, not what a pass means. Feed production back in. Review real calls, find the failure modes you didn’t imagine, reduce each to the smallest fixture that reproduces it, and add it to the suite.

Next steps

Reference

Every event your collector can record, and the known limitations worth writing scenarios against.

Prompt cookbook

The instructions text behind the behaviors these scenarios test.

Overview

Where turn-taking latency comes from, and why it dominates a turn.

Python from scratch

A working audio client to build a harness on top of.
Last modified on October 14, 2026