dsh-context-compression-improved 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/.gitattributes +1 -0
  2. package/.github/workflows/ci.yml +39 -0
  3. package/CHANGELOG.ja.md +39 -0
  4. package/CHANGELOG.ko.md +39 -0
  5. package/CHANGELOG.md +135 -0
  6. package/CHANGELOG.zh.md +39 -0
  7. package/CONTRIBUTING.md +22 -0
  8. package/README.ja.md +104 -0
  9. package/README.ko.md +103 -0
  10. package/README.md +89 -12
  11. package/README.zh.md +87 -12
  12. package/SECURITY.md +18 -0
  13. package/THIRD_PARTY_NOTICES.md +7 -31
  14. package/docs/installation.ja.md +76 -0
  15. package/docs/installation.ko.md +76 -0
  16. package/docs/installation.md +76 -0
  17. package/docs/installation.zh.md +76 -0
  18. package/docs/repair-log.md +582 -0
  19. package/eslint.config.js +30 -0
  20. package/package.json +84 -81
  21. package/packages/selector/LICENSE +21 -0
  22. package/packages/selector/README.md +26 -0
  23. package/packages/selector/README.zh.md +26 -0
  24. package/packages/selector/THIRD_PARTY_NOTICES.md +38 -0
  25. package/packages/selector/docs/history-tool-call-working-set-spec.md +112 -0
  26. package/packages/selector/docs/native-tool-result-selector-spec.md +34 -0
  27. package/packages/selector/docs/subagent-cache-reuse-spec.md +46 -0
  28. package/packages/selector/lib/style.css +308 -0
  29. package/packages/selector/package.json +115 -0
  30. package/{screenshots.json → packages/selector/screenshots.json} +6 -6
  31. package/packages/selector/src/client/CompressionProfileControls.tsx +229 -0
  32. package/packages/selector/src/client/CompressionProfileSelector.module.css +170 -0
  33. package/packages/selector/src/client/CompressionProfileSelector.tsx +79 -0
  34. package/packages/selector/src/client/CustomPolicyEditor.tsx +216 -0
  35. package/packages/selector/src/client/EstimatorControls.tsx +281 -0
  36. package/packages/selector/src/client/decode.ts +49 -0
  37. package/packages/selector/src/client/index.ts +111 -0
  38. package/packages/selector/src/client/locales.ts +198 -0
  39. package/packages/selector/src/client/preset-options.ts +70 -0
  40. package/packages/selector/src/client/settings-section.tsx +126 -0
  41. package/packages/selector/src/css-modules.d.ts +6 -0
  42. package/packages/selector/src/deepseek-v4-tokenizer.ts +210 -0
  43. package/packages/selector/src/estimator-catalog.ts +104 -0
  44. package/packages/selector/src/index.ts +327 -0
  45. package/packages/selector/src/invariant.ts +113 -0
  46. package/packages/selector/src/preset-overlay.ts +567 -0
  47. package/packages/selector/src/profiles.ts +342 -0
  48. package/packages/selector/src/pruner/content.ts +188 -0
  49. package/packages/selector/src/pruner/session.ts +94 -0
  50. package/packages/selector/src/pruner/state.ts +43 -0
  51. package/packages/selector/src/pruner/tuning.ts +23 -0
  52. package/packages/selector/src/pruner/types.ts +60 -0
  53. package/packages/selector/src/pruner.ts +2144 -0
  54. package/packages/selector/src/runtime/adaptive-cost.ts +194 -0
  55. package/packages/selector/src/runtime/audit.ts +215 -0
  56. package/packages/selector/src/runtime/config.ts +613 -0
  57. package/packages/selector/src/runtime/custom-policy.ts +278 -0
  58. package/packages/selector/src/runtime/deepseek-official-pricing.ts +298 -0
  59. package/packages/selector/src/runtime/deepseek-v4-vision-tokens.ts +254 -0
  60. package/packages/selector/src/runtime/measurement.ts +403 -0
  61. package/packages/selector/src/runtime/reducers.ts +656 -0
  62. package/packages/selector/src/runtime/retrieve.ts +457 -0
  63. package/packages/selector/src/runtime/session-events.ts +17 -0
  64. package/packages/selector/src/runtime/tail-trim.ts +166 -0
  65. package/packages/selector/src/runtime/token-count.ts +72 -0
  66. package/packages/selector/src/runtime/tokenpilot/dedup.ts +81 -0
  67. package/packages/selector/src/runtime/tokenpilot/estimator.ts +183 -0
  68. package/packages/selector/src/runtime/tokenpilot/locator.ts +128 -0
  69. package/packages/selector/src/runtime/tokenpilot/read-state.ts +77 -0
  70. package/packages/selector/src/runtime/types.ts +309 -0
  71. package/packages/selector/src/runtime/value.ts +48 -0
  72. package/packages/selector/tests/auto-compact.client.spec.tsx +226 -0
  73. package/packages/selector/tests/built/client-artifact.spec.ts +51 -0
  74. package/packages/selector/tests/cache-prefix-audit.spec.ts +123 -0
  75. package/packages/selector/tests/code-skeleton.client.spec.ts +88 -0
  76. package/packages/selector/tests/custom-contract.client.spec.ts +202 -0
  77. package/packages/selector/tests/estimator-catalog.spec.ts +70 -0
  78. package/packages/selector/tests/estimator-channel.client.spec.tsx +247 -0
  79. package/packages/selector/tests/estimator-route-registration.host.spec.ts +176 -0
  80. package/packages/selector/tests/host-preset-overlay.host.spec.ts +204 -0
  81. package/packages/selector/tests/preset-options-write.client.spec.ts +181 -0
  82. package/packages/selector/tests/preset-overlay-loader.e2e.host.spec.ts +196 -0
  83. package/packages/selector/tests/preset-overlay.host.spec.ts +243 -0
  84. package/packages/selector/tests/profiles.client.spec.tsx +434 -0
  85. package/packages/selector/tests/public/package-contract.client.spec.ts +33 -0
  86. package/packages/selector/tests/runtime/adaptive-cost.spec.ts +167 -0
  87. package/packages/selector/tests/runtime/audit.spec.ts +129 -0
  88. package/packages/selector/tests/runtime/auto-compact-config.spec.ts +523 -0
  89. package/packages/selector/tests/runtime/code-skeleton.spec.ts +141 -0
  90. package/packages/selector/tests/runtime/deepseek-official-pricing.spec.ts +186 -0
  91. package/packages/selector/tests/runtime/deepseek-v4-tokenizer.spec.ts +122 -0
  92. package/packages/selector/tests/runtime/deepseek-v4-vision-tokens.spec.ts +122 -0
  93. package/packages/selector/tests/runtime/fixtures/profile-baseline.json +273 -0
  94. package/packages/selector/tests/runtime/fixtures/tokenizer-golden.json +106 -0
  95. package/packages/selector/tests/runtime/fixtures/vision-golden.json +459 -0
  96. package/packages/selector/tests/runtime/public/public-runtime.spec.ts +2531 -0
  97. package/packages/selector/tests/runtime/session-events.spec.ts +27 -0
  98. package/packages/selector/tests/runtime/tokenizer-golden.spec.ts +53 -0
  99. package/packages/selector/tests/runtime/tokenpilot/dedup.spec.ts +52 -0
  100. package/packages/selector/tests/runtime/tokenpilot/estimator.spec.ts +56 -0
  101. package/packages/selector/tests/runtime/tokenpilot/locator.spec.ts +76 -0
  102. package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +100 -0
  103. package/packages/selector/tests/runtime/tokenpilot/read-state.spec.ts +58 -0
  104. package/packages/selector/tests/runtime/value.spec.ts +23 -0
  105. package/packages/selector/tests/standing-generation.host.spec.ts +631 -0
  106. package/packages/selector/tests/subagent-cache-reuse.host.spec.ts +250 -0
  107. package/packages/selector/tests/support/cache-prefix-audit.ts +105 -0
  108. package/packages/selector/tests/support/mock-adapter.ts +37 -0
  109. package/packages/selector/tests/support/ui-primitives.tsx +34 -0
  110. package/packages/selector/tsconfig.json +11 -0
  111. package/packages/selector/tsdown.client.config.ts +102 -0
  112. package/packages/selector/tsdown.config.ts +20 -0
  113. package/pnpm-workspace.yaml +19 -0
  114. package/scripts/capture-profile-baseline.ts +80 -0
  115. package/scripts/generate-tokenizer-fixtures.py +81 -0
  116. package/scripts/generate-vision-fixtures.py +208 -0
  117. package/scripts/packed-components-smoke.ts +713 -0
  118. package/scripts/packed-install-e2e.ts +1072 -0
  119. package/scripts/verify-release.ts +300 -0
  120. package/tests/TEST_INVENTORY.md +42 -0
  121. package/tsconfig.base.json +18 -0
  122. package/tsconfig.json +7 -0
  123. package/tsconfig.scripts.json +13 -0
  124. package/tsconfig.tests.json +15 -0
  125. package/vitest.built.config.ts +9 -0
  126. package/vitest.config.ts +43 -0
  127. /package/{assets → packages/selector/assets}/deepseek-v4/LICENSE.DeepSeek-V4-Pro.txt +0 -0
  128. /package/{assets → packages/selector/assets}/deepseek-v4/manifest.json +0 -0
  129. /package/{assets → packages/selector/assets}/deepseek-v4/tokenizer.json +0 -0
  130. /package/{assets → packages/selector/assets}/deepseek-v4/tokenizer_config.json +0 -0
  131. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/LICENSE.DeepSeek-V4-Flash-Vision-Exp.txt +0 -0
  132. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/manifest.json +0 -0
  133. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/tokenizer.json +0 -0
  134. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/tokenizer_config.json +0 -0
  135. /package/{assets → packages/selector/assets}/screenshots/context-compression-selector-profiles.jpg +0 -0
  136. /package/{assets → packages/selector/assets}/screenshots/context-compression-selector-settings.png +0 -0
  137. /package/{cordis.patch.yml → packages/selector/cordis.patch.yml} +0 -0
  138. /package/{dsh.plugin.json → packages/selector/dsh.plugin.json} +0 -0
  139. /package/{lib → packages/selector/lib}/client.d.ts +0 -0
  140. /package/{lib → packages/selector/lib}/client.js +0 -0
  141. /package/{lib → packages/selector/lib}/config.js +0 -0
  142. /package/{lib → packages/selector/lib}/index.d.ts +0 -0
  143. /package/{lib → packages/selector/lib}/index.js +0 -0
  144. /package/{lib → packages/selector/lib}/invariant.d.ts +0 -0
  145. /package/{lib → packages/selector/lib}/invariant.js +0 -0
  146. /package/{lib → packages/selector/lib}/pruner.d.ts +0 -0
  147. /package/{lib → packages/selector/lib}/pruner.js +0 -0
  148. /package/{lib → packages/selector/lib}/tail-trim.js +0 -0
