Événements

Les événements sont les éléments constitutifs fondamentaux de l'historique de conversation dans adk-rust. Chaque interaction avec un agent — qu'il s'agisse d'un message utilisateur, d'une réponse d'agent ou d'une exécution de Tool — est enregistrée comme un événement. Les événements forment un journal immuable qui capture la trace d'exécution complète d'une Session d'agent.

Aperçu

Le système d'événements remplit plusieurs objectifs critiques :

  • Historique de conversation : Les événements constituent l'enregistrement chronologique de toutes les interactions d'une Session
  • Gestion de l'état : Les événements transportent les changements d'état via le champ state_delta
  • Suivi des artefacts : Les événements enregistrent les opérations d'artefacts via le champ artifact_delta
  • Coordination des Agents : Les événements permettent les transferts et les escalades d'Agent
  • Débogage et Observabilité : Les événements fournissent une piste d'audit complète du comportement de l'Agent

Structure d'événement

Un Event représente une seule interaction dans une conversation. adk-rust utilise un type Event unifié qui intègre LlmResponse, correspondant au modèle de conception utilisé dans ADK-Go :

pub struct Event {
    pub id: String,                    // Unique event identifier (UUID)
    pub timestamp: DateTime<Utc>,      // When the event occurred
    pub invocation_id: String,         // Links related events in a single invocation
    pub branch: String,                // For future branching support
    pub author: String,                // Who created this event (user, agent name, tool name)
    pub llm_response: LlmResponse,     // Contains content and LLM metadata
    pub actions: EventActions,         // Side effects and metadata
    pub long_running_tool_ids: Vec<String>,  // IDs of long-running tools
}

La structure LlmResponse contient :

pub struct LlmResponse {
    pub content: Option<Content>,      // The message content (text, parts, etc.)
    pub usage_metadata: Option<UsageMetadata>,
    pub finish_reason: Option<FinishReason>,
    pub partial: bool,                 // True for streaming partial responses
    pub turn_complete: bool,           // True when the turn is complete
    pub interrupted: bool,             // True if generation was interrupted
    pub error_code: Option<String>,
    pub error_message: Option<String>,
}

Le Content est accédé de manière cohérente via event.llm_response.content :

if let Some(content) = &event.llm_response.content {
    for part in &content.parts {
        if let Part::Text { text } = part {
            println!("{}", text);
        }
    }
}

