【Claude Code 連載 第93回】セッションを外部ストレージに保存する(agent-sdk/session-storage)

スポンサーリンク
【Claude Code 連載 第93回】セッションを外部ストレージに保存する(agent-sdk/session-storage) 用語解説
【Claude Code 連載 第93回】セッションを外部ストレージに保存する(agent-sdk/session-storage)
この記事は約30分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

スポンサーリンク

背景

この連載では、Claude Codeの公式ドキュメントを1ページずつ読み解いていきます。公式ドキュメントは情報が網羅されている分、「結局どの機能を、どんな場面で使えばいいのか」は自分で考える必要があり、読むのに意外と時間がかかります。そこで、私が実務で使うために読み込んだ内容を「使う場面→実例コード」の順に整理して残していくことにしました。専門家の解説というより、一次情報を読んだ記録です。推測や動作を確認していない部分には、その都度そう書きます。

公式ドキュメント:https://code.claude.com/docs/ja/agent-sdk/session-storage

1. 一言でいうと

Agent SDK は通常、セッションの記録(トランスクリプト)を自分のマシンの ~/.claude/projects/ に JSONL ファイルとして書きます。SessionStore アダプターを使うと、その記録を S3 のようなオブジェクトストア、Redis のようなキーバリューストア、Postgres のようなデータベースにも書き写せます。そうすると、あるマシンで始めたセッションを、別のマシンから再開できるようになります。

第87回で「セッションのファイルは作ったマシンにしか無い。別のマシンで再開するにはセッションストアを使う」と見ました。今回はその仕組みの回です。


2. どういう場面で役立つか

シーン1:サーバーレスやオートスケールの環境で会話を続ける

サーバーレスの関数、台数が増減するワーカー、CI のランナーは、ファイルシステムを共有しません。ユーザーの1回目の問い合わせをマシン A が、2回目をマシン B が処理する、ということが普通に起きます。共有のストアに記録を置けば、どのマシンでも前回の続きから再開できます。

シーン2:コンテナを作り直しても記録を残す

コンテナは一時的なもので、再起動や再デプロイで中身が消えます。外部のストアに置いた記録は、それを越えて残ります。

シーン3:監査やコンプライアンスのために記録を管理する

会話の記録を、すでに管理しているストレージに置けば、自社の保持期間のルール、暗号化、アクセス制御をそのまま当てはめられます。

不要・向かないケース

  • 1台のマシンで完結するアプリ:ローカルのファイルで足ります(第87回)。
  • ファイルチェックポイントを使うアプリ:ストアと一緒に使えません。起動時にエラーになります(後述)。
  • 会話の記録を残したくない:ストアは記録を残す仕組みです。残さないなら TypeScript の persistSession: false などを使いますが、これもストアとは一緒に使えません(後述)。
  • 再開に頼らなくても済む設計:第87回で見たとおり、必要な結果だけをアプリの状態として保存して次のセッションに渡すほうが、記録を丸ごと運ぶより堅牢なことが多いと原文は書いていました。

3. コードと仕組みの解説

原文の fenced コードブロックは7本です(インターフェースの対訳、クイックスタートの対訳、S3 アダプターをつなぐ TypeScript の例、適合テストの準備と実行)。全数を引用します。コードは原文から機械的に抜き出したもので、入れ子に由来する先頭インデントを外した以外は変えていません。インターフェースのコードにある ... は、Python の型定義の書き方(中身を書かない印)として原文にあるものです。

3-0. バージョン要件

挙動必要バージョン
env で CLAUDE_CODE_PROJECT_DIR_NAME を設定したとき、そのクエリのエントリと resume・continue の検索をその名前で行うAgent SDK v0.3.234 以降
ストアから再開するとき、settings.json から additionalMarketplaces(extraKnownMarketplaces の別名)も取り除くTypeScript Agent SDK v0.3.232 以降
ストアから再開するとき、ユーザーの settings.json も一時ディレクトリにコピーする(それ以前は認証情報と .claude.json だけ)TypeScript Agent SDK v0.3.222 以降

3-1. SessionStore の形

SessionStore は、必須のメソッド2つ(append と load)と、任意のメソッド4つを持つオブジェクトです。SDK は、問い合わせの間に append で記録の項目を書き込み、再開のときに load で読み戻します。

