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

RealtimeModelRealtimeSession

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 hooks before_tool_call / after_tool_call.
  • Le pont ADK-outil.adk_tool(Arc<dyn Tool>) permet à n’importe quel adk_core::Tool normal (par exemple les éléments intégrés de adk-tool) de s’exécuter dans une session en temps réel via un ToolContext synthé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 :

FournisseurEntrée (micro → modèle)Sortie (modèle → vous)
OpenAI gpt-realtime-2.124 kHz24 kHz
Gemini Live16 kHz24 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énementSignification
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 / SpeechStoppedVAD 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 avec ServerEvent.

Suivant : Fournisseurs →