イベント

イベントは、ADK-Rustにおける会話履歴の基本的な構成要素です。ユーザーメッセージ、Agentの応答、Toolの実行など、Agentとの各インタラクションはイベントとして記録されます。イベントは、Agentセッションの完全な実行トレースを捕捉する不変のログを形成します。

概要

イベントシステムは、いくつかの重要な目的を果たします。

  • 会話履歴: イベントは、セッションにおけるすべてのインタラクションの時系列記録を形成します。
  • 状態管理: イベントは、state_deltaフィールドを通じて状態の変更を伝達します。
  • アーティファクト追跡: イベントは、artifact_deltaフィールドを通じてアーティファクト操作を記録します。
  • Agent連携: イベントは、Agentの転送とエスカレーションを可能にします。
  • デバッグと可観測性: イベントは、Agentの動作の完全な監査証跡を提供します。

イベント構造

Eventは、会話における単一のインタラクションを表します。ADK-Rustは、ADK-Goで採用されている設計パターンに合わせて、LlmResponseを組み込んだ統合されたEvent型を使用します。

pub struct Event {
    pub id: String,                    // ユニークなイベント識別子 (UUID)
    pub timestamp: DateTime<Utc>,      // イベントが発生した日時
    pub invocation_id: String,         // 単一のinvocation内の関連イベントをリンク
    pub branch: String,                // 将来のブランチングサポート用
    pub author: String,                // このイベントを作成したエンティティ (ユーザー、Agent名、Tool名)
    pub llm_response: LlmResponse,     // コンテンツとLLMメタデータを含む
    pub actions: EventActions,         // 副作用とメタデータ
    pub long_running_tool_ids: Vec<String>,  // 長時間実行ツールのID
}

LlmResponse structには以下が含まれます。

pub struct LlmResponse {
    pub content: Option<Content>,      // メッセージコンテンツ (テキスト、パートなど)
    pub usage_metadata: Option<UsageMetadata>,
    pub finish_reason: Option<FinishReason>,
    pub partial: bool,                 // ストリーミング中の部分的なレスポンスの場合にTrue
    pub turn_complete: bool,           // ターンが完了したときにTrue
    pub interrupted: bool,             // 生成が中断された場合にTrue
    pub error_code: Option<String>,
    pub error_message: Option<String>,
}

Contentは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);
        }
    }
}

主要フィールド

  • id: この特定のイベントを一意に識別するUUID。イベントの取得と順序付けに使用されます。

  • timestamp: イベントが作成されたUTCタイムスタンプ。イベントはセッション内で時系列順に並べられます。

  • invocation_id: 同じAgent invocationに属するイベントをグループ化します。Agentがメッセージを処理すると、生成されたすべてのイベント(Agentレスポンス、Tool呼び出し、サブAgent呼び出し)は同じinvocation_idを共有します。

  • branch: 将来のブランチング機能のために予約されています。現在は使用されていませんが、将来のバージョンで会話のブランチングを可能にします。

  • author: イベントを作成したエンティティを識別します。

    • ユーザーメッセージ: 通常 "user" またはユーザー識別子
    • Agentレスポンス: エージェントの名前
    • Tool実行: ツールの名前
    • システムイベント: "system"
  • llm_response: メッセージコンテンツとLLMメタデータを含みます。event.llm_response.contentを介してcontentにアクセスします。Content型には、テキスト、マルチモーダルパート(画像、音声)、または構造化データを含めることができます。一部のイベント(純粋な状態更新など)では、content: Noneとなる場合があります。

EventActions

EventActions構造体には、イベントに関連付けられたメタデータと副作用が含まれています。

pub struct EventActions {
    pub state_delta: HashMap<String, Value>,    // 適用する状態の変更
    pub artifact_delta: HashMap<String, i64>,   // アーティファクトのバージョン変更
    pub skip_summarization: bool,               // サマリーでこのイベントをスキップ
    pub transfer_to_agent: Option<String>,      // 他のAgentへの制御の引き渡し
    pub escalate: bool,                         // 人間またはスーパーバイザーへのエスカレーション
}

state_delta

state_deltaフィールドには、Sessionの状態への変更を表すキーと値のペアが含まれています。イベントがSessionに追加されると、これらの変更はSessionの状態にマージされます。

状態キーは、スコープを制御するためにプレフィックスを使用できます:

  • app:key - アプリケーションスコープの状態 (すべてのユーザー間で共有)
  • user:key - ユーザースコープの状態 (ユーザーのすべてのSession間で共有)
  • temp:key - 一時的な状態 (呼び出し間でクリアされます)
  • プレフィックスなし - Sessionスコープの状態 (デフォルト)

