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

11 KiB

Agent Note: 同会话 Goal Round 驱动器

Status: implemented

English | 中文

问题

目标领域可以保留目标,模型可见工具也可以变更其生命周期,但两者都不应决定下一个模型轮次何时开始。继续执行驱动器必须把活跃目标状态桥接到普通 agent(智能体)循环,同时不能向 dsh-agent-loop 添加目标专用分支、创建第二段对话,也不能把每个人类轮次都视为自主迭代。

这层连接还承担并发与持久性义务。人类输入、取消、目标编辑、持久化失败、会话重启、插件卸载以及下游提示词策略都可能与待处理的继续执行发生竞争。简单的 goal/changed -> agent.followup() 监听器可能接纳过期工作、与人类提示词同时运行、超出上限消耗资源,或在回放后未经新授权自行重启。

决策

位于 packages/goal/goal-session/@deepseek-ai/dsh-goal-session 是构建在 ctx.goals、公共 Agent 接口和持久会话事件之上的策略插件。它不导入具体 agent-loop 实现。对于每个对应的同一个实时 Agent 对象,它维护进程内调度状态,并且最多保留一个自动回合预留。

层次关系为目标(Goal)→ Goal Round → 轮次(Turn)→ 步骤(Step)。Goal Round 是外层继续执行策略的一次迭代;它会成为一个归属于目标的会话轮次,而该轮次可以包含任意数量的普通模型或工具步骤。同一会话中的人类轮次不是 Goal Round,也绝不会增加 roundsStarted

该插件没有配置项。maxGoalRoundsdsh-goal 解析并持久化;“相同阻塞条件”的门槛由 dsh-tool-goal 解析并写入提示词。若驱动器重复声明这些可调值,一个策略就会出现多个所有者。

预留与接纳

当 agent 空闲、没有竞争中的排队工作,且当前目标为 activearmed 时,驱动器会先将待处理的目标变更持久化到检查点,并在等待完成后重新校验所有条件。若 roundsStarted 已等于 maxGoalRounds,它会记录代码为 round-limitblocked;否则,它会先预留精确身份 { goalId, revision, round: roundsStarted + 1 } 和完整渲染提示词,再以 GoalMessageSource 调用 Agent.followup()。提示词用 JSON 引号编码目标描述,使多行或类似标签的文本在熟悉框架中仍是无歧义的数据值。

agent/pre-step waterfall(瀑布式事件)是进入栅栏。正数目标来源只有在完全匹配驱动器待处理的身份和内容、实时目标仍具有相同 id 与修订号、激活态仍为 armed,并且该回合仍是下一个编号时才会进入。插件在委托下游监听器前检查一次,在下游返回后再检查一次。第二次检查防止异步监听器编辑或暂停目标后,旧提示词仍会进入。

只有最终产生的 user/message 才是已进入的目标回合,并推进目标折叠。陈旧预留会关闭一个被阻塞的零步骤轮次;驱动器会把它标记为陈旧,不消耗回合数。若下游策略拒绝并非由陈旧状态导致,目标会进入 blocked,而不会绕过该策略自动重试。

人类工作与修订竞争

预留的 MessageId 会区分驱动器自己的完整记录与其他所有提示词。预留之前已经排队的普通工作会阻止调度;自动提示词待处理时进入的普通工作会使该预留过期,因此混合的已领取批次会拒绝自动提案。目标回合进入后到达的普通工作会保留在队列中,成为下一个独立轮次;只有 agent 再次空闲后才重新考虑继续执行。

目标在回合内发生变更时会推进持久修订号。旧修订的结算不得覆盖该变更。驱动器会丢弃旧尝试的结果、读取新投影,并且只在新修订仍为 active 与 armed 时继续。因此,模型记录的完成、暂停、阻塞和编辑相对于物理轮次稍后的关闭原因具有最终权威。

结算

驱动器按下表分类一个已经关闭、归属于目标的轮次:

轮次结果 动作
持久化的 completed 目标仍 active/armed 且未到上限时继续
取消已预留/接纳的 Goal Round,或该 Round 产生 aborted 结果 暂停并解除激活
代码为 RATE_LIMITQUOTAerror usage-limited 代码阻塞
其他 error turn-error 代码阻塞
max-tokens max-tokens 代码阻塞
持久化检查点失败 解除激活,但不改变持久化阶段
disposedinterrupted 解除激活
插件新增的未知结果 阻塞并等待检查

异常结果都不会请求自动重试。之后的人类提示词可以用任何语言要求继续;模型读取已停止目标并调用目标工具的 resume 动作,记录新修订并重新激活继续执行。

持久性与取消 seam

每次 goal/changed 通知都会产生一个检查点义务。驱动器在预留工作前等待 ctx.sessions.flush(session),随后检查是否出现了更新的变更、agent 生命周期变化或竞争提示词。轮次结束时的 flush 失败会在 turn/end 之后通过现有 agent/error 通知报告;即使并发的一次性注入已追加后续轮次,驱动器仍会找到对应的同一个已关闭轮次,把失败关联到对应的同一次尝试,并在下一次空闲决策前解除激活。

