Skip to main content
In the speech-to-speech implementation, audio flows straight in and out of one end-to-end speech model, which handles recognition, understanding, and synthesis internally. Compared with turn-based pipelines, it does not wait for the user to finish before running each stage, so latency stays low, tone and emotion survive the round trip, and the user can interrupt at any time. AgentScope implements this through RealtimeAgent, which supports:
  • Turn detection: let the provider API decide when the user has finished, or plug in a local VAD and decide yourself
  • Barge-in: the user speaking cuts off the current reply, and the context keeps only what the user actually heard; your code can interrupt as well
  • Tool calling with user confirmation: works with Toolkit and the permission system, and the audio stream keeps running while the user decides
  • Text input: send text during a voice conversation, on models that accept text
  • Automatic reconnection: after the provider API closes a session on idle or timeout, the next input reconnects and restores the conversation
  • Turn aggregation: merge sentences split by a pause, and drop acknowledgements that carry no content
The table below lists the supported provider APIs and models. Each model class takes the credential of the API it belongs to, exactly like every other model in AgentScope:
Calling list_models() on a model class returns the model cards of every model under that API, carrying its sample rates, context limits, and available voices, ready to render a model selector in the frontend.

Core Concepts

A speech-to-speech agent is built from three components:
  • Audio transport (TransportBase): where sound comes from and goes to, such as a local sound card or a browser
  • Realtime speech model (RealtimeModelBase): the session with the provider API, translating protocol messages into uniform model events
  • RealtimeAgent: sits between the two, tracking conversation turns and handling barge-in, tool calls, and event output
The diagram below shows how audio and events move between them: Each component owns a distinct set of responsibilities: Both the model and the transport extend from a base class, so you can adapt a new provider API or connect a different client such as a browser.

Quick Start

Start by installing the realtime extra, which brings in the WebSocket client and the local sound card library:
Install the realtime dependencies
The sound card library sounddevice depends on PortAudio. macOS and Windows ship it with the package; on Debian/Ubuntu, run apt install libportaudio2 first.
The four steps below build a voice agent on the local microphone that talks back and can be interrupted:
1

Create the realtime model

A model class takes a model name and the credential of the API it belongs to. The model card is matched by name, which fixes the sample rates and context limits, while the voice, turn detection method, and other tuneables go through Parameters. The tabs below create the model on each provider API; the three steps that follow are identical whichever you pick:
2

Create the agent

The agent owns the model session, and the system prompt is sent once when it connects:
Create the agent
3

Create the audio transport

The transport decides where sound comes from and goes to, and LocalAudioTransport uses this machine’s microphone and speaker. Its sample rates must match the model’s, so build it from the model’s properties instead of hardcoding the numbers:
Create the audio transport
4

Run the conversation

reply_stream() borrows the transport to pump audio continuously and emits events as an async iterator. The user speaking is reported as a reply too, with role set to "user", so the loop below tells the two sides apart by reply_id and prints both to the terminal:
Run the conversation and print both sides
Speak into the microphone to hear a reply, speak again mid-reply to interrupt it, and press Ctrl+C to exit.
Three lifecycles run in this example, each owned by a different object: Keeping the three apart means a client reconnecting does not lose the model session, and a model session timing out does not affect the transport. The same agent can call reply_stream() again with a different transport, with the conversation history still in agent.state.

Use the Agent

RealtimeAgent takes the following constructor arguments:
str
required
The agent’s name, written into agent messages and events.
str
required
The system prompt, sent once when connecting to the model, with the toolkit’s skill descriptions appended.
RealtimeModelBase
required
The realtime speech model, see the model table above.
Toolkit | None
default:"None"
The toolkit the model can call. Tools run on the agent side and go through permission checks.
AgentState | None
default:"None"
Conversation history, permission rules, and tool context. A new state is created when omitted.
VADBase | None
default:"None"
Local voice activity detection. When provided, it decides the turn boundaries and the provider API’s own turn detection is disabled, see Turn Detection.
TurnAggregator | None
default:"None"
The turn aggregator that merges split sentences and drops empty acknowledgements. Uses the default configuration when omitted.
Its core methods are:

Run and Handle Events

reply_stream() takes a transport that is already started, keeps feeding its audio to the model, and emits events as an async iterator until the transport’s input ends. You own the transport, so reply_stream() does not close it when it returns, and the same agent can run again with a different one. reply_stream() emits the same agent events as Agent.reply_stream, so event handling written for a text agent works unchanged. The user speaking is reported as a reply as well: a ReplyStartEvent with role set to "user" when they start, a ReplyEndEvent when they stop, and text block events once the transcript is final, which lets the outer loop assemble user and agent messages with one set of logic. The model’s audio arrives as data block events:

Send Input

On models that accept text input, send() delivers text during a voice conversation. The text first interrupts the current reply, then reaches the model as one user turn, and the reply still comes back as speech:
Send text input
The input types send() accepts line up with Agent.reply:
The Qwen-Omni API behind DashScopeRealtimeModel accepts no text turns. Every other model class supports text input, which the supports_text_input class attribute reports.

