Arquitectura en tiempo real
Esta página explica cómo funciona realmente una sesión en tiempo real en ADK-Rust: las capas, el bucle de eventos, la canalización de audio y el ciclo de vida de los turnos. Comprender esto hace que el resto de la sección (herramientas, multimodalidad y memoria) resulte obvio.
Las cuatro capas
┌──────────────────────────────────────────────────────────────┐
│ IntegratedRealtimeRunner (feature: integration) │
│ • SessionService → persists each completed turn │
│ • MemoryService → profile-card injection + turn storage │
│ • EnhancedPluginManager → before/after-tool hooks │
│ • ADK-tool bridge → run any `adk_core::Tool` in a session │
└───────────────┬──────────────────────────────────────────────┘
│ wraps
┌───────────────▼──────────────────────────────────────────────┐
│ RealtimeRunner │
│ • pulls ServerEvents from the session │
│ • on FunctionCallDone → executes the tool handler │
│ • sends the tool result back, triggers the spoken answer │
└───────────────┬──────────────────────────────────────────────┘
│ drives
┌───────────────▼──────────────────────────────────────────────┐
│ RealtimeSession (the live transport — a WebSocket) │
│ send_audio / send_text / send_video_frame / send_tool_output │
│ next_event() → ServerEvent stream │
└───────────────┬──────────────────────────────────────────────┘
│ created by connect()
┌───────────────▼──────────────────────────────────────────────┐
│ RealtimeModel OpenAIRealtimeModel | GeminiRealtimeModel │
└──────────────────────────────────────────────────────────────┘
RealtimeModel → RealtimeSession
Un RealtimeModel es una fábrica sencilla. OpenAIRealtimeModel::new(api_key, model_id)
o GeminiRealtimeModel::new(GeminiLiveBackend::studio(api_key), model_id) construyen
uno; BoxedModel es simplemente Arc<dyn RealtimeModel>. Al llamar a connect() se abre la
WebSocket y se devuelve una RealtimeSession: el objeto que realmente habla
el protocolo de comunicación del proveedor. Rara vez se interactúa directamente con la sesión; el ejecutor es quien la administra. Su interfaz es pequeña e independiente del proveedor:
trait RealtimeSession {
async fn send_audio_base64(&self, audio: &str) -> Result<()>;
async fn send_text(&self, text: &str) -> Result<()>;
async fn send_video_frame(&self, mime: &str, data_b64: &str) -> Result<()>;
async fn send_tool_output(&self, response: ToolResponse) -> Result<()>;
async fn create_response(&self) -> Result<()>;
async fn next_event(&self) -> Option<Result<ServerEvent>>;
async fn close(&self) -> Result<()>;
// …commit/clear audio, interrupt, mutate_context
}
Cada proveedor implementa esto de forma diferente internamente (OpenAI's
input_audio_buffer.append frente a realtimeInput de Gemini), pero al ejecutor no le
importa.
RealtimeRunner
Administra la sesión y ejecuta el bucle de eventos. Su tarea principal es el despacho de herramientas: cuando llega un ServerEvent::FunctionCallDone, busca tu controlador, lo ejecuta y envía el resultado de vuelta (consulta Herramientas). Expone los verbos de la sesión, además del registro de herramientas:
runner.connect().await?;
runner.send_audio(pcm16_base64).await?; // mic frames
runner.send_text("…").await?; // typed input
runner.send_video_frame("image/jpeg", b64).await?;
runner.create_response().await?; // trigger a response to text input
let ev = runner.next_event().await; // pull the next ServerEvent
runner.close().await?;
IntegratedRealtimeRunner
La capa de aplicación. Envuelve un RealtimeRunner y, a medida que fluyen los eventos, integra:
SessionService: los turnos completados se añaden al historial de la sesión.MemoryService: se consulta al conectarse (para obtener contexto) y se escribe en cada turno (configurable). Consulta Memoria.EnhancedPluginManager: las llamadas a herramientas pasan por los hooksbefore_tool_call/after_tool_call.- El puente de herramientas de ADK:
.adk_tool(Arc<dyn Tool>)permite que cualquieradk_core::Toolnormal (por ejemplo, los elementos integrados deadk-tool) se ejecute en una sesión en tiempo real mediante unToolContextsintetizado con el alcance de la identidad de la sesión.
Se construye mediante un constructor tipado:
let runner = IntegratedRealtimeRunner::builder()
.model(model)
.config(config)
.identity("app", "user", "session-id") // required
.session_service(sessions) // optional
.memory_service(memory) // optional
.integration_config(IntegrationConfig::default())
.tool(weather_def(), weather_handler()) // native realtime ToolHandler
.adk_tool(Arc::new(remember_tool)) // bridged adk_core::Tool
.build()?;
IntegrationConfig controla los comportamientos automáticos:
IntegrationConfig {
persist_transcripts: true, // append turns to the session
store_to_memory: true, // memory_service.add_session per turn
inject_memory_context: true, // query memory at connect
max_memory_injection: 10,
}
El puente del lado del servidor (aplicaciones web)
Los navegadores no pueden almacenar de forma segura el proveedor WebSocket — tu clave API se filtraría, y la gestión del audio y los eventos corresponde al lado del servidor. Por ello, la topología recomendada es un puente del lado del servidor: el navegador es un dispositivo delgado de audio/vídeo, y tu servidor Rust gestiona la sesión en tiempo real.
browser ──mic PCM16 + camera JPEG (base64 over your WS)──▶ your Axum /ws
browser ◀──agent PCM16 + transcripts + tool events────── IntegratedRealtimeRunner ──▶ provider
La clave API nunca llega al navegador; las herramientas se ejecutan en tu servidor. Todos los ejemplos web de esta sección utilizan este patrón; consulta Creación de aplicaciones web para ver el protocolo completo y el código de Web Audio.
La canalización de audio
El audio en tiempo real es PCM16 sin procesar, mono, little-endian — sin contenedores. Lo único que varía es la frecuencia de muestreo, y varía según el proveedor y la dirección:
| Proveedor | Entrada (micrófono → modelo) | Salida (modelo → tú) |
|---|---|---|
OpenAI gpt-realtime-2.1 | 24 kHz | 24 kHz |
| Gemini Live | 16 kHz | 24 kHz |
Como las tasas difieren, un puente las negocia con el navegador antes de que fluya cualquier audio (los ejemplos envían un mensaje ready con input_rate/output_rate, y el navegador crea sus AudioContext de captura/reproducción con esas tasas).
El audio atraviesa tu WebSocket codificado en base64; ServerEvent::AudioDelta contiene bytes PCM16 decodificados que vuelves a codificar para que el navegador los reproduzca sin interrupciones.
Ciclo del turno
Un «turno» es un intercambio. Con VAD del servidor, el proveedor detecta los límites del habla y responde automáticamente; no llamas a create_response() para el audio. Un turno de voz típico produce esta secuencia de eventos:
SpeechStarted → user began talking (flush any playing audio = barge-in)
InputTranscriptDelta… → live transcript of what the user is saying
SpeechStopped → user finished
(model thinks)
TranscriptDelta… → the agent's spoken answer, as text
AudioDelta… → the agent's spoken answer, as PCM16
ResponseDone → turn complete
Para la entrada de texto (un cuadro de chat), no hay ningún activador de VAD, por lo que debes llamar a create_response() después de send_text() para pedir al modelo que responda.
Los turnos de herramientas abarcan dos respuestas
Cuando el modelo llama a una herramienta, el turno es más largo:
(maybe a short spoken preamble) + FunctionCallDone(name, args)
ResponseDone ← the "dispatch" response ends here
→ runner executes your handler, sends the result back,
and triggers ONE follow-up response
TranscriptDelta… / AudioDelta… ← the spoken answer using the tool result
ResponseDone ← turn truly complete
Por eso, una interfaz de usuario debe considerar que un turno ha terminado únicamente con un ResponseDone que no contenga una llamada a una herramienta. ADK-Rust emite exactamente un response.create de seguimiento por turno, incluso cuando se llaman varias herramientas a la vez; consulta Herramientas.
Eventos del servidor que gestionarás
ServerEvent es la enumeración de eventos independiente del proveedor que produce el ejecutor. Los que normalmente renderizarás:
| Evento | Significado |
|---|---|
AudioDelta { delta, .. } | Bytes PCM16 del habla del agente: reproducirlos |
TranscriptDelta { delta, .. } | La respuesta hablada del agente, como texto |
InputTranscriptDelta { delta, .. } | Transcripción en directo del discurso del usuario (transmitida) |
InputTranscriptCompleted { transcript, .. } | Transcripción final del usuario (OpenAI envía una) |
SpeechStarted / SpeechStopped | VAD detectó que el usuario comenzaba o dejaba de hablar |
FunctionCallDone { name, arguments, call_id, .. } | El modelo quiere una herramienta |
ResponseDone { .. } | Una respuesta ha terminado |
TextDelta { delta, .. } | Texto no hablado (p. ej., el «razonamiento» de Gemini) — normalmente no se muestra |
Error { error, .. } | Error del proveedor |
#[non_exhaustive]: incluye siempre un brazo_ => {}al coincidir conServerEvent.
Siguiente: Proveedores →