1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Agent SDK入門 — rewind_filesで会話の任意ターンに戻す

1
Posted at

はじめに

Claude Agent SDK(Python版)に、セッション中のファイル変更を任意の時点まで巻き戻せる「file checkpointing」機能があります。ClaudeAgentOptions(enable_file_checkpointing=True) で有効化し、client.rewind_files(user_message_id) を呼ぶだけ、というのが公式のうたい文句です。

この記事では、実際にクラウド実行環境で pip install claude-agent-sdk して動かし、「どのメッセージの uuid を渡せば巻き戻せて、どれを渡すと失敗するか」を実機で検証しました。結論から言うと、公式サンプルコードのパターンをそのまま実装すると、意図したところで rewind_files() が失敗するケースがある ことが分かりました。

この記事で学べること

  • enable_file_checkpointing の有効化に必要な2つの設定
  • rewind_files() が実際に効く uuid と効かない uuid の違い(実機検証結果)
  • 複数ターンをまたいだ巻き戻しの挙動

対象読者

  • Claude Agent SDK でマルチターンのコーディングエージェントを組みたい方
  • 「変更を試して、ダメなら戻す」を自前実装しようとしている方

前提環境

  • Python 3.11.15
  • claude-agent-sdk 0.2.120(PyPI 最新版)
  • 実行環境: Claude Code on the web のクラウドコンテナ(rootユーザー)
  • claude CLI 2.1.211 がインストール済みで、SDK はこれをサブプロセスとして起動する構成

TL;DR

  • enable_file_checkpointing=True に加えて extra_args={"replay-user-messages": None} を渡さないと、UserMessage に uuid が乗ってこない(=巻き戻し先を取得できない)
  • rewind_files() に渡せるのは 各 query() 呼び出しの最初のユーザーメッセージの uuid だけ
  • ツール実行結果として発行される中間の UserMessage(content がリスト型になっているもの)の uuid を渡すと Exception: No file checkpoint found for this message. で失敗する
  • 複数回 query() を呼んでも、各ターン開始時点のチェックポイントへ正しく戻れる(v3 → v2 → v1 を実機確認)

セットアップ

まずインストールします。

pip install claude-agent-sdk
python3 -c "import claude_agent_sdk; print(claude_agent_sdk.__version__)"
# 0.2.120

ClaudeAgentOptions のフィールドを見ると、enable_file_checkpointing: bool = False が確認できます。

import dataclasses
import claude_agent_sdk

for f in dataclasses.fields(claude_agent_sdk.ClaudeAgentOptions):
    print(f.name)
# ... enable_file_checkpointing, session_store, task_budget ...

ClaudeSDKClient 側には rewind_files(user_message_id: str) -> None が生えています。

やったこと

ステップ1: 最初の実行で permission_mode に注意(実行環境固有の制約)

クラウドコンテナは root ユーザーで動作しているため、permission_mode="bypassPermissions" を指定すると次のエラーで接続に失敗しました。

claude_agent_sdk._errors.ProcessError: Command failed with exit code 1
--dangerously-skip-permissions cannot be used with root/sudo privileges for security reasons

これは Claude Code CLI 側の安全機構(root 権限での無条件承認スキップを拒否する)が働いているためで、SDK 固有の不具合ではありません。permission_mode="acceptEdits"(ファイル編集のみ自動承認)に変えると接続できました。root 環境で SDK を試す場合はこの制約を踏まえてモードを選ぶ必要があります。

ステップ2: checkpointingを有効化して1ターン編集する

import asyncio
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, UserMessage

async def main():
    options = ClaudeAgentOptions(
        cwd="/tmp/checkpoint-test",
        enable_file_checkpointing=True,
        extra_args={"replay-user-messages": None},  # これが無いとUserMessageにuuidが乗らない
        permission_mode="acceptEdits",
        max_turns=5,
    )
    checkpoints = []
    async with ClaudeSDKClient(options) as client:
        await client.query("test.txtの中身をEditツールで 'hello v2' に書き換えて。")
        async for msg in client.receive_response():
            if isinstance(msg, UserMessage) and getattr(msg, "uuid", None):
                checkpoints.append(msg.uuid)

    print(checkpoints)

asyncio.run(main())

実行すると、1ターンの中で 6個 の UserMessage(uuid付き)が発行されました。中身は「ユーザーの入力」1件と、Bash / Read / Edit といったツール実行結果を運ぶ UserMessage 5件です。

['9d6f...', '8b21...', '28b8...', '4328...', '38aa...', 'acec...']

