What should I trace in a voice assistant?
Rius traces a voice assistant best as one root span per conversation turn, with a span for each model call, tool and step, and a session and user on every span. Each turn becomes one trace, so the root span’s duration is the latency the caller heard. The session groups the turns of one call, and the user ties together that person’s calls.
How it works
A trace is one complete run, and it has exactly one root span. For a voice assistant, make a run one turn: the caller speaks, the assistant works, the assistant answers. The tree under the root span looks like this:
- Root span per turn. Wrap the function that handles a turn with
observeand theAGENTkind. Its duration and status are what run-level views show, so a failed root is a failed turn. - Generations for model calls. Each call to a language model is a span of kind
LLMthat records the model, provider, token usage, finish reasons and optionally the messages. Rius computes cost from the model and token counts, so you send usage and never cost. - Spans for tools. Each tool the assistant runs, such as a lookup or a booking, is a
TOOLspan named likeexecute_tool get_weather. - Spans for steps. Anything else in the turn, such as transcribing audio, rewriting the query, retrieving documents or preparing the spoken reply, is its own span. A bare
observemakes aCHAINspan named after the function, andRETRIEVERfits a search step. - Session and user on every span. The session is the call or conversation, and the user is the person speaking.
If your model calls are streamed, the integration records the first chunk for you; on a manual generation, call gen.record_first_token() when the first chunk arrives. Rius derives time to first token from it, which is the wait a caller feels before the assistant starts to answer.
Set it up
-
Install and initialize the SDK once. Tracing your code and the quickstart cover the full setup.
-
Wrap each turn in a root
AGENTspan, and wrap each tool with theTOOLkind. A tool function you trace yourself needskind=SpanKind.TOOL. A bareobservemakes aCHAINspan, which the Tool kind filter and the Tool Loop alert never see. -
Open a session scope per call and a user scope per caller. Both ride OpenTelemetry context, so every span inside them, including spans from auto-instrumentation, carries
session.idanduser.id.
import rius
from rius import SpanKind
rius.init(api_key="ri_...", service_name="voice-assistant")
@rius.observe(name="transcribe", capture_input=False)
def transcribe(audio): ...
@rius.observe(kind=SpanKind.TOOL)
def get_weather(city: str): ...
@rius.observe(kind=SpanKind.AGENT)
def handle_turn(audio):
text = transcribe(audio)
# the model call and get_weather(...) happen here
...
with rius.session(call_id): # your own call or conversation id
with rius.user(caller_id): # an opaque id, not a phone number or email
handle_turn(audio)-
Record the model calls. With an integration installed, such as the OpenAI or Anthropic one, each call becomes a generation automatically. If you trace a call by hand with a generation, turn that integration off by passing an explicit
instrumentslist without it torius.init, or the call is counted twice. Then set the usage yourself, or the generation carries no tokens or cost. -
Set the session id per call, not per process. A process-wide
RIUS_SESSION_IDwould merge every caller into one session on a server that handles many calls. Each turn stays its own trace. -
Pass
capture_input=Falseorcapture_output=Falseon steps whose arguments are raw audio or other large payloads. Values are truncated at 32 KB anyway. SettingRIUS_CAPTURE_CONTENTto false strips prompt and response content at export while keeping timing, tokens and cost.
What you get
- One trace per turn. The waterfall shows where the time went, and the root span’s duration is the turn’s latency.
- Cost per model call. Rius estimates it in US dollars at list price from the model and token counts.
- Time to first token. You get this number for streamed generations that record their first chunk.
- Calls as sessions. The turns of one call group together, even across deploys or replicas. If your session count equals your trace count, nothing is setting
session.id. - Callers as users. Each caller shows with their traces, failures, cost and time to first token. A span outside any user scope is anonymous.
- Errors in context. An exception sets ERROR status and
error.typeon the failing span, and the waterfall highlights the path from the root to it.
For how these pieces fit together, read Concepts. To watch the assistant once it runs, see Monitor AI agents in production.
When something else fits
- If the assistant is a single model call with no tools or steps, a bare generation under one root span is enough, and you can skip the extra step spans.
- If one process serves a single call from start to finish, such as a CLI test run, a process-wide session id with
RIUS_SESSION_IDis simpler than scoping each call.