Champs Clés

  • id : Un UUID unique identifiant cet événement spécifique. Utilisé pour la récupération et l'ordonnancement des événements.

  • timestamp : L'horodatage UTC de la création de l'événement. Les événements sont classés chronologiquement dans la Session.

  • invocation_id : Regroupe les événements appartenant à la même invocation d'Agent. Lorsqu'un Agent traite un message, tous les événements générés (réponse de l'Agent, appels de Tool, appels de sous-Agent) partagent le même invocation_id.

  • branch : Réservé pour de futures fonctionnalités de branching. Actuellement inutilisé mais permet le branching de conversation dans les futures versions.

  • author : Identifie qui a créé l'événement :

    • Messages utilisateur : typiquement "user" ou un identifiant utilisateur
    • Réponses d'Agent : le nom de l'Agent
    • Exécutions de Tool : le nom du Tool
    • Événements système : "system"
  • llm_response : Contient le Content du message et les métadonnées LLM. Accédez au Content via event.llm_response.content. Le type Content peut contenir du texte, des multimodal Parts (images, audio) ou des données structurées. Certains événements (comme les mises à jour d'état pures) peuvent avoir content: None.

EventActions

La structure EventActions contient des métadonnées et des effets secondaires associés à un événement :

pub struct EventActions {
    pub state_delta: HashMap<String, Value>,    // State changes to apply
    pub artifact_delta: HashMap<String, i64>,   // Artifact version changes
    pub skip_summarization: bool,               // Skip this event in summaries
    pub transfer_to_agent: Option<String>,      // Transfer control to another agent
    pub escalate: bool,                         // Escalate to human or supervisor
}

state_delta

Le champ state_delta contient des paires clé-valeur représentant les modifications de l'état de la Session. Lorsqu'un événement est ajouté à une Session, ces modifications sont fusionnées dans l'état de la Session.

Les clés d'état peuvent utiliser des préfixes pour contrôler la portée :

  • app:key - État de portée application (partagé entre tous les utilisateurs)
  • user:key - État de portée utilisateur (partagé entre toutes les Sessions pour un utilisateur)
  • temp:key - État temporaire (effacé entre les invocations)
  • Pas de préfixe - État de portée Session (par défaut)

Exemple :

let mut actions = EventActions::default();
actions.state_delta.insert("user_name".to_string(), json!("Alice"));
actions.state_delta.insert("temp:current_step".to_string(), json!(3));

artifact_delta

Le champ artifact_delta suit les modifications des artifacts. Les clés sont les noms des artifacts, et les valeurs sont les numéros de version. Cela permet au système de suivre quels artifacts ont été créés ou modifiés pendant un événement.

Exemple :

actions.artifact_delta.insert("report.pdf".to_string(), 1);
actions.artifact_delta.insert("chart.png".to_string(), 2);

skip_summarization

Lorsque true, cet événement sera exclu des résumés de conversation. Utile pour les événements internes, les informations de débogage ou les sorties d'Agent verbeuses qui ne devraient pas faire partie du flux de conversation principal.

transfer_to_agent

Lorsqu'il est défini sur un nom d'Agent, le contrôle est transféré à cet Agent. Cela permet des workflows multi-Agent où un Agent peut passer le relais à un autre. L'Agent cible doit être configuré comme un sous-Agent.

Exemple :

actions.transfer_to_agent = Some("specialist_agent".to_string());

escalate

Lorsque true, signale que la conversation doit être escaladée à un opérateur humain ou à un Agent superviseur. Le comportement d'escalade spécifique dépend de l'implémentation de votre application.

Formation de l'historique de conversation

Les événements forment l'historique de conversation en s'accumulant dans l'ordre chronologique au sein d'une Session. Lorsqu'un Agent traite une requête :

  1. Événement de message utilisateur : Un nouvel événement est créé avec l'entrée de l'utilisateur
  2. Traitement par l'Agent : L'Agent reçoit l'historique de conversation (tous les événements précédents)
  3. Événement de réponse de l'Agent : La réponse de l'Agent est enregistrée comme un nouvel événement
  4. Événements d'exécution de Tool : Chaque appel de Tool peut générer des événements supplémentaires
  5. Mises à jour d'état : Les deltas d'état de tous les événements sont fusionnés dans l'état de la Session

L'historique de conversation est construit par :

  • La récupération de tous les événements de la Session dans l'ordre chronologique
  • La conversion du contenu de chaque événement dans le format approprié pour le LLM
  • L'inclusion des informations d'état des deltas d'état accumulés
  • Le filtrage des événements marqués skip_summarization lorsque cela est approprié

Exemple de flux d'événements

Début de Session
  ↓
[Event 1] Utilisateur : "Quel temps fait-il à Tokyo ?"
  ↓
[Event 2] Agent : "Laissez-moi vérifier pour vous."
  ↓
[Event 3] Tool (weather_api) : {"temp": 22, "condition": "sunny"}
  ↓
[Event 4] Agent : "Il fait 22°C et ensoleillé à Tokyo."
  ↓
État de Session mis à jour

Chaque événement s'appuie sur les précédents, créant une piste d'audit complète de la conversation.

Travailler avec les événements

Accéder aux événements depuis une Session

use adk_rust::session::{SessionService, GetRequest};

// Récupérer une session avec ses événements
let session = session_service.get(GetRequest {
    app_name: "my_app".to_string(),
    user_id: "user_123".to_string(),
    session_id: session_id.clone(),
    num_recent_events: None,  // Obtenir tous les événements
    after: None,
}).await?;

// Accéder aux événements
let events = session.events();
println!("Total events: {}", events.len());

// Itérer sur les événements (note : les événements de session utilisent llm_response.content)
for i in 0..events.len() {
    if let Some(event) = events.at(i) {
        println!("Event {}: {} by {} at {}",
            event.id,
            event.llm_response.content.as_ref().map(|_| "has content").unwrap_or("no content"),
            event.author,
            event.timestamp
        );
    }
}

Inspecter les détails d'un événement

// Obtenir un événement spécifique de la session (utilise llm_response.content)
if let Some(event) = events.at(0) {
    // Vérifier l'auteur
    println!("Author: {}", event.author);

    // Vérifier le contenu (les événements de session utilisent llm_response.content)
    if let Some(content) = &event.llm_response.content {
        for part in &content.parts {
            if let Part::Text { text } = part {
                println!("Text: {}", text);
            }
        }
    }

    // Vérifier les changements d'état
    if !event.actions.state_delta.is_empty() {
        println!("State changes:");
        for (key, value) in &event.actions.state_delta {
            println!("  {} = {}", key, value);
        }
    }

    // Vérifier les transferts d'Agent
    if let Some(target) = &event.actions.transfer_to_agent {
        println!("Transfers to: {}", target);
    }

    // Vérifier les artefacts
    if !event.actions.artifact_delta.is_empty() {
        println!("Artifacts modified:");
        for (name, version) in &event.actions.artifact_delta {
            println!("  {} (v{})", name, version);
        }
    }
}

Limiter l'historique des événements

Pour les longues conversations, vous pouvez souhaiter récupérer uniquement les événements récents :

// Obtenir uniquement les 10 derniers événements
let session = session_service.get(GetRequest {
    app_name: "my_app".to_string(),
    user_id: "user_123".to_string(),
    session_id: session_id.clone(),
    num_recent_events: Some(10),
    after: None,
}).await?;

Comment les événements circulent : Génération et Traitement

Comprendre comment les événements sont créés et traités aide à clarifier la façon dont le framework gère les actions et l'historique.

Sources de Génération

Les événements sont créés à différents moments du cycle de vie de l'exécution d'un agent :

  1. Entrée Utilisateur : Le Runner enveloppe les messages utilisateur dans un Event avec author = "user".
  2. Réponses des Agents : Les Agents produisent des objets Event (en définissant author = agent.name()) pour communiquer les réponses.
  3. Sortie du LLM : La couche d'intégration du modèle traduit la sortie du LLM (texte, appels de fonction) en objets Event.
  4. Résultats d'Outil : Après l'exécution d'un outil, le framework génère un Event contenant la réponse de l'outil.

Flux de Traitement

Lorsqu'un événement est généré, il suit ce chemin de traitement :

  1. Génération : Un événement est créé et produit par sa source (agent, outil ou gestionnaire d'entrée utilisateur).
  2. Le Runner Reçoit : Le Runner exécutant l'agent reçoit l'événement.
  3. Traitement par le SessionService : Le Runner envoie l'événement au SessionService, qui :
    • Applique les Deltas : Fusionne state_delta dans l'état de la session et met à jour les enregistrements d'artefacts.
    • Finalise les Métadonnées : Attribue un id unique s'il n'est pas présent, définit le timestamp.
    • Persiste dans l'Historique : Ajoute l'événement à session.events.
  4. Sortie du Flux : Le Runner produit l'événement traité vers l'application appelante.

Ce flux garantit que les changements d'état et l'historique sont enregistrés de manière cohérente parallèlement au contenu de la communication.

// Conceptual flow
User Input → Runner → Agent → LLM → Event Generated
                                         ↓
                                   SessionService
                                   - Apply state_delta
                                   - Record in history
                                         ↓
                                   Event Stream → Application

Identification des Types d'Événements

Lorsque vous traitez des événements provenant du Runner, vous voudrez identifier le type d'événement auquel vous avez affaire :

Par Auteur

Le champ author vous indique qui a créé l'événement :

match event.author.as_str() {
    "user" => println!("User input"),
    agent_name => println!("Response from agent: {}", agent_name),
}

Par Type de Contenu

Vérifiez le champ llm_response.content pour déterminer le type de charge utile :

if let Some(content) = &event.llm_response.content {
    // Check for text content
    let has_text = content.parts.iter().any(|part| {
        matches!(part, Part::Text { .. })
    });

    // Check for function calls (tool requests)
    let has_function_call = content.parts.iter().any(|part| {
        matches!(part, Part::FunctionCall { .. })
    });

    // Check for function responses (tool results)
    let has_function_response = content.parts.iter().any(|part| {
        matches!(part, Part::FunctionResponse { .. })
    });

    if has_text {
        println!("Text message");
    } else if has_function_call {
        println!("Tool call request");
    } else if has_function_response {
        println!("Tool result");
    }
}

Par Actions

Vérifiez le champ actions pour les signaux de contrôle et les effets secondaires :

// State changes
if !event.actions.state_delta.is_empty() {
    println!("Event contains state changes");
}

// Agent transfer
if let Some(target) = &event.actions.transfer_to_agent {
    println!("Transfer to agent: {}", target);
}

// Escalation signal
if event.actions.escalate {
    println!("Escalation requested");
}

// Skip summarization
if event.actions.skip_summarization {
    println!("Skip this event in summaries");
}

Travailler avec des flux d'événements

Lors de l'exécution d'un Agent, vous recevez un flux d'événements. Voici comment les traiter efficacement :

Traitement des événements depuis le Runner

use futures::StreamExt;

let mut stream = runner.run(
    "user_123".to_string(),
    "session_id".to_string(),
    user_input,
).await?;

while let Some(event_result) = stream.next().await {
    match event_result {
        Ok(event) => {
            // Process the event
            println!("Event from: {}", event.author);

            // Extract text content
            if let Some(content) = &event.llm_response.content {
                for part in &content.parts {
                    if let Part::Text { text } = part {
                        print!("{}", text);
                    }
                }
            }

            // Check for state changes
            if !event.actions.state_delta.is_empty() {
                println!("\nState updated: {:?}", event.actions.state_delta);
            }
        }
        Err(e) => {
            eprintln!("Error: {}", e);
            break;
        }
    }
}

Extraction des appels de fonctions

Lorsque le LLM demande un Tool, l'événement contient des informations d'appel de fonction :

if let Some(content) = &event.llm_response.content {
    for part in &content.parts {
        if let Part::FunctionCall { name, args } = part {
            println!("Tool requested: {}", name);
            println!("Arguments: {}", args);

            // Your application might dispatch tool execution here
            // based on the tool name and arguments
        }
    }
}

Extraction des réponses de fonction

Après l'exécution d'un Tool, le résultat est renvoyé dans une réponse de fonction :

if let Some(content) = &event.llm_response.content {
    for part in &content.parts {
        if let Part::FunctionResponse { function_response, .. } = part {
            println!("Tool result from: {}", function_response.name);
            println!("Response: {}", function_response.response);

            // Process the tool result
            // The LLM will use this to continue the conversation
        }
    }
}

Modèles d'événements courants

Voici les séquences d'événements typiques que vous rencontrerez :

Échange de texte simple

[Event 1] author="user", content=Text("Hello")
[Event 2] author="assistant", content=Text("Hi! How can I help?")

Flux d'utilisation de Tool

[Event 1] author="user", content=Text("What's the weather?")
[Event 2] author="assistant", content=FunctionCall(name="get_weather", args={...})
[Event 3] author="assistant", content=FunctionResponse(name="get_weather", response={...})
[Event 4] author="assistant", content=Text("It's sunny and 72°F")

Mise à jour d'état

[Event 1] author="assistant", content=Text("I've saved your preference")
           actions.state_delta={"user_theme": "dark"}

Transfert d'Agent

[Event 1] author="router", content=Text("Transferring to specialist")
           actions.transfer_to_agent=Some("specialist_agent")
[Event 2] author="specialist_agent", content=Text("I can help with that")

Métadonnées et identifiants d'événement

ID d'événement

Chaque événement possède un id unique (UUID) pour une identification précise :

println!("Event ID: {}", event.id);

ID d'invocation

L'invocation_id regroupe tous les événements, d'une seule requête utilisateur à la réponse finale :

// All events in one interaction share the same invocation_id
println!("Invocation: {}", event.invocation_id);

// Use this for logging and tracing
log::info!("Processing event {} in invocation {}", event.id, event.invocation_id);

Horodatage

Les événements sont horodatés pour un classement chronologique :

println!("Event occurred at: {}", event.timestamp.format("%Y-%m-%d %H:%M:%S"));

Bonnes Pratiques

  1. Immuabilité des événements : Les événements ne doivent jamais être modifiés après leur création. Ils constituent un journal d'audit immuable.

  2. Gestion de l'état : Utilisez state_delta pour toutes les modifications d'état plutôt que de modifier l'état directement. Cela garantit que les changements sont suivis dans le journal des événements.

  3. Auteurs significatifs : Définissez des noms d'auteur clairs et descriptifs pour faciliter la compréhension des journaux d'événements.

  4. Synthèse sélective : Utilisez skip_summarization pour les événements verbeux ou internes qui encombreraient l'historique de la conversation.

  5. Regroupement des invocations : Conservez le même invocation_id pour tous les événements générés lors d'une seule invocation d'agent afin de maintenir un regroupement logique.

  6. Suivi des artifacts : Mettez toujours à jour artifact_delta lors de la création ou de la modification d'artifacts pour maintenir la cohérence.

  7. Traitement des flux : Gérez toujours les erreurs lors du traitement des flux d'événements. Les événements peuvent échouer en raison d'erreurs de LLM, de défaillances de tools ou de problèmes réseau.

  8. Vérification du contenu : Vérifiez toujours si llm_response.content est Some avant d'accéder aux parts. Certains événements (comme les mises à jour d'état pures) peuvent ne pas avoir de contenu.

  9. Correspondance de motifs : Utilisez la correspondance de motifs de Rust pour gérer élégamment différents types d'événements et de parts de contenu.

  10. Journalisation : Utilisez invocation_id pour corréler tous les événements au sein d'une seule interaction utilisateur pour le débogage et l'observabilité.

Documentation connexe


Précédent : ← Artifacts | Suivant : Télémétrie →