第 2 卷
每次变更都带 GoalRef(就是 id + revision)做比较后写入:revision 对得上才写,对不上(陈旧引用)就被拒绝;
已有非 complete 的目标时,create 被拒;目标 complete 之后,就能 create 一个新的了。
resume 只在还有剩余轮次容量时才接受;目标已经 active 且 armed 时也被拒,resume 本就是把它变成 active + armed,它已经在了。
activation(armed / disarmed)是进程本地状态,不写进日志,goal/change 事件里只有目标的持久字段,没有它。所以会话恢复、分叉,或进程重启后,它都回到 disarmed:
续跑不自动开,要由人明确 resume 才重新启用。这是刻意的安全设计:重启后环境(工作区、工具、provider)可能和崩溃前不一样,让模型在变了的环境里自动接着跑会出错,所以
日志回放出的 active 目标不会自动续跑,得等人确认环境、明确 resume 才继续。
怎么自动续跑:goal-round-driver 排下一轮
goal-round-driver 是一个插件,职责就一件:等 Agent 干完回到 idle(第 13 讲说的 “闲”状态),自动把下一轮排进去,也就是开篇说的“续跑”。
它只靠监听 goal/changed(goal/change 落盘后发出的事件,不写日志,只用来通知监听者)和 idle 状态,只在每次 agent 回到 idle 时做一次判断:
phase 是 active、activation 是 armed、且 roundsStarted < maxGoalRounds → 预留 roundsStarted + 1,调 agent.followup 往队列(第 13 讲的 agent.inbox)
里排一条续跑提示词。
这条续跑提示词会点明完整目标与 round / maxGoalRounds(比如 Round: 1/5),要求模型在标 complete 前给出证据、在尚未完成时保持目标 active。它还要求模型把当前工
作区、工具结果、会话状态当作判断依据,而不是只凭一句目标描述。
示例里,驱动器自动续了这么一段:
复制代码 1--- 驱动器在 idle 时自动排下一轮 ---
| 2 | 第1次回到idle:phase=active,roundsStarted=0 |
|---|---|
| 3 | 第2次回到idle:phase=active,roundsStarted=1 |
| 4 | 第3次回到idle:phase=complete,roundsStarted=2 |
第 1 次 idle 时目标刚建好、第一轮还没开始(0 / 5);第 2 次是第 1 轮干完、驱动器自动排了第 2 轮;第 3 次是第 2 轮把目标标成 complete,驱动器随即停手。
roundsStarted 是“已准入”的轮次数:先排进队列只算预留,模型真的开始跑、进了日志才算数。
每一轮的边界(turn/start、turn/end)和 goal 变更(goal/change)都落在日志里:
复制代码 1--- 会话日志里的 goal 与轮次事件 ---
| 2 | seq0 | goal/change(create) |
|---|---|---|
| 3 | seq2 | turn/start |
| 4 | seq5 | user/message(goal,round 1) |
| 5 | seq14 | turn/end |
| 6 | seq16 | turn/start |
| 7 | seq19 | user/message(goal,round 2) |
| 8 | seq26 | goal/change(complete) |
| 9 | seq39 | turn/end |
沿着 seq 读一遍:0 是 create 落盘的目标,2 到 14 是第一轮,16 起是第二轮,26 是第二轮里模型报 complete 落盘的目标,39 收尾。注意 goal/change 只在目标变更时出现,所
中间那两条 user/message(seq 5、19)就是 round-driver 排进来的 goal 消息。它虽然记录成 user/message,但来源要看 source,round-driver 是这样构造消息的:
复制代码 1createUserMessage({
2content, 3source: { kind: 'goal', goalId, revision, round },*// ← 来源是 goal,不是 user*
4})
kind: 'goal' 就是它和用户直接发的消息的区别(13 讲里用户消息是 kind:
'user');round 是轮次编号,所以日志里能看到 round 1、round 2。
怎么停下来:三种自然停
停这件事,主要靠模型报。模型操作目标,不直接调 ctx.goals,而是走 tool-goal 注册的三个工具:
| 工具 | 作用 |
|---|---|
| get_goal() | 读当前目标,没有则返回null |
| create_goal(objective, max_goal_rounds?) | 建一个目标 |
改目标:edit / pause / resume / complete / update_goal(goal_id, revision, action, ...)
blocked
这三个工具都是 ctx.goals 的模型门面:get_goal 内部就是调 ctx.goals.get, create_goal 内部就是调 ctx.goals.create,update_goal 的 action 参数对应
ctx.goals 的 edit / pause / resume / complete / block(工具里这个 action 叫