dsh-context-compression-improved 0.1.1 → 0.2.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/.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 +85 -82
- 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,613 @@
|
|
|
1
|
+
/** Configuration resolution for the mixed deterministic context-compression selector. */
|
|
2
|
+
|
|
3
|
+
import z from '@deepseek-ai/schemastery'
|
|
4
|
+
import type {
|
|
5
|
+
AutoCompactSettings,
|
|
6
|
+
CodeSkeletonSettings,
|
|
7
|
+
CompressionPolicy,
|
|
8
|
+
CompressionProfile,
|
|
9
|
+
CustomCompressionPolicy,
|
|
10
|
+
PresetOptions,
|
|
11
|
+
PresetOptionsSettings,
|
|
12
|
+
ContextCompressionSettings,
|
|
13
|
+
ResolvedConfig,
|
|
14
|
+
ToolResultPruneConfig,
|
|
15
|
+
} from './types.ts'
|
|
16
|
+
import { COMPRESSION_PROFILES } from './types.ts'
|
|
17
|
+
import {
|
|
18
|
+
CustomCompressionPolicySchema,
|
|
19
|
+
DEFAULT_CUSTOM_COMPRESSION_POLICY,
|
|
20
|
+
resolveCustomPolicy,
|
|
21
|
+
type CustomPolicyResolutionOptions,
|
|
22
|
+
} from './custom-policy.ts'
|
|
23
|
+
import { deepFreeze } from './value.ts'
|
|
24
|
+
|
|
25
|
+
/** Settings namespace shared by the Host service and browser selector. */
|
|
26
|
+
export const CONTEXT_COMPRESSION_SETTINGS_NAMESPACE = 'context-compression'
|
|
27
|
+
|
|
28
|
+
/** Fixed native fallback marker. */
|
|
29
|
+
export const PRUNE_MARKER = '\n\n[... tool result middle pruned ...]\n\n'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The one Auto Compact threshold contract shared by the settings UI, the
|
|
33
|
+
* persisted settings schema, and the runtime resolver. Every integer in the
|
|
34
|
+
* range is valid and entered directly in the UI.
|
|
35
|
+
*/
|
|
36
|
+
export const AUTO_COMPACT_THRESHOLD_LIMITS = deepFreeze({
|
|
37
|
+
min: 50,
|
|
38
|
+
max: 90,
|
|
39
|
+
step: 1,
|
|
40
|
+
default: 80,
|
|
41
|
+
} as const)
|
|
42
|
+
|
|
43
|
+
/** Narrow one untrusted value to a valid Auto Compact threshold percent. */
|
|
44
|
+
export function isValidAutoCompactThresholdPercent(value: unknown): value is number {
|
|
45
|
+
return typeof value === 'number'
|
|
46
|
+
&& Number.isSafeInteger(value)
|
|
47
|
+
&& value >= AUTO_COMPACT_THRESHOLD_LIMITS.min
|
|
48
|
+
&& value <= AUTO_COMPACT_THRESHOLD_LIMITS.max
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const AUTO_COMPACT_DEFAULT: AutoCompactSettings = deepFreeze({ thresholdPercent: AUTO_COMPACT_THRESHOLD_LIMITS.default })
|
|
52
|
+
|
|
53
|
+
/** Accept JSON-object records while rejecting class instances and exotic prototypes. */
|
|
54
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
55
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return false
|
|
56
|
+
const prototype = Object.getPrototypeOf(value) as object | null
|
|
57
|
+
return prototype === Object.prototype || prototype === null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Reject exotic prototypes anywhere in the JSON-like settings tree. */
|
|
61
|
+
function assertPlainDataTree(value: unknown, seen: WeakSet<object> = new WeakSet()): void {
|
|
62
|
+
if (value === null || typeof value !== 'object') return
|
|
63
|
+
if (seen.has(value)) return
|
|
64
|
+
seen.add(value)
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
for (const entry of value) assertPlainDataTree(entry, seen)
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
if (!isPlainRecord(value)) {
|
|
70
|
+
throw new TypeError('Context-compression settings must contain only plain objects')
|
|
71
|
+
}
|
|
72
|
+
for (const entry of Object.values(value)) assertPlainDataTree(entry, seen)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Strictly parse the persisted autoCompact section. Schemastery object
|
|
77
|
+
* defaults silently absorb null, empty, and extra-key sections, so this stays
|
|
78
|
+
* hand-validated beside the top-level unknown-key check.
|
|
79
|
+
*/
|
|
80
|
+
function parseAutoCompactSettings(value: unknown): AutoCompactSettings {
|
|
81
|
+
if (value === undefined) return AUTO_COMPACT_DEFAULT
|
|
82
|
+
if (!isPlainRecord(value)) {
|
|
83
|
+
throw new TypeError('Context-compression autoCompact must be a plain object')
|
|
84
|
+
}
|
|
85
|
+
const keys = Object.keys(value)
|
|
86
|
+
if (keys.length !== 1 || keys[0] !== 'thresholdPercent') {
|
|
87
|
+
throw new TypeError(`Context-compression autoCompact: expected exactly "thresholdPercent", got "${keys.join('", "')}"`)
|
|
88
|
+
}
|
|
89
|
+
const thresholdPercent = (value as Record<string, unknown>).thresholdPercent
|
|
90
|
+
if (!isValidAutoCompactThresholdPercent(thresholdPercent)) {
|
|
91
|
+
throw new TypeError(`Context-compression autoCompact.thresholdPercent (${String(thresholdPercent)}) must be an integer between ${String(AUTO_COMPACT_THRESHOLD_LIMITS.min)} and ${String(AUTO_COMPACT_THRESHOLD_LIMITS.max)}`)
|
|
92
|
+
}
|
|
93
|
+
return { thresholdPercent }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Strictly parse the persisted codeSkeleton section. The gate is orthogonal
|
|
98
|
+
* to every profile: absent inherits the lossless `false` default, while a
|
|
99
|
+
* present-but-invalid section is an explicitly invalid document.
|
|
100
|
+
*/
|
|
101
|
+
function parseCodeSkeletonSettings(value: unknown): CodeSkeletonSettings {
|
|
102
|
+
if (value === undefined) return { enabled: false }
|
|
103
|
+
if (!isPlainRecord(value)) {
|
|
104
|
+
throw new TypeError('Context-compression codeSkeleton must be a plain object')
|
|
105
|
+
}
|
|
106
|
+
const keys = Object.keys(value)
|
|
107
|
+
if (keys.length !== 1 || keys[0] !== 'enabled') {
|
|
108
|
+
throw new TypeError(`Context-compression codeSkeleton: expected exactly "enabled", got "${keys.join('", "')}"`)
|
|
109
|
+
}
|
|
110
|
+
const enabled = (value as Record<string, unknown>).enabled
|
|
111
|
+
if (typeof enabled !== 'boolean') {
|
|
112
|
+
throw new TypeError('Context-compression codeSkeleton.enabled must be a boolean')
|
|
113
|
+
}
|
|
114
|
+
return { enabled }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Parse the optional tokenpilot-inspired preset sub-capability section. Absent
|
|
119
|
+
* inherits the preset defaults; present-but-invalid is rejected, mirroring the
|
|
120
|
+
* codeSkeleton section semantics.
|
|
121
|
+
*/
|
|
122
|
+
export function parsePresetOptionsSettings(value: unknown): PresetOptionsSettings | undefined {
|
|
123
|
+
if (value === undefined) return undefined
|
|
124
|
+
if (!isPlainRecord(value)) {
|
|
125
|
+
throw new TypeError('Context-compression presetOptions must be a plain object')
|
|
126
|
+
}
|
|
127
|
+
const allowed = new Set([
|
|
128
|
+
'dedupeToolResults', 'summaryLocator', 'prefixStabilizer', 'readState', 'estimatorMode',
|
|
129
|
+
'estimatorProvider', 'estimatorModel', 'estimatorBaseUrl', 'estimatorApiKey', 'estimatorTimeoutMs',
|
|
130
|
+
])
|
|
131
|
+
const unknown = Object.keys(value).find(key => !allowed.has(key))
|
|
132
|
+
if (unknown !== undefined) {
|
|
133
|
+
throw new TypeError(`Context-compression presetOptions: unknown key "${unknown}"`)
|
|
134
|
+
}
|
|
135
|
+
const booleans = ['dedupeToolResults', 'summaryLocator', 'prefixStabilizer', 'readState'] as const
|
|
136
|
+
for (const key of booleans) {
|
|
137
|
+
const entry = value[key]
|
|
138
|
+
if (entry !== undefined && typeof entry !== 'boolean') {
|
|
139
|
+
throw new TypeError(`Context-compression presetOptions.${key} must be a boolean`)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const estimatorMode = value.estimatorMode
|
|
143
|
+
if (estimatorMode !== undefined && estimatorMode !== '' && estimatorMode !== 'host' && estimatorMode !== 'direct') {
|
|
144
|
+
throw new TypeError('Context-compression presetOptions.estimatorMode must be "", "host", or "direct"')
|
|
145
|
+
}
|
|
146
|
+
const estimatorTimeoutMs = value.estimatorTimeoutMs
|
|
147
|
+
if (estimatorTimeoutMs !== undefined
|
|
148
|
+
&& (typeof estimatorTimeoutMs !== 'number' || !Number.isSafeInteger(estimatorTimeoutMs)
|
|
149
|
+
|| estimatorTimeoutMs < 100 || estimatorTimeoutMs > 60_000)) {
|
|
150
|
+
throw new TypeError('Context-compression presetOptions.estimatorTimeoutMs must be an integer between 100 and 60000')
|
|
151
|
+
}
|
|
152
|
+
for (const key of ['estimatorProvider', 'estimatorModel', 'estimatorBaseUrl', 'estimatorApiKey'] as const) {
|
|
153
|
+
const entry = value[key]
|
|
154
|
+
if (entry !== undefined && typeof entry !== 'string') {
|
|
155
|
+
throw new TypeError(`Context-compression presetOptions.${key} must be a string`)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const result: {
|
|
159
|
+
-readonly [K in keyof PresetOptionsSettings]: PresetOptionsSettings[K]
|
|
160
|
+
} = {}
|
|
161
|
+
if (value.dedupeToolResults !== undefined) result.dedupeToolResults = value.dedupeToolResults as boolean
|
|
162
|
+
if (value.summaryLocator !== undefined) result.summaryLocator = value.summaryLocator as boolean
|
|
163
|
+
if (value.prefixStabilizer !== undefined) result.prefixStabilizer = value.prefixStabilizer as boolean
|
|
164
|
+
if (value.readState !== undefined) result.readState = value.readState as boolean
|
|
165
|
+
if (estimatorMode !== undefined) result.estimatorMode = estimatorMode as '' | 'host' | 'direct'
|
|
166
|
+
if (value.estimatorProvider !== undefined) result.estimatorProvider = value.estimatorProvider as string
|
|
167
|
+
if (value.estimatorModel !== undefined) result.estimatorModel = value.estimatorModel as string
|
|
168
|
+
if (value.estimatorBaseUrl !== undefined) result.estimatorBaseUrl = value.estimatorBaseUrl as string
|
|
169
|
+
if (value.estimatorApiKey !== undefined) result.estimatorApiKey = value.estimatorApiKey as string
|
|
170
|
+
if (estimatorTimeoutMs !== undefined) result.estimatorTimeoutMs = estimatorTimeoutMs as number
|
|
171
|
+
return result
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Settings schema used by the user-facing profile selector. */
|
|
175
|
+
const contextCompressionSettingsInputSchema = z.object({
|
|
176
|
+
profile: z.union([...COMPRESSION_PROFILES]).default('balanced'),
|
|
177
|
+
custom: CustomCompressionPolicySchema.default(DEFAULT_CUSTOM_COMPRESSION_POLICY),
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Reject a section that is PRESENT but not a usable value. Schemastery
|
|
182
|
+
* `.default(...)` silently substitutes null and undefined, which would turn a
|
|
183
|
+
* hand-corrupted store into the (lossy) default policy; only genuinely absent
|
|
184
|
+
* keys may inherit defaults, and that distinction must be made before any
|
|
185
|
+
* default can fire.
|
|
186
|
+
*/
|
|
187
|
+
function assertPresentSection(
|
|
188
|
+
candidate: Record<string, unknown>,
|
|
189
|
+
key: 'profile' | 'custom',
|
|
190
|
+
valid: (value: unknown) => boolean,
|
|
191
|
+
): void {
|
|
192
|
+
if (!Object.hasOwn(candidate, key)) return
|
|
193
|
+
if (!valid(candidate[key])) {
|
|
194
|
+
throw new TypeError(`Context-compression settings: "${key}" is present but invalid (${String(candidate[key])})`)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const isSupportedProfile = (value: unknown): boolean =>
|
|
199
|
+
typeof value === 'string' && (COMPRESSION_PROFILES as readonly string[]).includes(value)
|
|
200
|
+
|
|
201
|
+
const isUsableCustomDocument = (value: unknown): boolean =>
|
|
202
|
+
isPlainRecord(value)
|
|
203
|
+
|
|
204
|
+
const DEFAULT_CONTEXT_COMPRESSION_SETTINGS: ContextCompressionSettings = {
|
|
205
|
+
profile: 'balanced',
|
|
206
|
+
custom: structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY),
|
|
207
|
+
autoCompact: { thresholdPercent: AUTO_COMPACT_THRESHOLD_LIMITS.default },
|
|
208
|
+
codeSkeleton: { enabled: false },
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Parse one settings document with the persisted-section semantics: `undefined`
|
|
213
|
+
* inherits the defaults (an absent section), while `null` is an explicitly
|
|
214
|
+
* invalid document and must never silently become the default policy.
|
|
215
|
+
*/
|
|
216
|
+
export function parseContextCompressionSettings(value: unknown): ContextCompressionSettings {
|
|
217
|
+
if (!isPlainRecord(value)) {
|
|
218
|
+
throw new TypeError('Context-compression settings must be a plain object')
|
|
219
|
+
}
|
|
220
|
+
const keys = Object.keys(value)
|
|
221
|
+
// Documents surfaced by the settings service are always schema-resolved and
|
|
222
|
+
// complete; anything thinner is a hand-edited store and must not silently
|
|
223
|
+
// become a default (possibly lossy) policy.
|
|
224
|
+
if (keys.length === 0 || !keys.includes('profile') || !keys.includes('custom')) {
|
|
225
|
+
throw new TypeError('Context-compression settings document is missing its complete shape')
|
|
226
|
+
}
|
|
227
|
+
return ContextCompressionSettingsSchema(value as never)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Settings schema used by the user-facing profile selector. */
|
|
231
|
+
export const ContextCompressionSettingsSchema: z<ContextCompressionSettings> = z.transform(
|
|
232
|
+
z.any().required(),
|
|
233
|
+
(value: unknown): ContextCompressionSettings => {
|
|
234
|
+
if (!isPlainRecord(value)) {
|
|
235
|
+
throw new TypeError('Context-compression settings must be a plain object')
|
|
236
|
+
}
|
|
237
|
+
// Validate prototypes before cloning, then parse a detached mutable copy:
|
|
238
|
+
// SettingsProvider returns frozen records, while structuredClone would
|
|
239
|
+
// otherwise erase exotic prototypes before this boundary can reject them.
|
|
240
|
+
assertPlainDataTree(value)
|
|
241
|
+
const candidate = structuredClone(value)
|
|
242
|
+
// Settings stored before the autoCompact or codeSkeleton sections existed
|
|
243
|
+
// remain valid and inherit their defaults; the sections themselves stay
|
|
244
|
+
// strictly shaped.
|
|
245
|
+
const unknown = Object.keys(candidate).find(key =>
|
|
246
|
+
key !== 'profile' && key !== 'custom' && key !== 'autoCompact' && key !== 'codeSkeleton' && key !== 'presetOptions')
|
|
247
|
+
if (unknown !== undefined) {
|
|
248
|
+
throw new TypeError(`Context-compression settings: unknown key "${unknown}"`)
|
|
249
|
+
}
|
|
250
|
+
// Present-but-invalid sections must never fall through to the Schemastery
|
|
251
|
+
// defaults below: `profile: null` silently becoming the lossy `balanced`
|
|
252
|
+
// default is exactly the corruption the lossless-off fallback exists for.
|
|
253
|
+
// Only genuinely absent sections (internal partial fallbacks) may inherit.
|
|
254
|
+
assertPresentSection(candidate, 'profile', isSupportedProfile)
|
|
255
|
+
assertPresentSection(candidate, 'custom', isUsableCustomDocument)
|
|
256
|
+
const autoCompact = parseAutoCompactSettings(candidate.autoCompact)
|
|
257
|
+
const codeSkeleton = parseCodeSkeletonSettings(candidate.codeSkeleton)
|
|
258
|
+
const presetOptions = parsePresetOptionsSettings(candidate.presetOptions)
|
|
259
|
+
return {
|
|
260
|
+
...contextCompressionSettingsInputSchema(candidate),
|
|
261
|
+
autoCompact,
|
|
262
|
+
codeSkeleton,
|
|
263
|
+
...presetOptions === undefined ? {} : { presetOptions },
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
).default(DEFAULT_CONTEXT_COMPRESSION_SETTINGS) as z<ContextCompressionSettings>
|
|
267
|
+
|
|
268
|
+
/** Low-friction defaults; token budgets live in resolved profile policy. */
|
|
269
|
+
export const DEFAULTS: ResolvedConfig = deepFreeze({
|
|
270
|
+
profile: 'balanced',
|
|
271
|
+
headChars: 4096,
|
|
272
|
+
tailChars: 1024,
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
const CONFIG_KEYS: ReadonlySet<string> = new Set([
|
|
276
|
+
'profile',
|
|
277
|
+
'headChars',
|
|
278
|
+
'tailChars',
|
|
279
|
+
'nativeTriggerTokens',
|
|
280
|
+
'nativeTargetTokens',
|
|
281
|
+
'freshTriggerTokens',
|
|
282
|
+
'freshTargetTokens',
|
|
283
|
+
'aggregateTriggerTokens',
|
|
284
|
+
'aggregateTargetTokens',
|
|
285
|
+
'historyTriggerTokens',
|
|
286
|
+
'historyKeepRecentToolCalls',
|
|
287
|
+
'historyKeepRecentTokens',
|
|
288
|
+
'historyMinReclaimTokens',
|
|
289
|
+
'autoCompactThresholdPercent',
|
|
290
|
+
'presetOptions',
|
|
291
|
+
])
|
|
292
|
+
|
|
293
|
+
const LEGACY_GATE_REPLACEMENTS: Readonly<Record<string, string>> = Object.freeze({
|
|
294
|
+
thresholdChars: 'nativeTriggerTokens',
|
|
295
|
+
freshThresholdChars: 'freshTriggerTokens',
|
|
296
|
+
freshTargetChars: 'freshTargetTokens',
|
|
297
|
+
freshBatchTriggerChars: 'aggregateTriggerTokens',
|
|
298
|
+
freshBatchTargetChars: 'aggregateTargetTokens',
|
|
299
|
+
historyTriggerChars: 'historyTriggerTokens',
|
|
300
|
+
historyKeepRecentChars: 'historyKeepRecentTokens',
|
|
301
|
+
historyMinReclaimChars: 'historyMinReclaimTokens',
|
|
302
|
+
historyKeepRecentTurns: 'historyKeepRecentToolCalls',
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Count Unicode code points without splitting surrogate pairs.
|
|
307
|
+
* @param text - text whose code points are counted.
|
|
308
|
+
* @returns the number of Unicode code points.
|
|
309
|
+
*/
|
|
310
|
+
export function codePointLength(text: string): number {
|
|
311
|
+
let length = 0
|
|
312
|
+
for (const _point of text) length++
|
|
313
|
+
return length
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Test whether a settings value names a supported compression profile.
|
|
318
|
+
* @param value - untrusted settings value.
|
|
319
|
+
* @returns whether the value is a supported compression profile.
|
|
320
|
+
*/
|
|
321
|
+
export function isCompressionProfile(value: unknown): value is CompressionProfile {
|
|
322
|
+
return typeof value === 'string' && (COMPRESSION_PROFILES as readonly string[]).includes(value)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Resolve and validate plugin configuration.
|
|
327
|
+
* @param config - optional composition overrides.
|
|
328
|
+
* @returns a detached, deeply immutable configuration snapshot.
|
|
329
|
+
*/
|
|
330
|
+
export function resolveConfig(config: ToolResultPruneConfig = {}): ResolvedConfig {
|
|
331
|
+
for (const key of Object.keys(config)) {
|
|
332
|
+
const replacement = LEGACY_GATE_REPLACEMENTS[key]
|
|
333
|
+
if (replacement !== undefined) {
|
|
334
|
+
throw new Error(
|
|
335
|
+
`ToolResultPruneConfig: legacy gate "${key}" is no longer accepted; `
|
|
336
|
+
+ `choose "${replacement}" manually in tokens (no character conversion is applied)`,
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
if (!CONFIG_KEYS.has(key)) {
|
|
340
|
+
throw new Error(`ToolResultPruneConfig: unknown key "${key}"`)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const resolved: ResolvedConfig = {
|
|
344
|
+
profile: config.profile ?? DEFAULTS.profile,
|
|
345
|
+
headChars: config.headChars ?? DEFAULTS.headChars,
|
|
346
|
+
tailChars: config.tailChars ?? DEFAULTS.tailChars,
|
|
347
|
+
...config.nativeTriggerTokens === undefined ? {} : { nativeTriggerTokens: config.nativeTriggerTokens },
|
|
348
|
+
...config.nativeTargetTokens === undefined ? {} : { nativeTargetTokens: config.nativeTargetTokens },
|
|
349
|
+
...config.freshTriggerTokens === undefined ? {} : { freshTriggerTokens: config.freshTriggerTokens },
|
|
350
|
+
...config.freshTargetTokens === undefined ? {} : { freshTargetTokens: config.freshTargetTokens },
|
|
351
|
+
...config.aggregateTriggerTokens === undefined ? {} : { aggregateTriggerTokens: config.aggregateTriggerTokens },
|
|
352
|
+
...config.aggregateTargetTokens === undefined ? {} : { aggregateTargetTokens: config.aggregateTargetTokens },
|
|
353
|
+
...config.historyTriggerTokens === undefined ? {} : { historyTriggerTokens: config.historyTriggerTokens },
|
|
354
|
+
...config.historyKeepRecentToolCalls === undefined ? {} : { historyKeepRecentToolCalls: config.historyKeepRecentToolCalls },
|
|
355
|
+
...config.historyKeepRecentTokens === undefined ? {} : { historyKeepRecentTokens: config.historyKeepRecentTokens },
|
|
356
|
+
...config.historyMinReclaimTokens === undefined ? {} : { historyMinReclaimTokens: config.historyMinReclaimTokens },
|
|
357
|
+
...config.autoCompactThresholdPercent === undefined ? {} : { autoCompactThresholdPercent: config.autoCompactThresholdPercent },
|
|
358
|
+
...config.presetOptions === undefined ? {} : { presetOptions: config.presetOptions },
|
|
359
|
+
}
|
|
360
|
+
if (!isCompressionProfile(resolved.profile)) {
|
|
361
|
+
throw new Error(`ToolResultPruneConfig: unsupported profile "${String(resolved.profile)}"`)
|
|
362
|
+
}
|
|
363
|
+
assertNonNegativeInteger('headChars', resolved.headChars)
|
|
364
|
+
assertNonNegativeInteger('tailChars', resolved.tailChars)
|
|
365
|
+
for (const key of [
|
|
366
|
+
'nativeTriggerTokens', 'nativeTargetTokens', 'freshTriggerTokens', 'freshTargetTokens',
|
|
367
|
+
'aggregateTriggerTokens', 'aggregateTargetTokens', 'historyTriggerTokens',
|
|
368
|
+
'historyMinReclaimTokens',
|
|
369
|
+
] as const) {
|
|
370
|
+
const value = resolved[key]
|
|
371
|
+
if (value !== undefined) assertPositiveInteger(key, value)
|
|
372
|
+
}
|
|
373
|
+
if (resolved.historyKeepRecentToolCalls !== undefined) {
|
|
374
|
+
assertNonNegativeInteger('historyKeepRecentToolCalls', resolved.historyKeepRecentToolCalls)
|
|
375
|
+
}
|
|
376
|
+
if (resolved.historyKeepRecentTokens !== undefined) {
|
|
377
|
+
assertNonNegativeInteger('historyKeepRecentTokens', resolved.historyKeepRecentTokens)
|
|
378
|
+
}
|
|
379
|
+
if (resolved.autoCompactThresholdPercent !== undefined
|
|
380
|
+
&& !isValidAutoCompactThresholdPercent(resolved.autoCompactThresholdPercent)) {
|
|
381
|
+
throw new Error(`ToolResultPruneConfig: autoCompactThresholdPercent (${String(resolved.autoCompactThresholdPercent)}) must be an integer between ${String(AUTO_COMPACT_THRESHOLD_LIMITS.min)} and ${String(AUTO_COMPACT_THRESHOLD_LIMITS.max)}`)
|
|
382
|
+
}
|
|
383
|
+
assertTargetBelowTrigger('native', resolved.nativeTargetTokens, resolved.nativeTriggerTokens)
|
|
384
|
+
assertTargetBelowTrigger('fresh', resolved.freshTargetTokens, resolved.freshTriggerTokens)
|
|
385
|
+
assertTargetBelowTrigger('aggregate', resolved.aggregateTargetTokens, resolved.aggregateTriggerTokens)
|
|
386
|
+
return deepFreeze(structuredClone(resolved))
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Per-profile History linkage ratios applied to the Auto Compact watermark. */
|
|
390
|
+
const AUTO_COMPACT_HISTORY_RATIOS: Readonly<Record<string, Readonly<{
|
|
391
|
+
trigger: number
|
|
392
|
+
minReclaim: number
|
|
393
|
+
keepRecentTokens: number
|
|
394
|
+
}>>> = Object.freeze({
|
|
395
|
+
balanced: Object.freeze({ trigger: 0.625, minReclaim: 0.12, keepRecentTokens: 0.08 }),
|
|
396
|
+
savings: Object.freeze({ trigger: 0.50, minReclaim: 0.16, keepRecentTokens: 0.08 }),
|
|
397
|
+
'cache-strict': Object.freeze({ trigger: 0.75, minReclaim: 0.16, keepRecentTokens: 0.08 }),
|
|
398
|
+
adaptive: Object.freeze({ trigger: 0.625, minReclaim: 0.12, keepRecentTokens: 0.08 }),
|
|
399
|
+
'tokenpilot-inspired': Object.freeze({ trigger: 0.625, minReclaim: 0.12, keepRecentTokens: 0.08 }),
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
/** Micro-compact last-chance ratio: `D = floor(A × 0.875)`. */
|
|
403
|
+
const MICRO_DEADLINE_RATIO = 0.875
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* TokenPilot-inspired sub-capability defaults. Every capability is on except
|
|
407
|
+
* the estimator, which requires an explicit endpoint channel (host or direct)
|
|
408
|
+
* before any consumer may leave its rule-only fallback.
|
|
409
|
+
*/
|
|
410
|
+
const PRESET_OPTION_DEFAULTS: PresetOptions = deepFreeze({
|
|
411
|
+
noNetSavingsGuard: true,
|
|
412
|
+
skipReductionRecovery: true,
|
|
413
|
+
dedupeToolResults: true,
|
|
414
|
+
summaryLocator: true,
|
|
415
|
+
prefixStabilizer: true,
|
|
416
|
+
readState: true,
|
|
417
|
+
estimator: { mode: '' },
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Merge persisted presetOptions overrides over the tokenpilot-inspired
|
|
422
|
+
* defaults. Persisted booleans are three-state (undefined = inherit); the
|
|
423
|
+
* estimator channel overrides the default empty mode wholesale.
|
|
424
|
+
*/
|
|
425
|
+
function mergePresetOptions(overrides: PresetOptionsSettings | undefined): PresetOptions {
|
|
426
|
+
if (overrides === undefined) return PRESET_OPTION_DEFAULTS
|
|
427
|
+
return deepFreeze({
|
|
428
|
+
noNetSavingsGuard: true,
|
|
429
|
+
skipReductionRecovery: true,
|
|
430
|
+
dedupeToolResults: overrides.dedupeToolResults ?? PRESET_OPTION_DEFAULTS.dedupeToolResults,
|
|
431
|
+
summaryLocator: overrides.summaryLocator ?? PRESET_OPTION_DEFAULTS.summaryLocator,
|
|
432
|
+
prefixStabilizer: overrides.prefixStabilizer ?? PRESET_OPTION_DEFAULTS.prefixStabilizer,
|
|
433
|
+
readState: overrides.readState ?? PRESET_OPTION_DEFAULTS.readState,
|
|
434
|
+
estimator: { mode: overrides.estimatorMode ?? PRESET_OPTION_DEFAULTS.estimator.mode },
|
|
435
|
+
})
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Resolve the Auto-Compact-linked History watermarks for one standard profile.
|
|
440
|
+
*
|
|
441
|
+
* `A = floor(C × a)` is the Auto Compact token watermark for the routed
|
|
442
|
+
* context window `C` and the user threshold `a = p / 100`; the History
|
|
443
|
+
* trigger, minimum reclaim, and recent-token tail scale with `A`, and the
|
|
444
|
+
* micro-compact last-chance deadline is `D = floor(A × 0.875)`. At the shipped
|
|
445
|
+
* defaults (`C = 1,000,000`, `p = 80`) the ratios reproduce the previous fixed
|
|
446
|
+
* preset numbers exactly. Custom stays manual and Off/Native run no History,
|
|
447
|
+
* so none of them link.
|
|
448
|
+
*/
|
|
449
|
+
function resolveAutoCompactLinkage(
|
|
450
|
+
profile: CompressionProfile,
|
|
451
|
+
options: CustomPolicyResolutionOptions,
|
|
452
|
+
): { autoCompactTokens: number, microDeadlineTokens: number, historyTriggerTokens: number,
|
|
453
|
+
historyMinReclaimTokens: number, historyKeepRecentTokens: number } | undefined {
|
|
454
|
+
const ratios = profile === 'custom' ? undefined : AUTO_COMPACT_HISTORY_RATIOS[profile]
|
|
455
|
+
const contextWindow = options.contextWindowTokens
|
|
456
|
+
const threshold = options.autoCompactThresholdPercent
|
|
457
|
+
if (ratios === undefined) return undefined
|
|
458
|
+
if (!isValidAutoCompactThresholdPercent(threshold)) return undefined
|
|
459
|
+
if (!Number.isSafeInteger(contextWindow) || contextWindow === undefined || contextWindow <= 0) return undefined
|
|
460
|
+
// Mirror compaction-basic's evaluation order exactly: it multiplies by the
|
|
461
|
+
// generated ratio float (p / 100), and integer-first division differs by
|
|
462
|
+
// one token on some window/percent pairs (200k x 57).
|
|
463
|
+
const autoCompactTokens = Math.floor(contextWindow * (threshold / 100))
|
|
464
|
+
if (!Number.isSafeInteger(autoCompactTokens) || autoCompactTokens <= 0) return undefined
|
|
465
|
+
const linked = {
|
|
466
|
+
autoCompactTokens,
|
|
467
|
+
microDeadlineTokens: Math.floor(autoCompactTokens * MICRO_DEADLINE_RATIO),
|
|
468
|
+
historyTriggerTokens: Math.floor(ratios.trigger * autoCompactTokens),
|
|
469
|
+
historyMinReclaimTokens: Math.floor(ratios.minReclaim * autoCompactTokens),
|
|
470
|
+
historyKeepRecentTokens: Math.floor(ratios.keepRecentTokens * autoCompactTokens),
|
|
471
|
+
}
|
|
472
|
+
for (const value of Object.values(linked)) {
|
|
473
|
+
if (!Number.isSafeInteger(value) || value <= 0) return undefined
|
|
474
|
+
}
|
|
475
|
+
return linked
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Resolve one public profile into a complete mixed-strategy policy.
|
|
480
|
+
* @param config - validated composition configuration.
|
|
481
|
+
* @param profile - profile frozen for the target Session.
|
|
482
|
+
* @param custom - versioned Custom document used only by the `custom` profile.
|
|
483
|
+
* @param options - routed capacity and the frozen Auto Compact threshold used
|
|
484
|
+
* to resolve context-percent Custom values and standard-profile linkage.
|
|
485
|
+
* @returns the effective deterministic compression policy.
|
|
486
|
+
*/
|
|
487
|
+
export function resolvePolicy(
|
|
488
|
+
config: ResolvedConfig,
|
|
489
|
+
profile: CompressionProfile,
|
|
490
|
+
custom: CustomCompressionPolicy = DEFAULT_CUSTOM_COMPRESSION_POLICY,
|
|
491
|
+
options: CustomPolicyResolutionOptions = {},
|
|
492
|
+
): CompressionPolicy {
|
|
493
|
+
if (profile === 'custom') return resolveCustomPolicy(custom, options)
|
|
494
|
+
const presets: Record<Exclude<CompressionProfile, 'custom'>, Omit<CompressionPolicy, 'profile'>> = {
|
|
495
|
+
off: {
|
|
496
|
+
nativeToolResultEnabled: false, freshEnabled: false, aggregateEnabled: false, historyMode: 'disabled',
|
|
497
|
+
nativeTriggerTokens: Number.MAX_SAFE_INTEGER, nativeTargetTokens: Number.MAX_SAFE_INTEGER,
|
|
498
|
+
freshTriggerTokens: Number.MAX_SAFE_INTEGER, freshTargetTokens: Number.MAX_SAFE_INTEGER,
|
|
499
|
+
aggregateTriggerTokens: Number.MAX_SAFE_INTEGER, aggregateTargetTokens: Number.MAX_SAFE_INTEGER,
|
|
500
|
+
historyTriggerTokens: Number.MAX_SAFE_INTEGER, historyKeepRecentToolCalls: 10,
|
|
501
|
+
historyKeepRecentTokens: 64_000, historyMinReclaimTokens: Number.MAX_SAFE_INTEGER,
|
|
502
|
+
},
|
|
503
|
+
native: {
|
|
504
|
+
nativeToolResultEnabled: true, freshEnabled: false, aggregateEnabled: false, historyMode: 'disabled',
|
|
505
|
+
nativeTriggerTokens: 4_096, nativeTargetTokens: 2_048,
|
|
506
|
+
freshTriggerTokens: Number.MAX_SAFE_INTEGER, freshTargetTokens: Number.MAX_SAFE_INTEGER,
|
|
507
|
+
aggregateTriggerTokens: Number.MAX_SAFE_INTEGER, aggregateTargetTokens: Number.MAX_SAFE_INTEGER,
|
|
508
|
+
historyTriggerTokens: Number.MAX_SAFE_INTEGER, historyKeepRecentToolCalls: 10,
|
|
509
|
+
historyKeepRecentTokens: 64_000, historyMinReclaimTokens: Number.MAX_SAFE_INTEGER,
|
|
510
|
+
},
|
|
511
|
+
balanced: {
|
|
512
|
+
nativeToolResultEnabled: false, freshEnabled: true, aggregateEnabled: true, historyMode: 'routine',
|
|
513
|
+
nativeTriggerTokens: Number.MAX_SAFE_INTEGER, nativeTargetTokens: Number.MAX_SAFE_INTEGER,
|
|
514
|
+
freshTriggerTokens: 8_192, freshTargetTokens: 3_072,
|
|
515
|
+
aggregateTriggerTokens: 32_768, aggregateTargetTokens: 12_288,
|
|
516
|
+
historyTriggerTokens: 500_000, historyKeepRecentToolCalls: 10,
|
|
517
|
+
historyKeepRecentTokens: 64_000, historyMinReclaimTokens: 96_000,
|
|
518
|
+
},
|
|
519
|
+
'cache-strict': {
|
|
520
|
+
nativeToolResultEnabled: false, freshEnabled: true, aggregateEnabled: true, historyMode: 'capacity-pressure',
|
|
521
|
+
nativeTriggerTokens: Number.MAX_SAFE_INTEGER, nativeTargetTokens: Number.MAX_SAFE_INTEGER,
|
|
522
|
+
freshTriggerTokens: 8_192, freshTargetTokens: 3_072,
|
|
523
|
+
aggregateTriggerTokens: 32_768, aggregateTargetTokens: 12_288,
|
|
524
|
+
historyTriggerTokens: 600_000, historyKeepRecentToolCalls: 10,
|
|
525
|
+
historyKeepRecentTokens: 64_000, historyMinReclaimTokens: 128_000,
|
|
526
|
+
},
|
|
527
|
+
savings: {
|
|
528
|
+
nativeToolResultEnabled: false, freshEnabled: true, aggregateEnabled: true, historyMode: 'routine',
|
|
529
|
+
nativeTriggerTokens: Number.MAX_SAFE_INTEGER, nativeTargetTokens: Number.MAX_SAFE_INTEGER,
|
|
530
|
+
freshTriggerTokens: 4_096, freshTargetTokens: 1_536,
|
|
531
|
+
aggregateTriggerTokens: 16_384, aggregateTargetTokens: 4_096,
|
|
532
|
+
historyTriggerTokens: 400_000, historyKeepRecentToolCalls: 10,
|
|
533
|
+
historyKeepRecentTokens: 64_000, historyMinReclaimTokens: 128_000,
|
|
534
|
+
},
|
|
535
|
+
adaptive: {
|
|
536
|
+
nativeToolResultEnabled: false, freshEnabled: true, aggregateEnabled: true, historyMode: 'adaptive',
|
|
537
|
+
nativeTriggerTokens: Number.MAX_SAFE_INTEGER, nativeTargetTokens: Number.MAX_SAFE_INTEGER,
|
|
538
|
+
freshTriggerTokens: 8_192, freshTargetTokens: 3_072,
|
|
539
|
+
aggregateTriggerTokens: 32_768, aggregateTargetTokens: 12_288,
|
|
540
|
+
historyTriggerTokens: 500_000, historyKeepRecentToolCalls: 10,
|
|
541
|
+
historyKeepRecentTokens: 64_000, historyMinReclaimTokens: 96_000,
|
|
542
|
+
},
|
|
543
|
+
'tokenpilot-inspired': {
|
|
544
|
+
nativeToolResultEnabled: false, freshEnabled: true, aggregateEnabled: true, historyMode: 'routine',
|
|
545
|
+
nativeTriggerTokens: Number.MAX_SAFE_INTEGER, nativeTargetTokens: Number.MAX_SAFE_INTEGER,
|
|
546
|
+
freshTriggerTokens: 8_192, freshTargetTokens: 3_072,
|
|
547
|
+
aggregateTriggerTokens: 32_768, aggregateTargetTokens: 12_288,
|
|
548
|
+
historyTriggerTokens: 500_000, historyKeepRecentToolCalls: 10,
|
|
549
|
+
historyKeepRecentTokens: 64_000, historyMinReclaimTokens: 96_000,
|
|
550
|
+
},
|
|
551
|
+
}
|
|
552
|
+
const preset = presets[profile]
|
|
553
|
+
const linkage = resolveAutoCompactLinkage(profile, options)
|
|
554
|
+
const policy: CompressionPolicy = {
|
|
555
|
+
profile,
|
|
556
|
+
...preset,
|
|
557
|
+
nativeTriggerTokens: config.nativeTriggerTokens ?? preset.nativeTriggerTokens,
|
|
558
|
+
nativeTargetTokens: config.nativeTargetTokens ?? preset.nativeTargetTokens,
|
|
559
|
+
freshTriggerTokens: config.freshTriggerTokens ?? preset.freshTriggerTokens,
|
|
560
|
+
freshTargetTokens: config.freshTargetTokens ?? preset.freshTargetTokens,
|
|
561
|
+
aggregateTriggerTokens: config.aggregateTriggerTokens ?? preset.aggregateTriggerTokens,
|
|
562
|
+
aggregateTargetTokens: config.aggregateTargetTokens ?? preset.aggregateTargetTokens,
|
|
563
|
+
historyTriggerTokens: config.historyTriggerTokens
|
|
564
|
+
?? linkage?.historyTriggerTokens ?? preset.historyTriggerTokens,
|
|
565
|
+
historyKeepRecentToolCalls: config.historyKeepRecentToolCalls ?? preset.historyKeepRecentToolCalls,
|
|
566
|
+
historyKeepRecentTokens: config.historyKeepRecentTokens
|
|
567
|
+
?? linkage?.historyKeepRecentTokens ?? preset.historyKeepRecentTokens,
|
|
568
|
+
historyMinReclaimTokens: config.historyMinReclaimTokens
|
|
569
|
+
?? linkage?.historyMinReclaimTokens ?? preset.historyMinReclaimTokens,
|
|
570
|
+
...linkage === undefined ? {} : {
|
|
571
|
+
autoCompactTokens: linkage.autoCompactTokens,
|
|
572
|
+
microDeadlineTokens: linkage.microDeadlineTokens,
|
|
573
|
+
},
|
|
574
|
+
...profile === 'tokenpilot-inspired' ? {
|
|
575
|
+
presetOptions: mergePresetOptions(config.presetOptions),
|
|
576
|
+
} : {},
|
|
577
|
+
}
|
|
578
|
+
if (policy.nativeTargetTokens >= policy.nativeTriggerTokens && profile === 'native') {
|
|
579
|
+
throw new Error('context compression policy: native target must be below trigger')
|
|
580
|
+
}
|
|
581
|
+
if (policy.freshTargetTokens >= policy.freshTriggerTokens && policy.freshEnabled) {
|
|
582
|
+
throw new Error('context compression policy: fresh target must be below trigger')
|
|
583
|
+
}
|
|
584
|
+
if (policy.aggregateTargetTokens >= policy.aggregateTriggerTokens && policy.freshEnabled) {
|
|
585
|
+
throw new Error('context compression policy: aggregate target must be below trigger')
|
|
586
|
+
}
|
|
587
|
+
return deepFreeze(policy)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function assertTargetBelowTrigger(
|
|
591
|
+
label: string,
|
|
592
|
+
target: number | undefined,
|
|
593
|
+
trigger: number | undefined,
|
|
594
|
+
): void {
|
|
595
|
+
if ((target === undefined) !== (trigger === undefined)) {
|
|
596
|
+
throw new Error(`ToolResultPruneConfig: ${label} target and trigger tokens must be provided together`)
|
|
597
|
+
}
|
|
598
|
+
if (target !== undefined && trigger !== undefined && target >= trigger) {
|
|
599
|
+
throw new Error(`ToolResultPruneConfig: ${label} target tokens must be below trigger tokens`)
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function assertPositiveInteger(name: string, value: number): void {
|
|
604
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
605
|
+
throw new Error(`ToolResultPruneConfig: ${name} (${String(value)}) must be a positive safe integer`)
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function assertNonNegativeInteger(name: string, value: number): void {
|
|
610
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
611
|
+
throw new Error(`ToolResultPruneConfig: ${name} (${String(value)}) must be a non-negative safe integer`)
|
|
612
|
+
}
|
|
613
|
+
}
|