本页目录14
- 新版本(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 及更新的模型系列上默认不可用,除非显式开启:
TodoWriteTaskCreateTaskGetTaskUpdateTaskList在其他模型上,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_TASKS | string(\"0\") | 设为 \"0\" 时,旧模型改用 TodoWrite 而非默认的 Task 工具 | |
CLAUDE_CODE_ENABLE_TODO_TOOLS | string(\"1\") | 设为 \"1\" 时,在默认不提供任务追踪工具的新模型上重新开启这些工具 |
Todo 生命周期
Claude 会按以下可预测的流程推进每个 todo 项:
- 创建(Created):Claude 识别到一个任务时,将其加入 todo 列表,状态为
pending - 激活(Activated):Claude 开始处理该任务时,将状态设为
in_progress - 完成(Completed):任务成功完成时,Claude 将其标记为
completed - 移除(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 数组中每一项的字段结构:
| 字段名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
content | string | 任务的静态描述文本 | |
status | string(\"pending\" | \"in_progress\" | \"completed\") | 任务当前状态 | |
activeForm | string | 任务处于 in_progress 时展示的进行时描述文本 |
迁移到 Task 工具
Task 工具把单一的 TodoWrite 调用拆分为:每新增一项调用一次 TaskCreate,每次状态变化调用一次 TaskUpdate;此外还提供 TaskList 和 TaskGet 供模型读回当前列表。监控代码仍然是检查助手消息流中的 tool_use 块,但需要维护一个以任务 id 为键的映射,而不是每次调用都整体替换列表。
使用 TodoWrite | 使用 Task 工具 |
|---|---|
一次工具调用重写整个 todos 数组 | TaskCreate 新增一项,TaskUpdate 按 taskId 局部更新一项 |
匹配 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 输入字段
| 字段名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
subject | string | 任务标题/主题 | |
description | string | 任务描述 | |
activeForm | string(可选) | 任务处于 in_progress 时展示的进行时描述文本 | |
metadata | object(可选) | 附加元数据 |
TaskUpdate 输入字段
| 字段名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
taskId | string | 要更新的任务 id | |
status | string(可选,\"pending\" | \"in_progress\" | \"completed\" | \"deleted\") | 新状态;设为 \"deleted\" 表示删除该任务 | |
subject | string(可选) | 更新后的标题 | |
description | string(可选) | 更新后的描述 | |
activeForm | string(可选) | 更新后的进行时描述文本 | |
addBlocks | (可选) | 添加该任务所阻塞的其他任务 | |
addBlockedBy | (可选) | 添加阻塞该任务的其他任务 | |
owner | (可选) | 任务所有者 | |
metadata | object(可选) | 附加元数据 |
原文未给出
addBlocks、addBlockedBy、owner的具体类型,此处按原文留空,不做推测。
以流方式传入的 tool_use 输入是模型直接产出的原始字段形态。Claude Code 会在执行前修正部分「接近但不完全正确」的键名,例如把 id 或 task_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())