// Exported from @anthropic-ai/claude-agent-sdk as
// SessionStore, SessionKey, SessionStoreEntry, SessionSummaryEntry.

type SessionKey = {
  projectKey: string;
  sessionId: string;
  subpath?: string;
};

type SessionStore = {
  // Required
  append(key: SessionKey, entries: SessionStoreEntry[]): Promise<void>;
  load(key: SessionKey): Promise<SessionStoreEntry[] | null>;

  // Optional
  listSessions?(
    projectKey: string,
  ): Promise<Array<{ sessionId: string; mtime: number }>>;
  listSessionSummaries?(projectKey: string): Promise<SessionSummaryEntry[]>;
  delete?(key: SessionKey): Promise<void>;
  listSubkeys?(key: {
    projectKey: string;
    sessionId: string;
  }): Promise<string[]>;
};

type SessionSummaryEntry = {
  sessionId: string;
  mtime: number;
  data: Record<string, unknown>;
};
# Exported from claude_agent_sdk as
# SessionStore, SessionKey, SessionStoreEntry, SessionSummaryEntry.

class SessionKey(TypedDict):
    project_key: str
    session_id: str
    subpath: NotRequired[str]

class SessionStore(Protocol):
    # Required
    async def append(
        self, key: SessionKey, entries: list[SessionStoreEntry]
    ) -> None: ...
    async def load(self, key: SessionKey) -> list[SessionStoreEntry] | None: ...

    # Optional — omit or raise NotImplementedError
    async def list_sessions(
        self, project_key: str
    ) -> list[SessionStoreListEntry]: ...
    async def list_session_summaries(
        self, project_key: str
    ) -> list[SessionSummaryEntry]: ...
    async def delete(self, key: SessionKey) -> None: ...
    async def list_subkeys(self, key: SessionListSubkeysKey) -> list[str]: ...

class SessionSummaryEntry(TypedDict):
    session_id: str
    mtime: int
    data: dict[str, Any]

TypeScript は型(type)として、Python は Protocol と TypedDict で定義しています。フィールド名は TypeScript が projectKey・sessionId、Python が project_key・session_id と、それぞれの言語の書き方です。Python では任意のメソッドは「省略するか、NotImplementedError を投げる」と書かれています。

SessionKey:どの記録かを指すキー

SessionKey は1つの記録を指します。

フィールド意味
projectKey作業ディレクトリを、ファイル名として安全な形に安定して変換したもの
sessionIdセッションの UUID
subpathサブエージェントの記録などを指すときに付く、キーの後ろの部分

subpath の説明は、原文の日本語訳では「メイン会話に属する場合に設定される」と読めます。しかし同じページのすぐ後で「subpath が未定義の場合、キーはメイントランスクリプトを参照する」「サブエージェントの記録は subpath: "subagents/agent-<id>" の下に写される」と書かれているので、subpath が付くのはサブエージェントの記録やサイドカーのファイルのとき、付かないのがメインの会話のときと読むのが正しいと考えます。前者の一文は訳の段階で意味が逆になった可能性があります(筆者の指摘)。subpath は中身を解釈しない「キーの後ろに付く文字列」として扱い、ディスク上の配置(例:subagents/agent-<id>)に従います。

projectKey は作業ディレクトリから作られるので、ストアから再開・続行するときは、元の実行と同じ作業ディレクトリから行う必要があります(第87回で見た「同じ cwd から再開する」)。

マシンによって作業ディレクトリのパスが違う環境では、CLAUDE_CODE_PROJECT_DIR_NAME で名前を固定できます。TypeScript では、クエリの env オプションで CLAUDE_CONFIG_DIR と一緒にこれを設定すると、SDK はそのクエリの記録と、resume・continue の検索をその名前で行います(v0.3.234 以降)。ただし、listSessions や deleteSession のような単独で呼ぶ関数は env を受け取らず、プロセスの環境変数を読むので、ホストのプロセスの環境にも CLAUDE_CONFIG_DIR と同じ名前を設定しておく必要があります。

各メソッドが呼ばれるとき

