本文へ移動
Claude Tips

SDK のセッションと入出力

Agent SDK のセッションの継続・再開・分岐、外部ストアへの保存、入力モード、ストリーミング出力、承認と質問の処理、構造化出力、ファイルの巻き戻し、タスク追跡をまとめます。

Agent SDK のセッションは、プロンプト・ツール呼び出し・ツール結果・応答を含む会話の履歴です。SDK が自動でディスクに書くので、あとから文脈ごと戻れます。このページでは、セッションの扱い、メッセージの入出力、承認や質問の受け方、構造化出力、ファイルの巻き戻しを説明します。

  • 1回の query() で足りるなら、セッションの管理は要りません。複数のプロンプトで文脈を共有したいときに使います
  • 続きから始める方法は、継続(continue)・再開(resume)・分岐(fork)の3つです
  • 複数のホストで再開するには、sessionStore でトランスクリプトを自前のバックエンドへ写します
  • 入力は、ストリーミング入力(推奨)と単発の2通りです。出力は includePartialMessages でトークン単位に受けられます
  • 承認要求と質問は canUseTool で受け、結果の形は outputFormat で固定できます
  • 基本は Agent SDK の基本、型の一覧は SDK の API リファレンス にあります

補足

セッションが保存するのは会話であって、ファイルシステムではありません。エージェントが変えたファイルを戻すには、このページの「ファイルの巻き戻し」を使います。

セッションの扱い方を選ぶ#

作りたいもの 使うもの
1回の指示で終わる作業 何も要らない。query() を1回呼ぶ
1つのプロセスで複数回のやり取り Python は ClaudeSDKClient、TypeScript は continue: true
プロセスの再起動後に続きから始める continue_conversation=True(Python)/continue: true(TypeScript)。そのディレクトリで最新のセッションを、ID なしで再開する
最新でない特定のセッションに戻る セッション ID を控え、resume に渡す
元を残したまま別の方法を試す セッションを分岐する
何もディスクに書きたくない TypeScript は persistSession: false。Python は env に CLAUDE_CODE_SKIP_PROMPT_HISTORY を入れて書き込みを抑える

1回の query() の中では、エージェントが必要なだけターンを重ね、権限の確認や AskUserQuestion もループの中で処理されます(呼び出しは終わりません)。

  • 継続(continue):現在のディレクトリの最新のセッションを見つけて続ける。1度に会話が1つのアプリ向け
  • 再開(resume):ID で指定したセッションを続ける。複数ユーザーのアプリなど、複数のセッションを管理するときに必要
  • 分岐(fork):元の履歴のコピーから始まる新しいセッションを作る。元は変わらない

いずれも query() のオプションで指定します。

Python の ClaudeSDKClient#

ClaudeSDKClient は内部でセッション ID を持ち、client.query() を呼ぶたびに同じセッションを続けます。async with で使うと接続と切断を任せられます(手動なら connect() と disconnect())。

python
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:
        await client.query("auth モジュールを分析して")
        async for message in client.receive_response():
            print(message)
        # 同じセッションが続く
        await client.query("それを JWT を使う形に直して")
        async for message in client.receive_response():
            print(message)

asyncio.run(main())

TypeScript の continue: true#

TypeScript には、セッションを持つクライアントオブジェクトがありません。2回目以降の query() に continue: true を渡すと、現在のディレクトリの最新のセッションを続けます。

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

// 1回目:新しいセッション
for await (const message of query({
  prompt: "auth モジュールを分析して",
  options: { allowedTools: ["Read", "Glob", "Grep"] },
})) { /* ... */ }

// 2回目:最新のセッションを続ける
for await (const message of query({
  prompt: "それを JWT を使う形に直して",
  options: { continue: true, allowedTools: ["Read", "Edit", "Write", "Glob", "Grep"] },
})) { /* ... */ }

補足

試験的な V2 のセッション API(createSession() と send / stream)は、TypeScript Agent SDK 0.3.142 で削除されました。query() とこのページのセッションのオプションを使います。

セッション ID・再開・分岐#

セッション ID を控える#

再開と分岐には ID が要ります。結果メッセージの session_id(成功でもエラーでも付く)から取ります。TypeScript では、初期化の SystemMessage に直接のフィールドとしても早めに入っています。Python では SystemMessage.data の中です。

python
session_id = None
try:
    async for message in query(
        prompt="auth モジュールを分析して改善案を出して",
        options=ClaudeAgentOptions(allowed_tools=["Read", "Glob", "Grep"]),
    ):
        if isinstance(message, ResultMessage):
            session_id = message.session_id
except Exception as error:
    print(f"Session ended with an error: {error}")

ID で再開する#

resume に ID を渡すと、そのセッションの文脈を引き継いで続きます。よくある使い方です。

  • 完了した作業の続きを頼む(ファイルを読み直さずに済む)
  • error_max_turns や error_max_budget_usd で止まったあと、上限を上げて続ける(単発の query() はエラーの結果を返したあと例外を送出するので、再開の前に捕まえる)
  • プロセスを再起動したあとで会話を復元する
typescript
for await (const message of query({
  prompt: "提案したリファクタリングを実装して",
  options: { resume: sessionId, allowedTools: ["Read", "Edit", "Write", "Glob", "Grep"] },
})) { /* ... */ }

ヒント

