数据截至 (上游 commit 01198332ef92)
第 2 章 · 三块共享状态
ClawTeam 号称「零基础设施」。本章拆开它的三块共享状态——任务板、信箱、工作区——看清楚为什么纯文件就够用,以及其中处理并发的巧思。
2.1 数据模型先行
所有协作状态都是 Pydantic 模型,序列化成 JSON 文件。三个核心模型(clawteam/team/models.py):
| 模型 | 是什么 | 关键字段 |
|---|---|---|
TeamConfig | 一支队伍 | members(成员列表)、lead_agent_id(谁是 leader) |
TaskItem | 一张任务卡 | status、owner、blocked_by、blocks、locked_by |
TeamMessage | 一条消息 | type、from、to、content、request_id |
任务有四个状态(TaskStatus,clawteam/team/models.py:36-41):pending → in_progress → completed,外加 blocked。消息类型则很丰富(MessageType,第 50-62 行):普通消息、入队请求/批准、计划审批、关停请求、闲置通知、广播等——协作协议本身就编码在消息类型里。
2.2 任务板:依赖链 + 自动解锁 + 抢锁
它要解决的小问题
多个 agent 共享一块看板。难点有三:① 任务 B 依赖任务 A,A 没完成时 B 不该被领走;② A 一完成,B 要自动变成可领;③ 两个 agent 不能同时认领同一张卡。全部要在「没有数据库、只有文件」的前提下做到。
存储布局
每个任务是一个独立 JSON 文件:~/.clawteam/tasks/<team>/task-<id>.json(clawteam/store/file.py:_task_path,第 33-34 行)。一个任务一个文件,意味着列任务 = glob("task-*.json"),改一个任务不碰别的文件。
自动解锁:依赖图怎么工作
任务 B 的 blocked_by 列表里放着它依赖的任务 id。创建时若有 blocked_by,状态直接设为 blocked(clawteam/store/file.py:create,第 97-98 行)。
当任务 A 被更新为 completed,update() 会调用 _resolve_dependents_unlocked,扫描所有任务,把 A 从它们的 blocked_by 里移除;谁的 blocked_by 因此空了且当前是 blocked,就翻回 pending:
# clawteam/store/file.py:362-375 _resolve_dependents_unlocked 节选
for f in root.glob("task-*.json"):
task = TaskItem.model_validate(json.loads(f.read_text()))
if completed_task_id in task.blocked_by:
task.blocked_by.remove(completed_task_id)
if not task.blocked_by and task.status == TaskStatus.blocked:
task.status = TaskStatus.pending # 自动解锁!
self._save_unlocked(task)
这就是 README 演示里「architect 完成 → backend1/backend2 自动 unblock」的真实机制(README.md:320-323)。
环检测:不让你建出死锁
加依赖前会跑一次 DFS 环检测,把整张依赖图(现有任务 + 拟加的边)走一遍,有环就拒绝(_validate_blocked_by_unlocked,第 316-344 行;还单独挡掉「任务依赖自己」)。这是个教科书式的「三色 DFS 找环」。