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     │
└──────────────────────────────────────────────────────────────┘

RealtimeModelRealtimeSession

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 hooks before_tool_call / after_tool_call.
  • El puente de herramientas de ADK: .adk_tool(Arc<dyn Tool>) permite que cualquier adk_core::Tool normal (por ejemplo, los elementos integrados de adk-tool) se ejecute en una sesión en tiempo real mediante un ToolContext sintetizado 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:

ProveedorEntrada (micrófono → modelo)Salida (modelo → tú)
OpenAI gpt-realtime-2.124 kHz24 kHz
Gemini Live16 kHz24 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:

EventoSignificado
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 / SpeechStoppedVAD 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 con ServerEvent.

Siguiente: Proveedores →

Arquitectura en tiempo real - Documentación ADK-Rust | ADK-Rust