Files
deepseek-harness/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.zh.md
T

8.1 KiB
Raw Blame History

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。创建与显式恢复会激活目标;暂停、完成、阻塞和清除都会解除激活。编辑保留激活态及阻塞原因;恢复和完成会清除该原因。持久快照绝不包含激活态。

持久记录与回放

每次非清除变更都通过 Agent.inject() 追加一条模型可见的 context/message,其中包含带版本的完整快照;会话会将其内容原样投射给模型。清除操作追加带修订号的墓碑。上下文来源为 { kind: 'goal', goalId, revision, round: 0 };元数据必须与渲染后的 <goal_state>...</goal_state> 内容完全一致。这个描述性分隔符沿用了仓库已有的 <workspace_context> 约定,也符合 Anthropic 关于用一致且描述明确的 XML 标签组织混合提示词内容的公开指南。这是公开的模型体验先例,并非对任何提供方专有训练语料的推断。会话日志是唯一的持久真源,因此持久化和 fork 会继承目标记录,而无需另设数据库或头字段。

回放折叠会校验 JSON 形状、来源归属、渲染内容、从未出现过的 id、修订连续性、生命周期转换、计数器以及单个目标内单调递增的时间戳。Goal Round 是当前活跃修订上带正数且连续编号的 user/message 来源,且不能超过 maxGoalRounds;普通会话轮次不会影响该计数器。当前格式的畸形记录会使回放失败,而不会被忽略或修复。

当 Agent.inject() 在活跃工具批次中延迟变更时,服务会在进程内存中以覆盖层记录已接受的载荷,使后续变更可以使用新的修订号。FIFO 追加可见后,协调过程只移除完全匹配的载荷;重入的追加观察器对每次变更只投影一次。增量回放会在每个有效事件后推进游标,并停留在首个损坏事件处,因此后续读取会报告同一个持久故障。重启后仍以持久日志为准。

生命周期与实时激活态

最多只有一个当前目标。创建要求不存在未完成的当前目标,并始终生成该会话此前未使用过、修订号为一的 id;已完成目标可以被替换。其他每次变更都携带预期的 GoalRef,陈旧的 id 或修订号会被拒绝。仅当回合上限仍有余量时,暂停或阻塞阶段以及已解除激活的活跃目标才能恢复。领域层校验阻塞原因的形状,但会把原因代码和是否阻塞的决策留给策略消费者。

从任何种子构建的缓存都以未激活状态开始,每次 agent/session-start 边沿也会再次解除激活。GoalService.disarm(agent) 还允许生命周期所有者移除进程内权限,而不写入会话事件、不改变修订号,也不发出 goal/changed 通知。因此,会话恢复、fork 和继续执行驱动器替换都会保留持久化目标与历史,但绝不会自行启动工作。后续人类提示词可由模型解释,其策略表面可以显式调用恢复操作并激活目标。

服务边界

服务只接受在对应 id 下注册的同一个实时 Agent 对象。成功注入变更后,它会发出带作用域的 goal/changed 事件,并隔离监听器失败。策略消费者通过本服务、公共 Agent 接口和 agent/* 事件工作;目标领域既不导入也不修改 dsh-agent-loop。

测试

单元测试固定创建默认值、精确实时 agent 校验、比较并交换拒绝、所有生命周期转换、阻塞原因校验与保留、恢复时的上限执行、清除与替换、种子回放和 SessionStore.fork() 继承、会话启动与生命周期所有者解除激活、活跃目标重新激活、FIFO 延迟变更协调、重入追加观察、注入拒绝回滚、损坏事件的稳定回放、服务与监听器销毁、监听器隔离、挂钟回拨校正、严格记录解码、生命周期连续性、来源与内容一致性,以及连续 Goal Round 归属。无密钥 Loader/stdio 进程测试通过测试专用 cordis.yml 挂载服务与生命周期消费者,再从外部读取持久 JSONL,以验证模型可见快照以及不存在未经请求的 Goal Round。包源码受仓库逐文件 100% 覆盖率门禁约束。

考虑过的替代方案

  • 把目标存入独立数据库或会话头——不予采纳,因为会话日志已经提供顺序、持久化、fork 前缀与可重建性;第二份存储会引入原子性和谱系问题。
  • 使用模型不可见的纯日志事件——不予采纳,因为会改变后续模型行为的持久状态必须满足仓库日志不变量,保持模型可见且可重建。
  • 持久化激活态并自动重启——不予采纳,因为打开或恢复会话时必须等待人类输入;持久阶段记录状态,而不是再次消耗资源的授权。
  • 把所有会话轮次都计为 Goal Round——不予采纳,因为同一会话可以包含人类澄清、检查和无关工作;只有归属于目标的继续执行轮次才消耗该预算。
  • 向 dsh-agent-loop 添加目标状态或通用循环抽象——不予采纳,因为状态与继续执行策略可以通过现有插件、Agent 动词和事件组合,而无需赋予默认循环实现特权。

后果

  • 目标历史作为普通会话数据,在持久化、恢复、无关节点压缩和会话 fork 后继续保留。
  • 恢复与 fork 会暴露同一持久阶段,但在显式恢复变更激活目标前不会执行任何操作。
  • 完整快照便于检查和严格回放,但在压缩隐藏它们之前,会在模型历史中重复目标描述与状态字段。
  • 修订号与生命周期校验会尽早拒绝遭篡改、部分写入或生产者不一致的目标记录。
  • 回合上限只约束继续执行次数;当回合、token、费用、时间或提供方限制停止工作时,策略消费者会把它们映射为不同的阻塞原因。

已知限制与暂缓事项

  • 本领域记录状态,但不调度 Goal Round、不取消活跃轮次,也不分类异常停止。
  • 记录 complete 或 blocked 的参与者具有最终权威;独立评估器或完成证书延期到策略消费者中实现。
  • 每个会话只有一个当前目标;不存在并行目标图和跨会话目标存储。
  • 插件共享同一个受信任的进程边界。直接写入会话的插件可以伪造目标记录;严格回放会检测不一致并在违规记录处使目标访问失败,但不会隔离插件或修复日志。
  • GOAL_CHANGE_VERSION 在首次发布前不承诺兼容性,也不提供迁移路径。