Skip to content

RTVI

Audio is only half of what a client needs. A usable UI also wants transcriptions as they arrive, who is speaking right now, and when the bot is thinking. RTVI (Real-Time Voice Interface) is the JSON protocol that carries those events over the WebRTC data channel.

Because it is an open protocol with existing web, iOS and Android client SDKs, a jargo server works with clients you did not write.

One processor, placed at the top of the pipeline, ahead of the input transport. What the client sends travels down the pipeline from there, by the same path a real caller’s input takes, and the messages it sends back reach the output transport at the far end:

pipeline.New(
rtvi.NewProcessor(), // <- here
t.Input(), vadProc, stt, turnsProc,
agg.User(), llm, tts,
t.Output(), agg.Assistant(),
)

That is the whole integration. The processor watches frames going past and translates the interesting ones into client messages.

flowchart LR
    C(["client"]) -->|"client-ready"| P["rtvi.Processor"]
    P -->|"bot-ready"| C

    F["pipeline frames"] --> P
    P -->|"user-transcription<br/>bot-llm-text<br/>bot-started-speaking<br/>metrics …"| C

    style P fill:#dbeafe,stroke:#2563eb

Incoming client messages arrive as InputTransportMessageFrames; outgoing ones are pushed downstream as OutputTransportMessageUrgentFrames. Urgent, so a transcription reaches the UI ahead of queued audio instead of lagging behind the speech it describes.

Every message is {"label":"rtvi-ai","type":…,"id":…,"data":…}. The processor speaks protocol version 2.3.0.

The client-ready handshake settles which version the session speaks. A client of this generation is answered with 2.3.0; one of the previous generation (1.x) is answered with its own version, so it stays on the paths it understands. Any other version gets an error-response saying so, and the session goes ahead anyway: the client is better placed than the bot to decide whether to carry on. Processor.ClientVersion reports what the client declared.

The bot-ready also carries capabilities: what the bot does in the session (audio_in, audio_out, video_in, screen_in, video_out and metrics), so a client can show only the UI that applies. The worker derives it from its transports and from Params.EnableMetrics; set WorkerConfig.Capabilities for what the pipeline cannot show. A field nobody knows is left out, and the client keeps its default for it.

In an expressive conversation (alpha), each bot-output for text the bot has spoken carries delivery, the delivery the words were spoken in (such as ["cheerful", "emphasis"], empty for the voice’s baseline), and each sound or pause the TTS performed is a bot-output segment of its own, of type expression_sound (its text is the sound’s name) or expression_pause (its text is the milliseconds). Sounds and pauses came with protocol 2.3.0, so an earlier client, which would take one for part of the bot’s response, is not sent them.

Type Meaning
client-ready → bot-ready The handshake.
user-transcription What the user said (interim and final). Deprecated: read user-input or stt-raw-text.
user-input What the user said or typed: transcriptions (interim and final), backchannels, and the text sent with send-text, acknowledged with its id and, when the transport knows, its sender. In an expressive conversation (alpha), a transcription with a reading of how the user spoke carries it in perception.
bot-transcription What the bot said.
bot-llm-text / bot-tts-text Streamed response text, per stage. Deprecated: read llm-raw-text and tts-raw-text.
stt-raw-text / llm-raw-text / tts-raw-text The text each service produced, as it produced it. Off by default, and only sent to clients of protocol 2.2.0 or later.
user-started-speaking / user-stopped-speaking Turn boundaries.
vad-user-started-speaking / vad-user-stopped-speaking The raw VAD signal, off by default.
bot-started-speaking / bot-stopped-speaking Bot speech boundaries.
bot-interrupted The bot was cut off; drop what it was mid-saying.
bot-llm-started / bot-llm-stopped Model is generating.
bot-tts-started / bot-tts-stopped Speech is being synthesized.
llm-function-call-started / -in-progress / -stopped Tool activity, stage by stage.
user-audio-level / bot-audio-level How loud each side is, for a speaking meter. Off by default.
dtmf Client presses keypad keys.
metrics TTFB, processing time, token usage.
send-text Client sends text instead of speech.
raw-audio / raw-audio-batch Client sends audio it captured itself.
disconnect-bot Client hangs up; the pipeline ends gracefully.
llm-function-call-result Result of a tool the client ran for the bot.
client-message → server-response Anything else the client asks, and the answer.
server-message Anything else the bot tells the client, unprompted.
error-response A request could not be carried out.
error Something failed.

The constants live in processor/rtvi (rtvi.TypeUserTranscription and so on), so you do not hand-write the strings.

client-message carries whatever the protocol has no message for. It arrives as a rtvi.ClientMessageFrame travelling downstream, so a processor anywhere in the pipeline can answer it by pushing a rtvi.ServerResponseFrame naming the request:

if msg, ok := f.(*rtvi.ClientMessageFrame); ok && msg.Type == "set-theme" {
return p.PushFrame(ctx, rtvi.NewServerResponseFrame(msg, map[string]any{"ok": true}), processor.Upstream)
}

rtvi.NewServerErrorResponseFrame(msg, reason) refuses it instead, and the client gets an error-response. Either way the request is answered, so a client waiting on a reply is never left waiting.

Outside the pipeline, attach to the processor’s rtvi.EventClientMessage and answer with SendServerResponse or SendErrorResponse. To tell the client something nothing asked for, push a rtvi.ServerMessageFrame or call SendServerMessage.

A tool call’s name and its arguments can carry information a client has no business seeing, so the observer reports the tool call id alone by default. Raise it per function, with "*" setting the default for the rest:

params := rtvi.ObserverParams{
FunctionCallReportLevel: map[string]rtvi.FunctionCallReportLevel{
"*": rtvi.ReportNone, // id only
"get_weather": rtvi.ReportFull, // name, arguments and result
},
}
observer := rtvi.NewObserverWithParams(proc, params)

The levels are disabled (no event at all), none, name, arguments (the name and the arguments, but no result) and full. The raw VAD speaking events are off by default in the same way, under VADUserSpeakingEnabled.

So are the audio levels a client draws a speaking meter from, under UserAudioLevelEnabled and BotAudioLevelEnabled. They are a message every AudioLevelPeriod (150 ms by default) for as long as the call lasts, which a client that draws no meter does not want. The level is loudness on a 0..1 scale, measured over a rolling 400 ms window rather than per frame, so it reads 0 until enough audio has arrived to measure.

For the browser, use the client packages in jargo-client-react.

The examples on the dev runner (examples/getting-started, examples/voice and the others built on the runner package) serve a prebuilt browser client at http://localhost:7860, so they need nothing extra. examples/echo and examples/voicebot serve their own minimal page.

The processor is optional. Leave it out and you still have working audio. You just have no event stream, so the UI cannot show live transcriptions or speaking state. Phone transports have no data channel at all, which is why examples/twiliobot omits it.

To send your own application messages instead, push an OutputTransportMessageFrame (ordered with the audio) or an OutputTransportMessageUrgentFrame (ahead of it), and read InputTransportMessageFrame for what the client sends back.