Eventos

Eventos são os blocos de construção fundamentais do histórico de conversas no ADK-Rust. Cada interação com um Agent — seja uma mensagem de usuário, uma resposta do Agent ou uma execução de Tool — é registrada como um Event. Eventos formam um log imutável que captura o rastreamento de execução completo de uma Session do Agent.

Visão Geral

O sistema de Eventos serve a vários propósitos críticos:

  • Histórico da Conversa: Eventos formam o registro cronológico de todas as interações em uma Session
  • Gerenciamento de Estado: Eventos carregam mudanças de estado através do campo state_delta
  • Rastreamento de Artefatos: Eventos registram operações de artefatos através do campo artifact_delta
  • Coordenação de Agentes: Eventos permitem transferências e escalonamentos de Agent
  • Depuração e Observabilidade: Eventos fornecem uma trilha de auditoria completa do comportamento do Agent

Estrutura do Evento

Um Event representa uma única interação em uma conversa. O ADK-Rust usa um tipo de Event unificado que incorpora LlmResponse, combinando com o padrão de design usado no ADK-Go:

pub struct Event {
    pub id: String,                    // Identificador único do evento (UUID)
    pub timestamp: DateTime<Utc>,      // Quando o evento ocorreu
    pub invocation_id: String,         // Vincula eventos relacionados em uma única invocação
    pub branch: String,                // Para futuro suporte a ramificação
    pub author: String,                // Quem criou este evento (usuário, nome do agente, nome da ferramenta)
    pub llm_response: LlmResponse,     // Contém o conteúdo e metadados do LLM
    pub actions: EventActions,         // Efeitos colaterais e metadados
    pub long_running_tool_ids: Vec<String>,  // IDs de ferramentas de longa duração
}

A struct LlmResponse contém:

pub struct LlmResponse {
    pub content: Option<Content>,      // O conteúdo da mensagem (texto, partes, etc.)
    pub usage_metadata: Option<UsageMetadata>,
    pub finish_reason: Option<FinishReason>,
    pub partial: bool,                 // Verdadeiro para respostas parciais de streaming
    pub turn_complete: bool,           // Verdadeiro quando o turno está completo
    pub interrupted: bool,             // Verdadeiro se a geração foi interrompida
    pub error_code: Option<String>,
    pub error_message: Option<String>,
}

O Content é acessado consistentemente 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);
        }
    }
}

Campos Chave

  • id: Um UUID único identificando este Event específico. Usado para recuperação e ordenação de Eventos.

  • timestamp: O timestamp UTC de quando o Event foi criado. Os Eventos são ordenados cronologicamente na Session.

  • invocation_id: Agrupa Eventos que pertencem à mesma invocação de Agent. Quando um Agent processa uma mensagem, todos os Eventos gerados (resposta do Agent, chamadas de Tool, chamadas de sub-Agent) compartilham o mesmo invocation_id.

  • branch: Reservado para funcionalidade futura de ramificação. Atualmente não utilizado, mas permite ramificação de conversa em futuras versões.

  • author: Identifica quem criou o Event:

    • Mensagens de usuário: tipicamente "user" ou um identificador de usuário
    • Respostas de Agent: o nome do Agent
    • Execuções de Tool: o nome da Tool
    • Eventos de sistema: "system"
  • llm_response: Contém o Content da mensagem e metadados do LLM. Acesse o Content via event.llm_response.content. O tipo Content pode conter texto, partes multimodais (imagens, áudio) ou dados estruturados. Alguns Eventos (como atualizações puras de estado) podem ter content: None.

EventActions

A struct EventActions contém metadados e efeitos colaterais associados a um evento:

pub struct EventActions {
    pub state_delta: HashMap<String, Value>,    // Alterações de estado a serem aplicadas
    pub artifact_delta: HashMap<String, i64>,   // Alterações de versão do artefato
    pub skip_summarization: bool,               // Ignorar este evento em resumos
    pub transfer_to_agent: Option<String>,      // Transferir controle para outro Agent
    pub escalate: bool,                         // Escalonar para humano ou supervisor
}

state_delta

O campo state_delta contém pares chave-valor representando alterações no estado da Session. Quando um evento é anexado a uma Session, essas alterações são mescladas no estado da Session.

As chaves de estado podem usar prefixos para controlar o escopo:

  • app:key - Estado com escopo de aplicação (compartilhado entre todos os usuários)
  • user:key - Estado com escopo de usuário (compartilhado entre todas as Sessions para um usuário)
  • temp:key - Estado temporário (limpo entre invocações)
  • Sem prefixo - Estado com escopo de Session (padrão)

