Skip to main content
Version: Next 🚧

Todo List

TodoListMiddleware gives the agent a write_todos tool for maintaining a structured checklist during multi-step tasks. The model calls the tool to create and update items; the middleware enforces that the list stays in sync before the run ends.

Use it when agents need to track progress across many steps, or when you want the model to show the user a live breakdown of what it's doing.

Basic setup

TodoListMiddleware requires an extra_ref callable that returns the live AgentContext.extra dict. The middleware and the tool both read and write through this reference so state survives checkpointing.

from cubepi import Agent
from cubepi.middleware import TodoListMiddleware

agent_extra: dict = {}

agent = Agent(
model=provider.model("claude-sonnet-4-6"),
system_prompt="You are a thorough assistant.",
middleware=[
TodoListMiddleware(extra_ref=lambda: agent_extra),
],
)

When the agent is checkpointed, pass the same extra_ref that points to AgentContext.extra so todo state is persisted and restored across sessions:

from cubepi import Agent
from cubepi.checkpointer import PostgresCheckpointer
from cubepi.middleware import TodoListMiddleware

# extra_ref must return the same object as AgentContext.extra.
# The helper below is the standard pattern with a checkpointed agent.
ctx_holder: dict[str, dict] = {}

def extra_ref() -> dict:
return ctx_holder.setdefault("extra", {})

agent = Agent(
model=provider.model("claude-sonnet-4-6"),
checkpointer=PostgresCheckpointer(...),
thread_id="conv_123",
middleware=[
TodoListMiddleware(extra_ref=extra_ref),
],
)

In practice the simplest pattern is to pass lambda: agent.state.extra — but agent.state is only valid after the agent is constructed, so a late-binding lambda or a shared dict reference both work.

The write_todos tool

The tool accepts a single todos list. Each item has:

  • content — a short task description.
  • status — one of "pending", "in_progress", or "completed".

The model replaces the entire list on every call. The middleware validates the payload and rejects calls that would leave the list in an inconsistent state:

  • Empty content strings are rejected.
  • Exactly one item must be "in_progress" unless all items are "completed".
  • Calling write_todos more than once in a single turn is rejected; the list rolls back to its pre-turn state.
  • An empty list is only accepted when all prior items were already completed.

Finalization guard

When the model delivers a plain-text response (no tool calls) while unfinished items remain in the list, the middleware injects a correction message and loops the model back for one extra turn to update the checklist. After that forced turn the run proceeds normally regardless of what the model does.

This prevents the common pattern where a model completes work but forgets to mark items as done before responding.

Stale-todo reminder

If the model makes several tool calls in a row without touching write_todos, the middleware injects a soft reminder asking it to sync the list. The model is free to ignore the reminder; it is never a hard block.

The threshold is 5 consecutive non-write_todos turns, with a minimum of 5 turns between successive reminders.

Customizing the tool description and system prompt

Pass tool_description and system_prompt to override the defaults:

TodoListMiddleware(
extra_ref=extra_ref,
tool_description="Maintain a checklist of steps for the current task.",
system_prompt="## Task tracking\nUse write_todos for all multi-step work.",
)

tool_description is the text the model sees in its tool list; system_prompt is appended to the agent's system prompt by the transform_system_prompt hook.

State layout in ctx.extra

All state is stored under well-known keys in AgentContext.extra:

KeyTypeDescription
todoslist[Todo] | NoneCurrent checklist
todo_guard_retriesdictPer-guard retry counters
todo_guard_blockedTodoGuardBlocked | NoneActive guard escalation payload
todo_guard_suppressedboolGuard suppression flag after a blocked episode
todo_stale_iterationsintTurns since last write_todos call
todo_finalization_correctionbool | NoneWhether a finalization correction was injected this turn

These keys are stable across versions. Checkpointers persist them as part of ctx.extra, so a resumed session starts with the same checklist the model left.

When not to use it

Skip TodoListMiddleware for short or conversational agents — the tool description and system prompt instructions consume tokens on every turn. The tool is also self-governed: the model decides whether and when to call it. For workflows where you need guaranteed structured output at each step, consider explicit tool definitions instead.