Skip to content
My Blog
Go back

状态就是日志:plan、todo、goal 在 dsh 里怎么存

agent 产品里常见一批「状态」:计划模式开了没、待办列表写了啥、长期目标推进到哪。多数实现里它们是运行时内存对象或独立存储。dsh 给出的答案是同一个词:logged state——状态就是日志事件折叠出来的结果。

plantodogoal 三个包,正好是这个理念的三种深度。

plan:一个布尔,折叠出来

计划模式的状态就是一个 log-only 事件:

// packages/plan/plan-mode/src/index.ts
'plan/mode': { active: boolean }

export function foldPlanMode(events, end = events.length): boolean {
  // 遇到 plan/mode 事件就更新 active,扫完即当前状态
}

没有活体镜像,当前状态 = 把日志扫一遍。文档明确它「durable and replayable, never in the model transcript」——持久、可重放,但不出现在模型对话里。

有个细节暴露了这套设计的严谨:会话事件必须发生在 turn 内,可用户在 turn 外点了切换怎么办?答案是先记为 pending,等下一个被接受的 agent/pre-step 监听器(turn 内)把它追写进日志;只有当状态真的变了,才再补一条 user/message 提醒模型。状态变更不抢跑,也不静默。

todo:整表快照,越简单越对

todo_write 工具每次调用都整表替换,追加一条 todo/write: { todos } 快照事件。条目字段少得惊人:content 加一个状态(pending / in_progress / completed)。文档特意点明:「No id, priority, or activeForm — the list is replaced wholesale on every write.」

为什么不要 id?因为每次都是全量替换,谁也不需要做增量定位,状态天然是「最近一次快照」。包还自带一个 invariant 伴生:校验每条 todo/write 都发生在开放 turn 内、且无重复内容。

goal:带修订号的快照状态机

到了 goal,复杂度和深度都上来了:全快照的 goal/change 事件,外加 GoalRef { id, revision } 做比较并设(compare-and-set),防止并发下旧的写入覆盖新的。状态机的相位 GoalPhase = 'active' | 'paused' | 'blocked' | 'complete' 是持久相位,进日志;而「激活与否」(能不能继续下一轮)是进程本地的,故意不持久化。组内还配了 goal-round-driver 驱动、tool-goal 工具和 command-goal 命令。

为什么值得这么干

把状态存成日志事件,白捡三样东西:

从 plan 的一个布尔,到 todo 的整表快照,再到 goal 的带修订号状态机——同一个模式按复杂度递增演进了三次。这不是三个孤立的包,是一条被验证过可以伸缩的设计线。

本系列


Share this post:

Previous Post
压缩自己也进日志:dsh 的 Compaction 设计
Next Post
第 04 课 · 一次 Turn 的完整旅程