러너
adk-runner의 실행 런타임으로, 에이전트 실행을 조정합니다.
개요
Runner은 에이전트 실행의 전체 수명 주기를 관리합니다.
- 세션 관리 (세션 생성/검색)
- 메모리 주입 (관련 메모리 검색 및 주입)
- 아티팩트 처리 (범위가 지정된 아티팩트 액세스)
- 이벤트 스트리밍 (이벤트 처리 및 전달)
- 에이전트 전환 (다중 에이전트 핸드오프 처리)
설치
[dependencies]
adk-runner = "2.1.0"
RunnerConfig
필수 서비스를 사용하도록 러너를 구성합니다.
use adk_runner::{Runner, RunnerConfig};
use adk_session::InMemorySessionService;
use adk_artifact::InMemoryArtifactService;
use std::sync::Arc;
let config = RunnerConfig {
app_name: "my_app".to_string(),
agent: Arc::new(my_agent),
session_service: Arc::new(InMemorySessionService::new()),
artifact_service: Some(Arc::new(InMemoryArtifactService::new())),
memory_service: None,
plugin_manager: None,
run_config: None,
compaction_config: None,
context_cache_config: None,
cache_capable: None,
request_context: None,
cancellation_token: None,
};
let runner = Runner::new(config)?;
RunnerConfigBuilder (권장)
typestate 빌더를 사용하여 Runner를 구성합니다. 빌더는 컴파일 시 필수 필드를 확인하고 모든 선택적 필드에 기본값을 설정하므로, 향후 릴리스에서 새 필드가 추가되어도 코드가 중단되지 않습니다.
use adk_runner::Runner;
let runner = Runner::builder()
.app_name("my_app")
.agent(Arc::new(my_agent))
.session_service(Arc::new(InMemorySessionService::new()))
// Optional fields — only set what you need
.artifact_service(Arc::new(InMemoryArtifactService::new()))
.build()?;
빌더에는 세 가지 필드가 필요합니다: app_name, agent, session_service. 나머지는 모두 선택 사항이며 적절한 기본값이 제공됩니다. build() 메서드는 세 가지 필수 필드가 모두 설정된 후에만 사용할 수 있습니다. 하나라도 누락되면 런타임 오류가 아니라 컴파일 시간 오류가 발생합니다.
구성 필드
| 필드 | 유형 | 필수 | 설명 |
|---|---|---|---|
app_name | String | 예 | 애플리케이션 식별자 |
agent | Arc<dyn Agent> | 예 | 실행할 루트 에이전트 |
session_service | Arc<dyn SessionService> | 예 | 세션 저장소 백엔드 |
artifact_service | Option<Arc<dyn ArtifactService>> | 아니요 | 아티팩트 저장소 |
memory_service | Option<Arc<dyn Memory>> | 아니요 | 장기 메모리 |
plugin_manager | Option<Arc<PluginManager>> | 아니요 | 플러그인 수명 주기 후크 |
compaction_config | Option<EventsCompactionConfig> | 아니요 | 컨텍스트 압축 설정 |
run_config | Option<RunConfig> | 아니요 | 실행 옵션 |
context_cache_config | Option<ContextCacheConfig> | 아니요 | 실행기 수준 컨텍스트 캐시 수명 주기(실험적 — 아래 참조) |
cache_capable | Option<Arc<dyn CacheCapable>> | 아니요 | 캐시를 지원하는 모델 참조(실험적 — 아래 참조) |
request_context | Option<RequestContext> | 아니요 | 인증 미들웨어 컨텍스트 |
cancellation_token | Option<CancellationToken> | 아니요 | 협력적 취소 |
프롬프트 캐싱
캐싱은 제공업체 수준의 문제이므로 Runner 구성이 필요하지 않습니다. 각 제공업체 통합은 요청이 구성되는 지점에서 이를 처리합니다:
| 제공자 | 메커니즘 | 기본값 |
|---|---|---|
| Anthropic / Bedrock | cache_control 중단점 | 켜짐 (AnthropicConfig::prompt_caching, with_prompt_caching(false)로 선택 해제) |
| OpenAI | 서버 측 프롬프트 캐싱, 보존을 위한 PromptCacheRetention | 자동 |
| Gemini | 2.5/3.x에서 암시적 캐싱 — 공유 접두사는 코드 변경 없이 할인 혜택을 받음 | 자동 |
추가 연결 없이도 캐시 적중을 확인할 수 있습니다. Gemini 통합은 각 응답에 cachedContentTokenCount을 기록합니다.
context_cache_config및cache_capable는 실험적 기능이므로 설정하지 않은 상태로 두어야 합니다. 이는 Runner에서 Gemini의 명시적cachedContentsAPI를 구동합니다. 해당 API에서는 캐시가system_instruction,tools,tool_config을 대체해야 하므로 이들 중 하나와 함께 캐시를 보내면INVALID_ARGUMENT과 함께 거부됩니다. Runner는 agent가 도구를 확인하기 전에 캐시를 선택하므로 해당 요청을 구성할 수 없으며, 이러한 필드를 활성화해도 현재는 캐시 적중이 발생하지 않습니다. 최선의 노력 방식이 아닌 보장된 Gemini 캐싱은 다른 provider가 이를 처리하는 방식과 마찬가지로 모델 통합에 속합니다.
에이전트 실행
사용자 입력으로 agent를 실행합니다.
use adk_core::{Content, SessionId, UserId};
use futures::StreamExt;
let user_content = Content::new("user").with_text("Hello!");
let mut stream = runner.run(
UserId::new("user-123")?,
SessionId::new("session-456")?,
user_content,
).await?;
while let Some(event) = stream.next().await {
match event {
Ok(e) => {
if let Some(content) = e.content() {
for part in &content.parts {
if let Some(text) = part.text() {
print!("{}", text);
}
}
}
}
Err(e) => eprintln!("Error: {}", e),
}
}
문자열 편의 메서드
간단한 호출 지점에서는 run_str()가 일반 &str 인수를 받아 newtype 변환을 내부적으로 처리합니다.
let mut stream = runner.run_str(
"user-123",
"session-456",
Content::new("user").with_text("Hello!"),
).await?;
문자열이 유효성 검사를 통과하지 못하면(비어 있거나 null 바이트를 포함하거나 길이 제한을 초과하는 경우), run_str()는 agent 루프를 시작하기 전에 오류를 반환합니다. 타입이 지정된 UserId/SessionId을 사용하는 기존 run() 메서드는 변경되지 않습니다.
중단 및 실행 격리
실행은 run()가 스트림을 반환하는 즉시 등록되며, 스트림이 삭제될 때 등록이 해제됩니다. 스트림을 한 번도 폴링하지 않은 채 삭제되는 경우도 포함됩니다.
| 메서드 | 범위 |
|---|---|
interrupt(session_id) | 해당 세션 ID에 대한 모든 진행 중인 실행을 앱과 사용자 전반에서 취소합니다 |
interrupt_identity(app_name, user_id, session_id) | 하나의 정확한 ID에 대한 실행을 취소합니다 |
active_runs() | 진행 중인 모든 실행의 식별자입니다. 식별자가 반복되면 동시 실행을 의미합니다. |
active_session_ids() | 진행 중인 실행의 중복이 제거된 세션 ID |
// Cancel one tenant's run without touching another that shares the session ID
let cancelled = runner.interrupt_identity("my-app", "user-1", "session-1");
세션 ID는 앱과 사용자 내에서만 고유하므로, interrupt(session_id)는
포괄적인 형식이고 interrupt_identity는 정확한 형식입니다. 하나의 Runner가 둘 이상의 앱 또는 사용자를
서비스하는 경우에는 interrupt_identity을 우선 사용하세요.
실행은 세션 ID가 아니라 고유한 실행 ID로 추적되므로, 동일한 ID에 대한 두 실행은 별도로 추적되며 각각 자신만 등록 해제합니다.
지속성은 ID에 바인딩됨
Runner가 지속하는 모든 이벤트(사용자 발화, 모델 응답, 전송 이벤트,
플러그인 이벤트 및 압축 이벤트)는 전체
(app_name, user_id, session_id) 트리플과 함께 SessionService::append_event_for_identity을 통해
기록됩니다. 따라서 자연 키가 복합 키인 SessionService는
각 이벤트를 해당 테넌트에 바인딩할 수 있으며, 원시 세션 ID append_event 경로를
완전히 거부하거나 무시할 수 있습니다.
실행 흐름
┌─────────────────────────────────────────────────────────────┐
│ Runner.run() │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 1. Session Retrieval │
│ │
│ SessionService.get(app_name, user_id, session_id) │
│ → Creates new session if not exists │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. Agent Selection │
│ │
│ Check session state for active agent │
│ → Use root agent or transferred agent │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. Context Creation │
│ │
│ InvocationContext with: │
│ - Session (mutable) │
│ - Artifacts (scoped to session) │
│ - Memory (if configured) │
│ - Run config │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 4. Agent Execution │
│ │
│ agent.run(ctx) → EventStream │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 5. Event Processing │
│ │
│ For each event: │
│ - Update session state │
│ - Handle transfers │
│ - Forward to caller │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 6. Session Save │
│ │
│ SessionService.append_event(session, events) │
└─────────────────────────────────────────────────────────────┘
InvocationContext
실행 중 에이전트에 제공되는 컨텍스트:
pub trait InvocationContext: CallbackContext {
/// The agent being executed
fn agent(&self) -> Arc<dyn Agent>;
/// Memory service (if configured)
fn memory(&self) -> Option<Arc<dyn Memory>>;
/// Current session
fn session(&self) -> &dyn Session;
/// Execution configuration
fn run_config(&self) -> &RunConfig;
/// Signal end of invocation
fn end_invocation(&self);
/// Check if invocation has ended
fn ended(&self) -> bool;
}
RunConfig
실행 옵션:
pub struct RunConfig {
/// Streaming mode for responses
pub streaming_mode: StreamingMode,
// ... other fields (tool_confirmation_decisions, cached_content, etc.)
}
ToolExecutionStrategy
단일 LLM 응답에서 발생한 여러 도구 호출을 디스패치하는 방식을 제어합니다:
| 전략 | 동작 |
|---|---|
Sequential (기본값) | LLM이 반환한 순서대로 도구를 한 번에 하나씩 실행 |
Parallel | 모든 도구를 동시에 실행하며, 안전성은 호출자가 책임짐 |
Auto | 안전한 읽기 전용 하위 집합을 동시에 실행한 다음, 나머지 모든 호출을 순차적으로 실행 |
에이전트별로 LlmAgentBuilder을 통해 설정합니다:
use adk_core::ToolExecutionStrategy;
let agent = LlmAgentBuilder::new("fast_agent")
.model(model)
.tool_execution_strategy(ToolExecutionStrategy::Auto)
.tool(Arc::new(
search_tool
.with_read_only(true)
.with_concurrency_safe(true),
))
.tool(Arc::new(save_tool)) // runs after the concurrent safe subset
.build()?;
Auto 모드에서는 디스패치 루프가 is_read_only()와 is_concurrency_safe()을 모두 조회합니다. 선택된 도구가 두 메서드 모두에 대해 true을 반환하는 호출이 먼저 동시에 실행되고, 나머지 호출은 순차적으로 실행됩니다. 명시적인 호출자 재정의인 Parallel는 이러한 메타데이터 확인을 우회합니다. 전략과 관계없이 결과는 항상 원래 LLM에서 반환된 순서대로 다시 조합됩니다. 실패한 도구는 배치를 중단하지 않고 JSON 오류 응답을 생성합니다.
pub enum StreamingMode {
/// No streaming, return complete response
None,
/// Server-Sent Events (default)
SSE,
/// Bidirectional streaming (realtime)
Bidi,
}
에이전트 전환
Runner는 다중 에이전트 전환을 자동으로 처리합니다.
// In an agent's tool or callback
if should_transfer {
// Set transfer in event actions
ctx.set_actions(EventActions {
transfer_to_agent: Some("specialist_agent".to_string()),
..Default::default()
});
}
Runner는 다음을 수행합니다:
- 이벤트에서 전환 요청 감지
- sub_agents에서 대상 에이전트 찾기
- 새 활성 에이전트로 세션 상태 업데이트
- 새 에이전트로 실행 계속
컨텍스트 압축
장시간 실행되는 세션에서는 자동 컨텍스트 압축을 활성화하여 LLM 컨텍스트 창의 크기를 제한된 범위로 유지합니다.
use adk_runner::{Runner, RunnerConfig, EventsCompactionConfig};
use adk_agent::LlmEventSummarizer;
use std::sync::Arc;
let summarizer = LlmEventSummarizer::new(model.clone());
let config = RunnerConfig {
// ... other fields ...
compaction_config: Some(EventsCompactionConfig {
compaction_interval: 3, // Compact every 3 invocations
overlap_size: 1, // Keep 1 event overlap for continuity
summarizer: Arc::new(summarizer),
}),
// ...
};
압축이 트리거되면 이전 이벤트가 요약 이벤트로 대체됩니다. conversation_history()는 원래 이벤트 대신 요약을 자동으로 사용합니다.
자세한 문서는 컨텍스트 압축을 참조하세요.
Launcher와의 통합
Launcher는 내부적으로 Runner를 사용합니다.
// Launcher creates Runner with default services
Launcher::new(agent)
.app_name("my_app")
.run()
.await?;
// Equivalent to using the builder:
let runner = Runner::builder()
.app_name("my_app")
.agent(agent)
.session_service(Arc::new(InMemorySessionService::new()))
.build()?;
사용자 지정 Runner 사용
고급 시나리오에서는 Runner를 직접 사용합니다.
use adk_runner::Runner;
// Production configuration using the builder
let runner = Runner::builder()
.app_name("production_app")
.agent(my_agent)
.session_service(Arc::new(SqliteSessionService::new(db_pool)))
.artifact_service(Arc::new(S3ArtifactService::new(s3_client)))
.memory_service(Arc::new(QdrantMemoryService::new(qdrant_client)))
.build()?;
// Use in HTTP handler with run_str() for convenience
async fn chat_handler(runner: &Runner, request: ChatRequest) -> Response {
let stream = runner.run_str(
&request.user_id,
&request.session_id,
request.content,
).await?;
// Stream events to client
Response::sse(stream)
}
이전: ← 핵심 타입 | 다음: Launcher →