セッションは ~/.claude/projects/<encoded-cwd>/*.jsonl に保存されます。CLAUDE_CONFIG_DIR を設定した場合は $CLAUDE_CONFIG_DIR/projects/ です。<encoded-cwd> は、作業ディレクトリの絶対パスの英数字以外をすべて - にしたものです(/Users/me/proj は -Users-me-proj)。変換後の名前が200文字を超えると、切り詰めてハッシュが付くので、最初の200文字で探します。CLAUDE_CODE_PROJECT_DIR_NAME を CLAUDE_CONFIG_DIR と並べて設定した場合は、その名前で探します(TypeScript Agent SDK v0.3.234 以降、Python Agent SDK v0.2.140 以降)。

  • 作業ディレクトリが違っても再開できます。Claude Code は現在のプロジェクトのディレクトリの外も探します(探す順と重複の扱いは セッションの再開と管理)。ただしセッションのファイルが同じマシンにある必要があります
  • v2.1.223 より前は、探す範囲が現在のプロジェクトのディレクトリとその git worktree に限られていました。古い CLI を同梱した SDK は、今もこの動きです

分岐して別の案を試す#

分岐は、元の履歴のコピーから始まる新しいセッションを作ります。分岐には新しい ID が付き、元の ID と履歴は変わりません。

typescript
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; // 分岐の ID(sessionId とは別)
  }
}
python
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

注意

分岐で分かれるのは会話の履歴で、ファイルシステムではありません。分岐したエージェントがファイルを編集すると、同じディレクトリで動くどのセッションからも見える実際の変更になります。ファイルを戻すには「ファイルの巻き戻し」を使います。

別のホストで再開する#

セッションのファイルは、作ったマシンのローカルにあります。CI のワーカー・使い捨てのコンテナ・サーバーレスで再開するには、次のいずれかを選びます。

  • セッションストアを渡す:sessionStore / session_store のアダプターでトランスクリプトを自前のバックエンドへ写す。ストアの検索キーは作業ディレクトリから決まるので、元と同じ cwd で再開する
  • セッションのファイルを動かす:最初の実行の ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl を保存し、新しいホストの ~/.claude/projects/ 以下のどれかのディレクトリに戻してから resume を呼ぶ
  • 再開に頼らない:必要な結果(分析・判断・ファイルの差分)をアプリの状態として保存し、新しいセッションのプロンプトに渡す。トランスクリプトのファイルを持ち回るより堅牢なことが多い

セッションの列挙・読み出し・編集には次の関数があります。セッション選択画面・掃除・トランスクリプトの閲覧に使えます。

内容 TypeScript Python
一覧 listSessions() list_sessions()
メッセージの読み出し getSessionMessages() get_session_messages()
1件の情報 getSessionInfo() get_session_info()
名前の変更 renameSession() rename_session()
タグ付け tagSession() tag_session()

セッションストア#

標準では、トランスクリプトは ~/.claude/projects/ の JSONL ファイルに書かれます。SessionStore のアダプターを付けると、同じ内容をオブジェクトストア・KV ストア・データベースへ写し、別のホストでも(同じ作業ディレクトリなら)再開できます。使う理由は次のとおりです。

  • 複数ホスト:サーバーレス・オートスケール・CI はファイルシステムを共有しない
  • 耐久性:コンテナは使い捨てなので、外のストアなら再起動や再デプロイに耐える
  • コンプライアンスと監査:すでに管理しているストレージに、自前の保持期間・暗号化・アクセス制御で置ける

SessionStore の形#

append と load が必須で、ほかの4つは任意です。

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

type SessionStore = {
  append(key: SessionKey, entries: SessionStoreEntry[]): Promise<void>;
  load(key: SessionKey): Promise<SessionStoreEntry[] | null>;
  // 任意
  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[]>;
};

Python は SessionKey(project_key・session_id・subpath)と SessionStore の Protocol で、メソッドは append・load と、任意の list_sessions・list_session_summaries・delete・list_subkeys です(任意のものは省くか NotImplementedError を送出します)。

メソッド 必須 呼ばれるとき
append はい トランスクリプトの項目をローカルに書いたあと、バッチごとに。項目は JSON として安全なオブジェクトで、ローカルの JSONL の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 いいえ 再開時にサブエージェントのトランスクリプトを見つけるとき。未実装だと主トランスクリプトだけが復元される
  • projectKey は作業ディレクトリを安全な形に符号化した値、sessionId はセッションの UUID、subpath はサブエージェントのトランスクリプトや付随ファイルのときの接尾辞です(例:subagents/agent-<id>。意味を解釈せずにそのまま使う)
  • TypeScript で CLAUDE_CODE_PROJECT_DIR_NAME を CLAUDE_CONFIG_DIR と並べて env に指定すると、そのクエリの項目と resume・continue の検索がその名前をキーにします。listSessions や deleteSession のような単体の関数は env を取らないので、ホストのプロセスの環境にも同じ値を設定します(Agent SDK v0.3.234 以降)
  • SessionSummaryEntry の mtime は付随ファイルの保存時刻で、listSessions が返す mtime と同じ時計を使います。data は SDK が持つ不透明な状態で、解釈せずにそのまま保存します
  • 要約は、append の中でバッチごとに foldSessionSummary(Python は fold_session_summary)を呼んで作ります。subpath のあるバッチは飛ばします。mtime は保存時に自分で付けます。同じセッションへの append が競合しうるので、読み・畳み込み・書きをトランザクションや排他で直列化します

使ってみる#

開発・テスト用に InMemorySessionStore が付属します。

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

const store = new InMemorySessionStore();
let sessionId: string | undefined;

for await (const message of query({
  prompt: "src/ の TypeScript ファイルを一覧にして",
  options: { sessionStore: store },
})) {
  if (message.type === "result") sessionId = message.session_id;
}

// ストアから再開
for await (const message of query({
  prompt: "それらのファイルが何をするか要約して",
  options: { sessionStore: store, resume: sessionId },
})) { /* ... */ }
python
store = InMemorySessionStore()

async for message in query(
    prompt="src/ の Python ファイルを一覧にして",
    options=ClaudeAgentOptions(session_store=store),
):
    if isinstance(message, ResultMessage):
        session_id = message.session_id

async for message in query(
    prompt="それらのファイルが何をするか要約して",
    options=ClaudeAgentOptions(session_store=store, resume=session_id),
):
    ...

自作のアダプター#

append と load をバックエンドに合わせて実装します。listSessions()・1回での情報の読み出し・deleteSession()・サブエージェントの再開を使うなら、listSessions・listSessionSummaries・delete・listSubkeys も足します。

  • append に渡る項目は SessionStoreEntry({ type: string; ... })で、不透明な JSON として順番どおりに保存し、同じ順で load から返します
  • load が返す項目は、追加したものと deep-equal であれば足ります(バイト単位の一致は不要で、キーの順が変わるバイナリ JSON 型でも構いません)

両 SDK のリポジトリに、実行できる参照実装があります(パッケージとしては公開されていません。近い種類のものをコピーして、バックエンドのクライアントを入れて直します)。

ストレージの種類 保存の考え方 例
オブジェクトストア append() ごとに1つのパートファイル。load() が一覧し、並べて連結する S3
KV ストア トランスクリプトごとに1つのリスト。append() が追加し、load() が範囲で読む。セッションの並べ替え済みの索引も持つ Redis
リレーショナル DB・ドキュメントストア 項目ごとに1行(か1ドキュメント)を JSON で保存し、挿入時の鍵で並べる Postgres

各アダプターは設定済みのクライアントを受け取るので、認証情報・TLS・リージョン・コネクションプールは自分で決められます。

アダプターの検証用に、両 SDK に適合テストが付いています。TypeScript は例のディレクトリの shared/conformance.ts をテストにコピーし、Python はパッケージに同梱されています(pytest は SDK の依存に含まれないので、先に pip install pytest が要ります)。

python
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)

引数なしのファクトリーとして渡します。コンストラクターが設定済みのクライアントを取るなら lambda で包みます。各契約で同じセッションキーを使うので、ファクトリーが返すストアは毎回空で始まる必要があります(呼び出しごとに独立した保存先を用意します)。

