Eventos

Los Events son los bloques de construcción fundamentales del historial de conversación en ADK-Rust. Cada interacción con un Agent —ya sea un mensaje de usuario, una respuesta de Agent o una ejecución de Tool— se registra como un Event. Los Events forman un registro inmutable que captura el seguimiento completo de la ejecución de una Session de Agent.

Descripción general

El sistema de Events cumple varios propósitos críticos:

  • Historial de conversación: Los Events forman el registro cronológico de todas las interacciones en una Session.
  • Gestión de estado: Los Events transportan los cambios de estado a través del campo state_delta.
  • Seguimiento de Artifacts: Los Events registran las operaciones de Artifacts a través del campo artifact_delta.
  • Coordinación de Agents: Los Events permiten transferencias y escaladas de Agents.
  • Depuración y observabilidad: Los Events proporcionan un registro de auditoría completo del comportamiento del Agent.

Estructura de Event

Un Event representa una única interacción en una conversación. ADK-Rust utiliza un tipo de Event unificado que incrusta LlmResponse, coincidiendo con el patrón de diseño utilizado en 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 estructura LlmResponse contiene:

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

Se accede a Content de forma consistente a través de 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);
        }
    }
}

Campos clave

  • id: Un UUID único que identifica este Event específico. Se utiliza para la recuperación y el ordenamiento de Events.

  • timestamp: La marca de tiempo UTC en que se creó el Event. Los Events se ordenan cronológicamente en la Session.

  • invocation_id: Agrupa los Events que pertenecen a la misma invocación de Agent. Cuando un Agent procesa un mensaje, todos los Events generados (respuesta del Agent, llamadas a Tool, llamadas a sub-Agent) comparten el mismo invocation_id.

  • branch: Reservado para futuras funcionalidades de ramificación. Actualmente no se utiliza, pero permite la ramificación de conversaciones en futuras versiones.

  • author: Identifica quién creó el Event:

    • Mensajes de usuario: típicamente "user" o un identificador de usuario.
    • Respuestas de Agent: el nombre del Agent.
    • Ejecuciones de Tool: el nombre de la Tool.
    • Events del sistema: "system".
  • llm_response: Contiene el Content del mensaje y los metadatos de LLM. Acceda al Content a través de event.llm_response.content. El tipo Content puede contener texto, partes multimodales (imágenes, audio) o datos estructurados. Algunos Events (como las actualizaciones de estado puro) pueden tener content: None.

Acciones de Evento

La estructura EventActions contiene metadatos y efectos secundarios asociados con un evento:

pub struct EventActions {
    pub state_delta: HashMap<String, Value>,    // Cambios de estado a aplicar
    pub artifact_delta: HashMap<String, i64>,   // Cambios de versión de artefacto
    pub skip_summarization: bool,               // Omitir este evento en los resúmenes
    pub transfer_to_agent: Option<String>,      // Transferir el control a otro agent
    pub escalate: bool,                         // Escalar a un humano o supervisor
}

state_delta

El campo state_delta contiene pares clave-valor que representan cambios en el estado de la sesión. Cuando un evento se añade a una sesión, estos cambios se fusionan en el estado de la sesión.

Las claves de estado pueden usar prefijos para controlar el alcance:

  • app:key - Estado con alcance de aplicación (compartido entre todos los usuarios)
  • user:key - Estado con alcance de usuario (compartido entre todas las sesiones para un usuario)
  • temp:key - Estado temporal (borrado entre invocaciones)
  • Sin prefijo - Estado con alcance de sesión (predeterminado)

Ejemplo:

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

El campo artifact_delta rastrea los cambios en los artefactos. Las claves son nombres de artefactos y los valores son números de versión. Esto permite al sistema rastrear qué artefactos fueron creados o modificados durante un evento.

Ejemplo:

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

skip_summarization

