dsh-context-compression-improved 0.4.0-beta.1 → 0.5.0
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/CHANGELOG.ja.md +68 -36
- package/CHANGELOG.ko.md +67 -35
- package/CHANGELOG.md +195 -134
- package/CHANGELOG.zh.md +64 -36
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/docs/installation.ja.md +2 -2
- package/docs/installation.ko.md +2 -2
- package/docs/installation.md +103 -78
- package/docs/installation.zh.md +100 -77
- package/docs/repair-log.md +54 -0
- package/package.json +1 -1
- package/packages/selector/lib/{config.js → advisor-state.js} +329 -5
- package/packages/selector/lib/client.d.ts +7 -0
- package/packages/selector/lib/client.js +33 -3
- package/packages/selector/lib/index.d.ts +7 -0
- package/packages/selector/lib/index.js +112 -3
- package/packages/selector/lib/pruner.d.ts +128 -1
- package/packages/selector/lib/pruner.js +2802 -1374
- package/packages/selector/src/client/ReviewOverlay.tsx +1 -1
- package/packages/selector/src/client/index.ts +1 -1
- package/packages/selector/src/client/preset-options.ts +2 -0
- package/packages/selector/src/index.ts +129 -49
- package/packages/selector/src/profiles.ts +48 -0
- package/packages/selector/src/pruner/content.ts +18 -5
- package/packages/selector/src/pruner/state.ts +3 -0
- package/packages/selector/src/pruner/types.ts +23 -5
- package/packages/selector/src/pruner.ts +297 -162
- package/packages/selector/src/runtime/adaptive-cost.ts +23 -12
- package/packages/selector/src/runtime/audit.ts +40 -2
- package/packages/selector/src/runtime/config.ts +88 -1
- package/packages/selector/src/runtime/measurement.ts +31 -2
- package/packages/selector/src/runtime/reducers.ts +1115 -97
- package/packages/selector/src/runtime/tokenpilot/advisor-prompt.ts +188 -0
- package/packages/selector/src/runtime/tokenpilot/advisor-state.ts +133 -0
- package/packages/selector/src/runtime/tokenpilot/advisor.ts +419 -0
- package/packages/selector/src/runtime/tokenpilot/dedup.ts +1 -1
- package/packages/selector/src/runtime/tokenpilot/estimator.ts +8 -118
- package/packages/selector/src/runtime/tokenpilot/locator.ts +1 -1
- package/packages/selector/src/runtime/tokenpilot/proposal.ts +76 -32
- package/packages/selector/src/runtime/tokenpilot/read-state.ts +23 -2
- package/packages/selector/src/runtime/tokenpilot/review-registry.ts +117 -0
- package/packages/selector/src/runtime/tokenpilot/sidechannel.ts +303 -0
- package/packages/selector/src/runtime/toolclass.ts +103 -0
- package/packages/selector/src/runtime/types.ts +37 -0
- package/packages/selector/tests/advisor-report.host.spec.ts +223 -0
- package/packages/selector/tests/public/package-contract.client.spec.ts +2 -1
- package/packages/selector/tests/review-routes-registry.host.spec.ts +142 -0
- package/packages/selector/tests/runtime/adaptive-cost.spec.ts +7 -7
- package/packages/selector/tests/runtime/advisor-invariant.spec.ts +272 -0
- package/packages/selector/tests/runtime/advisor.spec.ts +226 -0
- package/packages/selector/tests/runtime/audit.spec.ts +88 -1
- package/packages/selector/tests/runtime/char-basis.spec.ts +30 -0
- package/packages/selector/tests/runtime/code-skeleton.spec.ts +14 -3
- package/packages/selector/tests/runtime/frequency-longstrings.spec.ts +74 -0
- package/packages/selector/tests/runtime/html-reducer.spec.ts +212 -0
- package/packages/selector/tests/runtime/line-mapping.spec.ts +153 -0
- package/packages/selector/tests/runtime/prose-reducers.spec.ts +133 -0
- package/packages/selector/tests/runtime/public/public-runtime.spec.ts +198 -27
- package/packages/selector/tests/runtime/read-input-cap.spec.ts +33 -0
- package/packages/selector/tests/runtime/search-reducer.spec.ts +110 -0
- package/packages/selector/tests/runtime/sidechannel.spec.ts +241 -0
- package/packages/selector/tests/runtime/toc-and-bundled.spec.ts +159 -0
- package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +12 -0
- package/packages/selector/tests/runtime/tokenpilot/proposal.spec.ts +194 -0
- package/packages/selector/tests/runtime/tokenpilot/pruner-review.spec.ts +70 -1
- package/packages/selector/tests/runtime/tokenpilot/read-state.spec.ts +24 -0
- package/packages/selector/tests/runtime/toolclass.spec.ts +156 -0
- package/scripts/toolclass-corpus-replay.mjs +281 -0
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
TokenCount,
|
|
6
6
|
} from './measurement.ts'
|
|
7
7
|
import { decimalRateNanoUnits } from './deepseek-official-pricing.ts'
|
|
8
|
+
import { charsToTokens } from './config.ts'
|
|
8
9
|
|
|
9
10
|
/** Inputs that conservatively bound one already-planned Adaptive History batch. */
|
|
10
11
|
export interface AdaptiveTokenBoundsInput {
|
|
@@ -25,7 +26,7 @@ export interface AdaptiveTokenBoundsInput {
|
|
|
25
26
|
/** Available lower-benefit and upper-risk bounds for one History batch. */
|
|
26
27
|
export interface AvailableAdaptiveTokenBounds {
|
|
27
28
|
readonly kind: 'available'
|
|
28
|
-
readonly measurementKind: 'exact-tokenizer' | 'tokenizer-estimate'
|
|
29
|
+
readonly measurementKind: 'exact-tokenizer' | 'tokenizer-estimate' | 'characters'
|
|
29
30
|
/** D: conservative lower bound of input tokens removed by the plan. */
|
|
30
31
|
readonly reclaimedLowerBoundTokens: number
|
|
31
32
|
/** A: conservative upper bound of retained suffix tokens that may lose a hit. */
|
|
@@ -89,24 +90,34 @@ export function deriveAdaptiveTokenBounds(input: AdaptiveTokenBoundsInput): Adap
|
|
|
89
90
|
|
|
90
91
|
let identity: { readonly tokenizerId: string; readonly tokenizerRevision: string } | undefined
|
|
91
92
|
let exactPrefixLowerBoundTokens = 0
|
|
93
|
+
let characterDerivedPrefix = false
|
|
92
94
|
const seen = new Set<number>()
|
|
93
95
|
for (const node of input.measuredNodes) {
|
|
94
96
|
if (!isCount(node.seq) || seen.has(node.seq)) return unknown('invalid-measured-node-sequence')
|
|
95
97
|
seen.add(node.seq)
|
|
96
|
-
if (node.seq >= input.earliestChangedSeq
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
if (node.seq >= input.earliestChangedSeq) continue
|
|
99
|
+
if (node.count.kind === 'exact-tokenizer') {
|
|
100
|
+
if (!isCount(node.count.tokens)) return unknown('invalid-exact-prefix-count')
|
|
101
|
+
if (node.count.tokenizerRevision !== input.expectedTokenizerRevision) {
|
|
102
|
+
return unknown('exact-prefix-tokenizer-revision-mismatch')
|
|
103
|
+
}
|
|
104
|
+
if (identity !== undefined
|
|
105
|
+
&& (identity.tokenizerId !== node.count.tokenizerId
|
|
106
|
+
|| identity.tokenizerRevision !== node.count.tokenizerRevision)) {
|
|
107
|
+
return unknown('exact-prefix-tokenizer-identity-mismatch')
|
|
108
|
+
}
|
|
109
|
+
identity ??= node.count
|
|
110
|
+
exactPrefixLowerBoundTokens += node.count.tokens
|
|
111
|
+
} else {
|
|
112
|
+
// Character-basis degrade: a prefix node without an exact count still
|
|
113
|
+
// occupies retained-request space, so charge its conservative
|
|
114
|
+
// character→token derivation instead of skipping it silently.
|
|
115
|
+
exactPrefixLowerBoundTokens += charsToTokens(node.characterPressure)
|
|
116
|
+
characterDerivedPrefix = true
|
|
100
117
|
}
|
|
101
|
-
if (identity !== undefined
|
|
102
|
-
&& (identity.tokenizerId !== node.count.tokenizerId
|
|
103
|
-
|| identity.tokenizerRevision !== node.count.tokenizerRevision)) {
|
|
104
|
-
return unknown('exact-prefix-tokenizer-identity-mismatch')
|
|
105
|
-
}
|
|
106
|
-
identity ??= node.count
|
|
107
|
-
exactPrefixLowerBoundTokens += node.count.tokens
|
|
108
118
|
if (!isCount(exactPrefixLowerBoundTokens)) return unknown('exact-prefix-overflow')
|
|
109
119
|
}
|
|
120
|
+
if (characterDerivedPrefix) measurementKind = 'characters'
|
|
110
121
|
|
|
111
122
|
const accounted = exactPrefixLowerBoundTokens + reclaimedLowerBoundTokens
|
|
112
123
|
if (!isCount(accounted) || accounted > input.previousPromptTokens) {
|
|
@@ -79,7 +79,7 @@ export interface CompressionPolicyResolvedAuditRecord extends CompressionAuditBa
|
|
|
79
79
|
readonly tokenizer?: CompressionTokenizerAuditFact
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
/** One committed model-free surface rewrite and its
|
|
82
|
+
/** One committed model-free surface rewrite and its accounting. */
|
|
83
83
|
export interface CompressionRewriteAuditRecord extends CompressionAuditBase {
|
|
84
84
|
readonly kind: 'rewrite'
|
|
85
85
|
readonly profile: CompressionProfile
|
|
@@ -94,8 +94,24 @@ export interface CompressionRewriteAuditRecord extends CompressionAuditBase {
|
|
|
94
94
|
readonly tokensBefore: number
|
|
95
95
|
readonly tokensAfter: number
|
|
96
96
|
readonly tokensRemoved: number
|
|
97
|
+
/**
|
|
98
|
+
* Which measurement backed this record's decision and token figures:
|
|
99
|
+
* 'exact-tokenizer' when both sides of the reduction carry one bundled
|
|
100
|
+
* tokenizer identity, 'characters' when the proof ran on Unicode code
|
|
101
|
+
* points and `tokensBefore/After` were derived at 4.0 chars per token
|
|
102
|
+
* (`tokenizerId: 'characters'`, `tokenizerRevision: 'chars-per-token-4.0'`).
|
|
103
|
+
*/
|
|
104
|
+
readonly measurementBasis: 'exact-tokenizer' | 'characters'
|
|
97
105
|
readonly tokenizerId: string
|
|
98
106
|
readonly tokenizerRevision: string
|
|
107
|
+
/**
|
|
108
|
+
* Original-event lines the reducer elided (task_4c/G7, from
|
|
109
|
+
* `ReducerOutput.elidedLines` via the plan). Present when the landing
|
|
110
|
+
* reducer reports it; absent for placeholder/trim rewrites. Exists so the
|
|
111
|
+
* compress→retrieve M/N ratio is computable from session logs alone —
|
|
112
|
+
* never printed into replacement content.
|
|
113
|
+
*/
|
|
114
|
+
readonly elidedLines?: number
|
|
99
115
|
}
|
|
100
116
|
|
|
101
117
|
/** Why an enabled component did not rewrite, or why it was disabled. */
|
|
@@ -107,7 +123,7 @@ export interface CompressionComponentEvaluationAuditRecord extends CompressionAu
|
|
|
107
123
|
readonly status: CompressionAuditEvaluationStatus
|
|
108
124
|
readonly reason: string
|
|
109
125
|
readonly historyMode?: HistoryMode
|
|
110
|
-
readonly measurementKind?: 'exact-tokenizer' | 'tokenizer-estimate' | 'unavailable'
|
|
126
|
+
readonly measurementKind?: 'exact-tokenizer' | 'tokenizer-estimate' | 'characters' | 'unavailable'
|
|
111
127
|
readonly currentTokens?: number
|
|
112
128
|
readonly triggerTokens?: number
|
|
113
129
|
readonly targetTokens?: number
|
|
@@ -167,6 +183,27 @@ export interface EstimatorOutcomeAuditRecord extends CompressionAuditBase {
|
|
|
167
183
|
readonly ok: boolean
|
|
168
184
|
}
|
|
169
185
|
|
|
186
|
+
/** One background relevance-advisor pass. Only numeric metadata — never prompts, keys, or content. */
|
|
187
|
+
export interface AdvisorOutcomeAuditRecord extends CompressionAuditBase {
|
|
188
|
+
readonly kind: 'advisor-outcome'
|
|
189
|
+
/** Which advisory stage produced this record. */
|
|
190
|
+
readonly phase: 'summary' | 'scoring' | 'decay'
|
|
191
|
+
/** LLM channel; omitted for the locally computed 'decay' phase. */
|
|
192
|
+
readonly channel?: 'host' | 'direct'
|
|
193
|
+
readonly ok: boolean
|
|
194
|
+
/** Whether the pass found candidates to sample (scoring phase). */
|
|
195
|
+
readonly sampledCount?: number
|
|
196
|
+
/** Prefix-decay figure in [0, 1] (decay phase; also emitted by summary/scoring on success). */
|
|
197
|
+
readonly decay?: number
|
|
198
|
+
/** Weighted surface the decay was computed over, in Unicode code points. */
|
|
199
|
+
readonly weightedChars?: number
|
|
200
|
+
/** Turn index the pass ran at. */
|
|
201
|
+
readonly turnIndex?: number
|
|
202
|
+
/** Aligned failure/skip reason code (e.g. 'no-direct-endpoint', 'parse-failed'). */
|
|
203
|
+
readonly reason?: string
|
|
204
|
+
readonly latencyMs: number
|
|
205
|
+
}
|
|
206
|
+
|
|
170
207
|
/** Lifecycle of one human-gated review proposal. Only numeric and enum fields — never content. */
|
|
171
208
|
export interface ReviewOutcomeAuditRecord extends CompressionAuditBase {
|
|
172
209
|
readonly kind: 'review-outcome'
|
|
@@ -203,6 +240,7 @@ export type CompressionAuditRecord =
|
|
|
203
240
|
| NativeAutoCompactAuditRecord
|
|
204
241
|
| SummaryLocatorAuditRecord
|
|
205
242
|
| EstimatorOutcomeAuditRecord
|
|
243
|
+
| AdvisorOutcomeAuditRecord
|
|
206
244
|
| ReviewOutcomeAuditRecord
|
|
207
245
|
|
|
208
246
|
/** Minimal logger method consumed by the audit publisher. */
|
|
@@ -128,6 +128,8 @@ export function parsePresetOptionsSettings(value: unknown): PresetOptionsSetting
|
|
|
128
128
|
'dedupeToolResults', 'summaryLocator', 'prefixStabilizer', 'readState', 'estimatorMode',
|
|
129
129
|
'estimatorProvider', 'estimatorModel', 'estimatorBaseUrl', 'estimatorApiKey', 'estimatorTimeoutMs',
|
|
130
130
|
'reviewMode', 'reviewTimeoutTurns', 'cacheHitDiscountAlpha', 'reviewHighImpactTokens',
|
|
131
|
+
'advisorMode', 'advisorTimeoutMs', 'advisorRefreshTurns', 'advisorScoreThreshold', 'advisorSampleLimit',
|
|
132
|
+
'advisorMinTokens',
|
|
131
133
|
])
|
|
132
134
|
const unknown = Object.keys(value).find(key => !allowed.has(key))
|
|
133
135
|
if (unknown !== undefined) {
|
|
@@ -144,6 +146,39 @@ export function parsePresetOptionsSettings(value: unknown): PresetOptionsSetting
|
|
|
144
146
|
if (estimatorMode !== undefined && estimatorMode !== '' && estimatorMode !== 'host' && estimatorMode !== 'direct') {
|
|
145
147
|
throw new TypeError('Context-compression presetOptions.estimatorMode must be "", "host", or "direct"')
|
|
146
148
|
}
|
|
149
|
+
const advisorMode = value.advisorMode
|
|
150
|
+
if (advisorMode !== undefined && advisorMode !== '' && advisorMode !== 'host' && advisorMode !== 'direct') {
|
|
151
|
+
throw new TypeError('Context-compression presetOptions.advisorMode must be "", "host", or "direct"')
|
|
152
|
+
}
|
|
153
|
+
const advisorTimeoutMs = value.advisorTimeoutMs
|
|
154
|
+
if (advisorTimeoutMs !== undefined
|
|
155
|
+
&& (typeof advisorTimeoutMs !== 'number' || !Number.isSafeInteger(advisorTimeoutMs)
|
|
156
|
+
|| advisorTimeoutMs < 100 || advisorTimeoutMs > 60_000)) {
|
|
157
|
+
throw new TypeError('Context-compression presetOptions.advisorTimeoutMs must be an integer between 100 and 60000')
|
|
158
|
+
}
|
|
159
|
+
const advisorRefreshTurns = value.advisorRefreshTurns
|
|
160
|
+
if (advisorRefreshTurns !== undefined
|
|
161
|
+
&& (typeof advisorRefreshTurns !== 'number' || !Number.isSafeInteger(advisorRefreshTurns)
|
|
162
|
+
|| advisorRefreshTurns < 1)) {
|
|
163
|
+
throw new TypeError('Context-compression presetOptions.advisorRefreshTurns must be an integer of at least 1')
|
|
164
|
+
}
|
|
165
|
+
const advisorScoreThreshold = value.advisorScoreThreshold
|
|
166
|
+
if (advisorScoreThreshold !== undefined
|
|
167
|
+
&& (typeof advisorScoreThreshold !== 'number' || !Number.isFinite(advisorScoreThreshold)
|
|
168
|
+
|| advisorScoreThreshold <= 0 || advisorScoreThreshold >= 1)) {
|
|
169
|
+
throw new TypeError('Context-compression presetOptions.advisorScoreThreshold must be a number strictly between 0 and 1')
|
|
170
|
+
}
|
|
171
|
+
const advisorSampleLimit = value.advisorSampleLimit
|
|
172
|
+
if (advisorSampleLimit !== undefined
|
|
173
|
+
&& (typeof advisorSampleLimit !== 'number' || !Number.isSafeInteger(advisorSampleLimit)
|
|
174
|
+
|| advisorSampleLimit < 1 || advisorSampleLimit > 64)) {
|
|
175
|
+
throw new TypeError('Context-compression presetOptions.advisorSampleLimit must be an integer between 1 and 64')
|
|
176
|
+
}
|
|
177
|
+
const advisorMinTokens = value.advisorMinTokens
|
|
178
|
+
if (advisorMinTokens !== undefined
|
|
179
|
+
&& (typeof advisorMinTokens !== 'number' || !Number.isSafeInteger(advisorMinTokens) || advisorMinTokens < 1)) {
|
|
180
|
+
throw new TypeError('Context-compression presetOptions.advisorMinTokens must be a positive integer')
|
|
181
|
+
}
|
|
147
182
|
const estimatorTimeoutMs = value.estimatorTimeoutMs
|
|
148
183
|
if (estimatorTimeoutMs !== undefined
|
|
149
184
|
&& (typeof estimatorTimeoutMs !== 'number' || !Number.isSafeInteger(estimatorTimeoutMs)
|
|
@@ -190,6 +225,12 @@ export function parsePresetOptionsSettings(value: unknown): PresetOptionsSetting
|
|
|
190
225
|
if (reviewTimeoutTurns !== undefined) result.reviewTimeoutTurns = reviewTimeoutTurns as number
|
|
191
226
|
if (cacheHitDiscountAlpha !== undefined) result.cacheHitDiscountAlpha = cacheHitDiscountAlpha as number
|
|
192
227
|
if (reviewHighImpactTokens !== undefined) result.reviewHighImpactTokens = reviewHighImpactTokens as number
|
|
228
|
+
if (advisorMode !== undefined) result.advisorMode = advisorMode as '' | 'host' | 'direct'
|
|
229
|
+
if (advisorTimeoutMs !== undefined) result.advisorTimeoutMs = advisorTimeoutMs as number
|
|
230
|
+
if (advisorRefreshTurns !== undefined) result.advisorRefreshTurns = advisorRefreshTurns as number
|
|
231
|
+
if (advisorScoreThreshold !== undefined) result.advisorScoreThreshold = advisorScoreThreshold as number
|
|
232
|
+
if (advisorSampleLimit !== undefined) result.advisorSampleLimit = advisorSampleLimit as number
|
|
233
|
+
if (advisorMinTokens !== undefined) result.advisorMinTokens = advisorMinTokens as number
|
|
193
234
|
return result
|
|
194
235
|
}
|
|
195
236
|
|
|
@@ -308,6 +349,7 @@ const CONFIG_KEYS: ReadonlySet<string> = new Set([
|
|
|
308
349
|
'historyKeepRecentToolCalls',
|
|
309
350
|
'historyKeepRecentTokens',
|
|
310
351
|
'historyMinReclaimTokens',
|
|
352
|
+
'readInputCapChars',
|
|
311
353
|
'autoCompactThresholdPercent',
|
|
312
354
|
'presetOptions',
|
|
313
355
|
])
|
|
@@ -335,6 +377,24 @@ export function codePointLength(text: string): number {
|
|
|
335
377
|
return length
|
|
336
378
|
}
|
|
337
379
|
|
|
380
|
+
/**
|
|
381
|
+
* Conservative upper bound on characters per token. Same convention the
|
|
382
|
+
* readInputCapChars invariant already uses (see resolvePolicy): English
|
|
383
|
+
* code runs approximately four characters per token at the upper bound.
|
|
384
|
+
*/
|
|
385
|
+
export const CHARS_PER_TOKEN = 4.0
|
|
386
|
+
|
|
387
|
+
/** Express one token-named policy gate on the character basis. */
|
|
388
|
+
export function charsForTokens(tokens: number): number {
|
|
389
|
+
return tokens * CHARS_PER_TOKEN
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** Derive the telemetry-only token figure from a character measurement. */
|
|
393
|
+
export function charsToTokens(chars: number): number {
|
|
394
|
+
if (!Number.isFinite(chars) || chars <= 0) return 0
|
|
395
|
+
return Math.max(1, Math.round(chars / CHARS_PER_TOKEN))
|
|
396
|
+
}
|
|
397
|
+
|
|
338
398
|
/**
|
|
339
399
|
* Test whether a settings value names a supported compression profile.
|
|
340
400
|
* @param value - untrusted settings value.
|
|
@@ -376,6 +436,7 @@ export function resolveConfig(config: ToolResultPruneConfig = {}): ResolvedConfi
|
|
|
376
436
|
...config.historyKeepRecentToolCalls === undefined ? {} : { historyKeepRecentToolCalls: config.historyKeepRecentToolCalls },
|
|
377
437
|
...config.historyKeepRecentTokens === undefined ? {} : { historyKeepRecentTokens: config.historyKeepRecentTokens },
|
|
378
438
|
...config.historyMinReclaimTokens === undefined ? {} : { historyMinReclaimTokens: config.historyMinReclaimTokens },
|
|
439
|
+
...config.readInputCapChars === undefined ? {} : { readInputCapChars: config.readInputCapChars },
|
|
379
440
|
...config.autoCompactThresholdPercent === undefined ? {} : { autoCompactThresholdPercent: config.autoCompactThresholdPercent },
|
|
380
441
|
...config.presetOptions === undefined ? {} : { presetOptions: config.presetOptions },
|
|
381
442
|
}
|
|
@@ -387,7 +448,7 @@ export function resolveConfig(config: ToolResultPruneConfig = {}): ResolvedConfi
|
|
|
387
448
|
for (const key of [
|
|
388
449
|
'nativeTriggerTokens', 'nativeTargetTokens', 'freshTriggerTokens', 'freshTargetTokens',
|
|
389
450
|
'aggregateTriggerTokens', 'aggregateTargetTokens', 'historyTriggerTokens',
|
|
390
|
-
'historyMinReclaimTokens',
|
|
451
|
+
'historyMinReclaimTokens', 'readInputCapChars',
|
|
391
452
|
] as const) {
|
|
392
453
|
const value = resolved[key]
|
|
393
454
|
if (value !== undefined) assertPositiveInteger(key, value)
|
|
@@ -443,6 +504,15 @@ const PRESET_OPTION_DEFAULTS: PresetOptions = deepFreeze({
|
|
|
443
504
|
reviewTimeoutTurns: 6,
|
|
444
505
|
cacheHitDiscountAlpha: 0.1,
|
|
445
506
|
reviewHighImpactTokens: 4000,
|
|
507
|
+
// Advisory advisor ships off: statistics and suggestions only, never a gate.
|
|
508
|
+
advisor: {
|
|
509
|
+
mode: '',
|
|
510
|
+
timeoutMs: 8_000,
|
|
511
|
+
refreshTurns: 8,
|
|
512
|
+
scoreThreshold: 0.35,
|
|
513
|
+
sampleLimit: 16,
|
|
514
|
+
minTokens: 250,
|
|
515
|
+
},
|
|
446
516
|
})
|
|
447
517
|
|
|
448
518
|
/**
|
|
@@ -464,6 +534,14 @@ function mergePresetOptions(overrides: PresetOptionsSettings | undefined): Prese
|
|
|
464
534
|
reviewTimeoutTurns: overrides.reviewTimeoutTurns ?? PRESET_OPTION_DEFAULTS.reviewTimeoutTurns,
|
|
465
535
|
cacheHitDiscountAlpha: overrides.cacheHitDiscountAlpha ?? PRESET_OPTION_DEFAULTS.cacheHitDiscountAlpha,
|
|
466
536
|
reviewHighImpactTokens: overrides.reviewHighImpactTokens ?? PRESET_OPTION_DEFAULTS.reviewHighImpactTokens,
|
|
537
|
+
advisor: {
|
|
538
|
+
mode: overrides.advisorMode ?? PRESET_OPTION_DEFAULTS.advisor.mode,
|
|
539
|
+
timeoutMs: overrides.advisorTimeoutMs ?? PRESET_OPTION_DEFAULTS.advisor.timeoutMs,
|
|
540
|
+
refreshTurns: overrides.advisorRefreshTurns ?? PRESET_OPTION_DEFAULTS.advisor.refreshTurns,
|
|
541
|
+
scoreThreshold: overrides.advisorScoreThreshold ?? PRESET_OPTION_DEFAULTS.advisor.scoreThreshold,
|
|
542
|
+
sampleLimit: overrides.advisorSampleLimit ?? PRESET_OPTION_DEFAULTS.advisor.sampleLimit,
|
|
543
|
+
minTokens: overrides.advisorMinTokens ?? PRESET_OPTION_DEFAULTS.advisor.minTokens,
|
|
544
|
+
},
|
|
467
545
|
})
|
|
468
546
|
}
|
|
469
547
|
|
|
@@ -599,6 +677,7 @@ export function resolvePolicy(
|
|
|
599
677
|
?? linkage?.historyKeepRecentTokens ?? preset.historyKeepRecentTokens,
|
|
600
678
|
historyMinReclaimTokens: config.historyMinReclaimTokens
|
|
601
679
|
?? linkage?.historyMinReclaimTokens ?? preset.historyMinReclaimTokens,
|
|
680
|
+
...config.readInputCapChars === undefined ? {} : { readInputCapChars: config.readInputCapChars },
|
|
602
681
|
...linkage === undefined ? {} : {
|
|
603
682
|
autoCompactTokens: linkage.autoCompactTokens,
|
|
604
683
|
microDeadlineTokens: linkage.microDeadlineTokens,
|
|
@@ -616,6 +695,14 @@ export function resolvePolicy(
|
|
|
616
695
|
if (policy.aggregateTargetTokens >= policy.aggregateTriggerTokens && policy.freshEnabled) {
|
|
617
696
|
throw new Error('context compression policy: aggregate target must be below trigger')
|
|
618
697
|
}
|
|
698
|
+
// G2 invariant: the fresh trigger is in TOKENS while a read input cap is in
|
|
699
|
+
// CHARACTERS. English code runs ≈4.0 chars/token at the conservative upper
|
|
700
|
+
// bound, so a cap at or below `freshTriggerTokens × 4.0` would truncate every
|
|
701
|
+
// read result below the trigger and silently silence the fresh path.
|
|
702
|
+
if (policy.readInputCapChars !== undefined
|
|
703
|
+
&& policy.readInputCapChars <= policy.freshTriggerTokens * 4.0) {
|
|
704
|
+
throw new Error('context compression policy: read input cap would silence the fresh path')
|
|
705
|
+
}
|
|
619
706
|
return deepFreeze(policy)
|
|
620
707
|
}
|
|
621
708
|
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
estimateDeepSeekVisionImageTokens,
|
|
17
17
|
} from './deepseek-v4-vision-tokens.ts'
|
|
18
18
|
import { unavailableTokenCount } from './token-count.ts'
|
|
19
|
+
import { pressureCost } from '../pruner/content.ts'
|
|
19
20
|
import type {
|
|
20
21
|
CanonicalTextTokenCounter,
|
|
21
22
|
ExactTokenizerTokenCount,
|
|
@@ -25,6 +26,19 @@ import type {
|
|
|
25
26
|
|
|
26
27
|
export type { TokenCount } from './token-count.ts'
|
|
27
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Character pressure of one model-visible content array, measured on the same
|
|
31
|
+
* level `SnapshotCandidate.characterPressure` uses: a tool/result message is
|
|
32
|
+
* measured by its inner tool-result content, never by the wrapper block, so the
|
|
33
|
+
* node-level and candidate-level figures stay directly comparable.
|
|
34
|
+
* @param content - the node's model-visible content blocks.
|
|
35
|
+
* @returns character pressure in Unicode code points, plus rich-block costs.
|
|
36
|
+
*/
|
|
37
|
+
function nodeCharacterPressure(content: readonly ContentBlock[]): number {
|
|
38
|
+
const only = content.length === 1 ? content[0] : undefined
|
|
39
|
+
return pressureCost(only?.type === 'tool-result' ? only.content : content)
|
|
40
|
+
}
|
|
41
|
+
|
|
28
42
|
/** Request identity retained only when every dimension is publicly known. */
|
|
29
43
|
export interface ProviderMeasurementKey {
|
|
30
44
|
readonly provider: string
|
|
@@ -77,6 +91,11 @@ export interface IntrinsicImageBlockDiagnostic {
|
|
|
77
91
|
export interface MeasuredTokenSurfaceNode {
|
|
78
92
|
readonly seq: number
|
|
79
93
|
readonly count: TokenCount
|
|
94
|
+
/**
|
|
95
|
+
* Authoritative decision metric for this node: character pressure in
|
|
96
|
+
* Unicode code points (same convention as SnapshotCandidate.characterPressure).
|
|
97
|
+
*/
|
|
98
|
+
readonly characterPressure: number
|
|
80
99
|
/** Intrinsic-grid diagnostic when usable image dimensions were available. */
|
|
81
100
|
readonly intrinsicImageBlockEstimate?: IntrinsicImageBlockDiagnostic
|
|
82
101
|
}
|
|
@@ -87,6 +106,11 @@ export interface CompactionTokenView extends TokenMeasurement {
|
|
|
87
106
|
readonly modelId?: string
|
|
88
107
|
readonly measuredNodes: readonly MeasuredTokenSurfaceNode[]
|
|
89
108
|
readonly currentSurface: TokenCount
|
|
109
|
+
/**
|
|
110
|
+
* Exact character analogue of `currentSurface`: sum of per-node
|
|
111
|
+
* `characterPressure` over the same nodes `currentSurface` covers.
|
|
112
|
+
*/
|
|
113
|
+
readonly currentSurfaceChars: number
|
|
90
114
|
/** Sum of per-node intrinsic padding minima; a diagnostic, not a token bound. */
|
|
91
115
|
readonly intrinsicImageBlockEstimateTokens: number
|
|
92
116
|
readonly latestEnvelopeKey?: ProviderMeasurementKey
|
|
@@ -120,11 +144,13 @@ export function measureForCompaction(ctx: Context, session: Session): Compaction
|
|
|
120
144
|
const measuredNodes = measurement.nodes.map((node): MeasuredTokenSurfaceNode => {
|
|
121
145
|
const event = eventsBySeq.get(Number(node.seq))
|
|
122
146
|
if (event === undefined) {
|
|
123
|
-
|
|
147
|
+
// The node's content is not model-visible, so it carries no character
|
|
148
|
+
// pressure on the decision surface.
|
|
149
|
+
return { seq: node.seq, count: unavailableTokenCount(`surface node ${String(node.seq)} is missing`), characterPressure: 0 }
|
|
124
150
|
}
|
|
125
151
|
const message = deriveEventMessage(event)
|
|
126
152
|
if (message === null) {
|
|
127
|
-
return { seq: node.seq, count: unavailableTokenCount(`surface node ${String(node.seq)} is not model-visible`) }
|
|
153
|
+
return { seq: node.seq, count: unavailableTokenCount(`surface node ${String(node.seq)} is not model-visible`), characterPressure: 0 }
|
|
128
154
|
}
|
|
129
155
|
const count = countCanonicalContent(message.content, counter, `surface node ${String(node.seq)}`)
|
|
130
156
|
const intrinsicImageBlockEstimate = count.kind === 'tokenizer-estimate'
|
|
@@ -133,6 +159,7 @@ export function measureForCompaction(ctx: Context, session: Session): Compaction
|
|
|
133
159
|
return {
|
|
134
160
|
seq: node.seq,
|
|
135
161
|
count,
|
|
162
|
+
characterPressure: nodeCharacterPressure(message.content),
|
|
136
163
|
...intrinsicImageBlockEstimate === undefined ? {} : { intrinsicImageBlockEstimate },
|
|
137
164
|
}
|
|
138
165
|
})
|
|
@@ -140,6 +167,7 @@ export function measureForCompaction(ctx: Context, session: Session): Compaction
|
|
|
140
167
|
measuredNodes.map(node => node.count),
|
|
141
168
|
'current surface',
|
|
142
169
|
)
|
|
170
|
+
const currentSurfaceChars = measuredNodes.reduce((sum, node) => sum + node.characterPressure, 0)
|
|
143
171
|
const intrinsicImageBlockEstimateTokens = measuredNodes.reduce(
|
|
144
172
|
(sum, node) => sum + (node.intrinsicImageBlockEstimate?.paddingMinimumTokens ?? 0),
|
|
145
173
|
0,
|
|
@@ -149,6 +177,7 @@ export function measureForCompaction(ctx: Context, session: Session): Compaction
|
|
|
149
177
|
...(target === undefined ? {} : { providerRoute: target.provider, modelId: target.model }),
|
|
150
178
|
measuredNodes: Object.freeze(measuredNodes),
|
|
151
179
|
currentSurface,
|
|
180
|
+
currentSurfaceChars,
|
|
152
181
|
intrinsicImageBlockEstimateTokens,
|
|
153
182
|
countCanonicalText: counter.countText,
|
|
154
183
|
})
|