メソッド必須呼ばれるとき
appendはい記録の項目のまとまり(バッチ)がローカルに書かれた後、毎回。項目は JSON として安全なオブジェクトで、ローカルの JSONL では1行に1つ
loadはいresume を指定したとき、または continue: true がサブプロセスを起動する前にストアの最新セッションを決めるとき。一覧が listSessionSummaries から代わりの方法に切り替わったときはセッションごとに1回。知らないセッションなら null を返す
listSessionsいいえlistSessions({ sessionStore }) と、continue: true を付けた query()/startup() から。未定義だと continue: true は例外を投げる。listSessions({ sessionStore }) も、listSessionSummaries が無ければ例外を投げる
listSessionSummariesいいえlistSessions({ sessionStore }) から、全セッションのメタデータを1回で読むため。要約は append の中で保存しておく。未定義なら、一覧は listSessions とセッションごとの load で代わりに作られる
deleteいいえdeleteSession({ sessionStore }) から。メインのキー(subpath なし)を消すときは、そのセッションの全てのサブキーと要約の項目も消す必要がある(消したセッションが一覧に出ないように)。未定義なら削除は何もしない。追記しかできないバックエンド向け
listSubkeysいいえ再開のときに、サブエージェントの記録を見つけるため。未定義だとメインの記録だけが戻る

最低限の実装(append と load だけ)でも resume は動きますが、continue: true を使うなら listSessions が、サブエージェントの記録まで戻したいなら listSubkeys が必要です。どの機能を使うかで、実装すべきメソッドが変わります(表からの筆者の整理)。

要約の項目(SessionSummaryEntry)の作り方

listSessionSummaries を実装する場合、SessionSummaryEntry の mtime はサイドカー(要約)を保存した時刻で、listSessions が返す mtime と同じ時計を使う必要があります。data は SDK が持つ中身の分からない状態なので、解釈せずにそのまま保存します。

項目を作るには、append の中で、バッチごとに SDK が用意している foldSessionSummary(Python は fold_session_summary)を呼びます。その際の注意は次のとおりです。

  • subpath を持つキーのバッチは飛ばす。サブエージェントの記録をメインのセッションの要約に混ぜてはいけない
  • この関数は mtime を設定しないので、保存するときに付ける(TypeScript は options.mtime 引数、Python は返された項目のフィールドを上書き)
  • 同じセッションへの append が同時に走ると、要約の読み書きがぶつかりうる。トランザクション、compare-and-swap、セッションごとのロックで「読む→畳み込む→書く」を直列にする。畳み込み自体は副作用の無い純粋な処理

3-2. クイックスタート

SDK には、開発とテスト用の InMemorySessionStore(メモリ上に置くストア)があります。次の例は、ストアをつないで問い合わせ、結果メッセージからセッション ID を取り、2回目の query() でストアから再開します。2回目は同じストアの実体と resume を渡すので、SDK はローカルのファイルではなくストアから記録を読みます。

import { query, InMemorySessionStore } from "@anthropic-ai/claude-agent-sdk";

const store = new InMemorySessionStore();

let sessionId: string | undefined;
try {
  for await (const message of query({
    prompt: "List the TypeScript files under src/",
    options: { sessionStore: store },
  })) {
    if (message.type === "result") {
      sessionId = message.session_id;
    }
  }
} catch (error) {
  // A single-shot query() throws after yielding an error result. If the
  // failure was an error result, sessionId was already captured by the loop
  // above; connection or process failures yield no result message.
  console.error(`Session ended with an error: ${error}`);
}

// Resume from the store. The agent has full context from the first call.
for await (const message of query({
  prompt: "Summarize what those files do",
  options: { sessionStore: store, resume: sessionId },
})) {
  if (message.type === "result" && message.subtype === "success") {
    console.log(message.result);
  }
}
import asyncio
from claude_agent_sdk import (
    ClaudeAgentOptions,
    InMemorySessionStore,
    ResultMessage,
    query,
)

store = InMemorySessionStore()


