源码范围: 本节已按当前本地仓库的 s05_todo_write/code.py 核对。图 1 复用博客 S04 总图的结构,标出本轮将扩展的旧接口;本节介绍教学实现,不将固定三轮提醒等设计视为现代 Claude Code 的内部规则。

我想弄清楚的问题

工具和 Hook 已经能帮助 Agent 执行与检查行动,但它做了几步之后,仍可能被眼前的工具结果带偏,忘记还有哪些工作没有完成。

S05 增加 todo_write,让模型把计划与进度变成显式状态。TODO 记录要做什么、做到哪里;真正的工作仍由原工具完成。

整体过程:从 S04 到 S05

先认出上一轮,再看这一轮的变化,最后单独理解 TODO 核心。图 1 保留 S04 的结构,橙色标出本轮将扩展的工具表与分发接口,仍展示旧实现;图 2、图 3 展示蓝色保留、橙色改造后的接口、紫色新增机制。本节没有真实移除的功能。

1. 上一轮:S04 在原循环中触发 Hook

上一轮整体流程:本章改造前的已有系统蓝色已有;橙色是本轮将改造的旧模块,图中仍展示旧实现。上一轮整体流程:本章改造前蓝色已有;橙色是本轮将改造的旧模块,图中仍展示旧实现。对话历史问题与工具结果请求模型SYSTEM + TOOLS判断响应是否 tool_use分发并执行handlers[name]回传结果按调用 ID 配对TOOLS五个已有工具定义用户输入原有输入步骤本轮结束原有结束动作权限回调None / 拒绝说明UserPromptSubmit已有输入事件PreToolUse已有执行前事件日志回调已有观察行为注册回调拒绝:跳过执行与后置事件PostToolUse已有执行后事件Stop已有退出判断事件反馈追加到历史原回传循环 → 直接回到历史文件工具实现read / write / edit / glob
颜色与线型
  • 蓝色:已有且保留
  • 橙色:本轮将改造(仍是旧实现)
  • 紫色:真正新增(本图未用)
  • 红色虚线:本轮将移出(仍是旧流程)(本图未用)
  • 灰色实线框:存储/辅助节点(本图未用)
  • 灰色虚线框:细节合并,功能保留(本图未用)
  • 灰字:来源/补充说明
  • 灰色虚线:关联,不是执行回环
  • 实线箭头:流程/数据方向
图 1:保留 S04 的结构;橙色工具表和分发接口是本轮将扩展的旧部分,其他功能蓝色。不补画 SYSTEM、TODO 或提醒计数器。左右滑动查看全图。

输入、执行前后和结束时的 Hook 已经存在,但没有专门管理计划的工具。

2. 这一轮:把计划更新接入同一套工具循环

图 2 · S05:计划更新仍走原工具循环蓝色保留 Hook;橙色扩展提示与工具接口;紫色新增 TODO 和提醒。图 2 · S05:计划更新仍走原工具循环蓝色保留 Hook;橙色扩展提示与工具接口;紫色新增 TODO 和提醒。对话历史问题与工具结果请求模型SYSTEM + TOOLS判断响应是否 tool_use分发并执行handlers[name]回传结果按调用 ID 配对TOOLS工具定义 + TODO用户输入沿用 S04 入口本轮结束原有结束动作权限回调None / 拒绝说明UserPromptSubmit保留输入事件PreToolUse保留执行前事件日志回调保留观察行为注册回调拒绝:跳过执行与后置事件PostToolUse保留执行后事件Stop保留退出判断事件反馈追加到历史回传前检查本轮是否使用 TODOSYSTEM原有 · 增加规划计数与条件提醒连续三轮未用 TODO → 附加 remindertodo_write新增工具实现TodoManager校验 · 内存状态分发表新增这一实现渲染进度 → 普通工具结果文件工具实现read / write / edit / glob
颜色与线型
  • 蓝色:已有且保留
  • 橙色:修改/职责迁移
  • 紫色:真正新增
  • 红色虚线:真实移除(本图未用)
  • 灰色实线框:存储/辅助节点(本图未用)
  • 灰色虚线框:细节合并,功能保留(本图未用)
  • 灰字:来源/补充说明
  • 灰色虚线:关联,不是执行回环
  • 实线箭头:流程/数据方向
图 2:蓝色保留 S04 流程;橙色工具表、分发表与 SYSTEM 被扩展。紫色 TODO 分支管理内存状态,紫色计数与提醒接在结果回传之前。灰色虚线表示实现关联,不是另一条执行循环。左右滑动查看全图。
位置S04S05
请求模型通用 SYSTEM、五个工具SYSTEM 增加规划要求,TOOLS 增加 todo_write
分发执行按工具名找到 handler分发表增加 TODO handler,原执行方式保留
计划状态没有专门的 TODO 管理器新增内存中的 TodoManager.items
回传结果工具输出组成 tool_result记录本轮是否使用 TODO,必要时将提醒附在同一结果消息中
输入、权限、执行后、结束S04 的 Hook 机制继续保留,不用 TODO 替代

