Runtime de Agente Gerenciado

ESTABILIDADE: Experimental — Este recurso é aditivo e está protegido por funcionalidade atrás de managed-runtime. Ele não afeta os existentes Runner/LlmAgent APIs quando o recurso está desabilitado. A superfície de API pode mudar em versões futuras.

Visão geral

O Runtime de Agente Gerenciado (adk-managed) é um mecanismo de execução de agentes durável, retomável e neutro em relação ao provedor. Ele recebe um ManagedAgentDef declarativo, cria um agente executável e o opera como uma sessão em segundo plano, com retomada a partir de checkpoints e transmissão de eventos.

O runtime é uma biblioteca, não um serviço. A plataforma o hospeda. Isso significa:

  • Testável isoladamente: zero dependências de HTTP/autenticação/faturamento
  • Incorporável: implantações auto-hospedadas usam diretamente o mesmo trait do runtime
  • Plataforma substituível: diferentes plataformas podem hospedar o mesmo runtime
  • Neutro em relação ao provedor: sequências de eventos idênticas independentemente do provedor do modelo

Início rápido

Adicione o recurso ao seu Cargo.toml:

[dependencies]
adk-rust = { version = "2.1.0", features = ["managed-runtime"] }

Ou use diretamente o crate adk-managed:

[dependencies]
adk-managed = "2.1.0"
adk-session = "2.1.0"

Exemplo mínimo (ScriptedLlm — sem chave 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(())
}

Arquitetura

┌─────────────────────────────────────────────────────────────┐
│                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    │    │
│  └──────────┘ └──────────┘ └──────────┘ └────────────┘    │
└─────────────────────────────────────────────────────────────┘

Tipos principais

Trait ManagedAgentRuntime

O trait assíncrono central que define todo o ciclo de vida do agente:

MétodoDescrição
create(def)Registra uma definição de agente e retorna AgentHandle
start_session(agent, env?)Inicia uma nova sessão, com status inicial Queued
send_event(session, event)Enviar um UserEvent para a sessão
stream_events(session, from_seq?)Assinar o fluxo SessionEvent
interrupt(session)Parar no próximo limite e emitir status.idle
pause(session)Criar um checkpoint e pausar o processamento
resume(session)Retomar após pausa ou reiniciar
status(session)Consultar SessionStatus atual
archive(session)Estado terminal, dados retidos
delete_session(session)Remover dados da sessão

ManagedAgentDef

Definição declarativa de agente com 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

Fluxo de eventos independente de provedor com números de sequência monotônicos:

  • agent.message — Conteúdo de texto do assistente
  • agent.tool_use — Invocação de ferramenta integrada
  • agent.custom_tool_use — Ferramenta personalizada executada pelo cliente (loop pausado)
  • agent.mcp_tool_use — Invocação de ferramenta MCP
  • status.running — Turno iniciado
  • status.idle — Turno concluído (com stop_reason)
  • error — Erro de execução

UserEvent

Eventos do cliente para o agente:

  • user.message — Enviar conteúdo ao agente
  • user.interrupt — Interromper o turno atual
  • user.tool_confirmation — Permitir/recusar a execução da ferramenta
  • user.custom_tool_result — Retornar resultados da ferramenta personalizada
  • user.tool_result — Resultado da ferramenta integrada (somente hospedagem própria)
  • user.define_outcome — Definir critérios de sucesso

ModelRef

Referência de modelo independente de provedor compatível com todos os provedores:

// 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,
}

Principais recursos

Sessões duráveis

Cada evento é armazenado atomicamente como um ponto de verificação. Em caso de falha do processo, resume() é reidratado a partir do último ponto de verificação consistente, sem perda de eventos:

// Before crash: events 0..5 committed
// After restart:
runtime.resume(&session).await?;
// Continues from seq=5, no gap, no duplicate

Pausa de ferramentas personalizadas

Quando o agente emite agent.custom_tool_use, o loop é pausado até que o cliente retorne os resultados ou até que um tempo limite configurável expire:

// 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?;

Reprodução de eventos

Compatibilidade com a reconexão de SSE Last-Event-ID por meio de reprodução baseada em sequência:

// Reconnect from seq 42 — replays events 43, 44, ... then live tail
let stream = runtime.stream_events(&session, Some(42)).await?;

Paridade entre provedores

ManagedAgentDef idêntico produz sequências de tipos de eventos idênticas byte a byte entre Gemini, OpenAI, Anthropic, Ollama e provedores compatíveis com OpenAI (fixture F-8).

Testes com ScriptedLlm

ScriptedLlm é um dublê determinístico de LLM que exercita todo o pipeline de execução. Somente a chamada API do provedor é substituída:

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![],
    },
]);

Referência de API

A documentação completa de API está disponível em docs.rs:

