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
- Split the plan: keep the todo list to at most 20 items and delete completed/irrelevant entries before adding new ones
- Replace, don't append: since update() takes the full list each time, drop finished items in the same call that adds new ones
- 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
- Prune completed items in the same update that adds new ones — the list is replaced wholesale each call
- Plan in phases (<=20 coarse items) rather than exhaustive micro-steps
- Track list length client-side before calling update
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
- Item {item_id}: text required
- Only one task can be in_progress at a time
- Item {item_id}: invalid status '{status}'
- Path escapes workspace: {p}
- Path escapes workspace: {p}
AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14).
Data as JSON: /api/errors/3eae2bbc7cf47474.
Report an issue: GitHub.