Runtime de Agente Gerenciado
ESTABILIDADE: Experimental — Este recurso é aditivo e está protegido por funcionalidade atrás de
managed-runtime. Ele não afeta os existentesRunner/LlmAgentAPIs 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étodo | Descriçã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 assistenteagent.tool_use— Invocação de ferramenta integradaagent.custom_tool_use— Ferramenta personalizada executada pelo cliente (loop pausado)agent.mcp_tool_use— Invocação de ferramenta MCPstatus.running— Turno iniciadostatus.idle— Turno concluído (comstop_reason)error— Erro de execução
UserEvent
Eventos do cliente para o agente:
user.message— Enviar conteúdo ao agenteuser.interrupt— Interromper o turno atualuser.tool_confirmation— Permitir/recusar a execução da ferramentauser.custom_tool_result— Retornar resultados da ferramenta personalizadauser.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:
| Durabilidade | Significado |
|---|---|
ProcessLocal | A 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. |
CrashDurable | O 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ção | Causa |
|---|---|
Queued → Running | Um turno começa |
Running → Idle | O turno é concluído e o uso é registrado |
qualquer → Paused | pause |
qualquer → Archived | archive ou delete_session |
Observação: antes, este era um único handle compartilhado,
statusreportavaQueueddurante 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:
- Define a sessão como terminal e cancela seu loop.
- Remove o handle de execução.
- Exclui a conversa persistida por meio do
SessionServiceinjetado, usando a mesma identidadestart_sessionque 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:
checkpointera 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,restoreem 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
_enve era descartado, portanto um chamador que fornecesse uma configuração de ambiente recebia uma sessão que a ignorava silenciosamente.