Skip to content

Todo Write

A todo-write tool gives the model a structured way to update task-progress state during execution.

Unlike a one-time plan proposal, todo write is a state mutation primitive that happens repeatedly during a turn:

  • create initial task list
  • mark active task in_progress
  • mark finished tasks completed
  • add/remove/cancel tasks as scope changes

The value is not only organization. It is observability: users can see what the agent thinks it is doing, in what order, and what remains.

Todo-write looks simple, but production behavior is sensitive to update semantics:

  • Is each write a full replacement or partial patch?
  • Are statuses strongly typed or free-form strings?
  • Is there a single-writer assumption (one agent) or multi-writer concurrency?
  • How are write events propagated to UI, API, and headless clients?

If the contract is vague, todo state becomes inconsistent and misleading.


  • Planning for plan-authoring mode semantics vs checklist state mutation.
  • Todo Read for read-path guarantees and consistency constraints.
  • Chat History for persistence/compaction pressure that makes typed todo state useful.

Aider at commit b9050e1d5faf8096eae7a46a9ecc05a86231384b has no first-class todo-write tool.

Aider exposes planning-adjacent chat modes (/ask, /architect) in aider/commands.py, but no cmd_todo or model-callable todo writer.

State tracking is conversational rather than structured:

  • model describes next steps in natural language
  • host/runtime does not persist a typed checklist object
  • UI cannot render task status transitions from a canonical event stream

Planning Text in Some Function Payloads Is Not Todo State

Section titled “Planning Text in Some Function Payloads Is Not Todo State”

Legacy function-coder variants include explanation fields described as step-by-step plans:

  • aider/coders/single_wholefile_func_coder.py
  • aider/coders/wholefile_func_coder.py
  • aider/coders/editblock_func_coder.py

These fields are tied to edit payloads and not managed as an independent, mutable todo list.

Aider can still complete complex tasks effectively, but task tracking is opaque to external clients. There is no typed todo.updated equivalent to drive structured progress UI.


Codex at commit 4ab44e2c5cc54ed47e47a6729dfd8aa5a3dc2476 uses update_plan as its todo-write pathway.

core/src/tools/handlers/plan.rs defines PLAN_TOOL named update_plan with parameters:

  • explanation?
  • plan[]
    • each item has step and status

Typed status enum in protocol/src/plan_tool.rs:

  • Pending
  • InProgress
  • Completed

There is no cancelled status in this schema.

Write flow:

  1. parse JSON arguments into UpdatePlanArgs
  2. reject call in collaboration Plan mode
  3. emit EventMsg::PlanUpdate(args)
  4. return Plan updated

The authoritative side effect is event emission, not tool output text.

Projection to Todo Items in Exec/SDK Layer

Section titled “Projection to Todo Items in Exec/SDK Layer”

exec/src/event_processor_with_jsonl_output.rs transforms PlanUpdate events into todo_list item lifecycle for thread output:

  • first write in turn -> ItemStarted
  • subsequent writes -> ItemUpdated
  • turn end -> ItemCompleted

Mapping rule:

  • StepStatus::Completed => TodoItem.completed=true
  • Pending / InProgress => completed=false

This projection is what SDK consumers see (sdk/typescript/src/items.ts TodoListItem).

TUI path:

  • tui/src/chatwidget.rs receives EventMsg::PlanUpdate
  • tui/src/history_cell.rs renders checklist-style cell

Visual semantics:

  • completed: checked/struck style
  • in-progress: highlighted open box
  • pending: dimmed open box

app-server/src/bespoke_event_handling.rs maps EventMsg::PlanUpdate to TurnPlanUpdatedNotification (API v2), with explicit code comment clarifying that this checklist update is distinct from plan-mode streaming.

Codex explicitly tests todo-write semantics:

  • core/tests/suite/tool_harness.rs
    • emits PlanUpdate on valid payload
    • rejects malformed payload
  • exec/tests/event_processor_with_json_output.rs
    • validates started/updated/completed todo lifecycle
  • prompt text recommends at most one in_progress, but handler does not enforce cardinality
  • no priority field
  • no cancelled/skipped status in typed contract

