HARNESS 107

Task System

"大目标拆小任务,排好序,记在磁盘上"

s03 的 TodoWrite 是便签纸——写完随手贴,风一吹就没了。
s07 的 Task System 是项目管理本——写在纸上,合上本子明天还能翻到。

1

先讲一个故事:装一个宜家书柜

1

你买了一个宜家书柜。打开包装,里面有 5 个步骤 的说明书:

📦 第 1 步:组装框架
🔩 第 2 步:装上隔板(需要第 1 步完成
🚪 第 3 步:装柜门(需要第 1 步完成
🔒 第 4 步:装门把手(需要第 3 步完成——门没装好,把手没地方拧)
第 5 步:检查所有螺丝(需要第 2 步和第 4 步都完成

你一看就明白了:第 2 步和第 3 步可以同时做(都只依赖第 1 步,互不依赖)。但第 4 步必须等第 3 步完成。第 5 步必须等第 2 和第 4 步都完成。

2

现在想象:这个说明书不是画在纸上的,而是写在 5 张便签纸上——每张写一个步骤,没有编号,没有箭头。你把它们贴在墙上。

问题来了:你怎么知道哪步先做?哪步后做?哪两步可以同时做?

你只能靠脑子猜。如果猜错了——比如先做第 4 步(装门把手)——你会发现门还没装,白费功夫。

3

这就是 s03(TodoWrite)和 s07(Task System)的区别。

s03 = 便签纸:每个任务独立写在一张纸上,贴在墙上。顺序和依赖关系全靠你(Agent)的脑子。

s07 = 说明书:每个任务有编号、有依赖箭头、有完成状态。系统帮你记住了"哪步要等哪步"——你不需要记,看说明书就行。

2

s03 的 Todo 为什么不够用?

先理解 s03 的 Todo 长什么样,然后看它在哪些场景下会出问题。

s03 的 Todo 就是一张"待办清单"

你见过手机上的备忘录吧?s03 就是那个:

[ ] 搭建项目骨架
[>] 实现登录功能 ← 正在做
[ ] 写单元测试

这张清单有三个特点:

① 存在脑子里(内存里):关掉程序,清单就没了。就像你把待办事项记在心里——忘了就是忘了。

② 每一项都是平等的:看不出来"写测试"依赖"实现登录"——它们是并列的。就像三张便签纸贴在墙上,没有箭头连接。

③ 只有做没做两种状态:pending(还没做)、in_progress(正在做)、completed(做完了)。没有"我在等别人先做完"这种状态。

三个具体的问题场景

场景一 —— 关掉程序再打开

你今天用 Agent 建了一个项目,分解成 5 个任务,完成了 2 个。明天打开 Agent 继续——它完全不记得昨天做过什么了。Todo 列表是从零开始的。

场景二 —— 任务 B 必须等 A 做完

"写单元测试"必须等"实现登录功能"完成。但 Todo 列表看不出来这种关系。Agent 可能同时做这两件事——然后发现测试写好了但登录代码还没写,测试当然全失败。

场景三 —— Agent 对话太长,被压缩了

s06 的压缩机制一跑,Todo 列表就没了——它在内存里,不在磁盘上。Agent 的"记忆"被重置了。

🗂️ 生活类比

s03 的 Todo = 你脑子里记的待办事项。洗个澡可能就忘了。

s07 的 Task System = 你写在笔记本上的待办事项。合上本子、过一周再翻开——还在。

而且 s07 的笔记本不光记"要做什么",还记"这件事要等那件事做完才能开始"——像宜家说明书的箭头。

3

s07 做了什么改变?

s07 做了两件事,每件都很简单,但合在一起就完全不一样了。

s03 的做法

存储位置:存在 Python 程序的内存里。
程序关了 → 没了。

任务关系:没有关系。
每个任务是孤立的。

任务格式:一条文本 + 一个状态标记。
"实现登录" [pending]

s07 的做法

存储位置:存在硬盘上,一个任务一个文件。
程序关了 → 文件还在。

任务关系:每个任务可以写"我要等谁做完"。
blockedBy: [1, 3]

任务格式:更丰富的信息。
标题 + 描述 + 状态 + 依赖列表 + 谁在做

"改变一":把任务写到硬盘上,而不是记在脑子里

s07 里每个任务是一个独立的 JSON 文件,存在 .tasks/ 文件夹下。JSON 是一种人类也能读懂的文本格式——你可以双击打开看。

下面是一个真实的任务文件内容:

📝 中文注释版
{ ← 一个花括号 = 一个任务 "id": 3, ← 编号:第 3 号任务 "subject": "实现密码哈希", ← 要做什么(标题) "description": "用bcrypt加密",← 详细说明(可选) "status": "pending", ← 还没开始做 "blockedBy": [1], ← ⭐ 依赖任务1,它完成了我才能开始 "owner": "" ← 谁在做(s09多Agent时用到) }
💻 英文代码原版
{ "id": 3, "subject": "Implement password hashing", "description": "Use bcrypt to hash user passwords", "status": "pending", "blockedBy": [1], "owner": "" }

保存到磁盘的关键好处:s06 的压缩不会影响它。Agent 重启后,扫一眼 .tasks/ 文件夹就知道所有任务的状态。就像你合上笔记本,第二天翻开——内容还在。

"改变二":任务之间可以写"我等谁"

s03 的每个任务彼此独立。s07 加了一个关键信息:"我等谁"(blockedBy)

#1 建框架
已完成 ✓
↓ 第1步完成,解锁了两个任务
#2 装隔板
正在做 →
等谁:[1]
#3 装柜门
正在做 →
等谁:[1]
↑ 这两个互不等对方——它们可以同时做
↓ 两个都完成才解锁下一步
#4 装把手
等待中...
等谁:[3]

关键理解:任务 2 和任务 3 为什么能同时做?不是因为有人告诉它们"你们可以并行"——而是因为它们都不在对方的"等谁"列表里。没有依赖 = 可以并行。这是一个非常自然的表达方式。

4

任务怎么存到磁盘?

不需要数据库,不需要安装任何东西。就是一个文件夹,里面放一堆 JSON 文件。

文件夹长这样

.tasks/ ← 一个叫 .tasks 的文件夹 ├── task_1.json ← 第 1 号任务 ├── task_2.json ← 第 2 号任务 ├── task_3.json ← 第 3 号任务 └── task_4.json ← 以此类推...

文件命名规则很简单:task_编号.json。编号是自动递增的——你创建第 1 个任务它是 1,第 2 个是 2,第 3 个是 3……

为什么每个任务是一个文件,而不是所有任务塞一个文件? 因为改一个任务时只需要写那一个文件。如果 50 个任务塞一个文件,你改第 23 号任务的状态,要重新写整个文件——慢而且容易出错。

程序怎么操作这些文件?

代码做四件事(专业术语叫 CRUD——创建、读取、更新、删除):

① 创建任务:程序给任务分配一个编号(比已有最大编号 +1),写好标题、状态、依赖等信息,写入一个新文件。

② 读取单个任务:程序根据编号找到对应的文件,读出来。

③ 更新任务:程序找到文件,读出来,修改里面的字段(比如把状态从 pending 改成 completed),再写回去。

④ 列出所有任务:程序扫描 .tasks/ 文件夹下的所有 task_*.json 文件,读取每个,整理成列表。

不需要担心并发冲突——目前只有一个 Agent 在操作这些文件。后面 s09 多个 Agent 同时操作时,才会涉及"谁先改谁后改"的问题。

核心代码:怎么创建和更新任务

📝 中文注释版
# 创建任务:填好信息 → 写入文件 → ID 自增 def create(self, subject, description=""): task = { "id": self._next_id, # 自动分配编号 "subject": subject, # 任务标题 "description": description, # 详细描述(可选) "status": "pending", # 新任务默认"还没开始" "blockedBy": [], # 初始不依赖任何人 "owner": "", } self._save(task) # 写入 task_{id}.json self._next_id += 1 # 编号+1 return json.dumps(task) # 返回给 LLM 看 # 更新任务:读文件 → 改字段 → 写回去 # ⭐ 标为 completed 时自动触发 _clear_dependency def update(self, task_id, status=None, add_blocked_by=None, remove_blocked_by=None): task = self._load(task_id) # 读文件 if status: task["status"] = status # 改状态 if status == "completed": # ⭐ 完成了 → 自动解锁下游 self._clear_dependency(task_id) if add_blocked_by: # 添加依赖 task["blockedBy"] = list(set(task["blockedBy"] + add_blocked_by)) if remove_blocked_by: # 移除依赖 task["blockedBy"] = [x for x in task["blockedBy"] if x not in remove_blocked_by] self._save(task) # 写回文件
💻 英文代码原版
def create(self, subject, description=""): task = { "id": self._next_id, "subject": subject, "description": description, "status": "pending", "blockedBy": [], "owner": "", } self._save(task) self._next_id += 1 return json.dumps(task, indent=2) def update(self, task_id, status=None, add_blocked_by=None, remove_blocked_by=None): task = self._load(task_id) if status: task["status"] = status if status == "completed": self._clear_dependency(task_id) if add_blocked_by: task["blockedBy"] = list(set(task["blockedBy"] + add_blocked_by)) if remove_blocked_by: task["blockedBy"] = [x for x in task["blockedBy"] if x not in remove_blocked_by] self._save(task)

注意 updateif status == "completed" 那行——标为完成时自动调用 _clear_dependency。Agent 只需说"我做完了",系统自动通知所有下游。

5

最巧妙的设计:完成一件事,自动解锁等它的人

这是 s07 最聪明的地方。不是让 Agent 手动去通知"我做完了,你们可以开始了"——而是系统自动帮你做这件事。

1

没有自动解锁的话,Agent 要这样做:

① 把任务 #1 标记为"完成"
② 去检查任务 #2:它等 #1 吗?是 → 把 #1 从 #2 的"等谁"列表里删掉
③ 去检查任务 #3:它等 #1 吗?是 → 把 #1 从 #3 的"等谁"列表里删掉
④ 去检查任务 #4:它等 #1 吗?...
⑤ 去检查任务 #5:...
做完一个任务,还要挨个通知所有"等它的人"。又麻烦又容易漏。

2

s07 的做法:Agent 只需要做一件事——把 #1 标为"完成"。

系统自动做剩下的:扫描所有任务文件,找到所有"等谁"列表里包含 #1 的任务,把 #1 从它们的列表里删掉。

Agent 完全不用操心"通知下游"这件事。它只需要说"我做完了",系统自动通知所有等它的人。这就像一个连锁反应——你推倒了第一块多米诺骨牌,后面的事情自动发生。

一个具体例子,走一遍

假设有三个任务:#1 搭建框架(已完成)→ #2 实现登录(等 #1)→ #3 写测试(等 #2)。

#1
已完成 ✓
#2
正在做
等谁:[1]
#3
等待中
等谁:[2]

Agent 把 #2 标为"完成"时,代码层面发生了什么:

📝 中文注释版
# Agent 调用 task_update(task_id=2, status="completed") # ↓ update() 内部检测到 status=="completed",自动触发: def _clear_dependency(self, completed_id): # 扫描 .tasks/ 下所有 task_*.json 文件 for f in self.dir.glob("task_*.json"): task = json.loads(f.read_text()) # 读取每个任务 # 如果这个任务的 blockedBy 里包含已完成的 ID if completed_id in task.get("blockedBy", []): task["blockedBy"].remove(completed_id) # 从依赖列表移除 self._save(task) # 写回磁盘 # 执行结果:#3 的 blockedBy 从 [2] 变成 [] → 自动解锁!
💻 英文代码原版
def _clear_dependency(self, completed_id): for f in self.dir.glob("task_*.json"): task = json.loads(f.read_text()) if completed_id in task.get("blockedBy", []): task["blockedBy"].remove(completed_id) self._save(task)

执行结果:#3 的"等谁"列表从 [2] 变成 [](空列表)。Agent 下次看任务列表时,发现 #3 不再被阻塞——可以开始做了。

Agent 从头到尾只做了一件事:把 #2 标为完成。它不需要知道谁在等它——系统替它通知了所有人。

💡 为什么用"全量扫描"而不是"反向索引"?
全量扫描 = 每次遍历所有文件找谁依赖了我。简单、容错、不需要维护额外数据。对于几十到几百个任务的规模,毫秒级完成。反向索引更快(O(1)查找),但需要额外维护一个"谁依赖谁"的映射表——复杂、容易出错。在 s07 的规模下,简单比高效更重要
6

Agent 怎么操作任务?——四个工具

s03 只有一个 todo 工具(创建和更新混在一起)。s07 拆成了四个,每个只做一件事——就像厨房里切菜刀、水果刀、面包刀各有各的用途。

四个工具,各司其职

🔨 task_create —— 创建一个新任务。告诉它"标题"(必填)和"详细描述"(可选)。系统自动分配编号,初始状态为"还没开始",初始依赖为空。

✏️ task_update —— 修改一个已有任务。可以改三样东西:① 状态(还没做 → 正在做 → 已完成);② 添加依赖(让它等人);③ 移除依赖(不再等人)。三样可以一次全改,也可以只改其中一样。

📋 task_list —— 看全局。列出所有任务,每个一行,显示编号、标题、状态、在等谁。就像看一个项目看板。

🔍 task_get —— 看详情。查看某一个任务的完整信息,包括详细描述。大部分时候用 task_list(省 token),需要看详情时才用这个。

这些工具怎么加到 Agent 里的?

和 s02 学的方式一模一样——在 dispatch map 里加四行:

# 工具名 → 执行哪个函数 "task_create" → TaskManager.创建任务() "task_update" → TaskManager.更新任务() "task_list" → TaskManager.列出所有任务() "task_get" → TaskManager.查看单个任务()

加新工具不需要改 Agent Loop——这就是 s02 学的 dispatch map 模式。从 s02 到 s07,这个模式一直没变过。

7

有了任务图,Agent 能回答三个关键问题

这三个问题是任何项目管理的核心。s03 的扁平清单一个都回答不了,s07 的任务图三个都能回答。

问题一:什么现在可以做?

task_list——状态是"还没开始"且"等谁"列表为空的任务,就是可以做的。Agent 不需要推理——直接看列表就知道。

就像你看宜家说明书:哪一步的箭头前面全是勾(已完成)?那就是下一步该做的。

问题二:什么被卡住了?

task_list——"等谁"列表不为空的任务,就是被卡住的。Agent 知道它们现在不能做,需要等。

有了这个信息,Agent 不会浪费时间尝试做被卡住的任务。

问题三:什么可以同时做?

如果有多个任务都满足"还没开始 + 等谁列表为空",它们可以并行执行——因为它们互不依赖。Agent 可以同时派多个子 Agent(s04)去处理它们。

s03 的 Agent 看不出并行机会——所有任务看起来都一样。s07 的 Agent 一眼就能识别。

这三种模式覆盖了几乎所有项目场景:
A 完成
B 可做
C 可做
D 等待
顺序执行——一个接一个
A 完成
↙ 解锁 ↘
B 可做
C 可做
并行执行——B和C可以同时开工
A 完成
B 完成
↓ 两个都完成才解锁
C 等待 (需要A和B)
汇聚等待——等所有前置都完成

🏗️ 这个模式无处不在

盖楼:打地基 → 砌墙 & 铺管道(并行)→ 两个都完成 → 封顶。
做菜:备菜 → 炒菜 & 煮饭(并行)→ 两个都完成 → 装盘上桌。
写软件:设计数据库 → 写后端 & 写前端(并行)→ 都完成 → 联调测试。
任何"分步骤、有依赖"的事情,都是一个任务图。

8

一句总结 & 三个核心认识

s07 一句话:把任务从"脑子里的便签"变成"硬盘上的说明书"——
有关键的依赖箭头,有自动的连锁解锁,压缩不掉,重启还在。
01

硬盘 > 脑子

记在磁盘上的信息比记在内存里的可靠得多。程序崩溃、压缩、重启——只要文件在,状态就在。

02

有箭头 > 没箭头

任务之间写清楚"我等谁",Agent 就知道什么能做、什么被卡、什么可并行。不需要推理,看箭头就行。

03

自动 > 手动

完成一个任务后,系统自动解锁所有等它的人。Agent 只管做事,不需要操心通知下游。

9

你能在哪些地方看到这个模式?

📋 Claude Code 的 TodoWrite

你在 Claude Code 里看到它列任务时,显示 [ ] #3: 修复登录Bug (blocked by: [1, 2])——这就是 s07 的任务图渲染格式。"blocked by" = "等谁"。

📦 GitHub Projects / Jira

GitHub 的 "Blocked by" 功能、Jira 的 "Depends on" 链接——都是任务图。每个 issue 有依赖关系,完成一个自动通知下游。

🔄 CI/CD Pipeline(自动化部署流程)

Build → Test & Lint(并行)→ Deploy。每个阶段等前面的完成。这就是一个被自动化执行的任务图。s07 的 Agent 就是"人形 CI/CD 引擎"——只不过每个阶段的"怎么执行"由 LLM 决定,而不是写死的脚本。

从 s08 开始,任务图将成为多 Agent 协作的核心。
一个 Agent 完成了一项任务 → 自动解锁了另一个 Agent 等待的任务。
多个 Agent 在同一个 .tasks/ 文件夹上有序工作——就像一个团队在看同一块白板。

s07 有了持久化任务图,但长任务会阻塞 Agent。
s08:怎么让慢操作在后台跑,Agent 不被卡住?

s08: Background Tasks →