dsh-exploration-guard
Verifieddsh-exploration-guard · v0.5.3 · MIT · Web UI
Progress-gated exploration brake for DeepSeek Harness: counts agent-loop steps since the last task-node advance and denies tool calls when the model keeps exploring without moving the task list or goal; optionally reshapes full-file reads into an outline,
Install
dsh plugin add dsh-exploration-guard Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Published to npm without a public repository. Inspect the package contents before installing.
Tags
Creators
Readme
dsh-exploration-guard
一句话:按任务进展熔断探索黑洞——连续 N 个 agent-loop step 没有推进任务节点,就否决非进度工具调用;再拖到 M 步,只剩"记任务 / 报告 / 求助"还能调。
病征(为什么不是"重复调用")
DeepSeek 系模型在卡住时不会原地重复同一调用,而是不断换新调查:找源码、解压会话、写临时脚本、换假设、再找一次——每一步都"有产出",但任务节点一动不动。用"同参数连续"判据抓不到它,因为参数每次都在变。
所以判据换成任务进展:
- 计数单位 =
agent/pre-step(agent-loop 的 step,不是 turn)。 - 进展 = 真的推进了任务节点:
todo_write:completed计数比上次多(首次建表也算一次进展,避免开局就被拦)update_goal:action为complete/blockedcreate_goal:新建目标
- 连"同一个列表原样重写"都不算进展(只改了措辞不算推进)。
两档熔断
| 档 | 触发 | 行为 |
|---|---|---|
| soft(默认 15 步/次) | 距上次进展 ≥ softSteps(只按步) |
原生:否决非进度工具;PTC:不打断正在跑的程序,警告追加在该次 run_code 结果末尾 |
| hard(默认 25 步/次) | ≥ hardSteps |
只放行 allowAlways(todo_write / get_goal / create_goal / update_goal / ask_user_question / meta_route / meta_sign),其余否决;PTC 下额外关闭 run_code 本身 |
PTC 与原生分档(v0.5.3):软阈值对 PTC 只警告不拦截——批量程序被打断的代价高于多跑几个调用,模型的典型反应还是盲目重试。警告追加在这次 run_code 结果的内容块末尾(原输出原样留在前面):不覆盖返回值、不另发消息、不改状态位——覆盖返回值等于篡改工具输出,发独立消息则与工具结果的顺序/状态对不上。硬阈值才真关闸,关的是 run_code 入口(已开跑的程序照跑完),文案刻意不同(run_code is closed at the hard threshold …),免得模型把「入口被锁」误读成「程序内某调用被拒」。
计量单位(v0.5.3):只按步。since = 当前步 - 上次进展步。一段 run_code 程序算一步,程序内多少次嵌套调用都不推进熔断——程序是原子的,按调用数计会惩罚「一个程序做一批事」这种本来最省的做法;PTC 侧真正兜底的是硬阈值对 run_code 入口的锁(迟到一步,但状态一致)。原生模式下一次调用≈一步,两种口径等价。
否决发生在 ctx.tools.guard():同步、单调、不可翻案,理由作为工具错误回到模型手里——不注入消息、不占用户发言位、不碰 llm/stream。
文案语言:Loops(模型面)按惯例只用英文——否决理由固定为英文,不随界面语言切换;中英双语只出现在设置面板上。
- 原生 soft / hard:见
messages.js的softBlock/hardBlock(Blocked: … 句式)。 - PTC soft 告警:
Progress warning: …(追加在结果末尾,不是错误);PTC hard:Blocked: run_code is closed at the hard threshold …。
被 hard 拦住后怎么自救:唯一出路是出现新的完成项(todo_write 中 status=completed 条目的内容签名此前没出现过)或 update_goal 走到终态;把同一张表原样重写不算。v0.5.2 起按身份集判定:只比 completed 计数会被「重写清单时删掉旧完成项」骗死(计数不变,永远解不开)。若一步里同时发 todo_write(推进)和后续调用,后者看到的是同一 step 内的 since = 0,会被放行——这是有意留的"记完就干"通道。
例外(不算无进展)
- 等 Owner:agent 进入
idle时计数清零(resetOnIdle,默认开)。 - 进度工具本身:永远放行,否则模型被拦住后无法自救。
- 无任务清单:默认按无进展计时(
noTaskMode: 'count');设'skip'则只对建过任务表的会话计时。
读取整形(v0.4.0,可关)
Owner 的观察:全文 read 是上下文成本的主项,而模型又习惯在 edit/write 之后重读整个文件确认改动。所以本插件在自己的 tools/post-execute 里加了一段纯读取整形:它不改变熔断的计数与阈值(ctx.tools.guard 一行未动),但和熔断共用整机开关 enabled——把 enabled 关掉,熔断与整形一起停(三个读取开关仍在面板上,此时点它们不会有作用)。五个读取开关在同一张设置卡片里:
| 开关 | 默认 | 行为 |
|---|---|---|
| Markdown 全文读 → 标题大纲 | 开 | 无 offset/limit 的 .md/.mdx 读,≥ outlineMinLines(默认 80)行时只回 # 标题 + 每节的行区间 |
| GDScript 全文读 → 成员表 | 开 | 同上,.gd 只回第 0 列的 class_name/extends/class/signal/enum/const/var/func(含 @export 等注解);函数体里的缩进局部变量永不计入 |
| 编辑后重读 → 编辑窗口 | 开 | 同会话里刚 edit/write 过的文件再被全文读时,只回锚点行上下共 verifyRegionLines(默认 24)行 + 总行数 + 指纹 |
| 整形最小行数 | 80 | 小于它的文件原样返回 |
| 编辑窗口行数 | 24 | 窗口总行数(不是单侧) |
永远可逆:offset/limit 的显式范围读永不被整形;大纲条目带"起行-止行"(末节终点即文件尾行,模型看不到正文时尾行只有这里给),末行只写选读意图——不给可照抄的整文件命令:示例照抄出来就是"再读一遍全文",把整形教成了绕行通道。
绝不说谎的判定顺序:只对 kind==='accept'、无 value、isError!==true、参数里没有 offset/limit、会话工作区已知(agent.session.header.cwd,不猜进程 cwd)、文件可读、结果含文本块、整形后严格更省(大纲 ≤ 原文 60%、窗口 ≤ 70%)的读生效;任何一步不满足就原样放行。.md 的围栏代码块里的 # 不算标题,utf8 BOM 先剥掉(否则第一行标题会整条消失——实测事故)。
已知边界:.md/.gd 之外的扩展名不整形;锚点必须逐字出现在文件里才出窗口,否则退回大纲或原文;锚点一次性消费(第二次全文读不再给窗口);edit/write 的锚点按会话隔离、20 分钟过期、最多 16 个路径。
grep 摘要(v0.4.1,可关)
grep 的单次开销来自命中条数而不是行长度(实测本项目:代码命中行 avg 52–62 字符、p90 104、max 192;.md 命中行才长,avg 152 / max 323)。而宽泛搜索动辄上千命中:var 在 project/**/*.gd 里是 2081 条、171 个文件,工具只返回前 250 条。
所以对命中数 ≥ grepSummaryAfter(默认 80) 的 grep,改回一份索引:
[exploration-guard grepSummary] Index, NOT all matches: showing 250 of 2081 matches, 26 files in this listing. This listing saved to: <spill 路径>
(The complete result could not be saved; narrow pattern, path, or include to see more.) ← 工具自己的括注原样带上来
path/File.gd (37 matches)
Line 4: @onready var vbox_container: VBoxContainer = $VBoxContainer
Line 6: ...
Line 8: ...
+34 more at lines 10, 12, 14, ... ← 行号一个不丢
要点:
- 每文件只留前 3 条原文(
grepPerFile,面板可调),其余只给行号——想读哪行就read file_path=<文件> offset=<行> limit=<n>,想再搜就收窄path/include。 - 索引自身必须小:最多列 40 个文件(其余一句
+N more files指向 locator)、总行数 ≤ 200;稠密文件(>20 条)只留 1 条样例,>100 条只给行号。原因很实在:dsh-plugin-save-token对 >240 行的工具输出还有一道 head/tail 窗口,索引要是超过它就会被二次裁掉。 - 无损可取:本次 grep 的完整输出逐字写进 spill(
spillStore),摘要第一行就给该路径 + 真实行数(offset=1 limit=<总行数>);工具自己另存的那份(若有)也一并列出。输出 >512KB 时不落盘、不整形(省下的上下文换成几 MB 临时文件不划算)。 - 识别工具自身的截断:
Found 250 of 769 matches这种头也能摘要,且明细条数必须与of前的数字相等才动手;对不上(例如被其它插件裁剪过)就原样放行。 - never-worse:摘要至少省 25%,否则给全量。
- 命中数低于阈值、无
spillStore、认不出格式、开关关闭 → 一律原样。
设置界面
设置 → 探索熔断(settings.section,order 640)里有十二个开关/数值:启用、软阈值、硬阈值、"无任务清单时"口径、等待 Owner 时清零、Markdown 大纲、GDScript 成员表、编辑后窗口、整形最小行数、编辑窗口行数、grep 摘要、grep 摘要阈值。保存即写入 $DSH_HOME/exploration-guard/config.json 并立即生效。
- 面板后端:
GET/POST /exploration-guard/api/config(宿主webServer.register)。 - 优先级:文件值覆盖 patch 里的初始 config;patch 只作出厂默认。
- 数值校验:step 阈值取 1–500 的整数;
hardSteps低于softSteps时自动抬到softSteps。
存储与隐私
- 文件内容:
$DSH_HOME/exploration-guard/config.json,JSON.stringify(values, null, 2)后以 UTF-8(无 BOM) 写出;纯 ASCII、两空格缩进,实测 40 字节。只写改过的键(sparse),不含路径、时间戳、会话 id、用户名。 - 面板显示:只显示符号路径
$DSH_HOME/exploration-guard/config.json,不显示C:\Users\...。 - API:
file字段是符号路径;宿主异常只回{ok:false,error:'settings request failed'},异常正文(可能含绝对路径)只写宿主日志。 - 回归:夹具断言响应既不含测试 home 目录,也不含
Users。
配置(patch 兜底)
profile 的 cordis.patch.yml 按 id 覆盖(整行 config 整体替换,要保留的键需列全):
- id: exploration-guard
name: dsh-exploration-guard
config:
enabled: true
softSteps: 15
hardSteps: 25
noTaskMode: count
resetOnIdle: true
outlineMd: true
outlineGd: true
verifyRegion: true
outlineMinLines: 80
verifyRegionLines: 24
grepSummary: true
grepSummaryAfter: 80
enabled: false 即整机关闭(等效于回到未安装状态)。
安装 / 回滚
dsh plugin --profile desktop add dsh-exploration-guard # 从 npm
dsh plugin --profile desktop add link:/abs/path/to/dsh-exploration-guard # 从本地源码
dsh plugin --profile desktop remove dsh-exploration-guard
回归
node dsh-plugins/dsh-exploration-guard/test/run.mjs # 9 条熔断夹具 + 10 条读取/grep 整形夹具
覆盖(v0.5.2:13 条熔断夹具):PTC 软档不打断 + 返回后告警、PTC 硬档锁 run_code 且文案独立、完成项身份判定(删旧项后新增仍算进展)、原样重写不算、阈值以下放行、soft 档否决与非进度工具豁免、首次建表算进展、完成数增量判定(原样重写不算)、hard 档文案与 allowAlways、idle 清零、noTaskMode: skip、开关关闭 / 无 agent / 跨 agent 隔离;以及 .md 大纲(围栏伪标题排除、正文不外泄、指纹与可照抄的逃生口)、.gd 成员表(缩进局部变量排除)、小文件/显式窗口/错误/非文本/读不到/无工作区一律原样、edit 后窗口(总行数+指纹+锚点逐字+窗口正好 verifyRegionLines 行+一次性消费)、write 后头部窗口(文案不说成 edited region)、verifyRegion 单独关闭、三个开关各自可关与配置 API 往返、BOM 文件首行标题不丢;grep 摘要(完整形态、Found N of M 截断形态、工具括注不当成文件、计数不符原样、无 store 原样、开关关闭原样、spill 内容逐字)。
跑夹具要用带 node 的运行时;本机 PATH 里没有 node,用 DSH 自带的:C:\Users\<user>\AppData\Local\Programs\DeepSeek Harness\resources\runtime\primary-runtime\dependencies\node\bin\node.exe。
边界
- 只认任务节点(todo / goal)为进展;写文件、读文件、跑测试都不算——这符合 Owner 的口径"没有推进任务节点即无进展"。
- 不判断探索是否"有价值":它只度量"有没有落到任务上"。
- 不结束回合、不取消 agent、不写会话事件。
文案契约(Owner 2026-10-28)
受限时模型最容易犯的错是"重写同一张清单然后继续",所以文案必须同时回答两件事:
| 必须说 | 内容 |
|---|---|
| 什么无用 | 重写同一张 todo 清单(completed 不增加就不算进展)、继续读/写/跑 shell |
| 什么有用 | 把真正做完的事标 completed、update_goal blocked、问 Owner |
| 什么禁止 | 为了让拦截放过而假标完成——文案明写 never mark work complete just to unblock |
| 现状锚点 | 带上 completed=N 与 step(s) 计数,模型才知道差多少 |
现行两条(英文,Loops 侧唯一语言):
soft: Blocked: {steps} step(s) since the last completed todo (soft={soft}, completed={done}). Rewriting the list does not count. Honest ways out: mark a genuinely finished item completed, update_goal blocked, or ask the Owner. Other tools are refused.
hard: Blocked: {steps} step(s) (hard={hard}, completed={done}). Only honest progress passes: mark a genuinely finished item completed, update_goal blocked, or ask the Owner — never mark work complete just to unblock. Everything else is refused.
实机验证(把 soft 临时降到 1):soft 档返回上面第一条、hard 档返回第二条,两条都自带"该做什么",恢复阈值后 {"softSteps":15,"hardSteps":25}。