動作の注意#

二重書き込み。 Claude Code のサブプロセスは、まずローカルのディスクに書き、そのあとで SDK が同じバッチを append() に送ります。ストアはローカルの置き換えではなく写しです。

実行の始まり方 どちらが残るか
新規のセッション、またはストアに何も無いセッションの再開 ローカルのトランスクリプトが残り、ストアに写しが入る
ストアから再開した実行 実行の終了時にローカルの写しが消え、ストアだけが耐久的な写しになる
  • 新規のセッションでもローカルにトランスクリプトを残したくなければ、options.env の CLAUDE_CONFIG_DIR を一時ディレクトリにします。TypeScript は env がサブプロセスの環境を置き換えるので、process.env も展開します
  • 設定ディレクトリのファイルでサインインしているアプリ(OAuth の認証情報や、user の settings.json の apiKeyHelper)は、先にそれらを一時ディレクトリへコピーするか、env に ANTHROPIC_API_KEY を入れます。そうしないと Not logged in で失敗します
  • ストアと併用すると起動時に例外になるオプションが2つあります:TypeScript の persistSession: false(ミラーの元になるローカルの書き込みを止める。Python に同等のオプションはない)と、ファイルチェックポイント(enableFileCheckpointing / enable_file_checkpointing。バックアップをローカルに直接書き、ストアへは写されない)

ストアからの再開。 ストアと一緒に resume(または continue: true / continue_conversation=True)を渡すと、SDK はサブプロセスの起動前にストアへトランスクリプトを求めます。resume は渡した ID のセッション、continue はストアの最新のセッションです。取れたら一時的な設定ディレクトリに書き、CLAUDE_CONFIG_DIR をそこへ向けてサブプロセスを動かし、終わったらそのディレクトリを(その実行のローカルのトランスクリプトごと)消します。

一時ディレクトリには、本物の設定ディレクトリのファイルも写されます。

言語 写されるもの
TypeScript 認証情報・.claude.json・user の settings.json。settings.json からは、一時ディレクトリで誤動作する enabledPlugins・extraKnownMarketplaces・その別名の additionalMarketplaces・env ブロックの CLAUDE_CONFIG_DIR を取り除く。apiKeyHelper などの設定の認証は使える
Python 認証情報と .claude.json だけ。user の settings.json の apiKeyHelper で認証するアプリは、ストアからの再開で Not logged in になる。managed や project の設定の apiKeyHelper は動く
  • 別名の取り除きは Agent SDK v0.3.232 から、settings.json を写すのは v0.3.222 からです(それ以前の TypeScript SDK は認証情報と .claude.json だけ)
  • ストアに何も無いときは、本物の設定ディレクトリで動きます。resume は ID をサブプロセスに渡し、ストア無しと同じにローカルのトランスクリプトを再開します。TypeScript の continue: true は新規のセッションを始め、Python の continue_conversation=True は最新のローカルのセッションを続けます

ミラーの書き込みは最善努力。 append() が失敗すると、短い待ちを挟んで最大2回再試行します(計3回まで)。タイムアウトした呼び出しは再試行しません(元の呼び出しが届く可能性があるため)。それでも失敗するとエラーを記録し、イテレーターに { type: "system", subtype: "mirror_error" } を流し、そのバッチを捨てて続行します。再試行で届いた項目が重複しうるので、append() は entry.uuid で重複を除きます。ストアが止まってもサブプロセスはローカルに書くのでエージェントは止まりません。ストアのデータ欠落を検知するには mirror_error を監視します。ストアから再開した実行では、捨てたバッチは実行の終了後にどこにも残りません。

そのほか。

  • getSessionMessages({ sessionStore }) は、再開時にエージェントが見る、圧縮後のメッセージの連なりを返します。自動圧縮後は古いターンが要約に置き換わるので、ストアに生の503項目があっても18メッセージを返すことがあります。圧縮前や付随の項目を含む生の履歴は store.load(key) で直接読みます
  • forkSession({ sessionStore }) は、元の項目を読み、sessionId を書き換え、メッセージの UUID を付け替えて、新しいキーで追記します。バイト単位のコピー(CopyObject など)は古いセッション ID を残すので使いません
  • サブエージェントのトランスクリプトは subpath: "subagents/agent-<id>" の下に写されます。listSubagents({ sessionStore }) はアダプターの listSubkeys が必要です。getSubagentMessages({ sessionStore }) は、あれば使い、無ければ直接のサブパスに切り替わります
  • SDK は、ストアのデータを自分では消しません。保持期間は、アダプターの責任です(バックエンドの期限切れ・ライフサイクルの仕組みか、定期的な掃除)。CLAUDE_CONFIG_DIR のローカルのトランスクリプトは cleanupPeriodDays の設定で別に掃除されます(.claude ディレクトリの中身)。ストアから再開した実行はローカルに残らないので、その分はストア側の保持だけが保持です

sessionStore に対応する関数#

TypeScript(sessionStore を受け取る) Python
query() ClaudeAgentOptions(session_store=...)
startup() 同等のものなし
listSessions() list_sessions_from_store()
getSessionInfo() get_session_info_from_store()
getSessionMessages() get_session_messages_from_store()
renameSession() rename_session_via_store()
tagSession() tag_session_via_store()
deleteSession() delete_session_via_store()
forkSession() fork_session_via_store()
listSubagents() list_subagents_from_store()
getSubagentMessages() get_subagent_messages_from_store()

Python の標準の関数(list_sessions() など)は、ローカルのセッションファイルを読みます。

入力のモード#

SDK への入力には、ストリーミング入力と単発の2通りがあります。

項目 ストリーミング入力(推奨) 単発のメッセージ
形 長く生きるプロセスとして、入力・割り込み・権限要求・セッション管理を受ける 1回きりの問い合わせ。セッションの状態と再開に頼る
画像の添付 できる できない
メッセージのキュー できる(順に処理し、割り込める) できない
割り込み・実行中の制御 できる できない
自然な複数ターン できる できない(continue などで続ける)
向く場面 対話的なアプリ 1回の応答、ラムダのようなステートレスな環境

ストリーミング入力#

非同期ジェネレーターで、メッセージを順に流し込みます。画像は base64 で添付します。

