Claude Code 学习站

Claude Agent SDK Todo 任务追踪指南

介绍 Claude Agent SDK 内置的 TodoWrite 与 Task 系列工具(TaskCreate/TaskUpdate/TaskList/TaskGet),及在消息流中监控任务进度的方法。

本页目录14
AI 摘要 · 已核查整理于 2026-08-01原文:Todo Lists(Anthropic)Agent SDK任务追踪TypeScript/Python
要点速览
  • 新版本(TypeScript SDK ≥0.3.233 / Python SDK ≥0.2.139)在 Opus 4.8、Sonnet 5、Fable 5、Mythos 5 等新模型上默认不提供 TodoWrite/TaskCreate/TaskGet/TaskUpdate/TaskList,需要主动开启
  • 开启方式三选一:在 allowedTools(Python 为 allowed_tools)中列出工具名;在 tools 选项中列出;或设置环境变量 CLAUDE_CODE_ENABLE_TODO_TOOLS=1
  • 旧模型默认提供 Task 工具,只有显式设置 CLAUDE_CODE_ENABLE_TASKS=0 才会改用 TodoWrite
  • TodoWrite 每次调用会重写整个 todos 数组,而 Task 工具是增量式的:TaskCreate 新增单项、TaskUpdate 按 taskId 局部更新
  • TaskCreate 返回的 tool_result 中才包含分配的任务 id(格式为 { task: { id, subject } }),不在 TaskCreate 的输入里
  • 对 TaskUpdate 输入字段要做防御性读取,因为流中的原始字段名可能是 id/task_id/active_form,Claude Code 会在执行前修正为 taskId/activeForm 但流中不体现这一修正

本文是对 Claude Agent SDK 官方文档「Todo Lists」页的中文整理,完整与最新内容以原文为准:https://code.claude.com/docs/en/agent-sdk/todo-tracking

概述

Claude Agent SDK 内置了 todo(任务清单)功能,帮助组织复杂工作流并让用户了解任务进度。

注意:在 TypeScript Agent SDK 0.3.233 及以上、或 Python Agent SDK 0.2.139 及以上版本中,以下工具在 Opus 4.8、Sonnet 5、Fable 5、Mythos 5 及更新的模型系列上默认不可用,除非显式开启:

  • TodoWrite
  • TaskCreate
  • TaskGet
  • TaskUpdate
  • TaskList

在其他模型上,Claude Code 默认提供 Task 工具;只有当你设置 CLAUDE_CODE_ENABLE_TASKS=0 时才会改为提供 TodoWrite

模型可用性

