コンテンツにスキップ

状態(State):セッションのメモ帳

セッション(会話のスレッド)内において、state属性はその特定の対話のためのエージェント専用のメモ帳のように機能します。session.eventsが完全な履歴を保持するのに対し、session.stateはエージェントが会話の最中に必要な動的な詳細を保存し、更新する場所です。

session.stateとは何か?

概念的に、session.stateはキーと値のペアを保持するコレクション(辞書またはMap)です。エージェントが現在の会話を効果的にするために思い出す、または追跡する必要がある情報のために設計されています:

  • 対話のパーソナライズ: 以前に言及されたユーザーの好みを記憶する(例:'user_preference_theme': 'dark')。
  • タスクの進捗追跡: 複数ターンにわたるプロセスのステップを把握する(例:'booking_step': 'confirm_payment')。
  • 情報の蓄積: リストや要約を作成する(例:'shopping_cart_items': ['book', 'pen'])。
  • 情報に基づいた意思決定: 次の応答に影響を与えるフラグや値を保存する(例:'user_is_authenticated': True)。

Stateの主な特徴

  1. 構造:シリアライズ可能なキーと値のペア

    • データはkey: valueとして保存されます。
    • キー: 常に文字列(str)です。明確な名前を使用してください(例:'departure_city''user:language_preference')。
    • 値: シリアライズ可能でなければなりません。これは、SessionServiceによって簡単に保存および読み込みができることを意味します。文字列、数値、ブール値、およびこれらの基本型のみを含む単純なリストや辞書など、特定の言語(Python/Java)の基本型に固執してください。(正確な詳細についてはAPIドキュメントを参照してください)。
    • ⚠️ 複雑なオブジェクトを避ける: シリアライズ不可能なオブジェクト(カスタムクラスのインスタンス、関数、接続など)を直接状態に保存しないでください。必要であれば単純な識別子を保存し、複雑なオブジェクトは他の場所で取得してください。
  2. 可変性:変化する

    • stateの内容は、会話が進むにつれて変化することが期待されます。
  3. 永続性:SessionServiceに依存

    • 状態がアプリケーションの再起動後も存続するかどうかは、選択したサービスに依存します:
      • InMemorySessionService永続的ではない。再起動時に状態は失われます。
      • DatabaseSessionService / VertexAiSessionService永続的。状態は確実に保存されます。

Note

プリミティブの具体的なパラメータやメソッド名は、SDKの言語によって若干異なる場合があります(例:Pythonではsession.state['current_intent'] = 'book_flight'、Javaではsession.state().put("current_intent", "book_flight"))。詳細は各言語のAPIドキュメントを参照してください。

プレフィックスによる状態の整理:スコープの重要性

状態キーのプレフィックスは、特に永続的なサービスにおいて、そのスコープと永続性の振る舞いを定義します:

  • プレフィックスなし(セッション状態):

    • スコープ: 現在のセッション(id)に固有。
    • 永続性: SessionServiceが永続的(DatabaseVertexAI)な場合にのみ永続します。
    • ユースケース: 現在のタスク内の進捗追跡(例:'current_booking_step')、この対話のための一時的なフラグ(例:'needs_clarification')。
    • 例: session.state['current_intent'] = 'book_flight'
  • user:プレフィックス(ユーザー状態):

    • スコープ: user_idに紐づけられ、そのユーザーのすべてのセッションで共有されます(同じapp_name内)。
    • 永続性: DatabaseまたはVertexAIで永続的。(InMemoryでは保存されるが、再起動時に失われる)。
    • ユースケース: ユーザー設定(例:'user:theme')、プロフィール詳細(例:'user:name')。
    • 例: session.state['user:preferred_language'] = 'fr'
  • app:プレフィックス(アプリ状態):

    • スコープ: app_nameに紐づけられ、そのアプリケーションのすべてのユーザーとセッションで共有されます。
    • 永続性: DatabaseまたはVertexAIで永続的。(InMemoryでは保存されるが、再起動時に失われる)。
    • ユースケース: グローバル設定(例:'app:api_endpoint')、共有テンプレート。
    • 例: session.state['app:global_discount_code'] = 'SAVE10'
  • temp:プレフィックス(一時的なセッション状態):

    • スコープ: 現在のセッション処理ターンに固有。
    • 永続性: 決して永続しない。永続的なサービスでも破棄されることが保証されます。
    • ユースケース: 直後にのみ必要な中間結果、明示的に保存したくないデータ。
    • 例: session.state['temp:raw_api_response'] = {...}

