Architecture en temps réel
Cette page explique comment fonctionne réellement une session en temps réel dans ADK-Rust — les couches, la boucle d’événements, le pipeline audio et le cycle de vie des tours. Comprendre cela rend évidente la suite de cette section (outils, multimodalité, mémoire).
Les quatre couches
┌──────────────────────────────────────────────────────────────┐
│ 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 est une fabrique légère. OpenAIRealtimeModel::new(api_key, model_id)
ou GeminiRealtimeModel::new(GeminiLiveBackend::studio(api_key), model_id) en construisent un ; BoxedModel n’est que Arc<dyn RealtimeModel>. L’appel à connect() ouvre la
WebSocket et renvoie un RealtimeSession — l’objet qui parle réellement
le protocole filaire du fournisseur. Vous interagissez rarement directement avec la session ; le runner la gère. Son interface est réduite et indépendante du fournisseur :
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
}
Chaque fournisseur implémente cela différemment en interne (OpenAI's
input_audio_buffer.append contre realtimeInput de Gemini), mais le runner ci-dessus
n’a pas à s’en préoccuper.
RealtimeRunner
Il gère la session et exécute la boucle d’événements. Son rôle principal est la distribution des outils : lorsqu’un ServerEvent::FunctionCallDone arrive, il recherche votre gestionnaire, l’exécute et renvoie le résultat (voir Outils). Il expose les verbes de la session ainsi que l’enregistrement des outils :
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 couche applicative. Elle encapsule un RealtimeRunner et, à mesure que les événements circulent, relie :
SessionService— les tours terminés sont ajoutés à l’historique de la session.MemoryService— interrogée lors de la connexion (pour le contexte) et alimentée à chaque tour (configurable). Voir Mémoire.EnhancedPluginManager— les appels d’outils passent par les hooksbefore_tool_call/after_tool_call.- Le pont ADK-outil —
.adk_tool(Arc<dyn Tool>)permet à n’importe queladk_core::Toolnormal (par exemple les éléments intégrés deadk-tool) de s’exécuter dans une session en temps réel via unToolContextsynthétisé et associé à l’identité de la session.
Vous le construisez avec un builder typé :
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 contrôle les comportements automatiques :
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,
}
Le pont côté serveur (applications web)
Les navigateurs ne peuvent pas conserver le fournisseur WebSocket de manière sécurisée — votre clé API serait exposée, et la gestion de l’audio et des événements doit être effectuée côté serveur. La topologie recommandée est donc un pont côté serveur : le navigateur est un simple périphérique audio/vidéo, et votre serveur Rust gère la session en temps réel.
browser ──mic PCM16 + camera JPEG (base64 over your WS)──▶ your Axum /ws
browser ◀──agent PCM16 + transcripts + tool events────── IntegratedRealtimeRunner ──▶ provider
La clé API n’atteint jamais le navigateur ; les outils s’exécutent sur votre serveur. Chaque exemple web de cette section utilise ce modèle — consultez Créer des applications web pour connaître le protocole complet et le code Web Audio.
Le pipeline audio
L’audio en temps réel est au format PCM16 brut, mono, little-endian — sans conteneur. La seule variable est la fréquence d’échantillonnage, qui varie selon le fournisseur et la direction :
| Fournisseur | Entrée (micro → modèle) | Sortie (modèle → vous) |
|---|---|---|
OpenAI gpt-realtime-2.1 | 24 kHz | 24 kHz |
| Gemini Live | 16 kHz | 24 kHz |
Comme les fréquences diffèrent, un pont les négocie avec le navigateur avant que l’audio ne circule (les exemples envoient un message ready avec input_rate/output_rate, et le navigateur crée ses AudioContexts de capture/lecture à ces fréquences).
L’audio traverse votre WebSocket encodé en base64 ; ServerEvent::AudioDelta contient les octets PCM16 décodés que vous réencodez pour que le navigateur les lise sans interruption.
Cycle du tour
Un « tour » est un échange. Avec le VAD côté serveur, le fournisseur détecte les limites de la parole et répond automatiquement ; vous n’appelez pas create_response() pour l’audio. Un tour vocal typique produit cette séquence d’événements :
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
Pour une entrée textuelle (une boîte de discussion), il n’y a pas de déclencheur VAD ; vous devez donc appeler create_response() après send_text() pour demander au modèle de répondre.
Les tours avec outils couvrent deux réponses
Lorsque le modèle appelle un outil, le tour est plus long :
(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
C’est pourquoi une interface utilisateur ne devrait considérer un tour comme terminé que sur un ResponseDone qui ne contenait pas d’appel d’outil. ADK-Rust émet exactement un response.create de suivi par tour, même lorsque plusieurs outils sont appelés simultanément — voir
Outils.
Événements serveur que vous traiterez
ServerEvent est l’énumération d’événements indépendante du fournisseur produite par l’exécuteur. Ceux que vous affichez généralement :
| Événement | Signification |
|---|---|
AudioDelta { delta, .. } | Octets PCM16 de la parole de l’agent — les lire |
TranscriptDelta { delta, .. } | Réponse orale de l’agent, sous forme de texte |
InputTranscriptDelta { delta, .. } | Transcription en direct de la parole de l’utilisateur (diffusée en continu) |
InputTranscriptCompleted { transcript, .. } | Transcription finale de l’utilisateur (OpenAI en envoie une) |
SpeechStarted / SpeechStopped | VAD a détecté que l’utilisateur commençait/s’arrêtait de parler |
FunctionCallDone { name, arguments, call_id, .. } | Le modèle souhaite utiliser un outil |
ResponseDone { .. } | Une réponse terminée |
TextDelta { delta, .. } | Texte non vocal (p. ex. la « réflexion » de Gemini) — généralement non affiché |
Error { error, .. } | Erreur du fournisseur |
#[non_exhaustive]: incluez toujours une branche_ => {}lors de la correspondance avecServerEvent.
Suivant : Fournisseurs →