@@ -0,0 +1,250 @@
1
+ import { afterEach, describe, expect, it } from 'vitest'
2
+ import { Context } from '@deepseek-ai/cordis'
3
+ import AgentLoop from '@deepseek-ai/dsh-agent-loop'
4
+ import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
5
+ import { createUserMessage, type GenerateOptions } from '@deepseek-ai/dsh-llm'
6
+ import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
7
+ import { SessionId } from '@deepseek-ai/dsh-session'
8
+ import SubagentRuntime from '@deepseek-ai/dsh-subagent'
9
+ import * as Fork from '@deepseek-ai/dsh-subagent-fork-in-process'
10
+ import * as Spawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
11
+ import TokenMeter from '@deepseek-ai/dsh-token-meter'
12
+ import { PublicMockAdapter, publicTextResponse } from './support/mock-adapter.ts'
13
+ import {
14
+ assessReusablePrefix,
15
+ buildSafeCacheAuditRecord,
16
+ fingerprintStablePrefix,
17
+ type JsonValue,
18
+ type StablePrefixEnvelope,
19
+ } from './support/cache-prefix-audit.js'
20
+
21
+ const SYSTEM = 'You are a terse assistant in an isolated parent-child cache-prefix test. '
22
+ + 'Follow the latest user instruction literally. Answer in one short sentence. '
23
+ + 'Do not use markdown, tools, timestamps, random identifiers, environment values, '
24
+ + 'or unstated facts. This deliberately long stable persona supplies enough repeated '
25
+ + 'context for the provider cache while remaining identical for parent and child requests.'
26
+
27
+ const PARENT_TEXT = 'Remember that the stable test phrase is cobalt-heron-42 and repeat it once.'
28
+ const CHILD_TEXT = 'Repeat the stable test phrase once more.'
29
+ const SPAWN_TEXT = 'Answer only with the word fresh.'
30
+
31
+ let ctx: Context | undefined
32
+
33
+ afterEach(async () => {
34
+ await ctx?.fiber.dispose()
35
+ ctx = undefined
36
+ })
37
+
38
+ const jsonValue = (value: unknown): JsonValue => JSON.parse(JSON.stringify(value)) as JsonValue
39
+
40
+ const envelope = (options: GenerateOptions, messageCount = options.messages.length): StablePrefixEnvelope => ({
41
+ provider: options.provider,
42
+ model: options.model,
43
+ system: options.system ?? null,
44
+ tools: jsonValue(options.tools ?? []) as JsonValue[],
45
+ messages: jsonValue(options.messages.slice(0, messageCount)) as JsonValue[],
46
+ })
47
+
48
+ async function liveHarness(requests: Map<string, GenerateOptions[]>): Promise<Context> {
49
+ const created = new Context()
50
+ await mountAgentLoopTestDependencies(created, { systemPrompt: { persona: SYSTEM } })
51
+ await created.plugin(TokenMeter)
52
+ await created.plugin(AgentLoop, { agents: [] })
53
+ await created.plugin(SubagentRuntime)
54
+ await created.plugin(Fork, { providerName: 'fork' })
55
+ await created.plugin(Spawn, { providerName: 'spawn' })
56
+ await created.plugin(LlmDeepSeek)
57
+ created.on('llm/stream', (options, next) => {
58
+ const sessionId = String(options.sessionId)
59
+ requests.set(sessionId, [...(requests.get(sessionId) ?? []), options])
60
+ return next()
61
+ })
62
+ return created
63
+ }
64
+
65
+ async function keylessHarness(requests: Map<string, GenerateOptions[]>): Promise<Context> {
66
+ const created = new Context()
67
+ await mountAgentLoopTestDependencies(created, { systemPrompt: { persona: SYSTEM } })
68
+ await created.plugin(AgentLoop, { agents: [] })
69
+ await created.plugin(SubagentRuntime)
70
+ await created.plugin(Fork, { providerName: 'fork' })
71
+ await created.plugin(Spawn, { providerName: 'spawn' })
72
+ created.llm.registerAdapter(['mock'], new PublicMockAdapter([
73
+ publicTextResponse('cobalt-heron-42'),
74
+ publicTextResponse('cobalt-heron-42'),
75
+ publicTextResponse('fresh'),
76
+ ]))
77
+ created.on('llm/stream', (options, next) => {
78
+ const sessionId = String(options.sessionId)
79
+ requests.set(sessionId, [...(requests.get(sessionId) ?? []), options])
80
+ return next()
81
+ })
82
+ return created
83
+ }
84
+
85
+ describe('parent/child prefix behavior (keyless full-loop E2E)', () => {
86
+ it('runs parent, fork, and spawn through the real loop and session providers', async () => {
87
+ const requests = new Map<string, GenerateOptions[]>()
88
+ ctx = await keylessHarness(requests)
89
+ const parent = ctx.agentLoop.create(SessionId('cache-parent-keyless-e2e'), {
90
+ provider: 'mock',
91
+ model: 'mock',
92
+ })
93
+ parent.followup(createUserMessage({
94
+ content: [{ type: 'text', text: PARENT_TEXT }],
95
+ source: { kind: 'user' },
96
+ }))
97
+ await parent.whenIdle()
98
+ const parentEvents = parent.session.events.slice()
99
+ const parentRequest = requests.get(String(parent.id))?.at(-1)
100
+ expect(parentRequest).toBeDefined()
101
+
102
+ const forkRun = await ctx.subagents.start('fork', {
103
+ prompt: [{ type: 'text', text: CHILD_TEXT }],
104
+ parent,
105
+ signal: new AbortController().signal,
106
+ })
107
+ await forkRun.result
108
+ const forkChild = forkRun.localAgent
109
+ const forkRequest = requests.get(String(forkRun.id))?.at(-1)
110
+ expect(forkChild).toBeDefined()
111
+ expect(forkRequest).toBeDefined()
112
+ if (forkChild === undefined || forkRequest === undefined || parentRequest === undefined) {
113
+ throw new Error('keyless fork did not publish its complete evidence')
114
+ }
115
+ expect(forkChild.session.header.seedLength).toBe(parentEvents.length)
116
+ expect(forkChild.session.events.slice(0, parentEvents.length)).toEqual(parentEvents)
117
+ const parentStable = envelope(parentRequest)
118
+ const forkStablePrefix = envelope(forkRequest, parentRequest.messages.length)
119
+ expect(fingerprintStablePrefix(forkStablePrefix)).toBe(fingerprintStablePrefix(parentStable))
120
+ expect(Object.isFrozen(forkRequest)).toBe(true)
121
+
122
+ const spawnRun = await ctx.subagents.start('spawn', {
123
+ prompt: [{ type: 'text', text: SPAWN_TEXT }],
124
+ parent,
125
+ signal: new AbortController().signal,
126
+ })
127
+ await spawnRun.result
128
+ const spawnChild = spawnRun.localAgent
129
+ const spawnRequest = requests.get(String(spawnRun.id))?.at(-1)
130
+ expect(spawnChild?.session.header.seedLength).toBeUndefined()
131
+ expect(spawnRequest).toBeDefined()
132
+ expect(JSON.stringify(spawnRequest?.messages)).not.toContain(PARENT_TEXT)
133
+ expect(assessReusablePrefix({
134
+ mode: 'spawn',
135
+ inheritsParentContext: false,
136
+ parent: parentStable,
137
+ child: envelope(spawnRequest!),
138
+ })).toEqual({ eligible: false, reason: 'spawn-does-not-inherit' })
139
+
140
+ const safeRecord = buildSafeCacheAuditRecord({
141
+ fingerprint: fingerprintStablePrefix(parentStable),
142
+ estimatedSharedPrefixTokens: Math.ceil(JSON.stringify(parentStable).length / 4),
143
+ parentSessionId: String(parent.session.header.id),
144
+ childSessionId: String(forkChild.session.header.id),
145
+ mode: 'fork',
146
+ eligible: true,
147
+ reason: 'identical-fork-prefix',
148
+ })
149
+ expect(safeRecord.confirmationStatus).toBe('unconfirmed')
150
+ expect(JSON.stringify(safeRecord)).not.toContain(PARENT_TEXT)
151
+
152
+ await spawnRun.dispose()
153
+ await forkRun.dispose()
154
+ })
155
+ })
156
+
157
+ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('parent/child natural cache-prefix reuse (real DeepSeek API)', () => {
158
+ it('preserves the fork prefix, separates spawn, and records only official usage evidence', async () => {
159
+ const requests = new Map<string, GenerateOptions[]>()
160
+ ctx = await liveHarness(requests)
161
+ const parent = ctx.agentLoop.create(SessionId('cache-parent-e2e'), {
162
+ provider: 'deepseek-official',
163
+ model: 'deepseek-v4-flash',
164
+ })
165
+ parent.followup(createUserMessage({
166
+ content: [{ type: 'text', text: PARENT_TEXT }],
167
+ source: { kind: 'user' },
168
+ }))
169
+ await parent.whenIdle()
170
+ const parentEvents = parent.session.events.slice()
171
+ const parentRequest = requests.get(String(parent.id))?.at(-1)
172
+ expect(parentRequest).toBeDefined()
173
+
174
+ const forkRun = await ctx.subagents.start('fork', {
175
+ prompt: [{ type: 'text', text: CHILD_TEXT }],
176
+ parent,
177
+ signal: new AbortController().signal,
178
+ })
179
+ await forkRun.result
180
+ const forkChild = forkRun.localAgent
181
+ expect(forkChild).toBeDefined()
182
+ if (forkChild === undefined) throw new Error('fork did not publish a local agent')
183
+ const forkRequest = requests.get(String(forkRun.id))?.at(-1)
184
+ expect(forkRequest).toBeDefined()
185
+ expect(forkChild.session.header).toMatchObject({
186
+ parentSession: parent.session.header.id,
187
+ seedLength: parentEvents.length,
188
+ })
189
+ expect(forkChild.session.events.slice(0, parentEvents.length)).toEqual(parentEvents)
190
+
191
+ const parentStable = envelope(parentRequest!)
192
+ const forkStablePrefix = envelope(forkRequest!, parentRequest!.messages.length)
193
+ expect(assessReusablePrefix({
194
+ mode: 'fork',
195
+ inheritsParentContext: true,
196
+ parent: parentStable,
197
+ child: forkStablePrefix,
198
+ })).toEqual({ eligible: true, reason: 'identical-fork-prefix' })
199
+ expect(fingerprintStablePrefix(parentStable)).toBe(fingerprintStablePrefix(forkStablePrefix))
200
+ expect(Object.isFrozen(forkRequest)).toBe(true)
201
+
202
+ const measurement = ctx.tokenMeter.measure(forkChild.session)
203
+ const usage = measurement.baseline.kind === 'usage' ? measurement.baseline.usage : undefined
204
+ const cacheMissTokens = usage === undefined
205
+ ? undefined
206
+ : usage.inputTokens + (usage.cacheWriteTokens ?? 0)
207
+ const observedPromptTokens = usage === undefined
208
+ ? undefined
209
+ : cacheMissTokens! + (usage.cacheReadTokens ?? 0)
210
+ const record = buildSafeCacheAuditRecord({
211
+ fingerprint: fingerprintStablePrefix(parentStable),
212
+ estimatedSharedPrefixTokens: Math.ceil(JSON.stringify(parentStable).length / 4),
213
+ parentSessionId: String(parent.session.header.id),
214
+ childSessionId: String(forkChild.session.header.id),
215
+ mode: 'fork',
216
+ eligible: true,
217
+ reason: 'identical-fork-prefix',
218
+ ...(usage?.cacheReadTokens === undefined ? {} : { cacheReadTokens: usage.cacheReadTokens }),
219
+ ...(cacheMissTokens === undefined ? {} : { cacheMissTokens }),
220
+ ...(observedPromptTokens === undefined ? {} : { observedPromptTokens }),
221
+ })
222
+ expect(JSON.stringify(record)).not.toContain(PARENT_TEXT)
223
+ expect(JSON.stringify(record)).not.toContain(CHILD_TEXT)
224
+ console.info(`SUBAGENT_CACHE_E2E ${JSON.stringify(record)}`)
225
+
226
+ const spawnRun = await ctx.subagents.start('spawn', {
227
+ prompt: [{ type: 'text', text: SPAWN_TEXT }],
228
+ parent,
229
+ signal: new AbortController().signal,
230
+ })
231
+ await spawnRun.result
232
+ const spawnChild = spawnRun.localAgent
233
+ expect(spawnChild).toBeDefined()
234
+ if (spawnChild === undefined) throw new Error('spawn did not publish a local agent')
235
+ const spawnRequest = requests.get(String(spawnRun.id))?.at(-1)
236
+ expect(spawnChild.session.header.parentSession).toBe(parent.session.header.id)
237
+ expect(spawnChild.session.header.seedLength).toBeUndefined()
238
+ expect(spawnRequest).toBeDefined()
239
+ expect(JSON.stringify(spawnRequest!.messages)).not.toContain(PARENT_TEXT)
240
+ expect(assessReusablePrefix({
241
+ mode: 'spawn',
242
+ inheritsParentContext: false,
243
+ parent: parentStable,
244
+ child: envelope(spawnRequest!),
245
+ })).toEqual({ eligible: false, reason: 'spawn-does-not-inherit' })
246
+
247
+ await spawnRun.dispose()
248
+ await forkRun.dispose()
249
+ }, 180_000)
250
+ })
@@ -0,0 +1,105 @@
1
+ import { createHash } from 'node:crypto'
2
+
3
+ const CACHE_PREFIX_AUDIT_VERSION = 'dsh-prefix-audit-v1' as const
4
+
5
+ type JsonPrimitive = boolean | number | string | null
6
+ export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue }
7
+
8
+ export interface StablePrefixEnvelope {
9
+ provider: string
10
+ model: string
11
+ system: JsonValue
12
+ tools: JsonValue[]
13
+ messages: JsonValue[]
14
+ }
15
+
16
+ export interface PrefixAssessmentInput {
17
+ mode: 'fork' | 'spawn'
18
+ inheritsParentContext: boolean
19
+ parent: StablePrefixEnvelope
20
+ child: StablePrefixEnvelope
21
+ }
22
+
23
+ export interface CacheUsage {
24
+ cacheReadTokens: number
25
+ cacheMissTokens: number
26
+ observedPromptTokens: number
27
+ }
28
+
29
+ export interface SafeCacheAuditInput {
30
+ fingerprint: string
31
+ estimatedSharedPrefixTokens: number
32
+ parentSessionId: string
33
+ childSessionId: string
34
+ mode: 'fork' | 'spawn'
35
+ eligible: boolean
36
+ reason: string
37
+ cacheReadTokens?: number
38
+ cacheMissTokens?: number
39
+ observedPromptTokens?: number
40
+ }
41
+
42
+ const canonicalize = (value: unknown): unknown => {
43
+ if (Array.isArray(value)) return value.map(canonicalize)
44
+ if (value === null || typeof value !== 'object') return value
45
+
46
+ return Object.fromEntries(
47
+ Object.entries(value as Record<string, unknown>)
48
+ .sort(([left], [right]) => left.localeCompare(right))
49
+ .map(([key, item]) => [key, canonicalize(item)]),
50
+ )
51
+ }
52
+
53
+ const canonicalJson = (value: unknown): string => JSON.stringify(canonicalize(value))
54
+
55
+ export const fingerprintStablePrefix = (envelope: StablePrefixEnvelope): string => {
56
+ const payload = canonicalJson({ auditVersion: CACHE_PREFIX_AUDIT_VERSION, envelope })
57
+ return createHash('sha256').update(payload, 'utf8').digest('hex')
58
+ }
59
+
60
+ export const assessReusablePrefix = (
61
+ input: PrefixAssessmentInput,
62
+ ): { eligible: boolean; reason: 'identical-fork-prefix' | 'spawn-does-not-inherit' | 'stable-prefix-mismatch' } => {
63
+ if (input.mode === 'spawn' || !input.inheritsParentContext) {
64
+ return { eligible: false, reason: 'spawn-does-not-inherit' }
65
+ }
66
+ if (canonicalJson(input.parent) !== canonicalJson(input.child)) {
67
+ return { eligible: false, reason: 'stable-prefix-mismatch' }
68
+ }
69
+ return { eligible: true, reason: 'identical-fork-prefix' }
70
+ }
71
+
72
+ export const validateCacheUsage = (usage: CacheUsage): boolean => (
73
+ Number.isSafeInteger(usage.cacheReadTokens)
74
+ && Number.isSafeInteger(usage.cacheMissTokens)
75
+ && Number.isSafeInteger(usage.observedPromptTokens)
76
+ && usage.cacheReadTokens >= 0
77
+ && usage.cacheMissTokens >= 0
78
+ && usage.observedPromptTokens >= 0
79
+ && usage.cacheReadTokens + usage.cacheMissTokens === usage.observedPromptTokens
80
+ )
81
+
82
+ export const buildSafeCacheAuditRecord = (input: SafeCacheAuditInput) => ({
83
+ auditVersion: CACHE_PREFIX_AUDIT_VERSION,
84
+ fingerprint: input.fingerprint,
85
+ estimatedSharedPrefixTokens: input.estimatedSharedPrefixTokens,
86
+ parentSessionId: input.parentSessionId,
87
+ childSessionId: input.childSessionId,
88
+ mode: input.mode,
89
+ eligible: input.eligible,
90
+ reason: input.reason,
91
+ cacheReadTokens: input.cacheReadTokens ?? null,
92
+ cacheMissTokens: input.cacheMissTokens ?? null,
93
+ observedPromptTokens: input.observedPromptTokens ?? null,
94
+ confirmationStatus: input.cacheReadTokens !== undefined
95
+ && input.cacheMissTokens !== undefined
96
+ && input.observedPromptTokens !== undefined
97
+ && input.cacheReadTokens > 0
98
+ && validateCacheUsage({
99
+ cacheReadTokens: input.cacheReadTokens,
100
+ cacheMissTokens: input.cacheMissTokens,
101
+ observedPromptTokens: input.observedPromptTokens,
102
+ })
103
+ ? 'confirmed'
104
+ : 'unconfirmed',
105
+ })
@@ -0,0 +1,37 @@
1
+ import type {
2
+ GenerateOptions,
3
+ LlmResolvedModelInfo,
4
+ StreamChunk,
5
+ } from '@deepseek-ai/dsh-llm'
6
+ import { LlmAdapter } from '@deepseek-ai/dsh-llm'
7
+
8
+ /** Minimal scripted adapter using only the published LLM test surface. */
9
+ export class PublicMockAdapter extends LlmAdapter {
10
+ constructor(private readonly script: StreamChunk[][]) {
11
+ super()
12
+ }
13
+
14
+ override resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo> {
15
+ return Promise.resolve({ provider, id: model, name: model })
16
+ }
17
+
18
+ async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
19
+ const chunks = this.script.shift()
20
+ if (chunks === undefined) throw new Error('PublicMockAdapter: script exhausted')
21
+ for (const chunk of chunks) {
22
+ if (options.signal?.aborted === true) throw new Error('aborted')
23
+ yield chunk
24
+ }
25
+ }
26
+ }
27
+
28
+ /** One deterministic text response for the concrete AgentLoop. */
29
+ export function publicTextResponse(text: string): StreamChunk[] {
30
+ return [
31
+ { type: 'block-start', index: 0, blockType: 'text' },
32
+ { type: 'text-delta', index: 0, text },
33
+ { type: 'block-end', index: 0, block: { type: 'text', text } },
34
+ { type: 'usage', usage: { inputTokens: 10, outputTokens: text.length } },
35
+ { type: 'finish', reason: { kind: 'stop' } },
36
+ ]
37
+ }
@@ -0,0 +1,34 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ interface MenuItem {
4
+ id: string
5
+ label: ReactNode
6
+ }
7
+
8
+ interface MenuProps {
9
+ anchor: ReactNode
10
+ items: readonly MenuItem[]
11
+ onSelect: (id: string) => void
12
+ open: boolean
13
+ }
14
+
15
+ export function IconChevronDownOutline14({ className }: { className?: string }) {
16
+ return <span aria-hidden="true" className={className}>⌄</span>
17
+ }
18
+
19
+ export function Menu({ anchor, items, onSelect, open }: MenuProps) {
20
+ return (
21
+ <>
22
+ {anchor}
23
+ {open ? (
24
+ <div role="menu">
25
+ {items.map(item => (
26
+ <button key={item.id} role="menuitem" type="button" onClick={() => { onSelect(item.id) }}>
27
+ {item.label}
28
+ </button>
29
+ ))}
30
+ </div>
31
+ ) : null}
32
+ </>
33
+ )
34
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "lib",
6
+ "composite": true,
7
+ "declaration": true,
8
+ "declarationMap": false
9
+ },
10
+ "include": ["src"]
11
+ }
@@ -0,0 +1,102 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { readFile } from 'node:fs/promises'
3
+ import { basename, dirname, relative, resolve } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { defineConfig } from 'tsdown'
6
+ import { transform } from 'lightningcss'
7
+
8
+ const PLUGIN_ID = 'dsh-context-compression-improved'
9
+ const SELECTOR_ROOT = dirname(fileURLToPath(import.meta.url))
10
+ const CSS_PREFIX = '\0dsh-context-compression-css:'
11
+ const CSS_SUFFIX = '.mjs'
12
+ const CLIENT_EXTERNALS = new Set([
13
+ 'react',
14
+ 'react/jsx-runtime',
15
+ '@deepseek-ai/dsh-client-ui-primitives',
16
+ ])
17
+ const cssFiles = new Map<string, string>()
18
+
19
+ function stableCssFileId(file: string): string {
20
+ const id = relative(SELECTOR_ROOT, file).replaceAll('\\', '/')
21
+ if (id === '..' || id.startsWith('../')) {
22
+ throw new Error(`CSS module is outside the selector package: ${id}`)
23
+ }
24
+ return id
25
+ }
26
+
27
+ function styleModule(file: string, css: string, classMap: Readonly<Record<string, string>>): string {
28
+ const tagId = `${PLUGIN_ID}/${basename(file)}`
29
+ return [
30
+ `const css = ${JSON.stringify(css)};`,
31
+ `const tagId = ${JSON.stringify(tagId)};`,
32
+ 'if (typeof document !== "undefined" && document.querySelector(`style[data-plugin-css="${tagId}"]`) === null) {',
33
+ ' const tag = document.createElement("style");',
34
+ ` tag.dataset.plugin = ${JSON.stringify(PLUGIN_ID)};`,
35
+ ' tag.dataset.pluginCss = tagId;',
36
+ ' tag.textContent = css;',
37
+ ' document.head.appendChild(tag);',
38
+ '}',
39
+ `export default ${JSON.stringify(classMap)};`,
40
+ ].join('\n')
41
+ }
42
+
43
+ export default defineConfig({
44
+ name: `${PLUGIN_ID}/client`,
45
+ entry: { client: 'src/client/index.ts' },
46
+ outDir: 'lib',
47
+ format: 'cjs',
48
+ platform: 'browser',
49
+ target: 'es2023',
50
+ dts: false,
51
+ clean: false,
52
+ fixedExtension: false,
53
+ hash: false,
54
+ sourcemap: false,
55
+ deps: {
56
+ neverBundle: specifier => CLIENT_EXTERNALS.has(specifier),
57
+ alwaysBundle: specifier => !CLIENT_EXTERNALS.has(specifier),
58
+ },
59
+ define: {
60
+ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV ?? 'production'),
61
+ 'import.meta.env.MODE': JSON.stringify(process.env.NODE_ENV ?? 'production'),
62
+ 'import.meta.env': JSON.stringify({ MODE: process.env.NODE_ENV ?? 'production' }),
63
+ },
64
+ plugins: [{
65
+ name: 'dsh-context-compression-css-inline',
66
+ resolveId(source: string, importer: string | undefined) {
67
+ if (!source.endsWith('.module.css')) return null
68
+ const file = importer === undefined ? source : resolve(dirname(importer), source)
69
+ const digest = createHash('sha256').update(stableCssFileId(file)).digest('hex').slice(0, 12)
70
+ const id = `${CSS_PREFIX}${digest}-${basename(file)}${CSS_SUFFIX}`
71
+ cssFiles.set(id, file)
72
+ return id
73
+ },
74
+ async load(id: string) {
75
+ const file = cssFiles.get(id)
76
+ if (file === undefined) return null
77
+ this.addWatchFile(file)
78
+ const source = await readFile(file)
79
+ const stableFile = stableCssFileId(file)
80
+ const result = transform({
81
+ // lightningcss includes filename in CSS Modules hashes. A package-
82
+ // relative identity keeps reviewed tarballs byte-for-byte stable when
83
+ // the repository is cloned or moved to a different absolute path.
84
+ filename: stableFile,
85
+ code: source,
86
+ cssModules: { pattern: '[hash]_[local]' },
87
+ minify: true,
88
+ })
89
+ const classMap: Record<string, string> = {}
90
+ for (const [local, value] of Object.entries(result.exports ?? {}).sort(([a], [b]) => a.localeCompare(b))) {
91
+ classMap[local] = value.name
92
+ }
93
+ return styleModule(file, result.code.toString(), classMap)
94
+ },
95
+ }],
96
+ outputOptions: {
97
+ entryFileNames: 'client.js',
98
+ banner: `window.__ModuleLoader__.load({ id: ${JSON.stringify(PLUGIN_ID)}, factory: (require) => {`,
99
+ intro: 'var module = { exports: {} }; var exports = module.exports;',
100
+ footer: 'return module.exports; } });',
101
+ },
102
+ })
@@ -0,0 +1,20 @@
1
+ import { defineConfig } from 'tsdown'
2
+
3
+ export default defineConfig({
4
+ entry: {
5
+ index: 'src/index.ts',
6
+ pruner: 'src/pruner.ts',
7
+ invariant: 'src/invariant.ts',
8
+ // This ESM face supplies client.d.ts. The sequential client build then
9
+ // overwrites only client.js with the Harness lazy-CJS artifact.
10
+ client: 'src/client/index.ts',
11
+ },
12
+ format: 'esm',
13
+ dts: true,
14
+ clean: true,
15
+ outDir: 'lib',
16
+ fixedExtension: false,
17
+ hash: false,
18
+ sourcemap: false,
19
+ deps: { neverBundle: true },
20
+ })
@@ -0,0 +1,19 @@
1
+ packages:
2
+ - packages/*
3
+
4
+ linkWorkspacePackages: true
5
+
6
+ peerDependencyRules:
7
+ allowedVersions:
8
+ typescript: ">=5 <7"
9
+
10
+ allowBuilds:
11
+ esbuild: true
12
+ "@huggingface/tokenizers": true
13
+
14
+ # GHSA-2883-xcg3-v3hh - js-yaml < 4.3.2 does not bound CPU use for empty merge
15
+ # sources. @deepseek-ai/cordis-plugin-include and @deepseek-ai/dsh-agent-presets
16
+ # both declare ranges that already allow the patch, yet keep resolving 4.3.1, so
17
+ # the floor is pinned here instead of waiting on their next release.
18
+ overrides:
19
+ js-yaml@<4.3.2: ^4.3.2
@@ -0,0 +1,80 @@
1
+ /**
2
+ * One-off baseline capture: freeze the resolvePolicy output for the pre-existing
3
+ * profiles (before 'tokenpilot-inspired' is added) into a JSON fixture used by
4
+ * tests/runtime/tokenpilot/profile-baseline.spec.ts as the backward-compat golden.
5
+ *
6
+ * Usage: node scripts-dist/capture-profile-baseline.js [baseline-file]
7
+ */
8
+ import { writeFileSync } from 'node:fs'
9
+ import { createRequire } from 'node:module'
10
+
11
+ /** The slice of the built pruner entry this capture consumes. */
12
+ interface PrunerRuntime {
13
+ resolveConfig: (input: Record<string, unknown>) => Record<string, unknown>
14
+ resolvePolicy: (
15
+ config: Record<string, unknown>,
16
+ profile: string,
17
+ custom: unknown,
18
+ options: Record<string, unknown>,
19
+ ) => Record<string, unknown>
20
+ }
21
+
22
+ /** One option matrix row. */
23
+ interface OptionCase {
24
+ label: string
25
+ options: Record<string, unknown>
26
+ }
27
+
28
+ /** One custom-document matrix row. */
29
+ interface CustomDocCase {
30
+ label: string
31
+ custom: unknown
32
+ }
33
+
34
+ /** The fixture shape `profile-baseline.spec.ts` reads back. */
35
+ interface ProfileBaseline {
36
+ capturedAt: string
37
+ profiles: Record<string, Record<string, Record<string, unknown>>>
38
+ }
39
+
40
+ const require = createRequire(import.meta.url)
41
+ const runtime = require('../packages/selector/lib/pruner.js') as PrunerRuntime
42
+
43
+ const OLD_PROFILES: readonly string[] = [
44
+ 'off',
45
+ 'native',
46
+ 'balanced',
47
+ 'cache-strict',
48
+ 'savings',
49
+ 'adaptive',
50
+ 'custom',
51
+ ]
52
+ const baseConfig = runtime.resolveConfig({})
53
+ const OPTION_CASES: readonly OptionCase[] = [
54
+ { label: 'defaults', options: {} },
55
+ { label: 'linkage', options: { contextWindowTokens: 128_000, autoCompactThresholdPercent: 80 } },
56
+ ]
57
+ const CUSTOM_DOCS: readonly CustomDocCase[] = [
58
+ { label: 'default-custom', custom: undefined },
59
+ ]
60
+
61
+ const snapshot: ProfileBaseline = { capturedAt: 'pre-tokenpilot-inspired', profiles: {} }
62
+ for (const profile of OLD_PROFILES) {
63
+ const byCase: Record<string, Record<string, unknown>> = {}
64
+ snapshot.profiles[profile] = byCase
65
+ for (const optionCase of OPTION_CASES) {
66
+ for (const customCase of CUSTOM_DOCS) {
67
+ const key = `${optionCase.label}${customCase.label === 'default-custom' ? '' : `/${customCase.label}`}`
68
+ byCase[key] = runtime.resolvePolicy(
69
+ baseConfig,
70
+ profile,
71
+ customCase.custom,
72
+ optionCase.options,
73
+ )
74
+ }
75
+ }
76
+ }
77
+
78
+ const out = process.argv[2] ?? 'packages/selector/tests/runtime/fixtures/profile-baseline.json'
79
+ writeFileSync(out, `${JSON.stringify(snapshot, null, 2)}\n`)
80
+ console.log(`baseline written: ${out}`)