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 │
└──────────────────────────────────────────────────────────────┘
RealtimeModel → RealtimeSession
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 hooksbefore_tool_call/after_tool_call.- A ponte ADK-ferramenta —
.adk_tool(Arc<dyn Tool>)permite que qualqueradk_core::Toolnormal (por exemplo, os recursos integrados deadk-tool) seja executado em uma sessão em tempo real por meio de umToolContextsintetizado, 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:
| Provedor | Entrada (microfone → modelo) | Saída (modelo → você) |
|---|---|---|
OpenAI gpt-realtime-2.1 | 24 kHz | 24 kHz |
| Gemini Live | 16 kHz | 24 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:
| Evento | Significado |
|---|---|
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 / SpeechStopped | O 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 aServerEvent.
Próximo: Provedores →