リアルタイムアーキテクチャ
このページでは、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 │
└──────────────────────────────────────────────────────────────┘
RealtimeModel → RealtimeSession
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.1 | 24 kHz | 24 kHz |
| Gemini Live | 16 kHz | 24 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.create を1 つだけ発行します — 詳細はツールを参照してください。
処理するサーバーイベント
ServerEvent は、ランナーが生成するプロバイダーに依存しないイベント列挙型です。通常レンダリングするイベントは次のとおりです。
| イベント | 意味 |
|---|---|
AudioDelta { delta, .. } | エージェントの音声の PCM16 バイト — 再生します |
TranscriptDelta { delta, .. } | エージェントが話した回答(テキスト) |
InputTranscriptDelta { delta, .. } | ユーザーの発話のライブ文字起こし(ストリーミング) |
InputTranscriptCompleted { transcript, .. } | 最終的なユーザーの文字起こし(OpenAIが送信する) |
SpeechStarted / SpeechStopped | VADがユーザーの発話の開始/停止を検出 |
FunctionCallDone { name, arguments, call_id, .. } | モデルがツールを要求 |
ResponseDone { .. } | レスポンスが完了しました |
TextDelta { delta, .. } | 発話されないテキスト(例: Gemini の「思考」)—通常は表示されません |
Error { error, .. } | プロバイダーエラー |
#[non_exhaustive]:ServerEventにマッチさせるときは、常に_ => {}アームを含めてください。
次へ: プロバイダー →