실시간 아키텍처

이 페이지에서는 ADK-Rust에서 실시간 세션이 실제로 작동하는 방식을 설명합니다. 계층, 이벤트 루프, 오디오 파이프라인, 턴 수명 주기를 다룹니다. 이를 이해하면 이 섹션의 나머지 내용(도구, 멀티모달, 메모리)도 쉽게 이해할 수 있습니다.

네 가지 계층

┌──────────────────────────────────────────────────────────────┐
│ 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**이 반환됩니다. 이는 실제로 공급자의 와이어 프로토콜과 통신하는 객체입니다. 세션을 직접 다루는 경우는 거의 없으며, 러너가 이를 소유합니다. 세션의 인터페이스는 작고 공급자에 종속되지 않습니다.

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의 기본 제공 기능)을 세션 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,
}

서버 측 브리지(웹 앱)

브라우저는 provider 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, mono, little-endian이며 컨테이너를 사용하지 않습니다. 유일하게 달라지는 것은 샘플링 레이트이며, provider별 및 방향별로 달라집니다:

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

요율이 서로 다르므로, 브리지는 오디오가 흐르기 전에 브라우저와 요율을 협상합니다 (예제에서는 input_rate/output_rate이 포함된 ready 메시지를 전송하고, 브라우저는 해당 요율로 캡처/재생 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는 러너가 생성하는 제공자에 종속되지 않는 이벤트 열거형입니다. 일반적으로 렌더링하는 이벤트는 다음과 같습니다.

이벤트의미
AudioDelta { delta, .. }에이전트 음성의 PCM16 바이트 — 재생
TranscriptDelta { delta, .. }텍스트 형식의 에이전트 음성 답변
InputTranscriptDelta { delta, .. }사용자의 음성이 실시간으로 스트리밍되는 대화 기록
InputTranscriptCompleted { transcript, .. }최종 사용자 대화 기록(OpenAI이 하나 전송함)
SpeechStarted / SpeechStoppedVAD가 사용자의 발화 시작/중지를 감지함
FunctionCallDone { name, arguments, call_id, .. }모델이 도구를 원함
ResponseDone { .. }응답 완료
TextDelta { delta, .. }음성이 아닌 텍스트(예: Gemini의 "thinking") — 일반적으로 표시되지 않음
Error { error, .. }제공업체 오류

#[non_exhaustive]: ServerEvent을 일치시킬 때는 항상 _ => {} 암을 포함하세요.

다음: 프로바이더 →