OpenCode at commit 7ed449974864361bad2c1f1405769fd2c2fcdf42 has a dedicated todowrite tool plus storage/event pipelines.

packages/opencode/src/tool/todo.ts defines:

  • tool id: todowrite
  • params: todos: Todo.Info[]

Todo.Info schema (session/todo.ts) uses fields:

  • content
  • status
  • priority

Status and priority are currently free-form z.string() values (described expected values, not strict enums).

Before writing, tool calls:

  • ctx.ask({ permission: "todowrite", patterns: ["*"], always: ["*"] })

So todo writes are permission-governed operations, not unconditional state mutations.

Storage Semantics: Full Replacement Transaction

Section titled “Storage Semantics: Full Replacement Transaction”

Todo.update(...) in session/todo.ts performs replace-all in one DB transaction:

  1. delete all existing todos for session
  2. insert new list with positional ordering

Backed by TodoTable in session/session.sql.ts with composite primary key (session_id, position).

This is deterministic and simple, but means every write must include the complete desired list.

After write, Todo.update publishes todo.updated bus event.

Consumers:

  • TUI sync store (cli/cmd/tui/context/sync.tsx) listens for todo.updated
  • TUI session route (cli/cmd/tui/routes/session/index.tsx) renders todowrite tool parts
  • CLI run mode (cli/cmd/run.ts) prints checklist-style block for todowrite
  • ACP bridge (acp/agent.ts) parses todo output and forwards as plan entries

Multiple provider/system prompt templates strongly encourage frequent todo updates, especially for complex tasks:

  • session/prompt/anthropic.txt
  • session/prompt/copilot-gpt-5.txt
  • tool guidance in tool/todowrite.txt

So OpenCode couples runtime support with prompt policy to increase usage frequency.


Replace-All Writes Can Accidentally Drop Tasks

Section titled “Replace-All Writes Can Accidentally Drop Tasks”

If the model writes only “changed” tasks without re-sending unchanged tasks, replace-all semantics delete omitted entries.

OpenCode describes expected statuses (pending, in_progress, completed, cancelled) but schema accepts any string, so typos can silently corrupt downstream UI logic.

Codex and OpenCode both guide “one in_progress” behavior in prompts/docs. Without handler-level validation, invalid multi-active states still pass.

If multiple agents can write todos concurrently without merge strategy, status thrash is unavoidable. Subagent permission-deny defaults for todo tools are a practical containment strategy.

The write acknowledgement string is secondary. Durable truth should come from persisted state + emitted events.


Define todo_write with explicit update mode:

  • mode: replace | patch
  • items[] with strongly typed fields
  • optional explanation

Default to replace for deterministic behavior, but support patch for safer incremental updates.

Use enums:

  • status: pending | in_progress | completed | cancelled | blocked
  • priority: high | medium | low

Enforce invariants server-side:

  • max one in_progress
  • unique task ids
  • stable ordering

Store todo rows with:

  • stable task_id
  • position
  • optimistic concurrency field (version)

Reject stale writes when version mismatches to prevent concurrent clobbering.

Emit explicit events:

  • TodoListReplaced
  • TodoPatched
  • TodoInvariantViolation

Provide a normalized TodoSnapshot event for easy client hydration.

All clients should consume the same canonical snapshot model. Tool output text can remain human-readable, but rendering must come from structured event payloads.

  • serde/schemars for schema
  • sqlx or rusqlite for persistence
  • tokio::sync::watch or broadcast channels for updates
  • ratatui widgets for checklist visualization
  1. Treat todo write as a state mutation API, not a chat convenience.
  2. Enforce invariants in handler code.
  3. Support both deterministic replace and safe patch workflows.
  4. Use stable task ids to make updates mergeable and auditable.