エージェントから見た場合: エージェントのコードは、単一のsession.stateコレクション(dict/Map)を介して、結合された状態と対話します。SessionServiceは、プレフィックスに基づいて適切な基盤となるストレージから状態を取得/マージする処理をします。

状態の更新方法:推奨されるメソッド

状態は常にsession_service.append_event()を使用してセッション履歴にEventを追加する一環として更新されるべきです。これにより、変更が追跡され、永続性が正しく機能し、更新がスレッドセーフであることが保証されます。

1. 簡単な方法:output_key(エージェントのテキスト応答用)

これは、エージェントの最終的なテキスト応答を直接状態に保存する最も簡単な方法です。LlmAgentを定義する際に、output_keyを指定します:

# ...(Pythonコードは変更しないため省略)...
import com.google.adk.agents.LlmAgent;
import com.google.adk.agents.RunConfig;
import com.google.adk.events.Event;
import com.google.adk.runner.Runner;
import com.google.adk.sessions.InMemorySessionService;
import com.google.adk.sessions.Session;
import com.google.genai.types.Content;
import com.google.genai.types.Part;
import java.util.List;
import java.util.Optional;

public class GreetingAgentExample {

  public static void main(String[] args) {
    // Define agent with output_key
    LlmAgent greetingAgent =
        LlmAgent.builder()
            .name("Greeter")
            .model("gemini-2.0-flash")
            .instruction("Generate a short, friendly greeting.")
            .description("Greeting agent")
            .outputKey("last_greeting") // Save response to state['last_greeting']
            .build();

    // --- Setup Runner and Session ---
    String appName = "state_app";
    String userId = "user1";
    String sessionId = "session1";

    InMemorySessionService sessionService = new InMemorySessionService();
    Runner runner = new Runner(greetingAgent, appName, null, sessionService); // artifactService can be null if not used

    Session session =
        sessionService.createSession(appName, userId, null, sessionId).blockingGet();
    System.out.println("Initial state: " + session.state().entrySet());

    // --- Run the Agent ---
    // Runner handles calling appendEvent, which uses the output_key
    // to automatically create the stateDelta.
    Content userMessage = Content.builder().parts(List.of(Part.fromText("Hello"))).build();

    // RunConfig is needed for runner.runAsync in Java
    RunConfig runConfig = RunConfig.builder().build();

    for (Event event : runner.runAsync(userId, sessionId, userMessage, runConfig).blockingIterable()) {
      if (event.finalResponse()) {
        System.out.println("Agent responded."); // Response text is also in event.content
      }
    }

    // --- Check Updated State ---
    Session updatedSession =
        sessionService.getSession(appName, userId, sessionId, Optional.empty()).blockingGet();
    assert updatedSession != null;
    System.out.println("State after agent run: " + updatedSession.state().entrySet());
    // Expected output might include: {'last_greeting': 'Hello there! How can I help you today?'}
  }
}

舞台裏では、Runneroutput_keyを使用して、state_deltaを持つ必要なEventActionsを作成し、append_eventを呼び出します。

2. 標準的な方法:EventActions.state_delta(複雑な更新用)

より複雑なシナリオ(複数のキーの更新、文字列以外の値、user:app:のような特定のスコープ、またはエージェントの最終テキストに直接結びつかない更新)では、EventActions内でstate_deltaを手動で構築します。

