todo_write 是当前会话步骤快照(进程内存);JSON Task DAG 是可独立认领完成的项目任务(磁盘持久化、有依赖)。
维度 todo_write JSON Task DAG
粒度 当前工作的步骤快照 可独立认领的项目任务
所有者 单个 Agent session 当前 workspace
存储 进程内存 .agent_tutorial/.tasks/{uuid}.json
依赖 无 blocked_by 有向边
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(); }
}
临时文件 + 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);
claim_task schema 只有 task_id;owner 来自 ToolContext.identity。额外字段会在副作用前被 schema 拒绝。
const task = await store.claimTask(
input.task_id, normalizeOwner(context.identity));
subject/description/blocked_by;锁内验证 ID 碰撞、依赖存在、无自依赖、全图无环。
pending → in_progress,owner 来自 identity,条件迁移;并发认领只有 1 个赢家,输家拿到 TaskStateError(task_invalid_state) 而非锁冲突。
in_progress + owner 匹配 → completed,返回本次直接解锁集合(只往下一层,不改写下游文件——ready 是算出来的)。
锁内重读全部 JSON,完整校验;坏文件/缺边/环明确失败。