Exemplo:

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

O campo artifact_delta rastreia alterações em artefatos. As chaves são nomes de artefatos e os valores são números de versão. Isso permite que o sistema rastreie quais artefatos foram criados ou modificados durante um evento.

Exemplo:

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

skip_summarization

Quando true, este evento será excluído dos resumos de conversas. Útil para eventos internos, informações de depuração ou saídas de Tool verbosas que não deveriam fazer parte do fluxo principal da conversa.

transfer_to_agent

Quando definido com um nome de Agent, o controle é transferido para esse Agent. Isso permite fluxos de trabalho multi-Agent onde um Agent pode passar a tarefa para outro. O Agent de destino deve ser configurado como um sub-Agent.

Exemplo:

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

escalate

Quando true, sinaliza que a conversa deve ser escalonada para um operador humano ou um Agent supervisor. O comportamento de escalonamento específico depende da implementação da sua aplicação.

Formação do Histórico de Conversa

Eventos formam o histórico da conversa acumulando-se em ordem cronológica dentro de uma Session. Quando um Agent processa uma requisição:

  1. Evento de Mensagem do Usuário: Um novo evento é criado com a entrada do usuário
  2. Processamento pelo Agent: O Agent recebe o histórico da conversa (todos os eventos anteriores)
  3. Evento de Resposta do Agent: A resposta do Agent é registrada como um novo evento
  4. Eventos de Execução de Tool: Cada chamada de Tool pode gerar eventos adicionais
  5. Atualizações de Estado: state_delta de todos os eventos são mesclados no estado da Session

O histórico da conversa é construído por:

  • Recuperar todos os eventos da Session em ordem cronológica
  • Converter o conteúdo de cada evento para o formato apropriado para o LLM
  • Incluir informações de estado de state_delta acumulados
  • Filtrar eventos marcados com skip_summarization quando apropriado

Exemplo de Fluxo de Eventos

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

Cada evento se baseia nos anteriores, criando um registro de auditoria completo da conversa.

Trabalhando com Eventos

Acessando Eventos de uma Session

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

// Recuperar uma session com seus 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,  // Obter todos os eventos
    after: None,
}).await?;

// Acessar os eventos
let events = session.events();
println!("Total events: {}", events.len());

// Iterar pelos eventos (nota: eventos da session usam 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
        );
    }
}

Inspecionando Detalhes do Evento

// Obter um evento específico da session (usa llm_response.content)
if let Some(event) = events.at(0) {
    // Verificar o autor
    println!("Author: {}", event.author);

    // Verificar conteúdo (eventos da session usam 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 alterações de estado
    if !event.actions.state_delta.is_empty() {
        println!("State changes:");
        for (key, value) in &event.actions.state_delta {
            println!("  {} = {}", key, value);
        }
    }

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

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

Limitando o Histórico de Eventos

Para conversas longas, você pode querer recuperar apenas eventos recentes:

// Obter apenas os ú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?;

Como os Eventos Fluem: Geração e Processamento

Compreender como os eventos são criados e processados ajuda a esclarecer como o framework gerencia ações e histórico.

Fontes de Geração

Os eventos são criados em diferentes pontos do ciclo de vida de execução do agent:

  1. Entrada do Usuário: O Runner encapsula mensagens do usuário em um Event com author = "user"
  2. Respostas do Agent: Agents retornam (yield) objetos Event (definindo author = agent.name()) para comunicar respostas
  3. Saída do LLM: A camada de integração do modelo traduz a saída do LLM (texto, chamadas de função) em objetos Event
  4. Resultados da Tool: Após a execução da tool, o framework gera um Event contendo a resposta da tool

Fluxo de Processamento

Quando um evento é gerado, ele segue este caminho de processamento:

  1. Geração: Um evento é criado e retornado (yielded) por sua fonte (agent, tool, ou manipulador de entrada do usuário)
  2. Runner Recebe: O Runner executando o agent recebe o evento
  3. Processamento do SessionService: O Runner envia o evento para o SessionService, que:
    • Aplica Deltas: Mescla state_delta no estado da sessão e atualiza os registros de artefatos
    • Finaliza Metadados: Atribui um id único se não presente, define timestamp
    • Persiste no Histórico: Adiciona o evento a session.events
  4. Saída de Stream: O Runner retorna (yields) o evento processado para a aplicação chamadora

Este fluxo garante que as mudanças de estado e o histórico sejam consistentemente registrados juntamente com o conteúdo da comunicação.

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

Identificando Tipos de Eventos

Ao processar eventos do Runner, você vai querer identificar o tipo de evento com o qual está lidando:

Por Autor

O campo author informa quem criou o evento:

match event.author.as_str() {
    "user" => println!("Entrada do usuário"),
    agent_name => println!("Resposta do agent: {}", agent_name),
}

Por Tipo de Conteúdo

Verifique o campo llm_response.content para determinar o 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!("Mensagem de texto");
    } else if has_function_call {
        println!("Requisição de chamada de tool");
    } else if has_function_response {
        println!("Resultado da tool");
    }
}