Barge-In

When the user speaks while a reply is playing, the agent stops playback immediately and cancels the reply on the model side. The transport reports how far playback actually got, and the agent truncates the agent message in the context to the part the user really heard, so the model does not assume the whole sentence landed. You can also interrupt from code, for example in response to a stop button:
Interrupt the current reply
An interrupted reply ends with a ReplyEndEvent whose finished_reason is interrupted. Because text deltas arrive ahead of audio, the frontend has already received more text than the user heard, so that text block’s TextBlockEndEvent carries a text field with the final text. Msg.append_event applies it automatically; a frontend assembling messages itself needs to replace the block’s content with it. The agent-side context is always truncated. What happens on the model side depends on the provider’s protocol, which a model class reports through its truncation attribute:

Call Tools

With a toolkit provided, the model can call tools during the conversation. Tools run on the agent side, and permission checks and user confirmation work as they do for a regular agent. The difference is that a realtime agent never pauses: after emitting RequireUserConfirmEvent, reply_stream() keeps emitting other events, and you send the result back through send() whenever it is ready, decoupled from the event stream itself:
Attach tools and receive confirmation requests
Once the user decides, send a UserConfirmResultEvent back to the agent. The call can live in a UI callback, a WebSocket message handler, or terminal input. The two tabs below show both:
Two things to keep in mind when using tools:
  • Only models whose model card sets supports_tools receive the tool list. Among DashScope’s Qwen-Omni models, that is the qwen3.5 series only; every model on the other provider APIs supports tools.
  • A confirmation request that goes unanswered for five minutes is treated as a rejection.
Realtime agents do not support meta tools (tool groups) yet: the tool list and system prompt are sent once when connecting to the model, so activating a tool group or adding a tool mid-session has no effect. Put every tool you need into the toolkit at construction time.

Turn Detection

Turn detection decides when the user has finished speaking, and only one side can own it. By default the provider API does, and the agent just reacts to the speech start and stop it reports. Passing a vad argument moves the decision to the agent and disables turn detection on the provider API side. The two modes compare as follows: Each provider API accepts its own turn_detection values, with sensitivity and silence duration configured through Parameters as well: Local detection means implementing VADBase: push() receives every PCM16 chunk the transport delivers and returns a SpeechTransition only on the chunk where speech starts or ends, and None otherwise; reset() clears the internal state when the audio stream breaks, such as on a reconnection. Passing vad sets turn_detection to none for you:
Plug in a local VAD
In either mode, the user transcript reported by the provider API or detected locally passes through a TurnAggregator before it is written to the context:
Configure turn aggregation

Automatic Reconnection

Every provider API closes sessions on its own, only the trigger differs: DashScope times out after around three minutes of silence, a Gemini Live audio session is capped at around 15 minutes, and OpenAI Realtime at around an hour. The agent treats this as normal: it logs an INFO line, keeps the transport open, and reconnects on the user’s next utterance, replaying the audio recorded in the meantime. The conversation history lives in agent.state, and on reconnection the agent appends the earlier transcript to the system prompt, so the model picks the topic back up.
Long silences and long conversations are both safe, and neither needs handling for the session timeout. To continue the same conversation after a client disconnects, keep the agent open and call reply_stream() again with a new transport.

Audio Transport

The audio transport decides where sound comes from and goes to, and the agent does not care whether it is a local sound card or a browser. AgentScope currently provides:

Local Sound Card

LocalAudioTransport takes the following arguments:
int
default:"16000"
The capture sample rate, which must equal the model’s input_sample_rate.
int
default:"24000"
The playback sample rate, which must equal the model’s output_sample_rate.
int | str | None
default:"None"
The input device’s index or name. Uses the system default when omitted.
int | str | None
default:"None"
The output device’s index or name. Uses the system default when omitted.
int
default:"100"
The duration of each uplink audio chunk.
int
default:"30"
The fade-out applied to audio still playing when an interruption happens, which avoids a pop.
Sample rates differ between provider APIs (DashScope captures at 16 kHz, OpenAI and xAI at 24 kHz), so build the transport from the model’s properties instead of hardcoding the numbers. When the default devices are not the right ones, list the available devices with sounddevice and pick one by index:
List audio devices
Two suggestions for working with a local sound card:
  • Wear headphones. On speakers, the microphone picks up the agent’s own voice, turn detection reads it as the user speaking, and the agent interrupts itself. LocalAudioTransport does no echo cancellation.
  • Do not let one Bluetooth headset handle both input and output. macOS switches devices such as AirPods into hands-free mode, which often ends up silent. Pair the headset microphone with the built-in speaker instead, for example LocalAudioTransport(input_device=3, output_device=2).

Custom Transport

To connect a browser or another audio source, subclass TransportBase and implement the following: The position returned by clear_audio() is what context truncation relies on, so track playout progress as close to the speaker as possible: in a browser, inside the AudioWorklet.