dsh-context-compression-improved 0.5.1 → 0.5.3
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/CHANGELOG.ja.md +144 -83
- package/CHANGELOG.ko.md +143 -82
- package/CHANGELOG.md +278 -212
- package/CHANGELOG.zh.md +131 -77
- package/docs/installation.md +103 -103
- package/docs/installation.zh.md +100 -100
- package/package.json +1 -1
- package/packages/selector/cordis.patch.yml +5 -6
- 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 +16 -234
- package/packages/selector/lib/pruner.d.ts +13 -248
- package/packages/selector/lib/pruner.js +148 -552
- package/packages/selector/src/client/EstimatorControls.tsx +0 -101
- package/packages/selector/src/client/index.ts +0 -17
- package/packages/selector/src/client/locales.ts +0 -38
- 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 +24 -271
- package/packages/selector/src/profiles.ts +4 -27
- package/packages/selector/src/pruner/state.ts +2 -25
- package/packages/selector/src/pruner.ts +75 -403
- 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-prompt.ts +188 -188
- package/packages/selector/src/runtime/tokenpilot/advisor-state.ts +149 -133
- package/packages/selector/src/runtime/tokenpilot/advisor.ts +419 -419
- package/packages/selector/src/runtime/tokenpilot/benefit.ts +200 -0
- package/packages/selector/src/runtime/types.ts +0 -17
- package/packages/selector/tests/advisor-report.host.spec.ts +223 -223
- package/packages/selector/tests/preset-options-write.client.spec.ts +7 -23
- package/packages/selector/tests/public/package-contract.client.spec.ts +20 -0
- package/packages/selector/tests/runtime/advice-never-withholds.host.spec.ts +232 -0
- package/packages/selector/tests/runtime/advisor-invariant.spec.ts +272 -272
- package/packages/selector/tests/runtime/advisor.spec.ts +226 -226
- package/packages/selector/tests/runtime/audit.spec.ts +35 -21
- package/packages/selector/tests/runtime/char-basis.spec.ts +30 -30
- 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 +45 -14
- package/scripts/toolclass-corpus-replay.mjs +281 -281
- 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
|
+
})
|