dsh-fast-mode 1.6.2 → 1.7.2
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 +113 -9
- package/CHANGELOG.md +56 -0
- package/README.en.md +13 -11
- package/README.md +14 -13
- package/client.js +102 -41
- package/docs/REFERENCE.md +40 -31
- package/index.js +74 -56
- package/package.json +2 -2
- package/src/config.js +19 -16
- package/src/deliberation.js +217 -0
- package/src/effort.js +7 -8
- package/src/state.js +17 -7
- package/src/usage.js +23 -0
- package/src/lazy.js +0 -245
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.2'
|
|
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.2",
|
|
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
|
*/
|
|
@@ -97,7 +98,7 @@ export const Config = z.object({
|
|
|
97
98
|
.default('late')
|
|
98
99
|
.volatile()
|
|
99
100
|
.description(
|
|
100
|
-
'输出纪律 / Output discipline
|
|
101
|
+
'输出纪律 / Output discipline(面板「精简输出」,也是没有自己条目的会话的默认值): 让模型少写过程、只在结论处落笔。`late`(默认)不往系统提示里放文风规则,第 4 步之后由调度器提醒一次——短任务零成本,长任务实测 6 对:墙钟中位 2.68×、步数 10.5→7.5、输出 token −52%、正确性 12/12 不变。`true` 全程生效;`false` 关闭。',
|
|
101
102
|
),
|
|
102
103
|
|
|
103
104
|
shellParallel: z
|
|
@@ -115,7 +116,7 @@ export const Config = z.object({
|
|
|
115
116
|
.default(0)
|
|
116
117
|
.volatile()
|
|
117
118
|
.description(
|
|
118
|
-
'单轮步数预算 / Steps per turn
|
|
119
|
+
'单轮步数预算 / Steps per turn(面板「步数上限」,0 显示为「不限」): 用户自设的硬上限,到达后调度器提醒一次按现状汇报;0 = 不限(默认)。步数只说明成本,不说明任务是否完成——默认不再按步数催停,避免把仍在推进的长任务截断。需要成本上限时自己设。',
|
|
119
120
|
),
|
|
120
121
|
|
|
121
122
|
finishLineSteps: z
|
|
@@ -158,29 +159,31 @@ export const Config = z.object({
|
|
|
158
159
|
.boolean()
|
|
159
160
|
.default(false)
|
|
160
161
|
.description(
|
|
161
|
-
'开局批量 / Opening batch
|
|
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
|
+
'不确定先查证 / Verify before guessing(面板「不确定先查证」): 每轮第一步的提示里加一句「拿不准的仓库事实就读一次:一次调用,选最便宜的那个(grep 先于 read,读范围先于整文件);结果出乎意料时先重读,再解释为什么」。只加提示文字,不改任何请求字段、不改调度、不改完成判定。依据是本插件唯一测出大收益的机制就是提示文字(LZ1 0.389×),而参数降档测不出效果;这条专门对着「用推理代替读取」这种过思考。默认关闭。',
|
|
170
171
|
),
|
|
171
172
|
|
|
172
|
-
|
|
173
|
-
.
|
|
174
|
-
.default(
|
|
173
|
+
actBeforePlan: z
|
|
174
|
+
.boolean()
|
|
175
|
+
.default(false)
|
|
176
|
+
.volatile()
|
|
175
177
|
.description(
|
|
176
|
-
'
|
|
178
|
+
'先执行后解释 / Act before explaining(面板「先执行后解释」): 每轮第一步加一句「不复述任务、不重讲计划;第一个工具调用之前最多一行计划,动作已经清楚就直接做」。省的是每一步的输出字符(实测输出中位一万字符以上),对推理量不一定有效,所以它和另外两条是不同机制、分开开关。默认关闭。',
|
|
177
179
|
),
|
|
178
180
|
|
|
179
|
-
|
|
181
|
+
repeatNudge: z
|
|
180
182
|
.boolean()
|
|
181
|
-
.default(
|
|
183
|
+
.default(false)
|
|
184
|
+
.volatile()
|
|
182
185
|
.description(
|
|
183
|
-
'
|
|
186
|
+
'重复时换方法 / Evidence nudge(面板「重复时换方法」): 轮内检测两种「没有新信息还在想」的证据——同一个调用(工具名+参数)本轮已经出现 ≥2 次,或连续 ≥3 次工具结果的内容完全相同;命中时在下一步之前发一句带计数的提醒,每轮最多两次、同一证据不重复。它只读本轮已经发生的调用,不发请求、不写请求字段。按推理字符数触发只是「过度思考」的代理指标,短任务上会误触发,这条改用它本身的定义。默认关闭。',
|
|
184
187
|
),
|
|
185
188
|
|
|
186
189
|
preferPatch: z
|
|
@@ -232,7 +235,7 @@ export const Config = z.object({
|
|
|
232
235
|
.default(false)
|
|
233
236
|
.volatile()
|
|
234
237
|
.description(
|
|
235
|
-
'工作区清单 / Workspace inventory
|
|
238
|
+
'工作区清单 / Workspace inventory(面板「开局附文件清单」): 会话开始时不调用任何工具,harness 自己走一遍 cwd(深度 2、最多 60 条、跳过 node_modules 等)把路径、大小和现有测试入口作为一条提示交给模型。实测 `def15` 会话里 35.7% 的工具调用是 ls/find/cat 这类探路,其中 40% 出现在前两次调用里——这一步就是把那次往返拿掉。只发一次,不隐藏任何文件,读文件仍由模型自己决定。',
|
|
236
239
|
),
|
|
237
240
|
|
|
238
241
|
mapEntries: z
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deliberation controls: the three opt-in mechanisms that replaced the retired
|
|
3
|
+
* effort step-down in 1.7.0.
|
|
4
|
+
*
|
|
5
|
+
* Why the old half went away: rewriting `reasoningEffort` for one request turned
|
|
6
|
+
* out to be unmeasurable on the route this plugin runs on (the provider reports
|
|
7
|
+
* `reasoning` usage as 0 on every row, so there is no evidence the parameter is
|
|
8
|
+
* honoured) and it was the one mechanism that changed answers rather than only
|
|
9
|
+
* latency. See BENCHMARK.md, LZ1: text-only 0.389x vs text+step-down 0.430x.
|
|
10
|
+
*
|
|
11
|
+
* What is left is three switches, all visible in the transcript instead:
|
|
12
|
+
* 1. `probeFirst` — one line at turn entry: look up the fact you are unsure
|
|
13
|
+
* of instead of reasoning about it.
|
|
14
|
+
* 2. `actBeforePlan`— one line at turn entry: at most one line of plan before
|
|
15
|
+
* the first call, and act when the next action is clear.
|
|
16
|
+
* 3. `repeatNudge` — an evidence nudge mid-turn: the same call already ran, or
|
|
17
|
+
* the last few results carried nothing new. This targets
|
|
18
|
+
* the definition of over-thinking ("no new information, and
|
|
19
|
+
* still thinking") instead of its proxy ("long reasoning").
|
|
20
|
+
*
|
|
21
|
+
* `actBeforePlan` also carries a length nudge. Repetition turned out to be blind
|
|
22
|
+
* on tasks that read one new file per step, so the second signal is the one the
|
|
23
|
+
* transcript already records: how many characters of reasoning this turn has
|
|
24
|
+
* written. It is counted from `assistant/message` events and never written back,
|
|
25
|
+
* so like everything else here it can only add text to the prompt.
|
|
26
|
+
*
|
|
27
|
+
* All three are prompt-side only. No config field is written, no request field
|
|
28
|
+
* changes, so nothing here can make the request differ from what the composer
|
|
29
|
+
* shows.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** Never-cut guard: the point of thinking less is not to skip the expensive checks. */
|
|
33
|
+
export const GUARD =
|
|
34
|
+
'None of this cuts validation at trust boundaries, data-loss handling, security, or accessibility.'
|
|
35
|
+
|
|
36
|
+
export const PROBE_TEXT =
|
|
37
|
+
'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.'
|
|
38
|
+
|
|
39
|
+
export const ACT_TEXT =
|
|
40
|
+
'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.'
|
|
41
|
+
|
|
42
|
+
export const PROBE_SUMMARY = 'probe-first: read the fact, do not reason about it'
|
|
43
|
+
export const ACT_SUMMARY = 'act-first: one line of plan, then work'
|
|
44
|
+
export const REPEAT_SUMMARY = 'repeat nudge: same call again, or no new information'
|
|
45
|
+
export const LONG_REASONING_SUMMARY = 'over-thinking: think with what you have'
|
|
46
|
+
|
|
47
|
+
/** Cumulative reasoning characters in one turn that earn the first length nudge. */
|
|
48
|
+
export const REASONING_CHARS = 4000
|
|
49
|
+
/** At most this many length nudges per turn. */
|
|
50
|
+
export const MAX_REASONING_NUDGES = 2
|
|
51
|
+
|
|
52
|
+
/** At most this many evidence nudges per turn. */
|
|
53
|
+
export const MAX_REPEAT_NUDGES = 2
|
|
54
|
+
/** The same call (name + arguments) seen this many times in one turn is a loop. */
|
|
55
|
+
export const REPEAT_CALLS = 2
|
|
56
|
+
/** Consecutive unchanged results beyond the first: three identical results in a row. */
|
|
57
|
+
export const STALE_RESULTS = 2
|
|
58
|
+
/** Bounded history per conversation; the watch is a WeakMap-free Map trimmed by age. */
|
|
59
|
+
const HISTORY_MAX = 8
|
|
60
|
+
const RUN_MAX = 64
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The turn-entry notice. Both text switches share one message and one guard line
|
|
64
|
+
* so that turning both on does not cost two notices.
|
|
65
|
+
* @param {{probe?: boolean, act?: boolean}} switches
|
|
66
|
+
* @returns {string|undefined}
|
|
67
|
+
*/
|
|
68
|
+
export function deliberationNotice(switches) {
|
|
69
|
+
const parts = []
|
|
70
|
+
if (switches?.probe === true) parts.push(PROBE_TEXT)
|
|
71
|
+
if (switches?.act === true) parts.push(ACT_TEXT)
|
|
72
|
+
if (parts.length === 0) return undefined
|
|
73
|
+
return `${parts.join('\n\n')}\n\n${GUARD}`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** @returns {{runs: Map<string, object>}} */
|
|
77
|
+
export function createProgressWatch() {
|
|
78
|
+
return { runs: new Map() }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function runOf(watch, sid) {
|
|
82
|
+
let run = watch.runs.get(sid)
|
|
83
|
+
if (!run) {
|
|
84
|
+
run = { key: '', calls: [], stale: 0, nudges: 0, nudgedFor: '', reasonChars: 0, reasonNudges: 0 }
|
|
85
|
+
watch.runs.set(sid, run)
|
|
86
|
+
if (watch.runs.size > RUN_MAX) {
|
|
87
|
+
const oldest = watch.runs.keys().next().value
|
|
88
|
+
if (oldest !== undefined && oldest !== sid) watch.runs.delete(oldest)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return run
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Start (or continue) a conversation's turn. A new user message resets the
|
|
96
|
+
* evidence: the previous turn's loop is not this turn's.
|
|
97
|
+
* @param {{runs: Map<string, object>}} watch
|
|
98
|
+
* @param {string} sid
|
|
99
|
+
* @param {string} key turn identity
|
|
100
|
+
*/
|
|
101
|
+
export function beginTurn(watch, sid, key) {
|
|
102
|
+
const run = runOf(watch, sid)
|
|
103
|
+
if (run.key !== key) {
|
|
104
|
+
run.key = key
|
|
105
|
+
run.calls = []
|
|
106
|
+
run.stale = 0
|
|
107
|
+
run.nudges = 0
|
|
108
|
+
run.nudgedFor = ''
|
|
109
|
+
run.reasonChars = 0
|
|
110
|
+
run.reasonNudges = 0
|
|
111
|
+
}
|
|
112
|
+
return run
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Record one finished tool call. `signature` must be stable across repeats of the
|
|
117
|
+
* same call (tool name + arguments), `digest` the hash of the result content.
|
|
118
|
+
* Error results are counted as "something happened", never as a loop signal: an
|
|
119
|
+
* error is information, and it is the one case where more thought is warranted.
|
|
120
|
+
* @param {{runs: Map<string, object>}} watch
|
|
121
|
+
* @param {string} sid
|
|
122
|
+
* @param {{signature?: string, digest?: string, isError?: boolean}} call
|
|
123
|
+
*/
|
|
124
|
+
export function noteCall(watch, sid, call) {
|
|
125
|
+
const run = runOf(watch, sid)
|
|
126
|
+
const signature = typeof call?.signature === 'string' ? call.signature : ''
|
|
127
|
+
const digest = typeof call?.digest === 'string' ? call.digest : ''
|
|
128
|
+
const last = run.calls[run.calls.length - 1]
|
|
129
|
+
if (call?.isError !== true && last !== undefined && digest !== '' && digest === last.digest) {
|
|
130
|
+
run.stale += 1
|
|
131
|
+
} else {
|
|
132
|
+
run.stale = 0
|
|
133
|
+
}
|
|
134
|
+
// An error is information: it resets the "nothing new" run, and it never counts
|
|
135
|
+
// as a repeated call, because the next step after a failure is exactly where
|
|
136
|
+
// more thought is warranted rather than less.
|
|
137
|
+
const isError = call?.isError === true
|
|
138
|
+
run.calls.push({ signature: isError ? '' : signature, digest, isError })
|
|
139
|
+
if (run.calls.length > HISTORY_MAX) run.calls.shift()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The evidence nudge for the next step, or undefined when the turn shows no loop.
|
|
144
|
+
* Every nudge names the count it saw, because "you are looping" without a number
|
|
145
|
+
* reads as an opinion and this one has to read as a finding.
|
|
146
|
+
* @param {{runs: Map<string, object>}} watch
|
|
147
|
+
* @param {string} sid
|
|
148
|
+
* @param {number} step
|
|
149
|
+
* @returns {string|undefined}
|
|
150
|
+
*/
|
|
151
|
+
export function planRepeatNudge(watch, sid, step) {
|
|
152
|
+
const run = runOf(watch, sid)
|
|
153
|
+
if (run.nudges >= MAX_REPEAT_NUDGES) return undefined
|
|
154
|
+
|
|
155
|
+
let condition = ''
|
|
156
|
+
let text = ''
|
|
157
|
+
|
|
158
|
+
const last = run.calls[run.calls.length - 1]
|
|
159
|
+
if (last !== undefined && last.signature !== '') {
|
|
160
|
+
const same = run.calls.filter((call) => call.signature === last.signature).length
|
|
161
|
+
if (same >= REPEAT_CALLS) {
|
|
162
|
+
// The digest is part of the condition: the same call returning new content
|
|
163
|
+
// is new evidence, and earns its own sentence once.
|
|
164
|
+
condition = `repeat:${last.signature}:${last.digest}`
|
|
165
|
+
text = `The call you are about to reason about has already run ${same} 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.`
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (text === '' && run.stale >= STALE_RESULTS) {
|
|
170
|
+
condition = `stale:${last?.digest ?? ''}`
|
|
171
|
+
text = `The last ${run.stale + 1} calls returned the same content. Nothing new is arriving from that direction: decide with what you have, or ask a different question.`
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (text === '' || condition === run.nudgedFor) return undefined
|
|
175
|
+
run.nudges += 1
|
|
176
|
+
run.nudgedFor = condition
|
|
177
|
+
run.debugWhy = `${condition} step=${step}`
|
|
178
|
+
return text
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Add one assistant message's reasoning size to this turn's running total.
|
|
183
|
+
*
|
|
184
|
+
* Per turn, not per session: the length of *this* attempt is what the next step
|
|
185
|
+
* needs to know, and the next user message starts the measurement over.
|
|
186
|
+
*
|
|
187
|
+
* @param {{runs: Map<string, object>}} watch
|
|
188
|
+
* @param {string} sid
|
|
189
|
+
* @param {number} chars
|
|
190
|
+
*/
|
|
191
|
+
export function noteReasoning(watch, sid, chars) {
|
|
192
|
+
if (!Number.isFinite(chars) || chars <= 0) return
|
|
193
|
+
const run = runOf(watch, sid)
|
|
194
|
+
run.reasonChars += chars
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The length nudge for the next step, or undefined when this turn has not yet
|
|
199
|
+
* thought longer than a decision needs. Fires at most `MAX_REASONING_NUDGES`
|
|
200
|
+
* times per turn and names the size it saw, so the message reads as a finding
|
|
201
|
+
* rather than an opinion.
|
|
202
|
+
*
|
|
203
|
+
* @param {{runs: Map<string, object>}} watch
|
|
204
|
+
* @param {string} sid
|
|
205
|
+
* @param {number} step
|
|
206
|
+
* @returns {string|undefined}
|
|
207
|
+
*/
|
|
208
|
+
export function planReasoningNudge(watch, sid, step) {
|
|
209
|
+
const run = runOf(watch, sid)
|
|
210
|
+
if (run.reasonNudges >= MAX_REASONING_NUDGES) return undefined
|
|
211
|
+
const threshold = REASONING_CHARS * (run.reasonNudges + 1)
|
|
212
|
+
if (run.reasonChars < threshold) return undefined
|
|
213
|
+
run.reasonNudges += 1
|
|
214
|
+
run.debugWhy = `reasoning:${run.reasonChars} step=${step}`
|
|
215
|
+
const seen = Math.round(run.reasonChars / 100) * 100
|
|
216
|
+
return `You have written about ${seen} 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.`
|
|
217
|
+
}
|
package/src/effort.js
CHANGED
|
@@ -277,15 +277,14 @@ export function resolveConfig(raw, overrides) {
|
|
|
277
277
|
? Math.max(0, Math.floor(number(config.finishLineSteps, 0)))
|
|
278
278
|
: 0,
|
|
279
279
|
/**
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
* the two halves can be measured apart.
|
|
280
|
+
* The three deliberation switches of 1.7.0, all prompt-side and all opt-in.
|
|
281
|
+
* They replaced the 1.6.x effort step-down, which could change an answer
|
|
282
|
+
* without being measurable on this route. `probeFirst` and `actBeforePlan`
|
|
283
|
+
* cost one notice at turn entry; `repeatNudge` reads the turn's own calls.
|
|
285
284
|
*/
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
285
|
+
probeFirst: gear.directive && config.probeFirst === true,
|
|
286
|
+
actBeforePlan: gear.directive && config.actBeforePlan === true,
|
|
287
|
+
repeatNudge: gear.directive && config.repeatNudge === true,
|
|
289
288
|
/**
|
|
290
289
|
* The stall guard, the one *hard* bound this plugin owns: it cancels a single
|
|
291
290
|
* tool call at its deadline. On by default; `false` or a negative
|
package/src/state.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* has an entry of its own. Only an edit in `cordis.patch.yml` changes them;
|
|
15
15
|
* the chip and the panel never write them.
|
|
16
16
|
* - The `sessions` field carries every per-session choice: `{ sessionId: {
|
|
17
|
-
* enabled, gear, discipline, stepBudget, map, batch,
|
|
17
|
+
* enabled, gear, discipline, stepBudget, map, batch, probeFirst, actBeforePlan, repeatNudge, ts } }` as a compact JSON
|
|
18
18
|
* string. It is volatile, so a write lands in the running plugin without a
|
|
19
19
|
* remount.
|
|
20
20
|
*
|
|
@@ -107,7 +107,9 @@ function sanitise(input) {
|
|
|
107
107
|
if (Number.isFinite(entry.stepBudget) && entry.stepBudget >= 0) clean.stepBudget = Math.floor(Number(entry.stepBudget))
|
|
108
108
|
if (typeof entry.map === 'boolean') clean.map = entry.map
|
|
109
109
|
if (typeof entry.batch === 'boolean') clean.batch = entry.batch
|
|
110
|
-
if (typeof entry.
|
|
110
|
+
if (typeof entry.probeFirst === 'boolean') clean.probeFirst = entry.probeFirst
|
|
111
|
+
if (typeof entry.actBeforePlan === 'boolean') clean.actBeforePlan = entry.actBeforePlan
|
|
112
|
+
if (typeof entry.repeatNudge === 'boolean') clean.repeatNudge = entry.repeatNudge
|
|
111
113
|
if (Number.isFinite(entry.ts)) clean.ts = Number(entry.ts)
|
|
112
114
|
if (
|
|
113
115
|
clean.enabled === undefined
|
|
@@ -116,7 +118,9 @@ function sanitise(input) {
|
|
|
116
118
|
&& clean.stepBudget === undefined
|
|
117
119
|
&& clean.map === undefined
|
|
118
120
|
&& clean.batch === undefined
|
|
119
|
-
&& clean.
|
|
121
|
+
&& clean.probeFirst === undefined
|
|
122
|
+
&& clean.actBeforePlan === undefined
|
|
123
|
+
&& clean.repeatNudge === undefined
|
|
120
124
|
) continue
|
|
121
125
|
out[sessionId] = clean
|
|
122
126
|
}
|
|
@@ -142,7 +146,9 @@ export function choiceFor(raw, sessionId) {
|
|
|
142
146
|
if (entry.stepBudget !== undefined) choice.stepBudget = entry.stepBudget
|
|
143
147
|
if (entry.map !== undefined) choice.map = entry.map
|
|
144
148
|
if (entry.batch !== undefined) choice.batch = entry.batch
|
|
145
|
-
if (entry.
|
|
149
|
+
if (entry.probeFirst !== undefined) choice.probeFirst = entry.probeFirst
|
|
150
|
+
if (entry.actBeforePlan !== undefined) choice.actBeforePlan = entry.actBeforePlan
|
|
151
|
+
if (entry.repeatNudge !== undefined) choice.repeatNudge = entry.repeatNudge
|
|
146
152
|
return choice
|
|
147
153
|
}
|
|
148
154
|
|
|
@@ -168,7 +174,9 @@ export function configOverridesFor(raw, sessionId) {
|
|
|
168
174
|
if (choice.stepBudget !== undefined) overrides.stepBudget = choice.stepBudget
|
|
169
175
|
if (choice.map !== undefined) overrides.workspaceMap = choice.map
|
|
170
176
|
if (choice.batch !== undefined) overrides.openBatch = choice.batch
|
|
171
|
-
if (choice.
|
|
177
|
+
if (choice.probeFirst !== undefined) overrides.probeFirst = choice.probeFirst
|
|
178
|
+
if (choice.actBeforePlan !== undefined) overrides.actBeforePlan = choice.actBeforePlan
|
|
179
|
+
if (choice.repeatNudge !== undefined) overrides.repeatNudge = choice.repeatNudge
|
|
172
180
|
return Object.keys(overrides).length === 0 ? undefined : overrides
|
|
173
181
|
}
|
|
174
182
|
|
|
@@ -220,12 +228,14 @@ export function writeChoice(raw, sessionId, choice, now) {
|
|
|
220
228
|
if (Number.isFinite(choice.stepBudget) && choice.stepBudget >= 0) next.stepBudget = Math.floor(Number(choice.stepBudget))
|
|
221
229
|
if (typeof choice.map === 'boolean') next.map = choice.map
|
|
222
230
|
if (typeof choice.batch === 'boolean') next.batch = choice.batch
|
|
223
|
-
if (typeof choice.
|
|
231
|
+
if (typeof choice.probeFirst === 'boolean') next.probeFirst = choice.probeFirst
|
|
232
|
+
if (typeof choice.actBeforePlan === 'boolean') next.actBeforePlan = choice.actBeforePlan
|
|
233
|
+
if (typeof choice.repeatNudge === 'boolean') next.repeatNudge = choice.repeatNudge
|
|
224
234
|
map[sessionId] = next
|
|
225
235
|
return { sessions: encodeChoices(map), map }
|
|
226
236
|
}
|
|
227
237
|
|
|
228
|
-
/** @typedef {{enabled?: boolean, gear?: string, discipline?: boolean|'late', stepBudget?: number, map?: boolean, batch?: boolean,
|
|
238
|
+
/** @typedef {{enabled?: boolean, gear?: string, discipline?: boolean|'late', stepBudget?: number, map?: boolean, batch?: boolean, probeFirst?: boolean, actBeforePlan?: boolean, repeatNudge?: boolean, ts?: number}} SessionEntry */
|
|
229
239
|
|
|
230
240
|
/**
|
|
231
241
|
* Serialise the map, dropping the oldest entries beyond {@link SESSIONS_LIMIT}.
|