Cuando es true, este evento será excluido de los resúmenes de conversación. Útil para eventos internos, información de depuración o salidas verbosas de Tool que no deberían ser parte del flujo principal de la conversación.

transfer_to_agent

Cuando se establece en un nombre de agent, el control se transfiere a ese agent. Esto permite flujos de trabajo multi-agent donde un agent puede pasar el control a otro. El agent objetivo debe estar configurado como un sub-agent.

Ejemplo:

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

escalate

Cuando es true, indica que la conversación debe ser escalada a un operador humano o a un agent supervisor. El comportamiento específico de escalada depende de la implementación de tu aplicación.

Formación del Historial de Conversación

Los eventos forman el historial de conversación acumulándose en orden cronológico dentro de una sesión. Cuando un agent procesa una solicitud:

  1. Evento de Mensaje de Usuario: Se crea un nuevo evento con la entrada del usuario
  2. Procesamiento del Agent: El agent recibe el historial de conversación (todos los eventos previos)
  3. Evento de Respuesta del Agent: La respuesta del agent se registra como un nuevo evento
  4. Eventos de Ejecución de Tool: Cada llamada a Tool puede generar eventos adicionales
  5. Actualizaciones de Estado: Los deltas de estado de todos los eventos se fusionan en el estado de la sesión

El historial de conversación se construye mediante:

  • Recuperando todos los eventos de la sesión en orden cronológico
  • Convirtiendo el contenido de cada evento al formato apropiado para el LLM
  • Incluyendo información de estado de los deltas de estado acumulados
  • Filtrando eventos marcados con skip_summarization cuando sea apropiado

Ejemplo de Flujo de Eventos

Inicio de Sesión
  ↓
[Event 1] Usuario: "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."
  ↓
Estado de Sesión Actualizado

Cada evento se basa en los anteriores, creando un rastro de auditoría completo de la conversación.

Trabajando con Eventos

Accediendo a Eventos desde una Sesión

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

// Recuperar una sesión con sus eventos
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,  // Obtener todos los eventos
    after: None,
}).await?;

// Acceder a los eventos
let events = session.events();
println!("Total events: {}", events.len());

// Iterar a través de los eventos (nota: los eventos de sesión usan 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
        );
    }
}

Inspeccionando los Detalles del Evento

// Obtener un evento específico de la sesión (usa llm_response.content)
if let Some(event) = events.at(0) {
    // Verificar el autor
    println!("Author: {}", event.author);

    // Verificar el contenido (los eventos de sesión usan 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);
            }
        }
    }

    // Verificar cambios de estado
    if !event.actions.state_delta.is_empty() {
        println!("State changes:");
        for (key, value) in &event.actions.state_delta {
            println!("  {} = {}", key, value);
        }
    }

    // Verificar transferencias de Agent
    if let Some(target) = &event.actions.transfer_to_agent {
        println!("Transfers to: {}", target);
    }

    // Verificar artefactos
    if !event.actions.artifact_delta.is_empty() {
        println!("Artifacts modified:");
        for (name, version) in &event.actions.artifact_delta {
            println!("  {} (v{})", name, version);
        }
    }
}

Limitando el Historial de Eventos

Para conversaciones largas, es posible que desee recuperar solo los eventos recientes:

// Obtener solo los últimos 10 eventos
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?;

Cómo Fluyen los Eventos: Generación y Procesamiento

Comprender cómo se crean y procesan los eventos ayuda a clarificar cómo el framework gestiona las acciones y el historial.

Fuentes de Generación

Los eventos se crean en diferentes puntos del ciclo de vida de ejecución del Agent:

  1. Entrada de Usuario: el Runner envuelve los mensajes del usuario en un Event con author = "user"
  2. Respuestas del Agent: los Agents producen objetos Event (estableciendo author = agent.name()) para comunicar respuestas
  3. Salida del LLM: la capa de integración del modelo traduce la salida del LLM (texto, llamadas a funciones) en objetos Event
  4. Resultados de Tool: Después de la ejecución de la Tool, el framework genera un Event que contiene la respuesta de la Tool

