Agents ambiants

Exécuter un agent ambiant

Un gestionnaire de dĂ©clencheur est requis. Le chemin pris en charge est with_invoker, qui accepte tout ce qui implĂ©mente adk_core::AgentInvoker — Runner le fait :

use adk_agent::ambient::{AmbientAgent, RunnerTriggerConfig};
use std::sync::Arc;

let mut ambient = AmbientAgent::new(agent, source)
    .with_invoker(runner, RunnerTriggerConfig::new("system"))
    .with_max_concurrent_triggers(4);

let mut outputs = ambient.take_output(64);
ambient.start().await?;

RunnerTriggerConfig contrÎle trois éléments :

MéthodeObjectifValeur par défaut
new(user_id)IdentitĂ© sous laquelle les exĂ©cutions sont enregistrĂ©es. Un dĂ©clencheur n’a pas d’utilisateur interactif ; utilisez donc "system" ou le nom d’un compte de service.obligatoire
with_session_policyPerTrigger attribue à chaque événement sa propre session ; Shared(id) en réutilise une.PerTrigger
with_promptTransforme l’évĂ©nement en texte d’invite.indique la source et sĂ©rialise la charge utile

PerTrigger est utilisĂ© par dĂ©faut, car un dĂ©clenchement chaque minute dans une session partagĂ©e unique fait croĂźtre sans limite l'historique de cette session — ainsi que le coĂ»t en jetons de chaque exĂ©cution ultĂ©rieure. Runner sĂ©rialise les tours invoquĂ©s de l'extĂ©rieur ciblant la mĂȘme session partagĂ©e jusqu'Ă  la fin de chaque flux d'Ă©vĂ©nements, tandis que des identifiants de session diffĂ©rents peuvent toujours s'exĂ©cuter simultanĂ©ment.

AgentInvoker::invoke crée la session lorsqu'elle n'existe pas. Runner::run ne le fait pas : il résout une session existante et produit session.not_found via le flux dans le cas contraire, ce qui laisse à une exécution déclenchée de l'extérieur aucune possibilité de la préenregistrer.

Lorsque l'appelant expose sa racine exĂ©cutable, comme le fait Runner, with_invoker utilise cet agent pour la journalisation et les diagnostics ambiants. Cela empĂȘche la tĂ©lĂ©mĂ©trie de nommer un agent alors qu'un autre traite effectivement le dĂ©clencheur.

Fournir directement un gestionnaire

with_trigger_handler reste disponible pour les appelants qui pilotent autre chose qu'un Runner. Il reçoit l'événement et l'agent, et doit retourner le flux d'événements ; la création de la session relÚve alors de la responsabilité du gestionnaire.

use adk_agent::ambient::{AmbientAgent, TriggerHandler};
use std::sync::Arc;

let handler: TriggerHandler = Arc::new(move |event, agent| {
    let backend = backend.clone();
    Box::pin(async move { backend.dispatch(event, agent).await })
});

let mut ambient = AmbientAgent::new(agent, source).with_trigger_handler(handler);

start échoue sans gestionnaire :

AmbientAgent has no trigger handler, so starting it would log trigger events without ever
invoking the agent. Call `with_trigger_handler` with a closure that drives the agent through a
Runner.

Important : démarrer sans gestionnaire réussissait auparavant, puis journalisait chaque déclencheur, de sorte que AmbientAgent::new(..).start() donnait l'impression d'exécuter un agent qui ne s'était jamais exécuté.

Sortie et concurrence

ComportementContrĂŽle
Les Ă©vĂ©nements et les erreurs produits par l’agent sont transmis Ă  un canaltake_output(capacity)
Déclencheurs traités simultanémentwith_max_concurrent_triggers (4 par défaut, zéro est traité comme un)

Les Ă©vĂ©nements produits Ă©taient auparavant consignĂ©s au niveau de dĂ©bogage, puis ignorĂ©s, de sorte qu’un appelant ne pouvait pas observer ce qu’une exĂ©cution avait fait ni dĂ©terminer si elle avait Ă©chouĂ©. Les dĂ©clencheurs Ă©taient Ă©galement traitĂ©s strictement un par un : la boucle Ă©puisait tout le flux d’évĂ©nements d’un gestionnaire avant d’interroger Ă  nouveau la source ; ainsi, un dĂ©clencheur lent bloquait tous les suivants.