typescript
import { query, type SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
import { readFile } from "fs/promises";

async function* generateMessages(): AsyncGenerator<SDKUserMessage> {
  yield {
    type: "user",
    message: { role: "user", content: "このコードベースのセキュリティ問題を分析して" },
    parent_tool_use_id: null,
  };
  await new Promise((resolve) => setTimeout(resolve, 2000));
  yield {
    type: "user",
    message: {
      role: "user",
      content: [
        { type: "text", text: "この構成図をレビューして" },
        { type: "image", source: { type: "base64", media_type: "image/png", data: await readFile("diagram.png", "base64") } },
      ],
    },
    parent_tool_use_id: null,
  };
}

for await (const message of query({
  prompt: generateMessages(),
  options: { maxTurns: 10, allowedTools: ["Read", "Grep"] },
})) {
  if (message.type === "result" && message.subtype === "success") console.log(message.result);
}

Python は ClaudeSDKClient で、await client.query(message_generator()) に同じ形のジェネレーターを渡します。receive_response() のループは最初の結果メッセージで終わるので、両方の応答を読むには、メッセージごとに query() と receive_response() の組を使います。

画像ブロックの source が無い、またはオブジェクトでないとき、SDK はエラーを報告しません。Claude Code は、画像の代わりに [Image could not be processed: image block has no source object] のような文を Claude に送り、セッションは続きます。

注意

TypeScript で、メッセージのジェネレーターが例外を投げる(読むファイルが無いなど)と、元のエラーではなく Claude Code process aborted by user でストリームが終わります。このメッセージが出たら、まずジェネレーターの中を確かめます。バンドルされた SDK のソースの長い行が前に出ることもあるので、出力の末尾までエラー文を読みます。Python では、ジェネレーターの例外はデバッグレベルで記録されるだけで、セッションが例外なしに止まります。出力なしで止まったら、デバッグログを有効にしてジェネレーターを調べます。

単発のメッセージ#

typescript
try {
  for await (const message of query({
    prompt: "認証の流れを説明して",
    options: { maxTurns: 5, allowedTools: ["Read", "Grep"] },
  })) {
    if (message.type === "result" && message.subtype === "success") console.log(message.result);
  }
} catch (error) {
  console.error(`Query failed: ${error}`);
}
python
try:
    async for message in query(
        prompt="認証の流れを説明して",
        options=ClaudeAgentOptions(max_turns=5, allowed_tools=["Read", "Grep"]),
    ):
        if isinstance(message, ResultMessage) and message.subtype == "success":
            print(message.result)
except Exception as e:
    print(f"Query failed: {e}")

error_max_turns などのエラー結果で終わると、単発の query() は最後の結果メッセージを返したあとで、失敗の文を含むエラーを送出します(Python は ResultError)。続けたいならループを try で囲みます。続きのターンは continue: true(Python は continue_conversation=True)で出せます。

ストリーミング出力#

標準では、SDK は空でないコンテンツブロック(テキストやツール呼び出し)ごとに、Claude がそのブロックを書き終えたあとで完全な AssistantMessage を返します。途中経過を受けるには、部分メッセージを有効にします。

include_partial_messages(Python)/includePartialMessages(TypeScript)を true にすると、普段の AssistantMessage と ResultMessage に加えて、API の生のイベントを包んだ StreamEvent が届きます。手順は次のとおりです。

  1. メッセージの型で StreamEvent を見分ける
  2. event を取り出し、その type を見る
  3. content_block_delta のうち delta.type が text_delta のものに、テキストの断片が入っている
python
from claude_agent_sdk import query, ClaudeAgentOptions
from claude_agent_sdk.types import StreamEvent

options = ClaudeAgentOptions(include_partial_messages=True, allowed_tools=["Bash", "Read"])
async for message in query(prompt="プロジェクトのファイルを一覧にして", options=options):
    if isinstance(message, StreamEvent):
        event = message.event
        if event.get("type") == "content_block_delta":
            delta = event.get("delta", {})
            if delta.get("type") == "text_delta":
                print(delta.get("text", ""), end="", flush=True)
typescript
for await (const message of query({
  prompt: "プロジェクトのファイルを一覧にして",
  options: { includePartialMessages: true, allowedTools: ["Bash", "Read"] },
})) {
  if (message.type === "stream_event") {
    const event = message.event;
    if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
      process.stdout.write(event.delta.text);
    }
  }
}

StreamEvent のリファレンス#

言語 型
Python StreamEvent(claude_agent_sdk.types からインポート)
TypeScript type: 'stream_event' を持つ SDKPartialAssistantMessage

どちらも、累積したテキストではなく API の生のイベントを持つので、テキストの断片は自分で足し合わせます。

  • parent_tool_use_id は、Python で None、TypeScript で null のままです。ストリームイベントはメインのセッションだけに出て、サブエージェントのトークン単位の差分は流れません。サブエージェントの出力を区別するには、parent_tool_use_id を持つ完全なメッセージを使います
  • Claude Code は、ターンの最初のピング以外のストリームイベントと、ターンが答えるメッセージが変わったときに、user_message_uuid を付けます。Python の StreamEvent はこのフィールドを持ちません
イベントの種類 内容
message_start 新しいメッセージの開始
content_block_start 新しいコンテンツブロック(テキストかツール使用)の開始
content_block_delta 内容の増分
content_block_stop コンテンツブロックの終わり
message_delta メッセージ単位の更新(停止の理由・使用量)
message_stop メッセージの終わり

メッセージの流れ#

AssistantMessage は、空でないコンテンツブロックが完了するたびに出ます。テキストとツール呼び出しを含む応答なら2つになり、どちらも同じメッセージ ID を持ちます(TypeScript は message.message.id、Python は message.message_id)。部分メッセージを有効にすると、各 AssistantMessage はそのブロックの content_block_stop より前に届き、順序は次のとおりです。

text
StreamEvent (message_start)
StreamEvent (content_block_start) - text block
StreamEvent (content_block_delta) - text chunks...
AssistantMessage - complete text block
StreamEvent (content_block_stop)
StreamEvent (content_block_start) - tool_use block
StreamEvent (content_block_delta) - tool input chunks...
AssistantMessage - complete tool_use block
StreamEvent (content_block_stop)
StreamEvent (message_delta)
StreamEvent (message_stop)
... ツールが実行される ...
... 次のターンのストリームイベント ...
ResultMessage - 最終結果

部分メッセージを有効にしないと、StreamEvent 以外のメッセージがすべて届きます。SystemMessage(初期化)・AssistantMessage・ResultMessage と、会話履歴が圧縮された境界のメッセージ(TypeScript は SDKCompactBoundaryMessage、Python は subtype が "compact_boundary" の SystemMessage)です。

ツール呼び出しのストリーミング#

ツール呼び出しも少しずつ流れます。3つのイベントで追えます。

イベント 意味
content_block_start(content_block.type が tool_use) ツールの開始
content_block_delta(delta.type が input_json_delta) 入力の JSON の断片(partial_json)
content_block_stop ツール呼び出しの完了
typescript
let currentTool: string | null = null;
let toolInput = "";

