Bỏ qua để đến nội dung

Theo dõi Todo List

Bài viết được dịch tự động từ bài viết gốc, chưa được kiểm tra lại bởi con người. Chỉ những bài viết có dấu tick xanh cạnh tiêu đề là đã được kiểm tra.

Todo tracking cung cấp cách có cấu trúc để quản lý tác vụ và hiển thị tiến độ cho người dùng. Claude Agent SDK có sẵn chức năng todo tích hợp giúp tổ chức workflow phức tạp và giữ người dùng nắm được tiến độ tác vụ.

Claude di chuyển mỗi todo qua một vòng đời có thể dự đoán:

  1. Created: Claude thêm todo ở trạng thái pending khi xác định một tác vụ
  2. Activated: Claude đặt todo thành in_progress khi bắt đầu làm
  3. Completed: Claude đánh dấu completed khi tác vụ hoàn thành thành công
  4. Removed: Claude xoá todo không còn cần bằng cách đặt status: "deleted" trong lệnh gọi TaskUpdate

Claude tạo todo cho hầu hết công việc nhiều bước, như:

  • Tác vụ nhiều bước phức tạp cần 3 hành động riêng biệt trở lên
  • Danh sách tác vụ do người dùng cung cấp khi nhiều mục được nhắc tới
  • Thao tác không tầm thường hưởng lợi từ việc theo dõi tiến độ
  • Yêu cầu tường minh khi người dùng hỏi về tổ chức todo

Claude có thể bỏ qua todo cho yêu cầu rất ngắn hoặc một bước.

Trước khi chạy các ví dụ này, cài Claude Agent SDK theo quickstart.

Mỗi ví dụ chạy tới khi agent hoàn tất và trả về message kết quả cuối. Nếu phiên chạm giới hạn lượt trước, message kết quả đó có subtype error_max_turns. Kiểm tra subtype để phát hiện kết thúc kiểu này.

Các ví dụ này dùng lệnh gọi query() một lần. Sau khi trả về kết quả error_max_turns, query() raise một lỗi kèm Reached maximum number of turns. Mỗi ví dụ bọc vòng lặp trong try block để thoát gọn khi điều đó xảy ra.

TypeScript
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",
// Bật lại TodoWrite, thứ ví dụ này giám sát. Không có nó, SDK dùng
// Task tools thay vào đó và các tool_use block này sẽ không xuất hiện.
options: { maxTurns: 15, env: { ...process.env, CLAUDE_CODE_ENABLE_TASKS: "0" } }
})) {
// Cập nhật todo phản ánh trong 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) {
// query() một lần throw sau khi trả kết quả lỗi,
// ví dụ khi chạm giới hạn maxTurns.
console.log(`Session ended with an error: ${error}`);
}
Python
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",
# Bật lại TodoWrite, thứ ví dụ này giám sát. Không có nó, SDK dùng
# Task tools thay vào đó và các tool_use block này sẽ không xuất hiện.
options=ClaudeAgentOptions(max_turns=15, env={"CLAUDE_CODE_ENABLE_TASKS": "0"}),
):
# Cập nhật todo phản ánh trong 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:
# query() một lần raise sau khi trả kết quả lỗi,
# ví dụ khi chạm giới hạn max_turns.
print(f"Session ended with an error: {error}")
asyncio.run(main())
TypeScript
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,
// Bật lại TodoWrite, thứ tracker này theo dõi.
options: { maxTurns: 20, env: { ...process.env, CLAUDE_CODE_ENABLE_TASKS: "0" } }
})) {
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) {
// query() một lần throw sau khi trả kết quả lỗi,
// ví dụ khi chạm giới hạn maxTurns.
console.log(`Session ended with an error: ${error}`);
}
}
}
// Cách dùng
const tracker = new TodoTracker();
await tracker.trackQuery("Build a complete authentication system with todos");
Python
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,
# Bật lại TodoWrite, thứ tracker này theo dõi.
options=ClaudeAgentOptions(max_turns=20, env={"CLAUDE_CODE_ENABLE_TASKS": "0"}),
):
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:
# query() một lần raise sau khi trả kết quả lỗi,
# ví dụ khi chạm giới hạn max_turns.
print(f"Session ended with an error: {error}")
# Cách dùng
async def main():
tracker = TodoTracker()
await tracker.track_query("Build a complete authentication system with todos")
asyncio.run(main())

Task tools tách một lệnh gọi TodoWrite duy nhất thành TaskCreate cho mỗi mục mới và TaskUpdate cho mỗi thay đổi trạng thái, với TaskListTaskGet để model đọc lại danh sách hiện tại. Code giám sát của bạn vẫn kiểm tra tool_use block trong assistant stream, nhưng duy trì một map theo task ID thay vì thay toàn bộ danh sách mỗi lần gọi. Task tools là mặc định từ TypeScript Agent SDK 0.3.142 và Claude Code v2.1.142 trở đi, nên không cần đổi options.env.

Với TodoWriteVới Task tools
Một lệnh gọi tool ghi lại toàn bộ mảng todosTaskCreate thêm một mục, TaskUpdate patch một mục theo taskId
Khớp block.name === "TodoWrite"Khớp block.name === "TaskCreate" hoặc "TaskUpdate"
Hình dạng mục: { content, status, activeForm }Input TaskCreate: { subject, description, activeForm?, metadata? }. Input TaskUpdate: { taskId, status?, subject?, description?, activeForm?, addBlocks?, addBlockedBy?, owner?, metadata? }. status"pending", "in_progress", hoặc "completed"; đặt status: "deleted" để xoá
Render block.input.todos trực tiếpTích luỹ mục qua các lệnh gọi, hoặc đọc snapshot từ kết quả tool TaskList

Task ID được gán không nằm trong input của TaskCreate. Nó đến trong tool_result khớp dạng { task: { id, subject } }, nên bắt lấy ID từ result block để key cho map của bạn. Ví dụ dưới cho thấy thay đổi tối thiểu với vòng lặp Giám sát thay đổi Todo ở trên. Nó chỉ đọc input của tool_use và bỏ qua việc bắt ID từ tool_result block. Để render một danh sách đầy đủ, theo dõi kết quả tool TaskList trong stream hoặc tích luỹ kết quả TaskCreate và input TaskUpdate vào một map.

Input tool_use được stream là hình dạng thô model đã tạo ra. Claude Code sửa một số tên key gần đúng nhưng sai trước khi thực thi, map id hoặc task_id thành taskIdactive_form thành activeForm, nhưng việc sửa đó không phản ánh trong stream. Đọc field input của TaskUpdate một cách phòng thủ, như các ví dụ dưới đây làm, thay vì giả định tên chuẩn luôn có mặt.

TypeScript
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",
options: { maxTurns: 15 },
})) {
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() một lần throw sau khi trả kết quả lỗi.
console.log(`Session ended with an error: ${error}`);
}
Python
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",
options=ClaudeAgentOptions(max_turns=15),
):
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:
# query() một lần raise sau khi trả kết quả lỗi.
print(f"Session ended with an error: {error}")
asyncio.run(main())