5 个工具、3 个状态,撸出一个生产级任务引擎
🎯 学习目标
- todo_write 和 JSON Task DAG 有什么不同?为什么不合并?
- 三个状态是什么?为什么没有第四个 blocked 状态?
- 状态机允许哪些单向推进?有没有 update/delete/unclaim/回退?
- Task 文件存哪?为什么锁的是整张图不是单个文件?
- claim 的并发唯一认领是怎么保证的?
- 进程重建后 in_progress 任务怎么处理?
🧠 核心概念
1 · TODO 与 Task 不是一回事 ▸
| 维度 | todo_write | JSON Task DAG |
|---|---|---|
| 粒度 | 当前工作步骤快照 | 可独立认领/完成的项目任务 |
| 所有者 | 单个 Agent session | 当前 workspace |
| 存储 | 进程内存 | .agent_tutorial/.tasks/{uuid}.json |
| 依赖 | 无 | blocked_by 有向边 |
| 重建 | 新会话为空 | 从磁盘严格恢复 |
P12 仍保留 todo_write。模型用 TODO 拆「怎样完成当前任务」,用 Task DAG 管「整个项目哪些先做、哪些被阻塞」。P06 的 task(一次性 subagent)也不等于项目 Task,三种概念不同。
2 · 严格模型:3 状态,无 blocked ▸
状态只有三个:pending、in_progress、completed。Task 是构造时校验并冻结的领域对象。
不变量:pending 必须 owner===null;in_progress 和 completed 必须有非空 owner。id 必须是 canonical UUID 且文件名与 payload ID 完全相同;blocked_by 每个值都是 canonical UUID 且不重复;额外字段/未知状态/坏 UTF-8/坏 JSON 都明确失败。
3 · 状态机:单向推进,无回退 ▸
pending --claim--> in_progress --complete--> completed
当前章节没有 update、delete、unclaim 或自动回退。进程重建后,in_progress 仍保持原状态和 owner,不猜测未知副作用是否可以重放。complete_task 返回本次直接解锁的 unblocked 列表——只往下一层,且不改写下游文件:「可以开始了」是算出来的,不是写进文件的。
4 · 路径与文件 ▸
workspace/
.agent_tutorial/
.tasks.lock
.tasks/
1b73c624-....json
9d59f5ec-....json
一个文件一个 Task,UTF-8、稳定字段顺序、结尾换行。外部传入的 task ID 先解析成 canonical UUID 再映射文件名——../outside、绝对路径、UUID 替代拼写都无法到文件系统。JsonTaskStore 还验证 .agent_tutorial 和 .tasks 解析结果仍在 workspace 内,目录 symlink/junction 指向外时创建前就失败。
5 · 为什么锁的是整张图 ▸
claim/complete 需要读依赖状态:一个任务能否 claim 取决于它所有 blocked_by 是否 completed。如果只锁单个文件,两个 Agent 同时认领同一任务、或同时改依赖和被依赖任务时会产生不一致。
整图锁:锁内重载全图 → 全图校验(环、缺边、ID 碰撞)→ 条件迁移 → 原子替换。这保证 claim 的并发唯一认领:两个竞争者都依次进入临界区,第二个看到状态已是 in_progress,输家拿到 TaskStateError(task_invalid_state)而非锁冲突错误——锁的作用是让「读→判断→写」决策不失效。原子写用临时文件+sync+rename,读方看不到半写文件;但原子写只保证「不写坏」不保证「不覆盖」,互斥必须靠锁。
6 · 5 个工具 ▸
五个任务工具使用带动词的完整名称(避免与 P06 的 task 混淆):
| 工具 | 作用 |
|---|---|
| create_task | 创建任务(可带 blocked_by) |
| list_tasks | 列出任务(可按状态过滤) |
| claim_task | 认领一个 ready(pending 且依赖全 completed)任务 |
| complete_task | 完成自己认领的任务 |
| get_task | 查询单个任务详情 |
主/子 Agent 共享同一任务图(同一 TaskStore),所以子 Agent 也能认领任务。但子 Agent 工具集仍不含 task(递归委派禁止)。owner 不能由模型参数伪造:claim_task 的 schema 只有 task_id,owner 一律取 ToolContext.identity(运行时缺省 "user");额外字段在副作用前就被 strict schema 拒绝。