DeepSeek Harness plan mode 子代理未结束提交?pre-execute 与 execute 校验

故障排查发布于 2026-10-20作者: DeepSeek Plugin 插件市场
DeepSeek Harnessplan modesubagentexit_plan_mode计划评审
DeepSeek Harness plan mode 下父 Agent 在后台 subagent 仍显示 running 时调用 exit_plan_mode,计划基于不完整调查进入评审。根因是该工具只校验四项、完全不读子代理。给出 pre-execute 拦截、execute 加检查、UI 禁用按钮三条路径。

这是 DeepSeek Harness 的「没有检查」,而不是「检查有竞态」。 exit_plan_mode 的四项校验里没有一项与委派有关,默认策略文本也没提在途子代理,模型只能靠轮询,轮完还提交不了就会被放行(来源)。

DeepSeek Harness plan mode 的真实 trace:第 138 步还在 running

复现的关键不是模型不听话,而是它按现有工具面做了唯一能做的事:轮询、看到 running、然后按策略要求在响应里只留一个工具调用。

  1. 在 plan mode 下用一次 assistant 消息起两个只读调研子代理,默认后台运行。预期两个都返回 id,父轮次继续。
  2. 让父 Agent 继续工作,观察 list_agents 的输出。预期多次返回 [running],报告里的 trace 轮询了 8 次。
  3. 在子代理还没产出报告时,让父 Agent 提交计划。预期 exit_plan_mode 成功进入评审,界面没有任何「仍有委派」提示。
  4. 检查父 Agent 的 reasoning。预期它明确写了「实施阶段再把这些子代理的发现并进来」——即它自己知道输入不全。
  5. 用户 dismiss 后再等报告落定,父 Agent 立刻提交 v2 并被批准。预期 v1 那次评审对用户是纯干扰。
  6. 翻一次会话事件,确认 tool/call 与 plan/mode 的时间关系。预期能看到提交发生在子代理 turn 结束之前,而不是之后。

子代理的生命周期与诊断还有另一类问题,见 DeepSeek Harness 子代理诊断与生命周期排查。

为什么「加提示」挡不住

因为提示与执行是两条路:策略文本可以写「先等」,但工具调用是否被接受由代码决定,中间没有强制关系。

  1. 看默认 plan:policy 文本:它列了完整计划要覆盖什么(目标、改动分组、接口与数据流、边界、失败模式、测试、验收),但一个字没提委派。
  2. 再看工具描述:subagent 说自己「立刻返回 id,结束后会通知你」,于是模型的等待原语只有「结束本轮等通知」。
  3. 但同一份策略又要求「提交计划时,让 exit_plan_mode 成为该响应里唯一且最后一个工具调用」,模型就把「先 list 一下再提交」当成合理路径。
  4. 结论:只改文案,模型仍可能在看到 [running] 后提交;只有让调用失败,计划才进不了评审。
  5. 检查当前发布版还剩哪句委派指引:讨论里核对到只剩「继续做有用的事,等它们跑完」,没有「只剩等待时就结束本轮」。预期你能在 profile 文本里找到同样的缺口。
  6. 自己做一次对照:在 policy 里加一句「有在途委派不要提交」,再跑同一 trace。预期提交仍可能发生,这就是需要硬门禁的实证。

根因:DeepSeek Harness 的 exit_plan_mode 只校验四项

四项校验分别是调用者 agent 存在、plan mode 对该会话处于 active、计划以 # 标题开头、userQuestions 通道可用;子代理、委派目录、收件箱一个都没读。

  1. 对照讨论中的源码复核,execute 的检查顺序就是上面四项,任何一项不满足才拒绝。预期你能在本地版本里找到同样结构。
  2. 注意「running」的语义:它是子代理自身 turn 的状态,不等于「报告尚未送达」。写守卫时要把这句话说清,否则误判。
  3. 查 list_agents 的投影:mode 不是 continuable 的子代理直接返回空。预期 one-shot 委派既等不到、也列不出。
  4. 查有没有阻塞式等待可用:树里确实有带 wait 的作业工具,但作业与委派是两套面,它 join 不到子代理。
  5. 所以现状是:没有强制检查、没有可见的等待原语、没有覆盖全部委派的列表。三个缺口叠在一起。

路径一:用 DSH插件 在 tools/pre-execute 按 exec.agent 与 exec.name deny

这是不改核心、当天就能上的路径:在每次工具调用前拦一道,调用者是某 agent、工具名是 exit_plan_mode、且它有子代理 turn 在跑,就拒绝并给出原因。

  1. 写一个 DSH插件,注册 tools/pre-execute 监听。预期每次工具调用都会经过它。
  2. 在监听里先判 exec.name === 'exit_plan_mode',其他工具直接放行。预期正常工具调用零影响。
  3. 用 exec.agent 查它的子代理列表,筛出 turn 状态为 running 的条目。预期拿到 id 与名称。
  4. 命中就返回 deny,reason 里直接写「还有 N 个委派在运行,先结束本轮等结算通知,再提交完整计划」。预期模型在当下读到这句,而不是靠 prompt 记忆。
  5. 释放条件挂在子代理结算事件上,不要用倒计时。预期不会把「提前提交」换成「永远提交不了」。
  6. 在 reason 里声明覆盖面:只对能被证明在跑的 continuable 子代理生效,one-shot 不在可见范围内。预期读者不会误判。
  7. 用第 1 节的 trace 重跑:预期第 139 步的调用被拒,父 Agent 结束本轮,报告落定后再提交。
  8. 观察一次「守卫误伤」:子代理刚好结算、状态尚未刷新时调用,预期最多多等一轮,而不是永久拒绝。