SYSTEM 原先就参与模型请求,只是没有单独画框。本轮展开它并用橙色表示内容修改。用户输入已经在 S04 画出,本轮保留蓝色,不再标为“本轮展开”。

蓝色文件工具实现继续保留在原位置;橙色分发框表示映射表增加了 todo_write,按名称查找 handler 的方式仍沿用。新增的 TODO handler 在下方单独展开,工具定义与本地实现各有清楚的归属。

3. 最小核心:校验整份清单,替换状态,回传进度

图 3 · TODO 核心:更新状态与条件提醒模型用普通工具调用提交整份清单,管理器校验后替换内存状态,结果回到历史。下方计数器决定是否附加提醒。图 3 · TODO 核心:更新状态与条件提醒先完整校验再覆盖;工具返回进度,下一次模型请求读到更新。模型提交整份 todos 列表todo_write沿用工具分发校验清单20 项 / 单项进行中替换状态TODO.items回传历史普通 tool_result工具轮次本轮 used_todo维护计数器用过 TODO 清零;否则加一达到三轮则附加提醒与工具结果一起回传;计数清零蓝色是原循环的调用与回传;紫色是新增的计划状态和提醒机制。校验失败保留旧清单;提醒不强制更新,进度标记不等于实际完成。
颜色与线型
  • 蓝色:已有且保留
  • 橙色:修改/职责迁移(本图未用)
  • 紫色:真正新增
  • 红色虚线:真实移除(本图未用)
  • 灰色实线框:存储/辅助节点(本图未用)
  • 灰色虚线框:细节合并,功能保留(本图未用)
  • 灰字:来源/补充说明
  • 灰色虚线:关联,不是执行回环(本图未用)
  • 实线箭头:流程/数据方向
图 3:TODO 更新走普通工具调用路径;下方计数器只决定是否附加提醒。计划不会自动执行,状态也不会自动推进。左右滑动查看全图。

这里有两条配合的机制:模型主动更新清单;Harness 在连续未使用 TODO 的工具轮次后提醒模型。

先让模型知道:什么时候计划,参数怎么传

SYSTEM 增加规划要求:

SYSTEM = (
    f"You are a coding agent at {WORKDIR}. "
    "Before starting any multi-step task, use todo_write to plan your steps. "
    "Update status as you go."
)

这是引导,不是一个阻止“没有计划就执行”的硬门禁。源码还有环境说明,此处省略与规划无关的部分。

工具的输入 schema 则定义清单形状,下面保留当前实现的关键约束:

todo_tool = {
    "name": "todo_write",
    "description": "Create and manage a task list for your current coding session.",
    "input_schema": {
        "type": "object",
        "properties": {
            "todos": {
                "type": "array",
                "maxItems": 20,
                "items": {
                    "type": "object",
                    "properties": {
                        "content": {"type": "string", "minLength": 1},
                        "status": {
                            "type": "string",
                            "enum": ["pending", "in_progress", "completed"],
                        },
                    },
                    "required": ["content", "status"],
                },
            },
        },
        "required": ["todos"],
    },
}

todos 是整份列表,最多 20 项;每项都有任务描述和三选一的状态。示例参数:

{
  "todos": [
    {"content": "检查现有模块", "status": "completed"},
    {"content": "实现所需改动", "status": "in_progress"},
    {"content": "运行测试", "status": "pending"}
  ]
}

然后加入原有工具表与分发表:

TOOLS.append(todo_tool)
TOOL_HANDLERS["todo_write"] = run_todo_write

模型决定调用哪个工具,原 handler 分发机制负责执行。TODO 没有专属的模型循环。

管理器保存什么,怎样接受更新

当前实现用 TODO = TodoManager() 保存内存状态。下面是其主要校验与替换逻辑的教学摘录,省略字符串兼容解析和部分错误信息:

class TodoManager:
    def __init__(self):
        self.items = []

    def update(self, todos):
        if not isinstance(todos, list):
            raise ValueError("todos must be a list")
        if len(todos) > 20:
            raise ValueError("Max 20 todos allowed")

        validated = []
        active = 0
        for todo in todos:
            if not isinstance(todo, dict):
                raise ValueError("Each todo must be an object")
            content = str(todo.get("content", "")).strip()
            status = str(todo.get("status", "pending")).lower()
            if not content:
                raise ValueError("Content must not be empty")
            if status not in ("pending", "in_progress", "completed"):
                raise ValueError("Invalid status")
            active += status == "in_progress"
            validated.append({"content": content, "status": status})

        if active > 1:
            raise ValueError("Only one todo can be in_progress at a time")

        self.items = validated
        return self.render()

