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/src/usage.js
CHANGED
|
@@ -122,3 +122,26 @@ export function findUsage(source) {
|
|
|
122
122
|
export function readOutputTokens(source) {
|
|
123
123
|
return readUsage(findUsage(source)).output
|
|
124
124
|
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Reasoning characters in one assistant message, summed over its blocks.
|
|
128
|
+
*
|
|
129
|
+
* This is the *observed* size of the model's thinking, read from the transcript;
|
|
130
|
+
* it is never written back into a request. Routes that do not persist reasoning
|
|
131
|
+
* blocks return 0, and a switch that reads this stays silent there — the correct
|
|
132
|
+
* failure mode for something that is off by default.
|
|
133
|
+
*
|
|
134
|
+
* @param {any} source an assistant message, or an event carrying one under `message`
|
|
135
|
+
* @returns {number}
|
|
136
|
+
*/
|
|
137
|
+
export function readReasoningChars(source) {
|
|
138
|
+
const message = source && typeof source === 'object' ? (source.message ?? source) : undefined
|
|
139
|
+
const content = message && Array.isArray(message.content) ? message.content : []
|
|
140
|
+
let total = 0
|
|
141
|
+
for (const block of content) {
|
|
142
|
+
if (block && typeof block === 'object' && block.type === 'reasoning' && typeof block.text === 'string') {
|
|
143
|
+
total += block.text.length
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return total
|
|
147
|
+
}
|
package/src/lazy.js
DELETED
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lazy mode: the one switch in this plugin that is allowed to touch how much the
|
|
3
|
-
* model deliberates.
|
|
4
|
-
*
|
|
5
|
-
* Everything else here buys speed while holding effort still — the promise is
|
|
6
|
-
* "same depth, sooner". This module is the deliberate exception, and it is the
|
|
7
|
-
* only place in the package that rewrites `reasoningEffort`. It exists because
|
|
8
|
-
* a long turn loses time in a way parallelism cannot fix: the model keeps
|
|
9
|
-
* expanding a problem it has already understood. The idea is borrowed from
|
|
10
|
-
* `ponytail` — a lazy senior developer solves the whole problem with the least
|
|
11
|
-
* new code, and is lazy about the solution but never about the reading.
|
|
12
|
-
*
|
|
13
|
-
* Three mechanisms, cheapest first:
|
|
14
|
-
*
|
|
15
|
-
* 1. **The ladder, in the prompt.** One short text asks for the first rung that
|
|
16
|
-
* works and a one-line "what I skipped". Cost is input tokens once per turn;
|
|
17
|
-
* it changes nothing about the harness.
|
|
18
|
-
* 2. **The nudge.** When a turn's streamed reasoning passes a threshold without
|
|
19
|
-
* a new tool result, one notice says so. It is observation, not policy: it
|
|
20
|
-
* repeats what the transcript already proves.
|
|
21
|
-
* 3. **The effort step-down.** Only when a step visibly over-thought (the
|
|
22
|
-
* streamed reasoner block is large) does the next request ask for one rung
|
|
23
|
-
* less. It never goes below `FLOOR` (the user's own floor, `low`), never
|
|
24
|
-
* fires twice in a row after an error, and is capped per turn.
|
|
25
|
-
*
|
|
26
|
-
* What is never cut, in this module or its text: validation at a trust boundary,
|
|
27
|
-
* error handling that protects data, security, accessibility, and the
|
|
28
|
-
* deliverable itself. `DIRECTIVE` says so explicitly, because a model told
|
|
29
|
-
* "write less" drops the wrong things first.
|
|
30
|
-
*
|
|
31
|
-
* ## Why the step-down keeps its own ledger
|
|
32
|
-
*
|
|
33
|
-
* The 0.9.x release lowered effort from step two of *every* turn. Measured
|
|
34
|
-
* later, that bought no wall clock on this route and did real damage: the
|
|
35
|
-
* rewrite reached the provider as a request header, so the composer showed an
|
|
36
|
-
* effort the user never chose, and it stayed there across restarts. So this
|
|
37
|
-
* module lowers effort only after evidence, restores the resolved value on the
|
|
38
|
-
* next request, and is off unless `lazy: true`.
|
|
39
|
-
*/
|
|
40
|
-
|
|
41
|
-
/** The prompt section. Short on purpose: it is paid on every request of a turn. */
|
|
42
|
-
export const DIRECTIVE = `Work like a lazy senior developer: the best step is the one not taken.
|
|
43
|
-
|
|
44
|
-
- Read before deciding. Trace the real flow of the code the change touches, then decide. Being lazy about the solution never means being lazy about the reading.
|
|
45
|
-
- Once a step fully does the job, stop planning it. No option, abstraction, or "for later" code nobody asked for.
|
|
46
|
-
- Take the first rung that works: what this repo already has, then the platform or a tool you already hold, then one line, then the minimum that works.
|
|
47
|
-
- Never cut these to save code or time: validation where input crosses a trust boundary, error handling that protects data, security, accessibility, and the deliverable the user asked for.
|
|
48
|
-
- End the turn with one line: what you skipped, and the risk that carries.`
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Bench variants of the ladder text, selected by `lazyVariant`. The measured
|
|
52
|
-
* block compares them in one run, so the control stays the released text.
|
|
53
|
-
*/
|
|
54
|
-
export const DIRECTIVES = Object.freeze({
|
|
55
|
-
v1: DIRECTIVE,
|
|
56
|
-
v2: `Work like a lazy senior developer: the best step is the one not taken.
|
|
57
|
-
|
|
58
|
-
- Read before deciding. Trace the real flow of the code the change touches, then decide. Being lazy about the solution never means being lazy about the reading.
|
|
59
|
-
- Once a step fully does the job, stop planning it. No option, abstraction, or "for later" code nobody asked for.
|
|
60
|
-
- Take the first rung that works: what this repo already has, then the platform or a tool you already hold, then one line, then the minimum that works.
|
|
61
|
-
- Never cut these to save code or time: validation where input crosses a trust boundary, error handling that protects data, security, accessibility, and the deliverable the user asked for.
|
|
62
|
-
- End the turn with one line: what you skipped, and the risk that carries.
|
|
63
|
-
- Do not restate the task or re-explain the plan before acting: the reader already has it.
|
|
64
|
-
- If the next action is already clear, take it now. Do not spend a step weighing alternatives you are not going to use.`,
|
|
65
|
-
v3: `Work like a lazy senior developer: the best step is the one not taken.
|
|
66
|
-
|
|
67
|
-
- Read the flow the change touches before you decide; never be lazy about the reading.
|
|
68
|
-
- Take the first rung that works: what the repo already has, the platform, a tool you hold, one line, then the minimum.
|
|
69
|
-
- Once the step fully does the job, stop planning it: no option, abstraction, or code nobody asked for.
|
|
70
|
-
- Never cut these: validation at a trust boundary, error handling that protects data, security, accessibility, and the deliverable.
|
|
71
|
-
- Do not restate the task. If the next action is clear, take it. End with one line: what you skipped and the risk.`,
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const DIRECTIVE_SUMMARY = 'lazy mode: first rung that works, no scope beyond the task'
|
|
76
|
-
|
|
77
|
-
/** Reasoning rungs a route may name, weakest first. */
|
|
78
|
-
export const EFFORT_RUNGS = Object.freeze(['minimal', 'low', 'medium', 'high', 'xhigh', 'max'])
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* The lowest rung this module will ask for. Effort is the user's contract; a
|
|
82
|
-
* watchdog may spend a little of the headroom a high setting leaves, and stops
|
|
83
|
-
* where the default would have been.
|
|
84
|
-
*/
|
|
85
|
-
const FLOOR = 'low'
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Streamed reasoning characters in one step that count as over-thinking. On the
|
|
89
|
-
* measured route a normal step streams a few hundred characters and a step stuck
|
|
90
|
-
* on scope streams thousands, so this is deliberately not a tight bound.
|
|
91
|
-
*/
|
|
92
|
-
const HOT_CHARS = 1600
|
|
93
|
-
|
|
94
|
-
/** Reasoning characters a turn may spend before the first nudge. */
|
|
95
|
-
const NUDGE_CHARS = 4000
|
|
96
|
-
|
|
97
|
-
/** How many notices one turn may receive. Two: one early, one late. */
|
|
98
|
-
const MAX_NUDGES = 2
|
|
99
|
-
|
|
100
|
-
/** How many effort reductions one turn may use. */
|
|
101
|
-
const MAX_REDUCTIONS = 2
|
|
102
|
-
|
|
103
|
-
/** Steps a reduction is off limits for after an error result (the step it fired on plus two). */
|
|
104
|
-
const ERROR_COOLDOWN = 2
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* The next rung down, or `undefined` when the value is not a rung this module
|
|
108
|
-
* understands (a route may use its own vocabulary — never guess at it) or is
|
|
109
|
-
* already at `FLOOR`.
|
|
110
|
-
*
|
|
111
|
-
* @param {string} value
|
|
112
|
-
* @returns {string | undefined}
|
|
113
|
-
*/
|
|
114
|
-
export function cheaperEffort(value) {
|
|
115
|
-
const at = EFFORT_RUNGS.indexOf(value)
|
|
116
|
-
if (at <= 0) return undefined
|
|
117
|
-
const floor = EFFORT_RUNGS.indexOf(FLOOR)
|
|
118
|
-
if (at <= floor) return undefined
|
|
119
|
-
return EFFORT_RUNGS[at - 1]
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Per-session bookkeeping; one turn at a time is all a session runs. */
|
|
123
|
-
const newTurn = () => ({
|
|
124
|
-
step: 0,
|
|
125
|
-
hot: false,
|
|
126
|
-
stepChars: 0,
|
|
127
|
-
turnChars: 0,
|
|
128
|
-
nudges: 0,
|
|
129
|
-
reductions: 0,
|
|
130
|
-
coolUntil: 0,
|
|
131
|
-
nudgedAt: 0,
|
|
132
|
-
reducedStep: -1,
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Create the per-session ledger. Pure and dependency-free so the policy can be
|
|
137
|
-
* tested without a harness.
|
|
138
|
-
*/
|
|
139
|
-
export function createLazyWatch() {
|
|
140
|
-
return { runs: new Map() }
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** @param {{runs: Map<string, any>}} watch @param {string} sid */
|
|
144
|
-
const runOf = (watch, sid) => {
|
|
145
|
-
let run = watch.runs.get(sid)
|
|
146
|
-
if (run === undefined) {
|
|
147
|
-
run = newTurn()
|
|
148
|
-
watch.runs.set(sid, run)
|
|
149
|
-
if (watch.runs.size > 64) watch.runs.delete(watch.runs.keys().next().value)
|
|
150
|
-
}
|
|
151
|
-
return run
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Count streamed reasoning characters. Called from the stream tap, i.e. before
|
|
156
|
-
* the step ends, which is what makes the effort decision possible at all: the
|
|
157
|
-
* decision has to be made for the *next* request.
|
|
158
|
-
*
|
|
159
|
-
* @param {{runs: Map<string, any>}} watch @param {string} sid @param {number} chars
|
|
160
|
-
*/
|
|
161
|
-
export function noteReasoning(watch, sid, chars) {
|
|
162
|
-
if (!Number.isFinite(chars) || chars <= 0) return
|
|
163
|
-
const run = runOf(watch, sid)
|
|
164
|
-
run.stepChars += chars
|
|
165
|
-
run.turnChars += chars
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Open a step: close the previous one's verdict, and say whether this step is
|
|
170
|
-
* the one that carries the ladder text.
|
|
171
|
-
*
|
|
172
|
-
* @param {{runs: Map<string, any>}} watch @param {string} sid
|
|
173
|
-
* @param {{turn?: number, step?: number, firstStep?: boolean}} where
|
|
174
|
-
* @returns {{directive?: string, nudge?: string} | undefined}
|
|
175
|
-
*/
|
|
176
|
-
export function beginStep(watch, sid, where) {
|
|
177
|
-
const run = runOf(watch, sid)
|
|
178
|
-
const step = Number.isFinite(where?.step) ? Math.floor(where.step) : run.step + 1
|
|
179
|
-
const newTurnStart = where?.firstStep === true || step <= 1
|
|
180
|
-
if (newTurnStart) {
|
|
181
|
-
const fresh = newTurn()
|
|
182
|
-
fresh.step = step
|
|
183
|
-
watch.runs.set(sid, fresh)
|
|
184
|
-
return { directive: DIRECTIVES[where?.variant] || DIRECTIVE }
|
|
185
|
-
}
|
|
186
|
-
// The previous step's streamed volume is the evidence the next request acts on.
|
|
187
|
-
run.hot = run.stepChars >= HOT_CHARS
|
|
188
|
-
run.stepChars = 0
|
|
189
|
-
run.step = step
|
|
190
|
-
const plan = {}
|
|
191
|
-
if (run.nudges < MAX_NUDGES && run.turnChars >= NUDGE_CHARS * (run.nudges + 1) && run.turnChars > run.nudgedAt) {
|
|
192
|
-
run.nudges += 1
|
|
193
|
-
run.nudgedAt = run.turnChars
|
|
194
|
-
run.hot = true
|
|
195
|
-
plan.nudge = `You have streamed about ${run.turnChars} characters of reasoning this turn. Use what you already have: decide, take the next rung, and say in one line what you skipped. Do not open a new line of investigation unless the last result contradicted an assumption.`
|
|
196
|
-
}
|
|
197
|
-
return Object.keys(plan).length === 0 ? undefined : plan
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* A tool result came back. An error is the one signal that the model may need
|
|
202
|
-
* *more* thinking, so it cancels any pending step-down for a couple of steps.
|
|
203
|
-
*
|
|
204
|
-
* @param {{runs: Map<string, any>}} watch @param {string} sid @param {{isError?: boolean}} observation
|
|
205
|
-
*/
|
|
206
|
-
export function noteToolResult(watch, sid, observation) {
|
|
207
|
-
if (observation?.isError !== true) return
|
|
208
|
-
const run = runOf(watch, sid)
|
|
209
|
-
run.hot = false
|
|
210
|
-
run.coolUntil = run.step + ERROR_COOLDOWN + 1
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* The effort this request should carry.
|
|
215
|
-
*
|
|
216
|
-
* Returns `base` untouched unless the ledger holds evidence: a hot previous
|
|
217
|
-
* step, no pending cooldown, budget left, and a base that is a rung above
|
|
218
|
-
* `FLOOR`. The caller must treat `effort === base` as "nothing happened".
|
|
219
|
-
*
|
|
220
|
-
* @param {{runs: Map<string, any>}} watch @param {string} sid
|
|
221
|
-
* @param {string | undefined} base - the effort the harness resolved.
|
|
222
|
-
* @returns {{effort: string | undefined, reduced: boolean}}
|
|
223
|
-
*/
|
|
224
|
-
export function planEffort(watch, sid, base) {
|
|
225
|
-
if (typeof base !== 'string') return { effort: base, reduced: false }
|
|
226
|
-
const run = runOf(watch, sid)
|
|
227
|
-
if (run.reducedStep === run.step) {
|
|
228
|
-
// One step, one reduction: a retry inside the same step must not walk the
|
|
229
|
-
// effort ladder down twice before anything has been measured again.
|
|
230
|
-
return { effort: base, reduced: false }
|
|
231
|
-
}
|
|
232
|
-
if (!run.hot || run.step < run.coolUntil || run.reductions >= MAX_REDUCTIONS) {
|
|
233
|
-
return { effort: base, reduced: false }
|
|
234
|
-
}
|
|
235
|
-
const cheaper = cheaperEffort(base)
|
|
236
|
-
if (cheaper === undefined) return { effort: base, reduced: false }
|
|
237
|
-
run.reductions += 1
|
|
238
|
-
run.reducedStep = run.step
|
|
239
|
-
return { effort: cheaper, reduced: true }
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/** The notice body for the nudge, built where it is sent (see `index.js`). */
|
|
243
|
-
export const NUDGE_SUMMARY = 'lazy mode: decide with what you have'
|
|
244
|
-
|
|
245
|
-
export { DIRECTIVE_SUMMARY }
|