DeepSeek Harness plan mode 子代理未结束提交?pre-execute 与 execute 校验
这是 DeepSeek Harness 的「没有检查」,而不是「检查有竞态」。 exit_plan_mode 的四项校验里没有一项与委派有关,默认策略文本也没提在途子代理,模型只能靠轮询,轮完还提交不了就会被放行(来源)。
DeepSeek Harness plan mode 的真实 trace:第 138 步还在 running
复现的关键不是模型不听话,而是它按现有工具面做了唯一能做的事:轮询、看到 running、然后按策略要求在响应里只留一个工具调用。
- 在 plan mode 下用一次 assistant 消息起两个只读调研子代理,默认后台运行。预期两个都返回 id,父轮次继续。
- 让父 Agent 继续工作,观察
list_agents的输出。预期多次返回[running],报告里的 trace 轮询了 8 次。 - 在子代理还没产出报告时,让父 Agent 提交计划。预期
exit_plan_mode成功进入评审,界面没有任何「仍有委派」提示。 - 检查父 Agent 的 reasoning。预期它明确写了「实施阶段再把这些子代理的发现并进来」——即它自己知道输入不全。
- 用户 dismiss 后再等报告落定,父 Agent 立刻提交 v2 并被批准。预期 v1 那次评审对用户是纯干扰。
- 翻一次会话事件,确认
tool/call与plan/mode的时间关系。预期能看到提交发生在子代理 turn 结束之前,而不是之后。
子代理的生命周期与诊断还有另一类问题,见 DeepSeek Harness 子代理诊断与生命周期排查。
为什么「加提示」挡不住
因为提示与执行是两条路:策略文本可以写「先等」,但工具调用是否被接受由代码决定,中间没有强制关系。
- 看默认
plan:policy文本:它列了完整计划要覆盖什么(目标、改动分组、接口与数据流、边界、失败模式、测试、验收),但一个字没提委派。 - 再看工具描述:
subagent说自己「立刻返回 id,结束后会通知你」,于是模型的等待原语只有「结束本轮等通知」。 - 但同一份策略又要求「提交计划时,让
exit_plan_mode成为该响应里唯一且最后一个工具调用」,模型就把「先 list 一下再提交」当成合理路径。 - 结论:只改文案,模型仍可能在看到
[running]后提交;只有让调用失败,计划才进不了评审。 - 检查当前发布版还剩哪句委派指引:讨论里核对到只剩「继续做有用的事,等它们跑完」,没有「只剩等待时就结束本轮」。预期你能在 profile 文本里找到同样的缺口。
- 自己做一次对照:在 policy 里加一句「有在途委派不要提交」,再跑同一 trace。预期提交仍可能发生,这就是需要硬门禁的实证。
根因:DeepSeek Harness 的 exit_plan_mode 只校验四项
四项校验分别是调用者 agent 存在、plan mode 对该会话处于 active、计划以 # 标题开头、userQuestions 通道可用;子代理、委派目录、收件箱一个都没读。
- 对照讨论中的源码复核,
execute的检查顺序就是上面四项,任何一项不满足才拒绝。预期你能在本地版本里找到同样结构。 - 注意「running」的语义:它是子代理自身 turn 的状态,不等于「报告尚未送达」。写守卫时要把这句话说清,否则误判。
- 查
list_agents的投影:mode不是continuable的子代理直接返回空。预期 one-shot 委派既等不到、也列不出。 - 查有没有阻塞式等待可用:树里确实有带
wait的作业工具,但作业与委派是两套面,它 join 不到子代理。 - 所以现状是:没有强制检查、没有可见的等待原语、没有覆盖全部委派的列表。三个缺口叠在一起。
路径一:用 DSH插件 在 tools/pre-execute 按 exec.agent 与 exec.name deny
这是不改核心、当天就能上的路径:在每次工具调用前拦一道,调用者是某 agent、工具名是 exit_plan_mode、且它有子代理 turn 在跑,就拒绝并给出原因。
- 写一个 DSH插件,注册
tools/pre-execute监听。预期每次工具调用都会经过它。 - 在监听里先判
exec.name === 'exit_plan_mode',其他工具直接放行。预期正常工具调用零影响。 - 用
exec.agent查它的子代理列表,筛出 turn 状态为 running 的条目。预期拿到 id 与名称。 - 命中就返回 deny,reason 里直接写「还有 N 个委派在运行,先结束本轮等结算通知,再提交完整计划」。预期模型在当下读到这句,而不是靠 prompt 记忆。
- 释放条件挂在子代理结算事件上,不要用倒计时。预期不会把「提前提交」换成「永远提交不了」。
- 在 reason 里声明覆盖面:只对能被证明在跑的 continuable 子代理生效,one-shot 不在可见范围内。预期读者不会误判。
- 用第 1 节的 trace 重跑:预期第 139 步的调用被拒,父 Agent 结束本轮,报告落定后再提交。
- 观察一次「守卫误伤」:子代理刚好结算、状态尚未刷新时调用,预期最多多等一轮,而不是永久拒绝。
路径二:改 DeepSeek Harness 的 execute,读取子代理后拒绝
如果你能改核心,门禁应该长在工具自己身上:进入评审之前先读 live descendants,有 running 就直接失败,评审界面根本不打开。
- 在
execute四项校验之后加一步:取当前 agent 的子代理列表。预期能拿到运行中的委派。 - 只要存在 running 的委派就抛错,错误信息列出具体 id 与名称。预期用户与模型都能看懂在等谁。
- 保证校验发生在「询问用户」之前,而不是评审之后再回滚。预期计划不进入评审界面。
- 保持
approved输出结构不变,只增加失败路径。预期调用方无需适配返回格式。 - 同步更新
plan:policy,写入「计划必须覆盖全部已委派调查;有在途委派时先等它们落定」。预期模型不必靠试错学会。 - 补一条回归用例:起一个后台子代理,立即调用
exit_plan_mode,断言失败且错误里带子代理 id。 - 再补一条 one-shot 用例:预期这条路径同样被拒,因为核心侧能读到全部 descendants,不像投影那样过滤。
- 验证拒绝后计划文本没有被写入评审记录。预期用户完全看不到 v1。
路径三:在 DeepSeek Harness 的 UI 禁用提交按钮
这是最弱的兜底,只减少误触,不阻止模型调用;它适合作为前两条之外的附加层,而不是替代品。
- 在计划评审界面订阅会话的子代理状态。预期 running 数量变化时按钮状态跟着变。
- 有 running 时把「批准」或「提交」置灰,并在旁边显示在途委派数量。预期用户能看懂为什么点不了。
- 给出「仍要查看」的只读入口,允许预览计划但不允许批准。预期不会把评审完全堵死。
- 明确这条路径的局限:模型侧的
exit_plan_mode调用不受按钮影响,计划仍可能已经产生。预期不把它当安全边界。 - 用同一 trace 验证:预期界面显示 2 个在途委派,v1 无法被批准,等报告落定后才可点。
- 刷新页面重试:预期状态从会话事件重建,不依赖组件内存,running 仍在时按钮依旧禁用。
DeepSeek Harness 拦截点该放在哪一层
只有前两条能挡住模型,UI 禁用只是人工兜底;要立刻见效选路径一,要正解选路径二。
| 路径 | 拦截点 | 能否挡住模型 | 主要限制 |
|---|---|---|---|
| pre-execute deny | 工具调用前 | 能 | 看不见 one-shot,只能拒不能等 |
| execute 加检查 | 工具内部 | 能,且最彻底 | 需要改核心,升级会冲突 |
| UI 禁用 | 评审界面 | 不能 | 只是人工兜底 |
要立刻见效选路径一;要正解选路径二;路径三只当附加。
顺带说清一个取舍:路径一能覆盖已发布版本,但它的可见范围受投影限制;路径二覆盖面完整,代价是每次升级都要重新对一遍改动。
守卫写歪了会变成什么
- 不要只用「在评审界面列出在途委派」当方案,报告里的 trace 已经证明模型照提不误。
- 守卫的释放必须来自状态变化(子代理结算),不能来自超时。
- 「running」是子代理 turn 在跑,不严格等于「报告未送达」,文案里别混用。
- one-shot 委派是已知盲区,任何声称「全量覆盖」的守卫都不准确。
- 计划质量本身无法被门禁保证:门禁只能强制顺序,不能强制模型读懂结论。
- 以上校验项与代码位置来自讨论中的社区核对源码,上游未修,按临时方案对待。
常见问题
因为 DeepSeek Harness 的 exit_plan_mode 在 execute 阶段只校验四项:调用者存在、plan mode 逻辑上处于 active、计划以 # 标题开头、有 userQuestions 通道。它完全不读子代理状态,所以委派未落定时照常进入评审。
DeepSeek Harness 的 pre-execute 能挡住:它是每次工具调用的瀑布,能拿到调用者 agent 与工具名,符合条件时返回 deny 并附上原因。它只能拒绝,不能让模型等待,所以释放条件必须是子代理真的结算,而不是计时器。
因为 DeepSeek Harness 的 list_agents 投影对 mode 不是 continuable 的子代理直接返回空,one-shot 子代理会被遍历但不会列出。所以守卫只能证明「看得见的那些还在跑」,文案里必须写清这个盲区。
只列出来不够:DeepSeek Harness 的报告里 trace 显示模型在 list_agents 返回 running 之后仍然提交了计划,说明提示拦不住 stale plan 进入评审。把决定权交给用户只是把系统问题降级成人工兜底。
DeepSeek Harness 的 execute 内部能读到全部 descendants,包括 list_agents 投影里看不见的 one-shot 委派,覆盖更完整。代价是要改核心,每次升级都得重新对一遍改动,适合能维护分支的团队。
相关术语
- exit_plan_mode
- exit_plan_mode 是 plan mode 下提交计划进入评审的工具,只有一个 plan 参数与一个 approved 布尔输出。当前实现只校验调用者、模式状态、标题格式与提问通道四项前置条件。— https://github.com/deepseek-ai/deepseek-harness/discussions/9876
- tools/pre-execute
- tools/pre-execute 是每次工具调用前的瀑布钩子,参数包含待执行调用的名称、解析后的参数与调用者 agent,返回值可以是 allow、deny、cancel 或 ask。它适合做按调用者状态的准入拦截。— https://github.com/deepseek-ai/deepseek-harness/discussions/9876
- continuable subagent
- continuable 指可继续交互的子代理模式,其条目会出现在 list_agents 的投影里。one-shot 子代理不在该投影中,因此任何基于 list_agents 的守卫都看不到它们。— https://github.com/deepseek-ai/deepseek-harness/discussions/9876
来源
- exit_plan_mode presents a plan while background subagents it depends on are still running· deepseek-ai/deepseek-harness Discussions
- DeepSeek Harness 官方仓库· deepseek-ai