先校验全部条目,再替换旧列表。 更新失败时,旧计划保留;更新成功时,整份清单被覆盖,模型可以同时调整步骤与状态。

原实现还接受字符串形式的列表:先尝试 json.loads,再尝试 ast.literal_eval,没有使用 eval。这是 handler 的兼容处理,schema 仍要求数组。

Schema 与运行时检查的区别

约束Schema管理器
输入为列表、每项为对象描述类型显式检查解析后的类型
最多 20 项maxItems: 20检查长度
描述不能为空minLength: 1去掉首尾空白后检查;也会转换成字符串
状态合法必需字段、枚举默认 pending,转小写后检查
最多一项进行中未表达跨条目约束统计 in_progress 数量并拒绝超限

两者并非完全相同:schema 是模型应该提交的接口约定,handler 还有自己的校验与兼容行为。不能仅凭 schema 就推断本地会怎样处理所有输入。

当前实现允许空列表;也不强制状态只能按 pending → in_progress → completed 转换。它是一份可覆盖的计划快照,还不是带依赖关系、自动调度和状态迁移规则的任务系统。

进度怎样回到模型

管理器把清单渲染成文本,handler 将其打印到终端并返回:

TODO = TodoManager()

def run_todo_write(todos):
    try:
        output = TODO.update(todos)
    except ValueError as error:
        return f"Error: {error}"
    print(output)  # 此处省略终端标题与颜色
    return output

返回结果类似:

[x] 检查现有模块
[>] 实现所需改动
[ ] 运行测试

(1/3 completed)

这个文本沿普通 tool_result 路径加入消息历史,所以模型能在后续请求中看到进度。源码没有在每次请求前把整个 TODO 列表重新注入 SYSTEM。

本节也没有将清单写入 .tasks/current_todos.json 或自动加载文件。状态存在当前进程的管理器中,不能据此认为它已经实现跨进程或跨会话持久化。

提醒插在回传前,而不是另开一个循环

循环每轮记录是否实际进入了 todo_write 的 handler 路径:

rounds_since_todo = 0 if used_todo else rounds_since_todo + 1
if rounds_since_todo >= 3:
    results.append({
        "type": "text",
        "text": "<reminder>Update your todos.</reminder>",
    })
    rounds_since_todo = 0

messages.append({"role": "user", "content": results})

一次模型响应有多个工具调用,仍只算一个工具轮次;没有工具调用的响应走 Stop 分支,不计入这里。

连续三轮没有使用 TODO 时,第三轮结果消息里同时包含 tool_result 与提醒文本,随后计数清零。下一次模型请求自然会读到它。模型仍需主动决定是否更新计划。

源码按工具名标记“使用过 TODO”,不检查更新是否成功:handler 返回校验错误,也会清零;若在 PreToolUse 阶段被拒绝,则不会标记为已使用。这个计数器记录的是调用路径,不是成功更新次数。

把 S05 串回已有 Agent Loop

下面是按当前源码控制流程整理的伪代码,省略 API 参数和结果字典构造细节:

# 输入阶段:沿用 S04
trigger_hooks("UserPromptSubmit", query)
messages.append(user_message(query))

rounds_since_todo = 0  # 每次进入 agent_loop 重新开始
while True:
    response = ask_llm(SYSTEM, TOOLS, messages)
    messages.append(assistant_message(response))
    calls = extract_tool_calls(response)

    if not calls:
        feedback = trigger_hooks("Stop", messages)
        if feedback:
            messages.append(user_message(feedback))
            continue
        break

    results = []
    used_todo = False                 # S05 新增:本轮是否使用 TODO
    for call in calls:
        blocked = trigger_hooks("PreToolUse", call)
        if blocked:
            results.append(tool_result(call.id, blocked))
            continue

        output = dispatch_with_error_capture(call)
        # todo_write -> TODO.update();其他工具 -> 原有实际工作
        trigger_hooks("PostToolUse", call, output)
        if call.name == "todo_write":
            used_todo = True          # 不等于更新成功
        results.append(tool_result(call.id, output))

    rounds_since_todo = 0 if used_todo else rounds_since_todo + 1
    if rounds_since_todo >= 3:        # S05 新增:回传前附加提醒
        results.append(reminder_text("Update your todos."))
        rounds_since_todo = 0

    messages.append(user_message(results))
    # 原有回环:把工具结果与可能的提醒一起交给模型

新增部分集中在三个位置:请求前的工具与提示定义、分发中的 TODO handler、结果回传前的计数与提醒。 S04 的 Hook 仍处于原来的调用时机。

当前的一句话理解

TodoWrite 让模型显式管理计划与进度,再借原有工具回环读到自己的更新;它记录工作,不执行工作,也不证明工作已经完成。

源码对照:S05 教学实现。