リアルタイムアーキテクチャ

このページでは、ADK-Rust におけるリアルタイムセッションの実際の動作、つまりレイヤー、イベントループ、オーディオパイプライン、ターンのライフサイクルについて説明します。これを理解すると、このセクションの残りの内容(ツール、マルチモーダル、メモリ)も簡単に理解できます。

4つのレイヤー

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

RealtimeModel は薄いファクトリです。OpenAIRealtimeModel::new(api_key, model_id) または GeminiRealtimeModel::new(GeminiLiveBackend::studio(api_key), model_id) が1つを構築し、BoxedModel は単に Arc<dyn RealtimeModel> です。connect() を呼び出すと WebSocket が開かれ、プロバイダーのワイヤプロトコルを実際に扱う RealtimeSession が返されます。セッションを直接操作することはほとんどありません。セッションはランナーが所有します。そのインターフェースは小さく、プロバイダーに依存しません。

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
}

各プロバイダーは内部でこれを異なる方法で実装しています(OpenAI の input_audio_buffer.append と Gemini の realtimeInput など)が、上位のランナーはその違いを意識しません。

RealtimeRunner

セッションを所有し、イベントループを実行します。主な役割は ツールのディスパッチ です。ServerEvent::FunctionCallDone が到着すると、対応するハンドラーを検索して実行し、その結果を送り返します(ツール を参照)。セッションの動詞に加えて、ツールの登録機能を公開します。

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

アプリケーション層です。RealtimeRunner をラップし、イベントの流れに応じて次の機能を組み込みます。

  • SessionService — 完了したターンをセッション履歴に追加します。
  • MemoryService — 接続時に(コンテキスト用に)問い合わせ、ターンごとに(設定可能)書き込みます。メモリ を参照してください。
  • EnhancedPluginManager — ツール呼び出しは before_tool_call / after_tool_call フックを通過します。
  • ADK-ツールブリッジ.adk_tool(Arc<dyn Tool>) により、通常の adk_core::Tool(例:adk-tool の組み込み機能)を、セッションのアイデンティティにスコープされた合成 ToolContext を介してリアルタイムセッションで実行できます。

型付きビルダーを使って構築します。

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 は自動動作を制御します。

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,
}

サーバーサイドブリッジ(ウェブアプリ)

ブラウザでプロバイダー WebSocket を安全に保持することはできません — API キーが漏洩してしまい、音声やイベントの処理はサーバーサイドに置く必要があるためです。そのため、推奨されるトポロジーはサーバーサイドブリッジです。ブラウザは薄い音声・動画デバイスとして機能し、Rust サーバーがリアルタイムセッションを管理します。

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

API キーがブラウザに届くことはなく、ツールはサーバー上で実行されます。このセクションのすべてのウェブ例ではこのパターンを使用しています。完全なプロトコルと Web Audio のコードについては、ウェブアプリの構築を参照してください。

音声パイプライン

リアルタイム音声はraw PCM16、モノラル、リトルエンディアンで、コンテナはありません。変化するのはサンプルレートだけであり、プロバイダーごと、方向ごとに異なります。

プロバイダー入力(マイク → モデル)出力(モデル → あなた)
OpenAI gpt-realtime-2.124 kHz24 kHz
Gemini Live16 kHz24 kHz

レートが異なるため、ブリッジは音声が流れる前にブラウザとネゴシエーションを行います(例では ready メッセージを input_rate/output_rate とともに送信し、ブラウザはそれらのレートでキャプチャ/再生用の AudioContext を作成します)。 音声は WebSocket を base64 エンコードした状態で通過し、ServerEvent::AudioDelta には、ブラウザで途切れなく再生できるよう再エンコードする、デコード済みの PCM16 バイト列が格納されます。

ターンのライフサイクル

「ターン」とは、1 回のやり取りです。サーバー VAD では、プロバイダーが音声の境界を検出して自動的に応答するため、音声に対して create_response() を呼び出す必要はありません。一般的な音声ターンでは、次のイベントシーケンスが生成されます。

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

テキスト入力(チャットボックス)の場合、VAD のトリガーがないため、モデルに応答を求めるには send_text() の後に create_response() を呼び出す必要があります。

ツールターンは 2 つの応答にまたがる

モデルがツールを呼び出す場合、ターンはより長くなります。

(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

このため、UI では、ツール呼び出しを含まない ResponseDone を受信した場合にのみ、ターンが完了したものとして扱う必要があります。ADK-Rust は、複数のツールが同時に呼び出された場合でも、ターンごとに後続の response.create1 つだけ発行します — 詳細はツールを参照してください。

処理するサーバーイベント

ServerEvent は、ランナーが生成するプロバイダーに依存しないイベント列挙型です。通常レンダリングするイベントは次のとおりです。

イベント意味
AudioDelta { delta, .. }エージェントの音声の PCM16 バイト — 再生します
TranscriptDelta { delta, .. }エージェントが話した回答(テキスト)
InputTranscriptDelta { delta, .. }ユーザーの発話のライブ文字起こし(ストリーミング)
InputTranscriptCompleted { transcript, .. }最終的なユーザーの文字起こし(OpenAIが送信する)
SpeechStarted / SpeechStoppedVADがユーザーの発話の開始/停止を検出
FunctionCallDone { name, arguments, call_id, .. }モデルがツールを要求
ResponseDone { .. }レスポンスが完了しました
TextDelta { delta, .. }発話されないテキスト(例: Gemini の「思考」)—通常は表示されません
Error { error, .. }プロバイダーエラー

#[non_exhaustive]: ServerEvent にマッチさせるときは、常に _ => {} アームを含めてください。

次へ: プロバイダー →

リアルタイムアーキテクチャ - ADK-Rust ドキュメント | ADK-Rust