dsh-fast-mode 1.6.1 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BENCHMARK.md +123 -1
- package/CHANGELOG.md +48 -0
- package/README.en.md +10 -8
- package/README.md +10 -9
- package/client.js +90 -26
- package/docs/REFERENCE.md +37 -29
- package/index.js +74 -56
- package/package.json +2 -2
- package/src/config.js +18 -8
- package/src/deliberation.js +217 -0
- package/src/effort.js +7 -7
- package/src/state.js +17 -7
- package/src/usage.js +23 -0
- package/src/lazy.js +0 -220
package/docs/REFERENCE.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# 参考手册:原理、配置与边界
|
|
2
2
|
|
|
3
|
-
本手册描述 **dsh-fast-mode 1.
|
|
3
|
+
本手册描述 **dsh-fast-mode 1.7.1** 的实际行为(默认配置下的注入行为与 1.5.0 一致;1.5.14 起多了右键面板,1.7.0 起为七行(只数设置项;闪电本身就是开关,不占行),七行都只影响本会话;1.7.1 给其中一行加了一条按思考长度的提醒,行数不变),对应仓库里的 `index.js`、`src/` 与 `client.js`。它不是宣传页:凡是能测量的地方给出数字,凡是没测量的地方直接说没测量。
|
|
4
4
|
|
|
5
|
-
**默认契约:Fast 模式不改变思考强度、thinking 预算或模型。** `agent/request`
|
|
5
|
+
**默认契约:Fast 模式不改变思考强度、thinking 预算或模型。** `agent/request` 钩子只做一件事——`await next()`,读一下请求携带的 `reasoningEffort`、路由名记进测量,然后原样返回。用户选的是哪一档(会话里的 UI 选择,或者 profile 里 agent-default-model 固定下来的那一档),每个请求就用哪一档。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**1.7.0 起这句话没有例外。** 任何模式、任何面板开关下,插件都不写 `reasoningEffort:`、不写 thinking 预算、不加任何 provider 专用字段;`agent/request` 的返回值就是 `next()` 交回来的那个对象,一个字段都不动。1.6.0 那个「唯一例外」(`lazy` 的请求侧降一档)已在 1.7.0 **连同代码一起删掉**,理由留在「六、三个逐会话可点的提示开关」末尾的历史段。
|
|
8
8
|
|
|
9
9
|
快的部分只来自三件事:更少的往返、更少的冗余输出、更少的卡顿等待。下面逐条说明。
|
|
10
10
|
|
|
11
11
|
## 交互细节:闪电开关与会话归属
|
|
12
12
|
|
|
13
13
|
- **一个图标就是开关。** 会话输入区的活动槽(`conversation.input.activity`)里只有一个闪电按钮:关闭时空心,开启时实心。点一下切换,再点一下切回;这一下只写当前会话。没有档位选择器,没有箭头,没有"选择 gear"的二级菜单。
|
|
14
|
-
- **右键是面板。**
|
|
15
|
-
- **会话之间互不影响,这是硬约束。** 每个会话的 `enabled`、`gear`、`outputDiscipline`、`stepBudget`、`workspaceMap`、`
|
|
14
|
+
- **右键是面板。** 右键闪电打开面板,标题是「更快」,下面是 7 行,7 行都只影响当前会话:"少写过程"(`outputDiscipline`)、"最多几步"(`stepBudget`,循环 0/15/30)、"先列文件清单"(`workspaceMap`)、"一次读多个文件"(`openBatch`)、"不确定时先查一次"(`probeFirst`)、"先动手再解释"(`actBeforePlan`)、"重复无进展时提醒"(`repeatNudge`)。7 行都把选择写进 `sessions` 列里本会话的条目,不碰共享默认值,下一个回合生效;只读或写入中时整块禁用,写失败回滚。行内 tooltip:`probeFirst`「拿不准的事实读一次,别用推理猜」、`actBeforePlan`「最多一行计划,动作清楚就直接做」、`repeatNudge`「重跑同一个调用或连续无新信息时提醒」。没有条目的会话沿用配置里的默认值(`outputDiscipline`/`stepBudget`/`workspaceMap`/`openBatch`/`probeFirst`/`actBeforePlan`/`repeatNudge`)。面板不暴露 `disciplineAfterSteps`、`disciplineEarlyCalls`、`shapeGate`、`finishLineSteps`、`shellParallel`、`disciplineRepeatAtSteps` 这些未证实或已判 HOLD 的旋钮。
|
|
15
|
+
- **会话之间互不影响,这是硬约束。** 每个会话的 `enabled`、`gear`、`outputDiscipline`、`stepBudget`、`workspaceMap`、`openBatch`、`probeFirst`、`actBeforePlan`、`repeatNudge` 都从 `sessions` 列里自己那一条解析:A 会话 `更快 + 少写过程`、B 会话 `更快 + 不写过程` 同时成立,C 会话没点过就沿用配置默认值。面板和闪电只写本会话的条目,从不写共享默认值;条目最多保留 200 个,按 `ts` 淘汰最旧的,被淘汰的会话回到默认值。
|
|
16
16
|
- **按钮只表达一个问题:这个会话开不开。** `aria-pressed` 就是状态,tooltip 写清"仅本会话 · 点击关闭 · 右键设置"。为什么快、快在哪里、单次调用上限这类说明放在插件介绍(`meta.description` 与配置项描述)里,不再占用输入区。
|
|
17
17
|
- **实时行引用的是被执行的数字**,而不是产品形容词:
|
|
18
18
|
- 开启、看门狗有上限:`自动调度 · 并行 2–8 · 单次调用 ≤ 2 分`
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
|
|
32
32
|
1. `const resolved = await next()`;
|
|
33
33
|
2. 读 `resolved.reasoningEffort`、路由名,写进测量标签;
|
|
34
|
-
3. `
|
|
34
|
+
3. `return resolved`——同一个对象、同一个字段集合,不构造新对象,也不写回任何档位。
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
它不写 thinking 预算,不换模型,也不添加任何 provider 专用字段。因此常备的"更快"杠杆仍是三个:并行扇出(减少往返)、输出纪律(减少过程文字)、卡顿看门狗(减少工具等待)。1.7.0 起另有三条**纯提示**的逐会话开关(先查一次、先动手再解释、重复时提醒),它们只往会话里加文字,同样不碰请求;见"六、三个逐会话可点的提示开关"。思考深度和解码速度不在常备杠杆里——前者是用户的契约,后者任何提示词都做不到。
|
|
37
37
|
|
|
38
38
|
### 二、自适应调度器:`gear: auto`
|
|
39
39
|
|
|
@@ -138,18 +138,25 @@
|
|
|
138
138
|
|
|
139
139
|
上限是 200 个会话,超出时淘汰 `ts` 最旧的条目——这是内存与配置文件体积的边界,不是权限模型。host 端的实现(`src/state.js`)和浏览器里的副本(`client.js`)是两份代码,测试用同一组 fixture 锁住它们的读写一致。
|
|
140
140
|
|
|
141
|
-
###
|
|
141
|
+
### 六、三个逐会话可点的提示开关(1.7.0 起,默认全关;1.7.1 给 `actBeforePlan` 补了第二个信号)
|
|
142
142
|
|
|
143
|
-
|
|
143
|
+
三条都只在提示侧:一个字都不写进请求,也不改调用。它们改变的是模型这一轮怎么开始(先读还是先想、先做还是先解释),以及在明显原地打转时被告知事实;面板逐会话打开,配置默认 `false`。
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
1.7.1 之后 `actBeforePlan` 一个开关带两个同方向的行为:回合开始的计划压缩,以及回合中途的思考长度提醒(见本节末尾)。面板仍七行,没有为它新增配置键。
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
- **effort 侧(`lazy: true` 且 `lazyEffort !== false`)。** 账本记两件事:上一步流出的思考字符(`agent/assistant-stream` 的 `reasoning-delta`)与本轮累计。上一步 ≥ 1600 字符,或本轮累计越过 4000 / 8000 字符,就把**下一个请求**降一档(`src/lazy.js` 的 `EFFORT_RUNGS` 顺序,最低到 `low`,从不升档);每轮最多降两次、每步最多降一次(重试不会连降),工具结果报错后两步内不降——报错通常说明该想得更多,不是更少。
|
|
147
|
+
**共用一条消息。** `probeFirst` 与 `actBeforePlan` 在本轮第一步合并成一条 notice,两个都关时不发;两个都开也只有一条消息、一句共同 GUARD(`src/deliberation.js` 的 `GUARD`):`None of this cuts validation at trust boundaries, data-loss handling, security, or accessibility.`
|
|
149
148
|
|
|
150
|
-
|
|
149
|
+
**`probeFirst`「不确定时先查一次」。** 何时发:打开的回合,第一步,一次。文本(`PROBE_TEXT`,逐字):``When you are unsure of a fact about the codebase — a file's contents, a signature, a test's expectation — read it once instead of reasoning about it. One call, the cheapest that answers the question: `grep` before `read`, a range before the whole file. Guessing costs a step and is usually wrong; looking costs the same step and settles it. Read again when a result surprises you, before theorising about why it surprised you.`` 它不指定查哪个文件、不替代判断,也不强制任何调用。
|
|
151
150
|
|
|
152
|
-
|
|
151
|
+
**`actBeforePlan`「先动手再解释」。** 何时发:同一个回合第一步、同一条消息里。文本(`ACT_TEXT`):`Do not restate the task and do not re-explain the plan: the reader already has it. Before your first tool call, one line of plan is enough; when the next action is already clear, take it now and explain afterwards only if the result is not self-evident.` "最多一行计划"是建议措辞,不是机器截断:插件不数行数、不拦截调用。
|
|
152
|
+
|
|
153
|
+
**`repeatNudge`「重复无进展时提醒」。** 何时发:轮中,只在有证据时。触发二选一——同一个调用(工具名 + 稳定参数,即 `signature`)本轮已运行 ≥ `REPEAT_CALLS`(2)次;或连续结果内容相同,即连续第 3 次相同(`STALE_RESULTS` = 2,比较结果 `digest`)。文本随实际次数走:`The call you are about to reason about has already run N times in this turn and returned what you have. If the answer did not change, a different call is what changes it — not more thinking about this one.`;或 `The last N calls returned the same content. Nothing new is arriving from that direction: decide with what you have, or ask a different question.` 上限:每轮最多 `MAX_REPEAT_NUDGES`(2)次;同一证据(`signature` + `digest`)只提醒一次;错误结果**不算**循环,并且重置「结果没变」的连击——报错是需要多想一会儿的情形。实现 `src/deliberation.js`(`createProgressWatch` / `beginTurn` / `noteCall` / `planRepeatNudge`),观察表上限 `RUN_MAX` = 64 个会话、每会话保留最近 `HISTORY_MAX` = 8 次调用;全部在既有 `catch` 内,异常不会改变工具结果。
|
|
154
|
+
|
|
155
|
+
**`actBeforePlan` 的第二个信号:思考长度(1.7.1)。** 触发不看工具调用,只看这一回合已经写了多少字符的思考。读数来自 `assistant/message` 事件里的 `reasoning` 块,由 `src/usage.js` 的 `readReasoningChars` 求和(块缺失或没有 `text` 就是 0——路由不吐思考块时这个开关保持沉默),按回合累加进 `noteReasoning` 的 `reasonChars`;`beginTurn` 在新用户消息时清零。阈值 `REASONING_CHARS`(4000)字符,第 n 次提醒的门槛是 `REASONING_CHARS × (n + 1)`,每回合上限 `MAX_REASONING_NUDGES`(2)。文本逐字:``You have written about N characters of reasoning in this turn. Past that length, more thinking about facts you already have rarely changes the next action: take the action the evidence supports, or read the one fact that is still missing.`` 因为触发条件只是长度,这段文本**不说**「没有新信息」——它给的是「按已有证据动手」或「去读那一个还缺的事实」两个出口。它不写任何请求字段,也不改变 `agent/request` 的返回值。
|
|
156
|
+
|
|
157
|
+
**它们不做什么。** 不写请求字段(含 `reasoningEffort`)、不改模型与 thinking 预算、不重写答案、不强制工具调用、不减少校验。单独开的墙钟收益很小、并且只有 `actBeforePlan` 接近显著:`debugfix` + `migrate` 共 12 对、5 臂 60 次运行里,`probeFirst` 逐对几何 0.883×〔CI 0.663–1.176〕、7/12 更快,`actBeforePlan` 0.761×〔0.578–1.032〕、8/12 更快(`debugfix` 一族 0.582×、6/6),`repeatNudge` 1.007×〔0.839–1.227〕基本没变;三条一起开才是 0.647×〔0.501–0.893〕、10/12 更快——这一条 CI 不跨 1。所以面板给的是「按任务形状试」,不是「单开就快」的保证。
|
|
158
|
+
|
|
159
|
+
**历史:`lazy` 为什么被删(1.6.0 → 1.7.0)。** `lazy`、`lazyEffort`、`lazyVariant`、`src/lazy.js` 与请求侧的 `reasoningEffort` 改写已全部删除,不是默认关闭。证据:`LZ1` 12 对 36 次运行里,「只发文字」逐对几何墙钟 0.389×〔CI 0.252–0.600〕,「文字 + 降档」0.430×〔CI 0.273–0.677〕,两臂直接对打 1.106×〔CI 0.682–1.792〕——区间跨 1,降档这一半没测出可测量的收益;而它是唯一会改变答案而不只是时延的机制,路由又在全部 9678 行计量里报 reasoning usage 0,所以「真的降了档」从来无法被证实。留一个不可验证、还可能改答案的旋钮不值得,于是连同代码删掉。
|
|
153
160
|
|
|
154
161
|
## 配置
|
|
155
162
|
|
|
@@ -181,8 +188,9 @@ Ponytail 那句"最好的代码是没写的代码"在这里只借一半:**先
|
|
|
181
188
|
| `sessions` | `''` | 插件自动维护的会话级覆盖,见上一节。**不要手改。** |
|
|
182
189
|
| `outputDiscipline` | `'late'` | 输出纪律。`late`(默认)前 4 步不写任何输出约束,第 4 步起由调度器提醒一次;`true` 全程生效;`false` 关闭输出纪律,仍注入并行与有界调用规则。 |
|
|
183
190
|
| `openBatch` | `false` | 开局批量:每轮第一步发一次「把已知要做的独立调用放进同一块、shell 命令用 `&&` 串起来」。短任务的钱几乎全是每步固定成本(首字延迟 ~0.5 s/次),少一次往返就省一次;只影响第一轮第一步,长任务可忽略。旧批次里它不优于默认,但那些数字已因测量缺陷撤回(运行目录在仓库内、指标按字节偏移切片),尚无干净重测。保持在 `false`,但 1.5.33 起进面板(「一次读多个文件」),逐会话可开;和 `workspaceMap` 一起开 11 对 0.742×,再加 `disciplineAfterSteps: 2` 三开 22 对 0.669×〔CI 0.554–0.799〕,单独开仍测不到显著。 |
|
|
184
|
-
| `
|
|
185
|
-
| `
|
|
191
|
+
| `probeFirst` | `false` | 逐会话提示开关:拿不准的代码事实(文件内容、签名、测试期望)先读一次,而不是用推理猜;`grep` 先于 `read`,先读范围再读整文件。本轮第一步发一条 notice,一句,且只在打开时发。不指定读哪个文件、不强制调用、不改请求。1.7.0 起进面板(「不确定时先查一次」)。 |
|
|
192
|
+
| `actBeforePlan` | `false` | 逐会话提示开关:不复述任务、不重讲计划;第一个工具调用之前最多一行计划,下一步已经清楚就直接做。与 `probeFirst` 共用同一条 notice 与同一句 GUARD,两个都开也只有一条消息。"最多一行"是建议措辞,不做机器截断。1.7.0 起进面板(「先动手再解释」)。 |
|
|
193
|
+
| `repeatNudge` | `false` | 逐会话提示开关:只在有证据时发一条轮中证据行——同一个调用(工具名 + 稳定参数)本轮已运行 ≥ 2 次,或连续 3 次结果内容相同;句子给出实际次数,每轮最多 2 次,同一证据只提醒一次,错误结果不算循环并重置连击。实现 `src/deliberation.js`,不写请求字段。1.7.0 起进面板(「重复无进展时提醒」)。 |
|
|
186
194
|
| `shapeGate` | `false` | 形状闸门:本轮累计调用 ≥ 4 且其中写入类(不在只读名单里的工具)占比 ≥ 60 % 时,**整轮不再注入任何提醒**,行为退回原生。判断只用已有的工具名,不发请求、不加请求体字段。它给的是「不适合就别管我」的退路:写密集型的任务没有可并行的调用,每条提醒都只是一次它用不上的往返。显式设了 `stepBudget` 时该收尾线优先于闸门——「别加速」不等于「无视我叫停」。默认关,因为它会同时关掉本可以救回长尾的收尾提醒。 |
|
|
187
195
|
| `preferPatch` | `false` | 整文件写入之后提醒一次「改已有文件时只发改动的行(edit hunk),不要重发整个文件」。本步 `write` 的正文 ≥ 2000 字符才武装,之后一步发一次,每轮最多一次;文本明确要求结果与原文件逐字节相同,不限制写什么、也不影响何时结束。默认关闭:它只作用于模型的输出形式(工具调用参数),正确性由文件内容决定、与发送形式无关;实测是否真省墙钟见 `BENCHMARK.md` 的 `pp16` 臂。 |
|
|
188
196
|
| `compactRereadHint` | false | 压缩提醒里多一句「本回合写过的文件不要为确认整份重读」;不新增消息。实验旋钮,默认关 |
|
|
@@ -210,7 +218,7 @@ Ponytail 那句"最好的代码是没写的代码"在这里只借一半:**先
|
|
|
210
218
|
|
|
211
219
|
三条规则让它们真的能一起用:
|
|
212
220
|
|
|
213
|
-
1. **不抢同一个字段。**
|
|
221
|
+
1. **不抢同一个字段。** 本插件不碰 `reasoningEffort`、`thinking`、`output_config`,也不改任何请求字段;压缩器与它没有共享字段——所以没有"两个组件同时钳制 effort"的问题。
|
|
214
222
|
2. **压缩步骤不需要特殊照顾。** 1.0.0 没有"按住某一步"的概念;历史折叠发生在请求发出之前,本插件看到的只是一个普通请求。
|
|
215
223
|
3. **摘要调用碰不到。** 压缩器自己的摘要请求往往跑在它自己的进程或 HTTP 层里,不经过 `agent/request`,本插件既不影响它,也不被它影响。
|
|
216
224
|
|
|
@@ -250,12 +258,12 @@ Ponytail 那句"最好的代码是没写的代码"在这里只借一半:**先
|
|
|
250
258
|
|
|
251
259
|
**完全支持,而且不需要任何配置。** 官方 DeepSeek API(`deepseek-official`)、官方 API 前面挂的代理和中转、自建网关(如 `litellm` + Anthropic Messages)、OpenAI 兼容端点,行为完全一样,原因很简单:
|
|
252
260
|
|
|
253
|
-
-
|
|
254
|
-
- 不做"这条路由声明了哪些档位"的过滤和降档。没有 `floor
|
|
261
|
+
- 请求体原样进出。本插件不写 `reasoningEffort`、不写 `thinking`、不写 `output_config`,也不添加任何 provider 专用字段;路由不报 reasoning token 也不影响提示侧照常工作;
|
|
262
|
+
- 不做"这条路由声明了哪些档位"的过滤和降档。没有 `floor`,没有档位阶梯,没有按步降档,也没有请求侧改写;
|
|
255
263
|
- 路由名只被记进测量和调试日志,不参与任何决策;
|
|
256
264
|
- token 用量按上游实际拼写读,不假定某一家:harness 的 `inputTokens/outputTokens/…`、OpenAI Chat 的 `prompt_tokens/completion_tokens`(含 `prompt_tokens_details.cached_tokens`、`completion_tokens_details.reasoning_tokens`)、新版 OpenAI 的 `input_tokens/output_tokens`、Anthropic 的 `cache_read_input_tokens/cache_creation_input_tokens`、Google 的 `candidatesTokenCount` 都会落到同一组数字;usage 挂在 `usage`、`message.usage` 或 `response.usage` 下都能找到。网关完全不报 usage 时按 0 处理——步的时间和调用数由 harness 自己量,不靠 provider。
|
|
257
265
|
|
|
258
|
-
所以无论 provider 声明的是 `off/low/high/max`、还是 `minimal/low/medium/high`、还是只有一档、还是什么都没声明,本插件的行为都一样:它注入提示段、给 shell 调用设上限,然后把你选的 effort
|
|
266
|
+
所以无论 provider 声明的是 `off/low/high/max`、还是 `minimal/low/medium/high`、还是只有一档、还是什么都没声明,本插件的行为都一样:它注入提示段、给 shell 调用设上限,然后把你选的 effort 原样发出去,一个字段都不改。
|
|
259
267
|
|
|
260
268
|
## 测量、实验与报告
|
|
261
269
|
|
|
@@ -271,8 +279,8 @@ Ponytail 那句"最好的代码是没写的代码"在这里只借一半:**先
|
|
|
271
279
|
| `ts` | 这一步结束的 ISO 时间 |
|
|
272
280
|
| `sid` / `turn` / `step` | 会话 id、轮次、步号 |
|
|
273
281
|
| `mode` / `gear` | `on` 或 `off`;关闭时 gear 记为 `off` |
|
|
274
|
-
| `effort` |
|
|
275
|
-
| `effortBase` |
|
|
282
|
+
| `effort` | **请求实际携带的思考强度**,就是你在 UI 里选的那档,一字不改(1.7.0 起没有任何路径改它) |
|
|
283
|
+
| `effortBase` | 旧字段,1.6.x 用来记降档前的选择。1.7.0 的代码不再设置它,所以新行里通常没有这个键;留着只为读旧计量文件 |
|
|
276
284
|
| `route` | 路由/模型的标签,仅用于分组 |
|
|
277
285
|
| `ttftMs` | 首字延迟:步开始到第一条 assistant 消息 |
|
|
278
286
|
| `llmMs` | 模型耗时:步开始到最后一条 assistant 消息 |
|
|
@@ -282,7 +290,7 @@ Ponytail 那句"最好的代码是没写的代码"在这里只借一半:**先
|
|
|
282
290
|
| `cacheRead` / `cacheWrite` | 上游 usage 的缓存读写 token |
|
|
283
291
|
| `assistantMessages` | 本步的 assistant 消息数 |
|
|
284
292
|
|
|
285
|
-
`effort`
|
|
293
|
+
`effort` 这一列是理解请求路径的关键:它记录的是请求**实际带出去的**值,也就是你在 UI 里选的那档(选了 `high`,每一行就是 `high`)。1.7.0 起这一列只是读数,不是插件写过的证据——没有任何路径能让它和你的选择不同。另有一条独立通道 `reasoningChars`:`agent/assistant-stream` 里数出来的思考字符数,不依赖 provider 报 `reasoning_tokens`,用来判断"这步想得久不久",与档位无关。
|
|
286
294
|
|
|
287
295
|
### 每个会话自己的开关
|
|
288
296
|
|
|
@@ -348,8 +356,8 @@ dsh-fast-mode-experiment stop # 结束实验
|
|
|
348
356
|
|
|
349
357
|
## 安全边界
|
|
350
358
|
|
|
351
|
-
- **只观察,不改写。** `tools/pre-execute` / `tools/post-execute` 是纯观察者(记录开始时间和调用结果),`agent/request`
|
|
352
|
-
-
|
|
359
|
+
- **只观察,不改写。** `tools/pre-execute` / `tools/post-execute` 是纯观察者(记录开始时间和调用结果),`agent/request` 原样返回请求。除了注入提示段和三条 producer-owned notice(压缩后提醒、调度指令,以及只在本会话打开 `repeatNudge` 时才有的重复提醒),插件不往会话里写任何东西。
|
|
360
|
+
- **模型与档位都不可写。** 源码里没有任何 `reasoningEffort:` 赋值,也没有 0.9.x 的 `decideEffort`;`test/effort.test.js` 扫描源码,要求 `/reasoningEffort\s*:/` 命中数为 **0**(每个文件都查),并且仍然禁止 `thinking:` 与 `floor`/`effort` 这类旧字段名。所以"插件偷偷降了档"不是被禁止,而是根本没有那条代码。
|
|
353
361
|
- **设置只能由这次点击改,且带 revision。** 没有后台静默改配置的路径;冲突写入会失败并回滚,原因是 tooltip。
|
|
354
362
|
- **计量是本地的。** 只写 `metricsFile`;`--lan` 是唯一的出网路径,只读,需要显式 token。
|
|
355
363
|
- **看门狗只改 signal。** 调用方原来的 signal 在 `finally` 里恢复;取消后调用体的 rejection 被吞掉,不会变成未处理的拒绝。
|
|
@@ -361,7 +369,7 @@ dsh-fast-mode-experiment stop # 结束实验
|
|
|
361
369
|
|
|
362
370
|
**扇出有前提。** 只有一步里真的存在多个互不依赖的查询时,加宽才有收益。单文件类的基准任务(HumanEval / AIME 形态)上,本插件测到的差异在噪声内(0.95×–1.09×),没有可测量的加速——那些任务本来就没有可并行的读集。把"这个插件让所有任务快 30%"当卖点是错的。
|
|
363
371
|
|
|
364
|
-
**默认没有按步数的停止线。** 1.5.0 把 `finishLineSteps` 与 `stepBudget` 的默认值都改成 0
|
|
372
|
+
**默认没有按步数的停止线。** 1.5.0 把 `finishLineSteps` 与 `stepBudget` 的默认值都改成 0:一个还在产出新结果的回合不会被步数计数器结束,也不会被提醒收尾——把一个做到一半的长任务按步数打断,比慢更贵。默认唯一按步数触发的循环提醒是连续 6 步无进展时的一句「换个做法」(`noProgressWarn`),它只换方法,不结束回合;1.7.0 的 `repeatNudge` 是另一条、独立的一路,默认关闭、只在有证据时发(同一个调用已跑 ≥2 次,或连续三个结果相同),两套阈值不要混着读。这些常量的数值来自小样本观测与设计推理,还缺真实会话的对照数据;`report` 与 `experiment` 是为了补这一课,不是为了证明它已经成立。
|
|
365
373
|
|
|
366
374
|
**token 可能变多,而且这是接受的交换。** 更宽的步意味着每步塞进更多工具结果,输入 token 上升;输出纪律只减少过程文字,不承诺总 token 下降。不要用"省 token"来介绍这个插件。
|
|
367
375
|
|
|
@@ -382,14 +390,14 @@ dsh-fast-mode-experiment stop # 结束实验
|
|
|
382
390
|
- `src/guidance.js`:模型可见的提示文本;
|
|
383
391
|
- `src/stallguard.js`:看门狗与预算优先级;
|
|
384
392
|
- `src/effort.js`:档位表与旧名字归一化(不再有 effort 阶梯);
|
|
385
|
-
- `src/
|
|
393
|
+
- `src/deliberation.js`:三个提示开关(`PROBE_TEXT` / `ACT_TEXT` / `GUARD`)、进展观察、重复提醒判定、思考长度提醒(`REASONING_CHARS` / `noteReasoning` / `planReasoningNudge`);
|
|
386
394
|
- `src/config.js`:schema 与表单字段;
|
|
387
395
|
- `src/state.js`:`sessions` 列的解析与编码;
|
|
388
396
|
- `src/notices.js`:producer-owned notice 的构造与来源校验;
|
|
389
397
|
- `src/metrics.js`:JSONL 计量与汇总;
|
|
390
398
|
- `src/benchmark.js`:按会话哈希分配;
|
|
391
399
|
- `src/peers.js`:冲突插件探测;
|
|
392
|
-
- `client.js
|
|
400
|
+
- `client.js`:闪电开关、右键面板(七行,按会话写入 `sessions` 列)。
|
|
393
401
|
|
|
394
402
|
跑测试:
|
|
395
403
|
|
|
@@ -397,6 +405,6 @@ dsh-fast-mode-experiment stop # 结束实验
|
|
|
397
405
|
node --test test/*.test.js
|
|
398
406
|
```
|
|
399
407
|
|
|
400
|
-
当前
|
|
408
|
+
当前 334 个测试在 Node 24.16.0 上全部通过(334/334,0 失败,`node --test test/*.test.js`,约 10.5 秒)。测试覆盖:调度器宽度与指令、预算优先级、提示段的开关条件、`sessions` 列在 host 与浏览器两份实现间的一致性、notice 来源校验、计量字段、分配哈希、报告与实验 CLI 的参数解析,三个提示开关的文本与进展观察(`test/deliberation.test.js`,10 条),以及一条扫描源码、确认 `reasoningEffort` 赋值出现 0 次的回归测试(`test/effort.test.js`)。
|
|
401
409
|
|
|
402
410
|
文档入口:`README.md`(中文)、`README.en.md`(English)、`BENCHMARK.md`(测量方法与历史数据)、`CHANGELOG.md`(每个版本删了什么、加了什么)。
|
package/index.js
CHANGED
|
@@ -82,10 +82,22 @@ import { classifyEntry } from './src/shape.js'
|
|
|
82
82
|
import { detectPeerPlugins, opsToolsLive } from './src/peers.js'
|
|
83
83
|
import { createStallGuard, stallBudgetFor } from './src/stallguard.js'
|
|
84
84
|
import { createRecorder } from './src/metrics.js'
|
|
85
|
-
import { readOutputTokens } from './src/usage.js'
|
|
85
|
+
import { readOutputTokens, readReasoningChars } from './src/usage.js'
|
|
86
86
|
import { configOverridesFor } from './src/state.js'
|
|
87
87
|
import { buildShellParallelTool } from './src/parallel.js'
|
|
88
|
-
import {
|
|
88
|
+
import {
|
|
89
|
+
ACT_SUMMARY,
|
|
90
|
+
LONG_REASONING_SUMMARY,
|
|
91
|
+
PROBE_SUMMARY,
|
|
92
|
+
REPEAT_SUMMARY,
|
|
93
|
+
beginTurn as beginDeliberationTurn,
|
|
94
|
+
createProgressWatch,
|
|
95
|
+
deliberationNotice,
|
|
96
|
+
noteCall as noteDeliberationCall,
|
|
97
|
+
noteReasoning as noteTurnReasoning,
|
|
98
|
+
planReasoningNudge,
|
|
99
|
+
planRepeatNudge,
|
|
100
|
+
} from './src/deliberation.js'
|
|
89
101
|
|
|
90
102
|
/**
|
|
91
103
|
* The settings-page schema. Re-exported from here because the Loader reads
|
|
@@ -99,7 +111,7 @@ const TAG = '[fast-mode]'
|
|
|
99
111
|
* Shipped identity of this build. Traced into the debug log so a stale install is
|
|
100
112
|
* visible at a glance, and pinned by a test against `package.json`.
|
|
101
113
|
*/
|
|
102
|
-
export const VERSION = '1.
|
|
114
|
+
export const VERSION = '1.7.1'
|
|
103
115
|
const GLOBAL_AGENT = '__global__'
|
|
104
116
|
|
|
105
117
|
/**
|
|
@@ -366,6 +378,10 @@ export function apply(ctx, rawConfig) {
|
|
|
366
378
|
try {
|
|
367
379
|
const out = readOutputTokens(event.data ?? event)
|
|
368
380
|
if (out > 0) noteOutputTokens(auto, agentKey(session), out)
|
|
381
|
+
// Same event, second reading: how much reasoning this turn has written
|
|
382
|
+
// so far. Observed from the transcript; never written into a request.
|
|
383
|
+
const chars = readReasoningChars(event.data ?? event)
|
|
384
|
+
if (chars > 0) noteTurnReasoning(progressWatch, agentKey(session), chars)
|
|
369
385
|
} catch {
|
|
370
386
|
// A counter that can fail a turn is worse than no counter.
|
|
371
387
|
}
|
|
@@ -429,11 +445,11 @@ export function apply(ctx, rawConfig) {
|
|
|
429
445
|
// The scheduler's memory, one entry per conversation: what has been tried, how
|
|
430
446
|
// long calls have been taking, and how many steps went by without a new result.
|
|
431
447
|
const auto = createAutoState()
|
|
432
|
-
const
|
|
448
|
+
const progressWatch = createProgressWatch()
|
|
433
449
|
// Which human turn each session is in. `agent/pre-step` carries no turn number,
|
|
434
450
|
// so the count of claimed (non-plugin) user messages is the boundary: it moves
|
|
435
451
|
// exactly when the user speaks again.
|
|
436
|
-
const
|
|
452
|
+
const deliberationTurnKey = new Map()
|
|
437
453
|
/**
|
|
438
454
|
* Live stall ceilings per conversation, as computed by the scheduler.
|
|
439
455
|
* Bounded like `compacted`: a long-lived host process sees thousands of
|
|
@@ -525,19 +541,6 @@ export function apply(ctx, rawConfig) {
|
|
|
525
541
|
if (cut <= 0) return
|
|
526
542
|
const sid = frame.attemptId.slice(0, cut)
|
|
527
543
|
try {
|
|
528
|
-
// Lazy mode reads the same stream, and must keep reading when the journal
|
|
529
|
-
// is off: the effort step-down is decided from how much the route actually
|
|
530
|
-
// streamed, not from a usage field this route reports as zero.
|
|
531
|
-
if (lazyWatch.runs.has(sid)) {
|
|
532
|
-
const chunk = frame.chunk
|
|
533
|
-
const kind = chunk && typeof chunk === 'object' ? chunk.type : undefined
|
|
534
|
-
if (kind === 'reasoning-delta') {
|
|
535
|
-
const text = typeof chunk.text === 'string'
|
|
536
|
-
? chunk.text
|
|
537
|
-
: typeof chunk.delta === 'string' ? chunk.delta : ''
|
|
538
|
-
lazyNoteReasoning(lazyWatch, sid, text.length)
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
544
|
if (!metrics.enabled) return
|
|
542
545
|
metrics.record({ id: sid }, {
|
|
543
546
|
type: 'assistant/chunk',
|
|
@@ -620,23 +623,37 @@ export function apply(ctx, rawConfig) {
|
|
|
620
623
|
}
|
|
621
624
|
}
|
|
622
625
|
}
|
|
623
|
-
if (cfg.
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
//
|
|
627
|
-
//
|
|
628
|
-
// the one already paid for.
|
|
626
|
+
if (cfg.probeFirst === true || cfg.actBeforePlan === true || cfg.repeatNudge === true) {
|
|
627
|
+
// Three prompt-side mechanisms, all opt-in and all visible in the
|
|
628
|
+
// transcript: one notice at turn entry (probe-first / act-first), and an
|
|
629
|
+
// evidence nudge when a turn repeats a call or stops learning anything.
|
|
630
|
+
// Nothing here touches a request field: see `src/deliberation.js`.
|
|
629
631
|
const key = `${sid}\u0000${claimedUserCount(payload?.messages)}`
|
|
630
|
-
const fresh =
|
|
632
|
+
const fresh = deliberationTurnKey.get(sid) !== key
|
|
631
633
|
if (fresh) {
|
|
632
|
-
|
|
633
|
-
trim(
|
|
634
|
+
deliberationTurnKey.set(sid, key)
|
|
635
|
+
trim(deliberationTurnKey, 200)
|
|
636
|
+
}
|
|
637
|
+
const turn = beginDeliberationTurn(progressWatch, sid, key)
|
|
638
|
+
let directive
|
|
639
|
+
if (fresh && (cfg.probeFirst === true || cfg.actBeforePlan === true)) {
|
|
640
|
+
directive = deliberationNotice({ probe: cfg.probeFirst === true, act: cfg.actBeforePlan === true })
|
|
641
|
+
if (directive) {
|
|
642
|
+
notices.push(createNoticeMessage(directive, cfg.probeFirst === true ? PROBE_SUMMARY : ACT_SUMMARY))
|
|
643
|
+
}
|
|
634
644
|
}
|
|
635
|
-
|
|
636
|
-
if (
|
|
637
|
-
if (
|
|
638
|
-
|
|
639
|
-
|
|
645
|
+
let nudge
|
|
646
|
+
if (cfg.repeatNudge === true) nudge = planRepeatNudge(progressWatch, sid, payload?.step)
|
|
647
|
+
if (nudge) notices.push(createNoticeMessage(nudge, REPEAT_SUMMARY))
|
|
648
|
+
let lengthNudge
|
|
649
|
+
if (cfg.actBeforePlan === true) lengthNudge = planReasoningNudge(progressWatch, sid, payload?.step)
|
|
650
|
+
if (lengthNudge) notices.push(createNoticeMessage(lengthNudge, LONG_REASONING_SUMMARY))
|
|
651
|
+
if (initial.debug) {
|
|
652
|
+
appendDebug(initial.debugFile, `deliberation v${VERSION}: turn=${key.split('\u0000').pop()}`
|
|
653
|
+
+ ` probe=${cfg.probeFirst === true ? 'on' : 'off'} act=${cfg.actBeforePlan === true ? 'on' : 'off'}`
|
|
654
|
+
+ ` directive=${directive ? 'yes' : 'no'} nudge=${nudge ? 'yes' : 'no'}`
|
|
655
|
+
+ ` reasoning=${turn.reasonChars} lengthNudge=${lengthNudge ? 'yes' : 'no'}`
|
|
656
|
+
+ ` calls=${turn.calls.length} stale=${turn.stale}`)
|
|
640
657
|
}
|
|
641
658
|
}
|
|
642
659
|
if (cfg.workspaceMap === true) {
|
|
@@ -785,9 +802,14 @@ export function apply(ctx, rawConfig) {
|
|
|
785
802
|
// The scheduler reads the same observation: a call that returns the same
|
|
786
803
|
// bytes for the same arguments is the raw material of the long tail.
|
|
787
804
|
noteTool(auto, agent, observation)
|
|
788
|
-
//
|
|
789
|
-
//
|
|
790
|
-
|
|
805
|
+
// The evidence nudge reads the same observation: same call again, or a
|
|
806
|
+
// run of results that carried nothing new, is what "over-thinking" is
|
|
807
|
+
// defined as here — not the length of the reasoning stream.
|
|
808
|
+
noteDeliberationCall(progressWatch, agent, {
|
|
809
|
+
signature: `${observation.name}\u0000${observation.argsKey}`,
|
|
810
|
+
digest: observation.digest,
|
|
811
|
+
isError: observation.isError,
|
|
812
|
+
})
|
|
791
813
|
}
|
|
792
814
|
} catch {
|
|
793
815
|
// Observation is best-effort. A failure here must not affect the tool
|
|
@@ -806,12 +828,12 @@ export function apply(ctx, rawConfig) {
|
|
|
806
828
|
const baseEffort = resolved?.reasoningEffort
|
|
807
829
|
const route = routeOf(resolved)
|
|
808
830
|
|
|
809
|
-
//
|
|
810
|
-
//
|
|
811
|
-
//
|
|
812
|
-
//
|
|
813
|
-
//
|
|
814
|
-
//
|
|
831
|
+
// Released in 1.7.0: this hook no longer rewrites any request field. The
|
|
832
|
+
// 1.6.x effort step-down was measurable neither in wall clock (LZ1: 1.106x
|
|
833
|
+
// against the same text without it, CI crossing 1) nor in usage (the route
|
|
834
|
+
// reports `reasoning` as 0), and it was the one mechanism that could make the
|
|
835
|
+
// request differ from what the composer showed. Every mechanism kept in 1.7.0
|
|
836
|
+
// is prompt-side.
|
|
815
837
|
if (!cfg.enabled) {
|
|
816
838
|
metrics.label(sid, { mode: 'off', gear: 'off', effort: summaryEffort(baseEffort), effortBase: null, route })
|
|
817
839
|
return resolved
|
|
@@ -819,20 +841,14 @@ export function apply(ctx, rawConfig) {
|
|
|
819
841
|
|
|
820
842
|
const schedule = cfg.gear === AUTO ? autoSchedule(auto, sid, { emit: false }) : undefined
|
|
821
843
|
checkParallelCap(cfg, schedule?.parallel)
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
const applied = planned && planned.reduced ? planned.effort : baseEffort
|
|
826
|
-
// The journal keeps both halves in one call: `effort` is what the request
|
|
827
|
-
// carried, `effortBase` is what the user chose, so a comparison can tell a
|
|
828
|
-
// lazy step-down from a session that was simply started at a lower rung.
|
|
829
|
-
// `null`, not omitted, on a step that did not step down — otherwise the
|
|
830
|
-
// previous reduction would leak into every following row.
|
|
844
|
+
// `effort` is recorded, `effortBase` stays null: nothing in this release
|
|
845
|
+
// changes it, and a reader comparing the two must be able to tell that the
|
|
846
|
+
// step-down is gone rather than merely not firing.
|
|
831
847
|
metrics.label(sid, {
|
|
832
848
|
mode: 'on',
|
|
833
849
|
gear: cfg.gear,
|
|
834
|
-
effort: summaryEffort(
|
|
835
|
-
effortBase:
|
|
850
|
+
effort: summaryEffort(baseEffort),
|
|
851
|
+
effortBase: null,
|
|
836
852
|
route,
|
|
837
853
|
})
|
|
838
854
|
|
|
@@ -840,7 +856,7 @@ export function apply(ctx, rawConfig) {
|
|
|
840
856
|
appendDebug(
|
|
841
857
|
cfg.debugFile,
|
|
842
858
|
`turn=${payload?.turn} step=${payload?.step} gear=${cfg.gear} effort=${String(summaryEffort(baseEffort))}`
|
|
843
|
-
+
|
|
859
|
+
+ ' (unchanged)'
|
|
844
860
|
+ ` route=${route}`
|
|
845
861
|
+ (schedule
|
|
846
862
|
? ` parallel=${schedule.parallel} budget=${schedule.stallBudgetMs}ms`
|
|
@@ -850,18 +866,20 @@ export function apply(ctx, rawConfig) {
|
|
|
850
866
|
)
|
|
851
867
|
}
|
|
852
868
|
|
|
853
|
-
return
|
|
869
|
+
return resolved
|
|
854
870
|
})
|
|
855
871
|
|
|
856
872
|
ctx.logger?.info?.(
|
|
857
|
-
'%s v%s loaded: enabled=%s gear=%s parallel=%d adaptive=%s
|
|
873
|
+
'%s v%s loaded: enabled=%s gear=%s parallel=%d adaptive=%s switches(probe/act/repeat)=%s metrics=%s',
|
|
858
874
|
TAG,
|
|
859
875
|
VERSION,
|
|
860
876
|
initial.enabled,
|
|
861
877
|
initial.gear,
|
|
862
878
|
initial.parallel,
|
|
863
879
|
initial.adaptive === true,
|
|
864
|
-
initial.
|
|
880
|
+
[initial.probeFirst, initial.actBeforePlan, initial.repeatNudge]
|
|
881
|
+
.map((on) => (on === true ? 'on' : 'off'))
|
|
882
|
+
.join('/'),
|
|
865
883
|
metrics.enabled ? metrics.file : 'off',
|
|
866
884
|
)
|
|
867
885
|
if (initial.legacyGear) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-fast-mode",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "DeepSeek Harness fast mode: fewer round trips at the same thinking effort. Adaptive parallel tool fan-out, output discipline that never trims the answer, a cancellable stall guard, and
|
|
3
|
+
"version": "1.7.1",
|
|
4
|
+
"description": "DeepSeek Harness fast mode: fewer round trips at the same thinking effort. Adaptive parallel tool fan-out, output discipline that never trims the answer, a cancellable stall guard, and three off-by-default switches that trim wasted deliberation (read the fact you are unsure of, act after one line of plan, stop when the evidence stops arriving). It never changes reasoning effort, the model, or the request body. Provider-agnostic, one dependency (@deepseek-ai/schemastery).",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/regrevia/dsh-fast-mode.git"
|
package/src/config.js
CHANGED
|
@@ -40,9 +40,10 @@
|
|
|
40
40
|
*
|
|
41
41
|
* `enabled` defaults to **false** on purpose: opt-in is what makes "off means
|
|
42
42
|
* completely off" verifiable. The old `floor` / `ladder` / `aliases` knobs are
|
|
43
|
-
* gone, along with the code that read them
|
|
44
|
-
*
|
|
45
|
-
*
|
|
43
|
+
* gone, along with the code that read them, and so is the 1.6.x effort
|
|
44
|
+
* step-down: no field in this schema changes a request field any more. The three
|
|
45
|
+
* deliberation switches (`probeFirst`, `actBeforePlan`, `repeatNudge`) only add
|
|
46
|
+
* prompt text, and all three are off by default.
|
|
46
47
|
*
|
|
47
48
|
* @module dsh-fast-mode/config
|
|
48
49
|
*/
|
|
@@ -161,19 +162,28 @@ export const Config = z.object({
|
|
|
161
162
|
'开局批量 / Opening batch(面板名「一次读多个文件」): 每轮第一步提醒一次「把已知要做的独立调用放进同一块、shell 命令用 && 串起来」。短任务的钱几乎全花在每步固定成本(首字延迟 ~0.5 s/次)上,少一次往返就省一次;只发一次,不改后续调度。面板可逐会话开;和 `workspaceMap` 一起开时实测 0.742×(11 对),单独开测不到显著,所以默认仍是 false。',
|
|
162
163
|
),
|
|
163
164
|
|
|
164
|
-
|
|
165
|
+
probeFirst: z
|
|
165
166
|
.boolean()
|
|
166
167
|
.default(false)
|
|
167
168
|
.volatile()
|
|
168
169
|
.description(
|
|
169
|
-
'
|
|
170
|
+
'不确定时先查一次 / Read before guessing(面板「不确定时先查一次」): 每轮第一步的提示里加一句「拿不准的仓库事实就读一次:一次调用,选最便宜的那个(grep 先于 read,读范围先于整文件);结果出乎意料时先重读,再解释为什么」。只加提示文字,不改任何请求字段、不改调度、不改完成判定。依据是本插件唯一测出大收益的机制就是提示文字(LZ1 0.389×),而参数降档测不出效果;这条专门对着「用推理代替读取」这种过思考。默认关闭。',
|
|
170
171
|
),
|
|
171
172
|
|
|
172
|
-
|
|
173
|
+
actBeforePlan: z
|
|
173
174
|
.boolean()
|
|
174
|
-
.default(
|
|
175
|
+
.default(false)
|
|
176
|
+
.volatile()
|
|
177
|
+
.description(
|
|
178
|
+
'先动手再解释 / Act first(面板「先动手再解释」): 每轮第一步加一句「不复述任务、不重讲计划;第一个工具调用之前最多一行计划,动作已经清楚就直接做」。省的是每一步的输出字符(实测输出中位一万字符以上),对推理量不一定有效,所以它和另外两条是不同机制、分开开关。默认关闭。',
|
|
179
|
+
),
|
|
180
|
+
|
|
181
|
+
repeatNudge: z
|
|
182
|
+
.boolean()
|
|
183
|
+
.default(false)
|
|
184
|
+
.volatile()
|
|
175
185
|
.description(
|
|
176
|
-
'
|
|
186
|
+
'重复无进展时提醒 / Evidence nudge(面板「重复无进展时提醒」): 轮内检测两种「没有新信息还在想」的证据——同一个调用(工具名+参数)本轮已经出现 ≥2 次,或连续 ≥3 次工具结果的内容完全相同;命中时在下一步之前发一句带计数的提醒,每轮最多两次、同一证据不重复。它只读本轮已经发生的调用,不发请求、不写请求字段。按推理字符数触发只是「过度思考」的代理指标,短任务上会误触发,这条改用它本身的定义。默认关闭。',
|
|
177
187
|
),
|
|
178
188
|
|
|
179
189
|
preferPatch: z
|