Claude Media
Agent SDKのセッション管理 — continue・resume・forkの使い分け

Agent SDKのセッション管理 — continue・resume・forkの使い分け

Agent SDKのセッションはディスクに自動保存されます。continue・resume・forkの違いと、Python/TypeScriptでの実装差分を手順で押さえます。

この記事で学ぶこと — continue・resume・forkの3つの選択肢

セッションとは、Agent SDKがエージェントの作業中に積み上げていく会話履歴です。プロンプト、エージェントが行ったすべてのツール呼び出しとその結果、すべての応答が含まれ、SDKは自動的にディスクへ書き出します。セッションに戻るとは、エージェントが以前の文脈をすべて持った状態に戻ることです。すでに読んだファイル、すでに終えた分析、すでに下した判断を、読み直さずに引き継げます。

セッションが持つのは会話だけで、ファイルシステムは含みません。エージェントが行ったファイル変更をスナップショットして元に戻したい場合は、ファイルチェックポイント機能を使います。この記事が扱うのは会話履歴の管理だけです。

どれだけセッション管理が必要かは、アプリケーションの形で変わります。1回の query() の中では、エージェントはすでに必要なだけターンを重ねますし、権限確認や AskUserQuestion もループの中で処理されて呼び出しを終了させません。セッション管理が効いてくるのは、共有すべき文脈を持つ複数のプロンプトを送るときです。

作りたいもの使うもの
1回のプロンプトで完結する単発タスク使うもの追加設定は不要。query() を1回呼べば済む
1プロセス内でのマルチターンなチャット使うものPythonは ClaudeSDKClient、TypeScriptは continue: true
プロセス再起動後に続きから再開使うものcontinue_conversation=True(Python)/ continue: true(TypeScript)。ディレクトリ内の最新セッションを、IDなしで再開する
最新ではない特定の過去セッションに戻る使うものセッションIDを控えて resume に渡す
元のセッションを失わずに別の方向性を試す使うものセッションをforkする
何もディスクに書き残したくない単発タスク使うものTypeScriptは persistSession: false。Pythonは envCLAUDE_CODE_SKIP_PROMPT_HISTORY を設定してトランスクリプト書き込みを抑止する

continueresume はどちらも既存のセッションに追記しますが、セッションの見つけ方が違います。continueはカレントディレクトリで最も新しいセッションを見つけます。IDを追跡する必要がなく、1度に1つの会話しか動かさないアプリに向きます。resumeは特定のセッションIDを指定します。IDを自分で管理する必要がありますが、マルチユーザーアプリでユーザーごとに1セッションを持つような、複数セッションが並存する場面や、最新ではないセッションに戻りたい場面で必須です。forkはこの2つとは性質が違い、元のセッションの履歴をコピーした新しいセッションを作ります。元のセッションは変更されないままなので、別方向を試しつつ戻る選択肢を残せます。

前提

  • Python版・TypeScript版どちらのAgent SDKでも同じ考え方が使えますが、コード例は言語ごとに分けて示します
  • セッションはローカルの ~/.claude 配下に保存されるため、実行ユーザーがそのディレクトリに書き込めることが前提です
  • v2.1.223以降のSDKと、それより前のバージョンとでresumeの探索範囲が異なります(ステップ2で詳述)

ステップ1: 自動セッション管理で会話を継続する

Python・TypeScriptとも、呼び出しをまたいでセッション状態を追跡してくれるインターフェースがあり、IDを自分で受け渡す必要がありません。1プロセス内のマルチターンな会話に向きます。

Pythonでは ClaudeSDKClient がセッションIDを内部で処理します。client.query() を呼ぶたびに同じセッションが自動的に継続され、client.receive_response() で現在のクエリのメッセージを反復処理します。非同期コンテキストマネージャーとして使えば、接続の確立と終了処理も任せられます。

import asyncio
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
 
 
async def main():
    options = ClaudeAgentOptions(allowed_tools=["Read", "Edit", "Glob", "Grep"])
 
    async with ClaudeSDKClient(options=options) as client:
        # 1回目: クライアントが内部でセッションIDを捕捉する
        await client.query("認証モジュールを分析して")
        async for message in client.receive_response():
            print(message)
 
        # 2回目: 同じセッションを自動的に継続する
        await client.query("JWTを使うようにリファクタリングして")
        async for message in client.receive_response():
            print(message)
 
 
asyncio.run(main())

TypeScriptにはPythonの ClaudeSDKClient に相当するセッション保持オブジェクトはありません。代わりに、以降の query() 呼び出しで continue: true を渡すと、SDKがカレントディレクトリの最新セッションを見つけて引き継ぎます。IDの追跡は不要です。

import { query } from "@anthropic-ai/claude-agent-sdk";
 
// 1回目: 新しいセッションを作る
for await (const message of query({
  prompt: "認証モジュールを分析して",
  options: { allowedTools: ["Read", "Glob", "Grep"] },
})) {
  if (message.type === "result" && message.subtype === "success") {
    console.log(message.result);
  }
}
 
