Claude Agent SDK セッション管理実践ガイド — fork・resume・rewindで「巻き戻せるAIエージェント」を作る
AIエージェントに「このコードをリファクタリングして」と指示したら、盛大に壊してしまった——そんな経験は、エージェント開発をしていれば一度はあるだろう。従来のエージェントは会話が一本道で、失敗したら最初からやり直すしかなかった。
Claude Agent SDKのセッション管理機能を使えば、会話をgitのようにブランチ・巻き戻しできる。本記事では、forkSessionによるA/Bテスト的試行、resumeによる中断再開、rewindFilesによるファイル復元を、実際に動くTypeScriptコードとともに解説する。
セッション管理が解決する3つの課題
リスクのある操作を安全に試したい(fork)
データベースのマイグレーションやコードのリファクタリングなど、破壊的な変更をエージェントに任せるのは怖い。forkSessionを使えば、元のセッションを汚さずに試行できる。失敗したら破棄するだけだ。
長時間タスクを中断・再開したい(resume)
コード分析に30分、修正にさらに30分——途中でプロセスが落ちたら最初からやり直しになる。resumeを使えば、セッションIDを指定して別プロセスから会話を再開できる。
失敗時にファイルを元に戻したい(rewind)
エージェントがファイルを書き換えたあとにビルドが通らない場合、手動でgit checkoutするのは面倒だ。rewindFilesなら、指定した時点の状態にファイルを自動復元できる。
この3つの機能——fork・resume・rewindは、プロダクション品質のエージェントに不可欠な「回復性」を実現する仕組みだ。
セッションの基本:persist と resume
セッションの自動永続化の仕組み
Agent SDKでは、persistSessionオプションがデフォルトでtrueになっている。セッションファイルは~/.claude/projects/<encoded-cwd>/<session-id>.jsonlに自動保存される。特別な設定なしに、すべての会話が永続化されるということだ。
sessionIdによる中断・再開パターン
query()のセッション関連オプションは以下の通り。
| オプション | 型 | 説明 |
|---|---|---|
continue | boolean | 同じプロセス内で会話を継続 |
resume | string | 別プロセスからセッションIDで再開 |
sessionId | string | セッションIDを明示指定 |
forkSession | boolean | 会話履歴をコピーした新セッションを作成 |
persistSession | boolean | セッションの永続化(デフォルト: true) |
enableFileCheckpointing | boolean | ファイルチェックポイントの有効化(デフォルト: false) |
ここで重要なのはcontinueとresumeの違いだ。continueは同一プロセス内での会話継続、resumeはセッションIDを指定して別プロセスから再開する。正直、最初はこの違いに戸惑った。
実用的なパターンとして、read-onlyフェーズで調査→ユーザー確認→writeフェーズで修正という2段階エージェントを紹介する。
import { query } from "@anthropic-ai/claude-agent-sdk";
let sessionId: string | undefined;
// Phase 1: read-onlyで分析
for await (const message of query({
prompt: "認証モジュールのセキュリティ問題を分析して。修正はまだしないで",
options: {
cwd: "/path/to/project",
allowedTools: ["Read", "Glob", "Grep"],
},
})) {
if (message.type === "system" && message.subtype === "init") {
sessionId = message.session_id;
}
if ("result" in message) {
console.log("分析結果:", message.result);
}
}
// ユーザーに確認を求める
const approved = await askUserConfirmation("この修正方針で進めますか?");
if (approved && sessionId) {
// Phase 2: resumeでwrite権限付きで再開
for await (const message of query({
prompt: "先ほど特定した問題を修正して",
options: {
resume: sessionId,
allowedTools: ["Read", "Edit", "Write", "Bash"],
permissionMode: "acceptEdits",
},
})) {
if ("result" in message) {
console.log("修正完了:", message.result);
}
}
}
forkSessionで「失敗しても壊れない」エージェントを作る
forkSessionの仕組みと使いどころ
forkSession: trueを指定すると、既存の会話履歴をコピーした新しいセッションが作られる。元のセッションは一切変更されない。query()のオプションとしてforkSession: trueを渡すだけで利用できる。
使いどころの判断基準はシンプルだ。「元の会話コンテキストを汚したくないか?」——答えがYesならfork、Noならcontinueを使う。
A/Bテスト的エージェント設計パターン
リファクタリングをforkで試行し、ビルド結果で採用/破棄を決定するパターンを見てみよう。
import { query } from "@anthropic-ai/claude-agent-sdk";
import { execSync } from "child_process";
const projectDir = "/path/to/project";
let mainSessionId: string | undefined;
// メインセッションでコードを分析
for await (const message of query({
prompt: "このプロジェクトの認証ロジックを分析して、リファクタリング方針を提案して",
options: {
cwd: projectDir,
allowedTools: ["Read", "Glob", "Grep"],
},
})) {
if (message.type === "system" && message.subtype === "init") {
mainSessionId = message.session_id;
}
}
// forkしてリファクタリングを試行
let forkSucceeded = false;
if (mainSessionId) {
for await (const message of query({
prompt: "提案した方針に基づいてリファクタリングを実行して",
options: {
resume: mainSessionId,
forkSession: true, // 元セッションを保護
cwd: projectDir,
allowedTools: ["Read", "Edit", "Write", "Bash"],
permissionMode: "acceptEdits",
},
})) {
if ("result" in message) {
// ビルドで検証
try {
execSync("npm run build", { cwd: projectDir, stdio: "pipe" });
execSync("npm test", { cwd: projectDir, stdio: "pipe" });
forkSucceeded = true;
console.log("リファクタリング成功 — 変更を採用");
} catch {
console.log("ビルド/テスト失敗 — 変更を破棄");
execSync("git checkout .", { cwd: projectDir });
}
}
}
}
if (!forkSucceeded) {
// 元のセッションは無傷なので、別のアプローチを試せる
console.log("元のセッションから別の方針で再試行可能");
}
このパターンが地味に便利で、メインセッションのコンテキスト(分析結果や方針)を保持したまま、何度でも試行錯誤できる。
rewindFilesでファイル変更を安全に巻き戻す
チェックポイントの有効化と巻き戻し手順
rewindFilesを使うには、enableFileCheckpointing: trueを指定する必要がある。rewindFilesはquery()が返すQueryオブジェクトのメソッドとして呼び出す。
import { query } from "@anthropic-ai/claude-agent-sdk";
const queryInstance = query({
prompt: "テストカバレッジを向上させるためにテストファイルを追加して",
options: {
cwd: "/path/to/project",
allowedTools: ["Read", "Edit", "Write"],
permissionMode: "acceptEdits",
enableFileCheckpointing: true, // チェックポイントを有効化
},
});
for await (const message of queryInstance) {
if ("result" in message) {
// ビルドで検証
try {
execSync("npm test", { cwd: "/path/to/project", stdio: "pipe" });
console.log("テスト追加成功");
} catch {
// 失敗時はrewindで復元(userMessageIdを指定して任意の時点に戻せる)
await queryInstance.rewindFiles(userMessageId);
console.log("ファイルを変更前の状態に復元しました");
}
}
}
rewindの制約と回避策
重要な制約がある。
rewindFilesはWrite、Edit、NotebookEditの3ツール経由の変更のみを追跡する。Bash経由のファイル変更(echo > file等)は追跡されない。
Bash経由の変更が追跡されない問題の回避策として、エージェントのプロンプトでこれらのツールの使用を強制する方法がある。
for await (const message of query({
prompt: `ファイルの変更はすべてEditツールを使って行うこと。
echoリダイレクトやsedコマンドでのファイル書き換えは禁止。
タスク: CSSモジュールをTailwindに移行して`,
options: {
enableFileCheckpointing: true,
allowedTools: ["Read", "Edit", "Write", "Glob", "Grep"],
// Bashを除外してEditツール使用を強制する方法もある
},
})) {
// ...
}
実践:3つの設計パターン
パターン1:段階的権限昇格(read → confirm → write)
初回は読み取り専用で分析し、ユーザーの承認後に書き込み権限付きでresumeする。前述のコード例がまさにこのパターンだ。安全性と効率を両立できるため、プロダクション環境での利用に最適。
パターン2:投機的実行(fork → validate → adopt/discard)
2つの異なるアプローチを並行してforkで試行し、テスト結果の良い方を採用するパターン。
import { query } from "@anthropic-ai/claude-agent-sdk";
import { execSync } from "child_process";
const projectDir = "/path/to/project";
let baseSessionId: string | undefined;
// ベースセッションで分析
for await (const message of query({
prompt: "パフォーマンスのボトルネックを特定して、2つの改善方針を提案して",
options: {
cwd: projectDir,
allowedTools: ["Read", "Glob", "Grep"],
},
})) {
if (message.type === "system" && message.subtype === "init") {
baseSessionId = message.session_id;
}
}
if (!baseSessionId) throw new Error("セッションID取得失敗");
// アプローチA: キャッシュ戦略で改善
const tryApproach = async (approach: string, label: string) => {
// git stashで作業ディレクトリをクリーンに保つ
execSync("git stash", { cwd: projectDir });
for await (const message of query({
prompt: `方針「${approach}」で実装して`,
options: {
resume: baseSessionId,
forkSession: true,
cwd: projectDir,
allowedTools: ["Read", "Edit", "Write", "Bash"],
permissionMode: "acceptEdits",
},
})) {
if ("result" in message) {
try {
execSync("npm run build", { cwd: projectDir, stdio: "pipe" });
const testOutput = execSync("npm test -- --reporter=json", {
cwd: projectDir,
encoding: "utf-8",
});
return { label, success: true, result: testOutput };
} catch (e) {
return { label, success: false, result: String(e) };
} finally {
execSync("git checkout .", { cwd: projectDir });
execSync("git stash pop", { cwd: projectDir });
}
}
}
return { label, success: false, result: "no result" };
};
const resultA = await tryApproach("キャッシュ層の追加", "A");
const resultB = await tryApproach("クエリの最適化", "B");
// 結果を比較して採用
const winner = resultA.success && !resultB.success ? resultA
: !resultA.success && resultB.success ? resultB
: resultA; // 両方成功なら任意の基準で選択
console.log(`採用: アプローチ ${winner.label}`);
パターン3:長時間タスクの中断・再開(persist → resume)
sessionIdを外部ストレージに保存し、プロセス再起動後もresumeで再開するパターン。launchdやcronで動くデーモンに最適。
import { query } from "@anthropic-ai/claude-agent-sdk";
import Database from "better-sqlite3";
const db = new Database("./sessions.db");
db.exec(`CREATE TABLE IF NOT EXISTS agent_sessions (
task_id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
status TEXT DEFAULT 'running'
)`);
async function startOrResume(taskId: string, prompt: string) {
const row = db.prepare(
"SELECT session_id FROM agent_sessions WHERE task_id = ? AND status = 'running'"
).get(taskId) as { session_id: string } | undefined;
const options = row
? { resume: row.session_id, allowedTools: ["Read", "Edit", "Write", "Bash"] as const }
: { allowedTools: ["Read", "Edit", "Write", "Bash"] as const };
for await (const message of query({ prompt, options })) {
if (message.type === "system" && message.subtype === "init") {
db.prepare(
"INSERT OR REPLACE INTO agent_sessions (task_id, session_id, status) VALUES (?, ?, 'running')"
).run(taskId, message.session_id);
}
if ("result" in message) {
db.prepare(
"UPDATE agent_sessions SET status = 'completed' WHERE task_id = ?"
).run(taskId);
return message.result;
}
}
}
判断フロー
どのパターンを使うかは以下で判断できる。
- ユーザー確認が必要 → パターン1(段階的権限昇格)
- 破壊的変更を伴い、複数の方法を試したい → パターン2(投機的実行)
- プロセスをまたいで作業を継続したい → パターン3(中断・再開)
よくあるハマりポイントと対策
resumeで「セッションが見つからない」
セッションファイルのパスは~/.claude/projects/<encoded-cwd>/<session-id>.jsonlだ。cwdが異なるとパスも変わるため、resumeする際は元のセッションと同じcwdを指定すること。また、persistSession: falseを設定していないか確認しよう。
Bash経由の変更がrewindで戻らない
前述の通り、rewindFilesはWrite、Edit、NotebookEditの3ツール経由の変更のみを追跡する。allowedToolsからBashを除外するか、プロンプトでこれらのツールの使用を強制するのが確実な対策だ。
forkしたセッションが肥大化する
forkは会話履歴をまるごとコピーするため、大量にforkするとディスクを圧迫する。不要なforkセッションを定期的にクリーンアップする仕組みを入れておくとよい。listSessions()でセッション一覧を取得し、古いものを削除するスクリプトを用意しておこう。
continueとresumeの混同
continue: trueは同じプロセス内の会話継続、resumeはセッションIDを指定した別プロセスからの再開だ。プロセスをまたぐ場合は必ずresumeを使うこと。
Claude Agent SDKのセッション管理は、エージェントに「やり直す力」を与える。fork・resume・rewindの3機能を組み合わせることで、失敗に強く、中断に強く、安全に試行錯誤できるエージェントが構築できる。まずはresumeによる中断再開から始め、慣れたらforkSessionによる投機的実行に進むのがおすすめだ。
