Arquitetura em tempo real

Esta página explica como uma sessão em tempo real realmente funciona em ADK-Rust — as camadas, o loop de eventos, o pipeline de áudio e o ciclo de vida dos turnos. Entender isso torna óbvio o restante da seção (ferramentas, multimodalidade, memória).

As quatro camadas

┌──────────────────────────────────────────────────────────────┐
│ 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

Um RealtimeModel é uma fábrica simples. OpenAIRealtimeModel::new(api_key, model_id) ou GeminiRealtimeModel::new(GeminiLiveBackend::studio(api_key), model_id) criam um; BoxedModel é apenas Arc<dyn RealtimeModel>. Chamar connect() abre o WebSocket e retorna uma RealtimeSession — o objeto que realmente fala o protocolo de comunicação do provedor. Raramente você interage diretamente com a sessão; o executor é o responsável por ela. Sua interface é pequena e independente do provedor:

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 provedor implementa isso de forma diferente internamente (o input_audio_buffer.append de OpenAI vs o realtimeInput do Gemini), mas o executor acima não precisa saber disso.

RealtimeRunner

É responsável pela sessão e executa o loop de eventos. Sua principal tarefa é o despacho de ferramentas: quando um ServerEvent::FunctionCallDone chega, ele procura seu manipulador, executa-o e envia o resultado de volta (consulte Ferramentas). Ele expõe os verbos da sessão e o registro de ferramentas:

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

A camada da aplicação. Ela encapsula um RealtimeRunner e, conforme os eventos fluem, conecta:

  • SessionService — turnos concluídos são adicionados ao histórico da sessão.
  • MemoryService — consultada na conexão (para obter contexto) e gravada a cada turno (configurável). Consulte Memória.
  • EnhancedPluginManager — as chamadas de ferramentas passam pelos hooks before_tool_call / after_tool_call.
  • A ponte ADK-ferramenta.adk_tool(Arc<dyn Tool>) permite que qualquer adk_core::Tool normal (por exemplo, os recursos integrados de adk-tool) seja executado em uma sessão em tempo real por meio de um ToolContext sintetizado, com escopo definido pela identidade da sessão.

Você a cria com um construtor 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 os comportamentos 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,
}

A ponte no lado do servidor (aplicações web)

Os navegadores não podem armazenar o provedor WebSocket com segurança — sua chave API vazaria, e o processamento de áudio/eventos pertence ao lado do servidor. Portanto, a topologia recomendada é uma ponte no lado do servidor: o navegador é um dispositivo fino de áudio/vídeo, e seu servidor Rust gerencia a sessão em tempo real.

  browser ──mic PCM16 + camera JPEG (base64 over your WS)──▶  your Axum /ws
  browser ◀──agent PCM16 + transcripts + tool events──────    IntegratedRealtimeRunner ──▶ provider

A chave API nunca chega ao navegador; as ferramentas são executadas no seu servidor. Todos os exemplos web nesta seção usam esse padrão — consulte Construindo aplicações web para ver o protocolo completo e o código Web Audio.

O pipeline de áudio

O áudio em tempo real é PCM16 bruto, mono, little-endian — sem contêineres. A única coisa que varia é a taxa de amostragem, e ela varia por provedor e por direção:

ProvedorEntrada (microfone → modelo)Saída (modelo → você)
OpenAI gpt-realtime-2.124 kHz24 kHz
Gemini Live16 kHz24 kHz

Como as taxas diferem, uma ponte as negocia com o navegador antes que qualquer áudio flua (os exemplos enviam uma mensagem ready com input_rate/output_rate, e o navegador cria seus AudioContexts de captura/reprodução nessas taxas). O áudio atravessa seu WebSocket codificado em base64; ServerEvent::AudioDelta transporta bytes PCM16 decodificados que você recodifica para que o navegador os reproduza sem interrupções.

O ciclo de turnos

Um "turno" é uma troca. Com VAD no servidor, o provedor detecta os limites da fala e responde automaticamente; você não chama create_response() para o áudio. Um turno de voz típico produz esta sequência 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 entrada de texto (uma caixa de conversa), não há acionador de VAD, então você precisa chamar create_response() depois de send_text() para solicitar que o modelo responda.

Turnos com ferramentas abrangem duas respostas

Quando o modelo chama uma ferramenta, o turno é mais longo:

(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 isso que uma interface deve considerar um turno concluído somente em um ResponseDone que não contenha uma chamada de ferramenta. ADK-Rust emite exatamente um response.create de acompanhamento por turno, mesmo quando várias ferramentas são chamadas de uma só vez — consulte Ferramentas.

Eventos do servidor que você tratará

ServerEvent é o enum de eventos independente do provedor que o executor produz. Os que você normalmente renderiza:

EventoSignificado
AudioDelta { delta, .. }Bytes PCM16 da fala do agente — reproduza-os
TranscriptDelta { delta, .. }A resposta falada do agente, como texto
InputTranscriptDelta { delta, .. }Transcrição ao vivo da fala do usuário (transmitida em fluxo)
InputTranscriptCompleted { transcript, .. }Transcrição final do usuário (OpenAI envia uma)
SpeechStarted / SpeechStoppedO VAD detectou o início/fim da fala do usuário
FunctionCallDone { name, arguments, call_id, .. }O modelo deseja uma ferramenta
ResponseDone { .. }Uma resposta foi concluída
TextDelta { delta, .. }Texto não falado (por exemplo, o “pensamento” do Gemini) — geralmente não é exibido
Error { error, .. }Erro do provedor

#[non_exhaustive]: sempre inclua um braço _ => {} ao corresponder a ServerEvent.

Próximo: Provedores →