# ...(Pythonコードは変更しないため省略)...
import com.google.adk.events.Event;
import com.google.adk.events.EventActions;
import com.google.adk.sessions.InMemorySessionService;
import com.google.adk.sessions.Session;
import java.time.Instant;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public class ManualStateUpdateExample {

  public static void main(String[] args) {
    // --- Setup ---
    InMemorySessionService sessionService = new InMemorySessionService();
    String appName = "state_app_manual";
    String userId = "user2";
    String sessionId = "session2";

    ConcurrentMap<String, Object> initialState = new ConcurrentHashMap<>();
    initialState.put("user:login_count", 0);
    initialState.put("task_status", "idle");

    Session session =
        sessionService.createSession(appName, userId, initialState, sessionId).blockingGet();
    System.out.println("Initial state: " + session.state().entrySet());

    // --- Define State Changes ---
    long currentTimeMillis = Instant.now().toEpochMilli(); // Use milliseconds for Java Event

    ConcurrentMap<String, Object> stateChanges = new ConcurrentHashMap<>();
    stateChanges.put("task_status", "active"); // Update session state

    // Retrieve and increment login_count
    Object loginCountObj = session.state().get("user:login_count");
    int currentLoginCount = 0;
    if (loginCountObj instanceof Number) {
      currentLoginCount = ((Number) loginCountObj).intValue();
    }
    stateChanges.put("user:login_count", currentLoginCount + 1); // Update user state

    stateChanges.put("user:last_login_ts", currentTimeMillis); // Add user state (as long milliseconds)
    stateChanges.put("temp:validation_needed", true); // Add temporary state

    // --- Create Event with Actions ---
    EventActions actionsWithUpdate = EventActions.builder().stateDelta(stateChanges).build();

    // This event might represent an internal system action, not just an agent response
    Event systemEvent =
        Event.builder()
            .invocationId("inv_login_update")
            .author("system") // Or 'agent', 'tool' etc.
            .actions(actionsWithUpdate)
            .timestamp(currentTimeMillis)
            // content might be None or represent the action taken
            .build();

    // --- Append the Event (This updates the state) ---
    sessionService.appendEvent(session, systemEvent).blockingGet();
    System.out.println("`appendEvent` called with explicit state delta.");

    // --- Check Updated State ---
    Session updatedSession =
        sessionService.getSession(appName, userId, sessionId, Optional.empty()).blockingGet();
    assert updatedSession != null;
    System.out.println("State after event: " + updatedSession.state().entrySet());
    // Expected: {'user:login_count': 1, 'task_status': 'active', 'user:last_login_ts': <timestamp_millis>}
    // Note: 'temp:validation_needed' is NOT present because InMemorySessionService's appendEvent
    // applies delta to its internal user/app state maps IF keys have prefixes,
    // and to the session's own state map (which is then merged on getSession).
  }
}

3. CallbackContextまたはToolContext経由(コールバックとツールに推奨)

エージェントのコールバック(例:on_before_agent_callon_after_agent_call)やツール関数内で状態を変更する場合、関数に提供されるCallbackContextまたはToolContextstate属性を使用するのが最善です。

  • callback_context.state['my_key'] = my_value
  • tool_context.state['my_key'] = my_value

これらのコンテキストオブジェクトは、それぞれの実行スコープ内で状態の変更を管理するために特別に設計されています。context.stateを変更すると、ADKフレームワークはこれらの変更が自動的にキャプチャされ、コールバックやツールによって生成されるイベントのEventActions.state_deltaに正しくルーティングされるようにします。この差分は、イベントが追加されるときにSessionServiceによって処理され、適切な永続性と追跡が保証されます。

この方法は、コールバックやツール内のほとんどの一般的な状態更新シナリオで、EventActionsstate_deltaの手動作成を抽象化し、コードをよりクリーンでエラーが発生しにくくします。