Por Ações

Verifique o campo actions para sinais de controle e efeitos colaterais:

// Mudanças de estado
if !event.actions.state_delta.is_empty() {
    println!("Evento contém mudanças de estado");
}

// Transferência de agent
if let Some(target) = &event.actions.transfer_to_agent {
    println!("Transferir para agent: {}", target);
}

// Sinal de escalonamento
if event.actions.escalate {
    println!("Escalonamento solicitado");
}

// Pular sumarização
if event.actions.skip_summarization {
    println!("Pular este evento nos resumos");
}

Trabalhando com Fluxos de Eventos

Ao executar um Agent, você recebe um fluxo de eventos. Veja como processá-los de forma eficaz:

Processando Eventos do 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) => {
            // Processa o evento
            println!("Event from: {}", event.author);

            // Extrai o conteúdo de texto
            if let Some(content) = &event.llm_response.content {
                for part in &content.parts {
                    if let Part::Text { text } = part {
                        print!("{}", text);
                    }
                }
            }

            // Verifica por mudanças de estado
            if !event.actions.state_delta.is_empty() {
                println!("\nState updated: {:?}", event.actions.state_delta);
            }
        }
        Err(e) => {
            eprintln!("Error: {}", e);
            break;
        }
    }
}

Extraindo Chamadas de Função

Quando o LLM solicita uma Tool, o evento contém informações de FunctionCall:

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

            // Sua aplicação pode despachar a execução da Tool aqui
            // com base no nome e argumentos da Tool
        }
    }
}

Extraindo Respostas de Função

Após a execução de uma Tool, o resultado é retornado em uma FunctionResponse:

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

            // Processa o resultado da Tool
            // O LLM usará isso para continuar a conversa
        }
    }
}

Padrões Comuns de Eventos

Aqui estão sequências típicas de eventos que você encontrará:

Troca Simples de Texto

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

Fluxo de Uso 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")

Atualização de Estado

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

Transferência 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")

Metadados e Identificadores de Eventos

ID do Evento

Cada evento possui um id (UUID) único para identificação precisa:

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

ID de Invocação

O invocation_id agrupa todos os eventos de uma única solicitação do usuário até a resposta final:

// Todos os eventos em uma interação compartilham o mesmo invocation_id
println!("Invocation: {}", event.invocation_id);

// Use isso para registro (logging) e rastreamento (tracing)
log::info!("Processing event {} in invocation {}", event.id, event.invocation_id);

Timestamp

Os eventos são carimbados com um timestamp para ordenação cronológica:

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

Melhores Práticas

  1. Imutabilidade de Eventos: Eventos nunca devem ser modificados após a criação. Eles formam um registro de auditoria imutável.

  2. Gerenciamento de Estado: Use state_delta para todas as mudanças de estado, em vez de modificar o estado diretamente. Isso garante que as mudanças sejam rastreadas no log de eventos.

  3. Autores Significativos: Defina nomes de autor claros e descritivos para facilitar a compreensão dos logs de eventos.

  4. Resumo Seletivo: Use skip_summarization para eventos verbosos ou internos que poluiriam o histórico da conversa.

  5. Agrupamento de Invocação: Mantenha o mesmo invocation_id para todos os eventos gerados durante uma única invocação de agent para manter o agrupamento lógico.

  6. Rastreamento de Artifacts: Sempre atualize artifact_delta ao criar ou modificar artifacts para manter a consistência.

  7. Processamento de Fluxo: Sempre trate erros ao processar fluxos de eventos. Eventos podem falhar devido a erros do LLM, falhas de tool ou problemas de rede.

  8. Verificação de Content: Sempre verifique se llm_response.content é Some antes de acessar partes. Alguns eventos (como atualizações de estado puras) podem não ter content.

  9. Pattern Matching: Use o pattern matching do Rust para lidar elegantemente com diferentes tipos de eventos e partes de content.

  10. Logging: Use invocation_id para correlacionar todos os eventos dentro de uma única interação do usuário para depuração e observabilidade.

Documentação Relacionada


Anterior: ← Artifacts | Próximo: Telemetry →