第 十二 章 Agent 架构实操 深入学习 · 交互式 含 QA 测试

实战干货:5 个工具、3 个状态,带你撸出一个生产级 Agent 任务引擎

💡 JSON Task DAG:五工具、三状态、加锁的原子的图持久化。
回答三个问题:哪些任务现在可以开始?谁已认领、谁有权完成?进程退出后任务图能否原样恢复?Todo 与 Task 分层;Task 只允许 pending/in_progress/completed,阻塞由依赖派生。
本章进度
0%
1 本章要掌握的目标
  • 五个任务工具从同一 ToolDefinition 生成 schema 与 handler。
  • Task 三态单向推进 pending→in_progress→completed。
  • 阻塞是派生事实:pending 且至少一个依赖未完成。
  • 全图排他锁包住加载、DAG 校验、条件迁移与原子 JSON 替换。
  • claim 只有一个并发赢家,complete 只能由 owner 执行;重建严格恢复。
2 核心知识点
TODO 与 Task 不是一回事

todo_write 是当前会话步骤快照(进程内存);JSON Task DAG 是可独立认领完成的项目任务(磁盘持久化、有依赖)。

维度      todo_write           JSON Task DAG
粒度      当前工作的步骤快照    可独立认领的项目任务
所有者   单个 Agent session  当前 workspace
存储     进程内存             .agent_tutorial/.tasks/{uuid}.json
依赖     无                  blocked_by 有向边
一个 Task 的严格模型

id 是 canonical UUID,文件名必须与 payload ID 相同;subject 非空;blocked_by 是 canonical UUID 且不重复;pending 必须 owner===null;in_progress/completed 必须有非空 owner。

const task = new Task({
  id, subject, description,
  status: TaskStatus.PENDING,
  owner: null, blockedBy: [],
});
为什么锁的是整张图

claim 的判断依赖所有 blocked_by 任务;create 要验证全图缺边与环。只锁单个文件无法把『读取依赖→判断→写入』做成原子决策。锁内重载是关键动作:拿到锁之后必须重新从磁盘读整张图,锁外读的数据在进临界区那一刻就已经过期。

async #withLock(paths, operation) {
  const release = await acquireFileLock(paths.tasks, {
    lockfilePath: paths.lock, realpath: true,
    stale: 30_000, update: 10_000, retries: 0,
  });
  try { return await operation(); }
  finally { await release(); }
}
原子 JSON 替换

临时文件 + wx 创建 + write + sync + rename 原子替换。失败时旧文件保持原字节,UUID 碰撞绝不覆盖旧任务。注意:原子写保证「不会写坏」,不保证「不会覆盖」——每次写入都可以是原子的,结果依然可能错,互斥必须靠锁(实验 8 验证)。

const temporary = join(dirname(path), `.${randomUUID()}.tmp`);
handle = await open(temporary, "wx");
await handle.writeFile(content);
await handle.sync();
await handle.close();
await rename(temporary, path);
owner 不能由模型参数伪造

claim_task schema 只有 task_id;owner 来自 ToolContext.identity。额外字段会在副作用前被 schema 拒绝。

const task = await store.claimTask(
  input.task_id, normalizeOwner(context.identity));
3 机制流程
1
create_task

subject/description/blocked_by;锁内验证 ID 碰撞、依赖存在、无自依赖、全图无环。

2
claim_task

pending → in_progress,owner 来自 identity,条件迁移;并发认领只有 1 个赢家,输家拿到 TaskStateError(task_invalid_state) 而非锁冲突。

3
complete_task

in_progress + owner 匹配 → completed,返回本次直接解锁集合(只往下一层,不改写下游文件——ready 是算出来的)。

4
重建

锁内重读全部 JSON,完整校验;坏文件/缺边/环明确失败。

4 术语表
TaskStatuspending / in_progress / completed 三态;无 update/delete/unclaim,只能单向前进。
blocked_by依赖边;blocked/ready 是派生事实而非第四种状态,不落盘。
canonical UUID规范 UUID 文本,映射为文件名;文件名与 payload ID 必须一致。
TaskStorageError持久化文件或目录边界损坏。
全图锁串行化所有读改写;锁内重载 + 原子写是两个独立机制。
fail closed一个坏文件毒化整张图:get_task 也要 loadGraph,全或无,不跳过不返回半张图。
刻意省略无 blocks 下游字段(一条边只有一处真相)、无 activeForm/metadata;lease/claim token/工作窃取留给 P17 SQLite。
5 QA 测试环节(自测题)
已完成 0 / 6 · 答对 0
Q1. Task 的状态有几个?
Q2. claim 成功时 owner 来自?
Q3. 为什么锁整张图而不是单个文件?
Q4. 创建任务时发现缺依赖/环会?
Q5. complete_task 的必要条件是?
Q6. 重建时怎么处理坏文件?
6 验证与实验
  • npm run test:ch12 覆盖坏 JSON、缺边、环、ID 碰撞、原子写失败、junction 逃逸、并发唯一认领;P12 主 Agent 工具序列为 13 个(原 8 个 + 5 个任务工具)。
  • 跨进程验证:三次独立 npm run ch12 接力建图 → 认领 → 完成,新进程仅凭磁盘恢复完整状态。
  • npm run ch12 -- --prompt "列出当前项目任务并认领一个 ready 任务"
  • 八个离线小实验可亲手弄坏磁盘看报错(无需 API Key);.env 需四字段(含 OPENAI_FALLBACK_MODEL)。