コンテンツにスキップ

状態(State):セッションのスクラッチパッド

ADKでサポートPython v0.1.0Go v0.1.0Java v0.1.0

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

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

    • 状態がアプリケーションの再起動後も存続するかどうかは、選択したサービスによって異なります。

    • InMemorySessionService永続的ではない。 再起動時に状態は失われます。

    • DatabaseSessionService / VertexAiSessionService永続的。 状態は確実に保存されます。

プリミティブの特定のパラメータやメソッド名は、SDK言語によって若干異なる場合があります(例:Pythonではsession.state['current_intent'] = 'book_flight'、Goではcontext.State().Set("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: プレフィックス(一時的な呼び出し状態):

    • スコープ: 現在の呼び出し(invocation)に固有です(エージェントがユーザー入力を受け取ってから、その入力に対する最終的な出力を生成するまでの全プロセス)。
    • 永続性: 永続的ではない。 呼び出しが完了した後に破棄され、次の呼び出しには引き継がれません。
    • ユースケース: 単一の呼び出し内でツール呼び出し間で渡される中間計算、フラグ、またはデータの保存。
    • 使用すべきでない場合: ユーザーの好み、会話履歴の要約、蓄積されたデータなど、異なる呼び出しにわたって永続する必要がある情報。
    • 例: session.state['temp:raw_api_response'] = {...}

サブエージェントと呼び出しコンテキスト

親エージェントがサブエージェントを呼び出す場合(例:SequentialAgentParallelAgentを使用)、そのInvocationContextをサブエージェントに渡します。これは、エージェント呼び出しのチェーン全体が同じ呼び出しIDを共有し、したがって同じtemp:状態を共有することを意味します。

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

エージェントの指示におけるセッション状態へのアクセス

LlmAgentインスタンスを扱う際、簡単なテンプレート構文を使用して、セッション状態の値をエージェントの指示文字列に直接挿入できます。これにより、自然言語の指示だけに頼らず、動的でコンテキストを認識する指示を作成できます。

{key} テンプレートの使用

セッション状態から値を挿入するには、目的の状態変数のキーを波括弧で囲みます:{key}。フレームワークは、指示をLLMに渡す前に、このプレースホルダーをsession.stateの対応する値で自動的に置き換えます。

例:

from google.adk.agents import LlmAgent

story_generator = LlmAgent(
    name="StoryGenerator",
    model="gemini-2.0-flash",
    instruction="""猫についての短い物語を、テーマ:{topic}に焦点を当てて書いてください。"""
)

# session.state['topic']が"friendship"に設定されていると仮定すると、LLMは
# 次の指示を受け取ります:
# "猫についての短い物語を、テーマ:friendshipに焦点を当てて書いてください。"
func main() {
    ctx := context.Background()
    sessionService := session.InMemoryService()

    // 1. Initialize a session with a 'topic' in its state.
    _, err := sessionService.Create(ctx, &session.CreateRequest{
        AppName:   appName,
        UserID:    userID,
        SessionID: sessionID,
        State: map[string]any{
            "topic": "friendship",
        },
    })
    if err != nil {
        log.Fatalf("Failed to create session: %v", err)
    }

    // 2. Create an agent with an instruction that uses a {topic} placeholder.
    //    The ADK will automatically inject the value of "topic" from the
    //    session state into the instruction before calling the LLM.
    model, err := gemini.NewModel(ctx, modelID, nil)
    if err != nil {
        log.Fatalf("Failed to create Gemini model: %v", err)
    }
    storyGenerator, err := llmagent.New(llmagent.Config{
        Name:        "StoryGenerator",
        Model:       model,
        Instruction: "Write a short story about a cat, focusing on the theme: {topic}.",
    })
    if err != nil {
        log.Fatalf("Failed to create agent: %v", err)
    }

    r, err := runner.New(runner.Config{
        AppName:        appName,
        Agent:          agent.Agent(storyGenerator),
        SessionService: sessionService,
    })
    if err != nil {
        log.Fatalf("Failed to create runner: %v", err)
    }

重要な考慮事項

  • キーの存在:指示文字列で参照するキーがsession.stateに存在することを確認してください。キーが見つからない場合、エージェントはエラーをスローします。存在するかもしれないし、しないかもしれないキーを使用するには、キーの後に疑問符(?)を含めることができます(例: {topic?})。
  • データ型:キーに関連付けられた値は、文字列または簡単に文字列に変換できる型である必要があります。
  • エスケープ:指示にリテラルの波括弧を使用する必要がある場合(例:JSONのフォーマット)、それらをエスケープする必要があります。

InstructionProviderによる状態の挿入のバイパス

場合によっては、状態挿入メカニズムをトリガーせずに、指示で{{}}を文字通り使用したいことがあります。たとえば、同じ構文を使用するテンプレート言語を扱うエージェントの指示を書いている場合などです。

これを実現するには、instructionパラメータに文字列の代わりに関数を提供します。この関数はInstructionProviderと呼ばれます。InstructionProviderを使用すると、ADKは状態の挿入を試みず、指示文字列はそのままモデルに渡されます。

InstructionProvider関数はReadonlyContextオブジェクトを受け取ります。これを使用して、指示を動的に構築する必要がある場合にセッション状態やその他のコンテキスト情報にアクセスできます。

from google.adk.agents import LlmAgent
from google.adk.agents.readonly_context import ReadonlyContext

# これはInstructionProviderです
def my_instruction_provider(context: ReadonlyContext) -> str:
    # オプションでコンテキストを使用して指示を構築できます
    # この例では、リテラルの波括弧を持つ静的な文字列を返します。
    return "これは置換されない{{リテラルの波括弧}}を持つ指示です。"

agent = LlmAgent(
    model="gemini-2.0-flash",
    name="template_helper_agent",
    instruction=my_instruction_provider
)
//  1. This InstructionProvider returns a static string.
//     Because it's a provider function, the ADK will not attempt to inject
//     state, and the instruction will be passed to the model as-is,
//     preserving the literal braces.
func staticInstructionProvider(ctx agent.ReadonlyContext) (string, error) {
    return "This is an instruction with {{literal_braces}} that will not be replaced.", nil
}

InstructionProviderを使用し、かつ指示に状態を挿入したい場合は、inject_session_stateユーティリティ関数を使用できます。

from google.adk.agents import LlmAgent
from google.adk.agents.readonly_context import ReadonlyContext
from google.adk.utils import instructions_utils

async def my_dynamic_instruction_provider(context: ReadonlyContext) -> str:
    template = "これは{adjective}な指示で、{{リテラルの波括弧}}を含みます。"
    # これは'adjective'状態変数を挿入しますが、リテラルの波括弧はそのままにします。
    return await instructions_utils.inject_session_state(template, context)

agent = LlmAgent(
    model="gemini-2.0-flash",
    name="dynamic_template_helper_agent",
    instruction=my_dynamic_instruction_provider
)
//  2. This InstructionProvider demonstrates how to manually inject state
//     while also preserving literal braces. It uses the instructionutil helper.
func dynamicInstructionProvider(ctx agent.ReadonlyContext) (string, error) {
    template := "This is a {adjective} instruction with {{literal_braces}}."
    // This will inject the 'adjective' state variable but leave the literal braces.
    return instructionutil.InjectSessionState(ctx, template)
}

直接挿入の利点

  • 明確さ:指示のどの部分が動的でセッション状態に基づいているかを明示的にします。
  • 信頼性:LLMが状態にアクセスするために自然言語の指示を正しく解釈することに依存するのを避けます。
  • 保守性:指示文字列を単純化し、状態変数名を更新する際のエラーのリスクを減らします。

他の状態アクセス方法との関係

この直接挿入方法は、LlmAgentの指示に特有のものです。他の状態アクセス方法の詳細については、次のセクションを参照してください。

状態の更新方法:推奨される方法

状態を修正する正しい方法

セッション状態を変更する必要がある場合、正しく最も安全な方法は、関数に提供されたContext上のstateオブジェクトを直接変更することです(例:callback_context.state['my_key'] = 'new_value')。フレームワークがこれらの変更を自動的に追跡するため、これは正しい方法での「直接的な状態操作」と見なされます。

これは、SessionServiceから取得したSessionオブジェクト上のstateを直接変更すること(例:my_session.state['my_key'] = 'new_value')とは決定的に異なります。これは避けるべきです。ADKのイベント追跡をバイパスし、データの損失につながる可能性があるためです。このページの最後にある「警告」セクションで、この重要な違いについて詳しく説明します。

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

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

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

from google.adk.agents import LlmAgent
from google.adk.sessions import InMemorySessionService, Session
from google.adk.runners import Runner
from google.genai.types import Content, Part

# output_keyでエージェントを定義
greeting_agent = LlmAgent(
    name="Greeter",
    model="gemini-2.0-flash", # 有効なモデルを使用
    instruction="短く、親しみやすい挨拶を生成してください。",
    output_key="last_greeting" # 応答をstate['last_greeting']に保存
)

# --- RunnerとSessionのセットアップ ---
app_name, user_id, session_id = "state_app", "user1", "session1"
session_service = InMemorySessionService()
runner = Runner(
    agent=greeting_agent,
    app_name=app_name,
    session_service=session_service
)
session = await session_service.create_session(app_name=app_name,
                                    user_id=user_id,
                                    session_id=session_id)
print(f"初期状態: {session.state}")

# --- エージェントの実行 ---
# Runnerがappend_eventの呼び出しを処理し、output_keyを使用して
# state_deltaを自動的に作成します。
user_message = Content(parts=[Part(text="Hello")])
for event in runner.run(user_id=user_id,
                        session_id=session_id,
                        new_message=user_message):
    if event.is_final_response():
      print(f"エージェントが応答しました。") # 応答テキストはevent.contentにもあります

# --- 更新された状態の確認 ---
updated_session = await session_service.get_session(app_name=APP_NAME, user_id=USER_ID, session_id=session_id)
print(f"エージェント実行後の状態: {updated_session.state}")
# 期待される出力例: {'last_greeting': 'こんにちは!何かお手伝いできることはありますか?'}
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?'}
  }
}
//  1. GreetingAgent demonstrates using `OutputKey` to save an agent's
//     final text response directly into the session state.
func greetingAgentExample(sessionService session.Service) {
    fmt.Println("--- Running GreetingAgent (output_key) Example ---")
    ctx := context.Background()

    modelGreeting, err := gemini.NewModel(ctx, modelID, nil)
    if err != nil {
        log.Fatalf("Failed to create Gemini model for greeting agent: %v", err)
    }
    greetingAgent, err := llmagent.New(llmagent.Config{
        Name:        "Greeter",
        Model:       modelGreeting,
        Instruction: "Generate a short, friendly greeting.",
        OutputKey:   "last_greeting",
    })
    if err != nil {
        log.Fatalf("Failed to create greeting agent: %v", err)
    }

    r, err := runner.New(runner.Config{
        AppName:        appName,
        Agent:          agent.Agent(greetingAgent),
        SessionService: sessionService,
    })
    if err != nil {
        log.Fatalf("Failed to create runner: %v", err)
    }

    // Run the agent
    userMessage := genai.NewContentFromText("Hello", "user")
    for event, err := range r.Run(ctx, userID, sessionID, userMessage, agent.RunConfig{}) {
        if err != nil {
            log.Printf("Agent Error: %v", err)
            continue
        }
        if isFinalResponse(event) {
            if event.LLMResponse.Content != nil {
                fmt.Printf("Agent responded with: %q\n", textParts(event.LLMResponse.Content))
            } else {
                fmt.Println("Agent responded.")
            }
        }
    }

    // Check the updated state
    resp, err := sessionService.Get(ctx, &session.GetRequest{AppName: appName, UserID: userID, SessionID: sessionID})
    if err != nil {
        log.Fatalf("Failed to get session: %v", err)
    }
    lastGreeting, _ := resp.Session.State().Get("last_greeting")
    fmt.Printf("State after agent run: last_greeting = %q\n\n", lastGreeting)
}

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

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

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

from google.adk.sessions import InMemorySessionService, Session
from google.adk.events import Event, EventActions
from google.genai.types import Part, Content
import time

# --- セットアップ ---
session_service = InMemorySessionService()
app_name, user_id, session_id = "state_app_manual", "user2", "session2"
session = await session_service.create_session(
    app_name=app_name,
    user_id=user_id,
    session_id=session_id,
    state={"user:login_count": 0, "task_status": "idle"}
)
print(f"初期状態: {session.state}")

# --- 状態変更の定義 ---
current_time = time.time()
state_changes = {
    "task_status": "active",              # セッション状態を更新
    "user:login_count": session.state.get("user:login_count", 0) + 1, # ユーザー状態を更新
    "user:last_login_ts": current_time,   # ユーザー状態を追加
    "temp:validation_needed": True        # 一時的な状態を追加(破棄されます)
}

# --- アクション付きのイベントを作成 ---
actions_with_update = EventActions(state_delta=state_changes)
# このイベントはエージェントの応答だけでなく、内部システムのアクションを表す場合があります
system_event = Event(
    invocation_id="inv_login_update",
    author="system", # または 'agent', 'tool' など
    actions=actions_with_update,
    timestamp=current_time
    # contentはNoneであるか、実行されたアクションを表す場合があります
)

# --- イベントの追加(これにより状態が更新されます) ---
await session_service.append_event(session, system_event)
print("明示的なstate deltaで`append_event`が呼び出されました。")

# --- 更新された状態の確認 ---
updated_session = await session_service.get_session(app_name=app_name,
                                            user_id=user_id,
                                            session_id=session_id)
print(f"イベント後の状態: {updated_session.state}")
# 期待される出力: {'user:login_count': 1, 'task_status': 'active', 'user:last_login_ts': <timestamp>}
# 注: 'temp:validation_needed'は存在しません。
//  2. manualStateUpdateExample demonstrates creating an event with explicit
//     state changes (a "state_delta") to update multiple keys, including
//     those with user- and temp- prefixes.
func manualStateUpdateExample(sessionService session.Service) {
    fmt.Println("--- Running Manual State Update (EventActions) Example ---")
    ctx := context.Background()
    s, err := sessionService.Get(ctx, &session.GetRequest{AppName: appName, UserID: userID, SessionID: sessionID})
    if err != nil {
        log.Fatalf("Failed to get session: %v", err)
    }
    retrievedSession := s.Session

    // Define state changes
    loginCount, _ := retrievedSession.State().Get("user:login_count")
    newLoginCount := 1
    if lc, ok := loginCount.(int); ok {
        newLoginCount = lc + 1
    }

    stateChanges := map[string]any{
        "task_status":            "active",
        "user:login_count":       newLoginCount,
        "user:last_login_ts":     time.Now().Unix(),
        "temp:validation_needed": true,
    }

    // Create an event with the state changes
    systemEvent := session.NewEvent("inv_login_update")
    systemEvent.Author = "system"
    systemEvent.Actions.StateDelta = stateChanges

    // Append the event to update the state
    if err := sessionService.AppendEvent(ctx, retrievedSession, systemEvent); err != nil {
        log.Fatalf("Failed to append event: %v", err)
    }
    fmt.Println("`append_event` called with explicit state delta.")

    // Check the updated state
    updatedResp, err := sessionService.Get(ctx, &session.GetRequest{AppName: appName, UserID: userID, SessionID: sessionID})
    if err != nil {
        log.Fatalf("Failed to get session: %v", err)
    }
    taskStatus, _ := updatedResp.Session.State().Get("task_status")
    loginCount, _ = updatedResp.Session.State().Get("user:login_count")
    lastLogin, _ := updatedResp.Session.State().Get("user:last_login_ts")
    temp, err := updatedResp.Session.State().Get("temp:validation_needed") // This should fail or be nil

    fmt.Printf("State after event: task_status=%q, user:login_count=%v, user:last_login_ts=%v\n", taskStatus, loginCount, lastLogin)
    if err != nil {
        fmt.Printf("As expected, temp state was not persisted: %v\n\n", err)
    } else {
        fmt.Printf("Unexpected temp state value: %v\n\n", temp)
    }
}
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に自動的に含まれます
    # ... コールバック/ツールの残りのロジック ...
//  3. contextStateUpdateExample demonstrates the recommended way to modify state
//     from within a tool function using the provided `tool.Context`.
func contextStateUpdateExample(sessionService session.Service) {
    fmt.Println("--- Running Context State Update (ToolContext) Example ---")
    ctx := context.Background()

    // Define the tool that modifies state
    updateActionCountTool, err := functiontool.New(
        functiontool.Config{Name: "update_action_count", Description: "Updates the user action count in the state."},
        func(tctx tool.Context, args struct{}) (struct{}, error) {
            actx, ok := tctx.(agent.CallbackContext)
            if !ok {
                log.Fatalf("tool.Context is not of type agent.CallbackContext")
            }
            s, err := actx.State().Get("user_action_count")
            if err != nil {
                log.Printf("could not get user_action_count: %v", err)
            }
            newCount := 1
            if c, ok := s.(int); ok {
                newCount = c + 1
            }
            if err := actx.State().Set("user_action_count", newCount); err != nil {
                log.Printf("could not set user_action_count: %v", err)
            }
            if err := actx.State().Set("temp:last_operation_status", "success from tool"); err != nil {
                log.Printf("could not set temp:last_operation_status: %v", err)
            }
            fmt.Println("Tool: Updated state via agent.CallbackContext.")
            return struct{}{}, nil
        },
    )
    if err != nil {
        log.Fatalf("Failed to create tool: %v", err)
    }

    // Define an agent that uses the tool
    modelTool, err := gemini.NewModel(ctx, modelID, nil)
    if err != nil {
        log.Fatalf("Failed to create Gemini model for tool agent: %v", err)
    }
    toolAgent, err := llmagent.New(llmagent.Config{
        Name:        "ToolAgent",
        Model:       modelTool,
        Instruction: "Use the update_action_count tool.",
        Tools:       []tool.Tool{updateActionCountTool},
    })
    if err != nil {
        log.Fatalf("Failed to create tool agent: %v", err)
    }

    r, err := runner.New(runner.Config{
        AppName:        appName,
        Agent:          agent.Agent(toolAgent),
        SessionService: sessionService,
    })
    if err != nil {
        log.Fatalf("Failed to create runner: %v", err)
    }

    // Run the agent to trigger the tool
    userMessage := genai.NewContentFromText("Please update the action count.", "user")
    for _, err := range r.Run(ctx, userID, sessionID, userMessage, agent.RunConfig{}) {
        if err != nil {
            log.Printf("Agent Error: %v", err)
        }
    }

    // Check the updated state
    resp, err := sessionService.Get(ctx, &session.GetRequest{AppName: appName, UserID: userID, SessionID: sessionID})
    if err != nil {
        log.Fatalf("Failed to get session: %v", err)
    }
    actionCount, _ := resp.Session.State().Get("user_action_count")
    fmt.Printf("State after tool run: user_action_count = %v\n", actionCount)
}
// エージェントのコールバックまたはツールメソッド内
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を更新します。
  • 同時更新に対するスレッドセーフティを保証します。

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

SessionServiceから直接取得したSessionオブジェクト(例:session_service.get_session()session_service.create_session()経由)上のsession.stateコレクション(辞書/Map)を、エージェント呼び出しの管理されたライフサイクルの外部で(つまり、CallbackContextToolContextを介さずに)直接変更することは避けてください。たとえば、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(イベントを手動で作成する場合)、またはそれぞれのスコープ内にあるときにCallbackContextToolContextオブジェクトのstateプロパティを変更することで状態を更新する方法に固執してください。これらの方法は、信頼性が高く、追跡可能で、永続的な状態管理を保証します。session.stateへの直接アクセス(SessionServiceから取得したセッションから)は、状態を読み取る場合にのみ使用してください。

状態設計のベストプラクティス再確認

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