async def main():
    session_id = None
    try:
        async for message in query(
            prompt="List the Python files under src/",
            options=ClaudeAgentOptions(session_store=store),
        ):
            if isinstance(message, ResultMessage):
                session_id = message.session_id
    except Exception as error:
        # A single-shot query() raises after yielding an error result. If the
        # failure was an error result, session_id was already captured by the
        # loop above; connection or process failures yield no result message.
        print(f"Session ended with an error: {error}")

    # Resume from the store. The agent has full context from the first call.
    async for message in query(
        prompt="Summarize what those files do",
        options=ClaudeAgentOptions(session_store=store, resume=session_id),
    ):
        if isinstance(message, ResultMessage) and message.subtype == "success":
            print(message.result)


asyncio.run(main())

2回目の問い合わせは1回目のファイルの要約を表示し、エージェントがストアから文脈を全部持って再開したことが分かります。

オプション名は TypeScript が sessionStore、Python が session_store です。InMemorySessionStore はプロセスのメモリに置くので、プロセスが終われば消え、別のマシンとも共有できません。仕組みを確かめるためのもので、本番では次の外部ストアのアダプターを使います(筆者の補足)。

3-3. 自分のアダプターを作る

自分のバックエンド向けに append と load を実装します。ストアに対して listSessions()、1回でのメタデータの読み込み、deleteSession()、サブエージェントの再開を使いたいなら、listSessions・listSessionSummaries・delete・listSubkeys も足します。

append に渡される項目は SessionStoreEntry({ type: string; ... } の形のオブジェクト)です。守るべき約束は次のとおりです。

  • 項目は中身を解釈しない JSON 値として扱う
  • 順序を保って保存し、load で同じ順序で返す
  • load は、追記したものと中身が等しい項目を返す。バイト単位で同じ書き出しである必要はないので、オブジェクトのキーの順番を並べ替えるバックエンド(バイナリ JSON の列型など)でも構わない

参考実装

両方の SDK のリポジトリに、動かせる参考のアダプターがあります。TypeScript は examples/session-stores/、Python は examples/session_stores/ の下です。ストレージの種類ごとに1つずつあり、append と load がその種類のバックエンドにどう対応するかを示しています。パッケージとしては公開されていないので、自分のバックエンドに一番近いものをプロジェクトにコピーし、バックエンドのクライアントを入れて手直しします。

種類保存の形例
オブジェクトストアappend() ごとに1つの部分ファイル。load() は部分を一覧し、並べ替えてつなげるS3
キーバリューストア記録ごとに1つのリスト。append() で追加し、load() で範囲を読む。加えてセッションの並べ替え済みの索引を持つRedis
リレーショナル DB/ドキュメントストア項目ごとに1行(1ドキュメント)。JSON として保存し、挿入時に割り当てたキーで順序を付けるPostgres

各アダプターは設定済みのクライアントを受け取るので、認証情報、TLS、リージョン、接続プールを自分で決められます。次の例は、オブジェクトストアのアダプターを query() につなぎ、別のマシンから再開する方法です。

import { query } from "@anthropic-ai/claude-agent-sdk";
import { S3Client } from "@aws-sdk/client-s3";
import { S3SessionStore } from "./S3SessionStore"; // copied from examples/session-stores/s3

const store = new S3SessionStore({
  bucket: "my-claude-sessions",
  prefix: "transcripts",
  client: new S3Client({ region: "us-east-1" }),
});

for await (const message of query({
  prompt: "Hello!",
  options: { sessionStore: store },
})) {
  if (message.type === "result" && message.subtype === "success") {
    console.log(message.result);
  }
}

// Later, possibly on a different host:
for await (const message of query({
  prompt: "Continue where we left off",
  options: { sessionStore: store, resume: "previous-session-id" },
})) {
  // ...
}

S3SessionStore は、参考実装の examples/session-stores/s3 からコピーしたものです(コメントのとおり)。バケット名と保存先の接頭辞(transcripts)と、リージョンを指定した S3 のクライアントを渡しています。2つ目の query() は「あとで、たぶん別のマシンで」実行するもので、同じストアと resume に前のセッション ID を渡しています。

アダプターを検証する

両方の SDK に、append・load・任意のメソッドが守るべき振る舞いの約束を確かめる適合テストがあります。任意のメソッドのテストは、そのメソッドを実装していなければ自動で飛ばされます。

  • TypeScript:例のディレクトリにある shared/conformance.ts を、自分のテストにコピーします。
  • Python:テストはパッケージに入っています。pytest で動かすには、まず pytest を入れます(pytest は SDK の依存関係ではありません)。
