dsh-drift-guard
v0.1.0
Published
drift-guard (防跑偏): zero-dep drift guard for DeepSeek Harness (dsh) coding agents — loop retry, empty-turn and result-stall detection, optional hard deny. Advisory by default.
Maintainers
Readme
dsh-drift-guard 防跑偏
DeepSeek Harness (dsh) 的 agent 漂移守护插件:零依赖、默认只提醒不拦截,在 coding agent 跑偏烧 token 时拉它一把。检测三种高发跑偏形态,并可选的硬阻断:
- 结果停滞(R1):参数在变、结果原地打转——"换着花样重试同一个失败",对目标无实际进展。
- 空转回合(C):一个回合烧了大量输出 token(多为 reasoning)却零工具调用、几乎无文本产出就结束。
- 循环重试(B):同一工具 + 同一参数反复调用(如
npm test连跑 3 次不读失败原因)。 - 硬阻断(L2,opt-in):
tools/pre-execute在链达阈值时执行前 deny(工具根本不跑)。
与官方守卫的分工:官方
@deepseek-ai/dsh-repeat-tool-reminder由 dsh-base 宿主层挂载 (阈值[3,5,8],建议式),所有 standard 系会话自带——循环重试(B)的建议提醒官方已覆盖。 因此 drift-guard 的循环提醒默认关闭;它的独有价值是官方没有的: 结果停滞(R1)(官方只看输入 name+args,抓不到"参数变了结果不变")、 空转回合(C)、以及官方纯建议、drift-guard 可执行前拦截的 L2。
安装
前置:Node.js ≥ 22.19、已装 dsh CLI(含 pnpm)。插件以 npm bundle 包形式提供,
dsh plugin 会把它装进你选择的 profile(web / dsh-tui / 其它),装完对
该 profile 下所有 preset 的会话生效(宿主层挂载,与官方 repeat-tool-reminder 同层)。
方式一 · dsh 命令(推荐)
把 <名字> 换成你的 profile(本机常见 dsh-tui 或 web):
dsh plugin --profile <名字> add dsh-drift-guard
# 例:dsh plugin --profile dsh-tui add dsh-drift-guard装完重启该 profile(退出 dsh-tui / 停掉 web 再启动),开新会话即生效。
确认装上的方法:dsh --profile <名字> --dump-config 里应能看到 id: drift-guard 行。
卸载:dsh plugin --profile <名字> remove dsh-drift-guard(同样需重启)。
方式二 · 让 dsh 自己安装(提示词)
在任意 dsh 会话里粘贴下面这段(dsh 会自己执行命令):
请帮我安装 dsh-drift-guard 插件:先运行
dsh plugin --profile dsh-tui add dsh-drift-guard(把dsh-tui换成我实际在用的 profile 名),确认安装成功后, 告诉我需要重启哪些进程(退出并重开 dsh-tui / 重启 web 服务)插件才会生效, 以及装好后怎么快速验证。
安装后验证(30 秒)
开一个新会话,故意做三件"错事",应各看到一条 guard 提醒(additionalContexts,
source 为 drift-guard):
- 同参循环:同一命令连发 3 次相同调用(官方 reminder 提醒,3/5/8 档);
- 改参空转:用不同参数连跑同一个失败命令 4 次(第 4 次触发
stalled × 3); - 空转回合:连续 2 个回合只推理不调用工具、不输出文本。
不想看到提醒的正常工作完全不受影响(低误报设计,见下)。
作用范围与去重(重要)
- 宿主层挂载 = 该 profile 的所有会话都生效,无论选哪个 agent preset;
- 若你的某份 agent preset 里也挂了 drift-guard 行(本地 preset 用法,见仓库
agent.cordis.yml里的 drift-guard 段),会和 npm 宿主挂载双挂重复提醒——二选一: 用 npm 包就把 preset 里的 drift-guard 行删掉;反之别装 npm 包。
配置(全部可选,默认值已可用)
作为宿主插件装好后
改你的 profile 自己的 patch($DSH_HOME/profiles/<名字>/cordis.patch.yml),
按 patch 语义整行替换(要写全所有键):
- id: drift-guard
name: dsh-drift-guard
config:
enabled: true
loop:
enabled: false # 官方已在 [3,5,8] 提醒;想用 drift-guard 的循环就开并避开 3/5/8
thresholds: [3, 5, 8]
exclude: []
argumentsPreviewChars: 500
hardDeny:
enabled: false # L2 硬阻断(执行前 deny),默认关
threshold: 10
emptyTurn:
enabled: true
minOutputTokens: 8000
maxTextChars: 600
minTurns: 2
resultStall:
enabled: true
mode: error # 'error'=只对失败结果建指纹(最稳) / 'any'=成功也算(更激进)
threshold: 3
minTextChars: 16
normalizeCap: 2048
exclude: []| 键 | 默认 | 说明 |
| --- | --- | --- |
| enabled | true | 总开关(false = 不注册任何监听器) |
| loop.enabled | false | 循环链建议提醒。默认关:官方 repeat-tool-reminder 已在 3/5/8 档提醒 |
| loop.thresholds | [3,5,8] | 连续同参调用达此数时提醒;想开时建议避开 [3,5,8](如 [6,10,15]) |
| loop.exclude | [] | 通配工具名,被排除的调用不计链也不重置链(例:['todo_write']) |
| loop.argumentsPreviewChars | 500 | 详细提醒里参数预览截断长度 |
| hardDeny.enabled | false | L2 硬阻断:链达阈值时执行前 deny(工具不跑)。可独立于 loop 工作 |
| hardDeny.threshold | 10 | 硬阻断阈值 |
| emptyTurn.enabled | true | 空转回合检测 |
| emptyTurn.minOutputTokens | 8000 | 回合累计输出 token 下限 |
| emptyTurn.maxTextChars | 600 | 回合文本产出上限(排除大文本回答/报告) |
| emptyTurn.minTurns | 2 | 连续空转回合数达到才提醒 |
| resultStall.enabled | true | 结果停滞检测(R1,默认主力之一) |
| resultStall.mode | error | error=只对失败结果建指纹;any=成功结果也算(慎开) |
| resultStall.threshold | 3 | 连续"参数变+结果不变"达此数提醒一次 |
| resultStall.minTextChars | 16 | 清洗后文本短于此的结果不建指纹(防琐碎输出误报) |
| resultStall.normalizeCap | 2048 | 参与哈希的清洗文本长度上限;0=全文 |
| resultStall.exclude | [] | 通配工具名,透明(不计不重置) |
| phrases.* | 见源码 | 提醒文案模板覆盖({tool} {count} {turns} {tokens} 占位符) |
干预档位组合语义
| loop | hardDeny | 行为 |
| --- | --- | --- |
| off | off | 无链行为(循环交给官方 3/5/8;guard 只跑空转 C + 结果停滞 R1) |
| off | on | 链静默推进,达阈值时执行前 deny(无建议、纯执法) |
| on | off | 与官方类似的建议式提醒(按自设阈值) |
| on | on | 先按 thresholds 建议,达 hardDeny.threshold 后执行前 deny |
resultStall(R1)与上表完全正交:只依赖 post-execute,loop/hardDeny 全关也照常工作;
精确重复(同参同果)对它透明(那是 loop/官方的活),不会与任何档位重复提醒。
检测口径(低误报设计)
- 读文件/探索不算跑偏;"做了却没产出"才算。
- 空转签名:
completed结束 + 零工具调用 + 输出 token ≥ 阈值 + 文本 ≤ 阈值。 - 结果停滞签名:参数 key 变化 + 结果指纹相同连续达阈值才提醒。指纹 = 清洗
(时间戳/epoch/UUID/hex 哈希/Windows 绝对路径/耗时 → 占位符、压空白)后的文本
FNV-1a 64 哈希;
mode:error下成功结果不参与。真正换了结果的调用重置连续计数。 - 提醒走
tools/post-execute的additionalContexts(官方 guard 同款通道:持久化、 模型可见、可回放);不替换任何结果内容。 - 状态仅驻留内存(与官方 guard 同取舍):resume 后从全新计数开始,不翻旧账。
- 任何运行时异常降级为"放行一切 + warnOnce",guard 自身的 bug 不会毁会话。
验证证据
- 假 ctx 单测 75 项断言全过(配置 fail-loud、链触发/重置、三类非误报豁免、
硬阻断 deny、各检测器解耦、
enabled:false零注册)。 - 离线回放:真实会话日志 255 对工具调用重放 → 结果停滞 0 误报; 合成"3 次改参同错"序列第 4 次准时触发。
- 真机挂载(preset 形态):real session 组合加载无错;同参演练 drift-guard 提醒恰好注入一次, 其余调用零误报;官方 reminder 另发一条(证明同参重复需靠 loop 默认关来避免)。
从源码开发 / 测试
git clone https://github.com/lxwallac/dsh-drift-guard
cd dsh-drift-guard
node test-drift-guard.mjs # 75 项断言
node replay-stall.mjs # 离线回放验证(读本机会话日志)另一种用法:作为 agent preset 的本地行(高级)
不装 npm 包时,也可以把 drift-guard.mjs 放进你自己的 user preset 目录
(~/.dsh/.agent-presets/<名字>/),并在其 agent.cordis.yml 加一行:
- id: drift-guard
name: ./drift-guard.mjs
config:
enabled: true
# ...同上配置表,缺省用默认这种用法只对你选了这个 preset 的会话生效,且不会与 npm 宿主挂载混用(见上文去重说明)。
仓库根目录的 agent.cordis.yml 是本地组合示例(含 @deepseek-ai standard 副本 + drift-guard 行),
按 .gitignore 不进本仓库。