// 2回目: continue: trueで最新セッションを再開する
for await (const message of query({
  prompt: "JWTを使うようにリファクタリングして",
  options: { continue: true, allowedTools: ["Read", "Edit", "Write", "Glob", "Grep"] },
})) {
  if (message.type === "result" && message.subtype === "success") {
    console.log(message.result);
  }
}

ステップ2: セッションIDを取得してresumeで特定の会話に戻る

resumeとforkにはセッションIDが要ります。IDは結果メッセージ(Pythonの ResultMessage、TypeScriptの SDKResultMessage)の session_id フィールドから読み取り、成功・エラーを問わずすべての結果に含まれます。TypeScriptでは初期化時の SystemMessage にも直接のフィールドとしてより早く現れますが、Pythonでは SystemMessage.data の中にネストされています。

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
 
 
async def main():
    session_id = None
    async for message in query(
        prompt="認証モジュールを分析して改善案を出して",
        options=ClaudeAgentOptions(allowed_tools=["Read", "Glob", "Grep"]),
    ):
        if isinstance(message, ResultMessage):
            session_id = message.session_id
            if message.subtype == "success":
                print(message.result)
 
    return session_id
 
 
session_id = asyncio.run(main())

セッションIDを resume に渡すと、その特定のセッションに戻ります。エージェントは、セッションが終わった時点からの文脈をすべて持った状態で再開します。resumeが要る典型的な場面は次の3つです。

  1. 完了したタスクにフォローアップするとき: ファイルを読み直させずに、すでに出た分析結果へ行動させたい場合
  2. 上限に達した実行から復帰するとき: error_max_turnserror_max_budget_usd で終わった実行を、より高い上限で再開する場合
  3. プロセスを再起動したとき: シャットダウン前に控えておいたIDで会話を復元する場合
session_id = "..."  # 前の手順で捕捉したID
 
async def main():
    async for message in query(
        prompt="さっき提案したリファクタリングを実装して",
        options=ClaudeAgentOptions(
            resume=session_id,
            allowed_tools=["Read", "Edit", "Write", "Glob", "Grep"],
        ),
    ):
        if isinstance(message, ResultMessage) and message.subtype == "success":
            print(message.result)
const sessionId = "..."; // 前の手順で捕捉したID
 
for await (const message of query({
  prompt: "さっき提案したリファクタリングを実装して",
  options: { resume: sessionId, allowedTools: ["Read", "Edit", "Write", "Glob", "Grep"] },
})) {
  if (message.type === "result" && message.subtype === "success") {
    console.log(message.result);
  }
}