ここでドキュメントのサンプルコード通りに checkpoints[-1](最後に受け取った uuid)を rewind_files() に渡すと失敗します。

await client.rewind_files(checkpoints[-1])
# Exception: No file checkpoint found for this message.

checkpoints[0](そのターンの最初のユーザーメッセージの uuid)を渡すと成功し、ファイルが編集前の状態に戻りました。

await client.rewind_files(checkpoints[0])
# OK. test.txt は 'hello v2' → 'hello v1' に巻き戻る

ステップ3: 複数ターンでの巻き戻しを検証

「途中のターンまで戻す」という本来のユースケースを想定し、2回 query() を呼んで確認しました。

async def verify_multi_turn_rewind(options):
    async with ClaudeSDKClient(options) as client:
        await client.query("test.txtをEditツールで 'hello v2' に書き換えて。")
        turn1_uuid = None
        async for msg in client.receive_response():
            if isinstance(msg, UserMessage) and msg.uuid and turn1_uuid is None:
                turn1_uuid = msg.uuid

        await client.query("test.txtをEditツールで 'hello v3' に書き換えて。")
        turn2_uuid = None
        async for msg in client.receive_response():
            if isinstance(msg, UserMessage) and msg.uuid and turn2_uuid is None:
                turn2_uuid = msg.uuid

        # ここで test.txt は 'hello v3'

        await client.rewind_files(turn2_uuid)
        # -> 'hello v2' に戻る

        await client.rewind_files(turn1_uuid)
        # -> 'hello v1' に戻る

実行結果は次の通りで、想定通りターンごとに正確に巻き戻せました。

操作 ファイル内容
turn1 実行後 hello v2
turn2 実行後 hello v3
rewind_files(turn2_uuid) 後 hello v2
rewind_files(turn1_uuid) 後 hello v1

ハマりポイント

ポイント1: uuid はメッセージ種別で区別が必要

receive_response() のループで受け取る UserMessage は1種類ではありません。人間が入力したメッセージと、ツール実行結果を運ぶメッセージの両方が同じ UserMessage 型で流れてきます。SDK の型定義上、content フィールドの型で見分けられます。

if isinstance(msg, UserMessage) and isinstance(msg.content, str):
    # 人間の入力メッセージ(= checkpoint に使えるuuid)
    checkpoint_id = msg.uuid

ツール結果側は content が list(ToolResultBlock 等を含む)になっているため、isinstance(msg.content, str) で判定すれば取り違えを防げます。公式ドキュメントのサンプルは「UserMessage を受け取ったら uuid を保存する」とだけ書かれており、この区別には触れていません。

rewind_files() の失敗は例外(Exception)で返るため、try/except を挟んでおかないと巻き戻し失敗がそのままクラッシュにつながります。

ポイント2: extra_args={"replay-user-messages": None} を忘れると詰む

extra_args を外して同じコードを実行すると、UserMessage.uuid が常に None になり、rewind_files() に渡す値そのものが手に入りません。enable_file_checkpointing=True だけでは不十分で、CLI 側に --replay-user-messages フラグを渡す extra_args が必須という組み合わせでした。この要件は Web版の公式ドキュメントには見当たりませんでしたが、inspect.getsource(ClaudeSDKClient.rewind_files) でSDK同梱のdocstringを読むと明記されています。GitHub上のリポジトリを読まずにパッケージ単体をインストールしただけでは気づきにくいポイントです。

著者視点の発見ポイント

公式サンプルコードは「receive_response() ループで最後に見つけた uuid を checkpoint_id として保存する」という書き方をしています。しかし実際に動かすと、ツール呼び出しを1回でも挟んだターンでは、ループの最後に流れてくる UserMessage はほぼ確実にツール結果側(content がリスト型)になり、そのまま rewind_files() に渡すと失敗します。実務でこの機能を使うなら、isinstance(msg.content, str) でメッセージ種別を判定し、各ターンの最初の UserMessage の uuid を保存する 実装にする必要があります。この点はドキュメントの疑似コード(docs/agent-sdk の該当ページ)には明記されていませんでした。

まとめ

  • enable_file_checkpointing=True と extra_args={"replay-user-messages": None} はセットで必要
  • rewind_files() に渡せるのは各ターンの 最初のユーザーメッセージ の uuid のみ。ツール結果側の uuid では失敗する
  • 複数ターンにまたがる巻き戻しは正しく機能する(実機確認済み)
  • 「エージェントに試行錯誤させて、ダメならターンごと戻す」というワークフローを組む場合は、content の型でメッセージ種別を判定してから uuid を保存する実装にすること

参考リンク

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?