dsh-context-compression-improved 0.1.1 → 0.2.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/.gitattributes +1 -0
- package/.github/workflows/ci.yml +39 -0
- package/CHANGELOG.ja.md +39 -0
- package/CHANGELOG.ko.md +39 -0
- package/CHANGELOG.md +135 -0
- package/CHANGELOG.zh.md +39 -0
- package/CONTRIBUTING.md +22 -0
- package/README.ja.md +104 -0
- package/README.ko.md +103 -0
- package/README.md +89 -12
- package/README.zh.md +87 -12
- package/SECURITY.md +18 -0
- package/THIRD_PARTY_NOTICES.md +7 -31
- package/docs/installation.ja.md +76 -0
- package/docs/installation.ko.md +76 -0
- package/docs/installation.md +76 -0
- package/docs/installation.zh.md +76 -0
- package/docs/repair-log.md +582 -0
- package/eslint.config.js +30 -0
- package/package.json +84 -81
- package/packages/selector/LICENSE +21 -0
- package/packages/selector/README.md +26 -0
- package/packages/selector/README.zh.md +26 -0
- package/packages/selector/THIRD_PARTY_NOTICES.md +38 -0
- package/packages/selector/docs/history-tool-call-working-set-spec.md +112 -0
- package/packages/selector/docs/native-tool-result-selector-spec.md +34 -0
- package/packages/selector/docs/subagent-cache-reuse-spec.md +46 -0
- package/packages/selector/lib/style.css +308 -0
- package/packages/selector/package.json +115 -0
- package/{screenshots.json → packages/selector/screenshots.json} +6 -6
- package/packages/selector/src/client/CompressionProfileControls.tsx +229 -0
- package/packages/selector/src/client/CompressionProfileSelector.module.css +170 -0
- package/packages/selector/src/client/CompressionProfileSelector.tsx +79 -0
- package/packages/selector/src/client/CustomPolicyEditor.tsx +216 -0
- package/packages/selector/src/client/EstimatorControls.tsx +281 -0
- package/packages/selector/src/client/decode.ts +49 -0
- package/packages/selector/src/client/index.ts +111 -0
- package/packages/selector/src/client/locales.ts +198 -0
- package/packages/selector/src/client/preset-options.ts +70 -0
- package/packages/selector/src/client/settings-section.tsx +126 -0
- package/packages/selector/src/css-modules.d.ts +6 -0
- package/packages/selector/src/deepseek-v4-tokenizer.ts +210 -0
- package/packages/selector/src/estimator-catalog.ts +104 -0
- package/packages/selector/src/index.ts +327 -0
- package/packages/selector/src/invariant.ts +113 -0
- package/packages/selector/src/preset-overlay.ts +567 -0
- package/packages/selector/src/profiles.ts +342 -0
- package/packages/selector/src/pruner/content.ts +188 -0
- package/packages/selector/src/pruner/session.ts +94 -0
- package/packages/selector/src/pruner/state.ts +43 -0
- package/packages/selector/src/pruner/tuning.ts +23 -0
- package/packages/selector/src/pruner/types.ts +60 -0
- package/packages/selector/src/pruner.ts +2144 -0
- package/packages/selector/src/runtime/adaptive-cost.ts +194 -0
- package/packages/selector/src/runtime/audit.ts +215 -0
- package/packages/selector/src/runtime/config.ts +613 -0
- package/packages/selector/src/runtime/custom-policy.ts +278 -0
- package/packages/selector/src/runtime/deepseek-official-pricing.ts +298 -0
- package/packages/selector/src/runtime/deepseek-v4-vision-tokens.ts +254 -0
- package/packages/selector/src/runtime/measurement.ts +403 -0
- package/packages/selector/src/runtime/reducers.ts +656 -0
- package/packages/selector/src/runtime/retrieve.ts +457 -0
- package/packages/selector/src/runtime/session-events.ts +17 -0
- package/packages/selector/src/runtime/tail-trim.ts +166 -0
- package/packages/selector/src/runtime/token-count.ts +72 -0
- package/packages/selector/src/runtime/tokenpilot/dedup.ts +81 -0
- package/packages/selector/src/runtime/tokenpilot/estimator.ts +183 -0
- package/packages/selector/src/runtime/tokenpilot/locator.ts +128 -0
- package/packages/selector/src/runtime/tokenpilot/read-state.ts +77 -0
- package/packages/selector/src/runtime/types.ts +309 -0
- package/packages/selector/src/runtime/value.ts +48 -0
- package/packages/selector/tests/auto-compact.client.spec.tsx +226 -0
- package/packages/selector/tests/built/client-artifact.spec.ts +51 -0
- package/packages/selector/tests/cache-prefix-audit.spec.ts +123 -0
- package/packages/selector/tests/code-skeleton.client.spec.ts +88 -0
- package/packages/selector/tests/custom-contract.client.spec.ts +202 -0
- package/packages/selector/tests/estimator-catalog.spec.ts +70 -0
- package/packages/selector/tests/estimator-channel.client.spec.tsx +247 -0
- package/packages/selector/tests/estimator-route-registration.host.spec.ts +176 -0
- package/packages/selector/tests/host-preset-overlay.host.spec.ts +204 -0
- package/packages/selector/tests/preset-options-write.client.spec.ts +181 -0
- package/packages/selector/tests/preset-overlay-loader.e2e.host.spec.ts +196 -0
- package/packages/selector/tests/preset-overlay.host.spec.ts +243 -0
- package/packages/selector/tests/profiles.client.spec.tsx +434 -0
- package/packages/selector/tests/public/package-contract.client.spec.ts +33 -0
- package/packages/selector/tests/runtime/adaptive-cost.spec.ts +167 -0
- package/packages/selector/tests/runtime/audit.spec.ts +129 -0
- package/packages/selector/tests/runtime/auto-compact-config.spec.ts +523 -0
- package/packages/selector/tests/runtime/code-skeleton.spec.ts +141 -0
- package/packages/selector/tests/runtime/deepseek-official-pricing.spec.ts +186 -0
- package/packages/selector/tests/runtime/deepseek-v4-tokenizer.spec.ts +122 -0
- package/packages/selector/tests/runtime/deepseek-v4-vision-tokens.spec.ts +122 -0
- package/packages/selector/tests/runtime/fixtures/profile-baseline.json +273 -0
- package/packages/selector/tests/runtime/fixtures/tokenizer-golden.json +106 -0
- package/packages/selector/tests/runtime/fixtures/vision-golden.json +459 -0
- package/packages/selector/tests/runtime/public/public-runtime.spec.ts +2531 -0
- package/packages/selector/tests/runtime/session-events.spec.ts +27 -0
- package/packages/selector/tests/runtime/tokenizer-golden.spec.ts +53 -0
- package/packages/selector/tests/runtime/tokenpilot/dedup.spec.ts +52 -0
- package/packages/selector/tests/runtime/tokenpilot/estimator.spec.ts +56 -0
- package/packages/selector/tests/runtime/tokenpilot/locator.spec.ts +76 -0
- package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +100 -0
- package/packages/selector/tests/runtime/tokenpilot/read-state.spec.ts +58 -0
- package/packages/selector/tests/runtime/value.spec.ts +23 -0
- package/packages/selector/tests/standing-generation.host.spec.ts +631 -0
- package/packages/selector/tests/subagent-cache-reuse.host.spec.ts +250 -0
- package/packages/selector/tests/support/cache-prefix-audit.ts +105 -0
- package/packages/selector/tests/support/mock-adapter.ts +37 -0
- package/packages/selector/tests/support/ui-primitives.tsx +34 -0
- package/packages/selector/tsconfig.json +11 -0
- package/packages/selector/tsdown.client.config.ts +102 -0
- package/packages/selector/tsdown.config.ts +20 -0
- package/pnpm-workspace.yaml +19 -0
- package/scripts/capture-profile-baseline.ts +80 -0
- package/scripts/generate-tokenizer-fixtures.py +81 -0
- package/scripts/generate-vision-fixtures.py +208 -0
- package/scripts/packed-components-smoke.ts +713 -0
- package/scripts/packed-install-e2e.ts +1072 -0
- package/scripts/verify-release.ts +300 -0
- package/tests/TEST_INVENTORY.md +42 -0
- package/tsconfig.base.json +18 -0
- package/tsconfig.json +7 -0
- package/tsconfig.scripts.json +13 -0
- package/tsconfig.tests.json +15 -0
- package/vitest.built.config.ts +9 -0
- package/vitest.config.ts +43 -0
- /package/{assets → packages/selector/assets}/deepseek-v4/LICENSE.DeepSeek-V4-Pro.txt +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4/manifest.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4/tokenizer.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4/tokenizer_config.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/LICENSE.DeepSeek-V4-Flash-Vision-Exp.txt +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/manifest.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/tokenizer.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/tokenizer_config.json +0 -0
- /package/{assets → packages/selector/assets}/screenshots/context-compression-selector-profiles.jpg +0 -0
- /package/{assets → packages/selector/assets}/screenshots/context-compression-selector-settings.png +0 -0
- /package/{cordis.patch.yml → packages/selector/cordis.patch.yml} +0 -0
- /package/{dsh.plugin.json → packages/selector/dsh.plugin.json} +0 -0
- /package/{lib → packages/selector/lib}/client.d.ts +0 -0
- /package/{lib → packages/selector/lib}/client.js +0 -0
- /package/{lib → packages/selector/lib}/config.js +0 -0
- /package/{lib → packages/selector/lib}/index.d.ts +0 -0
- /package/{lib → packages/selector/lib}/index.js +0 -0
- /package/{lib → packages/selector/lib}/invariant.d.ts +0 -0
- /package/{lib → packages/selector/lib}/invariant.js +0 -0
- /package/{lib → packages/selector/lib}/pruner.d.ts +0 -0
- /package/{lib → packages/selector/lib}/pruner.js +0 -0
- /package/{lib → packages/selector/lib}/tail-trim.js +0 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/** Public context-compression choices shared by the Host schema and browser selector. */
|
|
2
|
+
export const COMPRESSION_PROFILES = [
|
|
3
|
+
'off',
|
|
4
|
+
'native',
|
|
5
|
+
'balanced',
|
|
6
|
+
'cache-strict',
|
|
7
|
+
'savings',
|
|
8
|
+
'adaptive',
|
|
9
|
+
'tokenpilot-inspired',
|
|
10
|
+
'custom',
|
|
11
|
+
] as const
|
|
12
|
+
|
|
13
|
+
/** One supported context-compression profile. */
|
|
14
|
+
export type CompressionProfile = typeof COMPRESSION_PROFILES[number]
|
|
15
|
+
|
|
16
|
+
/** Single canonical unit stored by a version-1 browser Custom document. */
|
|
17
|
+
export type CustomCompressionUnit = 'tokens' | 'context-percent'
|
|
18
|
+
|
|
19
|
+
/** Whether Custom History may routinely rewrite an already-sent prefix. */
|
|
20
|
+
export type CustomPrefixPolicy = 'preserve' | 'pressure-break'
|
|
21
|
+
|
|
22
|
+
/** Browser representation of one independently enabled Fresh or Aggregate budget. */
|
|
23
|
+
export interface CustomCompressionBudget {
|
|
24
|
+
enabled: boolean
|
|
25
|
+
trigger: number
|
|
26
|
+
target: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Legacy browser representation of the Custom History working set. */
|
|
30
|
+
export interface LegacyCustomHistoryPolicy {
|
|
31
|
+
enabled: boolean
|
|
32
|
+
trigger: number
|
|
33
|
+
keepRecentTurns: number
|
|
34
|
+
keepRecent: number
|
|
35
|
+
minReclaim: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Browser representation of Custom History tool-call and token-tail protection. */
|
|
39
|
+
export interface CustomHistoryPolicy {
|
|
40
|
+
enabled: boolean
|
|
41
|
+
trigger: number
|
|
42
|
+
keepRecentToolCalls: number
|
|
43
|
+
keepRecentTokens: number
|
|
44
|
+
minReclaim: number
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Browser representation of the Custom-only Experimental TailTrim gate. */
|
|
48
|
+
export interface CustomTailTrimPolicy {
|
|
49
|
+
enabled: boolean
|
|
50
|
+
trigger: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface CustomCompressionPolicyCommon<HistoryPolicy> {
|
|
54
|
+
unit: CustomCompressionUnit
|
|
55
|
+
fresh: CustomCompressionBudget
|
|
56
|
+
aggregate: CustomCompressionBudget
|
|
57
|
+
history: HistoryPolicy
|
|
58
|
+
prefixPolicy: CustomPrefixPolicy
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Legacy public Custom document; accepted and normalized before editing. */
|
|
62
|
+
export interface CustomCompressionPolicyV1 extends CustomCompressionPolicyCommon<LegacyCustomHistoryPolicy> {
|
|
63
|
+
version: 1
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Legacy Custom document with a default-disabled TailTrim stage. */
|
|
67
|
+
export interface CustomCompressionPolicyV2 extends CustomCompressionPolicyCommon<LegacyCustomHistoryPolicy> {
|
|
68
|
+
version: 2
|
|
69
|
+
tailTrim: CustomTailTrimPolicy
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Public Custom document with tool-call working-set protection. */
|
|
73
|
+
export interface CustomCompressionPolicyV3 extends CustomCompressionPolicyCommon<CustomHistoryPolicy> {
|
|
74
|
+
version: 3
|
|
75
|
+
tailTrim: CustomTailTrimPolicy
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Exact public Custom document accepted by the Host and browser boundary. */
|
|
79
|
+
export type CustomCompressionPolicy = CustomCompressionPolicyV1 | CustomCompressionPolicyV2 | CustomCompressionPolicyV3
|
|
80
|
+
|
|
81
|
+
/** Browser-safe mirror of the Host's Balanced-equivalent Custom default. */
|
|
82
|
+
export const DEFAULT_CUSTOM_COMPRESSION_POLICY: CustomCompressionPolicyV3 = {
|
|
83
|
+
version: 3,
|
|
84
|
+
unit: 'tokens',
|
|
85
|
+
fresh: { enabled: true, trigger: 8_192, target: 3_072 },
|
|
86
|
+
aggregate: { enabled: true, trigger: 32_768, target: 12_288 },
|
|
87
|
+
history: {
|
|
88
|
+
enabled: true,
|
|
89
|
+
trigger: 500_000,
|
|
90
|
+
keepRecentToolCalls: 10,
|
|
91
|
+
keepRecentTokens: 64_000,
|
|
92
|
+
minReclaim: 96_000,
|
|
93
|
+
},
|
|
94
|
+
prefixPolicy: 'pressure-break',
|
|
95
|
+
tailTrim: { enabled: false, trigger: 700_000 },
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** User-tunable Auto Compact coordination preferences. */
|
|
99
|
+
export interface AutoCompactSettings {
|
|
100
|
+
thresholdPercent: number
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Orthogonal code-skeleton reducer gate, mirrored browser-safe from the
|
|
105
|
+
* runtime: independent of every profile, default off.
|
|
106
|
+
*/
|
|
107
|
+
export interface CodeSkeletonSettings {
|
|
108
|
+
enabled: boolean
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Decode the persisted codeSkeleton section with exactly the runtime schema's
|
|
113
|
+
* strictness: absent means the lossless off default; present values must be a
|
|
114
|
+
* plain object carrying only a boolean `enabled`. Anything else is invalid,
|
|
115
|
+
* never silently coerced.
|
|
116
|
+
*/
|
|
117
|
+
export function decodeCodeSkeletonSettings(value: unknown): CodeSkeletonSettings | undefined {
|
|
118
|
+
if (value === undefined) return { enabled: false }
|
|
119
|
+
if (!isPlainRecord(value)) return undefined
|
|
120
|
+
const keys = Object.keys(value)
|
|
121
|
+
if (keys.length !== 1 || keys[0] !== 'enabled') return undefined
|
|
122
|
+
const enabled = (value as Record<string, unknown>).enabled
|
|
123
|
+
return typeof enabled === 'boolean' ? { enabled } : undefined
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Browser-safe mirror of the runtime presetOptions section. */
|
|
127
|
+
export interface PresetOptionsSettings {
|
|
128
|
+
readonly dedupeToolResults?: boolean
|
|
129
|
+
readonly summaryLocator?: boolean
|
|
130
|
+
readonly prefixStabilizer?: boolean
|
|
131
|
+
readonly readState?: boolean
|
|
132
|
+
readonly estimatorMode?: '' | 'host' | 'direct'
|
|
133
|
+
readonly estimatorProvider?: string
|
|
134
|
+
readonly estimatorModel?: string
|
|
135
|
+
readonly estimatorBaseUrl?: string
|
|
136
|
+
readonly estimatorApiKey?: string
|
|
137
|
+
readonly estimatorTimeoutMs?: number
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Browser mirror of the runtime presetOptions section: absent inherits the
|
|
142
|
+
* preset defaults (decodes to `undefined`); present values must be a plain
|
|
143
|
+
* object carrying only the known keys with valid types.
|
|
144
|
+
*/
|
|
145
|
+
export function decodePresetOptionsSettings(value: unknown): PresetOptionsSettings | undefined {
|
|
146
|
+
if (value === undefined) return undefined
|
|
147
|
+
if (!isPlainRecord(value)) return undefined
|
|
148
|
+
const allowed = new Set([
|
|
149
|
+
'dedupeToolResults', 'summaryLocator', 'prefixStabilizer', 'readState', 'estimatorMode',
|
|
150
|
+
'estimatorProvider', 'estimatorModel', 'estimatorBaseUrl', 'estimatorApiKey', 'estimatorTimeoutMs',
|
|
151
|
+
])
|
|
152
|
+
if (Object.keys(value).some(key => !allowed.has(key))) return undefined
|
|
153
|
+
for (const key of ['dedupeToolResults', 'summaryLocator', 'prefixStabilizer', 'readState'] as const) {
|
|
154
|
+
const entry = value[key]
|
|
155
|
+
if (entry !== undefined && typeof entry !== 'boolean') return undefined
|
|
156
|
+
}
|
|
157
|
+
const estimatorMode = value.estimatorMode
|
|
158
|
+
if (estimatorMode !== undefined && estimatorMode !== '' && estimatorMode !== 'host' && estimatorMode !== 'direct') {
|
|
159
|
+
return undefined
|
|
160
|
+
}
|
|
161
|
+
for (const key of ['estimatorProvider', 'estimatorModel', 'estimatorBaseUrl', 'estimatorApiKey'] as const) {
|
|
162
|
+
const entry = value[key]
|
|
163
|
+
if (entry !== undefined && typeof entry !== 'string') return undefined
|
|
164
|
+
}
|
|
165
|
+
const estimatorTimeoutMs = value.estimatorTimeoutMs
|
|
166
|
+
if (estimatorTimeoutMs !== undefined
|
|
167
|
+
&& (typeof estimatorTimeoutMs !== 'number' || !Number.isSafeInteger(estimatorTimeoutMs)
|
|
168
|
+
|| estimatorTimeoutMs < 100 || estimatorTimeoutMs > 60_000)) {
|
|
169
|
+
return undefined
|
|
170
|
+
}
|
|
171
|
+
const decoded: {
|
|
172
|
+
-readonly [K in keyof PresetOptionsSettings]: PresetOptionsSettings[K]
|
|
173
|
+
} = {}
|
|
174
|
+
if (value.dedupeToolResults !== undefined) decoded.dedupeToolResults = value.dedupeToolResults as boolean
|
|
175
|
+
if (value.summaryLocator !== undefined) decoded.summaryLocator = value.summaryLocator as boolean
|
|
176
|
+
if (value.prefixStabilizer !== undefined) decoded.prefixStabilizer = value.prefixStabilizer as boolean
|
|
177
|
+
if (value.readState !== undefined) decoded.readState = value.readState as boolean
|
|
178
|
+
if (estimatorMode !== undefined) decoded.estimatorMode = estimatorMode as '' | 'host' | 'direct'
|
|
179
|
+
if (value.estimatorProvider !== undefined) decoded.estimatorProvider = value.estimatorProvider as string
|
|
180
|
+
if (value.estimatorModel !== undefined) decoded.estimatorModel = value.estimatorModel as string
|
|
181
|
+
if (value.estimatorBaseUrl !== undefined) decoded.estimatorBaseUrl = value.estimatorBaseUrl as string
|
|
182
|
+
if (value.estimatorApiKey !== undefined) decoded.estimatorApiKey = value.estimatorApiKey as string
|
|
183
|
+
if (estimatorTimeoutMs !== undefined) decoded.estimatorTimeoutMs = estimatorTimeoutMs as number
|
|
184
|
+
return decoded
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The one threshold contract shared by the UI, the persisted settings, and the
|
|
189
|
+
* runtime resolver; mirrored browser-safe from the runtime package.
|
|
190
|
+
*/
|
|
191
|
+
export const AUTO_COMPACT_THRESHOLD_LIMITS = Object.freeze({
|
|
192
|
+
min: 50,
|
|
193
|
+
max: 90,
|
|
194
|
+
step: 1,
|
|
195
|
+
default: 80,
|
|
196
|
+
} as const)
|
|
197
|
+
|
|
198
|
+
/** Narrow one unknown value to a valid Auto Compact threshold percent. */
|
|
199
|
+
export function isValidAutoCompactThresholdPercent(value: unknown): value is number {
|
|
200
|
+
return typeof value === 'number'
|
|
201
|
+
&& Number.isSafeInteger(value)
|
|
202
|
+
&& value >= AUTO_COMPACT_THRESHOLD_LIMITS.min
|
|
203
|
+
&& value <= AUTO_COMPACT_THRESHOLD_LIMITS.max
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Accept JSON-object records while rejecting class instances and exotic prototypes. */
|
|
207
|
+
export function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
208
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return false
|
|
209
|
+
const prototype = Object.getPrototypeOf(value) as object | null
|
|
210
|
+
return prototype === Object.prototype || prototype === null
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Decode the persisted autoCompact section with exactly the runtime schema's
|
|
215
|
+
* strictness: absent means the 80% default; present values must be a plain
|
|
216
|
+
* object carrying only a valid `thresholdPercent`. Anything else is invalid,
|
|
217
|
+
* never silently coerced.
|
|
218
|
+
*/
|
|
219
|
+
export function decodeAutoCompactSettings(value: unknown): AutoCompactSettings | undefined {
|
|
220
|
+
if (value === undefined) return { thresholdPercent: AUTO_COMPACT_THRESHOLD_LIMITS.default }
|
|
221
|
+
if (!isPlainRecord(value)) return undefined
|
|
222
|
+
const keys = Object.keys(value)
|
|
223
|
+
if (keys.length !== 1 || keys[0] !== 'thresholdPercent') return undefined
|
|
224
|
+
const thresholdPercent = (value as Record<string, unknown>).thresholdPercent
|
|
225
|
+
return isValidAutoCompactThresholdPercent(thresholdPercent)
|
|
226
|
+
? { thresholdPercent }
|
|
227
|
+
: undefined
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Durable settings section owned by this package. */
|
|
231
|
+
export interface ContextCompressionSettings {
|
|
232
|
+
/** Default profile captured when each Session first reaches the pruner. */
|
|
233
|
+
profile: CompressionProfile
|
|
234
|
+
/** Complete canonical policy captured with `profile` when the runtime first observes a Session. */
|
|
235
|
+
custom: CustomCompressionPolicy
|
|
236
|
+
/** Auto Compact trigger captured with `profile` when the runtime first observes a Session. */
|
|
237
|
+
autoCompact: AutoCompactSettings
|
|
238
|
+
/** Code-skeleton reducer gate captured independently of `profile`. */
|
|
239
|
+
codeSkeleton: CodeSkeletonSettings
|
|
240
|
+
/** Optional tokenpilot-inspired sub-capability overrides (presence-validated only). */
|
|
241
|
+
presetOptions?: PresetOptionsSettings
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Narrow an unknown settings value to a complete supported Custom policy.
|
|
246
|
+
* @param value - Candidate settings value received from the Host or edited locally.
|
|
247
|
+
* @returns Whether the value is a relation-valid Custom policy.
|
|
248
|
+
*/
|
|
249
|
+
export function isCustomCompressionPolicy(value: unknown): value is CustomCompressionPolicy {
|
|
250
|
+
if (!hasExactKeys(
|
|
251
|
+
value,
|
|
252
|
+
value !== null && typeof value === 'object' && 'version' in value && value.version === 1
|
|
253
|
+
? ['version', 'unit', 'fresh', 'aggregate', 'history', 'prefixPolicy']
|
|
254
|
+
: ['version', 'unit', 'fresh', 'aggregate', 'history', 'prefixPolicy', 'tailTrim'],
|
|
255
|
+
)) return false
|
|
256
|
+
if ((value.version !== 1 && value.version !== 2 && value.version !== 3)
|
|
257
|
+
|| (value.unit !== 'tokens' && value.unit !== 'context-percent')) return false
|
|
258
|
+
if (value.prefixPolicy !== 'preserve' && value.prefixPolicy !== 'pressure-break') return false
|
|
259
|
+
if (!isBudget(value.fresh) || !isBudget(value.aggregate)) return false
|
|
260
|
+
const modernHistory = value.version === 3
|
|
261
|
+
if (!hasExactKeys(
|
|
262
|
+
value.history,
|
|
263
|
+
modernHistory
|
|
264
|
+
? ['enabled', 'trigger', 'keepRecentToolCalls', 'keepRecentTokens', 'minReclaim']
|
|
265
|
+
: ['enabled', 'trigger', 'keepRecentTurns', 'keepRecent', 'minReclaim'],
|
|
266
|
+
)) return false
|
|
267
|
+
if (typeof value.history.enabled !== 'boolean'
|
|
268
|
+
|| typeof value.history.trigger !== 'number'
|
|
269
|
+
|| typeof value.history.minReclaim !== 'number') return false
|
|
270
|
+
const recent = modernHistory
|
|
271
|
+
? value.history.keepRecentTokens
|
|
272
|
+
: value.history.keepRecent
|
|
273
|
+
const calls = modernHistory ? value.history.keepRecentToolCalls : value.history.keepRecentTurns
|
|
274
|
+
if (typeof recent !== 'number'
|
|
275
|
+
|| typeof calls !== 'number'
|
|
276
|
+
|| !Number.isSafeInteger(calls)
|
|
277
|
+
|| calls < 0) return false
|
|
278
|
+
let tailTrimTrigger: number | undefined
|
|
279
|
+
if (value.version !== 1) {
|
|
280
|
+
const tailTrim = value.tailTrim
|
|
281
|
+
if (!hasExactKeys(tailTrim, ['enabled', 'trigger'])
|
|
282
|
+
|| typeof tailTrim.enabled !== 'boolean'
|
|
283
|
+
|| typeof tailTrim.trigger !== 'number') return false
|
|
284
|
+
tailTrimTrigger = tailTrim.trigger
|
|
285
|
+
}
|
|
286
|
+
const measured = [
|
|
287
|
+
value.fresh.trigger, value.fresh.target,
|
|
288
|
+
value.aggregate.trigger, value.aggregate.target,
|
|
289
|
+
value.history.trigger, recent, value.history.minReclaim,
|
|
290
|
+
...tailTrimTrigger === undefined ? [] : [tailTrimTrigger],
|
|
291
|
+
]
|
|
292
|
+
if (!measured.every(entry => typeof entry === 'number' && Number.isFinite(entry))) return false
|
|
293
|
+
if (value.fresh.trigger <= 0 || value.fresh.target <= 0
|
|
294
|
+
|| value.aggregate.trigger <= 0 || value.aggregate.target <= 0
|
|
295
|
+
|| value.history.trigger <= 0 || recent < 0
|
|
296
|
+
|| value.history.minReclaim <= 0
|
|
297
|
+
|| (tailTrimTrigger !== undefined && tailTrimTrigger <= 0)) return false
|
|
298
|
+
if (value.unit === 'tokens' && !measured.every(Number.isSafeInteger)) return false
|
|
299
|
+
if (value.unit === 'context-percent' && !measured.every(entry => entry <= 100)) return false
|
|
300
|
+
return value.fresh.target < value.fresh.trigger
|
|
301
|
+
&& value.aggregate.target < value.aggregate.trigger
|
|
302
|
+
&& value.history.minReclaim <= value.history.trigger
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function isBudget(value: unknown): value is CustomCompressionBudget {
|
|
306
|
+
return hasExactKeys(value, ['enabled', 'trigger', 'target'])
|
|
307
|
+
&& typeof value.enabled === 'boolean'
|
|
308
|
+
&& typeof value.trigger === 'number'
|
|
309
|
+
&& typeof value.target === 'number'
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Canonicalize one validated Custom document to version 3, mirroring the
|
|
314
|
+
* runtime's `canonicalizeCustomPolicy` exactly so the browser and runtime
|
|
315
|
+
* boundaries hand the SAME complete document to the UI and the policy
|
|
316
|
+
* resolver: legacy v1/v2 History working sets upgrade to the 10-call default,
|
|
317
|
+
* and a v1 document gains the default-disabled TailTrim stage.
|
|
318
|
+
*/
|
|
319
|
+
export function canonicalizeCustomPolicy(policy: CustomCompressionPolicy): CustomCompressionPolicyV3 {
|
|
320
|
+
if (policy.version === 3) return structuredClone(policy)
|
|
321
|
+
return {
|
|
322
|
+
version: 3,
|
|
323
|
+
unit: policy.unit,
|
|
324
|
+
fresh: structuredClone(policy.fresh),
|
|
325
|
+
aggregate: structuredClone(policy.aggregate),
|
|
326
|
+
history: {
|
|
327
|
+
enabled: policy.history.enabled,
|
|
328
|
+
trigger: policy.history.trigger,
|
|
329
|
+
keepRecentToolCalls: 10,
|
|
330
|
+
keepRecentTokens: policy.history.keepRecent,
|
|
331
|
+
minReclaim: policy.history.minReclaim,
|
|
332
|
+
},
|
|
333
|
+
prefixPolicy: policy.prefixPolicy,
|
|
334
|
+
tailTrim: policy.version === 1 ? { enabled: false, trigger: 700_000 } : structuredClone(policy.tailTrim),
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function hasExactKeys(value: unknown, expected: readonly string[]): value is Record<string, unknown> {
|
|
339
|
+
if (!isPlainRecord(value)) return false
|
|
340
|
+
const keys = Object.keys(value)
|
|
341
|
+
return keys.length === expected.length && keys.every(key => expected.includes(key))
|
|
342
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure content-analysis functions extracted from ToolResultPruner.
|
|
3
|
+
*
|
|
4
|
+
* Every function here has ZERO `this` dependency — they receive all inputs
|
|
5
|
+
* as explicit arguments. Grouped by natural cohesion (content blocks →
|
|
6
|
+
* token counting → recovery helpers).
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-context-compression-improved/pruner/content
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
|
12
|
+
import type {
|
|
13
|
+
CompactionTokenView,
|
|
14
|
+
ProviderMeasurementKey,
|
|
15
|
+
TokenCount,
|
|
16
|
+
} from '../runtime/measurement.ts'
|
|
17
|
+
import { countExactCanonicalTextFields } from '../runtime/token-count.ts'
|
|
18
|
+
import { codePointLength, PRUNE_MARKER } from '../runtime/config.ts'
|
|
19
|
+
import type { PrunedEntry, PruneResult } from '../runtime/types.ts'
|
|
20
|
+
import { RICH_BLOCK_PRESSURE_COST } from './tuning.ts'
|
|
21
|
+
|
|
22
|
+
// ── Content-block predicates ────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
function onlyTextBlock(blocks: readonly ContentBlock[]): Extract<ContentBlock, { type: 'text' }> | null {
|
|
25
|
+
return blocks.length === 1 && blocks[0]?.type === 'text' ? blocks[0] : null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function onlyTextBlocks(blocks: readonly ContentBlock[]): readonly Extract<ContentBlock, { type: 'text' }>[] | null {
|
|
29
|
+
return blocks.every((block): block is Extract<ContentBlock, { type: 'text' }> => block.type === 'text')
|
|
30
|
+
? blocks
|
|
31
|
+
: null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Token counting ──────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
function countToolContent(blocks: readonly ContentBlock[], view: CompactionTokenView): TokenCount {
|
|
37
|
+
const text = onlyTextBlocks(blocks)
|
|
38
|
+
if (text === null) return unavailableCount('tool result contains unsupported rich content')
|
|
39
|
+
return countExactCanonicalTextFields(
|
|
40
|
+
text.map(block => block.text),
|
|
41
|
+
candidate => view.countCanonicalText(candidate),
|
|
42
|
+
'tool result replacement',
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function exactTokens(count: TokenCount): number | undefined {
|
|
47
|
+
return count.kind === 'exact-tokenizer' ? count.tokens : undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Provider measurement key comparison ─────────────────────────────────
|
|
51
|
+
|
|
52
|
+
function sameProviderMeasurementKey(
|
|
53
|
+
left: Readonly<ProviderMeasurementKey>,
|
|
54
|
+
right: Readonly<ProviderMeasurementKey>,
|
|
55
|
+
): boolean {
|
|
56
|
+
return left.provider === right.provider
|
|
57
|
+
&& left.baseUrlClass === right.baseUrlClass
|
|
58
|
+
&& left.apiRoute === right.apiRoute
|
|
59
|
+
&& left.modelId === right.modelId
|
|
60
|
+
&& left.requestTemplateRevision === right.requestTemplateRevision
|
|
61
|
+
&& left.tokenizerRevision === right.tokenizerRevision
|
|
62
|
+
&& left.modality === right.modality
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Token helpers ───────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
function unavailableCount(reason: string): TokenCount {
|
|
68
|
+
return Object.freeze({ kind: 'unavailable', reason })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Recovery markers ────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
function recoveryMarker(sourceRef: string, label: string): string {
|
|
74
|
+
return `\n\n[... ${label}; source=${sourceRef}; use context_compression_retrieve if needed ...]\n\n`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Content measurement ─────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Measure text content in Unicode code points; non-text blocks cost zero.
|
|
81
|
+
* @param blocks - tool-result content to measure.
|
|
82
|
+
* @returns total Unicode code points across text blocks.
|
|
83
|
+
*/
|
|
84
|
+
function measureContent(blocks: readonly ContentBlock[]): number {
|
|
85
|
+
let chars = 0
|
|
86
|
+
for (const block of blocks) {
|
|
87
|
+
if (block.type === 'text') chars += codePointLength(block.text)
|
|
88
|
+
}
|
|
89
|
+
return chars
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function pressureCost(blocks: readonly ContentBlock[]): number {
|
|
93
|
+
let cost = 0
|
|
94
|
+
for (const block of blocks) {
|
|
95
|
+
switch (block.type) {
|
|
96
|
+
case 'text':
|
|
97
|
+
case 'reasoning':
|
|
98
|
+
cost += codePointLength(block.text)
|
|
99
|
+
break
|
|
100
|
+
case 'tool-call':
|
|
101
|
+
cost += RICH_BLOCK_PRESSURE_COST
|
|
102
|
+
+ codePointLength(block.name)
|
|
103
|
+
+ codePointLength(block.arguments)
|
|
104
|
+
break
|
|
105
|
+
case 'tool-result':
|
|
106
|
+
cost += RICH_BLOCK_PRESSURE_COST + pressureCost(block.content)
|
|
107
|
+
break
|
|
108
|
+
default: {
|
|
109
|
+
// ContentBlockMap is merge-extensible. Unknown model-visible blocks
|
|
110
|
+
// scale with their durable JSON payload instead of receiving a fixed
|
|
111
|
+
// token that a large provider block could bypass.
|
|
112
|
+
const serialized = JSON.stringify(block)
|
|
113
|
+
cost += Math.max(RICH_BLOCK_PRESSURE_COST, codePointLength(serialized))
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return cost
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Native content pruning ──────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
function nativePruneContent(
|
|
123
|
+
blocks: readonly ContentBlock[],
|
|
124
|
+
thresholdChars: number,
|
|
125
|
+
headChars: number,
|
|
126
|
+
tailChars: number,
|
|
127
|
+
marker: string = PRUNE_MARKER,
|
|
128
|
+
): ContentBlock[] | null {
|
|
129
|
+
const totalChars = measureContent(blocks)
|
|
130
|
+
if (totalChars <= thresholdChars) return null
|
|
131
|
+
const markerChars = codePointLength(marker)
|
|
132
|
+
const safeHead = Math.max(0, Math.min(headChars, thresholdChars - markerChars))
|
|
133
|
+
const safeTail = Math.max(0, Math.min(tailChars, thresholdChars - markerChars - safeHead))
|
|
134
|
+
const removedStart = safeHead
|
|
135
|
+
const removedEnd = totalChars - safeTail
|
|
136
|
+
const pruned: ContentBlock[] = []
|
|
137
|
+
let consumed = 0
|
|
138
|
+
let markerInserted = false
|
|
139
|
+
for (const block of blocks) {
|
|
140
|
+
if (block.type !== 'text') {
|
|
141
|
+
pruned.push(block)
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
const points = Array.from(block.text)
|
|
145
|
+
const blockStart = consumed
|
|
146
|
+
const blockEnd = blockStart + points.length
|
|
147
|
+
const headEnd = Math.min(points.length, Math.max(0, removedStart - blockStart))
|
|
148
|
+
const tailStart = Math.min(points.length, Math.max(0, removedEnd - blockStart))
|
|
149
|
+
const intersectsRemoved = blockStart < removedEnd && blockEnd > removedStart
|
|
150
|
+
const insertion = intersectsRemoved && !markerInserted ? marker : ''
|
|
151
|
+
if (insertion !== '') markerInserted = true
|
|
152
|
+
const text = points.slice(0, headEnd).join('') + insertion + points.slice(tailStart).join('')
|
|
153
|
+
if (text !== '') pruned.push({ ...block, text })
|
|
154
|
+
consumed = blockEnd
|
|
155
|
+
}
|
|
156
|
+
if (!markerInserted) return null
|
|
157
|
+
const charsAfter = measureContent(pruned)
|
|
158
|
+
return charsAfter <= thresholdChars && charsAfter < totalChars ? pruned : null
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Result aggregation ──────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
function summarize(entries: readonly PrunedEntry[]): PruneResult {
|
|
164
|
+
return {
|
|
165
|
+
pruned: entries,
|
|
166
|
+
charsRemoved: entries.reduce((sum, entry) => sum + entry.charsBefore - entry.charsAfter, 0),
|
|
167
|
+
tokensRemoved: entries.reduce((sum, entry) => sum + entry.tokensBefore - entry.tokensAfter, 0),
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function emptyResult(): PruneResult {
|
|
172
|
+
return { pruned: [], charsRemoved: 0, tokensRemoved: 0 }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export {
|
|
176
|
+
onlyTextBlock,
|
|
177
|
+
onlyTextBlocks,
|
|
178
|
+
countToolContent,
|
|
179
|
+
exactTokens,
|
|
180
|
+
sameProviderMeasurementKey,
|
|
181
|
+
unavailableCount,
|
|
182
|
+
recoveryMarker,
|
|
183
|
+
summarize,
|
|
184
|
+
emptyResult,
|
|
185
|
+
measureContent,
|
|
186
|
+
pressureCost,
|
|
187
|
+
nativePruneContent,
|
|
188
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure session-scanning functions extracted from ToolResultPruner.
|
|
3
|
+
*
|
|
4
|
+
* Every function here has ZERO `this` dependency — they receive all inputs
|
|
5
|
+
* as explicit arguments and only read session events.
|
|
6
|
+
*
|
|
7
|
+
* @module dsh-context-compression-improved/pruner/session
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Session } from '@deepseek-ai/dsh-session'
|
|
11
|
+
import { sessionEvents } from '../runtime/session-events.ts'
|
|
12
|
+
import { deepSeekV4TokenizerForModel } from '../deepseek-v4-tokenizer.ts'
|
|
13
|
+
import type { SnapshotCandidate, PlannedReplacement, HistoryPlanOutcome } from './types.ts'
|
|
14
|
+
|
|
15
|
+
/** Check whether the session currently has an open (unterminated) turn. */
|
|
16
|
+
function hasOpenTurn(session: Session): boolean {
|
|
17
|
+
let open = false
|
|
18
|
+
for (const event of sessionEvents(session)) {
|
|
19
|
+
if (event.type === 'turn/start') open = true
|
|
20
|
+
else if (event.type === 'turn/end') open = false
|
|
21
|
+
}
|
|
22
|
+
return open
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Walk the tool-result source chain to find the root result seq. */
|
|
26
|
+
function rootToolResultSeq(session: Session, seq: number): number {
|
|
27
|
+
const events = sessionEvents(session)
|
|
28
|
+
let current = seq
|
|
29
|
+
const seen = new Set<number>()
|
|
30
|
+
while (!seen.has(current)) {
|
|
31
|
+
seen.add(current)
|
|
32
|
+
const event = events[current]
|
|
33
|
+
if (event?.type !== 'tool/result' || typeof event.surfaceOp !== 'object') return current
|
|
34
|
+
const previous = event.sourceEventSeqs?.[0]
|
|
35
|
+
if (previous === undefined) return current
|
|
36
|
+
current = previous
|
|
37
|
+
}
|
|
38
|
+
return seq
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Build a session:// event reference string for a given seq. */
|
|
42
|
+
function sourceRef(session: Session, seq: number): string {
|
|
43
|
+
return `session://${session.id}/event/${String(seq)}`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Find the latest completed step number for a given turn. */
|
|
47
|
+
function latestCompletedToolStep(session: Session, turn: number): number | undefined {
|
|
48
|
+
let latest: number | undefined
|
|
49
|
+
for (const event of sessionEvents(session)) {
|
|
50
|
+
if (event.type === 'step/end' && event.data.turn === turn) latest = event.data.step
|
|
51
|
+
}
|
|
52
|
+
return latest
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Routed provider/model when the durable request header names one route. */
|
|
56
|
+
function routeAuditFact(session: Session): { provider: string, model: string } | undefined {
|
|
57
|
+
const header = session.requestHeader()?.config
|
|
58
|
+
if (header === undefined || header.provider.length === 0 || header.model.length === 0) return undefined
|
|
59
|
+
return { provider: header.provider, model: header.model }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Bundled tokenizer identity for one route, when the route is eligible. */
|
|
63
|
+
function tokenizerAuditFact(route: { provider: string, model: string }): { tokenizer: { repository: string, revision: string } } {
|
|
64
|
+
// Reuse the measurement eligibility boundary: a DeepSeek model id routed
|
|
65
|
+
// through another provider never used the bundled tokenizer.
|
|
66
|
+
const eligible = route.provider === 'deepseek' || route.provider === 'deepseek-official'
|
|
67
|
+
const identity = eligible ? deepSeekV4TokenizerForModel(route.model)?.countText('') : undefined
|
|
68
|
+
if (identity?.kind === 'exact-tokenizer') {
|
|
69
|
+
return { tokenizer: { repository: identity.tokenizerId, revision: identity.tokenizerRevision } }
|
|
70
|
+
}
|
|
71
|
+
return { tokenizer: { repository: 'unavailable', revision: 'unavailable' } }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Check whether a snapshot candidate represents an error result. */
|
|
75
|
+
function isError(candidate: SnapshotCandidate): boolean {
|
|
76
|
+
const result = candidate.event.data.message.content[0]
|
|
77
|
+
return result.isError === true || candidate.event.data.error !== undefined
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Wrap a plan list into a HistoryPlanOutcome. */
|
|
81
|
+
function historyOutcome(plans: readonly PlannedReplacement[]): HistoryPlanOutcome {
|
|
82
|
+
return { kind: 'planned', plans: [...plans] }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export {
|
|
86
|
+
hasOpenTurn,
|
|
87
|
+
rootToolResultSeq,
|
|
88
|
+
sourceRef,
|
|
89
|
+
latestCompletedToolStep,
|
|
90
|
+
routeAuditFact,
|
|
91
|
+
tokenizerAuditFact,
|
|
92
|
+
isError,
|
|
93
|
+
historyOutcome,
|
|
94
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consolidated per-session mutable state for {@link ToolResultPruner}.
|
|
3
|
+
*
|
|
4
|
+
* Every field is a WeakMap keyed by Session, keeping the service
|
|
5
|
+
* stateless across sessions and safe under GC.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Session } from '@deepseek-ai/dsh-session'
|
|
9
|
+
import type { DedupeTable } from '../runtime/tokenpilot/dedup.ts'
|
|
10
|
+
import type { EstimatorFailures } from '../runtime/tokenpilot/estimator.ts'
|
|
11
|
+
import type {
|
|
12
|
+
ContextCompressionSettings,
|
|
13
|
+
ResolvedConfig,
|
|
14
|
+
} from '../runtime/types.ts'
|
|
15
|
+
|
|
16
|
+
/** Mutable per-session state bag used inside {@link ToolResultPruner}. */
|
|
17
|
+
export interface PrunerState {
|
|
18
|
+
/** Resolved immutable deployment configuration. */
|
|
19
|
+
readonly config: ResolvedConfig
|
|
20
|
+
|
|
21
|
+
/** Complete canonical setting document frozen when each Session first reaches this root service. */
|
|
22
|
+
readonly sessionSettings: WeakMap<Session, ContextCompressionSettings>
|
|
23
|
+
/** Original result seqs whose first-exposure KEEP/REDUCE decision has committed. */
|
|
24
|
+
readonly firstExposure: WeakMap<Session, Set<number>>
|
|
25
|
+
/** Result seqs permanently exempt from further reduction (recovery outputs and registered equivalents). */
|
|
26
|
+
readonly recoveryExemptions: WeakMap<Session, Set<number>>
|
|
27
|
+
/** Per-session canonical-content hash index backing tokenpilot-inspired dedupe. */
|
|
28
|
+
readonly dedupeTables: WeakMap<Session, DedupeTable>
|
|
29
|
+
/** Advisory estimator verdicts consumed by the read-state classification. */
|
|
30
|
+
readonly estimatorVerdicts: WeakMap<Session, Map<number, boolean>>
|
|
31
|
+
/** Per-session estimator failure backoff state. */
|
|
32
|
+
readonly estimatorFailures: WeakMap<Session, EstimatorFailures>
|
|
33
|
+
/** Runtime prerequisite warnings deduplicated per Session and failure key. */
|
|
34
|
+
readonly warnedFailures: WeakMap<Session, Set<string>>
|
|
35
|
+
/** Last Adaptive postflight attempt emitted per Session; keeps diagnostics bounded and independent. */
|
|
36
|
+
readonly postflightDiagnostics: WeakMap<Session, string>
|
|
37
|
+
/** Current pre-step chain identity, shared by this producer and downstream compaction-basic. */
|
|
38
|
+
readonly activeRequestBoundaries: WeakMap<Session, object>
|
|
39
|
+
/** Boundary identity that already attempted one fully preflighted TailTrim publication. */
|
|
40
|
+
readonly tailTrimBoundaryAttempts: WeakMap<Session, object>
|
|
41
|
+
/** Last effective policy audit key emitted for each Session. */
|
|
42
|
+
readonly policyResolutionAudits: WeakMap<Session, string>
|
|
43
|
+
}
|