2> Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt2> Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt
3> Use this file to discover all available pages before exploring further.3> Use this file to discover all available pages before exploring further.
4 4
5# 할일 목록5# 할일 추적
6 6
7> Claude Agent SDK를 사용하여 할일을 추적하고 표시하여 체계적인 작업 관리를 수행합니다7> Agent SDK 세션에서 할일을 추적하고 구조화된 도구 호출에서 Claude의 진행 상황을 애플리케이션에 렌더링합니다
8 8
9할일 추적은 작업을 관리하고 사용자에게 진행 상황을 표시하는 구조화된 방법을 제공합니다. Claude Agent SDK에는 복잡한 워크플로우를 구성하고 사용자에게 작업 진행 상황을 알리는 데 도움이 되는 기본 제공 할일 기능이 포함되어 있습니다.9[모델 가용성](#model-availability)에 나열된 모델에서 Claude는 작성된 할일 목록 없이 다단계 작업을 추적하며, Claude Code는 기본적으로 세션에서 [작업 추적 도구](/docs/ko/tools-reference#task-tool-availability)를 제외합니다. 이 페이지의 내용이 없어도 Claude가 해당 모델에서 다단계 작업을 수행할 수 있습니다.
10
11작업 추적 도구가 있는 세션에서 Claude는 작성된 할일 목록을 유지하며 작업하면서 각 항목의 상태를 업데이트합니다. 메시지 스트림에서 각 변경 사항을 구조화된 도구 호출로 볼 수 있습니다. 애플리케이션이 작업 활동을 기록하거나 자체 진행 상황 표시를 렌더링하기 위해 해당 도구 호출을 읽을 때만 세션을 옵트인합니다.
12
13<h2 id="model-availability">
14 모델 가용성
15</h2>
10 16
11<Note>17<Note>
12 TypeScript Agent SDK 0.3.142 및 Claude Code v2.1.142부터 세션은 `TodoWrite` 대신 구조화된 Task 도구인 `TaskCreate`, `TaskUpdate`, `TaskGet`, `TaskList`를 사용합니다. Python SDK는 Python 패키지 버전이 아닌 실행하는 Claude Code CLI에서 이 변경 사항을 가져옵니다. pip 패키지 내에 번들된 CLI 또는 `cli_path`로 지정한 CLI가 v2.1.142 이상이면 전환이 적용됩니다. 모니터링 코드 변경 방법은 [Task 도구로 마이그레이션](#migrate-to-task-tools)을 참조하십시오. 이 페이지의 예제는 아직 마이그레이션하지 않은 세션에 대해 `TodoWrite`를 계속 표시하기 위해 `CLAUDE_CODE_ENABLE_TASKS=0`을 설정합니다.18 TypeScript Agent SDK 0.3.233 이상 또는 Python Agent SDK 0.2.139 이상에서 다음 제한이 적용됩니다.
19
20 The following tools aren't available on Opus 4.8, Sonnet 5, Fable 5, Mythos 5, or later versions of those families unless you opt in:
21
22 * `TodoWrite`
23 * `TaskCreate`
24 * `TaskGet`
25 * `TaskUpdate`
26 * `TaskList`
27
28 On other models, Claude Code provides the Task tools by default and `TodoWrite` only when you set `CLAUDE_CODE_ENABLE_TASKS=0`.
13</Note>29</Note>
14 30
15<h3 id="todo-lifecycle">31나열된 모델에서 세션을 옵트인하지 않으면 메시지 스트림에서 이러한 도구에 대한 `tool_use` 블록을 볼 수 없습니다. Agent SDK는 번들된 Claude Code 바이너리를 통해 이러한 기본값을 적용합니다. `pathToClaudeCodeExecutable`(TypeScript) 또는 `cli_path`(Python)를 자신의 Claude Code 설치로 지정하면 해당 설치가 제공하는 도구를 자체 기본값에 따라 얻습니다. 실행 중인 세션에서 정확한 집합을 보려면 [사용 가능한 도구 확인](/docs/ko/tools-reference#check-which-tools-are-available)을 참조하십시오. 세션을 옵트인하려면 다음 중 하나를 수행하십시오:
32
33* [`allowedTools`](/docs/ko/agent-sdk/permissions#allow-and-deny-rules)(TypeScript) 또는 `allowed_tools`(Python) 옵션에서 도구 중 하나의 이름을 지정합니다
34* `tools` 옵션에 도구를 나열합니다. 이는 세션의 기본 제공 도구를 이름이 지정된 도구로 제한합니다. 사용하는 다른 기본 제공 도구와 함께 원하는 도구를 포함합니다
35* 이 페이지의 예제처럼 `env` 옵션에서 `CLAUDE_CODE_ENABLE_TODO_TOOLS=1`을 설정합니다. TypeScript에서 `env`는 서브프로세스 환경을 대체하므로 `...process.env`를 전개하여 상속된 변수를 유지합니다. Python에서 `env`는 상속된 환경 위에 병합됩니다
36
37<h2 id="todo-lifecycle">
16 할일 생명주기38 할일 생명주기
17</h3>39</h2>
18 40
19할일은 예측 가능한 생명주기를 따릅니다:41Claude는 각 할일을 예측 가능한 생명주기를 통해 이동합니다:
20 42
211. **생성됨** - 작업이 식별될 때 `pending`으로 생성됨431. **생성됨**: Claude가 작업을 식별할 때 할일을 `pending`으로 추가합니다
222. **활성화됨** - 작업이 시작될 때 `in_progress`로 활성화됨442. **활성화됨**: Claude가 작업을 시작할 때 할일을 `in_progress`로 설정합니다
233. **완료됨** - 작업이 성공적으로 완료될 때453. **완료됨**: Claude가 작업이 성공적으로 완료되었을 때 표시합니다
244. **제거됨** - 그룹의 모든 작업이 완료될 때464. **제거됨**: Claude가 더 이상 필요하지 않은 할일을 `TaskUpdate` 호출에서 `status: "deleted"`를 설정하여 삭제합니다
25 47
26<h3 id="when-todos-are-used">48<h2 id="when-claude-creates-todos">
27 할일이 사용되는 경우49 Claude가 할일을 생성하는 경우
28</h3>50</h2>
29 51
30SDK는 대부분의 다단계 작업에 대해 할일을 생성합니다. 예를 들면:52[작업 추적 도구가 있는 세션](#model-availability)에서 Claude는 다음과 같은 대부분의 다단계 작업에 대해 할일을 생성합니다:
31 53
32* **복잡한 다단계 작업** - 3개 이상의 서로 다른 작업이 필요한 경우54* **복잡한 다단계 작업** - 3개 이상의 서로 다른 작업이 필요한 경우
33* **사용자 제공 작업 목록** - 여러 항목이 언급될 때55* **사용자 제공 작업 목록** - 여러 항목이 언급될 때
34* **중요한 작업** - 진행 상황 추적이 도움이 되는 경우56* **더 긴 작업** - 진행 상황 추적이 도움이 되는 경우
35* **명시적 요청** - 사용자가 할일 구성을 요청할 때57* **명시적 요청** - 사용자가 할일 구성을 요청할 때
36 58
37매우 짧거나 단일 단계의 요청에 대해서는 할일을 건너뛸 수 있습니다.59Claude는 매우 짧거나 단일 단계의 요청에 대해 할일을 건너뛸 수 있습니다.
38 60
39<h2 id="examples">61<h2 id="examples">
40 예제62 예제
41</h2>63</h2>
42 64
43이 예제들을 실행하기 전에 [빠른 시작](/docs/ko/agent-sdk/quickstart)을 따라 Claude Agent SDK를 설치하십시오.65이 예제들을 실행하기 전에 [빠른 시작](/docs/ko/agent-sdk/quickstart)을 따라 Claude Agent SDK를 설치하십시오. 이 페이지의 모든 예제는 동일한 권한 설정 및 종료 동작을 공유합니다:
44
45각 예제는 에이전트가 완료될 때까지 실행되고 최종 결과 메시지를 생성합니다. 세션이 먼저 턴 제한에 도달하면 해당 결과 메시지는 `error_max_turns` 서브타입을 가집니다. 해당 종료를 감지하려면 `subtype`을 확인하십시오.
46 66
47이 예제들은 단일 `query()` 호출을 사용합니다. `error_max_turns` 결과를 생성한 후 `query()`는 `Reached maximum number of turns`를 포함하는 오류를 발생시킵니다. 각 예제는 이것이 발생할 때 깔끔하게 종료하기 위해 루프를 try 블록으로 래핑합니다.67* **권한 모드**: 예제 프롬프트는 Claude에게 프로젝트에서 실제 작업을 수행하도록 요청하므로 각 예제는 `permissionMode: "acceptEdits"`(TypeScript) 또는 `permission_mode="acceptEdits"`(Python)를 설정하여 작업이 생성하는 파일 편집을 자동 승인합니다. [권한 모드](/docs/ko/agent-sdk/permissions#permission-modes)에서 대안을 참조하십시오.
68* **턴 제한**: 각 예제는 에이전트가 완료되고 최종 결과 메시지를 생성할 때까지 실행됩니다. 세션이 먼저 턴 제한에 도달하면 해당 결과 메시지는 `error_max_turns` 서브타입을 가집니다. 해당 종료를 감지하려면 `subtype`을 확인하십시오.
69* **오류 처리**: 이 예제들은 단일 `query()` 호출을 사용합니다. `error_max_turns` 결과를 생성한 후 `query()`는 `Reached maximum number of turns`를 포함하는 오류를 발생시킵니다. 각 예제는 이것이 발생할 때 깔끔하게 종료하기 위해 루프를 try 블록으로 래핑합니다. 결과 서브타입에 대해서는 [결과 처리](/docs/ko/agent-sdk/agent-loop#handle-the-result)를 참조하십시오.
48 70
49결과 서브타입에 대해서는 [결과 처리](/docs/ko/agent-sdk/agent-loop#handle-the-result)를 참조하십시오.71<Note>
72 작업 시스템 메시지인 [`SDKTaskNotificationMessage`](/docs/ko/agent-sdk/typescript#sdktasknotificationmessage)(TypeScript) 또는 [`TaskNotificationMessage`](/docs/ko/agent-sdk/python#tasknotificationmessage)(Python)는 백그라운드 명령 및 서브에이전트와 같은 백그라운드 작업을 보고합니다. 메시지 스트림에서 할일 활동을 어시스턴트 메시지의 `tool_use` 블록으로 볼 수 있습니다.
73</Note>
50 74
51<h3 id="monitoring-todo-changes">75<h3 id="monitor-todo-changes">
52 할일 변경 모니터링76 할일 변경 모니터링
53</h3>77</h3>
54 78
79다음 예제는 어시스턴트 스트림에서 `TaskCreate` 및 `TaskUpdate` `tool_use` 블록을 감시하고 각 새 작업의 주제와 함께 `+` 줄을 인쇄하며 각 상태 변경의 작업 ID와 새 상태와 함께 업데이트 줄을 인쇄합니다. 렌더링된 표시 대신 작업 활동의 로그를 원할 때 이 형태를 사용하십시오. `+` 줄에는 할당된 ID가 포함되지 않으므로 이 로그는 업데이트를 생성과 다시 일치시킬 수 없습니다. 해당 상관관계를 유지하려면 [실시간 진행 상황 표시](#display-progress-in-real-time)처럼 ID를 캡처하십시오.
80
81스트리밍된 `tool_use` 입력은 모델이 내보낸 원본 형태입니다. Claude Code는 실행 전에 일부 거의 올바른 키 이름을 수정하여 `id` 또는 `task_id`를 `taskId`로, `active_form`을 `activeForm`으로 매핑하지만, 이 수정은 스트림에 반영되지 않습니다. 이 페이지의 두 예제처럼 `TaskUpdate` 입력 필드를 방어적으로 읽으십시오. 정규 이름이 항상 존재한다고 가정하지 마십시오.
82
55<CodeGroup>83<CodeGroup>
56 ```typescript TypeScript theme={null}84 ```typescript TypeScript theme={null}
57 import { query } from "@anthropic-ai/claude-agent-sdk";85 import { query } from "@anthropic-ai/claude-agent-sdk";
58 86
59 try {87 try {
60 for await (const message of query({88 for await (const message of query({
61 prompt: "Optimize my React app performance and track progress with todos",89 prompt: "Create a static website with a home page, an about page, and a shared stylesheet, and track progress with todos",
62 // Re-enable TodoWrite, which this example monitors. Without it, the SDK uses90 // Keeps the Task tools on models where Claude Code otherwise doesn't provide them.
63 // Task tools instead and these tool_use blocks never appear.91 options: { maxTurns: 15, permissionMode: "acceptEdits", env: { ...process.env, CLAUDE_CODE_ENABLE_TODO_TOOLS: "1" } },
64 options: { maxTurns: 15, env: { ...process.env, CLAUDE_CODE_ENABLE_TASKS: "0" } }
65 })) {92 })) {
66 // Todo updates are reflected in the message stream93 if (message.type !== "assistant") continue;
67 if (message.type === "assistant") {
68 for (const block of message.message.content) {94 for (const block of message.message.content) {
69 if (block.type === "tool_use" && block.name === "TodoWrite") {95 if (block.type !== "tool_use") continue;
70 const todos = block.input.todos;96 if (block.name === "TaskCreate") {
71 97 const input = block.input as { subject: string };
72 console.log("Todo Status Update:");98 console.log(`+ ${input.subject}`);
73 todos.forEach((todo, index) => {99 } else if (block.name === "TaskUpdate") {
74 const status =100 const input = block.input as {
75 todo.status === "completed" ? "✅" : todo.status === "in_progress" ? "🔧" : "❌";101 taskId?: string;
76 console.log(`${index + 1}. ${status} ${todo.content}`);102 id?: string;
77 });103 task_id?: string;
78 }104 status?: string;
105 };
106 const taskId = input.taskId ?? input.id ?? input.task_id;
107 if (taskId && input.status) console.log(` ${taskId} -> ${input.status}`);
79 }108 }
80 }109 }
81 }110 }
82 } catch (error) {111 } catch (error) {
83 // A single-shot query() throws after yielding an error result,112 // A single-shot query() throws after yielding an error result.
84 // such as when the maxTurns limit is hit.
85 console.log(`Session ended with an error: ${error}`);113 console.log(`Session ended with an error: ${error}`);
86 }114 }
87 ```115 ```
95 async def main():122 async def main():
96 try:123 try:
97 async for message in query(124 async for message in query(
98 prompt="Optimize my React app performance and track progress with todos",125 prompt="Create a static website with a home page, an about page, and a shared stylesheet, and track progress with todos",
99 # Re-enable TodoWrite, which this example monitors. Without it, the SDK uses126 # Keeps the Task tools on models where Claude Code otherwise doesn't provide them.
100 # Task tools instead and these tool_use blocks never appear.127 options=ClaudeAgentOptions(max_turns=15, permission_mode="acceptEdits", env={"CLAUDE_CODE_ENABLE_TODO_TOOLS": "1"}),
101 options=ClaudeAgentOptions(max_turns=15, env={"CLAUDE_CODE_ENABLE_TASKS": "0"}),
102 ):128 ):
103 # Todo updates are reflected in the message stream129 if not isinstance(message, AssistantMessage):
104 if isinstance(message, AssistantMessage):130 continue
105 for block in message.content:131 for block in message.content:
106 if isinstance(block, ToolUseBlock) and block.name == "TodoWrite":132 if not isinstance(block, ToolUseBlock):
107 todos = block.input["todos"]133 continue
108 134 if block.name == "TaskCreate":
109 print("Todo Status Update:")135 print(f"+ {block.input.get('subject', '')}")
110 for i, todo in enumerate(todos):136 elif block.name == "TaskUpdate" and block.input.get("status"):
111 status = (137 task_id = (
112 "✅"138 block.input.get("taskId")
113 if todo["status"] == "completed"139 or block.input.get("id")
114 else "🔧"140 or block.input.get("task_id")
115 if todo["status"] == "in_progress"
116 else "❌"
117 )141 )
118 print(f"{i + 1}. {status} {todo['content']}")142 if task_id:
143 print(f" {task_id} -> {block.input['status']}")
119 except Exception as error:144 except Exception as error:
120 # A single-shot query() raises after yielding an error result,145 # A single-shot query() raises after yielding an error result.
121 # such as when the max_turns limit is hit.
122 print(f"Session ended with an error: {error}")146 print(f"Session ended with an error: {error}")
123 147
124 148
126 ```150 ```
127</CodeGroup>151</CodeGroup>
128 152
129<h3 id="real-time-progress-display">153<h3 id="display-progress-in-real-time">
130 실시간 진행 상황 표시154 실시간 진행 상황 표시
131</h3>155</h3>
132 156
157다음 예제는 어시스턴트 스트림에서 `TaskCreate` 및 `TaskUpdate` `tool_use` 블록을 감시하고 `TaskTracker` 클래스에서 작업 ID로 키가 지정된 작업 맵을 유지하며 모든 변경에서 진행 상황 요약을 다시 렌더링합니다. 요약은 완료되고 진행 중인 작업을 계산하고 각 활성 항목의 `activeForm` 레이블을 `subject` 대신 표시합니다. 애플리케이션이 각 이벤트를 기록하는 대신 진행 상황 표시를 유지할 때 이 형태를 사용하십시오.
158
159할당된 작업 ID는 `TaskCreate` 입력에 없습니다. Claude Code는 각 도구의 구조화된 출력을 `tool_result` 블록을 포함하는 사용자 메시지에서 `tool_use_result` 필드로 전달합니다. `TaskCreate`의 경우 해당 객체는 TypeScript의 [도구 출력 유형](/docs/ko/agent-sdk/typescript#tool-output-types) 아래 `TaskCreateOutput`으로 문서화되며, Python에서 필드는 동일한 형태의 일반 dict입니다. 추적기는 `tool_use_id`로 각 `tool_result` 블록을 해당 `tool_use` 호출과 쌍을 이루고 쌍을 이룬 메시지의 `tool_use_result`에서 `task.id`를 읽습니다. Claude는 `TaskList`로 목록을 다시 읽을 수 있고 `TaskGet`으로 한 작업의 전체 세부 정보를 읽을 수 있습니다.
160
133<CodeGroup>161<CodeGroup>
134 ```typescript TypeScript theme={null}162 ```typescript TypeScript theme={null}
135 import { query } from "@anthropic-ai/claude-agent-sdk";163 import { query } from "@anthropic-ai/claude-agent-sdk";
136 164
137 class TodoTracker {165 type Task = { subject: string; activeForm?: string; status: string };
138 private todos: any[] = [];166
167 class TaskTracker {
168 private tasks = new Map<string, Task>();
169 private pendingCreates = new Map<string, { subject: string; activeForm?: string }>();
139 170
140 displayProgress() {171 displayProgress() {
141 if (this.todos.length === 0) return;172 if (this.tasks.size === 0) {
173 console.log("\nProgress: no open tasks\n");
174 return;
175 }
142 176
143 const completed = this.todos.filter((t) => t.status === "completed").length;177 const items = [...this.tasks.values()];
144 const inProgress = this.todos.filter((t) => t.status === "in_progress").length;178 const completed = items.filter((t) => t.status === "completed").length;
145 const total = this.todos.length;179 const inProgress = items.filter((t) => t.status === "in_progress").length;
146 180
147 console.log(`\nProgress: ${completed}/${total} completed`);181 console.log(`\nProgress: ${completed}/${this.tasks.size} completed`);
148 console.log(`Currently working on: ${inProgress} task(s)\n`);182 console.log(`Currently working on: ${inProgress} task(s)\n`);
149 183
150 this.todos.forEach((todo, index) => {184 for (const [id, task] of this.tasks) {
151 const icon =185 const icon =
152 todo.status === "completed" ? "✅" : todo.status === "in_progress" ? "🔧" : "❌";186 task.status === "completed" ? "✅" : task.status === "in_progress" ? "🔧" : "❌";
153 const text = todo.status === "in_progress" ? todo.activeForm : todo.content;187 const text = task.status === "in_progress" && task.activeForm ? task.activeForm : task.subject;
154 console.log(`${index + 1}. ${icon} ${text}`);188 console.log(`${id}. ${icon} ${text}`);
189 }
190 }
191
192 handleToolUse(block: { id: string; name: string; input: unknown }) {
193 if (block.name === "TaskCreate") {
194 const input = block.input as { subject: string; activeForm?: string; active_form?: string };
195 this.pendingCreates.set(block.id, {
196 subject: input.subject,
197 activeForm: input.activeForm ?? input.active_form,
155 });198 });
199 } else if (block.name === "TaskUpdate") {
200 const input = block.input as {
201 taskId?: string;
202 id?: string;
203 task_id?: string;
204 status?: string;
205 activeForm?: string;
206 active_form?: string;
207 };
208 const taskId = input.taskId ?? input.id ?? input.task_id;
209 if (!taskId) return;
210 if (input.status === "deleted") {
211 this.tasks.delete(taskId);
212 this.displayProgress();
213 return;
214 }
215 const task = this.tasks.get(taskId);
216 if (!task) return;
217 if (input.status) task.status = input.status;
218 const active = input.activeForm ?? input.active_form;
219 if (active) task.activeForm = active;
220 this.displayProgress();
221 }
222 }
223
224 handleToolResult(block: { tool_use_id: string; is_error?: boolean }, result: unknown) {
225 const create = this.pendingCreates.get(block.tool_use_id);
226 if (!create) return;
227 this.pendingCreates.delete(block.tool_use_id);
228 if (block.is_error) return;
229 // The result's user message carries the tool's structured output as
230 // tool_use_result; for TaskCreate that's TaskCreateOutput,
231 // { task: { id, subject } }.
232 const out = result as { task?: { id: string } };
233 if (!out?.task?.id) return;
234 this.tasks.set(out.task.id, { ...create, status: "pending" });
235 this.displayProgress();
156 }236 }
157 237
158 async trackQuery(prompt: string) {238 async trackQuery(prompt: string) {
159 try {239 try {
160 for await (const message of query({240 for await (const message of query({
161 prompt,241 prompt,
162 // Re-enable TodoWrite, which this tracker watches for.242 options: { maxTurns: 20, permissionMode: "acceptEdits", env: { ...process.env, CLAUDE_CODE_ENABLE_TODO_TOOLS: "1" } },
163 options: { maxTurns: 20, env: { ...process.env, CLAUDE_CODE_ENABLE_TASKS: "0" } }
164 })) {243 })) {
165 if (message.type === "assistant") {244 if (message.type === "assistant") {
166 for (const block of message.message.content) {245 for (const block of message.message.content) {
167 if (block.type === "tool_use" && block.name === "TodoWrite") {246 if (block.type === "tool_use") this.handleToolUse(block);
168 this.todos = block.input.todos;
169 this.displayProgress();
170 }247 }
171 }248 }
249 if (message.type === "user" && Array.isArray(message.message.content)) {
250 for (const block of message.message.content) {
251 if (block.type === "tool_result") this.handleToolResult(block, message.tool_use_result);
252 }
172 }253 }
173 }254 }
174 } catch (error) {255 } catch (error) {
180 }261 }
181 262
182 // Usage263 // Usage
183 const tracker = new TodoTracker();264 const tracker = new TaskTracker();
184 await tracker.trackQuery("Build a complete authentication system with todos");265 await tracker.trackQuery("Build a complete authentication system with todos");
185 ```266 ```
186 267
187 ```python Python theme={null}268 ```python Python theme={null}
188 import asyncio269 import asyncio
189 270
190 from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, ToolUseBlock271 from claude_agent_sdk import (
191 from typing import List, Dict272 query,
273 ClaudeAgentOptions,
274 AssistantMessage,
275 UserMessage,
276 ToolUseBlock,
277 ToolResultBlock,
278 )
192 279
193 280
194 class TodoTracker:281 class TaskTracker:
195 def __init__(self):282 def __init__(self):
196 self.todos: List[Dict] = []283 self.tasks: dict[str, dict] = {}
284 self.pending_creates: dict[str, dict] = {}
197 285
198 def display_progress(self):286 def display_progress(self):
199 if not self.todos:287 if not self.tasks:
288 print("\nProgress: no open tasks\n")
200 return289 return
201 290
202 completed = len([t for t in self.todos if t["status"] == "completed"])291 completed = len([t for t in self.tasks.values() if t["status"] == "completed"])
203 in_progress = len([t for t in self.todos if t["status"] == "in_progress"])292 in_progress = len([t for t in self.tasks.values() if t["status"] == "in_progress"])
204 total = len(self.todos)
205 293
206 print(f"\nProgress: {completed}/{total} completed")294 print(f"\nProgress: {completed}/{len(self.tasks)} completed")
207 print(f"Currently working on: {in_progress} task(s)\n")295 print(f"Currently working on: {in_progress} task(s)\n")
208 296
209 for i, todo in enumerate(self.todos):297 for task_id, task in self.tasks.items():
210 icon = (298 icon = (
211 "✅"299 "✅"
212 if todo["status"] == "completed"300 if task["status"] == "completed"
213 else "🔧"301 else "🔧"
214 if todo["status"] == "in_progress"302 if task["status"] == "in_progress"
215 else "❌"303 else "❌"
216 )304 )
217 text = (305 text = (
218 todo["activeForm"]306 task["activeForm"]
219 if todo["status"] == "in_progress"307 if task["status"] == "in_progress" and task.get("activeForm")
220 else todo["content"]308 else task["subject"]
309 )
310 print(f"{task_id}. {icon} {text}")
311
312 def handle_tool_use(self, block: ToolUseBlock):
313 if block.name == "TaskCreate":
314 self.pending_creates[block.id] = {
315 "subject": block.input.get("subject", ""),
316 "activeForm": block.input.get("activeForm") or block.input.get("active_form"),
317 }
318 elif block.name == "TaskUpdate":
319 task_id = (
320 block.input.get("taskId")
321 or block.input.get("id")
322 or block.input.get("task_id")
221 )323 )
222 print(f"{i + 1}. {icon} {text}")324 if not task_id:
325 return
326 if block.input.get("status") == "deleted":
327 self.tasks.pop(task_id, None)
328 self.display_progress()
329 return
330 task = self.tasks.get(task_id)
331 if not task:
332 return
333 if block.input.get("status"):
334 task["status"] = block.input["status"]
335 active = block.input.get("activeForm") or block.input.get("active_form")
336 if active:
337 task["activeForm"] = active
338 self.display_progress()
339
340 def handle_tool_result(self, block: ToolResultBlock, tool_use_result):
341 create = self.pending_creates.pop(block.tool_use_id, None)
342 if create is None or block.is_error:
343 return
344 # The result's user message carries the tool's structured output as
345 # tool_use_result; for TaskCreate that's {"task": {"id": ..., "subject": ...}}.
346 task = (tool_use_result or {}).get("task") or {}
347 if not task.get("id"):
348 return
349 self.tasks[task["id"]] = {**create, "status": "pending"}
350 self.display_progress()
223 351
224 async def track_query(self, prompt: str):352 async def track_query(self, prompt: str):
225 try:353 try:
226 async for message in query(354 async for message in query(
227 prompt=prompt,355 prompt=prompt,
228 # Re-enable TodoWrite, which this tracker watches for.356 options=ClaudeAgentOptions(
229 options=ClaudeAgentOptions(max_turns=20, env={"CLAUDE_CODE_ENABLE_TASKS": "0"}),357 max_turns=20,
358 permission_mode="acceptEdits",
359 env={"CLAUDE_CODE_ENABLE_TODO_TOOLS": "1"},
360 ),
230 ):361 ):
231 if isinstance(message, AssistantMessage):362 if isinstance(message, AssistantMessage):
232 for block in message.content:363 for block in message.content:
233 if isinstance(block, ToolUseBlock) and block.name == "TodoWrite":364 if isinstance(block, ToolUseBlock):
234 self.todos = block.input["todos"]365 self.handle_tool_use(block)
235 self.display_progress()366 if isinstance(message, UserMessage) and isinstance(message.content, list):
367 for block in message.content:
368 if isinstance(block, ToolResultBlock):
369 self.handle_tool_result(block, message.tool_use_result)
236 except Exception as error:370 except Exception as error:
237 # A single-shot query() raises after yielding an error result,371 # A single-shot query() raises after yielding an error result,
238 # such as when the max_turns limit is hit.372 # such as when the max_turns limit is hit.
241 375
242 # Usage376 # Usage
243 async def main():377 async def main():
244 tracker = TodoTracker()378 tracker = TaskTracker()
245 await tracker.track_query("Build a complete authentication system with todos")379 await tracker.track_query("Build a complete authentication system with todos")
246 380
247 381
249 ```383 ```
250</CodeGroup>384</CodeGroup>
251 385
252<h2 id="migrate-to-task-tools">
253 Task 도구로 마이그레이션
254</h2>
255
256Task 도구는 단일 `TodoWrite` 호출을 각 새 항목에 대한 `TaskCreate`와 각 상태 변경에 대한 `TaskUpdate`로 분할하며, `TaskList`와 `TaskGet`은 모델이 현재 목록을 다시 읽을 수 있도록 사용 가능합니다. 모니터링 코드는 여전히 어시스턴트 스트림의 `tool_use` 블록을 검사하지만, 모든 호출에서 전체 목록을 바꾸는 대신 작업 ID로 키가 지정된 맵을 유지합니다. Task 도구는 TypeScript Agent SDK 0.3.142 및 Claude Code v2.1.142부터 기본값이므로 `options.env` 변경이 필요하지 않습니다.
257
258| `TodoWrite` 사용 | Task 도구 사용 |
259| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
260| 한 번의 도구 호출로 전체 `todos` 배열을 다시 작성 | `TaskCreate`는 한 항목을 추가하고, `TaskUpdate`는 `taskId`로 한 항목을 패치 |
261| `block.name === "TodoWrite"` 일치 | `block.name === "TaskCreate"` 또는 `"TaskUpdate"` 일치 |
262| 항목 형태: `{ 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"`를 설정 |
263| `block.input.todos`를 직접 렌더링 | 호출 전체에서 항목을 누적하거나, `TaskList` 도구 결과에서 스냅샷을 읽음 |
264
265할당된 작업 ID는 `TaskCreate` 입력에 없습니다. 일치하는 `tool_result`에서 `{ task: { id, subject } }`로 반환되므로, 맵을 키로 지정하기 위해 결과 블록에서 캡처합니다. 다음 예제는 [할일 변경 모니터링](#monitoring-todo-changes) 루프에 대한 최소한의 변경을 보여줍니다. 이는 `tool_use` 입력만 읽고 `tool_result` 블록에서 ID 캡처를 건너뜁니다. 전체 목록을 렌더링하려면 스트림에서 `TaskList` 도구 결과를 감시하거나 `TaskCreate` 결과와 `TaskUpdate` 입력을 맵으로 누적합니다.
266
267스트리밍된 `tool_use` 입력은 모델이 내보낸 원본 형태입니다. Claude Code는 실행 전에 일부 거의 올바른 키 이름을 수정하여 `id` 또는 `task_id`를 `taskId`로, `active_form`을 `activeForm`으로 매핑하지만, 이 수정은 스트림에 반영되지 않습니다. 아래 샘플처럼 `TaskUpdate` 입력 필드를 방어적으로 읽으십시오. 정규 이름이 항상 존재한다고 가정하지 마십시오.
268
269<CodeGroup>
270 ```typescript TypeScript theme={null}
271 import { query } from "@anthropic-ai/claude-agent-sdk";
272
273 try {
274 for await (const message of query({
275 prompt: "Optimize my React app performance and track progress with todos",
276 options: { maxTurns: 15 },
277 })) {
278 if (message.type !== "assistant") continue;
279 for (const block of message.message.content) {
280 if (block.type !== "tool_use") continue;
281 if (block.name === "TaskCreate") {
282 const input = block.input as { subject: string };
283 console.log(`+ ${input.subject}`);
284 } else if (block.name === "TaskUpdate") {
285 const input = block.input as {
286 taskId?: string;
287 id?: string;
288 task_id?: string;
289 status?: string;
290 };
291 const taskId = input.taskId ?? input.id ?? input.task_id;
292 if (taskId && input.status) console.log(` ${taskId} -> ${input.status}`);
293 }
294 }
295 }
296 } catch (error) {
297 // A single-shot query() throws after yielding an error result.
298 console.log(`Session ended with an error: ${error}`);
299 }
300 ```
301
302 ```python Python theme={null}
303 import asyncio
304
305 from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, ToolUseBlock
306
307 async def main():
308 try:
309 async for message in query(
310 prompt="Optimize my React app performance and track progress with todos",
311 options=ClaudeAgentOptions(max_turns=15),
312 ):
313 if not isinstance(message, AssistantMessage):
314 continue
315 for block in message.content:
316 if not isinstance(block, ToolUseBlock):
317 continue
318 if block.name == "TaskCreate":
319 print(f"+ {block.input['subject']}")
320 elif block.name == "TaskUpdate" and block.input.get("status"):
321 task_id = (
322 block.input.get("taskId")
323 or block.input.get("id")
324 or block.input.get("task_id")
325 )
326 if task_id:
327 print(f" {task_id} -> {block.input['status']}")
328 except Exception as error:
329 # A single-shot query() raises after yielding an error result.
330 print(f"Session ended with an error: {error}")
331
332
333 asyncio.run(main())
334 ```
335</CodeGroup>
336
337<h2 id="related-documentation">386<h2 id="related-documentation">
338 관련 문서387 관련 문서
339</h2>388</h2>
340 389
341* [TypeScript SDK 참고](/docs/ko/agent-sdk/typescript)390* [Agent SDK 참고 - TypeScript](/docs/ko/agent-sdk/typescript): TypeScript SDK의 옵션, 유형 및 도구 스키마(작업 도구 입력 및 출력 유형 포함)
342* [Python SDK 참고](/docs/ko/agent-sdk/python)391* [Agent SDK 참고 - Python](/docs/ko/agent-sdk/python): Python SDK의 옵션, 유형 및 도구 문서
343* [스트리밍 vs 단일 모드](/docs/ko/agent-sdk/streaming-vs-single-mode)392* [스트리밍 입력](/docs/ko/agent-sdk/streaming-vs-single-mode): 두 입력 모드 및 이 예제들이 사용하는 단일 샷 호출 대신 스트리밍 입력을 사용할 때
344* [사용자 정의 도구](/docs/ko/agent-sdk/custom-tools)393* [Claude에 사용자 정의 도구 제공](/docs/ko/agent-sdk/custom-tools): SDK의 인프로세스 MCP 서버로 자신의 도구를 정의합니다