Flujo de Procesamiento

Cuando se genera un evento, sigue esta ruta de procesamiento:

  1. Generación: Un evento es creado y producido por su fuente (Agent, Tool o manejador de entrada de usuario)
  2. Runner Recibe: el Runner que ejecuta el Agent recibe el evento
  3. Procesamiento de SessionService: el Runner envía el evento al SessionService, que:
    • Aplica Deltas: Fusiona state_delta en el estado de la Session y actualiza los registros de artefactos
    • Finaliza Metadatos: Asigna un id único si no está presente, establece timestamp
    • Persiste en el Historial: Añade el evento a session.events
  4. Salida del Stream: el Runner produce el evento procesado a la aplicación que realiza la llamada

Este flujo asegura que los cambios de estado y el historial se registren consistentemente junto con el contenido de la comunicación.

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

Identificando Tipos de Eventos

Al procesar eventos del Runner, querrás identificar con qué tipo de evento estás tratando:

Por Autor

El campo author te dice quién creó el evento:

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

Por Tipo de Contenido

Verifica el campo llm_response.content para determinar el tipo de payload:

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

Por Acciones

Verifica el campo actions en busca de señales de control y efectos secundarios:

// 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");
}

Trabajar con flujos de eventos

Al ejecutar un Agent, recibes un flujo de eventos. Así es como puedes procesarlos eficazmente:

Procesamiento de eventos desde 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;
        }
    }
}

Extracción de llamadas a funciones

Cuando el LLM solicita una Tool, el evento contiene información de la llamada a la función:

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

Extracción de respuestas de funciones

Después de que una Tool se ejecuta, el resultado se devuelve en una respuesta de función:

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

Patrones de eventos comunes

Aquí están las secuencias de eventos típicas que encontrarás:

Intercambio de texto simple

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

Flujo de uso de Tools

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

Actualización de estado

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

Transferencia de 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")

Metadatos e identificadores de eventos

Event ID

Cada evento tiene un id único (UUID) para una identificación precisa:

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

Invocation ID

El invocation_id agrupa todos los eventos de una única solicitud de usuario hasta la respuesta final:

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

Timestamp

Los eventos están marcados con un timestamp para un orden cronológico:

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

Mejores Prácticas

  1. Inmutabilidad de Eventos: Los eventos nunca deben modificarse después de su creación. Forman un registro de auditoría inmutable.

  2. Gestión de Estado: Use state_delta para todos los cambios de estado en lugar de modificar el estado directamente. Esto asegura que los cambios se registren en el log de eventos.

  3. Autores Significativos: Establezca nombres de autor claros y descriptivos para facilitar la comprensión de los logs de eventos.

  4. Resumen Selectivo: Use skip_summarization para eventos detallados o internos que saturarían el historial de conversación.

  5. Agrupación de Invocaciones: Mantenga el mismo invocation_id para todos los eventos generados durante una única invocación del Agent para mantener la agrupación lógica.

  6. Seguimiento de Artifacts: Siempre actualice artifact_delta al crear o modificar Artifacts para mantener la consistencia.

  7. Procesamiento de Flujos: Siempre maneje los errores al procesar flujos de eventos. Los eventos pueden fallar debido a errores del LLM, fallos de Tool o problemas de red.

  8. Verificación de Contenido: Siempre verifique si llm_response.content es Some antes de acceder a los Parts. Algunos eventos (como actualizaciones de estado puras) pueden no tener contenido.

  9. Coincidencia de Patrones: Use la coincidencia de patrones de Rust para manejar elegantemente diferentes tipos de eventos y Parts de contenido.

  10. Registro: Use invocation_id para correlacionar todos los eventos dentro de una única interacción de usuario para la depuración y la observabilidad.

Documentación Relacionada


Anterior: ← Artifacts | Siguiente: Telemetría →