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.

Python
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_RATE

Audio 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.

audio_session.py
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

Python
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()

Python
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.

Python
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:

  • RuntimeError if the graph contains no LiveAgent, or more than one.
  • RuntimeError if realtime() is called from a running event loop (use arealtime() 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).

input_queue.py
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

Python
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.

Python
@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=...).

realtime_config.py
from tenxgraph.core.realtime import RealtimeConfig, VADConfig

config = RealtimeConfig(
    model="gemini-2.5-flash",
    response_modalities=["AUDIO"],
    voice="Puck",
    vad=VADConfig(enabled=True),
)

Constructor

Python
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.

vad_config.py
from tenxgraph.core.realtime import VADConfig

vad = VADConfig(
    enabled=False,  # Manual VAD
    start_sensitivity="HIGH",
)

Constructor

Python
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.

reconnect_config.py
from tenxgraph.core.realtime import ReconnectConfig

reconnect = ReconnectConfig(
    base_delay=1.0,
    max_delay=30.0,
    max_attempts=10,
)

Constructor

Python
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 n waits min(base_delay * 2^(n-1), max_delay) seconds, up to max_attempts. After cap, fatal ErrorEvent(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.

handle_events.py
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:
            break

AudioDeltaEvent

Python
type: Literal["audio_delta"]
data: bytes        # PCM16 chunk
sample_rate: int   # 24000

Model audio output (PCM16). Write it to a speaker or file.

InputTranscriptEvent

Python
type: Literal["input_transcript"]
text: str
finished: bool     # True on final chunk

The 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

Python
type: Literal["output_transcript"]
text: str
finished: bool

Model’s speech transcript. Same streaming/finished semantics as InputTranscriptEvent.

ToolCallEvent

Python
type: Literal["tool_call"]
id: str
name: str
args: dict[str, Any]

Provider requests tool invocation. 10xGraph dispatches automatically via ToolNode.

ToolResultEvent

Python
type: Literal["tool_result"]
id: str
result: Any

Tool execution finished. Result already sent to model.

TurnCompleteEvent

Python
type: Literal["turn_complete"]

Model finished generating a turn (audio and transcription complete).

InterruptedEvent

Python
type: Literal["interrupted"]

Barge-in: the user spoke while the model was talking. Flush any queued playback.

SessionUpdateEvent

Python
type: Literal["session_update"]
resumption_handle: str | None

Provider issued a session-resumption handle. Stored in checkpointer thread metadata automatically.

GoAwayEvent

Python
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

Python
type: Literal["agent_changed"]
author: str

The active agent or persona changed. Reserved for a future multi-agent persona swap; the single-agent runtime does not emit it.

ErrorEvent

Python
type: Literal["error"]
code: str | None
message: str
fatal: bool

Normalized 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.

Python
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.

Python
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().

lifecycle_hooks.py
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.
Last updated for v0.10.0Edit this page on GitHubReport an issue