dsh-context-compression-improved 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +1 -0
- package/.github/workflows/ci.yml +39 -0
- package/CHANGELOG.ja.md +39 -0
- package/CHANGELOG.ko.md +39 -0
- package/CHANGELOG.md +135 -0
- package/CHANGELOG.zh.md +39 -0
- package/CONTRIBUTING.md +22 -0
- package/README.ja.md +104 -0
- package/README.ko.md +103 -0
- package/README.md +89 -12
- package/README.zh.md +87 -12
- package/SECURITY.md +18 -0
- package/THIRD_PARTY_NOTICES.md +7 -31
- package/docs/installation.ja.md +76 -0
- package/docs/installation.ko.md +76 -0
- package/docs/installation.md +76 -0
- package/docs/installation.zh.md +76 -0
- package/docs/repair-log.md +582 -0
- package/eslint.config.js +30 -0
- package/package.json +84 -81
- package/packages/selector/LICENSE +21 -0
- package/packages/selector/README.md +26 -0
- package/packages/selector/README.zh.md +26 -0
- package/packages/selector/THIRD_PARTY_NOTICES.md +38 -0
- package/packages/selector/docs/history-tool-call-working-set-spec.md +112 -0
- package/packages/selector/docs/native-tool-result-selector-spec.md +34 -0
- package/packages/selector/docs/subagent-cache-reuse-spec.md +46 -0
- package/packages/selector/lib/style.css +308 -0
- package/packages/selector/package.json +115 -0
- package/{screenshots.json → packages/selector/screenshots.json} +6 -6
- package/packages/selector/src/client/CompressionProfileControls.tsx +229 -0
- package/packages/selector/src/client/CompressionProfileSelector.module.css +170 -0
- package/packages/selector/src/client/CompressionProfileSelector.tsx +79 -0
- package/packages/selector/src/client/CustomPolicyEditor.tsx +216 -0
- package/packages/selector/src/client/EstimatorControls.tsx +281 -0
- package/packages/selector/src/client/decode.ts +49 -0
- package/packages/selector/src/client/index.ts +111 -0
- package/packages/selector/src/client/locales.ts +198 -0
- package/packages/selector/src/client/preset-options.ts +70 -0
- package/packages/selector/src/client/settings-section.tsx +126 -0
- package/packages/selector/src/css-modules.d.ts +6 -0
- package/packages/selector/src/deepseek-v4-tokenizer.ts +210 -0
- package/packages/selector/src/estimator-catalog.ts +104 -0
- package/packages/selector/src/index.ts +327 -0
- package/packages/selector/src/invariant.ts +113 -0
- package/packages/selector/src/preset-overlay.ts +567 -0
- package/packages/selector/src/profiles.ts +342 -0
- package/packages/selector/src/pruner/content.ts +188 -0
- package/packages/selector/src/pruner/session.ts +94 -0
- package/packages/selector/src/pruner/state.ts +43 -0
- package/packages/selector/src/pruner/tuning.ts +23 -0
- package/packages/selector/src/pruner/types.ts +60 -0
- package/packages/selector/src/pruner.ts +2144 -0
- package/packages/selector/src/runtime/adaptive-cost.ts +194 -0
- package/packages/selector/src/runtime/audit.ts +215 -0
- package/packages/selector/src/runtime/config.ts +613 -0
- package/packages/selector/src/runtime/custom-policy.ts +278 -0
- package/packages/selector/src/runtime/deepseek-official-pricing.ts +298 -0
- package/packages/selector/src/runtime/deepseek-v4-vision-tokens.ts +254 -0
- package/packages/selector/src/runtime/measurement.ts +403 -0
- package/packages/selector/src/runtime/reducers.ts +656 -0
- package/packages/selector/src/runtime/retrieve.ts +457 -0
- package/packages/selector/src/runtime/session-events.ts +17 -0
- package/packages/selector/src/runtime/tail-trim.ts +166 -0
- package/packages/selector/src/runtime/token-count.ts +72 -0
- package/packages/selector/src/runtime/tokenpilot/dedup.ts +81 -0
- package/packages/selector/src/runtime/tokenpilot/estimator.ts +183 -0
- package/packages/selector/src/runtime/tokenpilot/locator.ts +128 -0
- package/packages/selector/src/runtime/tokenpilot/read-state.ts +77 -0
- package/packages/selector/src/runtime/types.ts +309 -0
- package/packages/selector/src/runtime/value.ts +48 -0
- package/packages/selector/tests/auto-compact.client.spec.tsx +226 -0
- package/packages/selector/tests/built/client-artifact.spec.ts +51 -0
- package/packages/selector/tests/cache-prefix-audit.spec.ts +123 -0
- package/packages/selector/tests/code-skeleton.client.spec.ts +88 -0
- package/packages/selector/tests/custom-contract.client.spec.ts +202 -0
- package/packages/selector/tests/estimator-catalog.spec.ts +70 -0
- package/packages/selector/tests/estimator-channel.client.spec.tsx +247 -0
- package/packages/selector/tests/estimator-route-registration.host.spec.ts +176 -0
- package/packages/selector/tests/host-preset-overlay.host.spec.ts +204 -0
- package/packages/selector/tests/preset-options-write.client.spec.ts +181 -0
- package/packages/selector/tests/preset-overlay-loader.e2e.host.spec.ts +196 -0
- package/packages/selector/tests/preset-overlay.host.spec.ts +243 -0
- package/packages/selector/tests/profiles.client.spec.tsx +434 -0
- package/packages/selector/tests/public/package-contract.client.spec.ts +33 -0
- package/packages/selector/tests/runtime/adaptive-cost.spec.ts +167 -0
- package/packages/selector/tests/runtime/audit.spec.ts +129 -0
- package/packages/selector/tests/runtime/auto-compact-config.spec.ts +523 -0
- package/packages/selector/tests/runtime/code-skeleton.spec.ts +141 -0
- package/packages/selector/tests/runtime/deepseek-official-pricing.spec.ts +186 -0
- package/packages/selector/tests/runtime/deepseek-v4-tokenizer.spec.ts +122 -0
- package/packages/selector/tests/runtime/deepseek-v4-vision-tokens.spec.ts +122 -0
- package/packages/selector/tests/runtime/fixtures/profile-baseline.json +273 -0
- package/packages/selector/tests/runtime/fixtures/tokenizer-golden.json +106 -0
- package/packages/selector/tests/runtime/fixtures/vision-golden.json +459 -0
- package/packages/selector/tests/runtime/public/public-runtime.spec.ts +2531 -0
- package/packages/selector/tests/runtime/session-events.spec.ts +27 -0
- package/packages/selector/tests/runtime/tokenizer-golden.spec.ts +53 -0
- package/packages/selector/tests/runtime/tokenpilot/dedup.spec.ts +52 -0
- package/packages/selector/tests/runtime/tokenpilot/estimator.spec.ts +56 -0
- package/packages/selector/tests/runtime/tokenpilot/locator.spec.ts +76 -0
- package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +100 -0
- package/packages/selector/tests/runtime/tokenpilot/read-state.spec.ts +58 -0
- package/packages/selector/tests/runtime/value.spec.ts +23 -0
- package/packages/selector/tests/standing-generation.host.spec.ts +631 -0
- package/packages/selector/tests/subagent-cache-reuse.host.spec.ts +250 -0
- package/packages/selector/tests/support/cache-prefix-audit.ts +105 -0
- package/packages/selector/tests/support/mock-adapter.ts +37 -0
- package/packages/selector/tests/support/ui-primitives.tsx +34 -0
- package/packages/selector/tsconfig.json +11 -0
- package/packages/selector/tsdown.client.config.ts +102 -0
- package/packages/selector/tsdown.config.ts +20 -0
- package/pnpm-workspace.yaml +19 -0
- package/scripts/capture-profile-baseline.ts +80 -0
- package/scripts/generate-tokenizer-fixtures.py +81 -0
- package/scripts/generate-vision-fixtures.py +208 -0
- package/scripts/packed-components-smoke.ts +713 -0
- package/scripts/packed-install-e2e.ts +1072 -0
- package/scripts/verify-release.ts +300 -0
- package/tests/TEST_INVENTORY.md +42 -0
- package/tsconfig.base.json +18 -0
- package/tsconfig.json +7 -0
- package/tsconfig.scripts.json +13 -0
- package/tsconfig.tests.json +15 -0
- package/vitest.built.config.ts +9 -0
- package/vitest.config.ts +43 -0
- /package/{assets → packages/selector/assets}/deepseek-v4/LICENSE.DeepSeek-V4-Pro.txt +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4/manifest.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4/tokenizer.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4/tokenizer_config.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/LICENSE.DeepSeek-V4-Flash-Vision-Exp.txt +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/manifest.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/tokenizer.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/tokenizer_config.json +0 -0
- /package/{assets → packages/selector/assets}/screenshots/context-compression-selector-profiles.jpg +0 -0
- /package/{assets → packages/selector/assets}/screenshots/context-compression-selector-settings.png +0 -0
- /package/{cordis.patch.yml → packages/selector/cordis.patch.yml} +0 -0
- /package/{dsh.plugin.json → packages/selector/dsh.plugin.json} +0 -0
- /package/{lib → packages/selector/lib}/client.d.ts +0 -0
- /package/{lib → packages/selector/lib}/client.js +0 -0
- /package/{lib → packages/selector/lib}/config.js +0 -0
- /package/{lib → packages/selector/lib}/index.d.ts +0 -0
- /package/{lib → packages/selector/lib}/index.js +0 -0
- /package/{lib → packages/selector/lib}/invariant.d.ts +0 -0
- /package/{lib → packages/selector/lib}/invariant.js +0 -0
- /package/{lib → packages/selector/lib}/pruner.d.ts +0 -0
- /package/{lib → packages/selector/lib}/pruner.js +0 -0
- /package/{lib → packages/selector/lib}/tail-trim.js +0 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
import { apply } from '../src/client/index.ts'
|
|
5
|
+
import type { CompressionSelectorInjected } from '../src/client/CompressionProfileSelector.tsx'
|
|
6
|
+
import type { ContextCompressionSettings } from '../src/profiles.ts'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `settingsScope.set(field, value)` replaces the value at that field, so the
|
|
10
|
+
* write path is asserted through the document it stores: writing the bare
|
|
11
|
+
* patch is what silently deleted `estimatorMode` (and every sibling override)
|
|
12
|
+
* whenever the user touched a second field of the TokenPilot-inspired card.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
type ScopeFace = {
|
|
16
|
+
getSnapshot: () => unknown
|
|
17
|
+
subscribe: (listener: () => void) => () => void
|
|
18
|
+
set: (field: string, value: unknown) => Promise<void>
|
|
19
|
+
unset: (field: string) => Promise<void>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createScopeStub(initial: ContextCompressionSettings, commit = true): {
|
|
23
|
+
scope: ScopeFace
|
|
24
|
+
writes: { field: string, value: unknown }[]
|
|
25
|
+
section: () => Record<string, unknown>
|
|
26
|
+
sectionOf: (key: string) => unknown
|
|
27
|
+
} {
|
|
28
|
+
let revision = 1
|
|
29
|
+
let value: Record<string, unknown> = structuredClone(initial) as unknown as Record<string, unknown>
|
|
30
|
+
const writes: { field: string, value: unknown }[] = []
|
|
31
|
+
const listeners = new Set<() => void>()
|
|
32
|
+
const scope: ScopeFace = {
|
|
33
|
+
getSnapshot: () => ({
|
|
34
|
+
status: 'ready',
|
|
35
|
+
value: value as unknown as ContextCompressionSettings,
|
|
36
|
+
base: undefined,
|
|
37
|
+
user: undefined,
|
|
38
|
+
revision,
|
|
39
|
+
writable: true,
|
|
40
|
+
mode: 'host',
|
|
41
|
+
}),
|
|
42
|
+
subscribe: (listener: () => void) => {
|
|
43
|
+
listeners.add(listener)
|
|
44
|
+
return () => { listeners.delete(listener) }
|
|
45
|
+
},
|
|
46
|
+
set: (field: string, fieldValue: unknown) => {
|
|
47
|
+
writes.push({ field, value: fieldValue })
|
|
48
|
+
// The Host applies `set` AT the path: the section value is replaced.
|
|
49
|
+
if (commit) {
|
|
50
|
+
value = { ...value, [field]: structuredClone(fieldValue) }
|
|
51
|
+
revision += 1
|
|
52
|
+
for (const listener of listeners) listener()
|
|
53
|
+
}
|
|
54
|
+
return Promise.resolve()
|
|
55
|
+
},
|
|
56
|
+
unset: (field: string) => {
|
|
57
|
+
writes.push({ field, value: undefined })
|
|
58
|
+
if (commit) {
|
|
59
|
+
delete value[field]
|
|
60
|
+
revision += 1
|
|
61
|
+
for (const listener of listeners) listener()
|
|
62
|
+
}
|
|
63
|
+
return Promise.resolve()
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
scope,
|
|
68
|
+
writes,
|
|
69
|
+
section: () => value,
|
|
70
|
+
sectionOf: (key: string) => value[key],
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const DEFAULT_CUSTOM = {
|
|
75
|
+
version: 1,
|
|
76
|
+
unit: 'tokens',
|
|
77
|
+
fresh: { enabled: true, trigger: 4_096, target: 2_048 },
|
|
78
|
+
aggregate: { enabled: true, trigger: 16_384, target: 8_192 },
|
|
79
|
+
history: {
|
|
80
|
+
enabled: true,
|
|
81
|
+
trigger: 32_768,
|
|
82
|
+
keepRecentTurns: 2,
|
|
83
|
+
keepRecent: 16_384,
|
|
84
|
+
minReclaim: 8_192,
|
|
85
|
+
},
|
|
86
|
+
prefixPolicy: 'pressure-break',
|
|
87
|
+
} as const satisfies ContextCompressionSettings['custom']
|
|
88
|
+
|
|
89
|
+
function bindInjected(
|
|
90
|
+
presetOptions: ContextCompressionSettings['presetOptions'],
|
|
91
|
+
commit = true,
|
|
92
|
+
): {
|
|
93
|
+
injected: CompressionSelectorInjected
|
|
94
|
+
writes: { field: string, value: unknown }[]
|
|
95
|
+
sectionOf: (key: string) => unknown
|
|
96
|
+
} {
|
|
97
|
+
const settings: ContextCompressionSettings = {
|
|
98
|
+
profile: 'tokenpilot-inspired',
|
|
99
|
+
custom: structuredClone(DEFAULT_CUSTOM),
|
|
100
|
+
autoCompact: { thresholdPercent: 80 },
|
|
101
|
+
codeSkeleton: { enabled: false },
|
|
102
|
+
...(presetOptions === undefined ? {} : { presetOptions }),
|
|
103
|
+
}
|
|
104
|
+
const stub = createScopeStub(settings, commit)
|
|
105
|
+
let options: Record<string, unknown> | undefined
|
|
106
|
+
const ctx = {
|
|
107
|
+
effect: (factory: () => unknown) => { factory(); return () => {} },
|
|
108
|
+
locale: { register: () => {}, bind: () => (key: string) => key },
|
|
109
|
+
settingsScope: { bind: () => stub.scope },
|
|
110
|
+
slots: {
|
|
111
|
+
inject: (_slot: string, factory: () => (() => void) | Generator<() => void>) => {
|
|
112
|
+
const result = factory()
|
|
113
|
+
return typeof result === 'function' ? result : () => {}
|
|
114
|
+
},
|
|
115
|
+
register: (registerOptions: Record<string, unknown>) => {
|
|
116
|
+
options = registerOptions
|
|
117
|
+
return () => {}
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
apply(ctx as never)
|
|
122
|
+
if (options === undefined) throw new Error('the settings card never registered')
|
|
123
|
+
const factory = options['inject'] as () => CompressionSelectorInjected
|
|
124
|
+
return { injected: factory(), writes: stub.writes, sectionOf: stub.sectionOf }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
describe('presetOptions writes preserve sibling fields', () => {
|
|
128
|
+
it('keeps the estimator channel when a provider is chosen afterwards', async () => {
|
|
129
|
+
const { injected, sectionOf } = bindInjected({ estimatorMode: 'host' })
|
|
130
|
+
await injected.savePresetOptions({ estimatorProvider: 'local-35b' })
|
|
131
|
+
expect(sectionOf('presetOptions')).toEqual({ estimatorMode: 'host', estimatorProvider: 'local-35b' })
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('writes the merged document rather than the bare patch', async () => {
|
|
135
|
+
const { injected, writes } = bindInjected({ estimatorMode: 'direct', estimatorBaseUrl: 'http://192.168.100.242:8200' })
|
|
136
|
+
await injected.savePresetOptions({ estimatorModel: 'Qwen3.6-35B-A3B' })
|
|
137
|
+
expect(writes).toEqual([{
|
|
138
|
+
field: 'presetOptions',
|
|
139
|
+
value: {
|
|
140
|
+
estimatorMode: 'direct',
|
|
141
|
+
estimatorBaseUrl: 'http://192.168.100.242:8200',
|
|
142
|
+
estimatorModel: 'Qwen3.6-35B-A3B',
|
|
143
|
+
},
|
|
144
|
+
}])
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('keeps every unrelated override across successive edits', async () => {
|
|
148
|
+
const { injected, sectionOf } = bindInjected({
|
|
149
|
+
estimatorMode: 'direct',
|
|
150
|
+
dedupeToolResults: false,
|
|
151
|
+
readState: false,
|
|
152
|
+
})
|
|
153
|
+
await injected.savePresetOptions({ estimatorModel: 'Qwen3.6-35B-A3B' })
|
|
154
|
+
await injected.savePresetOptions({ estimatorProvider: 'local-35b' })
|
|
155
|
+
expect(sectionOf('presetOptions')).toEqual({
|
|
156
|
+
estimatorMode: 'direct',
|
|
157
|
+
dedupeToolResults: false,
|
|
158
|
+
readState: false,
|
|
159
|
+
estimatorModel: 'Qwen3.6-35B-A3B',
|
|
160
|
+
estimatorProvider: 'local-35b',
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('clears exactly the field a patch names with undefined', async () => {
|
|
165
|
+
const { injected, sectionOf } = bindInjected({ estimatorMode: 'direct', estimatorApiKey: 'sk-stored' })
|
|
166
|
+
await injected.savePresetOptions({ estimatorApiKey: undefined })
|
|
167
|
+
expect(sectionOf('presetOptions')).toEqual({ estimatorMode: 'direct' })
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('writes nothing when the patch already matches the stored section', async () => {
|
|
171
|
+
const { injected, writes } = bindInjected({ estimatorMode: 'host', estimatorProvider: 'local-35b' })
|
|
172
|
+
await injected.savePresetOptions({ estimatorMode: 'host' })
|
|
173
|
+
expect(writes).toEqual([])
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('reports a save the Host did not commit', async () => {
|
|
177
|
+
const { injected } = bindInjected({ estimatorMode: 'host' }, false)
|
|
178
|
+
await expect(injected.savePresetOptions({ estimatorProvider: 'local-35b' }))
|
|
179
|
+
.rejects.toThrow(/were not saved/)
|
|
180
|
+
})
|
|
181
|
+
})
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { pathToFileURL } from 'node:url'
|
|
5
|
+
import { Context } from '@deepseek-ai/cordis'
|
|
6
|
+
import Group from '@deepseek-ai/cordis-plugin-group'
|
|
7
|
+
import Include from '@deepseek-ai/cordis-plugin-include'
|
|
8
|
+
import Loader from '@deepseek-ai/cordis-plugin-loader'
|
|
9
|
+
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
|
|
10
|
+
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
|
11
|
+
import AgentPresets, { COMPOSITION_FILE } from '@deepseek-ai/dsh-agent-presets'
|
|
12
|
+
import CommandRuntime from '@deepseek-ai/dsh-commands'
|
|
13
|
+
import LlmRuntime from '@deepseek-ai/dsh-llm'
|
|
14
|
+
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
|
15
|
+
import { scopeOf } from '@deepseek-ai/dsh-scope'
|
|
16
|
+
import {
|
|
17
|
+
SettingsProvider,
|
|
18
|
+
settingsNamespace,
|
|
19
|
+
type SettingsNamespace,
|
|
20
|
+
} from '@deepseek-ai/dsh-settings'
|
|
21
|
+
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
|
22
|
+
import TokenMeter from '@deepseek-ai/dsh-token-meter'
|
|
23
|
+
import ToolRuntime from '@deepseek-ai/dsh-tools'
|
|
24
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
25
|
+
import { apply } from '../src/index.ts'
|
|
26
|
+
|
|
27
|
+
const CORDIS_ORIGINAL = Symbol.for('cordis.original')
|
|
28
|
+
type Traceable = { [CORDIS_ORIGINAL]?: unknown }
|
|
29
|
+
|
|
30
|
+
let ctx: Context | undefined
|
|
31
|
+
let root: string | undefined
|
|
32
|
+
|
|
33
|
+
afterEach(async () => {
|
|
34
|
+
await ctx?.fiber.dispose()
|
|
35
|
+
ctx = undefined
|
|
36
|
+
if (root !== undefined) await rm(root, { recursive: true, force: true })
|
|
37
|
+
root = undefined
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
class MemorySettings extends SettingsProvider {
|
|
41
|
+
readonly writable = true
|
|
42
|
+
|
|
43
|
+
protected load(): Promise<Record<string, unknown>> {
|
|
44
|
+
return Promise.resolve({})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
protected persist(_ns: SettingsNamespace, _section: Record<string, unknown>): Promise<void> {
|
|
48
|
+
return Promise.resolve()
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface CustomSettings {
|
|
53
|
+
profile: string
|
|
54
|
+
custom: {
|
|
55
|
+
history: { trigger: number }
|
|
56
|
+
tailTrim: { enabled: boolean; trigger: number }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function createPresetRoot(): Promise<string> {
|
|
61
|
+
root = await mkdtemp(join(tmpdir(), 'dsh-selector-loader-e2e-'))
|
|
62
|
+
const marker = join(root, 'marker.mjs')
|
|
63
|
+
await writeFile(marker, 'export function apply() {}\n')
|
|
64
|
+
const source = `- id: source-marker\n name: ${JSON.stringify(marker)}\n`
|
|
65
|
+
for (const id of ['standard', 'custom', 'minimal']) {
|
|
66
|
+
const directory = join(root, id)
|
|
67
|
+
await mkdir(directory)
|
|
68
|
+
await writeFile(join(directory, COMPOSITION_FILE), source)
|
|
69
|
+
}
|
|
70
|
+
return root
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function harness(): Promise<Context> {
|
|
74
|
+
const presets = await createPresetRoot()
|
|
75
|
+
const runtime = new Context()
|
|
76
|
+
runtime.baseUrl = `${pathToFileURL(presets).href}/`
|
|
77
|
+
await runtime.plugin(Loader)
|
|
78
|
+
runtime.loader.builtins.include = Include
|
|
79
|
+
runtime.loader.builtins.group = Group
|
|
80
|
+
await runtime.plugin(LlmRuntime)
|
|
81
|
+
await runtime.plugin(SessionStore)
|
|
82
|
+
await runtime.plugin(SystemPrompt, { persona: '' })
|
|
83
|
+
await runtime.plugin(ToolRuntime)
|
|
84
|
+
await runtime.plugin(AgentRegistry)
|
|
85
|
+
await runtime.plugin(AgentLoop, { agents: [] })
|
|
86
|
+
await runtime.plugin(CommandRuntime)
|
|
87
|
+
await runtime.plugin(TokenMeter)
|
|
88
|
+
await runtime.plugin(MemorySettings)
|
|
89
|
+
await runtime.plugin(AgentPresets, {
|
|
90
|
+
default: 'standard',
|
|
91
|
+
roots: [{ path: presets, trust: 'system' }],
|
|
92
|
+
includeUserRoot: false,
|
|
93
|
+
})
|
|
94
|
+
await runtime.plugin({
|
|
95
|
+
apply: (selectorCtx) => {
|
|
96
|
+
apply(selectorCtx, { presetOverlay: true })
|
|
97
|
+
},
|
|
98
|
+
}).await()
|
|
99
|
+
ctx = runtime
|
|
100
|
+
return runtime
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function agentOn(runtime: Context, sessionId: string, preset: string): Promise<Agent> {
|
|
104
|
+
const handle = await runtime.agents.create({
|
|
105
|
+
sessionId: SessionId(sessionId),
|
|
106
|
+
setup: async agentCtx => void await runtime.agentPresets.mount(agentCtx, preset),
|
|
107
|
+
})
|
|
108
|
+
return handle.agent
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function hasCompressionRetrieve(runtime: Context, agent: Agent): boolean {
|
|
112
|
+
return runtime.tools.get('context_compression_retrieve', scopeOf(agent.ctx)) !== undefined
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function hasCompactCommand(runtime: Context, agent: Agent): boolean {
|
|
116
|
+
return runtime.commands.list(agent).some(command => command.name === 'compact')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function expectCompleteCompressionStack(runtime: Context, agent: Agent): void {
|
|
120
|
+
expect(runtime.agentPresets.serviceFor(agent, 'toolResultPruner')).toBeDefined()
|
|
121
|
+
expect(runtime.agentPresets.serviceFor(agent, 'compaction')).toBeDefined()
|
|
122
|
+
expect(hasCompressionRetrieve(runtime, agent)).toBe(true)
|
|
123
|
+
expect(hasCompactCommand(runtime, agent)).toBe(true)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function expectNoCompressionStack(runtime: Context, agent: Agent): void {
|
|
127
|
+
expect(runtime.agentPresets.serviceFor(agent, 'toolResultPruner')).toBeUndefined()
|
|
128
|
+
expect(runtime.agentPresets.serviceFor(agent, 'compaction')).toBeUndefined()
|
|
129
|
+
expect(hasCompressionRetrieve(runtime, agent)).toBe(false)
|
|
130
|
+
expect(hasCompactCommand(runtime, agent)).toBe(false)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
describe('standalone selector Bundle through the real preset Loader', () => {
|
|
134
|
+
it('mounts the complete stack without changing source preset authoring', async () => {
|
|
135
|
+
const runtime = await harness()
|
|
136
|
+
const source = await runtime.agentPresets.resolve('standard')
|
|
137
|
+
const sourceText = await runtime.agentPresets.read('standard')
|
|
138
|
+
|
|
139
|
+
const agent = await agentOn(runtime, 'selector-loader-standard', 'standard')
|
|
140
|
+
|
|
141
|
+
expectCompleteCompressionStack(runtime, agent)
|
|
142
|
+
expect((await runtime.agentPresets.resolve('standard')).path).toBe(source.path)
|
|
143
|
+
expect(await runtime.agentPresets.read('standard')).toBe(sourceText)
|
|
144
|
+
expect(sourceText).not.toContain('tool-result-pruner')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('keeps compression across preset changes and excludes only exact Minimal', async () => {
|
|
148
|
+
const runtime = await harness()
|
|
149
|
+
const agent = await agentOn(runtime, 'selector-loader-switch', 'standard')
|
|
150
|
+
expectCompleteCompressionStack(runtime, agent)
|
|
151
|
+
|
|
152
|
+
await runtime.agentPresets.recompose(agent.ctx, 'minimal')
|
|
153
|
+
expectNoCompressionStack(runtime, agent)
|
|
154
|
+
|
|
155
|
+
// `custom` deliberately contains the same source rows as Minimal. Its id,
|
|
156
|
+
// not its copied contents, decides the one documented exception.
|
|
157
|
+
await runtime.agentPresets.recompose(agent.ctx, 'custom')
|
|
158
|
+
expectCompleteCompressionStack(runtime, agent)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('gives a child the parent\'s exact compression service instances', async () => {
|
|
162
|
+
const runtime = await harness()
|
|
163
|
+
const parent = await agentOn(runtime, 'selector-loader-parent', 'standard')
|
|
164
|
+
const childHandle = await runtime.agents.create({
|
|
165
|
+
sessionId: SessionId('selector-loader-child'),
|
|
166
|
+
setup: childCtx => void runtime.agentPresets.composeFrom(childCtx, parent.ctx),
|
|
167
|
+
})
|
|
168
|
+
const child = childHandle.agent
|
|
169
|
+
|
|
170
|
+
const parentPruner = runtime.agentPresets.serviceFor(parent, 'toolResultPruner')
|
|
171
|
+
const childPruner = runtime.agentPresets.serviceFor(child, 'toolResultPruner')
|
|
172
|
+
const parentCompaction = runtime.agentPresets.serviceFor(parent, 'compaction')
|
|
173
|
+
const childCompaction = runtime.agentPresets.serviceFor(child, 'compaction')
|
|
174
|
+
expect(parentPruner).toBeDefined()
|
|
175
|
+
expect(parentCompaction).toBeDefined()
|
|
176
|
+
expect(Object.is(
|
|
177
|
+
(childPruner as Traceable)[CORDIS_ORIGINAL],
|
|
178
|
+
(parentPruner as Traceable)[CORDIS_ORIGINAL],
|
|
179
|
+
)).toBe(true)
|
|
180
|
+
expect(Object.is(
|
|
181
|
+
(childCompaction as Traceable)[CORDIS_ORIGINAL],
|
|
182
|
+
(parentCompaction as Traceable)[CORDIS_ORIGINAL],
|
|
183
|
+
)).toBe(true)
|
|
184
|
+
expect(hasCompressionRetrieve(runtime, child)).toBe(true)
|
|
185
|
+
expect(hasCompactCommand(runtime, child)).toBe(true)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('publishes the daily Custom defaults from the plugin-owned settings row', async () => {
|
|
189
|
+
const runtime = await harness()
|
|
190
|
+
const namespace = settingsNamespace('context-compression')
|
|
191
|
+
const settings = runtime.settings.get(namespace) as CustomSettings
|
|
192
|
+
|
|
193
|
+
expect(settings.custom.history.trigger).toBe(500_000)
|
|
194
|
+
expect(settings.custom.tailTrim).toEqual({ enabled: false, trigger: 700_000 })
|
|
195
|
+
})
|
|
196
|
+
})
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { access, mkdir, mkdtemp, readFile, readdir, rm, stat, utimes, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
|
+
import { load } from 'js-yaml'
|
|
6
|
+
import type { AgentPreset } from '@deepseek-ai/dsh-agent-presets'
|
|
7
|
+
import {
|
|
8
|
+
decorateAgentPresets,
|
|
9
|
+
type CompressionModulePaths,
|
|
10
|
+
type OverlayableAgentPresets,
|
|
11
|
+
type PresetOverlayMetadataIo,
|
|
12
|
+
} from '../src/preset-overlay.ts'
|
|
13
|
+
|
|
14
|
+
const MODULES: CompressionModulePaths = {
|
|
15
|
+
compactionBasic: '/opt/context-selector/compaction-basic.js',
|
|
16
|
+
commandCompact: '/opt/context-selector/command-compact.js',
|
|
17
|
+
toolResultPruner: '/opt/context-selector/tool-result-pruner.js',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const roots: string[] = []
|
|
21
|
+
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true })))
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
async function fixture(): Promise<{
|
|
27
|
+
root: string
|
|
28
|
+
standard: AgentPreset
|
|
29
|
+
minimal: AgentPreset
|
|
30
|
+
}> {
|
|
31
|
+
const root = await mkdtemp(join(tmpdir(), 'dsh-selector-overlay-test-'))
|
|
32
|
+
roots.push(root)
|
|
33
|
+
const standardPath = join(root, 'standard', 'agent.cordis.yml')
|
|
34
|
+
const minimalPath = join(root, 'minimal', 'agent.cordis.yml')
|
|
35
|
+
await mkdir(dirname(standardPath), { recursive: true })
|
|
36
|
+
await mkdir(dirname(minimalPath), { recursive: true })
|
|
37
|
+
await writeFile(standardPath, [
|
|
38
|
+
'- id: persona',
|
|
39
|
+
" name: '/opt/preset/persona.js'",
|
|
40
|
+
'- id: tool-context-retrieve',
|
|
41
|
+
" name: '@deepseek-ai/dsh-tool-context-retrieve'",
|
|
42
|
+
'- id: compaction',
|
|
43
|
+
' name: cordis:group',
|
|
44
|
+
' group: true',
|
|
45
|
+
' isolate:',
|
|
46
|
+
' compaction: true',
|
|
47
|
+
' toolResultPruner: true',
|
|
48
|
+
' config:',
|
|
49
|
+
' - id: compaction-basic',
|
|
50
|
+
" name: '@deepseek-ai/dsh-compaction-basic'",
|
|
51
|
+
' - id: command-compact',
|
|
52
|
+
" name: '@deepseek-ai/dsh-command-compact'",
|
|
53
|
+
' - id: tool-result-pruner',
|
|
54
|
+
" name: '@deepseek-ai/dsh-compaction-tool-result-pruner'",
|
|
55
|
+
'',
|
|
56
|
+
].join('\n'))
|
|
57
|
+
await writeFile(minimalPath, "- id: persona\n name: '/opt/preset/minimal.js'\n")
|
|
58
|
+
return {
|
|
59
|
+
root,
|
|
60
|
+
standard: { id: 'standard', trust: 'system', path: standardPath },
|
|
61
|
+
minimal: { id: 'minimal', trust: 'system', path: minimalPath },
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class FakeAgentPresets implements OverlayableAgentPresets {
|
|
66
|
+
constructor(private readonly presets: ReadonlyMap<string, AgentPreset>) {}
|
|
67
|
+
|
|
68
|
+
async resolve(id = 'standard'): Promise<AgentPreset> {
|
|
69
|
+
const preset = this.presets.get(id)
|
|
70
|
+
if (preset === undefined) throw new Error(`unknown preset: ${id}`)
|
|
71
|
+
return preset
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async mount(_agentCtx: unknown, id?: string): Promise<AgentPreset> {
|
|
75
|
+
return await this.resolve(id)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async recompose(_agentCtx: unknown, id: string): Promise<AgentPreset> {
|
|
79
|
+
return await this.resolve(id)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async standingKeyFor(id?: string): Promise<string> {
|
|
83
|
+
return (await this.resolve(id)).path
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async read(id: string): Promise<string> {
|
|
87
|
+
return await readFile((await this.resolve(id)).path, 'utf8')
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function rowsAt(text: string): Array<Record<string, unknown>> {
|
|
92
|
+
const rows = load(text)
|
|
93
|
+
if (!Array.isArray(rows)) throw new TypeError('expected entry list')
|
|
94
|
+
return rows as Array<Record<string, unknown>>
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function flatten(rows: Array<Record<string, unknown>>): Array<Record<string, unknown>> {
|
|
98
|
+
return rows.flatMap((row) => {
|
|
99
|
+
const nested = row.group === true && Array.isArray(row.config)
|
|
100
|
+
? flatten(row.config as Array<Record<string, unknown>>)
|
|
101
|
+
: []
|
|
102
|
+
return [row, ...nested]
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
describe('plugin-owned preset overlay decorator', () => {
|
|
107
|
+
it('enhances only composition operations while authoring sees the source preset', async () => {
|
|
108
|
+
const { root, standard, minimal } = await fixture()
|
|
109
|
+
const presets = new FakeAgentPresets(new Map([
|
|
110
|
+
['standard', standard],
|
|
111
|
+
['minimal', minimal],
|
|
112
|
+
]))
|
|
113
|
+
const installation = decorateAgentPresets(presets, {
|
|
114
|
+
modules: MODULES,
|
|
115
|
+
excludedPresetIds: ['minimal'],
|
|
116
|
+
tempParent: root,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
expect((await presets.resolve('standard')).path).toBe(standard.path)
|
|
120
|
+
expect(await presets.read('standard')).toBe(await readFile(standard.path, 'utf8'))
|
|
121
|
+
|
|
122
|
+
const mounted = await presets.mount({}, 'standard')
|
|
123
|
+
expect(mounted.path).not.toBe(standard.path)
|
|
124
|
+
expect((await presets.recompose({}, 'standard')).path).toBe(mounted.path)
|
|
125
|
+
expect(await presets.standingKeyFor('standard')).toBe(mounted.path)
|
|
126
|
+
expect((await presets.mount({}, 'minimal')).path).toBe(minimal.path)
|
|
127
|
+
expect((await presets.resolve('standard')).path).toBe(standard.path)
|
|
128
|
+
|
|
129
|
+
const all = flatten(rowsAt(await readFile(mounted.path, 'utf8')))
|
|
130
|
+
expect(all.filter(row => row.id === 'tool-context-retrieve')).toHaveLength(1)
|
|
131
|
+
expect(all.filter(row => row.id === 'compaction')).toHaveLength(1)
|
|
132
|
+
expect(all.filter(row => row.id === 'compaction-basic')).toHaveLength(1)
|
|
133
|
+
expect(all.filter(row => row.id === 'command-compact')).toHaveLength(1)
|
|
134
|
+
expect(all.filter(row => row.id === 'tool-result-pruner')).toHaveLength(1)
|
|
135
|
+
expect(all.find(row => row.id === 'tool-context-retrieve')?.name)
|
|
136
|
+
.toBe('@deepseek-ai/dsh-tool-context-retrieve')
|
|
137
|
+
expect(all.find(row => row.id === 'tool-result-pruner')?.name).toBe(MODULES.toolResultPruner)
|
|
138
|
+
|
|
139
|
+
await installation.dispose()
|
|
140
|
+
expect((await presets.mount({}, 'standard')).path).toBe(standard.path)
|
|
141
|
+
await expect(access(dirname(mounted.path))).rejects.toThrow()
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('keeps direct concurrent resolve outside the operation-local overlay', async () => {
|
|
145
|
+
const { root, standard } = await fixture()
|
|
146
|
+
const presets = new FakeAgentPresets(new Map([['standard', standard]]))
|
|
147
|
+
const installation = decorateAgentPresets(presets, { modules: MODULES, tempParent: root })
|
|
148
|
+
|
|
149
|
+
const [mounted, resolved] = await Promise.all([
|
|
150
|
+
presets.mount({}, 'standard'),
|
|
151
|
+
presets.resolve('standard'),
|
|
152
|
+
])
|
|
153
|
+
|
|
154
|
+
expect(mounted.path).not.toBe(standard.path)
|
|
155
|
+
expect(resolved.path).toBe(standard.path)
|
|
156
|
+
await installation.dispose()
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('uses owner-only files and starts a new generation after source content changes', async () => {
|
|
160
|
+
const { root, standard } = await fixture()
|
|
161
|
+
const presets = new FakeAgentPresets(new Map([['standard', standard]]))
|
|
162
|
+
const installation = decorateAgentPresets(presets, { modules: MODULES, tempParent: root })
|
|
163
|
+
|
|
164
|
+
const first = await presets.mount({}, 'standard')
|
|
165
|
+
const same = await presets.mount({}, 'standard')
|
|
166
|
+
expect(same.path).toBe(first.path)
|
|
167
|
+
expect((await stat(dirname(first.path))).mode & 0o777).toBe(0o700)
|
|
168
|
+
expect((await stat(first.path)).mode & 0o777).toBe(0o600)
|
|
169
|
+
|
|
170
|
+
await writeFile(standard.path, `${await readFile(standard.path, 'utf8')}\n- id: later\n name: '/opt/preset/later.js'\n`)
|
|
171
|
+
const changed = await presets.mount({}, 'standard')
|
|
172
|
+
expect(changed.path).not.toBe(first.path)
|
|
173
|
+
expect(await readFile(changed.path, 'utf8')).toContain('id: later')
|
|
174
|
+
|
|
175
|
+
await installation.dispose()
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('shares one decoration across duplicate Bundle and built-in loads', async () => {
|
|
179
|
+
const { root, standard } = await fixture()
|
|
180
|
+
const presets = new FakeAgentPresets(new Map([['standard', standard]]))
|
|
181
|
+
const first = decorateAgentPresets(presets, { modules: MODULES, tempParent: root })
|
|
182
|
+
const second = decorateAgentPresets(presets, { modules: MODULES, tempParent: root })
|
|
183
|
+
|
|
184
|
+
const mounted = await presets.mount({}, 'standard')
|
|
185
|
+
const all = flatten(rowsAt(await readFile(mounted.path, 'utf8')))
|
|
186
|
+
expect(all.filter(row => row.id === 'compaction')).toHaveLength(1)
|
|
187
|
+
expect(all.filter(row => row.id === 'tool-result-pruner')).toHaveLength(1)
|
|
188
|
+
|
|
189
|
+
await first.dispose()
|
|
190
|
+
expect((await presets.mount({}, 'standard')).path).toBe(mounted.path)
|
|
191
|
+
|
|
192
|
+
await second.dispose()
|
|
193
|
+
expect((await presets.mount({}, 'standard')).path).toBe(standard.path)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('does not share one decoration across different metadata policies', async () => {
|
|
197
|
+
const { root, standard } = await fixture()
|
|
198
|
+
const presets = new FakeAgentPresets(new Map([['standard', standard]]))
|
|
199
|
+
const metadataIo = (): PresetOverlayMetadataIo => ({
|
|
200
|
+
async setTimes(path, stamp) {
|
|
201
|
+
await utimes(path, stamp, stamp)
|
|
202
|
+
},
|
|
203
|
+
async read(path) {
|
|
204
|
+
const observed = await stat(path)
|
|
205
|
+
return { mtimeMs: observed.mtimeMs, size: observed.size }
|
|
206
|
+
},
|
|
207
|
+
})
|
|
208
|
+
const first = decorateAgentPresets(presets, {
|
|
209
|
+
modules: MODULES,
|
|
210
|
+
tempParent: root,
|
|
211
|
+
metadataIo: metadataIo(),
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
expect(() => decorateAgentPresets(presets, {
|
|
215
|
+
modules: MODULES,
|
|
216
|
+
tempParent: root,
|
|
217
|
+
metadataIo: metadataIo(),
|
|
218
|
+
})).toThrow(/different compression overlay/u)
|
|
219
|
+
|
|
220
|
+
await first.dispose()
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('removes staging files when metadata preparation fails before publish', async () => {
|
|
224
|
+
const { root, standard } = await fixture()
|
|
225
|
+
const presets = new FakeAgentPresets(new Map([['standard', standard]]))
|
|
226
|
+
const installation = decorateAgentPresets(presets, {
|
|
227
|
+
modules: MODULES,
|
|
228
|
+
tempParent: root,
|
|
229
|
+
metadataIo: {
|
|
230
|
+
setTimes: () => Promise.reject(new Error('synthetic metadata failure')),
|
|
231
|
+
read: () => Promise.reject(new Error('metadata read must not run')),
|
|
232
|
+
},
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
await expect(presets.mount({}, 'standard')).rejects.toThrow(/synthetic metadata failure/u)
|
|
236
|
+
const generatedDirectories = (await readdir(root, { withFileTypes: true }))
|
|
237
|
+
.filter(entry => entry.isDirectory() && entry.name.startsWith('dsh-context-compression-presets-'))
|
|
238
|
+
expect(generatedDirectories).toHaveLength(1)
|
|
239
|
+
expect(await readdir(join(root, generatedDirectories[0]!.name))).toEqual([])
|
|
240
|
+
|
|
241
|
+
await installation.dispose()
|
|
242
|
+
})
|
|
243
|
+
})
|