관리형 에이전트 런타임

안정성: 실험적 — 이 기능은 추가 기능이며 managed-runtime 뒤에서 기능 플래그로 보호됩니다. 기능이 비활성화된 경우 기존 Runner/LlmAgent APIs에 영향을 주지 않습니다. 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은 세션 루프가 쓰는 것과 동일한 핸들을 읽으므로, 제어 플레인에 관한 전환뿐 아니라 일반 전환도 확인할 수 있습니다:

전환원인
QueuedRunning턴이 시작됨
RunningIdle턴이 완료되고 사용량이 기록됨
모든 → Pausedpause
모든 → Archivedarchive 또는 delete_session

참고: 이전에는 하나의 공유 핸들이었으므로, status은 세션이 실행 중인 턴을 포함하여 세션의 전체 수명 동안 Queued을 보고했습니다. 제어 플레인 전환(일시 중지, 재개, 보관)은 핸들에 직접 기록되었기 때문에 확인할 수 있었습니다.

삭제 의미 체계

delete_session은 두 계층을 모두 제거합니다.

  1. 세션을 종료 상태로 설정하고 해당 루프를 취소합니다.
  2. 런타임 핸들을 제거합니다.
  3. 주입된 SessionService을 통해, 세션을 생성할 때 사용한 동일한 ID start_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_varsworking_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로 명명되었다가 삭제되었으므로, 환경 구성을 제공한 호출자는 이를 조용히 무시하는 세션을 받았습니다.