Claude Agent SDK のセッションでタスク管理ツールを有効にすると、Claude は複数の手順からなる作業を進めるときに自分で TODO リストを書き、各項目の状態を更新しながら進めます。その変化はメッセージストリームに構造化された tool_use ブロックとして流れてくるので、アプリケーション側でログに残したり、独自の進捗表示に使ったりできます。
この記事では、公式ドキュメント「Track todos」に沿って、タスク管理ツールが既定で使えるモデルと有効化の方法、タスクの状態の遷移、TaskCreate と TaskUpdate を読んで進捗を表示するコード例を整理します。メッセージの流れ全体は「Claude Agent SDK でエージェントを構築する」を、ツールの許可は「Agent SDK の権限設定」を参照してください。
タスク管理ツールが使えるモデルと有効化
タスク管理に使うツールは次の5つです。
| ツール | 役割 |
|---|---|
TaskCreate | タスクリストに新しいタスクを作る |
TaskGet | 特定のタスクの詳細を取得する |
TaskList | すべてのタスクと現在の状態を一覧する |
TaskUpdate | タスクの状態や依存関係、詳細を更新する。タスクの削除もこれで行う |
TodoWrite | セッションのタスクチェックリストを管理する旧来のツール。既定では4つの Task ツールが代わりに使われる |
公式ドキュメントによると、これらのツールが既定で提供されるのは Claude Opus 4.7、Claude Sonnet 4.6、Claude Haiku 4.5 と、それより前のモデルだけです。それ以外のモデル(Claude Code が認識しないモデル ID を含む)では、オプトインしない限り提供されません。新しいモデルは TODO リストを書かなくても複数手順の作業を進められるため、Claude に作業を進めさせるだけならこのページの設定は不要です。オプトインするのは、アプリケーションがタスクの tool_use を読んでログや進捗表示に使う場合です。
ツールが使える環境では4つの Task ツールが提供され、環境変数 CLAUDE_CODE_ENABLE_TASKS=0 を設定した場合は代わりに TodoWrite が提供されます。この既定は Claude Code v2.1.268 以降のもので、TypeScript の Agent SDK では v0.3.268 から同梱されています。pathToClaudeCodeExecutable(TypeScript)や cli_path(Python)で別にインストールした Claude Code を指定した場合は、そのインストール側の既定に従います。
セッションをオプトインする3つの方法
allowedTools(Python はallowed_tools)オプションにツール名のどれかを書くtoolsオプションにツールを並べる。このオプションはセッションの組み込みツールを列挙したものだけに絞るので、ほかに使う組み込みツールも一緒に書くenvオプションでCLAUDE_CODE_ENABLE_TODO_TOOLS=1を設定する(このページの例はこの方法)
env の扱いは言語で異なります。TypeScript では env がサブプロセスの環境変数を置き換えるため、...process.env を展開して継承した変数を残します。Python では env は継承した環境変数の上にマージされます。なお CLI の Claude Code でも、CLAUDE_CODE_ENABLE_TODO_TOOLS=1 claude のように起動するか claude --allowedTools TaskCreate とすれば、どのモデルでも同じツールが使えるようになります。
タスクの状態の遷移
Claude は各タスクを決まった順序で進めます。
| 段階 | 内容 |
|---|---|
| 作成 | 作業を見つけた時点で、状態 pending のタスクとして追加する |
| 着手 | 作業を始めるときに in_progress にする |
| 完了 | 作業が成功したら completed にする |
| 削除 | 不要になったタスクは、TaskUpdate で status: "deleted" を設定して消す |
進捗表示を作るときは、この4つの状態を前提に集計します。削除は独立したツールではなく TaskUpdate の状態値として届く点に注意してください。
Claude がタスクを作る場面
タスク管理ツールがあるセッションでは、Claude はたいていの複数手順の作業でタスクを作ります。公式が挙げる例は次のとおりです。
- 3つ以上の別々の操作が必要な複雑な作業
- ユーザーが複数の項目を挙げてタスクリストを渡したとき
- 進捗を追う意味がある長めの処理
- ユーザーが TODO で整理するよう明示的に頼んだとき
ごく短い依頼や1手順で終わる依頼では、タスクを作らないこともあります。
例を動かす前の共通設定
以下の例はどれも、プロジェクトで実際にファイルを作らせるため permissionMode: "acceptEdits"(Python は permission_mode="acceptEdits")でファイル編集を自動承認しています。また単発の query() 呼び出しは、ターン上限に達するとサブタイプ error_max_turns の結果メッセージを返したあと、Reached maximum number of turns を含むエラーを投げます。そのため各例はループを try ブロックで囲んで、その場合もきれいに終了するようにしています。
なお、バックグラウンドのコマンドやサブエージェントを知らせる SDKTaskNotificationMessage(Python は TaskNotificationMessage)などのタスク系システムメッセージは、ここで扱う TODO とは別物です。TODO の動きは、アシスタントメッセージ内の tool_use ブロックとして現れます。
タスクの変化をログに出す
アシスタントのストリームから TaskCreate と TaskUpdate の tool_use ブロックを拾い、新しいタスクの件名(subject)と、状態変化(タスク ID と新しい状態)を1行ずつ出力する例です。進捗画面ではなく、タスクの動きをログとして残したいときに使う形です。
import { query } from "@anthropic-ai/claude-agent-sdk";
try {
for await (const message of query({
prompt: "Create a static website with a home page, an about page, and a shared stylesheet, and track progress with todos",
// Task ツールを既定で持たないモデルでも有効にする
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") {
const input = block.input as { subject: string };
console.log(`+ ${input.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}`);
}
}
}
} catch (error) {
// 単発の query() はエラー結果を返したあとに例外を投げる
console.log(`Session ended with an error: ${error}`);
}
Python では AssistantMessage の content を回し、ToolUseBlock の block.name と block.input.get(...) で同じ処理を書きます。
入力のキー名は防御的に読む
ストリームに流れる tool_use の入力は、モデルが出力したそのままの形です。Claude Code は実行前に、id や task_id を taskId に、active_form を activeForm に直すなど、惜しいキー名を補正しますが、その補正はストリームには反映されません。上の例のように、正規の名前が必ずあると決めつけず複数の候補を順に見ます。
また、+ の行には割り当てられたタスク ID が含まれないため、このログだけでは更新と作成を対応づけられません。対応づけが必要なら、次の節のように ID を取得します。
進捗をリアルタイムに表示する
タスク ID をキーにした表を持ち、変化のたびに進捗の要約を描き直す TaskTracker クラスの例です。完了数と着手中の数を数え、着手中のタスクは subject の代わりに activeForm(進行形の表示ラベル)を表示します。
タスク ID は tool_result 側から取る
割り当てられたタスク ID は TaskCreate の入力には入っていません。Claude Code は各ツールの構造化された出力を、tool_result ブロックを運ぶユーザーメッセージの tool_use_result フィールドに入れて返します。TaskCreate の場合、その中身は TypeScript で TaskCreateOutput 型として定義された { task: { id, subject } } で、Python では同じ形の辞書です。トラッカーは tool_use_id で tool_result と tool_use を対応づけ、task.id を読み取ります。
import { query } from "@anthropic-ai/claude-agent-sdk";
type Task = { subject: string; activeForm?: string; status: string };
class TaskTracker {
private tasks = new Map<string, Task>();
private pendingCreates = new Map<string, { subject: string; activeForm?: string }>();
displayProgress() {
const items = [...this.tasks.values()];
const completed = items.filter((t) => t.status === "completed").length;
console.log(`Progress: ${completed}/${this.tasks.size} completed`);
for (const [id, task] of this.tasks) {
const text = task.status === "in_progress" && task.activeForm ? task.activeForm : task.subject;
console.log(`${id}. [${task.status}] ${text}`);
}
}
handleToolUse(block: { id: string; name: string; input: any }) {
if (block.name === "TaskCreate") {
// ID はまだ分からないので tool_use の id で保留しておく
this.pendingCreates.set(block.id, {
subject: block.input.subject,
activeForm: block.input.activeForm ?? block.input.active_form,
});
} else if (block.name === "TaskUpdate") {
const taskId = block.input.taskId ?? block.input.id ?? block.input.task_id;
if (!taskId) return;
if (block.input.status === "deleted") { this.tasks.delete(taskId); this.displayProgress(); return; }
const task = this.tasks.get(taskId);
if (!task) return;
if (block.input.status) task.status = block.input.status;
const active = block.input.activeForm ?? block.input.active_form;
if (active) task.activeForm = active;
this.displayProgress();
}
}
handleToolResult(block: { tool_use_id: string; is_error?: boolean }, result: unknown) {
const create = this.pendingCreates.get(block.tool_use_id);
if (!create) return;
this.pendingCreates.delete(block.tool_use_id);
if (block.is_error) return;
const out = result as { task?: { id: string } }; // TaskCreateOutput
if (!out?.task?.id) return;
this.tasks.set(out.task.id, { ...create, status: "pending" });
this.displayProgress();
}
async trackQuery(prompt: string) {
try {
for await (const message of query({
prompt,
options: { maxTurns: 20, permissionMode: "acceptEdits", env: { ...process.env, CLAUDE_CODE_ENABLE_TODO_TOOLS: "1" } },
})) {
if (message.type === "assistant") {
for (const block of message.message.content) {
if (block.type === "tool_use") this.handleToolUse(block);
}
}
if (message.type === "user" && Array.isArray(message.message.content)) {
for (const block of message.message.content) {
if (block.type === "tool_result") this.handleToolResult(block, message.tool_use_result);
}
}
}
} catch (error) {
console.log(`Session ended with an error: ${error}`);
}
}
}
await new TaskTracker().trackQuery("Build a complete authentication system with todos");
上のコードは公式例の構造を保ったまま表示部分を短くしたものです(公式例は状態ごとに記号を付けて表示し、着手中の件数も出します)。Python 版は UserMessage の content から ToolResultBlock を拾い、message.tool_use_result の "task" キーから ID を読む同じ構成です。Claude 自身は、TaskList で一覧を、TaskGet で1件の詳細を読み返せます。
本ページは Track todos の内容に基づく非公式の日本語解説で、ツール名・既定で使えるモデル・環境変数は Tools reference の「Task tool availability」と照合しています(確認日 2026-10-17)。ツール名・フィールド名は公式表記のまま記載しています。最新の仕様は公式ドキュメントを確認してください。