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
|
@@ -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,14 +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
|
-
|
|
285
|
+
probeFirst: gear.directive && config.probeFirst === true,
|
|
286
|
+
actBeforePlan: gear.directive && config.actBeforePlan === true,
|
|
287
|
+
repeatNudge: gear.directive && config.repeatNudge === true,
|
|
288
288
|
/**
|
|
289
289
|
* The stall guard, the one *hard* bound this plugin owns: it cancels a single
|
|
290
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}.
|
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,220 +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
|
-
const DIRECTIVE_SUMMARY = 'lazy mode: first rung that works, no scope beyond the task'
|
|
51
|
-
|
|
52
|
-
/** Reasoning rungs a route may name, weakest first. */
|
|
53
|
-
export const EFFORT_RUNGS = Object.freeze(['minimal', 'low', 'medium', 'high', 'xhigh', 'max'])
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* The lowest rung this module will ask for. Effort is the user's contract; a
|
|
57
|
-
* watchdog may spend a little of the headroom a high setting leaves, and stops
|
|
58
|
-
* where the default would have been.
|
|
59
|
-
*/
|
|
60
|
-
const FLOOR = 'low'
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Streamed reasoning characters in one step that count as over-thinking. On the
|
|
64
|
-
* measured route a normal step streams a few hundred characters and a step stuck
|
|
65
|
-
* on scope streams thousands, so this is deliberately not a tight bound.
|
|
66
|
-
*/
|
|
67
|
-
const HOT_CHARS = 1600
|
|
68
|
-
|
|
69
|
-
/** Reasoning characters a turn may spend before the first nudge. */
|
|
70
|
-
const NUDGE_CHARS = 4000
|
|
71
|
-
|
|
72
|
-
/** How many notices one turn may receive. Two: one early, one late. */
|
|
73
|
-
const MAX_NUDGES = 2
|
|
74
|
-
|
|
75
|
-
/** How many effort reductions one turn may use. */
|
|
76
|
-
const MAX_REDUCTIONS = 2
|
|
77
|
-
|
|
78
|
-
/** Steps a reduction is off limits for after an error result (the step it fired on plus two). */
|
|
79
|
-
const ERROR_COOLDOWN = 2
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* The next rung down, or `undefined` when the value is not a rung this module
|
|
83
|
-
* understands (a route may use its own vocabulary — never guess at it) or is
|
|
84
|
-
* already at `FLOOR`.
|
|
85
|
-
*
|
|
86
|
-
* @param {string} value
|
|
87
|
-
* @returns {string | undefined}
|
|
88
|
-
*/
|
|
89
|
-
export function cheaperEffort(value) {
|
|
90
|
-
const at = EFFORT_RUNGS.indexOf(value)
|
|
91
|
-
if (at <= 0) return undefined
|
|
92
|
-
const floor = EFFORT_RUNGS.indexOf(FLOOR)
|
|
93
|
-
if (at <= floor) return undefined
|
|
94
|
-
return EFFORT_RUNGS[at - 1]
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Per-session bookkeeping; one turn at a time is all a session runs. */
|
|
98
|
-
const newTurn = () => ({
|
|
99
|
-
step: 0,
|
|
100
|
-
hot: false,
|
|
101
|
-
stepChars: 0,
|
|
102
|
-
turnChars: 0,
|
|
103
|
-
nudges: 0,
|
|
104
|
-
reductions: 0,
|
|
105
|
-
coolUntil: 0,
|
|
106
|
-
nudgedAt: 0,
|
|
107
|
-
reducedStep: -1,
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Create the per-session ledger. Pure and dependency-free so the policy can be
|
|
112
|
-
* tested without a harness.
|
|
113
|
-
*/
|
|
114
|
-
export function createLazyWatch() {
|
|
115
|
-
return { runs: new Map() }
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** @param {{runs: Map<string, any>}} watch @param {string} sid */
|
|
119
|
-
const runOf = (watch, sid) => {
|
|
120
|
-
let run = watch.runs.get(sid)
|
|
121
|
-
if (run === undefined) {
|
|
122
|
-
run = newTurn()
|
|
123
|
-
watch.runs.set(sid, run)
|
|
124
|
-
if (watch.runs.size > 64) watch.runs.delete(watch.runs.keys().next().value)
|
|
125
|
-
}
|
|
126
|
-
return run
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Count streamed reasoning characters. Called from the stream tap, i.e. before
|
|
131
|
-
* the step ends, which is what makes the effort decision possible at all: the
|
|
132
|
-
* decision has to be made for the *next* request.
|
|
133
|
-
*
|
|
134
|
-
* @param {{runs: Map<string, any>}} watch @param {string} sid @param {number} chars
|
|
135
|
-
*/
|
|
136
|
-
export function noteReasoning(watch, sid, chars) {
|
|
137
|
-
if (!Number.isFinite(chars) || chars <= 0) return
|
|
138
|
-
const run = runOf(watch, sid)
|
|
139
|
-
run.stepChars += chars
|
|
140
|
-
run.turnChars += chars
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Open a step: close the previous one's verdict, and say whether this step is
|
|
145
|
-
* the one that carries the ladder text.
|
|
146
|
-
*
|
|
147
|
-
* @param {{runs: Map<string, any>}} watch @param {string} sid
|
|
148
|
-
* @param {{turn?: number, step?: number, firstStep?: boolean}} where
|
|
149
|
-
* @returns {{directive?: string, nudge?: string} | undefined}
|
|
150
|
-
*/
|
|
151
|
-
export function beginStep(watch, sid, where) {
|
|
152
|
-
const run = runOf(watch, sid)
|
|
153
|
-
const step = Number.isFinite(where?.step) ? Math.floor(where.step) : run.step + 1
|
|
154
|
-
const newTurnStart = where?.firstStep === true || step <= 1
|
|
155
|
-
if (newTurnStart) {
|
|
156
|
-
const fresh = newTurn()
|
|
157
|
-
fresh.step = step
|
|
158
|
-
watch.runs.set(sid, fresh)
|
|
159
|
-
return { directive: DIRECTIVE }
|
|
160
|
-
}
|
|
161
|
-
// The previous step's streamed volume is the evidence the next request acts on.
|
|
162
|
-
run.hot = run.stepChars >= HOT_CHARS
|
|
163
|
-
run.stepChars = 0
|
|
164
|
-
run.step = step
|
|
165
|
-
const plan = {}
|
|
166
|
-
if (run.nudges < MAX_NUDGES && run.turnChars >= NUDGE_CHARS * (run.nudges + 1) && run.turnChars > run.nudgedAt) {
|
|
167
|
-
run.nudges += 1
|
|
168
|
-
run.nudgedAt = run.turnChars
|
|
169
|
-
run.hot = true
|
|
170
|
-
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.`
|
|
171
|
-
}
|
|
172
|
-
return Object.keys(plan).length === 0 ? undefined : plan
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* A tool result came back. An error is the one signal that the model may need
|
|
177
|
-
* *more* thinking, so it cancels any pending step-down for a couple of steps.
|
|
178
|
-
*
|
|
179
|
-
* @param {{runs: Map<string, any>}} watch @param {string} sid @param {{isError?: boolean}} observation
|
|
180
|
-
*/
|
|
181
|
-
export function noteToolResult(watch, sid, observation) {
|
|
182
|
-
if (observation?.isError !== true) return
|
|
183
|
-
const run = runOf(watch, sid)
|
|
184
|
-
run.hot = false
|
|
185
|
-
run.coolUntil = run.step + ERROR_COOLDOWN + 1
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* The effort this request should carry.
|
|
190
|
-
*
|
|
191
|
-
* Returns `base` untouched unless the ledger holds evidence: a hot previous
|
|
192
|
-
* step, no pending cooldown, budget left, and a base that is a rung above
|
|
193
|
-
* `FLOOR`. The caller must treat `effort === base` as "nothing happened".
|
|
194
|
-
*
|
|
195
|
-
* @param {{runs: Map<string, any>}} watch @param {string} sid
|
|
196
|
-
* @param {string | undefined} base - the effort the harness resolved.
|
|
197
|
-
* @returns {{effort: string | undefined, reduced: boolean}}
|
|
198
|
-
*/
|
|
199
|
-
export function planEffort(watch, sid, base) {
|
|
200
|
-
if (typeof base !== 'string') return { effort: base, reduced: false }
|
|
201
|
-
const run = runOf(watch, sid)
|
|
202
|
-
if (run.reducedStep === run.step) {
|
|
203
|
-
// One step, one reduction: a retry inside the same step must not walk the
|
|
204
|
-
// effort ladder down twice before anything has been measured again.
|
|
205
|
-
return { effort: base, reduced: false }
|
|
206
|
-
}
|
|
207
|
-
if (!run.hot || run.step < run.coolUntil || run.reductions >= MAX_REDUCTIONS) {
|
|
208
|
-
return { effort: base, reduced: false }
|
|
209
|
-
}
|
|
210
|
-
const cheaper = cheaperEffort(base)
|
|
211
|
-
if (cheaper === undefined) return { effort: base, reduced: false }
|
|
212
|
-
run.reductions += 1
|
|
213
|
-
run.reducedStep = run.step
|
|
214
|
-
return { effort: cheaper, reduced: true }
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/** The notice body for the nudge, built where it is sent (see `index.js`). */
|
|
218
|
-
export const NUDGE_SUMMARY = 'lazy mode: decide with what you have'
|
|
219
|
-
|
|
220
|
-
export { DIRECTIVE_SUMMARY }
|