TodoListMiddleware

TodoListMiddleware

A LangChain AgentMiddleware that gives agents a structured, persistent task list during complex multi-step operations. DeerFlow 2.0 extends it with two additional robustness layers.


Base class (LangChain)

State extension

PlanningState ⊃ AgentState + { todos: list[Todo] }

Todo = { content: str, status: "pending" | "in_progress" | "completed" }

write_todos tool

The sole interface for agents to manipulate the list. Key properties:

When agents should use it

Use it Skip it
Complex objective, ≥ 3 steps Single straightforward task
Explicit multi-phase plan Trivial / < 3 steps
User explicitly requested a plan

Task lifecycle discipline

pending → in_progress   (mark before starting the step)
in_progress → completed (mark immediately on completion — not "soon")

System prompt injection

wrap_model_call appends these rules to every model request before the call reaches the graph. No state field is used; the injection is purely at the model-call boundary.


DeerFlow extension (TodoMiddleware)

DeerFlow subclasses TodoListMiddleware to handle two failure modes that the base class ignores.

Failure mode 1 — Context loss

Problem: When summarization or compaction truncates the message history, the original write_todos call disappears. The model no longer knows it has a todo list.

Fix — before_model hook:

if todos in state
 AND no write_todos visible in messages
 AND no todo_reminder already injected:
    inject HumanMessage(name="todo_reminder")
      → <system_reminder> with formatted todo list

The todo_reminder message is idempotent (checked before injection) and survives further turns as long as it stays in the window.

Failure mode 2 — Premature exit

Problem: The model produces a final response (no tool calls) while incomplete todos remain — it declares victory too early.

Fix — after_model hook (decorated @hook_config(can_jump_to=["model"])):

if last AIMessage has no tool_calls
 AND todos exist with status != "completed"
 AND completion_reminder_count < 2:
    inject HumanMessage(name="todo_completion_reminder")
    return {"jump_to": "model", "messages": [reminder]}

The _MAX_COMPLETION_REMINDERS = 2 cap prevents an infinite retry loop if the model is genuinely stuck.

Hook execution order in DeerFlow

before_model
  └─ TodoMiddleware.before_model   ← context-loss check / inject reminder
model
after_model
  └─ TodoMiddleware.after_model
       └─ super().after_model()    ← parallel write_todos guard (base class)
       └─ DeerFlow premature-exit check

Reminder message anatomy

Name Direction Trigger
todo_reminder HumanMessage before_model: write_todos not visible in window
todo_completion_reminder HumanMessage after_model: final response but incomplete todos

Both use <system_reminder> XML tags in the content so the model can distinguish injected control messages from user content.