7.4 KiB
Agent Note: 持久化的同会话目标领域
Status: implemented
English | 中文
问题
长时间运行的目标会跨越单个提示词、轮次或模型请求。若把该目标视为内存中的循环变量,进程重启时就会丢失;若只存放在 UI 状态中,又无法重建模型行为。若把会话中的每个轮次都视为目标进度,与自动工作无关的人类消息也会消耗预算。
持久化的生命周期与继续执行的权限是两个不同事实。会话在重启或 fork(派生)后可以保留活跃目标,但用户打开会话时静默启动工作并不符合直觉。该领域需要可回放的状态,却不能持久化自动执行权限;它还必须作为公共 agent(智能体)与会话 seam 上的插件存在,而不是具体循环中的特例。
决策
位于 packages/goal/goal/ 的 @deepseek-ai/dsh-goal 通过 ctx.goals 管理一个当前的同会话目标。目标包含带品牌的 id、目标描述、持久化阶段、比较并交换修订号和 maxGoalRounds。defaultMaxGoalRounds 是经过校验的部署配置,默认值为 256;create() 在变更前于内部将其解析为完整值,而不会把解析过程暴露为额外的服务动词。
持久阶段包括 active、paused、blocked 和 complete。阻塞快照包含由策略定义的全小写 kebab-case 代码和规范化自由文本消息,因此用量限制、回合上限、执行失败和等待人工输入可以共享一个生命周期状态而不丢失原因。独立的实时激活态为 armed 或 disarmed。创建与显式恢复会激活目标;暂停、完成、阻塞和清除都会解除激活。编辑保留激活态及阻塞原因;恢复和完成会清除该原因。持久快照绝不包含激活态。
持久记录与回放
每次变更都会追加带版本的 goal/change 会话事件,其中包含完整快照;清除时则包含带修订号的墓碑。会话日志是唯一的持久真源,因此持久化和 fork 会继承目标记录,而无需另设数据库或头字段。目标自有持久事件决策负责将目标持久事件与收件箱状态和模型上下文分离。
回放折叠只从 goal/change 派生生命周期变更,并校验 JSON 形状、新 id、修订连续性、生命周期转换、计数器以及单个目标内单调递增的时间戳。只有当前活跃修订上带正数且连续编号、已接纳的 user/message 来源才会推进 Goal Round,且不能超过 maxGoalRounds;普通会话轮次不会影响该计数器。当前格式的畸形记录会使回放失败,而不会被忽略或修复。
增量回放会在每个有效事件后推进游标,并停留在首个损坏事件处,因此后续读取会报告同一个持久故障。重启后仍以持久日志为准。
生命周期与实时激活态
最多只有一个当前目标。创建要求不存在未完成的当前目标,并始终生成该会话此前未使用过、修订号为一的 id;已完成目标可以被替换。其他每次变更都携带预期的 GoalRef,陈旧的 id 或修订号会被拒绝。仅当回合上限仍有余量时,暂停或阻塞阶段以及已解除激活的活跃目标才能恢复。领域层校验阻塞原因的形状,但会把原因代码和是否阻塞的决策留给策略消费者。
从任何种子构建的缓存都以未激活状态开始,每次 agent/session-start 边沿也会再次解除激活。GoalService.disarm(agent) 还允许生命周期所有者移除进程内权限,而不写入会话事件、不改变修订号,也不发出 goal/changed 通知。因此,会话恢复、fork 和继续执行驱动器替换都会保留持久化目标与历史,但绝不会自行启动工作。后续人类提示词可由模型解释,其策略表面可以显式调用恢复操作并激活目标。
服务边界
服务只接受在对应 id 下注册的同一个实时 Agent 对象。变更提交后,它会发出带作用域的 goal/changed 事件,并隔离监听器失败。策略消费者通过本服务、公共 Agent 接口和 agent/* 事件工作;目标领域既不导入也不修改 dsh-agent-loop。
测试
单元测试固定创建默认值、精确实时 agent 校验、比较并交换拒绝、所有生命周期转换、阻塞原因校验与保留、恢复时的上限执行、清除与替换、种子回放和 SessionStore.fork() 继承、会话启动与生命周期所有者解除激活、活跃目标重新激活、持久事件折叠、收件箱独立性、损坏事件的稳定回放、服务与监听器销毁、监听器隔离、挂钟后退钳制、严格记录解码、生命周期连续性,以及连续 Goal Round 归属。无密钥 Loader/stdio 进程测试通过测试专用 cordis.yml 挂载服务与生命周期消费方,再从外部读取持久 JSONL,以验证目标记录以及不存在未经请求的 Goal Round。包源码受仓库逐文件 100% 覆盖率门禁约束。
考虑过的替代方案
- 把目标存入独立数据库或会话头——不予采纳,因为会话日志已经提供顺序、持久化、fork 前缀与可重建性;第二份存储会引入原子性和谱系问题。
- 把每次持久变更与排队的模型上下文绑定。 后续的 Goal 自有持久事件决策不采用这一方案:目标工具与已调度的继续执行提示词会在需要时暴露状态,而领域持久化不依赖队列结果。
- 持久化激活态并自动重启——不予采纳,因为打开或恢复会话时必须等待人类输入;持久阶段记录状态,而不是再次消耗资源的授权。
- 把所有会话轮次都计为 Goal Round——不予采纳,因为同一会话可以包含人类澄清、检查和无关工作;只有归属于目标的继续执行轮次才消耗该预算。
- 向
dsh-agent-loop添加目标状态或通用循环抽象——不予采纳,因为状态与继续执行策略可以通过现有插件、Agent动词和事件组合,而无需赋予默认循环实现特权。
后果
- 目标历史作为普通会话数据,在持久化、恢复、无关节点压缩和会话 fork 后继续保留。
- 恢复与 fork 会暴露同一持久阶段,但在显式恢复变更激活目标前不会执行任何操作。
- 完整快照便于检查、严格回放与 last-wins 投影,且不会向模型历史添加只用于变更的消息。
- 修订号与生命周期校验会尽早拒绝遭篡改、部分写入或生产者不一致的目标记录。
- 回合上限只约束继续执行次数;当回合、token、费用、时间或提供方限制停止工作时,策略消费者会把它们映射为不同的阻塞原因。
已知限制与暂缓事项
- 本领域记录状态,但不调度 Goal Round、不取消活跃轮次,也不分类异常停止。
- 记录
complete或blocked的参与者具有最终权威;独立评估器或完成证书延期到策略消费者中实现。 - 每个会话只有一个当前目标;不存在并行目标图和跨会话目标存储。
- 插件共享同一个受信任的进程边界。直接写入会话的插件可以伪造目标记录;严格回放会检测不一致并在违规记录处使目标访问失败,但不会隔离插件或修复日志。
GOAL_CHANGE_VERSION在首次发布前不承诺兼容性,也不提供迁移路径。