广义取消会清除待处理的收件箱工作,并中止活跃的循环阶段。目标驱动器通过收件箱领取/丢弃事件和持久化的 aborted 轮次结束事件来跟踪预留消息。由于轮次现在会在首次领取前打开,取消可以关闭已领取的零步骤尝试;驱动器会把该尝试标记为已取消,并让随后的空闲边沿暂停目标,与已接纳尝试的处理方式相同。没有匹配目标尝试的取消只会移除进程内激活态。若暂停变更抛错,驱动器会回退到解除激活,避免已取消的自动工作重新启动。

Agent.cancel() 仍是唯一的公共广义取消动词。若消费方依赖这些顺序,实现该接口的自定义 Agent 必须遵守收件箱、轮次结束、状态与完全停稳的顺序。

进程生命周期

GoalService.disarm(agent) 只移除进程内激活态。它不写会话事件、不改变修订号,也不发出目标变更。驱动器在加载到已有 agent、持久性存在不确定性以及卸载前调用该方法;之后的 resume 才是模型可见的持久激活边沿。

驱动器的事件监听器和完全停稳后关闭的流程嵌套在同一个有序 Cordis effect 中。Cordis 会并发卸载同级 effect;若监听器和清理分别注册,异步 disposer 仍在排空时提示词栅栏就可能已被移除。组合 effect 会先关闭接纳、解除目标激活、取消已接纳尝试,并等待 agent 与驱动器都完全停稳;之后才注销监听器。

紧邻插件开始卸载前,收件箱接纳可能赢得微任务竞争。在这种情况下,轮次甚至首个请求都可能已经开始,且该回合仍会持久化计入额度;卸载一旦开始,取消就会中止它,不会再调度后续回合,目标保持 active 但 disarmed。若假装已经观测到的接纳从未发生,就会破坏回放计数。

测试

单元测试使用真实 agent loop 与会话服务,只对模型编写脚本。覆盖内容包括精确连续接纳和上限执行、加载与恢复的惰性、所有结果分类、限流、请求错误、最大 token、下游提示词否决、接纳前与执行中取消、无关人类工作取消、暂停失败回退、人类输入排序、排队时与下游修订竞争、伪造目标来源、变更与轮次检查点失败(包括后续一次性注入)、调度器与自定义 agent 失败、会话启动重置、精确生命周期退出,以及排队中和运行中的插件卸载。新驱动器源码达到逐文件 100% 语句、分支、函数和行覆盖率。

无密钥 ACP 快照通过 cordis.yml 挂载已发布的自动化应用,以及真实目标领域、目标工具、目标驱动器、agent loop、持久化和回放适配器。一个源自人类的轮次创建并检查一个两回合目标;第一个自动轮次正常停止,ACP 随后取消刻意停滞的第二个回合并记录持久暂停。规范化的线协议记录和外部 JSONL 断言证明只有一个会话、回合来源依次为 1, 2、生命周期变更与回放计数精确,并且没有把 echo-agent 当作应用替身。

核心取消测试固定通知顺序与隔离:只有有效取消才会通知;观察者可以在清空收件箱前排入替代工作;抛错不能阻止后续观察者;空闲调用不会发出事件。

考虑过的替代方案

  • dsh-agent-loop 内添加目标循环——不予采纳,因为公共队列、提示词、会话、取消和状态 seam 已经足够,具体循环分支还会赋予某种策略特权。
  • 使用 agent/turn-continuation 把每个 Round 变成另一个步骤——不予采纳,因为 Goal Round 是外层策略迭代,必须拥有自己的持久用户提示词、轮次边界、Round 计数和失败结算。
  • 持久化待处理预留——不予采纳,因为崩溃无法证明进程内队列已经达到接纳点;只有持久 user/message 才消耗回合。
  • 自动重试提供方或持久化错误——不予采纳,因为重试会消耗资源,需要显式授权;停止阶段加之后的人类恢复更简单,也可观察。
  • 每回合 fork 对话历史或生成新 agent——本包不采用,因为此目标明确属于同会话工作。新 agent 的 Ralph 执行仍是基于 subagent 与 workflow 原语的独立工作流插件。
  • 把每个会话轮次当作回合计数——不予采纳,因为人类澄清和无关工作共享会话,但不共享自动工作预算。

后果

  • 目标继续执行仍是可移除插件,具体循环只新增一个通用的“取消前观察”通知。
  • 回放可以从精确目标来源和提示词重建每个已接纳回合;被拒绝的预留不会产生虚假的预算消耗。
  • 人类消息和生命周期变更可以在有文档约束的竞争中胜出,而不破坏修订号或计数器。
  • 恢复和 fork 在语义上的人类意图促使模型记录 resume 变更之前始终保持惰性。
  • 保守的失败映射可能要求在暂时性错误后手动继续,但绝不会隐藏自动重试。

已知限制与暂缓事项

  • 完成证据和阻塞条件的语义等价性仍由模型判断。独立评估器、完成证书或由验证器驱动的停止策略延期到独立策略插件。
  • 本包不提供 Ralph 风格的新 agent 尝试、上下文重置、跨回合评估反馈或工作流级并行;它们属于独立的 Ralph 工作流工具。
  • Cordis 卸载异步开始。已经被收件箱接受的条目可能先进入一个计费回合并启动一个请求,之后卸载取消才生效;关闭排空会阻止所有后续回合。
  • maxGoalRounds 只是已接纳回合上限。token、费用、挂钟时间和提供方使用预算需要独立策略。
  • 自定义 Agent 实现必须产生文档规定的会话事件、状态边沿、取消通知和静止语义;仅凭 TypeScript 结构兼容无法验证运行时顺序。