路径二:改 DeepSeek Harness 的 execute,读取子代理后拒绝

如果你能改核心,门禁应该长在工具自己身上:进入评审之前先读 live descendants,有 running 就直接失败,评审界面根本不打开。

  1. 在 execute 四项校验之后加一步:取当前 agent 的子代理列表。预期能拿到运行中的委派。
  2. 只要存在 running 的委派就抛错,错误信息列出具体 id 与名称。预期用户与模型都能看懂在等谁。
  3. 保证校验发生在「询问用户」之前,而不是评审之后再回滚。预期计划不进入评审界面。
  4. 保持 approved 输出结构不变,只增加失败路径。预期调用方无需适配返回格式。
  5. 同步更新 plan:policy,写入「计划必须覆盖全部已委派调查;有在途委派时先等它们落定」。预期模型不必靠试错学会。
  6. 补一条回归用例:起一个后台子代理,立即调用 exit_plan_mode,断言失败且错误里带子代理 id。
  7. 再补一条 one-shot 用例:预期这条路径同样被拒,因为核心侧能读到全部 descendants,不像投影那样过滤。
  8. 验证拒绝后计划文本没有被写入评审记录。预期用户完全看不到 v1。

路径三:在 DeepSeek Harness 的 UI 禁用提交按钮

这是最弱的兜底,只减少误触,不阻止模型调用;它适合作为前两条之外的附加层,而不是替代品。

  1. 在计划评审界面订阅会话的子代理状态。预期 running 数量变化时按钮状态跟着变。
  2. 有 running 时把「批准」或「提交」置灰,并在旁边显示在途委派数量。预期用户能看懂为什么点不了。
  3. 给出「仍要查看」的只读入口,允许预览计划但不允许批准。预期不会把评审完全堵死。
  4. 明确这条路径的局限:模型侧的 exit_plan_mode 调用不受按钮影响,计划仍可能已经产生。预期不把它当安全边界。
  5. 用同一 trace 验证:预期界面显示 2 个在途委派,v1 无法被批准,等报告落定后才可点。
  6. 刷新页面重试:预期状态从会话事件重建,不依赖组件内存,running 仍在时按钮依旧禁用。

DeepSeek Harness 拦截点该放在哪一层

只有前两条能挡住模型,UI 禁用只是人工兜底;要立刻见效选路径一,要正解选路径二。

路径拦截点能否挡住模型主要限制
pre-execute deny工具调用前能看不见 one-shot,只能拒不能等
execute 加检查工具内部能,且最彻底需要改核心,升级会冲突
UI 禁用评审界面不能只是人工兜底

要立刻见效选路径一;要正解选路径二;路径三只当附加。

顺带说清一个取舍:路径一能覆盖已发布版本,但它的可见范围受投影限制;路径二覆盖面完整,代价是每次升级都要重新对一遍改动。

守卫写歪了会变成什么

  1. 不要只用「在评审界面列出在途委派」当方案,报告里的 trace 已经证明模型照提不误。
  2. 守卫的释放必须来自状态变化(子代理结算),不能来自超时。
  3. 「running」是子代理 turn 在跑,不严格等于「报告未送达」,文案里别混用。
  4. one-shot 委派是已知盲区,任何声称「全量覆盖」的守卫都不准确。
  5. 计划质量本身无法被门禁保证:门禁只能强制顺序,不能强制模型读懂结论。
  6. 以上校验项与代码位置来自讨论中的社区核对源码,上游未修,按临时方案对待。

常见问题

DeepSeek Harness plan mode 里子代理还在 running,为什么还能提交计划?

因为 DeepSeek Harness 的 exit_plan_mode 在 execute 阶段只校验四项:调用者存在、plan mode 逻辑上处于 active、计划以 # 标题开头、有 userQuestions 通道。它完全不读子代理状态,所以委派未落定时照常进入评审。

用 DeepSeek Harness 的 tools/pre-execute 写一个 deny,能挡住提前提交的计划吗?

DeepSeek Harness 的 pre-execute 能挡住:它是每次工具调用的瀑布,能拿到调用者 agent 与工具名,符合条件时返回 deny 并附上原因。它只能拒绝,不能让模型等待,所以释放条件必须是子代理真的结算,而不是计时器。

为什么基于 list_agents 写的守卫只覆盖一半委派?

因为 DeepSeek Harness 的 list_agents 投影对 mode 不是 continuable 的子代理直接返回空,one-shot 子代理会被遍历但不会列出。所以守卫只能证明「看得见的那些还在跑」,文案里必须写清这个盲区。

只在 DeepSeek Harness 的评审界面把在途子代理列出来,够不够?

只列出来不够:DeepSeek Harness 的报告里 trace 显示模型在 list_agents 返回 running 之后仍然提交了计划,说明提示拦不住 stale plan 进入评审。把决定权交给用户只是把系统问题降级成人工兜底。

把检查写进 exit_plan_mode 的 execute,比插件拦截好在哪里?

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

来源