todo_write
Applies ordered mutations to the session todo list and returns a text summary plus the full phase/task state.
Source
- Entry:
packages/coding-agent/src/tools/todo-write.ts - Model-facing prompt:
packages/coding-agent/src/prompts/tools/todo-write.md - Key collaborators:
packages/coding-agent/src/tools/index.ts— registers tool, exposes session hooks, gates availability.packages/coding-agent/src/modes/controllers/event-controller.ts— updates the visible todo UI on tool completion.packages/coding-agent/src/session/agent-session.ts— stores cached phases, auto-clears done/dropped tasks, emits failure reminders.packages/coding-agent/src/modes/controllers/todo-command-controller.ts—/todocommand path, custom-entry persistence, transcript reminder injection.packages/coding-agent/src/tools/render-utils.ts— collapsed-preview cap for renderer trees.
Inputs
| Field | Type | Required | Description |
|---|---|---|---|
ops |
TodoOpEntry[] |
Yes | Ordered operations to apply. minItems: 1. |
TodoOpEntry
| Op | Required fields | Optional fields | Effect |
|---|---|---|---|
init |
list |
None of the other fields are used | Replaces the entire list with list; every new task starts pending before normalization. |
start |
task |
None | Marks one task in_progress; any other in_progress task is demoted to pending. |
done |
task or phase or neither |
None | Marks the target task, phase, or all tasks completed. |
drop |
task or phase or neither |
None | Marks the target task, phase, or all tasks abandoned. |
rm |
task or phase or neither |
None | Removes the target task, clears the phase's task list, or clears all task lists. |
append |
phase, items |
None | Appends new pending tasks to a phase; creates the phase if missing. |
note |
task, text |
None | Appends one trimmed note string to the task's notes array. |
Fields used inside ops
| Field | Type | Required | Description |
|---|---|---|---|
op |
`"init" | "start" | "done" |
list |
{ phase: string; items: string[] }[] |
For init |
Full replacement payload. Each items array has minItems: 1. |
task |
string |
For start; for task-targeted done/drop/rm/note |
Exact task content match. |
phase |
string |
For append; for phase-targeted done/drop/rm |
Exact phase name match, except append lazily creates a missing phase. |
items |
string[] |
For append |
Tasks to append. minItems: 1. |
text |
string |
For note |
Note text; trailing whitespace is stripped before storing. Empty-after-trim is rejected. |
Outputs
The tool returns a single-shot AgentToolResult:
content: one text part containing the summary fromformatSummary(...).- Empty final state with no errors:
Todo list cleared. - Non-empty final state: remaining-item list, current phase progress, then a per-phase tree.
- If the active
in_progresstask has notes, the summary includes the note bodies inline. - If any op produced validation/runtime errors, the summary starts with
Errors: ...; the returned tool result is markedisError: trueand still includes the mutated state.
- Empty final state with no errors:
details:phases: TodoPhase[]storage: "session" | "memory"completedTasks?: TodoCompletionTransition[]when a task changed from non-completed tocompletedduring the batch
TodoPhase / TodoItem state model:
TodoPhase:{ name: string, tasks: TodoItem[] }TodoItem:{ content: string, status: "pending" | "in_progress" | "completed" | "abandoned", notes?: string[] }
The TUI renderer (todoWriteToolRenderer) merges call and result into one transcript block, renders phases as a tree, shows note counts as superscripts, and renders the note bodies only for the current in_progress task. Collapsed transcript previews cap tree items at PREVIEW_LIMITS.COLLAPSED_ITEMS (8).
Flow
TodoWriteTool.execute(...)clones the current cached phases fromsession.getTodoPhases?.() ?? [](packages/coding-agent/src/tools/todo-write.ts).applyParams(...)walksparams.opsin order and applies each entry withapplyEntry(...).- Each op mutates the working phase array:
initPhases(...)rebuilds the list from scratch.startresolves a task by exactcontent, demotes every otherin_progresstask topending, then marks the targetin_progress.done/dropusegetTaskTargets(...)to target one task, one phase, or every task.rmremoves one task, clears one phase'stasks, or clears all phases' task arrays.appendItems(...)resolves or creates the target phase and pushes newpendingtasks unless the same task content already exists anywhere.notetrims trailing whitespace, rejects empty text, and appends the note totask.notes.
- Missing task/phase references are recorded in an
errorsarray byresolveTaskOrError(...)/resolvePhaseOrError(...); execution continues through the rest of the batch. - After the full batch,
normalizeInProgressTask(...)enforces the single-active-task invariant:- if multiple tasks are
in_progress, only the first stays active and the rest becomepending; - if none are
in_progress, the firstpendingtask in phase/task order is auto-promoted toin_progress.
- if multiple tasks are
execute(...)stores the normalized phases withsession.setTodoPhases?.(...)and reportsstorageas"session"whensession.getSessionFile()exists, else"memory".getCompletionTransitions(...)compares the previous and updated phases; newly completed tasks are returned indetails.completedTasks.- The agent runtime also watches
todo_writetool results inpackages/coding-agent/src/session/agent-session.ts; successful results refresh cached todos, failed results inject a hidden next-turn reminder telling the model that todo progress is not visible until it retries. - The event controller updates the visible todo UI from
result.details.phaseson success, or shows a warning on error (packages/coding-agent/src/modes/controllers/event-controller.ts).
Modes / Variants
State transitions
| Current status | start |
done |
drop |
rm |
append |
note |
|---|---|---|---|---|---|---|
pending |
in_progress on target |
completed |
abandoned |
Removed | New tasks enter as pending |
No status change |
in_progress |
Target stays in_progress; non-target active tasks become pending |
completed |
abandoned |
Removed | No status change | No status change |
completed |
Can be set back to in_progress if targeted |
Stays completed |
Becomes abandoned if targeted |
Removed | No status change | No status change |
abandoned |
Can be set back to in_progress if targeted |
Becomes completed if targeted |
Stays abandoned |
Removed | No status change | No status change |
Normalization then re-applies the single-active-task rule after the full op batch.
Op targeting rules
done,drop,rm:taskset: affect one exact-content task.- else
phaseset: affect every task in that exact-name phase. - else: affect every task in every phase.
appendis the only op that creates a missing phase.noteonly targets a single task.initdiscards previous phases entirely.
Markdown round-trip helpers
The same file also exposes non-tool helpers used by /todo:
phasesToMarkdown(...)serializes phases as headings plus checklist items ([ ],[/],[x],[-]) with blockquote note bodies.markdownToPhases(...)parses that format, defaults orphan tasks into aTodosphase, accepts>as anin_progressmarker and~asabandoned, and runs the same normalization step.
Side Effects
- Filesystem
- None in the tool itself.
- Session state (transcript, memory, jobs, checkpoints, registries)
- Mutates the session todo cache through
setTodoPhases. storagereports whether the session has a backing session file, but the tool does not append a custom session entry itself.- Successful tool-result messages carry
details.phases;getLatestTodoPhasesFromEntries(...)can reconstruct state later from those transcript entries. - Failed
todo_writeresults causeagent-sessionto enqueue a hidden next-turn reminder (customType: "todo-write-error-reminder").
- Mutates the session todo cache through
- User-visible prompts / interactive UI
- Transcript block is rendered by
todoWriteToolRendererand merged with the call line. event-controllerupdates the visible todo panel from successful results.- On error,
event-controllershowsTodo update failed...; the visible panel may stay stale until a later successful call.
- Transcript block is rendered by
- Background work / cancellation
AgentSession.setTodoPhases(...)schedules auto-clear timers forcompleted/abandonedtasks viatasks.todoClearDelay.
Limits & Caps
opsarray:minItems: 1(todoWriteSchema).init.list[*].items:minItems: 1.append.items:minItems: 1.- Renderer collapsed preview:
PREVIEW_LIMITS.COLLAPSED_ITEMS = 8(packages/coding-agent/src/tools/render-utils.ts). - Auto-clear delay:
tasks.todoClearDelaydefault60seconds;< 0disables auto-clear,0clears on the next microtask (packages/coding-agent/src/session/agent-session.ts). - Tool execution mode:
concurrency = "exclusive",strict = true,loadMode = "discoverable".
Errors
- Ordinary bad op payloads are accumulated as human-readable strings in
errors; the tool still returns the mutated state, but marks the resultisError: true. - Error strings come from the helpers in
packages/coding-agent/src/tools/todo-write.ts, including:Missing list for init operationMissing task contentTask "..." not foundwith an extra empty-list hint when applicableMissing phase namePhase "..." not foundMissing phase name for append operationMissing items for append operationTask "..." already existsMissing text for note operation
- Because ops are processed in order, earlier errors do not roll back later ops.
- Runtime-level tool failure is handled outside the tool body:
agent-sessioninjects a hidden reminder and the event controller warns the user that visible progress may be stale. - Idempotency is op-specific:
initis a full replacement; replaying the same payload yields the same state.start,done, anddropare effectively idempotent on an existing target state, butstartalso demotes any other active task.rmis not idempotent for targeted removals: the second call errors because the task or phase is gone.appendis not idempotent: duplicate task content is rejected withTask "..." already exists.noteis append-only and never idempotent; replaying it adds another note entry.
Notes
- Task lookup is exact string equality inside the tool. The model-facing prompt says task content and phase names are identifiers and should stay unique;
appendenforces task uniqueness globally, butinitdoes not validate duplicate task or phase names. findTaskByContent(...)returns the first matching task across phases. Duplicate task contents make later targeted ops ambiguous.normalizeInProgressTask(...)runs after the whole batch, not after each op. A single call can intentionally build an intermediate invalid state and rely on final normalization.storage: "session"means the session has a session-file backing; it does not mean this tool wrote a durable custom entry.- Reload persistence differs by path:
- plain
todo_writecalls survive in transcript tool-result details; /todocommand edits additionally appendcustomType: "user_todo_edit"entries and inject a visible-to-model<system-reminder>developer message describing the manual edit.
- plain
- On session resume,
AgentSession.#syncTodoPhasesFromBranch()stripscompletedandabandonedtasks before restoring the cached list. The/todocommand works around that by reading the latest transcript/custom-entry state so historical done/dropped tasks still appear to the user. - Tool availability is gated by
todo.enabled, and the registry excludes it whenincludeYieldis enabled (packages/coding-agent/src/tools/index.ts). - Subagents do not inherit
todo_write;packages/coding-agent/src/task/executor.tsfilters it out as a parent-owned tool.