例:

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

artifact_deltaフィールドは、アーティファクトへの変更を追跡します。キーはアーティファクト名、値はバージョン番号です。これにより、システムはイベント中にどのアーティファクトが作成または変更されたかを追跡できます。

例:

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

skip_summarization

trueの場合、このイベントは会話のサマリーから除外されます。内部イベント、デバッグ情報、または主要な会話フローの一部にすべきではない冗長なToolの出力に役立ちます。

transfer_to_agent

Agent名が設定されている場合、制御はそのAgentに転送されます。これにより、あるAgentが別のAgentに引き渡すマルチAgentワークフローが可能になります。ターゲットAgentはサブAgentとして設定されている必要があります。

例:

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

escalate

trueの場合、会話を人間のオペレーターまたはスーパーバイザーAgentにエスカレートすべきであることを示します。特定のエスカレーション動作は、アプリケーションの実装によって異なります。

会話履歴の形成

イベントは、Session内で時系列順に蓄積されることで会話履歴を形成します。Agentがリクエストを処理する際:

  1. ユーザーメッセージイベント: ユーザーの入力を含む新しいイベントが作成されます
  2. Agentの処理: Agentは会話履歴 (以前のすべてのイベント) を受け取ります
  3. Agent応答イベント: Agentの応答が新しいイベントとして記録されます
  4. Tool実行イベント: 各Tool呼び出しにより、追加のイベントが生成される場合があります
  5. 状態の更新: すべてのイベントからの状態デルタは、Sessionの状態にマージされます

会話履歴は、以下によって構築されます:

  • Sessionからすべてのイベントを時系列順に取得する
  • 各イベントのコンテンツをLLMに適した形式に変換する
  • 蓄積された状態デルタからの状態情報を含める
  • 適切な場合にskip_summarizationがマークされたイベントを除外する

イベントフローの例

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

各イベントは前のイベントに基づいて構築され、会話の完全な監査証跡を作成します。

イベントの操作

Sessionからのイベントへのアクセス

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

// セッションとそのイベントを取得します
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,  // すべてのイベントを取得
    after: None,
}).await?;

// イベントにアクセス
let events = session.events();
println!("Total events: {}", events.len());

// イベントを反復処理します (注: sessionイベントは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
        );
    }
}

イベント詳細の検査

// セッションから特定のイベントを取得します (llm_response.contentを使用)
if let Some(event) = events.at(0) {
    // 作成者を確認
    println!("Author: {}", event.author);

    // コンテンツを確認 (sessionイベントは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);
            }
        }
    }

    // 状態変更を確認
    if !event.actions.state_delta.is_empty() {
        println!("State changes:");
        for (key, value) in &event.actions.state_delta {
            println!("  {} = {}", key, value);
        }
    }

    // Agent転送を確認
    if let Some(target) = &event.actions.transfer_to_agent {
        println!("Transfers to: {}", target);
    }

    // アーティファクトを確認
    if !event.actions.artifact_delta.is_empty() {
        println!("Artifacts modified:");
        for (name, version) in &event.actions.artifact_delta {
            println!("  {} (v{})", name, version);
        }
    }
}

イベント履歴の制限

長い会話の場合、最近のイベントのみを取得したい場合があります。

// 最新の10個のイベントのみを取得
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?;

イベントのフロー:生成と処理

イベントがどのように作成され処理されるかを理解することは、フレームワークがアクションと履歴を管理する方法を明確にするのに役立ちます。

生成元

イベントは、Agentの実行ライフサイクルのさまざまな時点で作成されます。

  1. ユーザー入力: Runnerはユーザーメッセージをauthor = "user"を持つEventにラップします。
  2. Agentの応答: Agentは応答を伝えるためにEventオブジェクト(author = agent.name()を設定)を生成します。
  3. LLM出力: モデル統合層はLLM出力(テキスト、ファンクションコール)をEventオブジェクトに変換します。
  4. Toolの結果: Tool実行後、フレームワークはToolの応答を含むEventを生成します。

処理フロー

イベントが生成されると、以下の処理パスをたどります。

  1. 生成: イベントは、その生成元(Agent、Tool、またはユーザー入力ハンドラー)によって作成され、生成されます。
  2. Runnerの受信: Agentを実行しているRunnerがイベントを受信します。
  3. SessionServiceによる処理: RunnerはイベントをSessionServiceに送信し、SessionServiceは以下を実行します。
    • デルタの適用: state_deltaをセッション状態にマージし、成果物レコードを更新します。
    • メタデータの確定: idが存在しない場合は一意の値を割り当て、timestampを設定します。
    • 履歴への永続化: session.eventsにイベントを追加します。
  4. ストリーム出力: Runnerは処理されたイベントを呼び出し元のアプリケーションに生成します。

