dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
Fix malformed tool-call arguments (nested arguments wrappers, key aliases) at the dsh llm/stream seam. 修复畸形工具参数的 dsh 插件
修复模型产出的畸形工具参数,挂在官方
llm/streamwaterfall 上。 Fixes malformed tool-call arguments produced by models, hooked on the officialllm/streamwaterfall.
为什么需要它 · 修复家族 · 安全保证 · 验证数据 · 安装与使用 · 运维与排障 · 已知限制 — English section
部分模型(尤其小参数/长会话/高频工具调用场景)会产出畸形工具参数,最典型的是多套了一层 {"arguments":{…}} 包装。宿主把这层包装原样持久化进 tool/call 与 assistant/message,下一轮整段回放给模型;模型于是看到"我上一轮就是这么调的"→ 越模仿越像 → 越陷越深(实测包装层数 1→8→13→33→34→38,递归加深)。这就是"agent 自称跳不出来、只有重开会话才好"的真因——重开有效是因为坏示范没了,不是模型状态变了。
这个插件在参数被持久化之前拦截并修正,一处同时修好三件事:执行、持久化、历史回放——坏示范根本不会被写进会话。
不需要(v0.3.0 起)。 插件在两个方向同时闭环:
tool/call 在请求发往模型之前被就地修正(内存内克隆替换,持久化数据一个字节不动)。模型从此看不到坏示范 → 停止模仿 → 下一轮就自愈。输入侧与输出侧共用同一套判定(宿主校验器 + 违例严格减少 + 名字相关性),历史里的合法调用一个字节都不会变;WeakMap 记忆化保证同一条历史只修一次,不给每轮请求添开销。
v0.3.1 关键修正:llm/stream 的请求对象被 agent-loop
deepFreeze(含 messages),v0.3.0 在监听器里赋值options.messages会抛Cannot assign to read only property 'messages'并杀死整轮对话(只在有污染历史的老会话触发——宝可梦天国会话实测)。v0.3.1 改为包装llm.stream服务方法(clone-on-write:清洗结果放进浅克隆交给原方法,调用方的冻结对象零改动,与宿主 adapterStream 处理冻结请求的模式一致),清洗环节异常完全隔离——任何失败都降级为放行原始请求,绝不影响对话。
No (since v0.3.0). The plugin closes the loop in both directions: the output side fixes freshly produced malformed arguments before persistence (no new bad exemplars), and the input side (v0.3.0, redesigned in v0.3.1 as a llm.stream service wrapper with clone-on-write) sanitizes the replayed history before the request reaches the model. Both sides share the same acceptance rules (host validator + strict violation reduction + name relevance); valid history records are never touched, sanitizer failures degrade to pass-through, and a WeakMap memoizes each history block so repeated requests cost nothing.
以下每类都在 16,535 条真实 tool/call 记录里实测出现过,并逐条对着真实 tool/result 错误文本核对:
| 家族 | 形态 | 实测条数 | 报错 | 插件动作 |
|---|---|---|---|---|
| A | 嵌套包装 {"arguments":{…}}(1–38 层) | 547 | missing required property | unwrap:arguments(无条件解包) |
| B | 包装键别名 {"args":{…}} / {"parameters":{…}} | 2 | 同上 | unwrap:args 等 |
| C | 包装键 + 兄弟键混排 {"arguments":{…},"offset":5} | 1 | 同上 | merge:arguments(合并,冲突则放弃) |
| D | 参数键名别名 skill_name→name、path↔file_path、new_str→new_string、query→queries | ≈13 | 同上 | rename:x→y(需名字相关 + 违例减少) |
明确不修(语料证明它们不是缺陷,改了就是编造):把 JSON 当文本写进文件的 {"content":"{…}"}(75 条,报错全是"需先 read",与参数形状无关)、模型真漏必填(run_code 漏 description、{} 空参数)、兄弟键与内层重名的歧义合并。
判定基准是违例总数严格减少,不是"必填键齐了没":
@deepseek-ai/dsh-tools 的 validateJsonSchemaValue(宿主校验工具参数的那个函数)——修完后的参数是否真的过校验,由宿主说了算。加载不到时退化为内置保守计数器(只数必填缺失/浅层类型不符/多余键,刻意松:宁可少修不误修)。options.tools)判定,绝不缓存、绝不猜测。dev_stage_add 真有个 parameters 参数、str_replace_editor 真有个 path 参数,它们不会被解包/改名(语料里实测确认)。configuration 绝不会被塞进 description(语料里真实存在这个陷阱,已验证被拒绝)。arguments)无条件解包的理由:arguments 在 dsh 全量工具集里从未作为参数名出现(已在 node_modules 全量核验);它必须无条件,否则"无必填参数"的工具会静默吞掉包装——比报错更坏(模型以为调用成功,其实参数全丢)。null,块原样透传。missing required property 报错中,520 条完全修复(校验器判定 0 违例);3 条"修了但仍缺"(模型用了语义错键,如 glob 要 pattern 却写了 file_path——名字不相关,按纪律不猜);18 条未动(空 {} / 真漏必填 / 嵌套缺失,属"改了就是编造")。arguments 包装的潜在 bug(语义无损,只是让落盘的参数诚实)。BlockAssembler 全链路(适配器 chunk → 插件改写 → 宿主组装 → 校验器),修复后违例清零。把插件目录 link 进 profile,加进 bundles 数组,重启即持久生效:
// <profile>/package.json
{
"dependencies": { "@dsh-external/dsh-tool-args-unwrap": "link:/abs/path/to/dsh-tool-args-unwrap" },
"dsh": { "profile": { "bundles": ["@dsh-external/dsh-tool-args-unwrap"] } }
}
# <plugin>/cordis.patch.yml(仓库已附)
- bundle:
insert:
id: dsh-tool-args-unwrap
# 入口自动指向 lib/index.js
package.json 加 link: 依赖 + bundles 数组项(如上);确认生效:插件目录会写 state.json,看到 listenerRegistered: true 且 streamsSeen 随模型调用递增即在工作;hostValidator: true 表示已接入宿主校验器(最高保真);每次修正会打一条 warn 日志并记入 lastFix。
临时停用:设配置 enabled: false 即可完全停用流改写(不卸载插件)。
state.json:插件每次加载/修正时写。streamsSeen 不涨 → 监听器没装上(检查 bundle 是否装配);hostValidator: false → 用了内置兜底(功能仍正常,只是判定略松)。schemas-snapshot.json:每进程写一次,仅含工具名+参数名+类型+必填名,不含描述文本,用于离线核对。warn 日志含:provider/model、工具名、动作列表、字符数、累计次数。description、真漏了必填 → 插件不猜(也猜不了)。Fixes malformed tool-call arguments produced by models, hooked on the official
llm/streamwaterfall.
Some models (especially small-parameter ones, long sessions, high-frequency tool-calling) emit malformed tool arguments — most typically an extra {"arguments":{…}} wrapper. The host persists that wrapper verbatim into tool/call and assistant/message, then replays the whole history back to the model next turn; the model sees "that's what I did last time" and imitates its own mistake, recursively deepening (observed 1→8→13→33→34→38 layers). This is the real reason agents claim they "can't escape" and only a session restart helps — restart works because the bad exemplars vanish, not because model state resets. This plugin intercepts and fixes arguments before they are persisted, repairing three things at once: execution, persistence, and history replay — the bad exemplar never gets written into the session.
No (since v0.3.0). See the bilingual section above ("老会话需要重开吗?"). Both sides share the same acceptance rules; valid history records are never touched.
Every family below was actually observed in 16,535 real tool/call records and checked against the real tool/result error text one by one:
| Family | Shape | Observed | Error | Plugin action |
|---|---|---|---|---|
| A | Nested wrapper {"arguments":{…}} (1–38 layers) | 547 | missing required property | unwrap:arguments (unconditional) |
| B | Wrapper-key alias {"args":{…}} / {"parameters":{…}} | 2 | same | unwrap:args etc. |
| C | Wrapper + sibling keys {"arguments":{…},"offset":5} | 1 | same | merge:arguments (aborts on ambiguity) |
| D | Param-key alias skill_name→name, path↔file_path, new_str→new_string, query→queries | ≈13 | same | rename:x→y (needs name-relevance + fewer violations) |
Explicitly not fixed (the corpus proves these aren't defects; fixing them would be fabrication): {"content":"{…}"} cases that legitimately write JSON text into a file (75 of them, error was "must read first", unrelated to shape), genuine omissions (run_code missing description, empty {}), and ambiguous merges where a sibling key collides with an inner key.
The acceptance criterion is strictly fewer total violations, not "required keys present":
validateJsonSchemaValue from @deepseek-ai/dsh-tools (the very function the host uses) — whether a fixed argument actually passes is decided by the host itself. If unavailable it falls back to a conservative built-in counter (only missing-required / shallow type mismatch / extra keys), deliberately lenient: better to under-fix than mis-fix.options.tools), never cached, never guessed.dev_stage_add really has a parameters param, str_replace_editor really has a path param — they are not unwrapped/renamed (verified against the live toolset).configuration is never mapped onto description (this exact trap exists in the corpus and is verified rejected).arguments) is unconditional: arguments has never been a parameter name anywhere in the dsh toolset (verified across node_modules); it must be unconditional, otherwise tools with no required params would silently swallow the wrapper — worse than an error (the model thinks the call succeeded, but every argument was dropped).missing required property errors, 520 fully fixed (validator reports 0 violations); 3 "fixed but still failing" (model used a semantically wrong key, e.g. glob wants pattern but got file_path — not name-related, not guessed per policy); 18 untouched (empty {} / genuinely missing required / nested gaps — "fixing would be fabrication").arguments wrapper (semantically lossless — just makes the persisted arguments honest).BlockAssembler chain (adapter chunk → plugin rewrite → host assembly → validator), violations drop to zero after fixing.Recommended (how this plugin was verified): link the plugin dir into the profile and add it to the bundles array — persistent across restarts:
// <profile>/package.json
{
"dependencies": { "@dsh-external/dsh-tool-args-unwrap": "link:/abs/path/to/dsh-tool-args-unwrap" },
"dsh": { "profile": { "bundles": ["@dsh-external/dsh-tool-args-unwrap"] } }
}
# <plugin>/cordis.patch.yml (shipped with this repo)
- bundle:
insert:
id: dsh-tool-args-unwrap
Manual: clone the repo anywhere, add the link: dependency + bundles entry to the profile package.json (as above), restart dsh.
Confirm it's live: the plugin dir writes state.json; listenerRegistered: true with streamsSeen incrementing on each model call means it's working; hostValidator: true means the host validator is attached (highest fidelity); each fix emits one warn log and is recorded in lastFix.
Kill switch: set config enabled: false to fully disable stream rewriting without unloading.
state.json is written on load/fix; if streamsSeen doesn't grow, the listener isn't installed (check bundle assembly); hostValidator: false means the built-in fallback is in use (still works, just slightly looser). schemas-snapshot.json is written once per process — tool names + param names + types + required only, no description text — for offline auditing. Each fix's warn log carries provider/model, tool name, action list, char counts, and a running total.
Only shape-malformation is fixed, not semantic omission: if the model genuinely omitted description or a required key, the plugin doesn't guess (and can't). Cross-provider live reproduction isn't fully complete (network-restricted environment), but the defect shape and the fix judgement are provider-independent — the schema comes from the host, the validator is the host's. The deeper fix belongs upstream: validate + normalize tool-call arguments before persistence (this plugin is a pluggable reference implementation of exactly that).
lib/index.js the plugin, all logic
cordis.patch.yml bundle assembly declaration
schemas-snapshot.json live tool-schema snapshot (names/types/required only)
state.json live state (proves it's working)
package.json manifest
LICENSE MIT
MIT