for await (const message of query({
  prompt: "README.md を読んで",
  options: { includePartialMessages: true, allowedTools: ["Read", "Bash"] },
})) {
  if (message.type !== "stream_event") continue;
  const event = message.event;
  if (event.type === "content_block_start" && event.content_block.type === "tool_use") {
    currentTool = event.content_block.name;
    toolInput = "";
  } else if (event.type === "content_block_delta" && event.delta.type === "input_json_delta") {
    toolInput += event.delta.partial_json;
  } else if (event.type === "content_block_stop" && currentTool) {
    console.log(`Tool ${currentTool} called with: ${toolInput}`);
    currentTool = null;
  }
}

チャット画面では、ツール実行中かを示すフラグ(in_tool)を持ち、実行中は [Using Read...] のような表示を出し、そのあいだテキストの出力を止め、content_block_stop で「done」を出す形が使えます。

補足

構造化出力を併用して部分メッセージを有効にすると、JSON は検証前のツール呼び出しの input_json_delta として流れ、検証済みの結果は最終の ResultMessage.structured_output にだけ入ります。

承認と質問を受ける#

Claude がユーザーに確認したくなる場面は2つあります。ツールの使用に許可が要るとき、と、AskUserQuestion ツールで確認の質問をするときです。どちらも canUseTool コールバックを呼び、返事を返すまで実行が止まります。

  • 質問と選択肢は Claude が作ります。アプリの側で質問を足すことはできません
  • コールバックは無期限に待てます。ユーザーの返事に時間がかかりすぎて、プロセスを生かしておけない場合は、PreToolUse フックで defer の判断を返し、プロセスを終えてあとで保存したセッションから再開します(フックのリファレンス)
python
async def handle_tool_request(tool_name, input_data, context):
    ...  # ユーザーに尋ねて、許可か拒否を返す

options = ClaudeAgentOptions(can_use_tool=handle_tool_request)
typescript
const options = {
  canUseTool: async (toolName, input, options) => {
    // options は { signal: AbortSignal, suggestions?: PermissionUpdate[] }
    // ユーザーに尋ねて、許可か拒否を返す
  },
};

呼ばれるのは次の2つの場合です。

  1. ツールの承認が要るとき:権限ルールや権限モードで自動承認されないツールを Claude が使おうとした
  2. Claude が質問するとき:tool_name が "AskUserQuestion"。tools 配列を指定している場合は、AskUserQuestion を含める

注意

canUseTool は、自動承認されたツールには呼ばれません。許可ルールや acceptEdits・bypassPermissions のようなモードが先に承認すると、コールバックまで来ません。allowed_tools に素のツール名で入れたツールは、ask ルールや plan モードのように評価の流れが確認に戻すときだけ、canUseTool で検査されます。すべてのツール呼び出しに効かせたい処理は、ほかの流れより先に動き、許可・拒否・入力の変更ができる PreToolUse フックで書きます(SDK のツール・権限・拡張)。

Claude が承認待ちのときに、Slack・メール・プッシュで外部へ通知するには、PermissionRequest フックが使えます。dontAsk モードのように、コールバックを呼ばない構成もあります。

ツールの承認要求への返事#

コールバックには3つの引数が渡ります。

引数 内容
toolName Claude が使おうとするツール名("Bash"・"Write"・"Edit" など)
input Claude がツールへ渡すパラメーター。内容はツールによる
options(TypeScript)/context(Python) 追加の文脈。再確認を避けるための PermissionUpdate の提案 suggestions と、取り消しの信号を含む。TypeScript の signal は AbortSignal、Python の信号のフィールドは将来用に予約されている

input の例です。

ツール 入力のフィールド
Bash command、description、timeout
Write file_path、content
Edit file_path、old_string、new_string
Read file_path、offset、limit

返す値は許可か拒否のどちらかです。

返事 Python TypeScript
許可 PermissionResultAllow(updated_input=...) { behavior: "allow", updatedInput }
拒否 PermissionResultDeny(message=...) { behavior: "deny", message }
  • 許可のとき、入力を書き換えて返さなければ、Claude が求めた入力のままツールが動きます。v2.1.207 より前の Claude Code は、updatedInput を省いた許可の結果を検証エラーとして拒否していました
  • 拒否のときは、理由のメッセージを付けます。Claude がそれを読み、方針を変えることがあります

許可と拒否のほかに、次のような返し方ができます。

返し方 内容
承認 入力をそのまま返し、ツールが求めどおりに動く
変更つきで承認 実行前に入力を書き換える(パスの無害化・制約の追加)。Claude には書き換えたことが伝わらない
承認して覚える suggestions の提案を updatedPermissions に入れて返すと、ルールが適用される。destination が localSettings の提案は .claude/settings.local.json にルールを書くので、以後のセッションでは確認なしになる
拒否 ツールを止め、理由を Claude に伝える
代案を示す 拒否しつつ、ユーザーの望む方向をメッセージで伝える
完全に方向を変える ストリーミング入力で新しい指示を直接送る
python
async def can_use_tool(tool_name, input_data, context):
    choice = await ask_user(f"Allow {tool_name}?", ["once", "always", "no"])
    if choice == "always":
        persist = [s for s in context.suggestions if s.destination == "localSettings"]
        return PermissionResultAllow(updated_input=input_data, updated_permissions=persist)
    if choice == "once":
        return PermissionResultAllow(updated_input=input_data)
    return PermissionResultDeny(message="User declined")
typescript
canUseTool: async (toolName, input, { suggestions = [] }) => {
  const choice = await askUser(`Allow ${toolName}?`, ["once", "always", "no"]);
  if (choice === "always") {
    const persist = suggestions.filter((s) => s.destination === "localSettings");
    return { behavior: "allow", updatedInput: input, updatedPermissions: persist };
  }
  if (choice === "once") return { behavior: "allow", updatedInput: input };
  return { behavior: "deny", message: "User declined" };
};
  • 「承認して覚える」の Python の例は claude-agent-sdk 0.1.80 以降が必要です
  • TypeScript では、options に suppressAlwaysAllowRule: true が付いた要求には、常に許可する選択肢を出しません(Agent SDK v0.3.268 以降。Python の context には無い)

Python で can_use_tool を使うときは、ストリームを開いたままにするために、ダミーの PreToolUse フック({"continue_": True} を返す)を登録し、プロンプトを非同期ジェネレーターで渡す回避策が必要です。

確認の質問(AskUserQuestion)に答える#

Claude は、有効な進め方が複数あるときに AskUserQuestion を呼びます。canUseTool の toolName が AskUserQuestion になり、入力に選択式の質問が入ります。

ヒント

