Source Intelligence
Sweep 28 Aug 2026 ยท 00:00Z Build v2.1.250 478 read Stable v2.1.236 Latest v2.1.250 Next v2.1.250 Feeds RSS JSON llms.txt

DisclaimerUnofficial, and not affiliated with Anthropic. Nearly all of this is read straight out of what ships: npm bundles, captured prompts, published docs. Anthropic's own notes go in verbatim, marked as theirs. The rest is my reading, and every entry carries the strings behind it. If one looks wrong, vote it down and say why.

Page history

Track todos

agent-sdk/todo-tracking

4 recorded changes 373 lines First seen Last changed Upstream

History

agent-sdk/todo-tracking Changed · +272 / -247 lines

# Track todos ## Model availability ## Todo lifecycle ## When Claude creates todos ### Monitor todo changes ### Display progress in real time ## Related documentation # Todo Lists ### Model availability ### Todo Lifecycle ### When Todos Are Used ### Monitoring Todo Changes ### Real-time Progress Display ## Migrate to Task tools ## Related Documentation

The two sides of this change are too far apart to line up, so this is the differ's own diff of it.

from line 1
-# Todo Lists
-
-> Track and display todos using the Claude Agent SDK for organized task management
-
-The Claude Agent SDK includes built-in todo functionality that helps organize complex workflows and keep users informed about task progression.
+# Track todos
+
+> Track todos in Agent SDK sessions and render Claude's progress in your application from structured tool calls
+
+On the models listed under [Model availability](#model-availability), Claude tracks multi-step work without a written todo list, and Claude Code leaves the [task-tracking tools](/docs/en/tools-reference#task-tool-availability) out of sessions by default. You don't need anything on this page for Claude to work through multi-step tasks on those models.
+
+In a session that has the task-tracking tools, Claude keeps a written todo list, updating each item's status as it works. You see each change in the message stream as a structured tool call. Opt a session in only when your application reads those tool calls, whether to log task activity or to render its own progress display.
+
+## Model availability
 
 <Note>
   On TypeScript Agent SDK 0.3.233 and later, or Python Agent SDK 0.2.139 and later, 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:
from line 20
   On other models, Claude Code provides the Task tools by default and `TodoWrite` only when you set `CLAUDE_CODE_ENABLE_TASKS=0`.
 </Note>
 
-### Model availability
-
-On the [models that don't get the task-tracking tools](/docs/en/tools-reference#task-tool-availability), you see no `tool_use` blocks for them in the message stream unless you opt in. If you point `cli_path` in Python or `pathToClaudeCodeExecutable` in TypeScript at your own Claude Code install, you get whichever tools that install provides. To get the same tools as on other models, do one of the following:
-
-* Name one of the tools in the [`allowedTools`](/docs/en/agent-sdk/permissions#allow-and-deny-rules) option, `allowed_tools` in Python
+On the listed models, unless you opt a session in, you see no `tool_use` blocks for the tools in the message stream. The Agent SDK applies these defaults through the Claude Code binary that it bundles. If you point `pathToClaudeCodeExecutable` (TypeScript) or `cli_path` (Python) at your own Claude Code install, you get whichever tools that install provides, under its own defaults. To see the exact set in a running session, [check which tools are available](/docs/en/tools-reference#check-which-tools-are-available). To opt a session in, do one of the following:
+
+* Name one of the tools in the [`allowedTools`](/docs/en/agent-sdk/permissions#allow-and-deny-rules) (TypeScript) or `allowed_tools` (Python) option
 * List the tools in the `tools` option, which restricts the session's built-in tools to the ones it names. Include the tools you want alongside the other built-in tools you use
 * Set `CLAUDE_CODE_ENABLE_TODO_TOOLS=1` in the `env` option, as the examples on this page do. In TypeScript, `env` replaces the subprocess environment, so spread `...process.env` to keep inherited variables. In Python, `env` is merged on top of the inherited environment
 
-### Todo Lifecycle
+## Todo lifecycle
 
 Claude moves each todo through a predictable lifecycle:
 
from line 35
 3. **Completed**: Claude marks it completed when the task finishes successfully
 4. **Removed**: Claude deletes a todo it no longer needs by setting `status: "deleted"` in a `TaskUpdate` call
 
-### When Todos Are Used
+## When Claude creates todos
 
 In a [session that has the task-tracking tools](#model-availability), Claude creates todos for most multi-step work, such as:
 
-* **Complex multi-step tasks** requiring 3 or more distinct actions
+* **Complex multi-step tasks** requiring three or more distinct actions
 * **User-provided task lists** when multiple items are mentioned
-* **Non-trivial operations** that benefit from progress tracking
+* **Longer operations** that benefit from progress tracking
 * **Explicit requests** when users ask for todo organization
 
-It may skip todos for very short or single-step requests.
+Claude may skip todos for very short or single-step requests.
 
 ## Examples
 
-Before running these examples, install the Claude Agent SDK by following the [quickstart](/docs/en/agent-sdk/quickstart).
-
-Each example runs until the agent finishes and yields its final result message. If a session reaches its turn limit first, that result message has the `error_max_turns` subtype. Check `subtype` to detect that ending.
-
-These examples use single-shot `query()` calls. After yielding an `error_max_turns` result, `query()` raises an error that includes `Reached maximum number of turns`. Each example wraps its loop in a try block to exit cleanly when that happens.
-
-See [Handle the result](/docs/en/agent-sdk/agent-loop#handle-the-result) for the result subtypes.
-
-### Monitoring Todo Changes
-
-<CodeGroup>
-  ```typescript TypeScript theme={null}
-  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}`);
-  }
-  ```
-
-  ```python Python theme={null}
-  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())
-  ```
-</CodeGroup>
-
-### Real-time Progress Display
-
-<CodeGroup>
-  ```typescript TypeScript theme={null}
-  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");
-  ```
-
-  ```python Python theme={null}
-  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())
-  ```
-</CodeGroup>
-
-## Migrate to Task tools
-
-The Task tools split the single `TodoWrite` call into `TaskCreate` for each new item and `TaskUpdate` for each status change, with `TaskList` and `TaskGet` available for the model to read back the current list. Your monitoring code still inspects `tool_use` blocks in the assistant stream, but maintains a map keyed by task ID instead of replacing the whole list on every call.
-
-| With `TodoWrite`                              | With Task tools                                                                                                                                                                                                                                                                                     |
-| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| One tool call rewrites the full `todos` array | `TaskCreate` adds one item, `TaskUpdate` patches one item by `taskId`                                                                                                                                                                                                                               |
-| Match `block.name === "TodoWrite"`            | Match `block.name === "TaskCreate"` or `"TaskUpdate"`                                                                                                                                                                                                                                               |
-| Item shape: `{ content, status, activeForm }` | `TaskCreate` input: `{ subject, description, activeForm?, metadata? }`. `TaskUpdate` input: `{ taskId, status?, subject?, description?, activeForm?, addBlocks?, addBlockedBy?, owner?, metadata? }`. `status` is `"pending"`, `"in_progress"`, or `"completed"`; set `status: "deleted"` to delete |
-| Render `block.input.todos` directly           | Accumulate items across calls, or read a snapshot from a `TaskList` tool result                                                                                                                                                                                                                     |
-
-The assigned task ID is not in the `TaskCreate` input. It comes back in the matching `tool_result` as `{ task: { id, subject } }`, so capture it from the result block to key your map.
-
-The following example shows the minimal change to the [Monitoring Todo Changes](#monitoring-todo-changes) loop. It leaves `CLAUDE_CODE_ENABLE_TASKS` unset, because the Task tools are the default, and sets only `CLAUDE_CODE_ENABLE_TODO_TOOLS=1`, the [opt-in](#model-availability) for the models that otherwise don't get the tools. It reads only `tool_use` inputs and skips capturing IDs from `tool_result` blocks. To render a complete list, watch for a `TaskList` tool result in the stream or accumulate `TaskCreate` results and `TaskUpdate` inputs into a map.
-
-The streamed `tool_use` input is the raw shape the model emitted. Claude Code repairs some close-but-incorrect key names before execution, mapping `id` or `task_id` to `taskId` and `active_form` to `activeForm`, but that repair is not reflected in the stream. Read `TaskUpdate` input fields defensively, as the samples below do, rather than assuming the canonical name is always present.
+Before running these examples, install the Claude Agent SDK by following the [quickstart](/docs/en/agent-sdk/quickstart). Every example on this page shares the same permission setup and exit behavior:
+
+* **Permission mode**: the example prompts ask Claude to do real work on a project, so each example sets `permissionMode: "acceptEdits"` (TypeScript) or `permission_mode="acceptEdits"` (Python) to auto-approve the file edits that work produces. See [Permission modes](/docs/en/agent-sdk/permissions#permission-modes) for the alternatives.
+* **Turn limit**: each example runs until the agent finishes and yields its final result message. If a session reaches its turn limit first, that result message has the `error_max_turns` subtype. Check `subtype` to detect that ending.
+* **Error handling**: these examples use single-shot `query()` calls. After yielding an `error_max_turns` result, `query()` raises an error that includes `Reached maximum number of turns`. Each example wraps its loop in a try block to exit cleanly when that happens. See [Handle the result](/docs/en/agent-sdk/agent-loop#handle-the-result) for the result subtypes.
+
+<Note>
+  The task system messages, [`SDKTaskNotificationMessage`](/docs/en/agent-sdk/typescript#sdktasknotificationmessage) (TypeScript) or [`TaskNotificationMessage`](/docs/en/agent-sdk/python#tasknotificationmessage) (Python) among them, report background tasks such as backgrounded commands and subagents. In the message stream, you see todo activity as `tool_use` blocks in the assistant messages.
+</Note>
+
+### Monitor todo changes
+
+The following example watches the assistant stream for `TaskCreate` and `TaskUpdate` `tool_use` blocks and prints a `+` line with each new task's subject and an update line with each status change's task ID and new status. Use this shape when you want a log of task activity rather than a rendered display. The `+` lines don't include the assigned IDs, so this log can't match updates back to their creates. To keep that correlation, capture the IDs as [Display progress in real time](#display-progress-in-real-time) does.
+
+The streamed `tool_use` input is the raw shape the model emitted. Claude Code repairs some close-but-incorrect key names before execution, mapping `id` or `task_id` to `taskId` and `active_form` to `activeForm`, but that repair is not reflected in the stream. Read `TaskUpdate` input fields defensively, as both examples on this page do, rather than assuming the canonical name is always present.
 
 <CodeGroup>
   ```typescript TypeScript theme={null}
from line 72
     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" } },
+      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) {
from line 108
           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"}),
+              options=ClaudeAgentOptions(max_turns=15, permission_mode="acceptEdits", env={"CLAUDE_CODE_ENABLE_TODO_TOOLS": "1"}),
           ):
               if not isinstance(message, AssistantMessage):
                   continue
from line 116
                   if not isinstance(block, ToolUseBlock):
                       continue
                   if block.name == "TaskCreate":
-                      print(f"+ {block.input['subject']}")
+                      print(f"+ {block.input.get('subject', '')}")
                   elif block.name == "TaskUpdate" and block.input.get("status"):
                       task_id = (
                           block.input.get("taskId")
from line 134
   ```
 </CodeGroup>
 
-## Related Documentation
-
-* [TypeScript SDK Reference](/docs/en/agent-sdk/typescript)
-* [Python SDK Reference](/docs/en/agent-sdk/python)
-* [Streaming vs Single Mode](/docs/en/agent-sdk/streaming-vs-single-mode)
-* [Custom Tools](/docs/en/agent-sdk/custom-tools)
+### Display progress in real time
+
+The following example watches the assistant stream for `TaskCreate` and `TaskUpdate` `tool_use` blocks and keeps a map of tasks keyed by task ID in a `TaskTracker` class, rerendering a progress summary on every change. The summary counts completed and in-progress tasks and shows each active item's `activeForm` label in place of its `subject`. Use this shape when your application maintains a progress display instead of logging each event.
+
+The assigned task ID isn't in the `TaskCreate` input. Claude Code delivers each tool's structured output on the user message that carries its `tool_result` block, in the `tool_use_result` field. For `TaskCreate`, that object is documented for TypeScript as `TaskCreateOutput` under [Tool Output Types](/docs/en/agent-sdk/typescript#tool-output-types), and in Python the field is a plain dict of the same shape. The tracker pairs each `tool_result` block with its `tool_use` call by `tool_use_id` and reads `task.id` from the paired message's `tool_use_result`. Claude can read the list back with `TaskList` and one task's full details with `TaskGet`.
+
+<CodeGroup>
+  ```typescript TypeScript theme={null}
+  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() {
+      if (this.tasks.size === 0) {
+        console.log("\nProgress: no open tasks\n");
+        return;
+      }
+
+      const items = [...this.tasks.values()];
+      const completed = items.filter((t) => t.status === "completed").length;
+      const inProgress = items.filter((t) => t.status === "in_progress").length;
+
+      console.log(`\nProgress: ${completed}/${this.tasks.size} completed`);
+      console.log(`Currently working on: ${inProgress} task(s)\n`);
+
+      for (const [id, task] of this.tasks) {
+        const icon =
+          task.status === "completed" ? "โœ…" : task.status === "in_progress" ? "๐Ÿ”ง" : "โŒ";
+        const text = task.status === "in_progress" && task.activeForm ? task.activeForm : task.subject;
+        console.log(`${id}. ${icon} ${text}`);
+      }
+    }
+
+    handleToolUse(block: { id: string; name: string; input: unknown }) {
+      if (block.name === "TaskCreate") {
+        const input = block.input as { subject: string; activeForm?: string; active_form?: string };
+        this.pendingCreates.set(block.id, {
+          subject: input.subject,
+          activeForm: input.activeForm ?? input.active_form,
+        });
+      } else if (block.name === "TaskUpdate") {
+        const input = block.input as {
+          taskId?: string;
+          id?: string;
+          task_id?: string;
+          status?: string;
+          activeForm?: string;
+          active_form?: string;
+        };
+        const taskId = input.taskId ?? input.id ?? input.task_id;
+        if (!taskId) return;
+        if (input.status === "deleted") {
+          this.tasks.delete(taskId);
+          this.displayProgress();
+          return;
+        }
+        const task = this.tasks.get(taskId);
+        if (!task) return;
+        if (input.status) task.status = input.status;
+        const active = input.activeForm ?? 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;
+      // The result's user message carries the tool's structured output as
+      // tool_use_result; for TaskCreate that's TaskCreateOutput,
+      // { task: { id, subject } }.
+      const out = result as { task?: { id: string } };
+      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) {
+        // 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 TaskTracker();
+  await tracker.trackQuery("Build a complete authentication system with todos");
+  ```
+
+  ```python Python theme={null}
+  import asyncio
+
+  from claude_agent_sdk import (
+      query,
+      ClaudeAgentOptions,
+      AssistantMessage,
+      UserMessage,
+      ToolUseBlock,
+      ToolResultBlock,
+  )
+
+
+  class TaskTracker:
+      def __init__(self):
+          self.tasks: dict[str, dict] = {}
+          self.pending_creates: dict[str, dict] = {}
+
+      def display_progress(self):
+          if not self.tasks:
+              print("\nProgress: no open tasks\n")
+              return
+
+          completed = len([t for t in self.tasks.values() if t["status"] == "completed"])
+          in_progress = len([t for t in self.tasks.values() if t["status"] == "in_progress"])
+
+          print(f"\nProgress: {completed}/{len(self.tasks)} completed")
+          print(f"Currently working on: {in_progress} task(s)\n")
+
+          for task_id, task in self.tasks.items():
+              icon = (
+                  "โœ…"
+                  if task["status"] == "completed"
+                  else "๐Ÿ”ง"
+                  if task["status"] == "in_progress"
+                  else "โŒ"
+              )
+              text = (
+                  task["activeForm"]
+                  if task["status"] == "in_progress" and task.get("activeForm")
+                  else task["subject"]
+              )
+              print(f"{task_id}. {icon} {text}")
+
+      def handle_tool_use(self, block: ToolUseBlock):
+          if block.name == "TaskCreate":
+              self.pending_creates[block.id] = {
+                  "subject": block.input.get("subject", ""),
+                  "activeForm": block.input.get("activeForm") or block.input.get("active_form"),
+              }
+          elif block.name == "TaskUpdate":
+              task_id = (
+                  block.input.get("taskId")
+                  or block.input.get("id")
+                  or block.input.get("task_id")
+              )
+              if not task_id:
+                  return
+              if block.input.get("status") == "deleted":
+                  self.tasks.pop(task_id, None)
+                  self.display_progress()
+                  return
+              task = self.tasks.get(task_id)
+              if not task:
+                  return
+              if block.input.get("status"):
+                  task["status"] = block.input["status"]
+              active = block.input.get("activeForm") or block.input.get("active_form")
+              if active:
+                  task["activeForm"] = active
+              self.display_progress()
+
+      def handle_tool_result(self, block: ToolResultBlock, tool_use_result):
+          create = self.pending_creates.pop(block.tool_use_id, None)
+          if create is None or block.is_error:
+              return
+          # The result's user message carries the tool's structured output as
+          # tool_use_result; for TaskCreate that's {"task": {"id": ..., "subject": ...}}.
+          task = (tool_use_result or {}).get("task") or {}
+          if not task.get("id"):
+              return
+          self.tasks[task["id"]] = {**create, "status": "pending"}
+          self.display_progress()
+
+      async def track_query(self, prompt: str):
+          try:
+              async for message in query(
+                  prompt=prompt,
+                  options=ClaudeAgentOptions(
+                      max_turns=20,
+                      permission_mode="acceptEdits",
+                      env={"CLAUDE_CODE_ENABLE_TODO_TOOLS": "1"},
+                  ),
+              ):
+                  if isinstance(message, AssistantMessage):
+                      for block in message.content:
+                          if isinstance(block, ToolUseBlock):
+                              self.handle_tool_use(block)
+                  if isinstance(message, UserMessage) and isinstance(message.content, list):
+                      for block in message.content:
+                          if isinstance(block, ToolResultBlock):
+                              self.handle_tool_result(block, message.tool_use_result)
+          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 = TaskTracker()
+      await tracker.track_query("Build a complete authentication system with todos")
+
+
+  asyncio.run(main())
+  ```
+</CodeGroup>
+
+## Related documentation
+
+* [Agent SDK reference - TypeScript](/docs/en/agent-sdk/typescript): the options, types, and tool schemas for the TypeScript SDK, including the Task tool input and output types
+* [Agent SDK reference - Python](/docs/en/agent-sdk/python): the options, types, and tool documentation for the Python SDK
+* [Streaming Input](/docs/en/agent-sdk/streaming-vs-single-mode): the two input modes, and when to use streaming input instead of the single-shot calls these examples use
+* [Give Claude custom tools](/docs/en/agent-sdk/custom-tools): define your own tools with the SDK's in-process MCP server
 

agent-sdk/todo-tracking Changed · +1 / -1 lines

from line 35
 
 ### When Todos Are Used
 
-Claude creates todos for most multi-step work, such as:
+In a [session that has the task-tracking tools](#model-availability), Claude creates todos for most multi-step work, such as:
 
 * **Complex multi-step tasks** requiring 3 or more distinct actions
 * **User-provided task lists** when multiple items are mentioned

agent-sdk/todo-tracking Changed · +35 / -13 lines

### Model availability

from line 5
 The Claude Agent SDK includes built-in todo functionality that helps organize complex workflows and keep users informed about task progression.
 
 <Note>
-  As of TypeScript Agent SDK 0.3.142 and Claude Code v2.1.142, sessions use the structured Task tools `TaskCreate`, `TaskUpdate`, `TaskGet`, and `TaskList` instead of `TodoWrite`. The Python SDK gets this change from the Claude Code CLI it launches, not from the Python package version: the switch applies once that CLI โ€” the copy bundled inside the pip package, or one you point to with `cli_path` โ€” is v2.1.142 or later. See [Migrate to Task tools](#migrate-to-task-tools) for how monitoring code changes. The examples on this page set `CLAUDE_CODE_ENABLE_TASKS=0` to keep showing `TodoWrite` for sessions that have not migrated yet.
+  On TypeScript Agent SDK 0.3.233 and later, or Python Agent SDK 0.2.139 and later, 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:
+
+  * `TodoWrite`
+  * `TaskCreate`
+  * `TaskGet`
+  * `TaskUpdate`
+  * `TaskList`
+
+  On other models, Claude Code provides the Task tools by default and `TodoWrite` only when you set `CLAUDE_CODE_ENABLE_TASKS=0`.
 </Note>
 
+### Model availability
+
+On the [models that don't get the task-tracking tools](/docs/en/tools-reference#task-tool-availability), you see no `tool_use` blocks for them in the message stream unless you opt in. If you point `cli_path` in Python or `pathToClaudeCodeExecutable` in TypeScript at your own Claude Code install, you get whichever tools that install provides. To get the same tools as on other models, do one of the following:
+
+* Name one of the tools in the [`allowedTools`](/docs/en/agent-sdk/permissions#allow-and-deny-rules) option, `allowed_tools` in Python
+* List the tools in the `tools` option, which restricts the session's built-in tools to the ones it names. Include the tools you want alongside the other built-in tools you use
+* Set `CLAUDE_CODE_ENABLE_TODO_TOOLS=1` in the `env` option, as the examples on this page do. In TypeScript, `env` replaces the subprocess environment, so spread `...process.env` to keep inherited variables. In Python, `env` is merged on top of the inherited environment
+
 ### Todo Lifecycle
 
 Claude moves each todo through a predictable lifecycle:
from line 64
     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.
-      options: { maxTurns: 15, env: { ...process.env, CLAUDE_CODE_ENABLE_TASKS: "0" } }
+      // 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") {
from line 102
           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.
-              options=ClaudeAgentOptions(max_turns=15, env={"CLAUDE_CODE_ENABLE_TASKS": "0"}),
+              # 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):
from line 163
       try {
         for await (const message of query({
           prompt,
-          // Re-enable TodoWrite, which this tracker watches for.
-          options: { maxTurns: 20, env: { ...process.env, CLAUDE_CODE_ENABLE_TASKS: "0" } }
+          // 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) {
from line 229
           try:
               async for message in query(
                   prompt=prompt,
-                  # Re-enable TodoWrite, which this tracker watches for.
-                  options=ClaudeAgentOptions(max_turns=20, env={"CLAUDE_CODE_ENABLE_TASKS": "0"}),
+                  # 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:
from line 255
 
 ## Migrate to Task tools
 
-The Task tools split the single `TodoWrite` call into `TaskCreate` for each new item and `TaskUpdate` for each status change, with `TaskList` and `TaskGet` available for the model to read back the current list. Your monitoring code still inspects `tool_use` blocks in the assistant stream, but maintains a map keyed by task ID instead of replacing the whole list on every call. The Task tools are the default as of TypeScript Agent SDK 0.3.142 and Claude Code v2.1.142, so no `options.env` change is needed.
+The Task tools split the single `TodoWrite` call into `TaskCreate` for each new item and `TaskUpdate` for each status change, with `TaskList` and `TaskGet` available for the model to read back the current list. Your monitoring code still inspects `tool_use` blocks in the assistant stream, but maintains a map keyed by task ID instead of replacing the whole list on every call.
 
 | With `TodoWrite`                              | With Task tools                                                                                                                                                                                                                                                                                     |
 | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
from line 264
 | Item shape: `{ content, status, activeForm }` | `TaskCreate` input: `{ subject, description, activeForm?, metadata? }`. `TaskUpdate` input: `{ taskId, status?, subject?, description?, activeForm?, addBlocks?, addBlockedBy?, owner?, metadata? }`. `status` is `"pending"`, `"in_progress"`, or `"completed"`; set `status: "deleted"` to delete |
 | Render `block.input.todos` directly           | Accumulate items across calls, or read a snapshot from a `TaskList` tool result                                                                                                                                                                                                                     |
 
-The assigned task ID is not in the `TaskCreate` input. It comes back in the matching `tool_result` as `{ task: { id, subject } }`, so capture it from the result block to key your map. The following example shows the minimal change to the [Monitoring Todo Changes](#monitoring-todo-changes) loop. It reads only `tool_use` inputs and skips capturing IDs from `tool_result` blocks. To render a complete list, watch for a `TaskList` tool result in the stream or accumulate `TaskCreate` results and `TaskUpdate` inputs into a map.
+The assigned task ID is not in the `TaskCreate` input. It comes back in the matching `tool_result` as `{ task: { id, subject } }`, so capture it from the result block to key your map.
 
+The following example shows the minimal change to the [Monitoring Todo Changes](#monitoring-todo-changes) loop. It leaves `CLAUDE_CODE_ENABLE_TASKS` unset, because the Task tools are the default, and sets only `CLAUDE_CODE_ENABLE_TODO_TOOLS=1`, the [opt-in](#model-availability) for the models that otherwise don't get the tools. It reads only `tool_use` inputs and skips capturing IDs from `tool_result` blocks. To render a complete list, watch for a `TaskList` tool result in the stream or accumulate `TaskCreate` results and `TaskUpdate` inputs into a map.
+
 The streamed `tool_use` input is the raw shape the model emitted. Claude Code repairs some close-but-incorrect key names before execution, mapping `id` or `task_id` to `taskId` and `active_form` to `activeForm`, but that repair is not reflected in the stream. Read `TaskUpdate` input fields defensively, as the samples below do, rather than assuming the canonical name is always present.
 
 <CodeGroup>
from line 277
   try {
     for await (const message of query({
       prompt: "Optimize my React app performance and track progress with todos",
-      options: { maxTurns: 15 },
+      // 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) {
from line 313
       try:
           async for message in query(
               prompt="Optimize my React app performance and track progress with todos",
-              options=ClaudeAgentOptions(max_turns=15),
+              # 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

agent-sdk/todo-tracking First recorded · 326 lines, first recorded

# Todo Lists ### Todo Lifecycle ### When Todos Are Used ## Examples ### Monitoring Todo Changes ### Real-time Progress Display ## Migrate to Task tools ## Related Documentation

The first capture of this source. The page was already there, and this is what it said.

# Todo Lists

> Track and display todos using the Claude Agent SDK for organized task management

The Claude Agent SDK includes built-in todo functionality that helps organize complex workflows and keep users informed about task progression.

<Note>
  As of TypeScript Agent SDK 0.3.142 and Claude Code v2.1.142, sessions use the structured Task tools `TaskCreate`, `TaskUpdate`, `TaskGet`, and `TaskList` instead of `TodoWrite`. The Python SDK gets this change from the Claude Code CLI it launches, not from the Python package version: the switch applies once that CLI โ€” the copy bundled inside the pip package, or one you point to with `cli_path` โ€” is v2.1.142 or later. See [Migrate to Task tools](#migrate-to-task-tools) for how monitoring code changes. The examples on this page set `CLAUDE_CODE_ENABLE_TASKS=0` to keep showing `TodoWrite` for sessions that have not migrated yet.
</Note>

### Todo Lifecycle

Claude moves each todo through a predictable lifecycle:

1. **Created**: Claude adds the todo as `pending` when it identifies a task
2. **Activated**: Claude sets the todo to `in_progress` when it starts the work
3. **Completed**: Claude marks it completed when the task finishes successfully
4. **Removed**: Claude deletes a todo it no longer needs by setting `status: "deleted"` in a `TaskUpdate` call

### When Todos Are Used

Claude creates todos for most multi-step work, such as:

* **Complex multi-step tasks** requiring 3 or more distinct actions
* **User-provided task lists** when multiple items are mentioned
* **Non-trivial operations** that benefit from progress tracking
* **Explicit requests** when users ask for todo organization

It may skip todos for very short or single-step requests.

## Examples

Before running these examples, install the Claude Agent SDK by following the [quickstart](/docs/en/agent-sdk/quickstart).

Each example runs until the agent finishes and yields its final result message. If a session reaches its turn limit first, that result message has the `error_max_turns` subtype. Check `subtype` to detect that ending.

These examples use single-shot `query()` calls. After yielding an `error_max_turns` result, `query()` raises an error that includes `Reached maximum number of turns`. Each example wraps its loop in a try block to exit cleanly when that happens.

See [Handle the result](/docs/en/agent-sdk/agent-loop#handle-the-result) for the result subtypes.

### Monitoring Todo Changes

<CodeGroup>
  ```typescript TypeScript theme={null}
  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.
      options: { maxTurns: 15, env: { ...process.env, CLAUDE_CODE_ENABLE_TASKS: "0" } }
    })) {
      // 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}`);
  }
  ```

  ```python Python theme={null}
  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.
              options=ClaudeAgentOptions(max_turns=15, env={"CLAUDE_CODE_ENABLE_TASKS": "0"}),
          ):
              # 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())
  ```
</CodeGroup>

### Real-time Progress Display

<CodeGroup>
  ```typescript TypeScript theme={null}
  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,
          // Re-enable TodoWrite, which this tracker watches for.
          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) {
        // 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");
  ```

  ```python Python theme={null}
  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,
                  # Re-enable TodoWrite, which this tracker watches for.
                  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:
              # 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())
  ```
</CodeGroup>

## Migrate to Task tools

The Task tools split the single `TodoWrite` call into `TaskCreate` for each new item and `TaskUpdate` for each status change, with `TaskList` and `TaskGet` available for the model to read back the current list. Your monitoring code still inspects `tool_use` blocks in the assistant stream, but maintains a map keyed by task ID instead of replacing the whole list on every call. The Task tools are the default as of TypeScript Agent SDK 0.3.142 and Claude Code v2.1.142, so no `options.env` change is needed.

| With `TodoWrite`                              | With Task tools                                                                                                                                                                                                                                                                                     |
| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| One tool call rewrites the full `todos` array | `TaskCreate` adds one item, `TaskUpdate` patches one item by `taskId`                                                                                                                                                                                                                               |
| Match `block.name === "TodoWrite"`            | Match `block.name === "TaskCreate"` or `"TaskUpdate"`                                                                                                                                                                                                                                               |
| Item shape: `{ content, status, activeForm }` | `TaskCreate` input: `{ subject, description, activeForm?, metadata? }`. `TaskUpdate` input: `{ taskId, status?, subject?, description?, activeForm?, addBlocks?, addBlockedBy?, owner?, metadata? }`. `status` is `"pending"`, `"in_progress"`, or `"completed"`; set `status: "deleted"` to delete |
| Render `block.input.todos` directly           | Accumulate items across calls, or read a snapshot from a `TaskList` tool result                                                                                                                                                                                                                     |

The assigned task ID is not in the `TaskCreate` input. It comes back in the matching `tool_result` as `{ task: { id, subject } }`, so capture it from the result block to key your map. The following example shows the minimal change to the [Monitoring Todo Changes](#monitoring-todo-changes) loop. It reads only `tool_use` inputs and skips capturing IDs from `tool_result` blocks. To render a complete list, watch for a `TaskList` tool result in the stream or accumulate `TaskCreate` results and `TaskUpdate` inputs into a map.

The streamed `tool_use` input is the raw shape the model emitted. Claude Code repairs some close-but-incorrect key names before execution, mapping `id` or `task_id` to `taskId` and `active_form` to `activeForm`, but that repair is not reflected in the stream. Read `TaskUpdate` input fields defensively, as the samples below do, rather than assuming the canonical name is always present.

<CodeGroup>
  ```typescript TypeScript theme={null}
  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) {
    // A single-shot query() throws after yielding an error result.
    console.log(`Session ended with an error: ${error}`);
  }
  ```

  ```python Python theme={null}
  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):

Cut at 300 lines.