Claude Codeはセッションを ~/.claude/projects/<encoded-cwd>/*.jsonl の下に保存します。CLAUDE_CONFIG_DIR を設定していれば、その配下の projects/ を探します。作業ディレクトリの絶対パスに含まれる英数字以外の文字をすべて - に置き換えた名前がディレクトリ名になり、/Users/me/proj なら -Users-me-proj です。resumeはカレントディレクトリ以外からも呼べます。Claude Codeはカレントプロジェクトディレクトリを越えてIDを探索します(v2.1.223より前のバージョンを同梱するSDKでは、探索範囲がカレントプロジェクトディレクトリとそのgit worktreeに限られる挙動のままです)。ただし、セッションファイル自体は作成したマシン上に存在する必要があります。

ステップ3: forkで元のセッションを壊さず別のアプローチを試す

forkは、元のセッションの履歴をコピーした新しいセッションを作り、そこから分岐します。forkされた側は独自のセッションIDを持ち、元のセッションのIDと履歴は変わりません。結果として、それぞれ独立に再開できる2つのセッションが手に入ります。

すでに認証モジュールを分析したセッション(session_id)があり、JWTの検討スレッドを失わずにOAuth2も検討したいとします。次の例では、まずセッションをforkしてforkのID(forked_id)を捕捉し、続けて元の session_id をresumeしてJWTの検討を続けます。

session_id = "..."  # 前の手順で捕捉したID
 
async def main():
    # fork: session_idから分岐した新しいセッションを作る
    forked_id = None
    async for message in query(
        prompt="JWTではなくOAuth2で認証モジュールを組む場合の設計を出して",
        options=ClaudeAgentOptions(resume=session_id, fork_session=True, max_turns=5),
    ):
        if isinstance(message, ResultMessage):
            forked_id = message.session_id  # session_idとは別のID
            if message.subtype == "success":
                print(message.result)
 
    # 元のセッションは変わっていないので、resumeでJWTの検討を続けられる
    async for message in query(
        prompt="JWTの方針で続けて",
        options=ClaudeAgentOptions(resume=session_id),
    ):
        if isinstance(message, ResultMessage) and message.subtype == "success":
            print(message.result)
const sessionId = "..."; // 前の手順で捕捉したID
 
let forkedId: string | undefined;
for await (const message of query({
  prompt: "JWTではなくOAuth2で認証モジュールを組む場合の設計を出して",
  options: { resume: sessionId, forkSession: true, maxTurns: 5 },
})) {
  if (message.type === "system" && message.subtype === "init") {
    forkedId = message.session_id; // sessionIdとは別のID
  }
  if (message.type === "result" && message.subtype === "success") {
    console.log(message.result);
  }
}
 
// 元のセッションは変わっていないので、resumeでJWTの検討を続けられる
for await (const message of query({
  prompt: "JWTの方針で続けて",
  options: { resume: sessionId },
})) {
  if (message.type === "result" && message.subtype === "success") {
    console.log(message.result);
  }
}

forked_id が元の session_id と異なる値になっていれば成功です。元のセッションをresumeしてもJWTのスレッドがそのまま続き、forkが元の履歴を書き換えていないことが確認できます。

複数ホストにまたがってセッションを再開する3つの方法

セッションファイルは、それを作ったマシンにローカルなものです。CIワーカー・エフェメラルなコンテナ・サーバーレスのような別ホストでセッションを再開するには、3通りの方法があります。

  1. セッションストアを渡す: sessionStore / session_store アダプターを付けると、SDKはトランスクリプトを自前のバックエンドへミラーし、別のホストがそこから再開できます。ストアの検索キーは作業ディレクトリから導かれるため、元の実行と同じ cwd から再開する必要があります。詳しい実装はAgent SDK本番ホスティングのセッションパターンにまとめています
  2. セッションファイルを移す: 1回目の実行から ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl を持ち出し、新しいホストの ~/.claude/projects/ 配下の任意のディレクトリに置いてから resume を呼びます
  3. 再開に頼らない: 必要な結果(分析結果・判断・ファイルの差分)をアプリケーション側の状態として捕捉し、新しいセッションのプロンプトへ渡します。トランスクリプトファイルを持ち回るより堅牢なことが多い選択肢です

自社でホストするAgent SDKの構成でどのパターンを選ぶかは、上記のAgent SDK本番ホスティングの記事が判断材料になります。

両SDKとも、ディスク上のセッションを列挙してメッセージを読むための関数を提供しています。TypeScriptの listSessions() / getSessionMessages()、Pythonの list_sessions() / get_session_messages() です。個別のセッションを検索・変更する関数(get_session_info() / rename_session() / tag_session() とそのTypeScript版)もあり、タグ付けや人間が読める名前の付与に使えます。

よくあるつまずき

セッション保持の仕組みがPython/TypeScriptで違う

Pythonの ClaudeSDKClient に相当するオブジェクトはTypeScriptに存在しません。TypeScriptで会話を継続するには、毎回の query() 呼び出しに continue: true を渡す必要があります。この非対称性を知らずにPythonのコードをそのままTypeScriptへ移植しようとすると、セッション保持オブジェクトを探して迷います。

セッションIDの取得タイミングが言語で違う

TypeScriptでは初期化時の SystemMessage に直接のフィールドとしてIDが早く現れますが、Pythonでは SystemMessage.data の中にネストされています。どちらの言語でも確実なのは、結果メッセージの session_id フィールドを見ることです。成功・エラーを問わずすべての結果に含まれます。

resumeの探索範囲はバージョンで変わる

v2.1.223以降のClaude Codeは、カレントプロジェクトディレクトリを越えてセッションIDを探索します。それより古いCLIを同梱するSDKバージョンでは、探索範囲がカレントプロジェクトディレクトリとそのgit worktreeに限られます。古いバージョンのまま「resumeがカレントディレクトリ外で見つからない」とつまずいたら、まずSDKのバージョンを疑います。

Claude Code CLI本体の/resumeとは別物

Claude CodeのCLI本体にも /resume コマンドがあります。これはAgent SDKの resume オプションとは別のインターフェースですが、同じセッションファイルの仕組みの上に乗っています。CLIから対話的に会話を選び直す使い方はClaude Codeの/resumeコマンドにまとまっています。

よくある質問

continueとresumeを同時に使えますか

いいえ。continueはカレントディレクトリの最新セッションを自動で探すのに対し、resumeは指定したIDのセッションを探します。同じ呼び出しで両方を意味のある形で組み合わせることはできません。用途に応じてどちらか一方を選びます。

forkしたセッションは元のセッションのトークン使用量に影響しますか

forkは履歴のコピーを新しいセッションとして作るだけで、元のセッションのトランスクリプトやトークン使用量の記録には触れません。forkした側でのやり取りは、fork後の新しいセッションIDに対して課金・記録されます。

セッションIDを紛失した場合、会話を復元する方法はありますか

listSessions() / list_sessions() でディスク上のセッションを列挙し、getSessionMessages() / get_session_messages() で中身を確認すれば、IDをメモしていなくても目的のセッションを見つけられます。

まとめ

continueは「今のディレクトリで一番新しい会話に戻る」、resumeは「特定のIDの会話に戻る」、forkは「元の会話を残したまま枝分かれする」という役割分担です。1プロセス内で完結するチャットならPythonの ClaudeSDKClient かTypeScriptの continue: true に任せ、ユーザーごとに複数セッションを並存させるならIDを自分で管理してresumeを使い、別方針を試しながら後戻りできる余地を残したいならforkを使います。別ホストでの再開が必要になった時点で、セッションストアかセッションファイルの持ち運びかを選ぶ判断が追加で必要です。

この記事を共有:XはてブLinkedIn