確認の質問は、Claude がコードを調べて計画の前に質問する plan モードで特に多く出ます。要件を集めてから変更させたい対話的な流れに向きます。

手順です。

  1. canUseTool を渡す。AskUserQuestion は標準で使える。読み取り専用のエージェントなどで tools 配列を指定するなら、そこに AskUserQuestion を含める(含めないと質問できない)
  2. コールバックで toolName が AskUserQuestion かを見て、ほかのツールと分ける
  3. 入力の questions 配列を読む
  4. ユーザーに提示して選択を集める(端末・Web フォーム・モバイルのダイアログなど)
  5. answers を作って返す。キーが質問文(question)、値が選んだ選択肢の label
json
{
  "questions": [
    {
      "question": "出力の形式はどうしますか?",
      "header": "Format",
      "options": [
        { "label": "Summary", "description": "要点の概要" },
        { "label": "Detailed", "description": "詳しい説明" }
      ],
      "multiSelect": false
    }
  ]
}
質問のフィールド 内容
question 表示する質問文の全体
header 質問の短い見出し(最大12文字)
options 2〜4個の選択肢。各 label と description。TypeScript では preview も付けられる
multiSelect true なら複数選べる
返す値のフィールド 内容
questions 元の質問の配列をそのまま渡す(ツールの処理に必須)
answers キーが質問文、値が選んだ label のオブジェクト
response 構造化された質問に答えず、ユーザーが自由に打った返事(任意)
typescript
return {
  behavior: "allow",
  updatedInput: {
    questions: input.questions,
    answers: {
      "出力の形式はどうしますか?": "Summary",
      "含める節は?": "Introduction, Conclusion",
    },
  },
};
python
return PermissionResultAllow(
    updated_input={
        "questions": input_data.get("questions", []),
        "answers": {
            "出力の形式はどうしますか?": "Summary",
            "含める節は?": ["Introduction", "Conclusion"],
        },
    }
)
  • 複数選択は、ラベルの配列か、", " で連結した文字列で渡します
  • Claude の選択肢で足りないときのために、「Other」のような自由入力の選択肢を足し、ユーザーが打った文を answers の値にします(「Other」という語は入れない)
  • response は、ユーザーが質問のカードを閉じて、個別の質問への答えではない一般の返事を打てる UI のときだけ設定します。設定すると、Claude には質問ごとの答えの一覧でなく「The user responded: …」が届きます

選択肢のプレビュー(TypeScript)#

toolConfig.askUserQuestion.previewFormat を設定すると、各選択肢に preview が付き、ラベルの横に見た目の見本を出せます。設定しなければ Claude はプレビューを作らず、フィールドもありません。

previewFormat preview の中身
未設定(既定) フィールドなし
"markdown" ASCII アートとコードブロック
"html" スタイル付きの <div> の断片(<script>・<style>・<!DOCTYPE> は、コールバックの前に SDK が拒否する)

形式はセッション内の全質問に適用されます。視覚的な比較が役立つ選択肢(レイアウトや配色)にだけ付き、はい・いいえのような確認には付きません。描画の前に undefined を確かめます。

typescript
for await (const message of query({
  prompt: "カードのレイアウトを選ぶのを手伝って",
  options: {
    toolConfig: { askUserQuestion: { previewFormat: "html" } },
    canUseTool: async (toolName, input) => ({ behavior: "allow", updatedInput: input }),
  },
})) { /* ... */ }

制限#

  • サブエージェント(Agent ツールで起こしたもの)では、いまのところ AskUserQuestion を使えません
  • 1回の AskUserQuestion は、1〜4個の質問で、各質問に2〜4個の選択肢です

そのほかの入力の方法#

  • ストリーミング入力:実行中の割り込み・先回りの文脈の追加・長い処理の途中の追い質問に向きます。承認の節目だけでなく、実行の間ずっとユーザーとやり取りする UI に向きます
  • 自作ツール:AskUserQuestion の選択式を超えるフォームや多段の流れ、既存の承認システムとの連携、アプリ固有のやり取りに向きます。実装の手間は canUseTool より大きいですが、やり取りを完全に制御できます(SDK のツール・権限・拡張)

構造化出力#

構造化出力は、エージェントが返すデータの形を JSON Schema で決める機能です。エージェントは必要なツールを使い、最後に、スキーマに合うと検証された JSON を返します。合わなければ再度の入力を促し、再試行の上限内に成功しなければ、構造化データでなくエラーの結果になります。Zod(TypeScript)や Pydantic(Python)でスキーマを書くと、型付きのオブジェクトで受け取れます。

outputFormat(Python は output_format)にスキーマを渡し、結果メッセージの structured_output に検証済みのデータが入ります。

typescript
const schema = {
  type: "object",
  properties: {
    company_name: { type: "string" },
    founded_year: { type: "number" },
    headquarters: { type: "string" },
  },
  required: ["company_name"],
};

try {
  for await (const message of query({
    prompt: "Anthropic について調べて、会社の基本情報を返して",
    options: { outputFormat: { type: "json_schema", schema } },
  })) {
    if (message.type === "result" && message.subtype === "success" && message.structured_output) {
      console.log(message.structured_output);
    }
  }
} catch (error) {
  console.error(`Session ended with an error: ${error}`);
}
python
async for message in query(
    prompt="Anthropic について調べて、会社の基本情報を返して",
    options=ClaudeAgentOptions(output_format={"type": "json_schema", "schema": schema}),
):
    if isinstance(message, ResultMessage) and message.structured_output:
        print(message.structured_output)

Zod と Pydantic#

  • SDK は JSON Schema の draft-07 で検証するので、新しい版を宣言したスキーマは拒否されます。Zod は既定で draft 2020-12 を出すため、変換のとき target: "draft-7" を渡します
  • Pydantic は Model.model_json_schema() でスキーマを作り、結果は Model.model_validate(message.structured_output) で検証して型付きにします
typescript
const schema = z.toJSONSchema(FeaturePlan, { target: "draft-7" });
// ... outputFormat: { type: "json_schema", schema }
const parsed = FeaturePlan.safeParse(message.structured_output);

outputFormat の指定#

フィールド 内容
type "json_schema" を指定する
schema 出力の構造を表す JSON Schema のオブジェクト
  • 基本の型(object・array・string・number・boolean・null)・enum・const・required・入れ子のオブジェクト・$ref が使えます。対応する機能と制限の全体は、Claude API の構造化出力のドキュメントにある「JSON Schema limitations」にあります
  • 不正な JSON Schema は、起動時に問題を示すエラーで失敗します。v2.1.205 より前は黙って無視され、エージェントが構造のないテキストを返していました
  • format(例:"format": "email")は注釈として受け付けますが、SDK の検証では強制されません。v2.1.205 より前は、format を含むスキーマは不正として扱われていました

