shareAI-lab/learn-claude-code · warning · ValueError

Max 20 todos allowed

Error message

Max 20 todos allowed

What it means

Raised by TodoManager.update() in agents/s03_todo_write.py:58 when the model passes a todo list longer than 20 items to the todo tool. The 20-item cap is a hard validation limit that exists to keep the todo state (which is re-rendered into context on every update) from bloating the conversation. It aborts the entire update, not just the excess items.

Source

Thrown at agents/s03_todo_write.py:58

    os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)

WORKDIR = Path.cwd()
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
MODEL = os.environ["MODEL_ID"]

SYSTEM = f"""You are a coding agent at {WORKDIR}.
Use the todo tool to plan multi-step tasks. Mark in_progress before starting, completed when done.
Prefer tools over prose."""


# -- TodoManager: structured state the LLM writes to --
class TodoManager:
    def __init__(self):
        self.items = []

    def update(self, items: list) -> str:
        if len(items) > 20:
            raise ValueError("Max 20 todos allowed")
        validated = []
        in_progress_count = 0
        for i, item in enumerate(items):
            text = str(item.get("text", "")).strip()
            status = str(item.get("status", "pending")).lower()
            item_id = str(item.get("id", str(i + 1)))
            if not text:
                raise ValueError(f"Item {item_id}: text required")
            if status not in ("pending", "in_progress", "completed"):
                raise ValueError(f"Item {item_id}: invalid status '{status}'")
            if status == "in_progress":
                in_progress_count += 1
            validated.append({"id": item_id, "text": text, "status": status})
        if in_progress_count > 1:
            raise ValueError("Only one task can be in_progress at a time")
        self.items = validated
        return self.render()

View on GitHub (pinned to 985456f4ad)

Solutions

  1. Split the plan: keep the todo list to at most 20 items and delete completed/irrelevant entries before adding new ones
  2. Replace, don't append: since update() takes the full list each time, drop finished items in the same call that adds new ones
  3. If the task genuinely needs more than 20 steps, group related steps into one todo item with sub-bullets in the text

Example fix

# before
todo.update([{ "text": f"step {i}" } for i in range(30)])
# ValueError: Max 20 todos allowed

# after
 todo.update([{ "text": f"phase {i}: ..." } for i in range(6)])  # 6 coarse phases
Defensive patterns

Strategy: validation

Validate before calling

items = [i for i in items if str(i.get("text", "")).strip()]  # drop empties first
items = items[:20]  # or trim oldest completed entries
assert len(items) <= 20, f"{len(items)} > 20; prune completed items first"

Type guard

from typing import Any

def is_valid_todo_batch(items: Any) -> bool:
    return (
        isinstance(items, list)
        and len(items) <= 20
        and all(
            isinstance(i, dict)
            and str(i.get("text", "")).strip()
            and str(i.get("status", "pending")).lower() in ("pending", "in_progress", "completed")
            for i in items
        )
    )

Try / catch

try:
    TODO.update(items)
except ValueError as e:
    if "Max 20" in str(e):
        # keep last known-good list, retry with pruned items
        pruned = [i for i in items if i.get("status") != "completed"][:20]
        return TODO.update(pruned) if pruned else TODO.render()
    raise

Prevention

When it happens

Trigger: The LLM calls todo_update with an `items` array of 21+ entries — typically when it plans a very large task up front, or when it re-submits a growing list each turn and the list crosses 20. Because update() replaces the whole list each call, one oversized submission fails wholesale and the previous list is kept.

Common situations: Over-planning models that decompose a big job into 30 micro-steps in one shot. Cumulative lists where completed items are never pruned. Agent loops that retry the identical oversized payload after the error instead of shrinking it.

Related errors


AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14). Data as JSON: /api/errors/3eae2bbc7cf47474. Report an issue: GitHub.