Remarque : la limite rĂ©git la concurrence, et non le parallĂ©lisme. Les gestionnaires partagent la tĂąche ambiante ; un gestionnaire qui bloque le thread immobilise donc toujours la boucle. Utilisez tokio::task::spawn_blocking dans ce cas. La responsabilitĂ© des dĂ©calages persistants des dĂ©clencheurs, de la gestion des lettres mortes et des nouvelles tentatives reste Ă  la charge de l’appelant. Les agents ambiants rĂ©agissent aux Ă©vĂ©nements plutĂŽt qu’à une intervention de l’utilisateur. Un EventSource produit des TriggerEvents et l’agent s’exĂ©cute pour chacun d’eux.

Nécessite la fonctionnalité ambient :

[dependencies]
adk-agent = { version = "2.1.0", features = ["ambient"] }

Sources d’évĂ©nements

SourceSe déclenche surPrincipal
CronTriggerUne planification cronNone — une planification n’a aucun appelant
FileWatchTriggerUne modification correspondante du systùme de fichiersNone — une modification de fichier n’a aucun appelant
WebhookTriggerUn POST HTTP autoriséLe résultat du vérificateur

TriggerEvent::principal permet Ă  un gestionnaire de distinguer un dĂ©clencheur autorisĂ© d’un dĂ©clencheur anonyme, plutĂŽt que de traiter chaque Ă©vĂ©nement comme Ă©tant Ă©galement digne de confiance.

Tics manqués

CronTrigger::subscribe calcule le prochain tic Ă  partir du moment oĂč il est appelĂ©. Un dĂ©clencheur qui redĂ©marre aprĂšs une interruption, ou qui s’exĂ©cute sur un hĂŽte suspendu, reprend donc au prochain tic futur, et chaque tic arrivĂ© Ă  Ă©chĂ©ance entre-temps est ignorĂ©.

MissedTickPolicy détermine ce qui se passe pendant cet intervalle :

PolitiqueComportementÀ utiliser pour
SkipIgnorer les intervalles Ă©coulĂ©s et attendre le prochain intervalle planifiĂ©. Par dĂ©faut.Planifications pour lesquelles une exĂ©cution tardive n’a aucune valeur
CoalesceOneÉmettre un Ă©vĂ©nement couvrant toute la pĂ©riode Ă©coulĂ©e.Parcours pour lesquels seul l’état actuel importe
AllÉmet un Ă©vĂ©nement par cycle Ă©coulĂ©, du plus ancien au plus rĂ©cent.Planifications oĂč chaque occurrence possĂšde son propre travail

Une stratĂ©gie Ă  elle seule ne couvre que les lacunes au sein d’un mĂȘme abonnement. DĂ©tecter une lacune qui s’étend sur un redĂ©marrage du processus nĂ©cessite un TickWatermark pour enregistrer l’endroit oĂč la planification s’est arrĂȘtĂ©e :

use std::sync::Arc;
use adk_agent::ambient::{CronTrigger, FileTickWatermark, MissedTickPolicy};

let trigger = CronTrigger::new("0 */5 * * * *")?
    .with_missed_tick_policy(MissedTickPolicy::CoalesceOne)
    .with_watermark(Arc::new(FileTickWatermark::new("/var/lib/my-agent/sweep.tick")));

FileTickWatermark stocke un curseur RFC 3339. Il écrit via un fichier temporaire frÚre unique, le synchronise, puis remplace atomiquement la destination sous Unix et Windows. Implémentez TickWatermark pour les autres magasins de sauvegarde.

Limiter une relecture

All selon une planification fréquente peut laisser des milliers de déclenchements en attente aprÚs une longue panne. with_max_catch_up limite le nombre de relectures effectuées en un seul passage (64 par défaut) ; une fois la limite atteinte, le reste de la lacune est abandonné, le curseur persistant avance au-delà de celle-ci et le déclencheur reprend au prochain déclenchement futur, en journalisant le nombre de déclenchements supprimés. Un redémarrage avant le prochain déclenchement ordinaire ne récupÚre pas le reste abandonné.

Contrat de livraison