TODO を探して git blame で書いた人を調べる例のように、複数のツールを使う作業でも使えます。git blame の情報が無いこともあるので、author や date は省略可能にします。

エラーの扱い#

エージェントがスキーマに合う JSON を作れないと失敗します。スキーマが作業に対して複雑すぎる、作業が曖昧、検証エラーの修正で再試行の上限に達した、などが原因です。検証の失敗がなくても、モデルのフォールバックが、完了済みの出力をストリームの途中で取り消し、再試行で置き換わらなければ同じエラーになります。どちらかは、結果メッセージの errors リストで見分けます。

subtype 意味
success 出力が作られ、検証にも通った
error_max_structured_output_retries 複数回の試行のあと、有効な出力が残らなかった(検証の失敗、または再試行なしのフォールバックによる取り消し)

subtype が success でも structured_output が無いことがあります(エージェントが構造化出力を作らずに終わった場合など)。これも失敗として扱います。成功とみなすのは、subtype が success で、かつ structured_output があるときだけにします。

typescript
if (msg.type === "result") {
  if (msg.subtype === "success" && msg.structured_output) {
    console.log(msg.structured_output);
  } else if (msg.subtype === "error_max_structured_output_retries") {
    console.error("Could not produce valid output");
  } else {
    console.error("Run ended without a structured output");
  }
}

エラーを避けるコツです。

ヒント

スキーマは絞る(深い入れ子と必須フィールドの多さは満たしにくい。単純に始めて足す)。作業で得られないかもしれない情報のフィールドは省略可能にする。プロンプトは曖昧にしない。

ファイルの巻き戻し(チェックポイント)#

ファイルのチェックポイントは、セッション中の Write・Edit・NotebookEdit によるファイル変更を記録し、ファイルを以前の状態に戻せるようにします。変更の前にバックアップを作り、応答のストリームの user メッセージに、復元点になる UUID が付きます(チェックポイントの仕組みは対話の Claude Code にもあります:チェックポイントと巻き戻し)。

注意

記録されるのは、Write・Edit・NotebookEdit による変更だけです。Bash のコマンド(echo > file.txt や sed -i)による変更は記録されません。サブエージェントが加える編集も、フォアグラウンドで動く context: fork のスキルを除き、記録されません。

  • 巻き戻しは、ディスク上のファイルを戻すだけで、会話は戻りません。rewindFiles()(Python は rewind_files())を呼んだあとも、会話の履歴と文脈はそのままです
  • 巻き戻すと、Claude Code は作成したファイルを削除し、変更したファイルを、その時点の内容に戻します
  • シンボリックリンク・ハードリンク・通常でないファイルのパス、親ディレクトリがチェックポイントの時点の場所に解決されないファイル、バックアップを安全に読めないファイルは飛ばされます。飛ばした数は RewindFilesResult の skippedLinks に入ります(飛ばす動きは Claude Code v2.1.216 以降。それより前は、リンクを通して書き込み・削除していました)

使い方#

  1. チェックポイントを有効にし、チェックポイントの UUID を受け取れるようにする
  2. 応答のストリームから UUID(user メッセージの uuid)と、必要ならセッション ID を控える
  3. 巻き戻す
オプション Python TypeScript 内容
チェックポイントを有効にする enable_file_checkpointing=True enableFileCheckpointing: true ファイルの変更を追跡する
チェックポイントの UUID を受け取る extra_args={"replay-user-messages": None} extraArgs: { 'replay-user-messages': null } ストリームの user メッセージに UUID を付ける(必須)
typescript
const opts = {
  enableFileCheckpointing: true,
  permissionMode: "acceptEdits" as const,
  extraArgs: { "replay-user-messages": null }, // UUID を受け取るのに必要
};

const response = query({ prompt: "認証モジュールをリファクタリングして", options: opts });
let checkpointId: string | undefined;
let sessionId: string | undefined;

for await (const message of response) {
  if (message.type === "user" && message.uuid && !checkpointId) checkpointId = message.uuid;
  if ("session_id" in message && !sessionId) sessionId = message.session_id;
}

// あとで:空のプロンプトでセッションを再開して巻き戻す
if (checkpointId && sessionId) {
  const rewindQuery = query({ prompt: "", options: { ...opts, resume: sessionId } });
  for await (const msg of rewindQuery) {
    await rewindQuery.rewindFiles(checkpointId);
    break;
  }
}
python
options = ClaudeAgentOptions(
    enable_file_checkpointing=True,
    permission_mode="acceptEdits",
    extra_args={"replay-user-messages": None},
)

async with ClaudeSDKClient(options) as client:
    await client.query("認証モジュールをリファクタリングして")
    async for message in client.receive_response():
        if isinstance(message, UserMessage) and message.uuid and not checkpoint_id:
            checkpoint_id = message.uuid
        if isinstance(message, ResultMessage) and not session_id:
            session_id = message.session_id

async with ClaudeSDKClient(
    ClaudeAgentOptions(enable_file_checkpointing=True, resume=session_id)
) as client:
    await client.query("")  # 空のプロンプトで接続を開く
    async for message in client.receive_response():
        await client.rewind_files(checkpoint_id)
        break
  • 多くの用途では最初の user メッセージの UUID を控えます。そこへ戻すと、追跡中のファイルが元の状態に戻ります。途中の状態へ戻すには、全部の UUID を控えます(下の「複数の復元点」)
  • セッション ID は、ストリームが終わったあとで巻き戻すなら必要で、ストリームの処理中にすぐ rewindFiles() を呼ぶなら要りません
  • 巻き戻しのセッションでも、チェックポイントを有効にします。巻き戻しは、応答のループの中から呼びます
  • ストリームを読み終えたあとでは、CLI のプロセスとの接続が閉じているので、rewindFiles() は呼べません(ProcessTransport is not ready for writing)。空のプロンプトで再開してから呼びます

セッション ID とチェックポイントの ID を控えていれば、CLI からも巻き戻せます。claude の実行ファイルが要ります。SDK はチェックポイントを自動で有効にしますが、claude -p を直接実行するときは CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING を設定します。

bash
CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING=true claude -p --resume <session-id> --rewind-files <checkpoint-uuid>

--rewind-files は claude --help に出ませんが、CLI は受け付けます。成功すると Files rewound to state at message <checkpoint-uuid> を出し、プロンプトを送らずに終了します(CLI のコマンドとフラグ)。

