실시간 아키텍처

이 페이지에서는 ADK-Rust에서 실시간 세션이 실제로 어떻게 작동하는지 — 계층, 이벤트 루프, 오디오 파이프라인, 그리고 턴 라이프사이클에 대해 설명합니다. 이를 이해하면 이 섹션의 나머지 부분(tools, multimodal, memory)이 명확해집니다.

네 가지 계층

┌──────────────────────────────────────────────────────────────┐
│ 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)가 하나를 빌드합니다; BoxedModel는 단지 Arc<dyn RealtimeModel>입니다. connect()를 호출하면 WebSocket가 열리고 **RealtimeSession**이 반환됩니다 — 이는 실제로 공급자의 와이어 프로토콜을 사용하는 객체입니다. 세션을 직접 다루는 경우는 거의 없으며, runner가 이를 소유합니다. 그 표면은 작고 공급자 독립적입니다:

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), 위의 runner는 신경 쓰지 않습니다.

RealtimeRunner

세션을 소유하고 이벤트 루프를 실행합니다. 주요 작업은 tool dispatch입니다: ServerEvent::FunctionCallDone가 도착하면 핸들러를 찾아 실행하고 결과를 다시 보냅니다 (Tools 참조). 세션의 동사(verbs)와 tool 등록을 노출합니다:

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 — 연결 시 (컨텍스트를 위해) 쿼리되고 턴마다 기록됩니다 (구성 가능). Memory 참조.
  • EnhancedPluginManager — tool 호출은 before_tool_call / after_tool_call 훅을 통과합니다.
  • ADK-tool 브리지.adk_tool(Arc<dyn Tool>)는 모든 일반 adk_core::Tool (예: adk-tool의 내장 기능)가 세션 ID에 범위가 지정된 합성된 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 키는 브라우저에 도달하지 않습니다; tools는 서버에서 실행됩니다. 이 섹션의 모든 웹 예제는 이 패턴을 사용합니다 — 전체 프로토콜 및 Web Audio 코드는 Building web apps를 참조하십시오.

오디오 파이프라인

실시간 오디오는 raw PCM16, 모노, little-endian입니다 — 컨테이너가 없습니다. 유일하게 달라지는 것은 샘플 레이트이며, 이는 공급자별 및 방향별로 다릅니다:

제공자입력 (마이크 → 모델)출력 (모델 → 사용자)
OpenAI gpt-realtime24 kHz24 kHz
Gemini Live16 kHz24 kHz

요율이 다르기 때문에, 브릿지는 오디오가 흐르기 전에 브라우저와 요율을 협상합니다 (예제는 ready 메시지를 input_rate/output_rate와 함께 보내고, 브라우저는 해당 요율로 캡처/재생 AudioContext를 생성합니다). 오디오는 WebSocket를 통해 base64로 인코딩되어 전달됩니다. ServerEvent::AudioDelta는 브라우저가 끊김 없이 재생할 수 있도록 재인코딩하는 디코딩된 PCM16 바이트를 전달합니다.

턴 라이프사이클

"턴"은 한 번의 교환을 의미합니다. 서버 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()를 호출해야 합니다.

도구 턴은 두 번의 응답에 걸쳐 발생합니다

모델이 도구를 호출하면 턴이 더 길어집니다.

(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를 발행합니다 — 도구를 참조하십시오.

처리할 서버 이벤트

ServerEvent는 러너가 생성하는 공급자 독립적인 이벤트 열거형입니다. 일반적으로 렌더링하는 이벤트는 다음과 같습니다.

EventMeaning
AudioDelta { delta, .. }PCM16 bytes의 에이전트 음성 — 재생하세요
TranscriptDelta { delta, .. }에이전트의 음성 답변, 텍스트 형식으로
InputTranscriptDelta { delta, .. }사용자 음성의 실시간 전사 (스트리밍됨)
InputTranscriptCompleted { transcript, .. }최종 사용자 전사 (OpenAI가 하나를 보냅니다)
SpeechStarted / SpeechStoppedVAD가 사용자가 시작/중지하는 것을 감지했습니다
FunctionCallDone { name, arguments, call_id, .. }모델이 도구를 원합니다
ResponseDone { .. }응답이 완료됨
TextDelta { delta, .. }음성이 아닌 텍스트 (예: Gemini "생각 중") — 일반적으로 표시되지 않음
Error { error, .. }제공자 오류

#[non_exhaustive]: ServerEvent를 매칭할 때 항상 _ => {} arm을 포함해야 합니다.

다음: 공급자 →