Entorno de ejecución de agentes gestionado
ESTABILIDAD: Experimental — Esta funcionalidad es aditiva y está protegida por funcionalidades detrás de
managed-runtime. No afecta aRunner/LlmAgentAPIs existentes cuando la funcionalidad está deshabilitada. La superficie de API puede cambiar en futuras versiones.
Descripción general
El Entorno de ejecución de agentes gestionado (adk-managed) es un motor de ejecución de agentes duradero, reanudable y neutral respecto al proveedor. Recibe un ManagedAgentDef declarativo, construye un agente ejecutable y lo opera como una sesión en segundo plano cuyos puntos de control pueden reanudarse y cuyos eventos se transmiten.
El entorno de ejecución es una biblioteca, no un servicio. La plataforma lo aloja. Esto significa:
- Probable de probar de forma aislada: Cero dependencias de HTTP/autenticación/facturación
- Integrable: Las implementaciones autohospedadas utilizan directamente el mismo trait del entorno de ejecución
- Plataforma intercambiable: Diferentes plataformas pueden alojar el mismo entorno de ejecución
- Neutral respecto al proveedor: Secuencias de eventos idénticas independientemente del proveedor del modelo
Inicio rápido
Añade la funcionalidad a tu Cargo.toml:
[dependencies]
adk-rust = { version = "2.1.0", features = ["managed-runtime"] }
O utiliza directamente el crate adk-managed:
[dependencies]
adk-managed = "2.1.0"
adk-session = "2.1.0"
Ejemplo mínimo (ScriptedLlm — sin clave de 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(())
}
Arquitectura
┌─────────────────────────────────────────────────────────────┐
│ 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 principales
Trait ManagedAgentRuntime
El trait async central que define todo el ciclo de vida del agente:
| Método | Descripción |
|---|---|
create(def) | Registra una definición de agente y devuelve AgentHandle |
start_session(agent, env?) | Inicia una sesión nueva, con estado inicial Queued |
send_event(session, event) | Enviar un UserEvent a la sesión |
stream_events(session, from_seq?) | Suscribirse al flujo de SessionEvent |
interrupt(session) | Detenerse en el siguiente límite y emitir status.idle |
pause(session) | Crear un punto de control y pausar el procesamiento |
resume(session) | Reanudar desde la pausa o reiniciar |
status(session) | Consultar SessionStatus actual |
archive(session) | Estado terminal, datos conservados |
delete_session(session) | Eliminar datos de la sesión |
ManagedAgentDef
Definición declarativa de agente con 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
Flujo de eventos independiente del proveedor con números de secuencia monotónicos:
agent.message— Contenido de texto del asistenteagent.tool_use— Invocación de herramienta integradaagent.custom_tool_use— Herramienta personalizada ejecutada por el cliente (el bucle se pausa)agent.mcp_tool_use— Invocación de herramienta MCPstatus.running— Inicio del turnostatus.idle— Turno completado (constop_reason)error— Error de ejecución
UserEvent
Eventos del cliente al agente:
user.message— Enviar contenido al agenteuser.interrupt— Detener el turno actualuser.tool_confirmation— Permitir o denegar la ejecución de la herramientauser.custom_tool_result— Devolver los resultados de la herramienta personalizadauser.tool_result— Resultado de herramienta integrada (solo autoalojada)user.define_outcome— Establecer criterios de éxito
ModelRef
Referencia de modelo independiente del proveedor compatible con todos los proveedores:
// 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,
}
Características principales
Sesiones duraderas
Cada evento se guarda atómicamente como punto de control. Si el proceso falla, resume() se rehidrata
desde el último punto de control coherente sin perder ningún evento:
// Before crash: events 0..5 committed
// After restart:
runtime.resume(&session).await?;
// Continues from seq=5, no gap, no duplicate
Pausa de herramientas personalizadas
Cuando el agente emite agent.custom_tool_use, el bucle se pausa hasta que el cliente
devuelve los resultados o transcurre un tiempo de espera configurable:
// 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?;
Reproducción de eventos
Admite la reconexión de SSE Last-Event-ID mediante la reproducción basada en secuencias:
// Reconnect from seq 42 — replays events 43, 44, ... then live tail
let stream = runtime.stream_events(&session, Some(42)).await?;
Paridad entre proveedores
Un ManagedAgentDef idéntico produce secuencias de tipos de eventos idénticas byte a byte en
Gemini, OpenAI, Anthropic, Ollama y proveedores compatibles con OpenAI (fixture F-8).
Pruebas con ScriptedLlm
ScriptedLlm es un doble determinista de LLM que ejercita todo el flujo
de ejecución. Solo se reemplaza la llamada del proveedor 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![],
},
]);
Referencia de API
La documentación completa de API está disponible en docs.rs:
Ejemplo de prueba de humo
Se proporciona un crate de ejemplo independiente para los equipos de plataforma:
cargo run --manifest-path examples/managed_runtime_hello/Cargo.toml
Esto ejecuta el fixture F-1 de extremo a extremo con ScriptedLlm (no se requiere ninguna clave API).
Durabilidad del estado gestionado
El estado gestionado de la sesión —el registro de eventos, la posición de secuencia, las llamadas a herramientas aparcadas y el estado del ciclo de vida— reside en un ManagedStateStore. El almacén informa de su propia garantía:
| Durabilidad | Significado |
|---|---|
ProcessLocal | La reproducción y la reanudación funcionan mientras el proceso está en ejecución. Un fallo provoca la pérdida del estado y otro proceso no puede reanudar la sesión. |
CrashDurable | El estado se escribe en un almacén de respaldo antes de confirmar la escritura, de modo que otro proceso pueda reconstruir la sesión. |
Solo se incluye InMemoryManagedStateStore, y es ProcessLocal. Comprueba la garantía en
lugar de inferirla por la presencia de puntos de control:
use adk_managed::{Durability, InMemoryManagedStateStore, ManagedStateStore};
let store = InMemoryManagedStateStore::new();
assert_eq!(store.durability(), Durability::ProcessLocal);
assert!(!store.durability().survives_process_loss());
Puntos de control frente al vaciado
CheckpointManager::checkpoint registra un evento y el nuevo estado de ejecución conjuntamente, por lo que la reproducción nunca
ve uno sin el otro. Eso es una escritura en los propios campos del gestor. flush escribe la
instantánea en el almacén configurado, y restore reconstruye un gestor a partir de ella:
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(())
# }
Informes de estado
ManagedAgentRuntime::status lee el mismo identificador al que escribe el bucle de sesión, por lo que las transiciones normales
son visibles, no solo las del plano de control:
| Transición | Causa |
|---|---|
Queued → Running | Comienza un turno |
Running → Idle | El turno se completa y se registra el uso |
cualquiera → Paused | pause |
cualquiera → Archived | archive o delete_session |
Nota: antes de esto existía un único identificador compartido,
statusinformabaQueueddurante toda la vida útil de una sesión, incluso mientras ejecutaba turnos. Las transiciones del plano de control (pausa, reanudación, archivado) eran visibles porque escribían directamente en el identificador.
Semántica de eliminación
delete_session elimina ambos planos:
- Marca la sesión como terminal y cancela su bucle.
- Elimina el identificador del tiempo de ejecución.
- Elimina la conversación persistida mediante el
SessionServiceinyectado, bajo la misma identidadstart_sessioncon la que se creó.
Si el paso 3 falla, delete_session devuelve un error que identifica la aplicación, el usuario y la sesión que todavía contienen datos —el identificador ya ha desaparecido en ese momento, por lo que se debe informar a quien realiza la llamada de lo que requiere una limpieza manual, en lugar de permitirle asumir que la operación tuvo éxito.
runtime.delete_session(&session).await?;
// The handle is gone and the conversation is no longer in the session backend.
Importante: la eliminación elimina la conversación de la sesión bajo la identidad de su propietario. Consulta Propiedad de la sesión.
Propiedad de la sesión
start_session requiere un ManagedOwner. La sesión se persiste bajo esa identidad, y
cada llamada de Runner que realiza el bucle de la sesión utiliza dicha identidad:
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:
checkpointestaba documentado como «persistir atómicamente», con la garantía de que «la reproducción verá una vista coherente después de cualquier fallo», y se describía la carga como una operación que devolvía «todo lo necesario para reconstruir una sesión después de un reinicio». Ninguna de las dos afirmaciones era cierta: ambas operaban sobre campos en memoria sin ninguna transacción con un almacén persistente. Con el almacén incluido,restoreen un proceso nuevo no encuentra nada. Ambos componentes son obligatorios y no pueden estar en blanco. Las sesiones pertenecientes a propietarios diferentes se direccionan por separado, por lo que la búsqueda y la eliminación están delimitadas a un único propietario y no pueden acceder a los datos de otro.
Nota: todas las sesiones administradas se persistían anteriormente bajo las constantes
managed/managed_user, por lo que todas compartían un único espacio de nombres lógico: nada podía limitarse al ámbito de un llamador y ninguna sesión podía atribuirse a uno.
Configuración del entorno
EnvironmentConfig incluye env_vars y working_dir. Este entorno de ejecución rechaza una
configuración que solicite cualquiera de los dos:
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.
Las sesiones se ejecutan dentro del proceso, por lo que aplicar variables de entorno o un directorio de trabajo por sesión modificaría el estado compartido con todas las demás sesiones. Rechazarla es el resultado honesto; un límite de ejecución aislado es lo que haría posible satisfacer la solicitud.
Nota: el argumento se llamaba anteriormente
_envy se descartaba, por lo que un llamador que proporcionara configuración del entorno recibía una sesión que la ignoraba silenciosamente.