状態(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
の主な特徴¶
-
構造:シリアライズ可能なキーと値のペア
- データは
key: value
として保存されます。 - キー: 常に文字列(
str
)です。明確な名前を使用してください(例:'departure_city'
、'user:language_preference'
)。 - 値: シリアライズ可能でなければなりません。これは、
SessionService
によって簡単に保存および読み込みができることを意味します。文字列、数値、ブール値、およびこれらの基本型のみを含む単純なリストや辞書など、特定の言語(Python/Java)の基本型に固執してください。(正確な詳細についてはAPIドキュメントを参照してください)。 - ⚠️ 複雑なオブジェクトを避ける: シリアライズ不可能なオブジェクト(カスタムクラスのインスタンス、関数、接続など)を直接状態に保存しないでください。必要であれば単純な識別子を保存し、複雑なオブジェクトは他の場所で取得してください。
- データは
-
可変性:変化する
state
の内容は、会話が進むにつれて変化することが期待されます。
-
永続性:
SessionService
に依存- 状態がアプリケーションの再起動後も存続するかどうかは、選択したサービスに依存します:
InMemorySessionService
:永続的ではない。再起動時に状態は失われます。DatabaseSessionService
/VertexAiSessionService
:永続的。状態は確実に保存されます。
- 状態がアプリケーションの再起動後も存続するかどうかは、選択したサービスに依存します:
Note
プリミティブの具体的なパラメータやメソッド名は、SDKの言語によって若干異なる場合があります(例:Pythonではsession.state['current_intent'] = 'book_flight'
、Javaではsession.state().put("current_intent", "book_flight")
)。詳細は各言語のAPIドキュメントを参照してください。
プレフィックスによる状態の整理:スコープの重要性¶
状態キーのプレフィックスは、特に永続的なサービスにおいて、そのスコープと永続性の振る舞いを定義します:
-
プレフィックスなし(セッション状態):
- スコープ: 現在のセッション(
id
)に固有。 - 永続性:
SessionService
が永続的(Database
、VertexAI
)な場合にのみ永続します。 - ユースケース: 現在のタスク内の進捗追跡(例:
'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
を指定します:
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?'}
}
}
舞台裏では、Runner
がoutput_key
を使用して、state_delta
を持つ必要なEventActions
を作成し、append_event
を呼び出します。
2. 標準的な方法:EventActions.state_delta
(複雑な更新用)
より複雑なシナリオ(複数のキーの更新、文字列以外の値、user:
やapp:
のような特定のスコープ、またはエージェントの最終テキストに直接結びつかない更新)では、EventActions
内でstate_delta
を手動で構築します。
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_call
、on_after_agent_call
)やツール関数内で状態を変更する場合、関数に提供されるCallbackContext
またはToolContext
のstate
属性を使用するのが最善です。
callback_context.state['my_key'] = my_value
tool_context.state['my_key'] = my_value
これらのコンテキストオブジェクトは、それぞれの実行スコープ内で状態の変更を管理するために特別に設計されています。context.state
を変更すると、ADKフレームワークはこれらの変更が自動的にキャプチャされ、コールバックやツールによって生成されるイベントのEventActions.state_delta
に正しくルーティングされるようにします。この差分は、イベントが追加されるときにSessionService
によって処理され、適切な永続性と追跡が保証されます。
この方法は、コールバックやツール内のほとんどの一般的な状態更新シナリオで、EventActions
やstate_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
の役割:
Event
をsession.events
に追加します。- イベントの
actions
からstate_delta
を読み取ります。 SessionService
によって管理される状態にこれらの変更を適用し、サービスタイプに基づいてプレフィックスと永続性を正しく処理します。- セッションの
last_update_time
を更新します。 - 同時更新に対するスレッドセーフを保証します。
⚠️ 直接的な状態変更に関する警告¶
エージェントの呼び出しの管理されたライフサイクルの外で(つまり、CallbackContext
やToolContext
を介さずに)、SessionService
から直接取得したSession
オブジェクトのsession.state
コレクション(辞書/Map)を直接変更することは避けてください。例えば、retrieved_session = await session_service.get_session(...); retrieved_session.state['key'] = value
のようなコードは問題があります。
コールバックやツール内でCallbackContext.state
やToolContext.state
を使用して状態を変更することが、変更が追跡されることを保証する正しい方法です。これらのコンテキストオブジェクトは、イベントシステムとの必要な統合を処理します。
なぜ直接的な変更(コンテキスト外)が強く非推奨なのか:
- イベント履歴をバイパスする: 変更が
Event
として記録されず、監査可能性が失われます。 - 永続性を壊す: このように行われた変更は、
DatabaseSessionService
やVertexAiSessionService
によって保存されない可能性が高いです。これらは保存をトリガーするためにappend_event
に依存しています。 - スレッドセーフではない: 競合状態や更新の喪失につながる可能性があります。
- タイムスタンプ/ロジックを無視する:
last_update_time
を更新したり、関連するイベントロジックをトリガーしたりしません。
推奨事項: output_key
、EventActions.state_delta
(イベントを手動で作成する場合)、またはそれぞれのスコープ内でCallbackContext
またはToolContext
オブジェクトのstate
プロパティを変更することで、状態を更新することに固執してください。これらの方法は、信頼性が高く、追跡可能で、永続的な状態管理を保証します。SessionService
から取得したセッションのsession.state
への直接アクセスは、状態の読み取りにのみ使用してください。
状態設計のベストプラクティスのまとめ¶
- ミニマリズム: 不可欠で動的なデータのみを保存します。
- シリアライズ: 基本的でシリアライズ可能な型を使用します。
- 説明的なキーとプレフィックス: 明確な名前と適切なプレフィックス(
user:
、app:
、temp:
、またはなし)を使用します。 - 浅い構造: 可能な限り深いネストを避けます。
- 標準的な更新フロー:
append_event
に依存します。