OpenTelemetry · GenAI semantic conventions

Your voice agent's worst moment is invisible to your tracing.

Every LLM observability tool assumes request → response. Real-time voice agents speak over a persistent duplex socket where neither exists — so the silence your user actually sat through never appears in a single span.

cadence reconstructs conversational turns from a raw streaming audio socket, measures the silence, catches every interruption, and ships it all to SigNoz over OTLP.

No signup, no API key — the replay runs a recorded session through the real console.

1180ms vs 340ms — the canyons in your telemetry
you speaking the silence nobody measures agent speaking
the gap

Four things break the moment audio goes bidirectional

The GenAI semantic conventions are still pre-1.0, and they model generative AI as a request/response call. For chat completions that is exactly right. For a voice agent it falls apart.

01

There is no span boundary

Nothing on the wire marks a request. Audio simply streams. Instrument it naively and you get one span per session covering twenty exchanges — which tells you nothing about any of them.

02

Duration is the wrong metric

What makes an agent feel alive isn't how long the exchange took. It's how long the human sat in silence before hearing anything. A turn can have great total duration and still feel broken, because all the delay landed at the front.

03

Barge-in doesn't exist

The most common failure mode of voice agents — the user talking over the model — has no representation in any convention, because in a request/response world it cannot happen.

04

Cost accounting breaks

Input isn't a countable prompt. It's an open microphone billed per second, plus video frames. Token totals without a modality split hide which stream is spending the money.

the model

Conversational structure, carved out of a signal stream

realtime.* composes with gen_ai.* rather than replacing it. Model inference inside a turn is still a standard chat span, so existing backends light up unchanged.

realtime.session                  one connected session
├── realtime.turn                    one exchange
│   ├── realtime.audio.user_utterance      VAD start → end
│   ├── chat                      [gen_ai.*]
│   │   └── execute_tool          incl. mid-stream
│   └── realtime.audio.agent_utterance     first audio → done
│       └── (event) realtime.barge_in  offset_ms
└── realtime.turn

Latency is measured at the audio boundary

From the last inbound frame of user audio to the first outbound frame of agent audio — deliberately including VAD dwell, network, queueing and time-to-first-token, because all of it is silence to the person waiting.

A barge-in ends the turn and starts a new one

An interruption isn't an error inside a turn. It's the user taking the floor, which is what a new turn is. The event lands on the utterance it cut off, at its true offset.

A missing metric beats a wrong one

Agent-initiated turns have no user speech to measure from, so TTFA is omitted rather than faked from session start. A fabricated value silently corrupts the p95 — and you'd act on it.

why the distribution matters

Same barge-in count. Opposite root causes.

offsets clustering < 400ms

Your VAD is misfiring

Users aren't interrupting — your agent thinks they are, because voice activity detection is triggering on background noise. Fix the VAD sensitivity.

offsets consistently > 2s

Your agent is rambling

People are cutting it off out of impatience. Shorten the replies. Nothing is wrong with the audio pipeline at all.

A barge-in counter cannot tell these apart. The offset histogram can — which is why realtime.barge_in.offset is a histogram, not a count.

signoz

Written to SigNoz. Then read back by the agent itself.

cadence exports over OTLP. The demo agent then queries the SigNoz API for its own traces — ask how fast it's been responding and it runs a p95 against the histogram its own turns populated seconds earlier, then tells you out loud.

1cadencetraces the duplex stream
2SigNozstores spans + metrics
3the agentqueries its own telemetry
instrumenttypewhat it tells you
realtime.turn.time_to_first_audiohistogramThe silence the user sat through. Buckets 50ms–5s, not OTel's HTTP-shaped defaults.
realtime.barge_in.offsethistogramHow far into a reply interruptions land. The distribution is the diagnostic.
realtime.turn.countcounterSplit by end reason — completed, interrupted, abandoned.
realtime.audio.input.secondscounterThe meter that runs while nobody is talking.
gen_ai.client.token.usagecounterStandard GenAI instrument, plus a modality dimension. Audio dominates.

Cardinality: realtime.session.id is deliberately not a metric attribute. Unique ids on metric dimensions are the standard way to melt a time-series backend. Session correlation belongs on spans, where it's free.

two lines

Wrap the session. Nothing else changes.

import cadence
cadence.configure(service_name="my-voice-agent")

async with client.aio.live.connect(model=MODEL, config=config) as raw:
    async with cadence.CadenceSession(raw, model=MODEL) as session:
        async for message in session.receive():
            ...  # every message passes through untouched

CadenceSession wraps rather than monkey-patches — the Live API surface is still moving, and patching a library mid-flight is how instrumentation gets silently broken by a minor release. Instrumentation also never raises into the audio path: a bug in cadence must not take down the agent it's watching. There's a test for exactly that.