Where the system splits
Dialog-RSN-1 writes each reply before it’s spoken, and an evaluation harness asks for it as text withoutput_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 setoutput_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
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_msis 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_idfromresponse.createdthrough every report about it. - Always send
stopped. A generation clears the open report, so astartedwith nostoppedwon’t wedge the session, but it does leave a window where the server believes a finished reply is still playing.
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:- Loads the written request, the context and tools the agent gets, and the expected outcome, which the agent never sees.
- Streams the synthesized audio in paced PCM frames, at roughly the rate a real caller’s audio would arrive.
- Lets the agent answer, ask, refuse, or call a tool, running the tool against a fixture application.
- Records the event timeline, the reply text, the tool calls, and the final application state.
- Grades all of it against the expected outcome and saves the artifacts.
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
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.stoppedis 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 againstspoken_text, and keep the full reply text for grading the model.
- Loads the caller’s goal, persona, and opening line, plus the agent’s own context and the expected outcome it never sees.
- 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.
- Lets the agent answer, clarify, refuse, or call tools while the caller reacts.
- Detects that the conversation has resolved, then drains remaining playback and any in-flight tool work.
- 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.
Collecting the evidence
One collector serves all three stages. What matters is a single monotonic timeline with both directions on it.timeline collector
reporting playback
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.
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.