このフローにより、状態変更と履歴が通信内容とともに一貫して記録されることが保証されます。

// 概念的なフロー
User Input → Runner → Agent → LLM → Event Generated
                                         ↓
                                   SessionService
                                   - state_deltaを適用
                                   - 履歴に記録
                                         ↓
                                   Event Stream → Application

イベントタイプの識別

Runnerからのイベントを処理する際、それがどのようなタイプのイベントであるかを識別したくなるでしょう。

Authorによる識別

authorフィールドは、誰がイベントを作成したかを示します。

match event.author.as_str() {
    "user" => println!("ユーザー入力"),
    agent_name => println!("Agentからの応答: {}", agent_name),
}

Contentタイプによる識別

ペイロードタイプを特定するには、llm_response.contentフィールドを確認してください。

if let Some(content) = &event.llm_response.content {
    // テキストコンテンツの確認
    let has_text = content.parts.iter().any(|part| {
        matches!(part, Part::Text { .. })
    });

    // ファンクションコール(Toolリクエスト)の確認
    let has_function_call = content.parts.iter().any(|part| {
        matches!(part, Part::FunctionCall { .. })
    });

    // ファンクション応答(Toolの結果)の確認
    let has_function_response = content.parts.iter().any(|part| {
        matches!(part, Part::FunctionResponse { .. })
    });

    if has_text {
        println!("テキストメッセージ");
    } else if has_function_call {
        println!("Tool呼び出しリクエスト");
    } else if has_function_response {
        println!("Toolの結果");
    }
}

Actionsによる識別

制御シグナルと副作用については、actionsフィールドを確認してください。

// 状態変更
if !event.actions.state_delta.is_empty() {
    println!("イベントに状態変更が含まれています");
}

// Agentの転送
if let Some(target) = &event.actions.transfer_to_agent {
    println!("Agentへの転送: {}", target);
}

// エスカレーションシグナル
if event.actions.escalate {
    println!("エスカレーションが要求されました");
}

// 要約をスキップ
if event.actions.skip_summarization {
    println!("このイベントを要約でスキップします");
}

イベントストリームの操作

Agentを実行すると、イベントのストリームが返されます。ここでは、それらを効果的に処理する方法を説明します。

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

Function Callの抽出

LLMがToolをリクエストすると、イベントにはFunction Call情報が含まれます。

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

Function Responseの抽出

Toolの実行後、結果はFunction Responseとして返されます。

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

一般的なイベントパターン

ここでは、遭遇する典型的なイベントシーケンスを紹介します。

シンプルなテキスト交換

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

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

状態の更新

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

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

イベントのメタデータと識別子

Event ID

各イベントには、正確な識別のための一意のid (UUID) があります。

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

Invocation ID

invocation_idは、単一のユーザーリクエストから最終的な応答までのすべてのイベントをグループ化します。

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

タイムスタンプ

イベントは、時系列順に並べられるようにタイムスタンプが付けられます。

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

ベストプラクティス

  1. イベントの不変性: イベントは作成後に変更してはなりません。それらは不変の監査ログを形成します。

  2. 状態管理: stateを直接変更するのではなく、すべての状態変更にstate_deltaを使用してください。これにより、変更がイベントログに追跡されます。

  3. 意味のあるオーサー: イベントログを理解しやすくするために、明確で分かりやすいオーサー名を設定してください。

  4. 選択的な要約: 会話履歴を乱雑にするような冗長なイベントや内部イベントにはskip_summarizationを使用してください。

  5. 呼び出しのグループ化: 単一のAgent呼び出し中に生成されるすべてのイベントについて、同じinvocation_idを保持し、論理的なグループ化を維持してください。

  6. Artifactの追跡: 一貫性を維持するために、Artifactを作成または変更する際には常にartifact_deltaを更新してください。

  7. ストリーム処理: イベントストリームを処理する際には、常にエラーを処理してください。イベントはLLMエラー、Toolの失敗、またはネットワークの問題により失敗する可能性があります。

  8. Contentの確認: Partにアクセスする前に、常にllm_response.contentSomeであるかを確認してください。一部のイベント(純粋な状態更新など)にはContentがない場合があります。

  9. パターンマッチング: Rustのパターンマッチングを使用して、異なるイベントタイプとContentのPartをエレガントに処理してください。

  10. ロギング: デバッグと可観測性のために、単一のユーザーインタラクション内のすべてのイベントを関連付けるためにinvocation_idを使用してください。

関連ドキュメント


前へ: ← Artifacts | 次へ: Telemetry →