관리형 에이전트 런타임
안정성: 실험적 — 이 기능은 추가 기능이며
managed-runtime뒤에서 기능 플래그로 보호됩니다. 기능이 비활성화된 경우 기존Runner/LlmAgentAPIs에 영향을 주지 않습니다. API 표면은 향후 릴리스에서 변경될 수 있습니다.
개요
관리형 에이전트 런타임(adk-managed)은 제공자에 중립적이며, 내구성과 재개 기능을
갖춘 에이전트 실행 엔진입니다. 선언적 ManagedAgentDef을 받아 실행 가능한 에이전트를
빌드하고, 체크포인트에서 재개할 수 있으며 이벤트를 스트리밍하는 백그라운드
세션으로 실행합니다.
런타임은 서비스가 아니라 라이브러리입니다. 플랫폼이 이를 호스팅합니다. 이는 다음을 의미합니다.
- 독립적으로 테스트 가능: HTTP/인증/청구 종속성이 전혀 없음
- 임베드 가능: 자체 호스팅 배포에서는 동일한 런타임 트레이트를 직접 사용
- 교체 가능한 플랫폼: 서로 다른 플랫폼에서 동일한 런타임을 호스팅 가능
- 제공자 중립적: 모델 제공자와 관계없이 동일한 이벤트 시퀀스
빠른 시작
Cargo.toml에 기능을 추가합니다.
[dependencies]
adk-rust = { version = "2.1.0", features = ["managed-runtime"] }
또는 adk-managed 크레이트를 직접 사용합니다.
[dependencies]
adk-managed = "2.1.0"
adk-session = "2.1.0"
최소 예제(ScriptedLlm — API 키 없음)
use std::sync::Arc;
use adk_managed::{
DefaultManagedAgentRuntime, ManagedAgentRuntime, ModelResolver,
ScriptedLlm, ScriptedTurn,
resolver::ResolverResult,
types::{ContentBlock, ManagedAgentDef, ModelRef, UserEvent},
};
use adk_session::InMemorySessionService;
use async_trait::async_trait;
use futures::StreamExt;
// A resolver that returns our scripted LLM
struct MockResolver { llm: Arc<dyn adk_core::Llm> }
#[async_trait]
impl ModelResolver for MockResolver {
async fn resolve(&self, _: &ModelRef) -> ResolverResult<Arc<dyn adk_core::Llm>> {
Ok(self.llm.clone())
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Create a scripted LLM (deterministic, offline, $0)
let llm = Arc::new(ScriptedLlm::new("test-model", vec![
ScriptedTurn { text: Some("Hello!".into()), tool_calls: vec![] },
]));
// 2. Build the runtime
let runtime = DefaultManagedAgentRuntime::new(
Arc::new(MockResolver { llm }),
Arc::new(InMemorySessionService::new()),
);
// 3. Create an agent
let def = ManagedAgentDef::new("my-agent", ModelRef::Shorthand("test-model".into()))
.with_system("You are helpful.");
let agent = runtime.create(def).await?;
// 4. Start a session
let session = runtime.start_session(&agent, None).await?;
// 5. Subscribe to events and send a message
let mut stream = runtime.stream_events(&session, None).await?;
runtime.send_event(&session, UserEvent::Message {
content: vec![ContentBlock::Text { text: "Hi!".into() }],
}).await?;
// 6. Collect events
while let Some(event) = stream.next().await {
println!("{event:?}");
}
Ok(())
}
아키텍처
┌─────────────────────────────────────────────────────────────┐
│ Platform Layer (ep-* crates) │
│ HTTP Routes │ Auth │ Billing │ Multi-tenancy │
└──────────────────────────┬──────────────────────────────────┘
│ Rust trait calls (in-process)
▼
┌─────────────────────────────────────────────────────────────┐
│ Runtime Layer (adk-managed) │
│ │
│ ManagedAgentRuntime trait + DefaultManagedAgentRuntime │
│ ─────────────────────────────────────────────────── │
│ • Builds runnable agents from ManagedAgentDef │
│ • Runs supervised session loop (durable, resumable) │
│ • Emits provider-neutral SessionEvent stream │
│ • Manages custom tool parking, checkpoints, interrupts │
│ • Resolves ModelRef → Arc<dyn Llm> │
│ │
│ Composes existing crates: │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
│ │adk-runner│ │adk-session│ │adk-model │ │adk-tool │ │
│ └──────────┘ └──────────┘ └──────────┘ └────────────┘ │
└─────────────────────────────────────────────────────────────┘
핵심 타입
ManagedAgentRuntime 트레이트
전체 에이전트 수명 주기를 정의하는 중심 비동기 트레이트입니다:
| 메서드 | 설명 |
|---|---|
create(def) | 에이전트 정의를 등록하고 AgentHandle를 반환합니다 |
start_session(agent, env?) | 새 세션을 시작하며 초기 상태는 Queued입니다 |
send_event(session, event) | 세션에 UserEvent 보내기 |
stream_events(session, from_seq?) | SessionEvent 스트림 구독 |
interrupt(session) | 다음 경계에서 중지하고 status.idle 내보내기 |
pause(session) | 체크포인트를 생성하고 처리를 일시 중지 |
resume(session) | 일시 중지 또는 재시작에서 재개 |
status(session) | 현재 SessionStatus 쿼리 |
archive(session) | 터미널 상태, 데이터 유지 |
delete_session(session) | 세션 데이터 제거 |
ManagedAgentDef
builder API를 사용한 선언적 에이전트 정의:
let def = ManagedAgentDef::new("my-agent", ModelRef::Shorthand("gemini-3.7-flash".into()))
.with_system("You are a helpful assistant.")
.with_description("Research agent with web search")
.with_tools(vec![ToolConfig::BuiltIn(ManagedBuiltinTool::WebSearch)]);
SessionEvent
단조 증가하는 시퀀스 번호를 사용하는 공급자 중립적 이벤트 스트림:
agent.message— 어시스턴트 텍스트 콘텐츠agent.tool_use— 기본 제공 도구 호출agent.custom_tool_use— 클라이언트에서 실행하는 사용자 지정 도구(루프 일시 중지)agent.mcp_tool_use— MCP 도구 호출status.running— 턴 시작status.idle— 턴 완료(stop_reason포함)error— 실행 오류
UserEvent
클라이언트에서 에이전트로 전송되는 이벤트:
user.message— 에이전트에 콘텐츠 전송user.interrupt— 현재 턴 중지user.tool_confirmation— 도구 실행 허용/거부user.custom_tool_result— 사용자 지정 도구 결과 반환user.tool_result— 기본 제공 도구 결과(자체 호스팅 전용)user.define_outcome— 성공 기준 설정
ModelRef
모든 공급자를 지원하는 공급자 중립적 모델 참조:
// Shorthand (provider inferred from name)
ModelRef::Shorthand("gemini-3.7-flash".into())
ModelRef::Shorthand("gpt-5.6-terra".into())
ModelRef::Shorthand("claude-sonnet-5".into())
// Structured (explicit provider)
ModelRef::Structured {
provider: Provider::OpenaiCompatible,
model: ModelConfig::Compatible {
model: "my-model".into(),
base_url: "https://my-endpoint.com/v1".into(),
api_key: "sk-...".into(),
},
speed: None,
}
주요 기능
내구성 있는 세션
모든 이벤트는 원자적으로 체크포인트에 저장됩니다. 프로세스가 중단되면 resume()는 이벤트 손실 없이 마지막으로 일관된 체크포인트에서 다시 초기화됩니다:
// Before crash: events 0..5 committed
// After restart:
runtime.resume(&session).await?;
// Continues from seq=5, no gap, no duplicate
사용자 지정 도구 일시 중지
에이전트가 agent.custom_tool_use를 생성하면 클라이언트가 결과를 반환하거나 구성 가능한 시간 초과가 경과할 때까지 루프가 일시 중지됩니다:
// Agent emits: agent.custom_tool_use { custom_tool_use_id: "ct_1", name: "deploy" }
// Client executes the tool, then:
runtime.send_event(&session, UserEvent::CustomToolResult {
custom_tool_use_id: "ct_1".into(),
content: vec![ContentBlock::Text { text: "Deployed successfully".into() }],
}).await?;
이벤트 재생
시퀀스 기반 재생을 통해 SSE Last-Event-ID 재연결을 지원합니다:
// Reconnect from seq 42 — replays events 43, 44, ... then live tail
let stream = runtime.stream_events(&session, Some(42)).await?;
공급자 동등성
동일한 ManagedAgentDef는 Gemini, OpenAI, Anthropic, Ollama 및 OpenAI 호환 공급자 전반에서 바이트 단위로 동일한 이벤트 유형 시퀀스를 생성합니다(픽스처 F-8).
ScriptedLlm를 사용한 테스트
ScriptedLlm는 전체 런타임 파이프라인을 실행하는 결정론적 LLM 이중 객체입니다. 공급자 API 호출만 대체됩니다:
use adk_managed::testing::{ScriptedLlm, ScriptedTurn, ScriptedToolCall};
use serde_json::json;
let llm = ScriptedLlm::new("test", vec![
ScriptedTurn {
text: Some("I'll search for that.".into()),
tool_calls: vec![ScriptedToolCall {
name: "web_search".into(),
input: json!({"query": "rust agents"}),
id: Some("tc_1".into()),
}],
},
ScriptedTurn {
text: Some("Here are the results...".into()),
tool_calls: vec![],
},
]);
API 참조
전체 API 문서는 docs.rs에서 확인할 수 있습니다:
스모크 테스트 예제
플랫폼 팀을 위한 독립 실행형 예제 크레이트가 제공됩니다.
cargo run --manifest-path examples/managed_runtime_hello/Cargo.toml
이 예제는 ScriptedLlm을 사용하여 픽스처 F-1을 엔드투엔드로 실행합니다(API 키가 필요하지 않음).
관리형 상태 내구성
이벤트 로그, 시퀀스 위치, 보류된 도구 호출 및 수명 주기 상태를 포함한 관리형 세션 상태는 ManagedStateStore에 저장됩니다. 저장소는 자체 보장을 보고합니다:
| 내구성 | 의미 |
|---|---|
ProcessLocal | 프로세스가 실행되는 동안 재생 및 재개가 작동합니다. 충돌이 발생하면 상태가 손실되며 다른 프로세스에서 세션을 재개할 수 없습니다. |
CrashDurable | 쓰기가 승인되기 전에 백업 저장소에 상태가 기록되므로 다른 프로세스에서 세션을 재구성할 수 있습니다. |
InMemoryManagedStateStore만 제공되며, ProcessLocal입니다. 체크포인트 기능이 있다는 사실로 추론하지 말고 보장 내용을 확인하세요:
use adk_managed::{Durability, InMemoryManagedStateStore, ManagedStateStore};
let store = InMemoryManagedStateStore::new();
assert_eq!(store.durability(), Durability::ProcessLocal);
assert!(!store.durability().survives_process_loss());
체크포인트와 플러시 비교
CheckpointManager::checkpoint은 이벤트와 새로운 실행 상태를 함께 기록하므로, 재생 시 둘 중 하나만 존재하는 경우가 발생하지 않습니다. 이는 관리자의 자체 필드에 대한 쓰기입니다. flush은 스냅샷을 구성된 저장소에 기록하고, restore은 해당 저장소에서 관리자를 다시 구성합니다:
use adk_managed::{CheckpointManager, InMemoryManagedStateStore, ManagedStateStore};
use std::sync::Arc;
# async fn example() -> Result<(), adk_managed::types::RuntimeError> {
let store: Arc<dyn ManagedStateStore> = Arc::new(InMemoryManagedStateStore::new());
let manager = CheckpointManager::new("session-1".to_string()).with_store(Arc::clone(&store));
manager.flush().await?;
let restored = CheckpointManager::restore("session-1".to_string(), store).await?;
assert_eq!(restored.session_id(), "session-1");
# Ok(())
# }
상태 보고
ManagedAgentRuntime::status은 세션 루프가 쓰는 것과 동일한 핸들을 읽으므로, 제어 플레인에 관한 전환뿐 아니라 일반 전환도 확인할 수 있습니다:
| 전환 | 원인 |
|---|---|
Queued → Running | 턴이 시작됨 |
Running → Idle | 턴이 완료되고 사용량이 기록됨 |
모든 → Paused | pause |
모든 → Archived | archive 또는 delete_session |
참고: 이전에는 하나의 공유 핸들이었으므로,
status은 세션이 실행 중인 턴을 포함하여 세션의 전체 수명 동안Queued을 보고했습니다. 제어 플레인 전환(일시 중지, 재개, 보관)은 핸들에 직접 기록되었기 때문에 확인할 수 있었습니다.
삭제 의미 체계
delete_session은 두 계층을 모두 제거합니다.
- 세션을 종료 상태로 설정하고 해당 루프를 취소합니다.
- 런타임 핸들을 제거합니다.
- 주입된
SessionService을 통해, 세션을 생성할 때 사용한 동일한 IDstart_session로 영속화된 대화를 삭제합니다.
3단계가 실패하면 delete_session은 여전히 데이터를 보유한 앱, 사용자, 세션을 명시하는 오류를 반환합니다. 이 시점에는 핸들이 이미 사라졌으므로, 호출자에게 수동으로 정리해야 할 대상을 알려야 하며 성공한 것으로 간주하도록 허용해서는 안 됩니다.
runtime.delete_session(&session).await?;
// The handle is gone and the conversation is no longer in the session backend.
중요: 삭제는 소유자에 속한 세션의 대화를 제거합니다. 세션 소유권을 참조하세요.
세션 소유권
start_session에는 ManagedOwner이 필요합니다. 세션은 해당 ID로 영속화되며, 세션 루프가 수행하는 모든 Runner 호출은 이를 사용합니다.
use adk_managed::{ManagedAgentRuntime, ManagedOwner};
# async fn start(runtime: &dyn ManagedAgentRuntime, agent: &adk_managed::AgentHandle)
# -> Result<(), adk_managed::RuntimeError> {
let owner = ManagedOwner::new("support-console", "user-42")?;
let session = runtime.start_session(agent, &owner, None).await?;
# Ok(())
# }
중요:
checkpoint은 "원자적으로 영속화"하며 "어떤 충돌 이후에도 재생 시 일관된 뷰를 확인할 수 있다"는 보장을 제공하는 것으로 문서화되었고, 로드는 "재시작 후 세션을 재구성하는 데 필요한 모든 것"을 반환하는 것으로 설명되었습니다. 둘 다 사실이 아니었습니다. 두 작업 모두 영속 저장소에 대한 트랜잭션 없이 메모리 내 필드에서 동작했습니다. 제공된 저장소를 사용하면 새 프로세스에서restore을 호출해도 아무것도 찾지 못합니다. 두 구성 요소는 모두 필요하며 비어 있지 않아야 합니다. 서로 다른 소유자에 속한 세션은 별도로 주소가 지정되므로 조회와 삭제는 한 소유자의 범위로 제한되며 다른 소유자의 데이터에 접근할 수 없습니다.
참고: 모든 관리형 세션은 이전에 상수
managed/managed_user에 따라 지속되었으므로, 모두 하나의 논리적 네임스페이스를 공유했습니다. 호출자별로 범위를 지정할 수 없었고 어떤 세션도 특정 호출자에 귀속할 수 없었습니다.
환경 구성
EnvironmentConfig에는 env_vars 및 working_dir가 포함됩니다. 이 런타임은 다음 중 하나를 요청하는
구성을 거부합니다.
invalid request: EnvironmentConfig cannot be honoured by this runtime: sessions run
in-process, so per-session environment variables and working directories would have to mutate
process-global state shared with other sessions. Pass `None`, or configure a sandboxed runtime.
세션은 프로세스 내에서 실행되므로 세션별 환경 변수나 작업 디렉터리를 적용하면 다른 모든 세션과 공유되는 상태가 변경됩니다. 거부하는 것이 정직한 결과입니다. 요청을 충족하려면 샌드박스 실행 경계가 필요합니다.
참고: 이 인수는 이전에
_env로 명명되었다가 삭제되었으므로, 환경 구성을 제공한 호출자는 이를 조용히 무시하는 세션을 받았습니다.