Runtime d’agent géré
STABILITÉ : Expérimental — Cette fonctionnalité est additive et protégée par
managed-runtime. Elle n’affecte pas lesRunner/LlmAgentAPIs existants lorsque la fonctionnalité est désactivée. L’interface API peut changer dans les prochaines versions.
Vue d’ensemble
Le runtime d’agent géré (adk-managed) est un moteur d’exécution d’agents durable, pouvant reprendre son exécution et indépendant du fournisseur. Il prend une ManagedAgentDef déclarative, construit un agent exécutable et l’exécute comme une session d’arrière-plan dont l’exécution peut reprendre depuis un point de contrôle et qui diffuse des événements.
Le runtime est une bibliothèque, et non un service. La plateforme l’héberge. Cela signifie :
- Testable de manière isolée : aucune dépendance à HTTP/l’authentification/la facturation
- Intégrable : les déploiements auto-hébergés utilisent directement le même trait de runtime
- Plateforme interchangeable : différentes plateformes peuvent héberger le même runtime
- Indépendant du fournisseur : séquences d’événements identiques quel que soit le fournisseur de modèles
Démarrage rapide
Ajoutez la fonctionnalité à votre Cargo.toml :
[dependencies]
adk-rust = { version = "2.1.0", features = ["managed-runtime"] }
Ou utilisez directement le crate adk-managed :
[dependencies]
adk-managed = "2.1.0"
adk-session = "2.1.0"
Exemple minimal (ScriptedLlm — sans clé 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(())
}
Architecture
┌─────────────────────────────────────────────────────────────┐
│ 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 │ │
│ └──────────┘ └──────────┘ └──────────┘ └────────────┘ │
└─────────────────────────────────────────────────────────────┘
Types principaux
Trait ManagedAgentRuntime
Le trait asynchrone central qui définit l’intégralité du cycle de vie de l’agent :
| Méthode | Description |
|---|---|
create(def) | Enregistre une définition d’agent et renvoie AgentHandle |
start_session(agent, env?) | Démarre une nouvelle session, avec le statut initial Queued |
send_event(session, event) | Envoyer un UserEvent à la session |
stream_events(session, from_seq?) | S’abonner au flux SessionEvent |
interrupt(session) | S’arrêter à la prochaine limite et émettre status.idle |
pause(session) | Créer un point de contrôle et suspendre le traitement |
resume(session) | Reprendre après une pause ou redémarrer |
status(session) | Interroger SessionStatus actuel |
archive(session) | État terminal, données conservées |
delete_session(session) | Supprimer les données de session |
ManagedAgentDef
Définition déclarative d’un agent avec le générateur 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
Flux d’événements indépendant du fournisseur avec des numéros de séquence monotones :
agent.message— Contenu textuel de l’assistantagent.tool_use— Appel d’un outil intégréagent.custom_tool_use— Outil personnalisé exécuté par le client (la boucle se met en pause)agent.mcp_tool_use— Appel de l’outil MCPstatus.running— Début du tourstatus.idle— Tour terminé (avecstop_reason)error— Erreur d’exécution
UserEvent
Événements du client vers l’agent :
user.message— Envoyer du contenu à l’agentuser.interrupt— Arrêter le tour actueluser.tool_confirmation— Autoriser/refuser l’exécution de l’outiluser.custom_tool_result— Retourner les résultats de l’outil personnaliséuser.tool_result— Résultat de l’outil intégré (uniquement auto-hébergé)user.define_outcome— Définir les critères de réussite
ModelRef
Référence de modèle indépendante du fournisseur prenant en charge tous les fournisseurs :
// 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,
}
Fonctionnalités clés
Sessions durables
Chaque événement est enregistré de manière atomique. En cas de panne du processus, resume() se réhydrate
à partir du dernier point de contrôle cohérent, sans perte d’événements :
// Before crash: events 0..5 committed
// After restart:
runtime.resume(&session).await?;
// Continues from seq=5, no gap, no duplicate
Mise en pause des outils personnalisés
Lorsque l’agent émet agent.custom_tool_use, la boucle se met en pause jusqu’à ce que le client
retourne les résultats ou qu’un délai d’expiration configurable soit écoulé :
// 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?;
Relecture des événements
Prise en charge de la reconnexion de SSE Last-Event-ID via une relecture fondée sur les séquences :
// Reconnect from seq 42 — replays events 43, 44, ... then live tail
let stream = runtime.stream_events(&session, Some(42)).await?;
Parité entre fournisseurs
Un même ManagedAgentDef produit des séquences de types d’événements identiques octet par octet avec
Gemini, OpenAI, Anthropic, Ollama et les fournisseurs compatibles avec OpenAI (fixture F-8).
Tests avec ScriptedLlm
ScriptedLlm est un double LLM déterministe qui exerce l’intégralité du pipeline d’exécution.
Seul l’appel API du fournisseur est remplacé :
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![],
},
]);
Référence de API
La documentation complète de API est disponible sur docs.rs :
Exemple de test de fumée
Un crate d’exemple autonome est fourni aux équipes de plateforme :
cargo run --manifest-path examples/managed_runtime_hello/Cargo.toml
Cela exécute le fixture F-1 de bout en bout avec ScriptedLlm (aucune clé API n’est requise).
Durabilité de l’état géré
L’état de session géré — le journal des événements, la position dans la séquence, les appels d’outils mis en attente et
l’état du cycle de vie — réside dans un ManagedStateStore. Le stockage indique sa propre garantie :
| Durabilité | Signification |
|---|---|
ProcessLocal | La relecture et la reprise fonctionnent pendant l’exécution du processus. Un plantage entraîne la perte de l’état, et un autre processus ne peut pas reprendre la session. |
CrashDurable | L’état est écrit dans un magasin de stockage avant que l’écriture ne soit confirmée, afin qu’un autre processus puisse reconstruire la session. |
Seul InMemoryManagedStateStore est livré, et il est ProcessLocal. Vérifiez la garantie
plutôt que de la déduire de la présence de points de contrôle :
use adk_managed::{Durability, InMemoryManagedStateStore, ManagedStateStore};
let store = InMemoryManagedStateStore::new();
assert_eq!(store.durability(), Durability::ProcessLocal);
assert!(!store.durability().survives_process_loss());
Points de contrôle par rapport à l’écriture sur disque
CheckpointManager::checkpoint enregistre un événement et le nouvel état d’exécution ensemble, de sorte que la relecture ne voit jamais l’un sans l’autre. Il s’agit d’une écriture dans les propres champs du gestionnaire. flush écrit
l’instantané dans le stockage configuré, et restore reconstruit un gestionnaire à partir de celui-ci :
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(())
# }
Rapport d’état
ManagedAgentRuntime::status lit le même handle dans lequel la boucle de session écrit, de sorte que les
transitions normales sont visibles, et pas seulement celles du plan de contrôle :
| Transition | Cause |
|---|---|
Queued → Running | Un tour commence |
Running → Idle | Le tour se termine et l'utilisation est enregistrée |
quelconque → Paused | pause |
quelconque → Archived | archive ou delete_session |
Remarque : auparavant, il s’agissait d’une poignée partagée,
statussignalaitQueuedpendant toute la durée de vie d’une session, y compris pendant l’exécution de ses tours. Les transitions du plan de contrôle (pause, reprise, archivage) étaient visibles, car elles écrivaient directement dans la poignée.
Sémantique de suppression
delete_session supprime les deux plans :
- Définit la session comme terminée et annule sa boucle.
- Supprime la poignée d’exécution.
- Supprime la conversation persistée via le
SessionServiceinjecté, sous la même identitéstart_sessionque celle avec laquelle elle a été créée.
Si l’étape 3 échoue, delete_session renvoie une erreur indiquant l’application, l’utilisateur et la session
qui détiennent encore les données — la poignée a déjà été supprimée à ce stade ; l’appelant doit donc être informé
de ce qui nécessite un nettoyage manuel, plutôt que d’être autorisé à supposer que l’opération a réussi.
runtime.delete_session(&session).await?;
// The handle is gone and the conversation is no longer in the session backend.
Important : la suppression retire la conversation de la session sous l’identité de son propriétaire. Voir Propriété de la session.
Propriété de la session
start_session nécessite un ManagedOwner. La session est persistée sous cette identité, et
chaque appel du Runner effectué par la boucle de session l’utilise :
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(())
# }
Important :
checkpointétait documenté comme « persister atomiquement », avec la garantie que « la relecture verrait une vue cohérente après tout plantage », et le chargement était décrit comme renvoyant « tout ce qui est nécessaire pour reconstruire une session après un redémarrage ». Aucune de ces affirmations n’était vraie : les deux opérations utilisaient des champs en mémoire, sans transaction avec un magasin persistant. Avec le magasin fourni,restoredans un nouveau processus ne trouve rien.
Les deux composants sont requis et ne doivent pas être vides. Les sessions appartenant à différents propriétaires sont adressées séparément ; la recherche et la suppression sont donc limitées à un seul propriétaire et ne peuvent pas atteindre les données d’un autre.
Remarque : chaque session gérée était auparavant persistée sous les constantes
managed/managed_user, de sorte qu’elles partageaient toutes un même espace de noms logique : rien ne pouvait être limité à un appelant et aucune session ne pouvait lui être attribuée.
Configuration de l’environnement
EnvironmentConfig contient env_vars et working_dir. Cet environnement d’exécution rejette une
configuration qui demande l’un ou l’autre :
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.
Les sessions s’exécutent dans le processus, donc l’application de variables d’environnement par session ou d’un répertoire de travail modifierait l’état partagé avec toutes les autres sessions. Le refus est l’issue honnête ; une limite d’exécution isolée est ce qui rendrait la demande satisfaisable.
Remarque : l’argument s’appelait auparavant
_envet était ignoré, de sorte qu’un appelant fournissant une configuration d’environnement recevait une session qui l’ignorait silencieusement.