不提供任务追踪工具的模型上,除非主动开启,否则消息流中不会出现这些工具对应的 tool_use 块。如果你在 Python 中通过 cli_path、或在 TypeScript 中通过 pathToClaudeCodeExecutable 指向自己安装的 Claude Code,则会获得该安装版本所提供的工具。要在这些新模型上获得与其他模型相同的工具,可采用以下任一方式:

  • allowedTools 选项中列出所需工具名(Python 中为 allowed_tools
  • tools 选项中列出工具名——该选项会把会话内置工具限制为其中列出的工具,因此需要把想要的工具和其他内置工具一并列出
  • env 选项中设置 CLAUDE_CODE_ENABLE_TODO_TOOLS=1(本文示例均采用此方式)。TypeScript 中 env 会替换子进程的整个环境变量,因此需要展开 ...process.env 以保留继承的变量;Python 中 env 会合并到继承的环境变量之上

环境变量

变量名类型默认值说明
CLAUDE_CODE_ENABLE_TASKSstring(\"0\")设为 \"0\" 时,旧模型改用 TodoWrite 而非默认的 Task 工具
CLAUDE_CODE_ENABLE_TODO_TOOLSstring(\"1\")设为 \"1\" 时,在默认不提供任务追踪工具的新模型上重新开启这些工具

Todo 生命周期

Claude 会按以下可预测的流程推进每个 todo 项:

  1. 创建(Created):Claude 识别到一个任务时,将其加入 todo 列表,状态为 pending
  2. 激活(Activated):Claude 开始处理该任务时,将状态设为 in_progress
  3. 完成(Completed):任务成功完成时,Claude 将其标记为 completed
  4. 移除(Removed):Claude 在 TaskUpdate 调用中将 status 设为 \"deleted\",以删除不再需要的任务

何时使用 Todo

拥有任务追踪工具的会话中,Claude 会为大多数多步骤工作创建 todo,例如:

  • 复杂多步骤任务:需要 3 个或更多不同操作
  • 用户提供的任务列表:提及多个事项时
  • 非平凡操作:能从进度追踪中受益的任务
  • 显式请求:用户明确要求组织 todo 时

对于非常简短或单步骤的请求,Claude 可能会跳过创建 todo。

示例代码

运行以下示例前,请先按照快速开始安装 Claude Agent SDK。

每个示例会一直运行到 agent 完成并产出最终的 result 消息为止。如果会话先达到轮次上限,该 result 消息的 subtype 会是 error_max_turns。可通过检查 subtype 判断结束原因。

这些示例使用单次(single-shot)的 query() 调用。在产出 error_max_turns 的 result 之后,query() 会抛出一个包含 Reached maximum number of turns 的错误。每个示例都用 try 代码块包裹循环,以便在此情况发生时干净地退出。

关于 result 的各种 subtype,详见处理 result

监控 Todo 变化

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

try {
  for await (const message of query({
    prompt: "Optimize my React app performance and track progress with todos",
    // Re-enable TodoWrite, which this example monitors. Without it, the SDK uses
    // Task tools instead and these tool_use blocks never appear. ENABLE_TODO_TOOLS
    // keeps the tools on models where Claude Code otherwise doesn't provide them.
    options: { maxTurns: 15, env: { ...process.env, CLAUDE_CODE_ENABLE_TASKS: "0", CLAUDE_CODE_ENABLE_TODO_TOOLS: "1" } }
  })) {
    // Todo updates are reflected in the message stream
    if (message.type === "assistant") {
      for (const block of message.message.content) {
        if (block.type === "tool_use" && block.name === "TodoWrite") {
          const todos = block.input.todos;

          console.log("Todo Status Update:");
          todos.forEach((todo, index) => {
            const status =
              todo.status === "completed" ? "✅" : todo.status === "in_progress" ? "🔧" : "❌";
            console.log(`${index + 1}. ${status} ${todo.content}`);
          });
        }
      }
    }
  }
} catch (error) {
  // A single-shot query() throws after yielding an error result,
  // such as when the maxTurns limit is hit.
  console.log(`Session ended with an error: ${error}`);
}
import asyncio

from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, ToolUseBlock


async def main():
    try:
        async for message in query(
            prompt="Optimize my React app performance and track progress with todos",
            # Re-enable TodoWrite, which this example monitors. Without it, the SDK uses
            # Task tools instead and these tool_use blocks never appear. ENABLE_TODO_TOOLS
            # keeps the tools on models where Claude Code otherwise doesn't provide them.
            options=ClaudeAgentOptions(max_turns=15, env={"CLAUDE_CODE_ENABLE_TASKS": "0", "CLAUDE_CODE_ENABLE_TODO_TOOLS": "1"}),
        ):
            # Todo updates are reflected in the message stream
            if isinstance(message, AssistantMessage):
                for block in message.content:
                    if isinstance(block, ToolUseBlock) and block.name == "TodoWrite":
                        todos = block.input["todos"]

                        print("Todo Status Update:")
                        for i, todo in enumerate(todos):
                            status = (
                                "✅"
                                if todo["status"] == "completed"
                                else "🔧"
                                if todo["status"] == "in_progress"
                                else "❌"
                            )
                            print(f"{i + 1}. {status} {todo['content']}")
    except Exception as error:
        # A single-shot query() raises after yielding an error result,
        # such as when the max_turns limit is hit.
        print(f"Session ended with an error: {error}")


asyncio.run(main())

实时进度展示

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

class TodoTracker {
  private todos: any[] = [];

  displayProgress() {
    if (this.todos.length === 0) return;

    const completed = this.todos.filter((t) => t.status === "completed").length;
    const inProgress = this.todos.filter((t) => t.status === "in_progress").length;
    const total = this.todos.length;

    console.log(`\nProgress: ${completed}/${total} completed`);
    console.log(`Currently working on: ${inProgress} task(s)\n`);

    this.todos.forEach((todo, index) => {
      const icon =
        todo.status === "completed" ? "✅" : todo.status === "in_progress" ? "🔧" : "❌";
      const text = todo.status === "in_progress" ? todo.activeForm : todo.content;
      console.log(`${index + 1}. ${icon} ${text}`);
    });
  }

  async trackQuery(prompt: string) {
    try {
      for await (const message of query({
        prompt,
        // On every model, re-enable TodoWrite, which this tracker watches for.
        options: { maxTurns: 20, env: { ...process.env, CLAUDE_CODE_ENABLE_TASKS: "0", CLAUDE_CODE_ENABLE_TODO_TOOLS: "1" } }
      })) {
        if (message.type === "assistant") {
          for (const block of message.message.content) {
            if (block.type === "tool_use" && block.name === "TodoWrite") {
              this.todos = block.input.todos;
              this.displayProgress();
            }
          }
        }
      }
    } catch (error) {
      // A single-shot query() throws after yielding an error result,
      // such as when the maxTurns limit is hit.
      console.log(`Session ended with an error: ${error}`);
    }
  }
}

// Usage
const tracker = new TodoTracker();
await tracker.trackQuery("Build a complete authentication system with todos");
import asyncio

from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, ToolUseBlock
from typing import List, Dict


class TodoTracker:
    def __init__(self):
        self.todos: List[Dict] = []

    def display_progress(self):
        if not self.todos:
            return

        completed = len([t for t in self.todos if t["status"] == "completed"])
        in_progress = len([t for t in self.todos if t["status"] == "in_progress"])
        total = len(self.todos)

        print(f"\nProgress: {completed}/{total} completed")
        print(f"Currently working on: {in_progress} task(s)\n")

        for i, todo in enumerate(self.todos):
            icon = (
                "✅"
                if todo["status"] == "completed"
                else "🔧"
                if todo["status"] == "in_progress"
                else "❌"
            )
            text = (
                todo["activeForm"]
                if todo["status"] == "in_progress"
                else todo["content"]
            )
            print(f"{i + 1}. {icon} {text}")

    async def track_query(self, prompt: str):
        try:
            async for message in query(
                prompt=prompt,
                # On every model, re-enable TodoWrite, which this tracker watches for.
                options=ClaudeAgentOptions(max_turns=20, env={"CLAUDE_CODE_ENABLE_TASKS": "0", "CLAUDE_CODE_ENABLE_TODO_TOOLS": "1"}),
            ):
                if isinstance(message, AssistantMessage):
                    for block in message.content:
                        if isinstance(block, ToolUseBlock) and block.name == "TodoWrite":
                            self.todos = block.input["todos"]
                            self.display_progress()
        except Exception as error:
            # A single-shot query() raises after yielding an error result,
            # such as when the max_turns limit is hit.
            print(f"Session ended with an error: {error}")


# Usage
async def main():
    tracker = TodoTracker()
    await tracker.track_query("Build a complete authentication system with todos")


asyncio.run(main())

TodoWrite 中单个 todo 项的字段

根据示例代码可归纳出 todos 数组中每一项的字段结构:

字段名类型默认值说明
contentstring任务的静态描述文本
statusstring(\"pending\" | \"in_progress\" | \"completed\")任务当前状态
activeFormstring任务处于 in_progress 时展示的进行时描述文本

迁移到 Task 工具

Task 工具把单一的 TodoWrite 调用拆分为:每新增一项调用一次 TaskCreate,每次状态变化调用一次 TaskUpdate;此外还提供 TaskListTaskGet 供模型读回当前列表。监控代码仍然是检查助手消息流中的 tool_use 块,但需要维护一个以任务 id 为键的映射,而不是每次调用都整体替换列表。

使用 TodoWrite使用 Task 工具
一次工具调用重写整个 todos 数组TaskCreate 新增一项,TaskUpdatetaskId 局部更新一项
匹配 block.name === \"TodoWrite\"匹配 block.name === \"TaskCreate\"\"TaskUpdate\"
每项结构:{ content, status, activeForm }TaskCreate 输入:{ subject, description, activeForm?, metadata? }TaskUpdate 输入:{ taskId, status?, subject?, description?, activeForm?, addBlocks?, addBlockedBy?, owner?, metadata? }status\"pending\"\"in_progress\"\"completed\";设置 status: \"deleted\" 即可删除
直接渲染 block.input.todos需跨调用累积各项,或从 TaskList 工具结果中读取快照

分配的任务 id 不在 TaskCreate 的输入(input)中,而是出现在对应的 tool_result 里,格式为 { task: { id, subject } },因此需要从 result 块中捕获它来作为映射的 key。

TaskCreate 输入字段

字段名类型默认值说明
subjectstring任务标题/主题
descriptionstring任务描述
activeFormstring(可选)任务处于 in_progress 时展示的进行时描述文本
metadataobject(可选)附加元数据

TaskUpdate 输入字段

字段名类型默认值说明
taskIdstring要更新的任务 id
statusstring(可选,\"pending\" | \"in_progress\" | \"completed\" | \"deleted\"新状态;设为 \"deleted\" 表示删除该任务
subjectstring(可选)更新后的标题
descriptionstring(可选)更新后的描述
activeFormstring(可选)更新后的进行时描述文本
addBlocks(可选)添加该任务所阻塞的其他任务
addBlockedBy(可选)添加阻塞该任务的其他任务
owner(可选)任务所有者
metadataobject(可选)附加元数据

原文未给出 addBlocksaddBlockedByowner 的具体类型,此处按原文留空,不做推测。

以流方式传入的 tool_use 输入是模型直接产出的原始字段形态。Claude Code 会在执行前修正部分「接近但不完全正确」的键名,例如把 idtask_id 映射为 taskId、把 active_form 映射为 activeForm,但这一修正不会体现在消息流中。因此读取 TaskUpdate 输入字段时应像下方示例一样做防御性处理,而不要假设一定存在规范字段名。

迁移示例:最小改动版本

下面示例展示了在「监控 Todo 变化」循环基础上的最小改动。示例未设置 CLAUDE_CODE_ENABLE_TASKS(因为 Task 工具是默认项),只设置了 CLAUDE_CODE_ENABLE_TODO_TOOLS=1 这一开关,用于在默认不提供该工具的模型上开启。示例只读取 tool_use 的输入,不从 tool_result 块中捕获 id。若要渲染完整列表,可监听流中的 TaskList 工具结果,或将 TaskCreate 的结果与 TaskUpdate 的输入累积进一个映射。

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

try {
  for await (const message of query({
    prompt: "Optimize my React app performance and track progress with todos",
    // Keeps the Task tools on models where Claude Code otherwise doesn't provide them.
    options: { maxTurns: 15, 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) {
  // A single-shot query() throws after yielding an error result.
  console.log(`Session ended with an error: ${error}`);
}
import asyncio

from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, ToolUseBlock

async def main():
    try:
        async for message in query(
            prompt="Optimize my React app performance and track progress with todos",
            # Keeps the Task tools on models where Claude Code otherwise doesn't provide them.
            options=ClaudeAgentOptions(max_turns=15, env={"CLAUDE_CODE_ENABLE_TODO_TOOLS": "1"}),
        ):
            if not isinstance(message, AssistantMessage):
                continue
            for block in message.content:
                if not isinstance(block, ToolUseBlock):
                    continue
                if block.name == "TaskCreate":
                    print(f"+ {block.input['subject']}")
                elif block.name == "TaskUpdate" and block.input.get("status"):
                    task_id = (
                        block.input.get("taskId")
                        or block.input.get("id")
                        or block.input.get("task_id")
                    )
                    if task_id:
                        print(f"  {task_id} -> {block.input['status']}")
    except Exception as error:
        # A single-shot query() raises after yielding an error result.
        print(f"Session ended with an error: {error}")


asyncio.run(main())

相关文档