コンテキストオブジェクトに関するより包括的な詳細については、コンテキストのドキュメントを参照してください。

# エージェントのコールバックまたはツール関数内
from google.adk.agents import CallbackContext # または ToolContext

def my_callback_or_tool_function(context: CallbackContext, # または ToolContext
                                 # ... 他のパラメータ ...
                                ):
    # 既存の状態を更新
    count = context.state.get("user_action_count", 0)
    context.state["user_action_count"] = count + 1

    # 新しい状態を追加
    context.state["temp:last_operation_status"] = "success"

    # 状態の変更は自動的にイベントのstate_deltaの一部となる
    # ... コールバック/ツールの残りのロジック ...
// エージェントのコールバックまたはツールメソッド内
import com.google.adk.agents.CallbackContext; // または ToolContext
// ... 他のインポート ...

public class MyAgentCallbacks {
    public void onAfterAgent(CallbackContext callbackContext) {
        // 既存の状態を更新
        Integer count = (Integer) callbackContext.state().getOrDefault("user_action_count", 0);
        callbackContext.state().put("user_action_count", count + 1);

        // 新しい状態を追加
        callbackContext.state().put("temp:last_operation_status", "success");

        // 状態の変更は自動的にイベントのstate_deltaの一部となる
        // ... コールバックの残りのロジック ...
    }
}

append_eventの役割:

  • Eventsession.eventsに追加します。
  • イベントのactionsからstate_deltaを読み取ります。
  • SessionServiceによって管理される状態にこれらの変更を適用し、サービスタイプに基づいてプレフィックスと永続性を正しく処理します。
  • セッションのlast_update_timeを更新します。
  • 同時更新に対するスレッドセーフを保証します。

⚠️ 直接的な状態変更に関する警告

エージェントの呼び出しの管理されたライフサイクルので(つまり、CallbackContextToolContextを介さずに)、SessionServiceから直接取得したSessionオブジェクトのsession.stateコレクション(辞書/Map)を直接変更することは避けてください。例えば、retrieved_session = await session_service.get_session(...); retrieved_session.state['key'] = valueのようなコードは問題があります。

コールバックやツール内でCallbackContext.stateToolContext.stateを使用して状態を変更することが、変更が追跡されることを保証する正しい方法です。これらのコンテキストオブジェクトは、イベントシステムとの必要な統合を処理します。

なぜ直接的な変更(コンテキスト外)が強く非推奨なのか:

  1. イベント履歴をバイパスする: 変更がEventとして記録されず、監査可能性が失われます。
  2. 永続性を壊す: このように行われた変更は、DatabaseSessionServiceVertexAiSessionServiceによって保存されない可能性が高いです。これらは保存をトリガーするためにappend_eventに依存しています。
  3. スレッドセーフではない: 競合状態や更新の喪失につながる可能性があります。
  4. タイムスタンプ/ロジックを無視する: last_update_timeを更新したり、関連するイベントロジックをトリガーしたりしません。

推奨事項: output_keyEventActions.state_delta(イベントを手動で作成する場合)、またはそれぞれのスコープ内でCallbackContextまたはToolContextオブジェクトのstateプロパティを変更することで、状態を更新することに固執してください。これらの方法は、信頼性が高く、追跡可能で、永続的な状態管理を保証します。SessionServiceから取得したセッションのsession.stateへの直接アクセスは、状態の読み取りにのみ使用してください。

状態設計のベストプラクティスのまとめ

  • ミニマリズム: 不可欠で動的なデータのみを保存します。
  • シリアライズ: 基本的でシリアライズ可能な型を使用します。
  • 説明的なキーとプレフィックス: 明確な名前と適切なプレフィックス(user:app:temp:、またはなし)を使用します。
  • 浅い構造: 可能な限り深いネストを避けます。
  • 標準的な更新フロー: append_eventに依存します。