dsh-context-compression-improved 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.ja.md +51 -0
- package/CHANGELOG.ko.md +51 -0
- package/CHANGELOG.md +55 -0
- package/CHANGELOG.zh.md +45 -0
- package/package.json +1 -1
- package/packages/selector/lib/advisor-state.js +4 -231
- package/packages/selector/lib/client.d.ts +0 -24
- package/packages/selector/lib/client.js +6 -501
- package/packages/selector/lib/index.d.ts +4 -10
- package/packages/selector/lib/index.js +65 -235
- package/packages/selector/lib/pruner.d.ts +13 -248
- package/packages/selector/lib/pruner.js +148 -552
- package/packages/selector/src/client/EstimatorControls.tsx +277 -378
- package/packages/selector/src/client/index.ts +0 -17
- package/packages/selector/src/client/locales.ts +196 -234
- package/packages/selector/src/client/preset-options.ts +3 -2
- package/packages/selector/src/client/settings-section.tsx +8 -17
- package/packages/selector/src/index.ts +463 -710
- package/packages/selector/src/preset-overlay.ts +60 -1
- package/packages/selector/src/profiles.ts +4 -27
- package/packages/selector/src/pruner/state.ts +50 -73
- package/packages/selector/src/pruner.ts +2402 -2730
- package/packages/selector/src/runtime/audit.ts +27 -21
- package/packages/selector/src/runtime/config.ts +6 -32
- package/packages/selector/src/runtime/tokenpilot/advisor-state.ts +16 -0
- package/packages/selector/src/runtime/tokenpilot/benefit.ts +200 -0
- package/packages/selector/src/runtime/types.ts +0 -17
- package/packages/selector/tests/built/client-artifact.spec.ts +9 -5
- package/packages/selector/tests/preset-options-write.client.spec.ts +7 -23
- package/packages/selector/tests/runtime/advice-never-withholds.host.spec.ts +232 -0
- package/packages/selector/tests/runtime/audit.spec.ts +35 -21
- package/packages/selector/tests/runtime/deprecated-preset-options.spec.ts +96 -0
- package/packages/selector/tests/runtime/tokenpilot/benefit.spec.ts +217 -0
- package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +4 -5
- package/packages/selector/tests/settings-seat.client.spec.ts +16 -10
- package/packages/selector/tests/standing-generation.host.spec.ts +54 -5
- package/scripts/packed-components-smoke.mjs +30 -8
- package/scripts/packed-install-e2e.mjs +69 -15
- package/packages/selector/src/client/ReviewOverlay.tsx +0 -320
- package/packages/selector/src/client/review-scope.ts +0 -16
- package/packages/selector/src/runtime/tokenpilot/proposal.ts +0 -267
- package/packages/selector/src/runtime/tokenpilot/review-queue.ts +0 -231
- package/packages/selector/src/runtime/tokenpilot/review-registry.ts +0 -117
- package/packages/selector/src/runtime/tokenpilot/review-storage.ts +0 -122
- package/packages/selector/tests/review-overlay.client.spec.tsx +0 -118
- package/packages/selector/tests/review-routes-registry.host.spec.ts +0 -142
- package/packages/selector/tests/review-routes.host.spec.ts +0 -290
- package/packages/selector/tests/runtime/tokenpilot/proposal.spec.ts +0 -393
- package/packages/selector/tests/runtime/tokenpilot/pruner-review.spec.ts +0 -382
- package/packages/selector/tests/runtime/tokenpilot/review-queue.spec.ts +0 -168
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The retired review gate's replacement, pinned at the host integration level.
|
|
3
|
+
*
|
|
4
|
+
* This spec exists to make the OLD failure impossible to reintroduce: with the
|
|
5
|
+
* exact settings that used to divert 100% of a fresh batch into the human-gated
|
|
6
|
+
* review queue (`reviewMode: true` + a `reviewHighImpactTokens` threshold far
|
|
7
|
+
* below the batch), the batch must LAND, and the benefit model must publish its
|
|
8
|
+
* opinion as a `reduction-advice` audit instead of withholding anything.
|
|
9
|
+
*
|
|
10
|
+
* "缩减不阻断自动处理": advice describes a landing, it never gates one.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
13
|
+
import { Context } from '@deepseek-ai/cordis'
|
|
14
|
+
import {
|
|
15
|
+
ToolCallId as CallId,
|
|
16
|
+
createMessage,
|
|
17
|
+
createUserMessage,
|
|
18
|
+
createToolResultMessage,
|
|
19
|
+
} from '@deepseek-ai/dsh-llm'
|
|
20
|
+
import SessionStore, {
|
|
21
|
+
Session,
|
|
22
|
+
SessionId,
|
|
23
|
+
canonicalHeader,
|
|
24
|
+
} from '@deepseek-ai/dsh-session'
|
|
25
|
+
import {
|
|
26
|
+
SettingsProvider,
|
|
27
|
+
type SettingsNamespace,
|
|
28
|
+
} from '@deepseek-ai/dsh-settings'
|
|
29
|
+
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
|
30
|
+
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
|
|
31
|
+
import TokenMeter from '@deepseek-ai/dsh-token-meter'
|
|
32
|
+
import ToolRuntime from '@deepseek-ai/dsh-tools'
|
|
33
|
+
import * as SelectorHost from '../../src/index.ts'
|
|
34
|
+
import ToolResultPruner, {
|
|
35
|
+
CONTEXT_COMPRESSION_SETTINGS_NAMESPACE,
|
|
36
|
+
} from '../../src/pruner.ts'
|
|
37
|
+
import { measureForCompaction } from '../../src/runtime/measurement.ts'
|
|
38
|
+
import {
|
|
39
|
+
COMPRESSION_AUDIT_PREFIX,
|
|
40
|
+
type CompressionAuditRecord,
|
|
41
|
+
type CompressionRewriteAuditRecord,
|
|
42
|
+
type ReductionAdviceAuditRecord,
|
|
43
|
+
} from '../../src/runtime/audit.ts'
|
|
44
|
+
|
|
45
|
+
const MODEL = 'deepseek-v4-flash'
|
|
46
|
+
|
|
47
|
+
class TestSettings extends SettingsProvider {
|
|
48
|
+
readonly writable = true
|
|
49
|
+
private readonly stored: Record<string, unknown> = {}
|
|
50
|
+
|
|
51
|
+
protected override load(): Promise<Record<string, unknown>> {
|
|
52
|
+
return Promise.resolve(structuredClone(this.stored))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
protected override persist(namespace: SettingsNamespace, section: Record<string, unknown>): Promise<void> {
|
|
56
|
+
this.stored[namespace] = structuredClone(section)
|
|
57
|
+
return Promise.resolve()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function runtimeContext(): Promise<Context> {
|
|
62
|
+
const ctx = new Context()
|
|
63
|
+
await ctx.plugin(SessionStore).await()
|
|
64
|
+
await ctx.plugin(SystemPrompt).await()
|
|
65
|
+
await ctx.plugin(ToolRuntime).await()
|
|
66
|
+
await ctx.plugin(SessionProjectionRegistry).await()
|
|
67
|
+
await ctx.plugin(TokenMeter).await()
|
|
68
|
+
return ctx
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function captureAudit(ctx: Context): { records(): CompressionAuditRecord[] } {
|
|
72
|
+
const info = vi.spyOn(ctx.logger, 'info').mockImplementation(() => ctx.logger)
|
|
73
|
+
return {
|
|
74
|
+
records: () => info.mock.calls.flatMap((call) => {
|
|
75
|
+
const line = String(call[0])
|
|
76
|
+
return line.startsWith(COMPRESSION_AUDIT_PREFIX)
|
|
77
|
+
? [JSON.parse(line.slice(COMPRESSION_AUDIT_PREFIX.length)) as CompressionAuditRecord]
|
|
78
|
+
: []
|
|
79
|
+
}),
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function rewrites(records: readonly CompressionAuditRecord[]): CompressionRewriteAuditRecord[] {
|
|
84
|
+
return records.filter((record): record is CompressionRewriteAuditRecord => record.kind === 'rewrite')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function adviceOf(records: readonly CompressionAuditRecord[]): ReductionAdviceAuditRecord[] {
|
|
88
|
+
return records.filter((record): record is ReductionAdviceAuditRecord => record.kind === 'reduction-advice')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const nsBrand = (value: string): SettingsNamespace => value as unknown as SettingsNamespace
|
|
92
|
+
|
|
93
|
+
function appendToolTurn(session: Session, turn: number, text: string): void {
|
|
94
|
+
const callId = CallId(`call-${String(turn)}`)
|
|
95
|
+
session.append('turn/start', { turn })
|
|
96
|
+
if (session.requestHeader() === undefined) {
|
|
97
|
+
session.append('request/header', {
|
|
98
|
+
reason: 'initial',
|
|
99
|
+
header: canonicalHeader({ config: { provider: 'deepseek', model: MODEL } }),
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
session.append('user/message', createUserMessage({
|
|
103
|
+
content: [{ type: 'text', text: `user turn ${String(turn)}` }],
|
|
104
|
+
source: { kind: 'user' },
|
|
105
|
+
}), { surfaceOp: 'append' })
|
|
106
|
+
session.append('step/start', { turn, step: 1 })
|
|
107
|
+
session.append('assistant/message', {
|
|
108
|
+
stream: [],
|
|
109
|
+
turn,
|
|
110
|
+
step: 1,
|
|
111
|
+
message: createMessage({
|
|
112
|
+
role: 'assistant',
|
|
113
|
+
content: [{ type: 'tool-call', id: callId, name: 'bash', arguments: '{}' }],
|
|
114
|
+
source: { kind: 'model', provider: 'deepseek', model: MODEL },
|
|
115
|
+
}),
|
|
116
|
+
}, { surfaceOp: 'append' })
|
|
117
|
+
session.append('tool/call', { turn, step: 1, callId, name: 'bash', arguments: '{}' })
|
|
118
|
+
session.append('tool/result', {
|
|
119
|
+
turn,
|
|
120
|
+
step: 1,
|
|
121
|
+
message: createToolResultMessage({
|
|
122
|
+
callId,
|
|
123
|
+
content: [{ type: 'text', text }],
|
|
124
|
+
isError: false,
|
|
125
|
+
}),
|
|
126
|
+
}, { surfaceOp: 'append' })
|
|
127
|
+
session.append('step/end', { turn, step: 1 })
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The settings that used to withhold everything: review mode ON with a
|
|
132
|
+
* high-impact threshold of 1 token. History pricing is kept production-like
|
|
133
|
+
* (64,000 protected tail tokens) so the batch is only landable through the
|
|
134
|
+
* fresh stage.
|
|
135
|
+
*/
|
|
136
|
+
async function gatedSettings(ctx: Context): Promise<void> {
|
|
137
|
+
await ctx.plugin(TestSettings).await()
|
|
138
|
+
await ctx.plugin(SelectorHost).await()
|
|
139
|
+
await ctx.settings.update(nsBrand(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
|
|
140
|
+
profile: 'tokenpilot-inspired',
|
|
141
|
+
presetOptions: { reviewMode: true, reviewHighImpactTokens: 1 },
|
|
142
|
+
})
|
|
143
|
+
await ctx.plugin(ToolResultPruner, {
|
|
144
|
+
profile: 'tokenpilot-inspired',
|
|
145
|
+
freshTriggerTokens: 200,
|
|
146
|
+
freshTargetTokens: 100,
|
|
147
|
+
aggregateTriggerTokens: 1_000_000,
|
|
148
|
+
aggregateTargetTokens: 900_000,
|
|
149
|
+
historyTriggerTokens: 400,
|
|
150
|
+
historyKeepRecentToolCalls: 0,
|
|
151
|
+
historyKeepRecentTokens: 64_000,
|
|
152
|
+
historyMinReclaimTokens: 1,
|
|
153
|
+
}).await()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function freshSession(ctx: Context, id: string, text: string): Session {
|
|
157
|
+
const session = Session.create(SessionId(id))
|
|
158
|
+
appendToolTurn(session, 1, text)
|
|
159
|
+
const total = measureForCompaction(ctx, session).totalTokens
|
|
160
|
+
session.append('request/context', {
|
|
161
|
+
provider: 'deepseek',
|
|
162
|
+
model: MODEL,
|
|
163
|
+
contextWindow: Math.floor(total / 0.6),
|
|
164
|
+
})
|
|
165
|
+
// Fresh landing publishes a surface replacement, which requires an open turn.
|
|
166
|
+
session.append('turn/start', { turn: 2 })
|
|
167
|
+
return session
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** ~36,400 characters. The shipped advice threshold is 4,000 tokens, and this
|
|
171
|
+
* fixture is repetitive enough that the exact tokenizer packs it denser than
|
|
172
|
+
* the conservative 4-chars-per-token estimate (an 18,200-character variant
|
|
173
|
+
* measured under 4,000 tokens), so the volume is doubled to keep the
|
|
174
|
+
* `high-impact` label deterministic. */
|
|
175
|
+
const HIGH_IMPACT_TEXT = 'fresh reviewable evidence '.repeat(1_400)
|
|
176
|
+
|
|
177
|
+
describe('advice never withholds (retired review gate)', () => {
|
|
178
|
+
it('lands the fresh batch the gate used to divert, and advises instead', async () => {
|
|
179
|
+
const ctx = await runtimeContext()
|
|
180
|
+
await gatedSettings(ctx)
|
|
181
|
+
const audit = captureAudit(ctx)
|
|
182
|
+
const session = freshSession(ctx, 'advice-lands', HIGH_IMPACT_TEXT)
|
|
183
|
+
|
|
184
|
+
const result = ctx.toolResultPruner.pruneSession(session, { stage: 'fresh', freshTurn: 1, freshStep: 1 })
|
|
185
|
+
const records = audit.records()
|
|
186
|
+
|
|
187
|
+
// 1. The reduction LANDED. Under the retired gate this was the assertion
|
|
188
|
+
// that failed: the batch was withheld pending human approval.
|
|
189
|
+
expect(result.pruned).toHaveLength(1)
|
|
190
|
+
expect(rewrites(records).some(entry => entry.component === 'fresh')).toBe(true)
|
|
191
|
+
|
|
192
|
+
// 2. The benefit model still speaks — as advice about the landing.
|
|
193
|
+
const advice = adviceOf(records)
|
|
194
|
+
expect(advice).toHaveLength(1)
|
|
195
|
+
expect(advice[0]!.stage).toBe('fresh')
|
|
196
|
+
expect(advice[0]!.band).toBe('high-impact')
|
|
197
|
+
expect(advice[0]!.maxTokensBefore).toBeGreaterThanOrEqual(4_000)
|
|
198
|
+
expect(advice[0]!.recoveredTokens).toBeGreaterThan(0)
|
|
199
|
+
// The batch it describes is exactly the batch that landed.
|
|
200
|
+
const landedSeqs = rewrites(records).flatMap(entry => [...entry.sourceSeqs])
|
|
201
|
+
expect(advice[0]!.itemSeqs.every(seq => landedSeqs.includes(seq))).toBe(true)
|
|
202
|
+
|
|
203
|
+
// 3. Every advice record describes a landing, never a pending decision.
|
|
204
|
+
expect(advice.length).toBeLessThanOrEqual(rewrites(records).length)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('keeps the automatic path identical when no legacy gate key is present', async () => {
|
|
208
|
+
const ctx = await runtimeContext()
|
|
209
|
+
await ctx.plugin(TestSettings).await()
|
|
210
|
+
await ctx.plugin(SelectorHost).await()
|
|
211
|
+
await ctx.settings.update(nsBrand(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
|
|
212
|
+
profile: 'tokenpilot-inspired',
|
|
213
|
+
})
|
|
214
|
+
await ctx.plugin(ToolResultPruner, {
|
|
215
|
+
profile: 'tokenpilot-inspired',
|
|
216
|
+
freshTriggerTokens: 200,
|
|
217
|
+
freshTargetTokens: 100,
|
|
218
|
+
aggregateTriggerTokens: 1_000_000,
|
|
219
|
+
aggregateTargetTokens: 900_000,
|
|
220
|
+
historyTriggerTokens: 400,
|
|
221
|
+
historyKeepRecentToolCalls: 0,
|
|
222
|
+
historyKeepRecentTokens: 64_000,
|
|
223
|
+
historyMinReclaimTokens: 1,
|
|
224
|
+
}).await()
|
|
225
|
+
const audit = captureAudit(ctx)
|
|
226
|
+
const session = freshSession(ctx, 'advice-default', HIGH_IMPACT_TEXT)
|
|
227
|
+
|
|
228
|
+
const result = ctx.toolResultPruner.pruneSession(session, { stage: 'fresh', freshTurn: 1, freshStep: 1 })
|
|
229
|
+
expect(result.pruned).toHaveLength(1)
|
|
230
|
+
expect(adviceOf(audit.records())).toHaveLength(1)
|
|
231
|
+
})
|
|
232
|
+
})
|
|
@@ -170,18 +170,22 @@ describe('context-compression audit records', () => {
|
|
|
170
170
|
}).not.toThrow()
|
|
171
171
|
})
|
|
172
172
|
|
|
173
|
-
it('keeps
|
|
173
|
+
it('keeps reduction-advice records free of content, digests, and keys', () => {
|
|
174
174
|
const record: CompressionAuditRecord = {
|
|
175
175
|
schemaVersion: 1,
|
|
176
|
-
kind: '
|
|
177
|
-
sessionId: '
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
receiptStatus: 'applied',
|
|
176
|
+
kind: 'reduction-advice',
|
|
177
|
+
sessionId: 'advice-session',
|
|
178
|
+
profile: 'tokenpilot-inspired',
|
|
179
|
+
band: 'high-impact',
|
|
180
|
+
stage: 'fresh',
|
|
182
181
|
itemSeqs: [7, 9],
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
pricedCandidates: 2,
|
|
183
|
+
maxTokensBefore: 9_200,
|
|
184
|
+
tokensBefore: 12_400,
|
|
185
|
+
tokensAfter: 3_100,
|
|
186
|
+
recoveredTokens: 9_300,
|
|
187
|
+
penaltyTokens: 0,
|
|
188
|
+
paybackTurns: 0,
|
|
185
189
|
turnIndex: 12,
|
|
186
190
|
}
|
|
187
191
|
|
|
@@ -195,23 +199,33 @@ describe('context-compression audit records', () => {
|
|
|
195
199
|
expect(line).not.toContain('apiKey')
|
|
196
200
|
})
|
|
197
201
|
|
|
198
|
-
it('
|
|
202
|
+
it('describes an advised batch without any gate vocabulary', () => {
|
|
203
|
+
// The band can be the model's worst opinion — `not-worth-it` — and the
|
|
204
|
+
// record still only DESCRIBES a batch that landed: the retired review
|
|
205
|
+
// gate's decision vocabulary cannot be expressed any more.
|
|
199
206
|
const record: CompressionAuditRecord = {
|
|
200
207
|
schemaVersion: 1,
|
|
201
|
-
kind: '
|
|
202
|
-
sessionId: '
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
reasonCode: 'review_receipt_digest_invalid',
|
|
208
|
+
kind: 'reduction-advice',
|
|
209
|
+
sessionId: 'advice-session',
|
|
210
|
+
profile: 'tokenpilot-inspired',
|
|
211
|
+
band: 'not-worth-it',
|
|
212
|
+
stage: 'history',
|
|
207
213
|
itemSeqs: [3],
|
|
208
|
-
|
|
209
|
-
|
|
214
|
+
pricedCandidates: 1,
|
|
215
|
+
maxTokensBefore: 5_000,
|
|
216
|
+
tokensBefore: 5_000,
|
|
217
|
+
tokensAfter: 4_000,
|
|
218
|
+
recoveredTokens: 1_000,
|
|
219
|
+
penaltyTokens: 3_600,
|
|
220
|
+
paybackTurns: 36,
|
|
221
|
+
expectedSaving: 0,
|
|
210
222
|
}
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
) as CompressionAuditRecord
|
|
223
|
+
const line = formatCompressionAudit(record)
|
|
224
|
+
const parsed = JSON.parse(line.slice(COMPRESSION_AUDIT_PREFIX.length)) as CompressionAuditRecord
|
|
214
225
|
expect(parsed).toEqual(record)
|
|
226
|
+
for (const gone of ['proposalId', 'proposalKind', 'decision', 'receipt', '"event"', 'reasonCode']) {
|
|
227
|
+
expect(line).not.toContain(gone)
|
|
228
|
+
}
|
|
215
229
|
})
|
|
216
230
|
|
|
217
231
|
it('keeps advisor-outcome records free of prompts, keys, and content', () => {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upgrade safety for the retired review gate's settings keys.
|
|
3
|
+
*
|
|
4
|
+
* The gate is gone, but its keys are PERSISTED: a live settings document still
|
|
5
|
+
* carries `reviewMode: false`, and both the runtime parser and the browser
|
|
6
|
+
* decoder reject unknown keys. Removing the keys from the accepted set would
|
|
7
|
+
* therefore make the plugin fail to load (runtime) or report the whole settings
|
|
8
|
+
* card as unreadable (client). This spec pins the accept-and-ignore contract in
|
|
9
|
+
* both directions, and pins that the keys never reach the resolved policy.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, expect, it } from 'vitest'
|
|
12
|
+
import {
|
|
13
|
+
parsePresetOptionsSettings,
|
|
14
|
+
resolveConfig,
|
|
15
|
+
resolvePolicy,
|
|
16
|
+
} from '../../src/runtime/config.ts'
|
|
17
|
+
import { decodePresetOptionsSettings } from '../../src/profiles.ts'
|
|
18
|
+
|
|
19
|
+
/** The exact legacy section a pre-0.5.2 install has on disk. */
|
|
20
|
+
const LEGACY_SECTION = {
|
|
21
|
+
estimatorMode: 'host',
|
|
22
|
+
estimatorProvider: 'local-35b',
|
|
23
|
+
estimatorModel: 'Qwen3.6-35B-A3B',
|
|
24
|
+
estimatorBaseUrl: 'http://192.168.100.242:8200/v1',
|
|
25
|
+
reviewMode: false,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('retired review-gate keys are accepted and ignored (runtime)', () => {
|
|
29
|
+
it('parses the legacy section without throwing and keeps the live keys', () => {
|
|
30
|
+
const parsed = parsePresetOptionsSettings(LEGACY_SECTION)
|
|
31
|
+
expect(parsed).toEqual({
|
|
32
|
+
estimatorMode: 'host',
|
|
33
|
+
estimatorProvider: 'local-35b',
|
|
34
|
+
estimatorModel: 'Qwen3.6-35B-A3B',
|
|
35
|
+
estimatorBaseUrl: 'http://192.168.100.242:8200/v1',
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('drops every retired key, even with values the old schema rejected', () => {
|
|
40
|
+
// The retired keys are not validated any more — a stale invalid value in an
|
|
41
|
+
// existing document must not be able to break the load.
|
|
42
|
+
const parsed = parsePresetOptionsSettings({
|
|
43
|
+
reviewMode: 'yes',
|
|
44
|
+
reviewTimeoutTurns: -1,
|
|
45
|
+
cacheHitDiscountAlpha: 'wide',
|
|
46
|
+
reviewHighImpactTokens: null,
|
|
47
|
+
})
|
|
48
|
+
expect(parsed).toEqual({})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('still rejects a genuinely unknown key', () => {
|
|
52
|
+
expect(() => parsePresetOptionsSettings({ reviewModes: true })).toThrow(/unknown key/)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('never lets a retired key reach the resolved policy matrix', () => {
|
|
56
|
+
// The section arrives as `unknown` from the settings document, so the cast
|
|
57
|
+
// mirrors the production path rather than widening the config surface.
|
|
58
|
+
const policy = resolvePolicy(
|
|
59
|
+
resolveConfig({
|
|
60
|
+
presetOptions: { reviewMode: true, reviewHighImpactTokens: 1 },
|
|
61
|
+
} as unknown as Parameters<typeof resolveConfig>[0]),
|
|
62
|
+
'tokenpilot-inspired',
|
|
63
|
+
)
|
|
64
|
+
const matrix = policy.presetOptions
|
|
65
|
+
expect(matrix).toBeDefined()
|
|
66
|
+
for (const gone of ['reviewMode', 'reviewTimeoutTurns', 'cacheHitDiscountAlpha', 'reviewHighImpactTokens']) {
|
|
67
|
+
expect(Object.hasOwn(matrix as object, gone)).toBe(false)
|
|
68
|
+
}
|
|
69
|
+
// The surviving matrix is exactly the live capability set.
|
|
70
|
+
expect(Object.keys(matrix as object).sort()).toEqual([
|
|
71
|
+
'advisor', 'dedupeToolResults', 'estimator', 'noNetSavingsGuard',
|
|
72
|
+
'prefixStabilizer', 'readState', 'skipReductionRecovery', 'summaryLocator',
|
|
73
|
+
])
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('retired review-gate keys are accepted and ignored (client mirror)', () => {
|
|
78
|
+
it('decodes a legacy section instead of reporting the card unreadable', () => {
|
|
79
|
+
const decoded = decodePresetOptionsSettings(LEGACY_SECTION)
|
|
80
|
+
// `undefined` here would blank the settings card's presetOptions section.
|
|
81
|
+
expect(decoded).toEqual({
|
|
82
|
+
estimatorMode: 'host',
|
|
83
|
+
estimatorProvider: 'local-35b',
|
|
84
|
+
estimatorModel: 'Qwen3.6-35B-A3B',
|
|
85
|
+
estimatorBaseUrl: 'http://192.168.100.242:8200/v1',
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('tolerates legacy values the old decoder rejected', () => {
|
|
90
|
+
expect(decodePresetOptionsSettings({ reviewMode: 'yes', reviewHighImpactTokens: -5 })).toEqual({})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('still refuses an unknown key', () => {
|
|
94
|
+
expect(decodePresetOptionsSettings({ reviewModes: true })).toBeUndefined()
|
|
95
|
+
})
|
|
96
|
+
})
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
adviseCandidates,
|
|
4
|
+
computeBenefit,
|
|
5
|
+
DEFAULT_ADVICE_ALPHA,
|
|
6
|
+
DEFAULT_ADVICE_HIGH_IMPACT_TOKENS,
|
|
7
|
+
} from '../../../src/runtime/tokenpilot/benefit.ts'
|
|
8
|
+
|
|
9
|
+
/** Reference values follow the spec formulas exactly:
|
|
10
|
+
* R = Σ(before − after); penalty = (1−α)·tail; payback = penalty / (α·R);
|
|
11
|
+
* expectedSaving = α·R·max(0, Ŝ − payback). */
|
|
12
|
+
|
|
13
|
+
describe('computeBenefit', () => {
|
|
14
|
+
it('returns zero recovery and no payback for an empty batch', () => {
|
|
15
|
+
const result = computeBenefit([], { alpha: 0.1, tailTokens: 4000 })
|
|
16
|
+
expect(result.recoveredTokens).toBe(0)
|
|
17
|
+
expect(result.penaltyTokens).toBe(3600)
|
|
18
|
+
expect(result.paybackTurns).toBeUndefined()
|
|
19
|
+
expect(result.expectedSaving).toBeUndefined()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('computes the single-candidate benefit with a known remaining-turn estimate', () => {
|
|
23
|
+
// R = 4000 − 400 = 3600; penalty = 0.9·4000 = 3600; per-turn = 0.1·3600 = 360
|
|
24
|
+
const result = computeBenefit(
|
|
25
|
+
[{ sourceSeq: 7, tokensBefore: 4000, tokensAfter: 400 }],
|
|
26
|
+
{ alpha: 0.1, tailTokens: 4000, remainingTurns: 12 },
|
|
27
|
+
)
|
|
28
|
+
expect(result.recoveredTokens).toBe(3600)
|
|
29
|
+
expect(result.penaltyTokens).toBeCloseTo(3600)
|
|
30
|
+
expect(result.paybackTurns).toBeCloseTo(10)
|
|
31
|
+
// α·R·max(0, 12 − 10) = 360·2
|
|
32
|
+
expect(result.expectedSaving).toBeCloseTo(720)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('omits expectedSaving while Ŝ is unknown', () => {
|
|
36
|
+
const result = computeBenefit(
|
|
37
|
+
[{ sourceSeq: 7, tokensBefore: 4000, tokensAfter: 400 }],
|
|
38
|
+
{ alpha: 0.1, tailTokens: 4000 },
|
|
39
|
+
)
|
|
40
|
+
expect(result.paybackTurns).toBeCloseTo(10)
|
|
41
|
+
expect(result.expectedSaving).toBeUndefined()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('merges candidates into one batch so the penalty is paid once', () => {
|
|
45
|
+
const result = computeBenefit(
|
|
46
|
+
[
|
|
47
|
+
{ sourceSeq: 1, tokensBefore: 2000, tokensAfter: 500 },
|
|
48
|
+
{ sourceSeq: 2, tokensBefore: 3000, tokensAfter: 1000 },
|
|
49
|
+
],
|
|
50
|
+
{ alpha: 0.1, tailTokens: 4000, remainingTurns: 20 },
|
|
51
|
+
)
|
|
52
|
+
expect(result.recoveredTokens).toBe(3500)
|
|
53
|
+
expect(result.penaltyTokens).toBeCloseTo(3600)
|
|
54
|
+
expect(result.paybackTurns).toBeCloseTo(3600 / 350)
|
|
55
|
+
expect(result.expectedSaving).toBeCloseTo(350 * (20 - 3600 / 350))
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('clamps a growing candidate to zero recovery instead of negative batch credit', () => {
|
|
59
|
+
const result = computeBenefit(
|
|
60
|
+
[{ sourceSeq: 3, tokensBefore: 100, tokensAfter: 400 }],
|
|
61
|
+
{ alpha: 0.1, tailTokens: 4000 },
|
|
62
|
+
)
|
|
63
|
+
expect(result.recoveredTokens).toBe(0)
|
|
64
|
+
expect(result.paybackTurns).toBeUndefined()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('stays finite as α → 0: no division, payback undefined, expectedSaving negative', () => {
|
|
68
|
+
const result = computeBenefit(
|
|
69
|
+
[{ sourceSeq: 4, tokensBefore: 4000, tokensAfter: 400 }],
|
|
70
|
+
{ alpha: 0, tailTokens: 4000, remainingTurns: 12 },
|
|
71
|
+
)
|
|
72
|
+
expect(result.recoveredTokens).toBe(3600)
|
|
73
|
+
expect(result.paybackTurns).toBeUndefined()
|
|
74
|
+
// With zero per-turn saving the batch can only lose the refill penalty.
|
|
75
|
+
expect(result.expectedSaving).toBeCloseTo(-4000)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('never reports a positive expectedSaving once Ŝ is inside the payback window', () => {
|
|
79
|
+
// payback = 10 turns; Ŝ = 5 → max(0, Ŝ − payback) = 0
|
|
80
|
+
const result = computeBenefit(
|
|
81
|
+
[{ sourceSeq: 7, tokensBefore: 4000, tokensAfter: 400 }],
|
|
82
|
+
{ alpha: 0.1, tailTokens: 4000, remainingTurns: 5 },
|
|
83
|
+
)
|
|
84
|
+
expect(result.expectedSaving).toBeCloseTo(0)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('waives the refill penalty for a refill-exempt batch: payback 0, saving α·R·Ŝ', () => {
|
|
88
|
+
// Fresh-stage shaping: the content never entered the KV cache, so no
|
|
89
|
+
// refill penalty applies — the whole discounted recovery is pure gain.
|
|
90
|
+
const result = computeBenefit(
|
|
91
|
+
[{ sourceSeq: 7, tokensBefore: 4000, tokensAfter: 400 }],
|
|
92
|
+
{ alpha: 0.1, tailTokens: 4000, remainingTurns: 12, refillPenaltyExempt: true },
|
|
93
|
+
)
|
|
94
|
+
expect(result.recoveredTokens).toBe(3600)
|
|
95
|
+
expect(result.penaltyTokens).toBe(0)
|
|
96
|
+
expect(result.paybackTurns).toBe(0)
|
|
97
|
+
// α·R·max(0, Ŝ − 0) = 360·12
|
|
98
|
+
expect(result.expectedSaving).toBeCloseTo(4320)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('keeps the refill penalty when the exemption flag is absent', () => {
|
|
102
|
+
const result = computeBenefit(
|
|
103
|
+
[{ sourceSeq: 7, tokensBefore: 4000, tokensAfter: 400 }],
|
|
104
|
+
{ alpha: 0.1, tailTokens: 4000 },
|
|
105
|
+
)
|
|
106
|
+
expect(result.penaltyTokens).toBeCloseTo(3600)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
/** The shipped advice defaults are part of the contract: the audit and the
|
|
111
|
+
* report quote them, so a silent drift would change what the label means. */
|
|
112
|
+
describe('advice defaults', () => {
|
|
113
|
+
it('pins the shipped α and high-impact threshold', () => {
|
|
114
|
+
expect(DEFAULT_ADVICE_ALPHA).toBe(0.1)
|
|
115
|
+
expect(DEFAULT_ADVICE_HIGH_IMPACT_TOKENS).toBe(4_000)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
describe('adviseCandidates', () => {
|
|
120
|
+
// The shipped threshold is exercised by its own case; every other case lifts
|
|
121
|
+
// it out of the way so one band can be asserted at a time.
|
|
122
|
+
const base = {
|
|
123
|
+
alpha: 0.1,
|
|
124
|
+
tailTokens: 4000,
|
|
125
|
+
highImpactTokens: 1_000_000,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
it('returns undefined when no candidate carries a positive recovery', () => {
|
|
129
|
+
expect(adviseCandidates([], base)).toBeUndefined()
|
|
130
|
+
expect(adviseCandidates([{ sourceSeq: 1, tokensBefore: 100, tokensAfter: 400 }], base)).toBeUndefined()
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('labels a fast-payback batch profitable', () => {
|
|
134
|
+
// R = 3600, penalty = 3600, α·R = 360 → payback 10 ≤ 0.25·Ŝ requires Ŝ ≥ 40.
|
|
135
|
+
const advice = adviseCandidates(
|
|
136
|
+
[{ sourceSeq: 7, tokensBefore: 4_000, tokensAfter: 400 }],
|
|
137
|
+
{ ...base, remainingTurns: 60 },
|
|
138
|
+
)
|
|
139
|
+
expect(advice?.band).toBe('profitable')
|
|
140
|
+
expect(advice?.priced).toBe(1)
|
|
141
|
+
expect(advice?.maxTokensBefore).toBe(4_000)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('labels an unpriceable batch when α prices no recovery', () => {
|
|
145
|
+
const advice = adviseCandidates(
|
|
146
|
+
[{ sourceSeq: 7, tokensBefore: 4_000, tokensAfter: 400 }],
|
|
147
|
+
{ ...base, alpha: 0, remainingTurns: 12 },
|
|
148
|
+
)
|
|
149
|
+
expect(advice?.band).toBe('unpriceable')
|
|
150
|
+
expect(advice?.benefit.expectedSaving).toBeCloseTo(-4000)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('lets one high-impact candidate label the whole batch, ahead of a good payback', () => {
|
|
154
|
+
// The 8k candidate alone reaches the shipped threshold; the small sibling
|
|
155
|
+
// would be profitable on its own. One label per batch, high impact wins.
|
|
156
|
+
const advice = adviseCandidates(
|
|
157
|
+
[
|
|
158
|
+
{ sourceSeq: 1, tokensBefore: 200, tokensAfter: 100 },
|
|
159
|
+
{ sourceSeq: 2, tokensBefore: 8_192, tokensAfter: 1_000 },
|
|
160
|
+
],
|
|
161
|
+
{ ...base, highImpactTokens: DEFAULT_ADVICE_HIGH_IMPACT_TOKENS, alpha: 1, remainingTurns: 60 },
|
|
162
|
+
)
|
|
163
|
+
expect(advice?.band).toBe('high-impact')
|
|
164
|
+
expect(advice?.maxTokensBefore).toBe(8_192)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('labels a slowly paying batch slow-payback when Ŝ is known', () => {
|
|
168
|
+
// R = 12000 → penalty 3600, α·R = 1200 → payback 3. Ŝ = 11 keeps
|
|
169
|
+
// 0.25·Ŝ = 2.75 strictly below the payback, so the batch is not profitable.
|
|
170
|
+
const advice = adviseCandidates(
|
|
171
|
+
[{ sourceSeq: 7, tokensBefore: 12_000, tokensAfter: 0 }],
|
|
172
|
+
{ ...base, remainingTurns: 11 },
|
|
173
|
+
)
|
|
174
|
+
expect(advice?.band).toBe('slow-payback')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('labels a batch that never pays back not-worth-it, and still returns its benefit', () => {
|
|
178
|
+
const advice = adviseCandidates(
|
|
179
|
+
[{ sourceSeq: 7, tokensBefore: 4_000, tokensAfter: 400 }],
|
|
180
|
+
{ ...base, remainingTurns: 2 },
|
|
181
|
+
)
|
|
182
|
+
expect(advice?.band).toBe('not-worth-it')
|
|
183
|
+
expect(advice?.benefit.recoveredTokens).toBe(3600)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('never closes a band on Ŝ: an unknown remaining-turn count keeps the batch priceable', () => {
|
|
187
|
+
const advice = adviseCandidates(
|
|
188
|
+
[{ sourceSeq: 7, tokensBefore: 4_000, tokensAfter: 400 }],
|
|
189
|
+
base,
|
|
190
|
+
)
|
|
191
|
+
// payback 10 > 1 and Ŝ is unknown → the profit bands cannot be argued, so
|
|
192
|
+
// the batch is labelled as not worth the cache break rather than fabricated.
|
|
193
|
+
expect(advice?.band).toBe('not-worth-it')
|
|
194
|
+
expect(advice?.benefit.expectedSaving).toBeUndefined()
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('prices a fresh-stage batch without the refill penalty', () => {
|
|
198
|
+
const advice = adviseCandidates(
|
|
199
|
+
[{ sourceSeq: 7, tokensBefore: 4_000, tokensAfter: 400 }],
|
|
200
|
+
{ ...base, highImpactTokens: 1_000_000, stage: 'fresh', remainingTurns: 12 },
|
|
201
|
+
)
|
|
202
|
+
expect(advice?.benefit.penaltyTokens).toBe(0)
|
|
203
|
+
expect(advice?.band).toBe('profitable')
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('counts only the positively-recovering candidates it priced', () => {
|
|
207
|
+
const advice = adviseCandidates(
|
|
208
|
+
[
|
|
209
|
+
{ sourceSeq: 1, tokensBefore: 100, tokensAfter: 400 },
|
|
210
|
+
{ sourceSeq: 2, tokensBefore: 4_000, tokensAfter: 400 },
|
|
211
|
+
],
|
|
212
|
+
{ ...base, remainingTurns: 60 },
|
|
213
|
+
)
|
|
214
|
+
expect(advice?.priced).toBe(1)
|
|
215
|
+
expect(advice?.maxTokensBefore).toBe(4_000)
|
|
216
|
+
})
|
|
217
|
+
})
|
|
@@ -61,15 +61,14 @@ describe('tokenpilot-inspired preset', () => {
|
|
|
61
61
|
prefixStabilizer: true,
|
|
62
62
|
readState: true,
|
|
63
63
|
estimator: { mode: '' },
|
|
64
|
-
// Review pipeline (beta) ships off with its documented defaults.
|
|
65
|
-
reviewMode: false,
|
|
66
|
-
reviewTimeoutTurns: 6,
|
|
67
|
-
cacheHitDiscountAlpha: 0.1,
|
|
68
|
-
reviewHighImpactTokens: 4000,
|
|
69
64
|
// Advisory advisor ships off with its documented defaults. The
|
|
70
65
|
// resolved matrix intentionally gains this group so it follows the
|
|
71
66
|
// estimator's resolved-consumption pattern; see
|
|
72
67
|
// .agents/plans/ctx-relevance-advisor/spec.md decision record.
|
|
68
|
+
//
|
|
69
|
+
// The retired review gate's four keys are intentionally absent here:
|
|
70
|
+
// they are still ACCEPTED by the parser for upgrade safety but never
|
|
71
|
+
// reach the resolved policy (a reduction must not be gated).
|
|
73
72
|
advisor: {
|
|
74
73
|
mode: '',
|
|
75
74
|
timeoutMs: 8000,
|