Realtime audio
In shortPython API reference for live audio-to-audio agent sessions with Gemini Live, including AudioAgent, RealtimeConfig, LiveInputQueue and RealtimeEvent.
- 14 min read
- 13 sections
- Updated
- v0.10.0
- Markdown
This page is the Python API reference for live audio-to-audio sessions backed by Gemini Live: AudioAgent, arealtime(), LiveInputQueue, the config objects, and the RealtimeEvent types. Realtime is a separate runtime from invoke and stream, where the provider drives each turn. Install it with pip install "10xgraph[realtime]". The matching server WebSocket route is covered in Live WebSocket.
The provider owns the turn loop. 10xGraph adds input queueing, tool dispatch, transcript persistence, and automatic reconnection around it. The provider SDK is imported lazily, so importing tenxgraph.core.realtime does not require the extra.
Import paths
Everything except AudioAgent and the sample-rate constants is exported from tenxgraph.core.realtime.
from tenxgraph.core.realtime import (
# Input queue
LiveInputQueue, LiveInput, LiveInputKind,
# Config types
RealtimeConfig, VADConfig, ReconnectConfig,
# Event union and all event classes
RealtimeEvent,
AudioDeltaEvent, InputTranscriptEvent, OutputTranscriptEvent,
ToolCallEvent, ToolResultEvent, TurnCompleteEvent, InterruptedEvent,
SessionUpdateEvent, GoAwayEvent, AgentChangedEvent, ErrorEvent,
# Provider client (Gemini)
RealtimeClient, GeminiLiveClient, normalize_message,
)
from tenxgraph.prebuilt.agent import AudioAgent
from tenxgraph.core.realtime.base import INPUT_SAMPLE_RATE, OUTPUT_SAMPLE_RATEAudio formats: INPUT_SAMPLE_RATE = 16000 Hz (PCM16), OUTPUT_SAMPLE_RATE = 24000 Hz (PCM16).
AudioAgent
AudioAgent is the prebuilt builder for realtime audio-to-audio sessions. It mirrors ReactAgent, but wraps a LiveAgent as the graph root and is driven by CompiledGraph.arealtime() instead of invoke or stream. Only Gemini Live is supported. For a walkthrough, see Audio agent.
import asyncio
from tenxgraph.core.realtime import LiveInputQueue
from tenxgraph.prebuilt.agent import AudioAgent
def get_time() -> str:
"""Return the current time as a string."""
from datetime import datetime
return datetime.now().isoformat(timespec="seconds")
agent = AudioAgent(
model="gemini-2.5-flash", # use the Gemini Live model your account has access to
system_prompt=[{"role": "system", "content": "You are a helpful assistant."}],
tools=[get_time],
)
app = agent.compile()
async def main() -> None:
queue = LiveInputQueue()
queue.send_text("What time is it?")
queue.close() # closing the queue ends the session once the provider goes idle
async for event in app.arealtime(queue, config={"thread_id": "demo"}):
if event.type == "output_transcript" and event.finished:
print("Model:", event.text)
elif event.type == "error":
print("Error:", event.message)
asyncio.run(main())Set GEMINI_API_KEY or GOOGLE_API_KEY before running.
Constructor
AudioAgent(
model: str,
state: StateT | None = None,
context_manager: BaseContextManager | None = None,
publisher: BasePublisher | list[BasePublisher] | None = None,
id_generator: BaseIDGenerator = DefaultIDGenerator(),
container: Any | None = None,
*,
realtime_config: RealtimeConfig | None = None,
system_prompt: list[dict[str, Any]] | None = None,
tools: Iterable[Callable] | None = None,
client: Any = None,
pass_user_info_to_mcp: bool = False,
skills: SkillConfig | None = None,
memory: MemoryConfig | None = None,
realtime_client_factory: Callable[[], RealtimeClient] | None = None,
live_node_name: str = "LIVE",
**agent_kwargs: Any,
)| Parameter | Type | Default | Description |
|---|---|---|---|
model |
str | Required | Gemini Live model name. The provider is detected from the name (an optional gemini/ prefix works); any provider other than Google raises ValueError. |
state |
StateT | None |
None | Initial AgentState subclass instance. |
context_manager |
BaseContextManager | None |
None | Context trimming or summarization manager. |
publisher |
BasePublisher | list[BasePublisher] | None |
None | One publisher or a list of them. |
id_generator |
BaseIDGenerator |
DefaultIDGenerator() |
ID generator for thread, run, and message IDs. |
container |
Any | None |
None | InjectQ container for dependency injection isolation. |
realtime_config |
RealtimeConfig | None |
None | Per-session config overrides; defaults to RealtimeConfig(model=model). |
system_prompt |
list[dict[str, Any]] | None |
None | System prompt dicts with role and content keys. |
tools |
Iterable[Callable] | None |
None | Callable tools advertised to the provider. |
client |
Any | None |
None | MCP client (fastmcp or mcp module). |
pass_user_info_to_mcp |
bool | False | Forward run’s user identity to MCP tool calls. |
skills |
SkillConfig | None |
None | Dynamic skill injection config. |
memory |
MemoryConfig | None |
None | Long-term memory config. |
realtime_client_factory |
Callable[[], RealtimeClient] | None |
None | Custom provider client factory (for testing or pre-auth). |
live_node_name |
str | “LIVE” | Name of the internal LiveAgent node. |
**agent_kwargs |
Any | none | Forwarded to the underlying LiveAgent. api_key, use_vertex_ai, project and location configure the Gemini client; the rest go to the base agent. |
Returns: an AudioAgent instance.
A ToolNode is created only if tools or client is provided. The graph has a single node (named by live_node_name) that owns the whole session loop.
For Vertex AI, pass use_vertex_ai=True and set GOOGLE_CLOUD_PROJECT (or pass project=); GOOGLE_CLOUD_LOCATION defaults to us-central1.
compile()
def compile(
self,
checkpointer: BaseCheckpointer | None = None,
store: BaseStore | None = None,
callback_manager: CallbackManager | None = None,
shutdown_timeout: float = 30.0,
) -> CompiledGraph| Parameter | Type | Default | Description |
|---|---|---|---|
checkpointer |
BaseCheckpointer | None |
None | Persistence layer for thread state and resumption. |
store |
BaseStore | None |
None | Long-term memory store. |
callback_manager |
CallbackManager | None |
None | Lifecycle hooks (on_graph_start, on_turn_end, etc.). |
shutdown_timeout |
float | 30.0 | Seconds to wait for graceful shutdown. |
Returns: a CompiledGraph ready to drive with arealtime() or realtime(). Unlike ReactAgent.compile(), it does not accept media_store, interrupt_before or interrupt_after. Images and video go frame by frame through LiveInputQueue.send_image.
CompiledGraph.arealtime() / realtime()
arealtime() runs a realtime session and yields normalized events. realtime() is a synchronous wrapper for code with no running event loop. The session ends when the input queue is closed and the provider goes idle, or on a fatal error.
async def arealtime(
input_queue: LiveInputQueue,
config: dict[str, Any] | None = None,
state: AgentState | None = None,
) -> AsyncIterator[RealtimeEvent]
def realtime(
input_queue: LiveInputQueue,
config: dict[str, Any] | None = None,
state: AgentState | None = None,
) -> Generator[RealtimeEvent]| Parameter | Type | Default | Description |
|---|---|---|---|
input_queue |
LiveInputQueue |
Required | Input queue fed by audio/text/control frames. |
config |
dict[str, Any] | None |
None | Runtime config: thread_id, user_id, and realtime, a dict of RealtimeConfig field overrides for this session (unknown keys and None values are ignored; the merged result is re-validated). |
state |
AgentState | None |
None | Initial state. Falls back to the state given to AudioAgent, then an empty AgentState. |
Yields: RealtimeEvent objects (audio chunks, transcripts, tool calls, errors).
Raises:
RuntimeErrorif the graph contains noLiveAgent, or more than one.RuntimeErrorifrealtime()is called from a running event loop (usearealtime()instead).
Calling invoke or stream on a graph that contains a LiveAgent also raises RuntimeError.
LiveInputQueue
Non-blocking input queue for audio, text, and control frames. Synchronous send_* methods allow producers to feed frames from any thread without blocking (e.g., audio capture callbacks).
from tenxgraph.core.realtime import LiveInputQueue
queue = LiveInputQueue(maxsize=100)
queue.send_audio(b"\x00\x00" * 1600, sample_rate=16000) # 100 ms of PCM16 silence
queue.send_text("Hello")
queue.send_activity_end() # Manual VAD only: mark end of user turn
queue.close()
async def drain() -> None:
async for frame in queue: # yields LiveInput frames until the close sentinel
print(frame.kind, frame.sample_rate)Constructor
LiveInputQueue(maxsize: int = 0)| Parameter | Type | Default | Description |
|---|---|---|---|
maxsize |
int | 0 | Queue size. 0 means unbounded. A full queue drops new frames and logs a warning. |
The read-only property closed: bool is True once close() has been called.
Methods
| Method | Signature | Description |
|---|---|---|
send_audio |
(data: bytes, sample_rate: int = 16000) -> None |
Enqueue PCM16 audio chunk. |
send_text |
(text: str) -> None |
Inject text turn. |
send_image |
(data: bytes, mime_type: str = "image/jpeg") -> None |
Send image or video frame. |
send_activity_start |
() -> None |
Manual VAD: mark start of user speech. Use when vad.enabled=False. |
send_activity_end |
() -> None |
Manual VAD: mark end of user speech. Pairs with send_activity_start. |
close |
() -> None |
Signal end of input. Idempotent; enqueues a close sentinel. Later sends are dropped silently. |
LiveInput
LiveInput is the dataclass for a single frame enqueued by the send_* methods. You normally do not construct it directly.
@dataclass(slots=True)
class LiveInput:
kind: LiveInputKind # "audio" | "text" | "image" | "activity_start" | "activity_end" | "close"
data: bytes | None = None
text: str | None = None
sample_rate: int = 16000 # INPUT_SAMPLE_RATE
mime_type: str | None = None| Field | Type | Default | Notes |
|---|---|---|---|
kind |
LiveInputKind |
Required | Frame type discriminator. |
data |
bytes | None |
None | Audio or image bytes (only set for "audio", "image"). |
text |
str | None |
None | Text content (only set for "text"). |
sample_rate |
int | 16000 | Sample rate for audio frames. |
mime_type |
str | None |
None | MIME type for image frames. |
RealtimeConfig
RealtimeConfig is the per-session configuration passed to the provider client at connect time. It is a Pydantic model; pass it to AudioAgent(realtime_config=...).
from tenxgraph.core.realtime import RealtimeConfig, VADConfig
config = RealtimeConfig(
model="gemini-2.5-flash",
response_modalities=["AUDIO"],
voice="Puck",
vad=VADConfig(enabled=True),
)Constructor
RealtimeConfig(
model: str,
response_modalities: list[Literal["AUDIO", "TEXT"]] = ["AUDIO"],
voice: str | None = None,
system_instruction: str | None = None,
input_audio_transcription: bool = True,
output_audio_transcription: bool = True,
vad: VADConfig = VADConfig(),
reconnect: ReconnectConfig = ReconnectConfig(),
context_window_compression: bool = False,
session_resumption: bool = True,
tools: list[Any] | None = None,
tools_tags: list[str] | None = None,
)| Field | Type | Default | Description |
|---|---|---|---|
model |
str | Required | Gemini Live model string. |
response_modalities |
list[Literal["AUDIO", "TEXT"]] |
["AUDIO"] |
Exactly one modality per session (validated). Use ["TEXT"] for text-only. |
voice |
str | None |
None | Provider voice name (e.g. "Puck", "Breeze"). Provider default when None. |
system_instruction |
str | None |
None | System instruction fixed at connect time. Used only when AudioAgent.system_prompt is empty. |
input_audio_transcription |
bool | True | Enable provider-side transcription of user speech. |
output_audio_transcription |
bool | True | Enable provider-side transcription of model speech. |
vad |
VADConfig |
VADConfig() |
Voice-activity detection settings. |
reconnect |
ReconnectConfig |
ReconnectConfig() |
Reconnect/backoff policy for drops. |
context_window_compression |
bool | False | Enable provider-side context compression. |
session_resumption |
bool | True | Use provider resumption handles. Loading and saving a handle across sessions requires a checkpointer and thread_id. |
tools |
list[Any] | None |
None | Override auto-derived tool schemas. |
tools_tags |
list[str] | None |
None | Filter tools by tag. |
Validation: response_modalities must contain exactly one entry; ["AUDIO", "TEXT"] raises ValueError.
VADConfig
VADConfig holds voice-activity detection settings. Disable it for push-to-talk workflows and drive turns with send_activity_start and send_activity_end.
from tenxgraph.core.realtime import VADConfig
vad = VADConfig(
enabled=False, # Manual VAD
start_sensitivity="HIGH",
)Constructor
VADConfig(
enabled: bool = True,
start_sensitivity: str | None = None,
end_sensitivity: str | None = None,
prefix_padding_ms: int | None = None,
silence_duration_ms: int | None = None,
)| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | True | Automatic VAD. Set False for manual push-to-talk via send_activity_start/end. |
start_sensitivity |
str | None |
None | Provider-neutral hint (mapped per provider). None = provider default. |
end_sensitivity |
str | None |
None | Provider-neutral hint. None = provider default. |
prefix_padding_ms |
int | None |
None | Audio prepended before detected speech onset. |
silence_duration_ms |
int | None |
None | Silence duration triggering end-of-speech detection. |
ReconnectConfig
ReconnectConfig sets the reconnect and exponential backoff policy for dropped sessions. All three fields must be zero or greater.
from tenxgraph.core.realtime import ReconnectConfig
reconnect = ReconnectConfig(
base_delay=1.0,
max_delay=30.0,
max_attempts=10,
)Constructor
ReconnectConfig(
base_delay: float = 0.5,
max_delay: float = 10.0,
max_attempts: int = 5,
)| Field | Type | Default | Description |
|---|---|---|---|
base_delay |
float | 0.5 | Base delay in seconds for exponential backoff. |
max_delay |
float | 10.0 | Maximum delay cap in seconds. |
max_attempts |
int | 5 | Maximum error-driven reconnect attempts. Set 0 to disable. |
Reconnect rules:
go_away(provider-initiated rotation): reconnect immediately, attempts counter not incremented.- Transient drop / receive error: attempt
nwaitsmin(base_delay * 2^(n-1), max_delay)seconds, up tomax_attempts. After cap, fatalErrorEvent(code="reconnect_failed")is emitted. - Any successful receive resets attempts counter to 0.
RealtimeEvent
RealtimeEvent is a discriminated union of event types keyed on the type field. Every event is a Pydantic model, and arealtime() yields them in provider order.
from tenxgraph.core.realtime import RealtimeEvent
async for event in app.arealtime(queue):
if event.type == "audio_delta":
speaker.write(event.data)
elif event.type == "input_transcript":
print(f"User: {event.text}" if event.finished else f"User (partial): {event.text}")
elif event.type == "error":
if event.fatal:
breakAudioDeltaEvent
type: Literal["audio_delta"]
data: bytes # PCM16 chunk
sample_rate: int # 24000Model audio output (PCM16). Write it to a speaker or file.
InputTranscriptEvent
type: Literal["input_transcript"]
text: str
finished: bool # True on final chunkThe user’s speech transcript. Chunks arrive with finished=False, then the finished marker. The runtime turns the accumulated transcript into a message on the finished marker.
OutputTranscriptEvent
type: Literal["output_transcript"]
text: str
finished: boolModel’s speech transcript. Same streaming/finished semantics as InputTranscriptEvent.
ToolCallEvent
type: Literal["tool_call"]
id: str
name: str
args: dict[str, Any]Provider requests tool invocation. 10xGraph dispatches automatically via ToolNode.
ToolResultEvent
type: Literal["tool_result"]
id: str
result: AnyTool execution finished. Result already sent to model.
TurnCompleteEvent
type: Literal["turn_complete"]Model finished generating a turn (audio and transcription complete).
InterruptedEvent
type: Literal["interrupted"]Barge-in: the user spoke while the model was talking. Flush any queued playback.
SessionUpdateEvent
type: Literal["session_update"]
resumption_handle: str | NoneProvider issued a session-resumption handle. Stored in checkpointer thread metadata automatically.
GoAwayEvent
type: Literal["go_away"]
time_left: str | None # Provider duration string, e.g. "5s"Provider will close socket soon (planned rotation). Runtime reconnects automatically with cached resumption handle.
AgentChangedEvent
type: Literal["agent_changed"]
author: strThe active agent or persona changed. Reserved for a future multi-agent persona swap; the single-agent runtime does not emit it.
ErrorEvent
type: Literal["error"]
code: str | None
message: str
fatal: boolNormalized provider error. Non-fatal errors are transient (session continues). Fatal errors (fatal=True) end session. code="reconnect_failed" means reconnect attempts exhausted.
RealtimeClient (Protocol)
RealtimeClient is the provider-neutral, runtime-checkable protocol that every realtime provider client implements. Application code rarely uses it directly; use AudioAgent instead, and implement this protocol only to supply a custom or fake client.
from tenxgraph.core.realtime import RealtimeClient| Method | Signature | Description |
|---|---|---|
connect |
async (config: RealtimeConfig, resume_handle: str | None = None) -> None |
Open provider socket. |
send_audio |
async (pcm: bytes, sample_rate: int) -> None |
Send PCM16 audio input. |
send_text |
async (text: str) -> None |
Send text turn. |
send_image |
async (data: bytes, mime_type: str) -> None |
Send image frame. |
send_activity_start |
async () -> None |
Manual VAD: start marker. |
send_activity_end |
async () -> None |
Manual VAD: end marker. |
send_tool_response |
async (call_id: str, name: str, result: Any) -> None |
Return tool result to model. |
reseed_history |
async (messages: list[Any]) -> None |
Seed conversation history into fresh session. |
receive |
() -> AsyncIterator[RealtimeEvent] |
Yield normalized events from provider. |
close |
async () -> None |
Close socket. Safe to call multiple times. |
GeminiLiveClient
GeminiLiveClient implements RealtimeClient for Gemini Live. LiveAgent creates one per connection by default, so you only need it to customize how the client is built.
from tenxgraph.core.realtime import GeminiLiveClient, normalize_message
from tenxgraph.prebuilt.agent import AudioAgent
# Inject a custom client per connection (for tests or pre-built credentials).
def client_factory() -> GeminiLiveClient:
return GeminiLiveClient(api_key="YOUR_API_KEY") # placeholder, load from your secret store
app = AudioAgent("gemini-2.5-flash", realtime_client_factory=client_factory).compile()The constructor is GeminiLiveClient(client=None, *, connector=None, api_key=None, use_vertex_ai=False, project=None, location=None). Without api_key, it reads GEMINI_API_KEY or GOOGLE_API_KEY.
normalize_message(message) maps a Google LiveServerMessage (the SDK object, not a dict) to a list[RealtimeEvent] in wire order.
Lifecycle hooks
Realtime sessions fire GraphLifecycleHook methods: the session acts as the graph run, and each model turn fires turn hooks. Register a hook with CallbackManager.register_lifecycle_hook() and pass the manager to compile().
from tenxgraph.utils.callbacks import CallbackManager, GraphLifecycleHook
class MyHook(GraphLifecycleHook):
async def on_graph_start(self, context, state):
print("Session started")
return None # None keeps the current state
async def on_turn_end(self, context, state, turn_index):
print(f"Turn {turn_index} complete")
return None
cb = CallbackManager()
cb.register_lifecycle_hook(MyHook())
app = AudioAgent("gemini-2.5-flash").compile(callback_manager=cb)| Hook | When | Notes |
|---|---|---|
on_graph_start |
Once, before first turn | Session opened. |
on_graph_end |
Once, when the session ends | Signature (context, final_state, messages, total_steps); total_steps is the number of turns. |
on_turn_start |
On the first content event of a turn | turn_index is 1-based. |
on_turn_end |
After turn_complete or interrupted |
Same turn_index as the matching start. |
on_turn_start and on_turn_end fire only in realtime sessions, never for invoke or stream. See Lifecycle callbacks for the full hook list.
API server integration
The API server exposes the WebSocket route /v1/graph/live for graphs that contain a LiveAgent. The frame protocol, auth and close codes are documented in Live WebSocket, and the connection settings in WebSockets.
Common errors
| Error | Cause | Fix |
|---|---|---|
ImportError: google-genai SDK is required for Gemini realtime |
Extra not installed. | pip install "10xgraph[realtime]". |
ValueError: Gemini realtime requires credentials |
No API key found. | Set GEMINI_API_KEY or GOOGLE_API_KEY, pass api_key=, or use use_vertex_ai=True with GOOGLE_CLOUD_PROJECT. |
ValueError: LiveAgent v1 supports only Gemini Live (google provider) |
Model resolved to a non-Google provider. | Use a Gemini model name, optionally prefixed with "gemini/". |
RuntimeError: This graph contains a LiveAgent; use .arealtime() / .realtime()... |
Called invoke/stream on realtime graph. |
Switch to arealtime() or realtime(). |
RuntimeError: arealtime() requires a graph rooted at a LiveAgent |
No LiveAgent in the graph. |
Build the graph with AudioAgent. |
RuntimeError: realtime() (sync) cannot be called from a running event loop |
Called realtime() in async context. |
Use await arealtime() instead. |
ValueError: response_modalities must contain exactly one modality |
Passed two modalities. | Pass ["AUDIO"] or ["TEXT"], not both. |
ErrorEvent(code="reconnect_failed", fatal=True) |
Reconnect attempts exhausted. | Check network; increase max_attempts or max_delay. |