Exemplo de teste de fumaça

Um crate de exemplo independente é fornecido para as equipes de plataforma:

cargo run --manifest-path examples/managed_runtime_hello/Cargo.toml

Isso executa o fixture F-1 de ponta a ponta com ScriptedLlm (nenhuma chave API é necessária).

Durabilidade do estado gerenciado

O estado gerenciado da sessão — o registro de eventos, a posição da sequência, as chamadas de ferramenta estacionadas e o status do ciclo de vida — reside em um ManagedStateStore. O armazenamento informa sua própria garantia:

DurabilidadeSignificado
ProcessLocalA reprodução e a retomada funcionam enquanto o processo está em execução. Uma falha faz o estado ser perdido, e outro processo não pode retomar a sessão.
CrashDurableO estado é gravado em um armazenamento de suporte antes que a gravação seja confirmada, para que outro processo possa reconstruir a sessão.

Apenas InMemoryManagedStateStore é fornecido, e ele é ProcessLocal. Verifique a garantia em vez de inferi-la pela presença de checkpointing:

use adk_managed::{Durability, InMemoryManagedStateStore, ManagedStateStore};

let store = InMemoryManagedStateStore::new();
assert_eq!(store.durability(), Durability::ProcessLocal);
assert!(!store.durability().survives_process_loss());

Checkpointing versus descarregamento

CheckpointManager::checkpoint registra um evento e o novo estado da execução juntos, portanto a reprodução nunca vê um sem o outro. Isso é uma gravação nos próprios campos do gerenciador. flush grava o snapshot no armazenamento configurado, e restore reconstrói um gerenciador a partir dele:

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(())
# }

Relato de status

ManagedAgentRuntime::status lê o mesmo identificador que o loop da sessão grava, portanto as transições normais ficam visíveis, não apenas as do plano de controle:

TransiçãoCausa
QueuedRunningUm turno começa
RunningIdleO turno é concluído e o uso é registrado
qualquer → Pausedpause
qualquer → Archivedarchive ou delete_session

Observação: antes, este era um único handle compartilhado, status reportava Queued durante toda a vida de uma sessão, inclusive enquanto ela executava turnos. As transições do plano de controle (pausar, retomar, arquivar) ficavam visíveis porque gravavam diretamente no handle.

Semântica de exclusão

delete_session remove ambos os planos:

  1. Define a sessão como terminal e cancela seu loop.
  2. Remove o handle de execução.
  3. Exclui a conversa persistida por meio do SessionService injetado, usando a mesma identidade start_session que a criou.

Se a etapa 3 falhar, delete_session retorna um erro identificando o app, o usuário e a sessão que ainda mantêm os dados — o handle já terá sido removido nesse ponto, portanto o chamador precisa ser informado sobre o que requer limpeza manual, em vez de poder presumir sucesso.

runtime.delete_session(&session).await?;
// The handle is gone and the conversation is no longer in the session backend.

Importante: a exclusão remove a conversa da sessão sob seu proprietário. Consulte Propriedade da sessão.

Propriedade da sessão

start_session requer um ManagedOwner. A sessão é persistida sob essa identidade, e todas as chamadas do Runner que o loop da sessão realiza usam essa identidade:

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(())
# }

Importante: checkpoint era documentado como "persistir atomicamente", com a garantia de que "a reprodução verá uma visualização consistente após qualquer falha", e o carregamento era descrito como retornando "tudo o que fosse necessário para reconstruir uma sessão após uma reinicialização". Nenhuma das duas afirmações era verdadeira: ambos operavam sobre campos na memória, sem uma transação contra qualquer armazenamento persistente. Com o armazenamento fornecido, restore em um novo processo não encontra nada. Ambos os componentes são obrigatórios e não podem estar em branco. As sessões pertencentes a proprietários diferentes são endereçadas separadamente, portanto a consulta e a exclusão têm escopo limitado a um proprietário e não podem acessar os dados de outro.

Observação: todas as sessões gerenciadas eram anteriormente persistidas sob as constantes managed / managed_user, portanto todas compartilhavam um único namespace lógico: nada podia ser delimitado por um chamador e nenhuma sessão podia ser atribuída a um deles.

Configuração do ambiente

EnvironmentConfig contém env_vars e working_dir. Esse tempo de execução rejeita uma configuração que solicite qualquer um dos seguintes:

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.

As sessões são executadas no mesmo processo, portanto aplicar variáveis de ambiente ou um diretório de trabalho por sessão modificaria o estado compartilhado com todas as outras sessões. Recusar é o resultado honesto; um limite de execução isolado é o que tornaria a solicitação atendível.

Observação: o argumento se chamava anteriormente _env e era descartado, portanto um chamador que fornecesse uma configuração de ambiente recebia uma sessão que a ignorava silenciosamente.