pip install pytest

そのうえで、テストのファイルでアダプターをテストに渡します。引数なしで呼べるファクトリとして渡し、run_session_store_conformance は約束ごとに1回ずつ呼んで新しいストアを作ります。

import pytest
from claude_agent_sdk.testing import run_session_store_conformance


@pytest.mark.anyio
async def test_my_store_conformance():
    await run_session_store_conformance(MyRedisStore)

この例のように MyRedisStore のクラス自体を渡せるのは、コンストラクターが引数を取らない場合です。設定済みのクライアントを受け取るアダプターでは、ストアを作るラムダを渡します。テストは同じセッションキーを使い回すので、ファクトリが返す各ストアは空の状態で始まる必要があります。新しいメモリ上の偽物、他と重ならないキーの接頭辞、新しいテスト用データベースなど、呼び出しごとに分かれた保存先を用意します。

3-4. 振る舞いの注意

二重書き込み:ストアはローカルの「写し」

Claude Code のサブプロセスは、記録の各バッチを必ず先にローカルのディスクに書き、その後で SDK が同じバッチをストアの append() に渡します。つまりストアはローカルの記録の写しであって、置き換えではありません。どちらの写しが実行の後まで残るかは、実行の始め方で決まります。

実行の始め方残るもの
新しいセッション、またはストアにそのセッションが無い状態での再開設定ディレクトリの下のローカルの記録が残り、ストアにも写しが行く
ストアから再開(3-4 の次項)ローカルの写しは実行の終わりに消えるので、ストアが唯一の永続的な写しになる

新しいセッションでもローカルのディスクに記録を残したくない場合は、options.env で CLAUDE_CONFIG_DIR を一時ディレクトリにします(ストアから再開する実行はもともとローカルの写しを消すので不要)。TypeScript では env が子プロセスの環境を置き換えるので、process.env を展開して入れます(第79回)。

ログインの注意。 アプリが設定ディレクトリの中のファイル(OAuth の認証情報や、ユーザーの settings.json の apiKeyHelper など)でサインインしている場合、CLAUDE_CONFIG_DIR を一時ディレクトリに変えると、それらのファイルが見つからなくなります。先にそれらを一時ディレクトリにコピーするか、代わりに env で ANTHROPIC_API_KEY を設定します。そうしないと実行は Not logged in で失敗します。

ストアと一緒に使えないオプションが2つあります。どちらかをストアと組み合わせると、SDK は起動時に例外を投げます。

オプション理由
TypeScript の persistSession: false写しの元になるローカルへの書き込みを止めてしまう。Python にはこれに当たるオプションが無い
ファイルチェックポイント(TypeScript は enableFileCheckpointing、Python は enable_file_checkpointing)ファイルのバックアップをローカルのディスクに直接書き、SDK はそれをストアに写さない

2つ目は、第88回のファイルチェックポイントとセッションストアのどちらかを選ぶ必要がある、ということです。複数のマシンで動かすアプリでファイルの変更を戻したいなら、git など別の仕組みが必要になります(第88回との組み合わせによる筆者の指摘)。

ストアから再開するときの動き

resume、または continue: true(Python は continue_conversation=True)をストアと一緒に渡すと、SDK はサブプロセスを起動する前に、ストアに記録を求めます。

  • resume:渡したセッション ID のセッションを求める
  • continue: true/continue_conversation=True:ストアの最新のセッションを求める

ストアが記録を返すと、SDK はそれを一時的な設定ディレクトリに書き、CLAUDE_CONFIG_DIR がそこを指すようにしてサブプロセスを動かし、実行の終わりにディレクトリごと消します。その実行が書いたローカルの記録も一緒に消えます。これが、この経路ではストアが唯一の永続的な写しになる理由です。

SDK は、本当の設定ディレクトリのファイルを一時ディレクトリにも用意します。何をコピーするかが言語で違います。

