Agent SDK でタスクの進捗を追う:TaskCreate と TaskUpdate

Claude Agent SDK エンジニアリング 6分で読めます

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つの方法

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 はたいていの複数手順の作業でタスクを作ります。公式が挙げる例は次のとおりです。

ごく短い依頼や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)。ツール名・フィールド名は公式表記のまま記載しています。最新の仕様は公式ドキュメントを確認してください。