Le filigrane avance lorsque le dĂ©clencheur Ă©met un dĂ©clenchement, et non lorsque le consommateur termine son traitement. Un plantage entre l’émission et l’achĂšvement supprime cette exĂ©cution au lieu de la rĂ©pĂ©ter — au plus une fois, et non au moins une fois. C’est ce qui empĂȘche un consommateur qui cesse d’interroger le systĂšme de rejouer la mĂȘme lacune Ă  chaque redĂ©marrage. Les consommateurs dont le travail doit survivre Ă  un plantage en cours d’exĂ©cution doivent enregistrer leur propre Ă©tat d’achĂšvement. Si un filigrane configurĂ© ne peut pas ĂȘtre persistĂ©, le flux cron s’arrĂȘte avant d’émettre l’évĂ©nement concernĂ©, plutĂŽt que d’affaiblir silencieusement cette garantie.

Déclencheurs webhook

Un webhook accessible constitue un point d’entrĂ©e distant vers la logique de l’application ; WebhookTrigger est donc dĂ©fini par dĂ©faut sur l’interface de bouclage et refuse de servir une adresse plus large sans vĂ©rificateur.

Développement local

use adk_agent::ambient::WebhookTrigger;

// Binds 127.0.0.1 — reachable only from this host.
let trigger = WebhookTrigger::new(8080, "/webhook");

Les écouteurs exposés nécessitent un vérificateur

use adk_agent::ambient::{WebhookRequest, WebhookTrigger, WebhookVerifier};
use std::net::SocketAddr;
use std::sync::Arc;

#[derive(Debug)]
struct SharedToken(String);

impl WebhookVerifier for SharedToken {
    fn verify(&self, request: &WebhookRequest<'_>) -> Result<String, String> {
        match request.header("x-webhook-token") {
            Some(value) if value == self.0 => Ok("ci-system".to_string()),
            Some(_) => Err("token mismatch".to_string()),
            None => Err("missing x-webhook-token".to_string()),
        }
    }
}

let address: SocketAddr = "0.0.0.0:8080".parse().unwrap();
let trigger = WebhookTrigger::new(8080, "/webhook")
    .with_bind_address(address)
    .with_verifier(Arc::new(SharedToken(std::env::var("WEBHOOK_TOKEN").unwrap_or_default())));

S’abonner Ă  une adresse qui n’est pas de bouclage local sans vĂ©rificateur Ă©choue avec agent.ambient.webhook_unauthenticated. La vĂ©rification a lieu au moment de l’abonnement, lorsque l’erreur est encore facile Ă  corriger.

Important : un vĂ©rificateur reçoit le corps brut via WebhookRequest::body, car les schĂ©mas de signature sont calculĂ©s sur les octets exacts reçus. VĂ©rifiez avant de faire confiance Ă  toute forme analysĂ©e de la requĂȘte.

Traitement des requĂȘtes

ConditionRĂ©ponseÉvĂ©nement
Corps dépassant la limite (1 MiB par défaut, with_max_body_bytes)413aucun
Le vérificateur rejette401aucun
Le corps n’est pas valide JSON400aucun
Le corps n’est pas valide JSON, accept_non_json() dĂ©fini200la charge utile est une chaĂźne JSON
L’abonnĂ© a disparu503aucun
Sinon200la charge utile est le JSON analysé

Un 401 ne contient aucun dĂ©tail sur la partie de l’identifiant qui a Ă©chouĂ© ; la raison est plutĂŽt enregistrĂ©e, de sorte que le point de terminaison ne peut pas ĂȘtre utilisĂ© pour sonder les identifiants.

accept_non_json est dĂ©sactivĂ© par dĂ©faut, car un corps malformĂ© encapsulĂ© en tant que chaĂźne produit un Ă©vĂ©nement de dĂ©clenchement impossible Ă  distinguer d’un Ă©vĂ©nement dĂ©libĂ©rĂ©.

Durée de vie

L’écouteur HTTP appartient au flux renvoyĂ© par subscribe. La suppression du flux arrĂȘte gracieusement le serveur et libĂšre le port, de sorte que le mĂȘme port peut ĂȘtre rĂ©affectĂ© :

let stream = trigger.subscribe().await?;
// ... consume events ...
drop(stream); // the listener stops and the port is free

Remarque : auparavant, l’écouteur survivait Ă  son consommateur. La suppression du flux laissait le serveur liĂ©, continuant d’accepter des requĂȘtes qu’il ne pouvait pas transmettre, et un redĂ©marrage sur le mĂȘme port Ă©chouait.

Agents ambiants - Documentation ADK-Rust | ADK-Rust