言語コピーするもの結果
TypeScript認証情報、.claude.json、ユーザーの settings.json。ただし settings.json から、一時ディレクトリでうまく動かないキー(enabledPlugins、extraKnownMarketplaces とその別名 additionalMarketplaces、ファイルの env ブロック内の CLAUDE_CONFIG_DIR)を取り除くapiKeyHelper など設定による認証は、ストアから再開しても動く
Python認証情報と .claude.json だけユーザーの settings.json の apiKeyHelper で認証するアプリは、ストアから再開すると Not logged in で失敗する。管理設定やプロジェクトの設定の apiKeyHelper は、CLAUDE_CONFIG_DIR の影響を受けない場所から読まれるので動く

TypeScript が enabledPlugins などを取り除くということは、ストアから再開した実行では、ユーザーの設定で有効にしていたプラグインが読み込まれないことになりそうです(取り除くキーからの筆者の読み)。

ストアにそのセッションが無い場合、SDK は本当の設定ディレクトリの下で動き、結果は渡したオプションで変わります。

オプション動き
resume両言語とも ID をサブプロセスに渡す。サブプロセスは、ストアが無いときと同じようにローカルの記録を再開する
TypeScript の continue: true新しいセッションを始める
Python の continue_conversation=True最新のローカルのセッションから続ける

continue の振る舞いが言語で逆になっている点は要注意です。ストアに何も無いとき、TypeScript は新しく始め、Python はローカルの最新に続けます。複数のマシンで動かすアプリで Python を使う場合、たまたまそのマシンに残っていた別の会話に続いてしまう可能性があります(表からの筆者の指摘)。

ストアへの書き込みはベストエフォート

append() が失敗した場合、SDK は短い間隔をあけて最大2回再試行します(合計で最大3回)。タイムアウトした呼び出しは再試行しません。元の呼び出しがまだ届く可能性があるからです。それでも失敗したバッチについては、SDK は次のように動きます。

  • エラーをログに出す
  • { type: "system", subtype: "mirror_error" } のメッセージを流す
  • そのバッチを捨てて、問い合わせを続ける

再試行したバッチは、すでに届いていた項目をもう一度届けることがあるので、append() の実装で entry.uuid を使って重複を除きます。

ストアの障害はエージェントを止めません。サブプロセスが先にローカルに書いているからです。ストアのデータの欠落を見つけたいなら mirror_error を見張ります。ストアから再開した実行では、捨てられたバッチは実行の後に残る写しがどこにもありません。

監査やコンプライアンスのためにストアを使う場合、この「黙って続ける」振る舞いは重要です。記録が欠けていても処理は成功するので、mirror_error を受け取ったら警報を出すなど、アプリ側で必ず拾う仕組みが要ります(筆者の指摘)。

getSessionMessages は圧縮後のつながりを返す

getSessionMessages({ sessionStore }) は、エージェントが再開時に見る、つながったメッセージの列を返します。自動のコンパクション(要約)の後は、前のターンが要約に置き換わっているので、ストアには503個の生の項目があっても、getSessionMessages からは18個のメッセージしか返らない、ということが起きます。コンパクション前のターンやメタデータの項目を含む生の履歴を全部見たいなら、store.load(key) を直接呼びます。監査で全履歴が必要なら、こちらを使うことになります。

forkSession はそのままのコピーではない

forkSession({ sessionStore }) は、元の項目を読み、全ての sessionId フィールドを書き換え、メッセージの UUID を付け替えてから、変換した項目を新しいキーの下に追記します。アダプターのレベルでのコピーや、S3 の CopyObject のような近道は、古いセッション ID を指す記録を作ってしまうので、SDK は使いません。fork を自前で実装しようとしてストレージの中身をそのまま複製してはいけない、ということです。

サブエージェントの記録

サブエージェントの記録は subpath: "subagents/agent-<id>" の下に写されます。

  • listSubagents({ sessionStore }) は、アダプターが listSubkeys を実装している必要がある
  • getSubagentMessages({ sessionStore }) は、あれば listSubkeys を使い、無ければその subpath を直接読む
  • 再開でも listSubkeys を呼んでサブエージェントのファイルを戻す。無ければメインの記録だけが復元される

第89回で、サブエージェントは再開できる(agentId で続きを頼める)と見ました。ストア経由でそれをするなら listSubkeys の実装が必要です(第89回との対応づけは筆者)。

