Ereignisse

Events sind die fundamentalen Bausteine der Konversationshistorie in ADK-Rust. Jede Interaktion mit einem Agenten – sei es eine Benutzernachricht, eine Agentenantwort oder eine Tool-Ausführung – wird als ein Event aufgezeichnet. Events bilden ein unveränderliches Protokoll, das die vollständige Ausführungsverfolgung einer Agenten-Session festhält.

Übersicht

Das Event-System dient mehreren wichtigen Zwecken:

  • Konversationshistorie: Events bilden die chronologische Aufzeichnung aller Interaktionen in einer Session
  • Zustandsverwaltung: Events übermitteln Zustandsänderungen über das state_delta-Feld
  • Artefakt-Verfolgung: Events zeichnen Artefakt-Operationen über das artifact_delta-Feld auf
  • Agenten-Koordination: Events ermöglichen Agenten-Transfers und Eskalationen
  • Debugging & Observability: Events bieten eine vollständige Prüfspur des Agentenverhaltens

Event-Struktur

Ein Event repräsentiert eine einzelne Interaktion in einer Konversation. ADK-Rust verwendet einen vereinheitlichten Event-Typ, der LlmResponse einbettet, entsprechend dem in ADK-Go verwendeten Designmuster:

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
}

Die LlmResponse-Struktur enthält:

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>,
}

Auf den Content wird konsistent über event.llm_response.content zugegriffen:

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

Schlüsselfelder

  • id: Eine eindeutige UUID, die dieses spezifische Event identifiziert. Wird zum Abrufen und Sortieren von Events verwendet.

  • timestamp: Der UTC-Zeitstempel, wann das Event erstellt wurde. Events werden in der Session chronologisch geordnet.

  • invocation_id: Gruppiert Events, die zur selben Agenten-Invocation gehören. Wenn ein Agent eine Nachricht verarbeitet, teilen alle generierten Events (Agentenantwort, Tool-Aufrufe, Sub-Agenten-Aufrufe) dieselbe invocation_id.

  • branch: Reserviert für zukünftige Branching-Funktionalität. Derzeit ungenutzt, ermöglicht aber Konversations-Branching in zukünftigen Versionen.

  • author: Identifiziert, wer das Event erstellt hat:

    • Benutzernachrichten: typischerweise "user" oder ein Benutzer-Identifikator
    • Agentenantworten: der Name des Agenten
    • Tool-Ausführungen: der Name des Tools
    • System-Events: "system"
  • llm_response: Enthält den Nachrichten-Content und LLM-Metadaten. Greifen Sie auf den Content über event.llm_response.content zu. Der Content-Typ kann Text, multimodale Parts (Bilder, Audio) oder strukturierte Daten enthalten. Einige Events (wie reine Zustandsaktualisierungen) können content: None haben.

EventActions

Das EventActions-Struct enthält Metadaten und Nebeneffekte, die mit einem Ereignis verbunden sind:

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

Das state_delta-Feld enthält Schlüssel-Wert-Paare, die Änderungen am Session-State darstellen. Wenn ein Event zu einer Session hinzugefügt wird, werden diese Änderungen in den State der Session zusammengeführt.

State-Schlüssel können Präfixe verwenden, um den Geltungsbereich zu steuern:

  • app:key - App-bezogener State (wird von allen Benutzern geteilt)
  • user:key - Benutzerbezogener State (wird von allen Sessions für einen Benutzer geteilt)
  • temp:key - Temporärer State (wird zwischen Aufrufen gelöscht)
  • Kein Präfix - Session-bezogener State (Standard)

Example:

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

Das artifact_delta-Feld verfolgt Änderungen an Artifacts. Schlüssel sind Artifact-Namen und Werte sind Versionsnummern. Dies ermöglicht dem System, zu verfolgen, welche Artifacts während eines Events erstellt oder modifiziert wurden.

Example:

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

skip_summarization

Wenn true, wird dieses Event aus Gesprächszusammenfassungen ausgeschlossen. Nützlich für interne Events, Debugging-Informationen oder ausführliche Tool-Ausgaben, die nicht Teil des Hauptgesprächsflusses sein sollten.

transfer_to_agent

Wenn auf einen agent-Namen gesetzt, wird die Kontrolle an diesen agent übertragen. Dies ermöglicht Multi-agent-Workflows, bei denen ein agent die Kontrolle an einen anderen übergeben kann. Der Ziel-agent muss als Sub-agent konfiguriert sein.

Example:

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

escalate

Wenn true, signalisiert dies, dass das Gespräch an einen menschlichen Operator oder Supervisor-agent eskaliert werden sollte. Das spezifische Eskalationsverhalten hängt von der Implementierung Ihrer Anwendung ab.

Conversation History Formation

Events bilden die Gesprächshistorie, indem sie in chronologischer Reihenfolge innerhalb einer Session akkumuliert werden. Wenn ein agent eine Anfrage bearbeitet:

  1. User Message Event: Ein neues Event wird mit der Benutzereingabe erstellt
  2. Agent Processing: Der agent empfängt die Gesprächshistorie (alle vorherigen Events)
  3. Agent Response Event: Die Antwort des agents wird als neues Event aufgezeichnet
  4. Tool Execution Events: Jeder Tool-Aufruf kann zusätzliche Events generieren
  5. State Updates: State-Deltas von allen Events werden in den Session-State zusammengeführt

Die Gesprächshistorie wird konstruiert durch:

  • Abrufen aller Events aus der Session in chronologischer Reihenfolge
  • Konvertieren des Inhalts jedes Events in das entsprechende Format für den LLM
  • Einbeziehung von State-Informationen aus akkumulierten State-Deltas
  • Herausfiltern von Events, die mit skip_summarization markiert sind, wenn angemessen

Event Flow Example

Session Start
  ↓
[Event 1] User: "What's the weather in Tokyo?"
  ↓
[Event 2] Agent: "Let me check that for you."
  ↓
[Event 3] Tool (weather_api): {"temp": 22, "condition": "sunny"}
  ↓
[Event 4] Agent: "It's 22°C and sunny in Tokyo."
  ↓
Session State Updated

Jedes Event baut auf den vorherigen auf und erstellt einen vollständigen Audit-Trail des Gesprächs.

Arbeiten mit Events

Zugriff auf Events aus einer Session

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

// Eine Session mit ihren Events abrufen
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,  // Alle Events abrufen
    after: None,
}).await?;

// Auf die Events zugreifen
let events = session.events();
println!("Total events: {}", events.len());

// Events durchlaufen (Hinweis: Session Events verwenden 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
        );
    }
}

Event-Details prüfen

// Ein bestimmtes Event aus der Session abrufen (verwendet llm_response.content)
if let Some(event) = events.at(0) {
    // Den Autor prüfen
    println!("Author: {}", event.author);

    // Inhalt prüfen (Session Events verwenden 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);
            }
        }
    }

    // Auf Statusänderungen prüfen
    if !event.actions.state_delta.is_empty() {
        println!("State changes:");
        for (key, value) in &event.actions.state_delta {
            println!("  {} = {}", key, value);
        }
    }

    // Auf Agent-Transfers prüfen
    if let Some(target) = &event.actions.transfer_to_agent {
        println!("Transfers to: {}", target);
    }

    // Auf Artefakte prüfen
    if !event.actions.artifact_delta.is_empty() {
        println!("Artifacts modified:");
        for (name, version) in &event.actions.artifact_delta {
            println!("  {} (v{})", name, version);
        }
    }
}

Event-Historie begrenzen

Für lange Unterhaltungen möchten Sie möglicherweise nur die neuesten Events abrufen:

// Nur die letzten 10 Events abrufen
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?;

Wie Ereignisse fließen: Generierung und Verarbeitung

Das Verständnis, wie Ereignisse erstellt und verarbeitet werden, hilft zu klären, wie das Framework Aktionen und den Verlauf verwaltet.

Generierungsquellen

Ereignisse werden an verschiedenen Punkten im Lebenszyklus der Agenten-Ausführung erstellt:

  1. Benutzereingabe: Der Runner verpackt Benutzernachrichten in ein Event mit author = "user"
  2. Agenten-Antworten: Agents erzeugen Event-Objekte (setzen author = agent.name()), um Antworten zu kommunizieren
  3. LLM Output: Die Modellintegrationsschicht übersetzt LLM output (Text, Funktionsaufrufe) in Event-Objekte
  4. Tool Results: Nach der Tool-Ausführung generiert das Framework ein Event, das die Tool-Antwort enthält

Verarbeitungsfluss

Wenn ein Ereignis generiert wird, folgt es diesem Verarbeitungspfad:

  1. Generierung: Ein Ereignis wird von seiner Quelle (Agent, Tool oder Benutzereingabe-Handler) erstellt und ausgegeben
  2. Runner empfängt: Der Runner, der den Agent ausführt, empfängt das Ereignis
  3. SessionService Verarbeitung: Der Runner sendet das Ereignis an den SessionService, der:
    • Deltas anwenden: state_delta in den Session State zusammenführt und Artefakt-Aufzeichnungen aktualisiert
    • Metadaten finalisieren: Eine eindeutige id zuweist, falls nicht vorhanden, timestamp setzt
    • Im Verlauf speichern: Das Ereignis an session.events anhängt
  4. Stream-Ausgabe: Der Runner gibt das verarbeitete Ereignis an die aufrufende Anwendung aus

Dieser Fluss stellt sicher, dass Statusänderungen und der Verlauf konsistent zusammen mit dem Kommunikationsinhalt aufgezeichnet werden.

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

Ereignistypen identifizieren

Beim Verarbeiten von Ereignissen vom Runner möchten Sie identifizieren, um welchen Ereignistyp es sich handelt:

Nach Autor

Das Feld author gibt an, wer das Ereignis erstellt hat:

match event.author.as_str() {
    "user" => println!("Benutzereingabe"),
    agent_name => println!("Antwort vom Agent: {}", agent_name),
}

Nach Inhaltstyp

Überprüfen Sie das Feld llm_response.content, um den Payload-Typ zu bestimmen:

if let Some(content) = &event.llm_response.content {
    // Auf Textinhalt prüfen
    let has_text = content.parts.iter().any(|part| {
        matches!(part, Part::Text { .. })
    });

    // Auf Funktionsaufrufe (Tool-Anfragen) prüfen
    let has_function_call = content.parts.iter().any(|part| {
        matches!(part, Part::FunctionCall { .. })
    });

    // Auf Funktionsantworten (Tool-Ergebnisse) prüfen
    let has_function_response = content.parts.iter().any(|part| {
        matches!(part, Part::FunctionResponse { .. })
    });

    if has_text {
        println!("Textnachricht");
    } else if has_function_call {
        println!("Tool-Aufrufanfrage");
    } else if has_function_response {
        println!("Tool-Ergebnis");
    }
}

Nach Aktionen

Überprüfen Sie das Feld actions auf Steuerungssignale und Nebenwirkungen:

// Statusänderungen
if !event.actions.state_delta.is_empty() {
    println!("Ereignis enthält Statusänderungen");
}

// Agenten-Transfer
if let Some(target) = &event.actions.transfer_to_agent {
    println!("Transfer zum Agent: {}", target);
}

// Eskalationssignal
if event.actions.escalate {
    println!("Eskalation angefordert");
}

// Zusammenfassung überspringen
if event.actions.skip_summarization {
    println!("Dieses Ereignis in Zusammenfassungen überspringen");
}

Arbeiten mit Ereignisströmen

Beim Ausführen eines Agents erhalten Sie einen Ereignisstrom. So verarbeiten Sie diese effektiv:

Verarbeiten von Ereignissen vom 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) => {
            // Ereignis verarbeiten
            println!("Event from: {}", event.author);

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

            // Auf Zustandsänderungen prüfen
            if !event.actions.state_delta.is_empty() {
                println!("\nState updated: {:?}", event.actions.state_delta);
            }
        }
        Err(e) => {
            eprintln!("Error: {}", e);
            break;
        }
    }
}

Extrahieren von Funktionsaufrufen

Wenn das LLM ein Tool anfordert, enthält das Ereignis Informationen zum Funktionsaufruf:

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);

            // Ihre Anwendung könnte hier die Tool-Ausführung auslösen
            // basierend auf dem Tool-Namen und den Argumenten
        }
    }
}

Extrahieren von Funktionsantworten

Nachdem ein Tool ausgeführt wurde, wird das Ergebnis in einer Funktionsantwort zurückgegeben:

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);

            // Das Tool-Ergebnis verarbeiten
            // Das LLM wird dies nutzen, um die Konversation fortzusetzen
        }
    }
}

Häufige Ereignismuster

Hier sind typische Ereignissequenzen, denen Sie begegnen werden:

Einfacher Textaustausch

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

Tool-Nutzungsfluss

[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")

Zustandsaktualisierung

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

Agentenübertragung

[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")

Ereignis-Metadaten und -Identifikatoren

Ereignis-ID

Jedes Ereignis hat eine eindeutige id (UUID) zur präzisen Identifizierung:

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

Aufruf-ID

Die invocation_id gruppiert alle Ereignisse von einer einzelnen Benutzeranfrage bis zur endgültigen Antwort:

// Alle Ereignisse in einer Interaktion teilen sich dieselbe invocation_id
println!("Invocation: {}", event.invocation_id);

// Dies für die Protokollierung und Nachverfolgung verwenden
log::info!("Processing event {} in invocation {}", event.id, event.invocation_id);

Zeitstempel

Ereignisse werden für die chronologische Reihenfolge mit einem Zeitstempel versehen:

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

Bewährte Praktiken

  1. Event Immutability: Ereignisse sollten niemals nach ihrer Erstellung geändert werden. Sie bilden ein unveränderliches Prüfprotokoll.

  2. State Management: Verwenden Sie state_delta für alle Zustandsänderungen, anstatt den Zustand direkt zu ändern. Dies stellt sicher, dass Änderungen im Ereignisprotokoll verfolgt werden.

  3. Meaningful Authors: Verwenden Sie klare, beschreibende Autorennamen, um Ereignisprotokolle leichter verständlich zu machen.

  4. Selective Summarization: Verwenden Sie skip_summarization für ausführliche oder interne Ereignisse, die den Konversationsverlauf überladen würden.

  5. Invocation Grouping: Behalten Sie dieselbe invocation_id für alle Ereignisse bei, die während einer einzelnen Agent-Aufrufs generiert werden, um eine logische Gruppierung aufrechtzuerhalten.

  6. Artifact Tracking: Aktualisieren Sie immer artifact_delta, wenn Sie artifacts erstellen oder ändern, um die Konsistenz zu wahren.

  7. Stream Processing: Behandeln Sie immer Fehler bei der Verarbeitung von Ereignisströmen. Ereignisse können aufgrund von LLM-Fehlern, Tool-Fehlern oder Netzwerkproblemen fehlschlagen.

  8. Content Checking: Überprüfen Sie immer, ob llm_response.content den Wert Some hat, bevor Sie auf Parts zugreifen. Einige Ereignisse (wie reine Zustandsaktualisierungen) haben möglicherweise keinen Content.

  9. Pattern Matching: Verwenden Sie Rusts pattern matching, um verschiedene Ereignistypen und Content-Parts elegant zu behandeln.

  10. Logging: Verwenden Sie invocation_id, um alle Ereignisse innerhalb einer einzelnen Benutzerinteraktion für die Fehlersuche und Observability zu korrelieren.

Zugehörige Dokumentation


Vorherige: ← Artifacts | Nächste: Telemetry →