よくある使い方#

  • 危険な操作の前のチェックポイント:各ターンの前に最新の UUID だけを控えて上書きし、問題が起きたら、最後の安全な状態へ巻き戻してループを抜けます。自分の条件(エラー検出・検証の失敗・ユーザーの入力)で判断します
  • 複数の復元点:全部の UUID をメタデータ(説明・時刻)つきで配列に控え、セッションが終わったあとで好きな時点へ戻します。ターン1でリファクタリングし、ターン2でテストを足したときに、リファクタリングを残してテストだけ戻すような使い方です

制限#

制限 内容
Write・Edit・NotebookEdit だけ Bash のコマンドによる変更は追跡されない
サブエージェントの編集 記録・復元されない(フォアグラウンドで動く context: fork のスキルを除く)。記録されない編集は git で戻す
同じセッション チェックポイントは、作ったセッションに結び付く
ファイルの内容だけ ディレクトリの作成・移動・削除は、巻き戻しで元に戻らない
ローカルのファイル リモートやネットワーク上のファイルは追跡されない

トラブルシューティング#

症状 原因と対処
enableFileCheckpointing や rewindFiles() が無い SDK が古い。pip install --upgrade claude-agent-sdk か npm install @anthropic-ai/claude-agent-sdk@latest で更新する
user メッセージに uuid が無い replay-user-messages を設定していない。extra_args か extraArgs を足す
No file checkpoint found for this message 元のセッションでチェックポイントが有効でなかった、またはセッションを完了させる前に再開して巻き戻そうとした。元のセッションで有効にし、最初の user メッセージの UUID を控え、セッションを最後まで終え、空のプロンプトで再開して rewindFiles() を1回呼ぶ
File rewinding is not enabled チェックポイントを有効にしていないセッション(再開したものを含む)や、素の claude -p --rewind-files で巻き戻そうとした。SDK は、巻き戻しをするセッションで有効にしたときだけ CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING を内部で設定する。素の CLI では、その環境変数を自分で設定する
ProcessTransport is not ready for writing 応答を読み終えたあとで巻き戻しを呼んだ。空のプロンプトで再開し、新しい query に対して呼ぶ

タスク(ToDo)の追跡#

タスク管理のツール(TodoWrite・TaskCreate・TaskGet・TaskUpdate・TaskList)は、標準では、一部のモデルでだけ付きます。そのモデルでは、Claude が書いた ToDo リストを持ち、作業しながら各項目の状態を更新し、その変化がメッセージストリームに構造化されたツール呼び出しとして流れます。アプリがそのツール呼び出しを読んで、タスクの記録や進捗の表示をしたいときだけ、有効にします。新しいモデルは ToDo リストなしでも複数ステップの作業を進めるので、そのときはこの節は要りません。

補足

標準で使えるのは、Claude 3.x のモデル、Opus 4〜4.7、Sonnet 4〜4.6、Haiku 4.5 です。Claude Code が認識しないモデル ID を含む、そのほかのモデルでは、有効にしない限り使えません。使える場所では、4つの Task ツールが提供され、CLAUDE_CODE_ENABLE_TASKS=0 のときは代わりに TodoWrite が提供されます。この既定の範囲は Claude Code v2.1.268 以降のもので、TypeScript Agent SDK は v0.3.268 から同梱しています。

SDK は同梱の Claude Code のバイナリを通して、この既定を適用します。pathToClaudeCodeExecutable(Python は cli_path)で自分の Claude Code を指すと、その版の既定に従います。使えるツールの確認は ツール一覧 を見てください。

有効にする方法は次のいずれかです。

  • allowedTools(Python は allowed_tools)に、そのツールを1つ指定する
  • tools オプションにツールを列挙する(セッションの組み込みツールが、列挙したものに絞られるので、ほかに使うものも含める)
  • env に CLAUDE_CODE_ENABLE_TODO_TOOLS=1 を入れる。TypeScript は env が環境を置き換えるので ...process.env を展開し、Python は継承した環境に重ねられる

ToDo の一生#

  1. 作成:Claude がタスクを見つけたら pending で追加する
  2. 開始:作業を始めるときに in_progress にする
  3. 完了:成功したら完了にする
  4. 削除:不要になったら、TaskUpdate で status: "deleted" にする

Claude は、3つ以上の別々の動作が要る複雑な作業、複数の項目を挙げたユーザーの一覧、進捗の追跡が役立つ長い処理、ToDo を求められたときに作ります。ごく短い作業や1ステップの依頼では作らないことがあります。

ストリームから読む#

アシスタントのストリームの TaskCreate と TaskUpdate の tool_use ブロックを見ます。

typescript
for await (const message of query({
  prompt: "ホームページ・紹介ページ・共通スタイルシートの静的サイトを、ToDo で進捗を追いながら作って",
  options: {
    maxTurns: 15,
    permissionMode: "acceptEdits",
    env: { ...process.env, CLAUDE_CODE_ENABLE_TODO_TOOLS: "1" },
  },
})) {
  if (message.type !== "assistant") continue;
  for (const block of message.message.content) {
    if (block.type !== "tool_use") continue;
    if (block.name === "TaskCreate") {
      console.log(`+ ${(block.input as { subject: string }).subject}`);
    } else if (block.name === "TaskUpdate") {
      const input = block.input as { taskId?: string; id?: string; task_id?: string; status?: string };
      const taskId = input.taskId ?? input.id ?? input.task_id;
      if (taskId && input.status) console.log(`  ${taskId} -> ${input.status}`);
    }
  }
}
  • ストリームに流れる tool_use の入力は、モデルが出した生の形です。Claude Code は、近いが違うキー名(id・task_id を taskId に、active_form を activeForm に)を実行前に直しますが、その直しはストリームには反映されません。TaskUpdate の入力は、正規のキー名があるとは限らない前提で読みます
  • 割り当てられたタスク ID は TaskCreate の入力にありません。ID は、tool_result ブロックを持つ user メッセージの tool_use_result に入ります(TypeScript では TaskCreateOutput、Python では同じ形の dict)。tool_use_id で、tool_use の呼び出しと tool_result を対応づけ、ID を読みます。上の例は ID を控えないので、更新を作成に結び付けられません。進捗表示を作るなら、ID を控えるクラス(TaskTracker)でタスクを ID をキーに持ち、変化のたびに、完了と進行中の件数と、進行中の項目の activeForm を表示します
  • バックグラウンドのタスク(バックグラウンドのコマンドやサブエージェント)は、別に SDKTaskNotificationMessage(Python は TaskNotificationMessage)などのタスクのシステムメッセージで報告されます。ToDo の動きは、アシスタントメッセージの tool_use ブロックとして見えます
  • error_max_turns で終わると、単発の query() は Reached maximum number of turns を含むエラーを送出するので、ループを try で囲みます

公式ドキュメント(英語)

2026年10月5日時点の内容をもとに、日本語でまとめています。

ページの一覧