保持

SDK が自分の判断でストアから消すことはありません。 保持期間の管理はアダプターの責任です。コンプライアンスの要件に合わせて、バックエンドの有効期限やライフサイクルの仕組みを使うか、定期的な掃除を走らせます。

CLAUDE_CONFIG_DIR の下のローカルの記録は、cleanupPeriodDays の設定によって別に掃除されます(第66・68回で見た既定30日の設定)。ストアから再開した実行はローカルの記録を残さないので、そうした実行ではストアの保持期間だけが唯一の保持です。

記録には、エージェントが読んだファイルの中身やツールの結果も入りえます(第87回)。ストアに置くなら、暗号化とアクセス制御、保持期間の設定は最初から決めておくべきです(筆者の指摘)。

3-5. 対応している関数

TypeScript では、次の関数が sessionStore オプションを受け取り、渡されればローカルのファイルではなくストアを相手に動きます。

query()、startup()、listSessions()、getSessionInfo()、getSessionMessages()、renameSession()、tagSession()、deleteSession()、forkSession()、listSubagents()、getSubagentMessages()

Python では、ClaudeAgentOptions の session_store を設定すると、query() がストアを相手に動きます。それ以外の操作には、ストアを引数に取る専用の関数があります。

操作Python のストア対応の関数
一覧list_sessions_from_store()
情報get_session_info_from_store()
メッセージget_session_messages_from_store()
サブエージェントの一覧list_subagents_from_store()
サブエージェントのメッセージget_subagent_messages_from_store()
名前の変更rename_session_via_store()
タグ付けtag_session_via_store()
削除delete_session_via_store()
forkfork_session_via_store()

startup() には Python の同等品がありません。Python リファレンスに載っている list_sessions() などの単独の関数は、ローカルのセッションファイルを読みます。第87回で見た list_sessions() をストアを使うアプリでそのまま呼ぶと、ストアではなく手元のファイルを見てしまう、ということです。TypeScript は同じ関数に sessionStore を渡す形、Python は名前の違う関数を使う形、と覚えておくと取り違えにくくなります(筆者の整理)。


4. まとめ

  • SessionStore は、セッションの記録をローカルの JSONL に加えて自分のバックエンドに写す仕組み。同じ作業ディレクトリからなら、別のマシンで再開できる。
  • 必須は append(バッチごとに追記)と load(再開時に読む。知らなければ null)。continue には listSessions、サブエージェントの復元には listSubkeys、削除には delete、一覧の高速化には listSessionSummaries が要る。
  • 項目は解釈しない JSON として順序を保って保存し、中身が等しいものを返す。重複は entry.uuid で除く。要約の読み書きは直列にする。
  • 開発には InMemorySessionStore。本番は S3・Redis・Postgres の参考実装をコピーして手直しし、適合テストで確かめる。
  • ストアは写しで、ローカルが先に書かれる。ストアから再開した実行ではローカルの写しは消え、ストアが唯一の記録になる。
  • 書き込みはベストエフォート。最大3回試して失敗したバッチは捨てられ、問い合わせは続く。mirror_error を見張る。
  • persistSession: false とファイルチェックポイントは、ストアと一緒に使えない。
  • ストアから再開すると一時的な設定ディレクトリで動く。Python はユーザーの settings.json をコピーしないので、そこの apiKeyHelper では Not logged in になる。ストアに無いときの continue は TypeScript が新規、Python がローカルの最新。
  • getSessionMessages は圧縮後の列、全履歴は store.load()。fork は ID を書き換えるのでそのままのコピーではない。
  • SDK はストアから消さない。保持期間はアダプター側で決める。Python のストア操作は *_from_store()/*_via_store() という別の関数。

次回予告

次回は 「SDK をホストする」(agent-sdk/hosting) を取り上げる予定です。

今回、サーバーレスの関数や台数が増減するワーカーで会話を続けるために、セッションの記録を共有のストアに置く方法を見ました。第91回でも、Lambda のようなステートレスな環境ではシングルメッセージ入力が向くと触れています。次回は、SDK で作ったエージェントを実際にサーバーやコンテナでどう動かすか、複数のマシンにまたがる配置の考え方を見ていきます。


よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました