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,2531 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { Context } from '@deepseek-ai/cordis'
|
|
3
|
+
import {
|
|
4
|
+
CallId,
|
|
5
|
+
createMessage,
|
|
6
|
+
createUserMessage,
|
|
7
|
+
createToolResultMessage,
|
|
8
|
+
LlmAdapter,
|
|
9
|
+
type ContentBlock,
|
|
10
|
+
type GenerateOptions,
|
|
11
|
+
type LlmResolvedModelInfo,
|
|
12
|
+
type StreamChunk,
|
|
13
|
+
} from '@deepseek-ai/dsh-llm'
|
|
14
|
+
import LlmRuntime from '@deepseek-ai/dsh-llm'
|
|
15
|
+
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
|
16
|
+
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
|
|
17
|
+
import BasicCompactionEngine from '@deepseek-ai/dsh-compaction-basic'
|
|
18
|
+
import SessionStore, {
|
|
19
|
+
Session,
|
|
20
|
+
SessionId,
|
|
21
|
+
canonicalHeader,
|
|
22
|
+
} from '@deepseek-ai/dsh-session'
|
|
23
|
+
import {
|
|
24
|
+
agentEvents,
|
|
25
|
+
Inbox,
|
|
26
|
+
type Agent,
|
|
27
|
+
} from '@deepseek-ai/dsh-agent'
|
|
28
|
+
import {
|
|
29
|
+
SettingsProvider,
|
|
30
|
+
settingsNamespace,
|
|
31
|
+
type SettingsNamespace,
|
|
32
|
+
} from '@deepseek-ai/dsh-settings'
|
|
33
|
+
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
|
34
|
+
import TokenMeter from '@deepseek-ai/dsh-token-meter'
|
|
35
|
+
import ToolRuntime from '@deepseek-ai/dsh-tools'
|
|
36
|
+
import { CompactionId } from '@deepseek-ai/dsh-compaction'
|
|
37
|
+
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
|
|
38
|
+
import * as SelectorHost from '../../../src/index.ts'
|
|
39
|
+
import * as RuntimeInvariant from '../../../src/invariant.ts'
|
|
40
|
+
import ToolResultPruner, {
|
|
41
|
+
CONTEXT_COMPRESSION_SETTINGS_NAMESPACE,
|
|
42
|
+
DEFAULT_CUSTOM_COMPRESSION_POLICY,
|
|
43
|
+
resolveConfig,
|
|
44
|
+
resolvePolicy,
|
|
45
|
+
} from '../../../src/pruner.ts'
|
|
46
|
+
import type { CustomCompressionPolicy } from '../../../src/pruner.ts'
|
|
47
|
+
import { DEEPSEEK_V4_TOKENIZER_ARTIFACT, deepSeekV4TokenizerForModel } from '../../../src/deepseek-v4-tokenizer.ts'
|
|
48
|
+
import {
|
|
49
|
+
DEEPSEEK_VISION_DEFAULT_IMAGE_TOKENS,
|
|
50
|
+
DEEPSEEK_VISION_IMAGE_ESTIMATOR,
|
|
51
|
+
DEEPSEEK_VISION_PROJECTION,
|
|
52
|
+
deepSeekVisionImageBlockTokens,
|
|
53
|
+
deepSeekVisionImageGrid,
|
|
54
|
+
} from '../../../src/runtime/deepseek-v4-vision-tokens.ts'
|
|
55
|
+
import { validatePublishedTailTrim } from '../../../src/runtime/tail-trim.ts'
|
|
56
|
+
import { measureForCompaction } from '../../../src/runtime/measurement.ts'
|
|
57
|
+
import {
|
|
58
|
+
COMPRESSION_AUDIT_PREFIX,
|
|
59
|
+
type CompressionAuditRecord,
|
|
60
|
+
type CompressionRewriteAuditRecord,
|
|
61
|
+
} from '../../../src/runtime/audit.ts'
|
|
62
|
+
|
|
63
|
+
const MODEL = 'deepseek-v4-flash'
|
|
64
|
+
const VISION_MODEL = 'deepseek-v4-flash-vision-exp'
|
|
65
|
+
const activeContexts: Context[] = []
|
|
66
|
+
|
|
67
|
+
/** Metadata-only raster image block; never carries image bytes. */
|
|
68
|
+
function imageBlock(width: number, height: number): ContentBlock {
|
|
69
|
+
// AttachmentId is an opaque brand over string; tests construct the durable
|
|
70
|
+
// reference structurally without importing the attachment package.
|
|
71
|
+
return {
|
|
72
|
+
type: 'image',
|
|
73
|
+
attachment: {
|
|
74
|
+
attachmentId: `test-image-${String(width)}x${String(height)}`,
|
|
75
|
+
mediaType: 'image/png',
|
|
76
|
+
bytes: 1_024,
|
|
77
|
+
width,
|
|
78
|
+
height,
|
|
79
|
+
},
|
|
80
|
+
} as ContentBlock
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
afterEach(async () => {
|
|
84
|
+
for (const ctx of activeContexts.splice(0)) await ctx.fiber.dispose()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
class TestSettings extends SettingsProvider {
|
|
88
|
+
readonly writable = true
|
|
89
|
+
private readonly stored: Record<string, unknown> = {}
|
|
90
|
+
|
|
91
|
+
protected override load(): Promise<Record<string, unknown>> {
|
|
92
|
+
return Promise.resolve(structuredClone(this.stored))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
protected override persist(namespace: SettingsNamespace, section: Record<string, unknown>): Promise<void> {
|
|
96
|
+
this.stored[namespace] = structuredClone(section)
|
|
97
|
+
return Promise.resolve()
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
class NativeSummaryAdapter extends LlmAdapter {
|
|
102
|
+
constructor(
|
|
103
|
+
private readonly responses: string[],
|
|
104
|
+
private readonly contextWindow: number,
|
|
105
|
+
) {
|
|
106
|
+
super()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
override resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo> {
|
|
110
|
+
return Promise.resolve({
|
|
111
|
+
provider,
|
|
112
|
+
id: model,
|
|
113
|
+
name: model,
|
|
114
|
+
context: { contextWindow: this.contextWindow },
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
override async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
|
119
|
+
options.signal?.throwIfAborted()
|
|
120
|
+
const text = this.responses.shift()
|
|
121
|
+
if (text === undefined) throw new Error('NativeSummaryAdapter: response script exhausted')
|
|
122
|
+
yield { type: 'block-start', index: 0, blockType: 'text' }
|
|
123
|
+
yield { type: 'text-delta', index: 0, text }
|
|
124
|
+
yield { type: 'block-end', index: 0, block: { type: 'text', text } }
|
|
125
|
+
yield { type: 'usage', usage: { inputTokens: 10, outputTokens: text.length } }
|
|
126
|
+
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function runtimeContext(): Promise<Context> {
|
|
131
|
+
const ctx = new Context()
|
|
132
|
+
activeContexts.push(ctx)
|
|
133
|
+
await ctx.plugin(SessionStore)
|
|
134
|
+
await ctx.plugin(SystemPrompt)
|
|
135
|
+
await ctx.plugin(ToolRuntime)
|
|
136
|
+
await ctx.plugin(TokenMeter)
|
|
137
|
+
return ctx
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function captureAudit(ctx: Context): {
|
|
141
|
+
records(): CompressionAuditRecord[]
|
|
142
|
+
} {
|
|
143
|
+
const info = vi.spyOn(ctx.logger, 'info').mockImplementation(() => ctx.logger)
|
|
144
|
+
return {
|
|
145
|
+
records: () => info.mock.calls.flatMap((call) => {
|
|
146
|
+
const line = String(call[0])
|
|
147
|
+
return line.startsWith(COMPRESSION_AUDIT_PREFIX)
|
|
148
|
+
? [JSON.parse(line.slice(COMPRESSION_AUDIT_PREFIX.length)) as CompressionAuditRecord]
|
|
149
|
+
: []
|
|
150
|
+
}),
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function rewrites(records: readonly CompressionAuditRecord[]): CompressionRewriteAuditRecord[] {
|
|
155
|
+
return records.filter((record): record is CompressionRewriteAuditRecord => record.kind === 'rewrite')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function appendToolTurn(
|
|
159
|
+
session: Session,
|
|
160
|
+
turn: number,
|
|
161
|
+
text: string,
|
|
162
|
+
closeTurn: boolean,
|
|
163
|
+
userText?: string,
|
|
164
|
+
provider = 'deepseek',
|
|
165
|
+
model: string = MODEL,
|
|
166
|
+
): { readonly assistantSeq: number; readonly resultSeq: number } {
|
|
167
|
+
const callId = CallId(`call-${String(turn)}`)
|
|
168
|
+
session.append('turn/start', { turn })
|
|
169
|
+
if (session.requestHeader() === undefined) {
|
|
170
|
+
session.append('request/header', {
|
|
171
|
+
reason: 'initial',
|
|
172
|
+
header: canonicalHeader({ config: { provider, model } }),
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
if (userText !== undefined) {
|
|
176
|
+
session.append('user/message', createUserMessage({
|
|
177
|
+
content: [{ type: 'text', text: userText }],
|
|
178
|
+
source: { kind: 'user' },
|
|
179
|
+
}), { surfaceOp: 'append' })
|
|
180
|
+
}
|
|
181
|
+
session.append('step/start', { turn, step: 1 })
|
|
182
|
+
const assistant = session.append('assistant/message', {
|
|
183
|
+
turn,
|
|
184
|
+
step: 1,
|
|
185
|
+
message: createMessage({
|
|
186
|
+
role: 'assistant',
|
|
187
|
+
content: [{ type: 'tool-call', id: callId, name: 'bash', arguments: '{}' }],
|
|
188
|
+
source: { kind: 'model', provider, model },
|
|
189
|
+
}),
|
|
190
|
+
}, { surfaceOp: 'append' })
|
|
191
|
+
session.append('tool/call', { turn, step: 1, callId, name: 'bash', arguments: '{}' })
|
|
192
|
+
const result = session.append('tool/result', {
|
|
193
|
+
turn,
|
|
194
|
+
step: 1,
|
|
195
|
+
message: createToolResultMessage({
|
|
196
|
+
callId,
|
|
197
|
+
content: [{ type: 'text', text }],
|
|
198
|
+
isError: false,
|
|
199
|
+
}),
|
|
200
|
+
}, { surfaceOp: 'append' })
|
|
201
|
+
session.append('step/end', { turn, step: 1 })
|
|
202
|
+
if (closeTurn) session.append('turn/end', { turn, reason: { kind: 'completed' } })
|
|
203
|
+
return { assistantSeq: assistant.seq, resultSeq: result.seq }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function appendToolBatchTurn(
|
|
207
|
+
session: Session,
|
|
208
|
+
turn: number,
|
|
209
|
+
texts: readonly string[],
|
|
210
|
+
closeTurn: boolean,
|
|
211
|
+
userText?: string,
|
|
212
|
+
): { readonly assistantSeq: number; readonly resultSeqs: readonly number[] } {
|
|
213
|
+
const calls = texts.map((_, index) => ({
|
|
214
|
+
id: CallId(`call-${String(turn)}-${String(index + 1)}`),
|
|
215
|
+
name: 'bash',
|
|
216
|
+
}))
|
|
217
|
+
session.append('turn/start', { turn })
|
|
218
|
+
if (session.requestHeader() === undefined) {
|
|
219
|
+
session.append('request/header', {
|
|
220
|
+
reason: 'initial',
|
|
221
|
+
header: canonicalHeader({ config: { provider: 'deepseek', model: MODEL } }),
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
if (userText !== undefined) {
|
|
225
|
+
session.append('user/message', createUserMessage({
|
|
226
|
+
content: [{ type: 'text', text: userText }],
|
|
227
|
+
source: { kind: 'user' },
|
|
228
|
+
}), { surfaceOp: 'append' })
|
|
229
|
+
}
|
|
230
|
+
session.append('step/start', { turn, step: 1 })
|
|
231
|
+
const assistant = session.append('assistant/message', {
|
|
232
|
+
turn,
|
|
233
|
+
step: 1,
|
|
234
|
+
message: createMessage({
|
|
235
|
+
role: 'assistant',
|
|
236
|
+
content: calls.map(call => ({
|
|
237
|
+
type: 'tool-call' as const,
|
|
238
|
+
id: call.id,
|
|
239
|
+
name: call.name,
|
|
240
|
+
arguments: '{}',
|
|
241
|
+
})),
|
|
242
|
+
source: { kind: 'model', provider: 'deepseek', model: MODEL },
|
|
243
|
+
}),
|
|
244
|
+
}, { surfaceOp: 'append' })
|
|
245
|
+
const resultSeqs: number[] = []
|
|
246
|
+
for (const [index, call] of calls.entries()) {
|
|
247
|
+
session.append('tool/call', {
|
|
248
|
+
turn,
|
|
249
|
+
step: 1,
|
|
250
|
+
callId: call.id,
|
|
251
|
+
name: call.name,
|
|
252
|
+
arguments: '{}',
|
|
253
|
+
})
|
|
254
|
+
const result = session.append('tool/result', {
|
|
255
|
+
turn,
|
|
256
|
+
step: 1,
|
|
257
|
+
message: createToolResultMessage({
|
|
258
|
+
callId: call.id,
|
|
259
|
+
content: [{ type: 'text', text: texts[index] ?? '' }],
|
|
260
|
+
isError: false,
|
|
261
|
+
}),
|
|
262
|
+
}, { surfaceOp: 'append' })
|
|
263
|
+
resultSeqs.push(result.seq)
|
|
264
|
+
}
|
|
265
|
+
session.append('step/end', { turn, step: 1 })
|
|
266
|
+
if (closeTurn) session.append('turn/end', { turn, reason: { kind: 'completed' } })
|
|
267
|
+
return { assistantSeq: assistant.seq, resultSeqs }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function stubAgent(ctx: Context, session: Session): Agent {
|
|
271
|
+
return {
|
|
272
|
+
id: session.id,
|
|
273
|
+
options: {},
|
|
274
|
+
session,
|
|
275
|
+
inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
|
|
276
|
+
status: 'idle',
|
|
277
|
+
ctx,
|
|
278
|
+
send: () => {},
|
|
279
|
+
followup: () => {},
|
|
280
|
+
steer: () => ({ outcome: Promise.resolve({ status: 'rejected' as const }) }),
|
|
281
|
+
inject: () => {},
|
|
282
|
+
cancel() {},
|
|
283
|
+
runMaintenance: task => task(new AbortController().signal),
|
|
284
|
+
whenIdle: () => Promise.resolve(),
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
describe('standalone runtime on published Harness APIs', () => {
|
|
289
|
+
it('loads the pinned official tokenizers and refuses unverified model ids', () => {
|
|
290
|
+
const tokenizer = deepSeekV4TokenizerForModel(MODEL)
|
|
291
|
+
expect(tokenizer?.countText('DeepSeek Harness').tokens).toBeGreaterThan(0)
|
|
292
|
+
expect(deepSeekV4TokenizerForModel('deepseek-v4-flash-vision-exp')?.countText('DeepSeek Harness')).toMatchObject({
|
|
293
|
+
kind: 'exact-tokenizer',
|
|
294
|
+
tokenizerId: 'deepseek-ai/DeepSeek-V4-Flash-Vision-Exp',
|
|
295
|
+
tokenizerRevision: '6821d6ad3681a4b137b066b76094fa82ebd0a380',
|
|
296
|
+
})
|
|
297
|
+
expect(deepSeekV4TokenizerForModel('deepseek-v4-flash-vision')).toBeUndefined()
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it.each([
|
|
301
|
+
['off', Number.MAX_SAFE_INTEGER],
|
|
302
|
+
['native', Number.MAX_SAFE_INTEGER],
|
|
303
|
+
['balanced', 500_000],
|
|
304
|
+
['cache-strict', 600_000],
|
|
305
|
+
['savings', 400_000],
|
|
306
|
+
['adaptive', 500_000],
|
|
307
|
+
] as const)('resolves %s with the shared 10-call and 64k token-tail History working set', (profile, trigger) => {
|
|
308
|
+
const policy = resolvePolicy(resolveConfig(), profile)
|
|
309
|
+
|
|
310
|
+
expect(policy.historyTriggerTokens).toBe(trigger)
|
|
311
|
+
expect(policy.historyKeepRecentToolCalls).toBe(10)
|
|
312
|
+
expect(policy.historyKeepRecentTokens).toBe(64_000)
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it.each([
|
|
316
|
+
'balanced',
|
|
317
|
+
'cache-strict',
|
|
318
|
+
'savings',
|
|
319
|
+
'adaptive',
|
|
320
|
+
'custom',
|
|
321
|
+
] as const)('disables native head/middle/tail pruning for %s', (profile) => {
|
|
322
|
+
expect(resolvePolicy(resolveConfig(), profile).nativeToolResultEnabled).toBe(false)
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
it('enables native head/middle/tail pruning only for the Native profile', () => {
|
|
326
|
+
expect(resolvePolicy(resolveConfig(), 'native').nativeToolResultEnabled).toBe(true)
|
|
327
|
+
expect(resolvePolicy(resolveConfig(), 'off').nativeToolResultEnabled).toBe(false)
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('lands a standard compaction/prune plus tool-result replacement without custom events', async () => {
|
|
331
|
+
const ctx = await runtimeContext()
|
|
332
|
+
const audit = captureAudit(ctx)
|
|
333
|
+
await ctx.plugin(ToolResultPruner, {
|
|
334
|
+
profile: 'native',
|
|
335
|
+
nativeTriggerTokens: 100,
|
|
336
|
+
nativeTargetTokens: 64,
|
|
337
|
+
headChars: 8,
|
|
338
|
+
tailChars: 8,
|
|
339
|
+
}).await()
|
|
340
|
+
const pruner = ctx.toolResultPruner
|
|
341
|
+
const session = Session.create(SessionId('public-native-prune'))
|
|
342
|
+
const source = appendToolTurn(session, 1, 'x'.repeat(8_000), false)
|
|
343
|
+
const view = measureForCompaction(ctx, session)
|
|
344
|
+
expect(ctx.tools.get('context_compression_retrieve')).toBeDefined()
|
|
345
|
+
expect(view.currentSurface.kind).toBe('exact-tokenizer')
|
|
346
|
+
if (view.currentSurface.kind === 'exact-tokenizer') {
|
|
347
|
+
expect(view.currentSurface.tokens).toBeGreaterThan(100)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const result = pruner.pruneSession(session, { stage: 'pressure' })
|
|
351
|
+
|
|
352
|
+
expect(result.pruned).toHaveLength(1)
|
|
353
|
+
const manifest = session.events.at(-2)
|
|
354
|
+
const replacement = session.events.at(-1)
|
|
355
|
+
expect(manifest?.type).toBe('compaction/prune')
|
|
356
|
+
expect(session.events.some(event => event.type === ('compaction/group-trim' as string))).toBe(false)
|
|
357
|
+
expect(replacement?.type).toBe('tool/result')
|
|
358
|
+
if (replacement?.type !== 'tool/result') throw new Error('Native prune did not append a tool result replacement')
|
|
359
|
+
expect(replacement.surfaceOp).toEqual({
|
|
360
|
+
op: 'replace',
|
|
361
|
+
start: source.resultSeq,
|
|
362
|
+
end: source.resultSeq,
|
|
363
|
+
})
|
|
364
|
+
expect(replacement.sourceEventSeqs).toContain(source.resultSeq)
|
|
365
|
+
expect(rewrites(audit.records())).toContainEqual(expect.objectContaining({
|
|
366
|
+
component: 'native-tool-result',
|
|
367
|
+
stage: 'pressure',
|
|
368
|
+
manifestSeq: manifest?.seq,
|
|
369
|
+
replacementSeq: replacement?.seq,
|
|
370
|
+
tokensBefore: expect.any(Number),
|
|
371
|
+
tokensAfter: expect.any(Number),
|
|
372
|
+
}))
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
it('proves isolated Fresh with exact before/after audit evidence', async () => {
|
|
376
|
+
const ctx = await runtimeContext()
|
|
377
|
+
const audit = captureAudit(ctx)
|
|
378
|
+
await ctx.plugin(ToolResultPruner, {
|
|
379
|
+
profile: 'balanced',
|
|
380
|
+
freshTriggerTokens: 100,
|
|
381
|
+
freshTargetTokens: 64,
|
|
382
|
+
aggregateTriggerTokens: 100_000,
|
|
383
|
+
aggregateTargetTokens: 90_000,
|
|
384
|
+
historyTriggerTokens: 100_000,
|
|
385
|
+
}).await()
|
|
386
|
+
const session = Session.create(SessionId('public-fresh-e2e'))
|
|
387
|
+
appendToolTurn(session, 1, 'fresh evidence '.repeat(1_000), false, undefined, 'deepseek-official')
|
|
388
|
+
|
|
389
|
+
const result = ctx.toolResultPruner.pruneSession(session, {
|
|
390
|
+
stage: 'fresh',
|
|
391
|
+
freshTurn: 1,
|
|
392
|
+
freshStep: 1,
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
expect(result.pruned).toHaveLength(1)
|
|
396
|
+
const record = rewrites(audit.records()).find(entry => entry.component === 'fresh')
|
|
397
|
+
expect(record).toMatchObject({
|
|
398
|
+
stage: 'fresh',
|
|
399
|
+
reducer: expect.any(String),
|
|
400
|
+
manifestEventType: 'compaction/prune',
|
|
401
|
+
tokenizerId: expect.any(String),
|
|
402
|
+
tokenizerRevision: expect.any(String),
|
|
403
|
+
})
|
|
404
|
+
expect(record?.tokensBefore).toBeGreaterThan(100)
|
|
405
|
+
expect(record?.tokensAfter).toBeLessThanOrEqual(64)
|
|
406
|
+
expect(record?.tokensRemoved).toBe((record?.tokensBefore ?? 0) - (record?.tokensAfter ?? 0))
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
it('proves isolated Aggregate while Fresh and History remain below their gates', async () => {
|
|
410
|
+
const ctx = await runtimeContext()
|
|
411
|
+
const audit = captureAudit(ctx)
|
|
412
|
+
await ctx.plugin(ToolResultPruner, {
|
|
413
|
+
profile: 'balanced',
|
|
414
|
+
freshTriggerTokens: 100_000,
|
|
415
|
+
freshTargetTokens: 90_000,
|
|
416
|
+
aggregateTriggerTokens: 100,
|
|
417
|
+
aggregateTargetTokens: 64,
|
|
418
|
+
historyTriggerTokens: 100_000,
|
|
419
|
+
}).await()
|
|
420
|
+
const session = Session.create(SessionId('public-aggregate-e2e'))
|
|
421
|
+
appendToolTurn(session, 1, 'aggregate evidence '.repeat(1_000), false, undefined, 'deepseek-official')
|
|
422
|
+
|
|
423
|
+
const result = ctx.toolResultPruner.pruneSession(session, {
|
|
424
|
+
stage: 'fresh',
|
|
425
|
+
freshTurn: 1,
|
|
426
|
+
freshStep: 1,
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
expect(result.pruned).toHaveLength(1)
|
|
430
|
+
const record = rewrites(audit.records()).find(entry => entry.component === 'aggregate')
|
|
431
|
+
expect(record).toMatchObject({
|
|
432
|
+
stage: 'fresh',
|
|
433
|
+
reducer: 'fresh-step-aggregate',
|
|
434
|
+
manifestEventType: 'compaction/prune',
|
|
435
|
+
})
|
|
436
|
+
expect(record?.tokensBefore).toBeGreaterThan(100)
|
|
437
|
+
expect(record?.tokensAfter).toBeLessThanOrEqual(64)
|
|
438
|
+
expect(rewrites(audit.records()).some(entry => entry.component === 'fresh')).toBe(false)
|
|
439
|
+
expect(rewrites(audit.records()).some(entry => entry.component === 'history')).toBe(false)
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
it('ages only results outside the recent tool-call and token-tail working set', async () => {
|
|
443
|
+
const ctx = await runtimeContext()
|
|
444
|
+
const session = Session.create(SessionId('public-history-tool-call-working-set'))
|
|
445
|
+
const batch = appendToolBatchTurn(
|
|
446
|
+
session,
|
|
447
|
+
1,
|
|
448
|
+
Array.from({ length: 5 }, () => 'working-set evidence '.repeat(1_000)),
|
|
449
|
+
true,
|
|
450
|
+
)
|
|
451
|
+
session.append('turn/start', { turn: 2 })
|
|
452
|
+
const view = measureForCompaction(ctx, session)
|
|
453
|
+
const counts = batch.resultSeqs.map((seq) => {
|
|
454
|
+
const entry = view.measuredNodes.find(node => node.seq === seq)?.count
|
|
455
|
+
if (entry?.kind !== 'exact-tokenizer') throw new Error('working-set test needs exact result counts')
|
|
456
|
+
return entry.tokens
|
|
457
|
+
})
|
|
458
|
+
const recentTailTokens = counts.slice(-3).reduce((sum, tokens) => sum + tokens, 0) - 1
|
|
459
|
+
const totalToolTokens = counts.reduce((sum, tokens) => sum + tokens, 0)
|
|
460
|
+
|
|
461
|
+
await ctx.plugin(ToolResultPruner, {
|
|
462
|
+
profile: 'balanced',
|
|
463
|
+
freshTriggerTokens: 100_000,
|
|
464
|
+
freshTargetTokens: 90_000,
|
|
465
|
+
aggregateTriggerTokens: 100_000,
|
|
466
|
+
aggregateTargetTokens: 90_000,
|
|
467
|
+
// The strict required-reclaim gate demands one batch pull tool tokens
|
|
468
|
+
// back below the trigger; 72% leaves exactly the two oldest results as
|
|
469
|
+
// the reclaimable margin above the placeholder residue.
|
|
470
|
+
historyTriggerTokens: Math.floor(totalToolTokens * 0.72),
|
|
471
|
+
historyKeepRecentToolCalls: 2,
|
|
472
|
+
historyKeepRecentTokens: recentTailTokens,
|
|
473
|
+
historyMinReclaimTokens: 1,
|
|
474
|
+
}).await()
|
|
475
|
+
|
|
476
|
+
const result = ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
|
|
477
|
+
|
|
478
|
+
expect(result.pruned).toHaveLength(2)
|
|
479
|
+
expect(result.pruned.map(entry => entry.originalSeq)).toEqual(batch.resultSeqs.slice(0, 2))
|
|
480
|
+
expect(result.pruned.map(entry => entry.originalSeq)).not.toContain(batch.resultSeqs.at(-1))
|
|
481
|
+
expect(result.pruned.map(entry => entry.originalSeq)).not.toContain(batch.resultSeqs.at(-2))
|
|
482
|
+
expect(result.pruned.map(entry => entry.originalSeq)).not.toContain(batch.resultSeqs.at(-3))
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
it('proves isolated routine History against completed old turns', async () => {
|
|
486
|
+
const ctx = await runtimeContext()
|
|
487
|
+
const audit = captureAudit(ctx)
|
|
488
|
+
await ctx.plugin(ToolResultPruner, {
|
|
489
|
+
profile: 'savings',
|
|
490
|
+
freshTriggerTokens: 100_000,
|
|
491
|
+
freshTargetTokens: 90_000,
|
|
492
|
+
aggregateTriggerTokens: 100_000,
|
|
493
|
+
aggregateTargetTokens: 90_000,
|
|
494
|
+
historyTriggerTokens: 1_200,
|
|
495
|
+
historyKeepRecentToolCalls: 0,
|
|
496
|
+
historyKeepRecentTokens: 1,
|
|
497
|
+
historyMinReclaimTokens: 1,
|
|
498
|
+
}).await()
|
|
499
|
+
const session = Session.create(SessionId('public-history-e2e'))
|
|
500
|
+
appendToolTurn(session, 1, 'old historical evidence '.repeat(1_000), true)
|
|
501
|
+
// The one-token recent working-set budget protects at least the newest
|
|
502
|
+
// completed result, so provide a second completed turn and age the oldest.
|
|
503
|
+
appendToolTurn(session, 2, 'newer protected evidence', true)
|
|
504
|
+
session.append('turn/start', { turn: 3 })
|
|
505
|
+
|
|
506
|
+
const result = ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
|
|
507
|
+
|
|
508
|
+
expect(result.pruned).toHaveLength(1)
|
|
509
|
+
const record = rewrites(audit.records()).find(entry => entry.component === 'history')
|
|
510
|
+
expect(record).toMatchObject({
|
|
511
|
+
stage: 'pressure',
|
|
512
|
+
historyMode: 'routine',
|
|
513
|
+
manifestEventType: 'compaction/prune',
|
|
514
|
+
})
|
|
515
|
+
expect(record?.tokensBefore).toBeGreaterThan(100)
|
|
516
|
+
expect(record?.tokensAfter).toBeLessThan(record?.tokensBefore ?? 0)
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
it('moves the capacity-pressure gate with the frozen Auto Compact threshold', async () => {
|
|
520
|
+
const ctx = await runtimeContext()
|
|
521
|
+
await ctx.plugin(TestSettings).await()
|
|
522
|
+
await ctx.plugin(SelectorHost).await()
|
|
523
|
+
const audit = captureAudit(ctx)
|
|
524
|
+
const namespace = settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
|
|
525
|
+
|
|
526
|
+
// A high threshold delays the micro-compact last-chance gate past the old
|
|
527
|
+
// fixed 0.7 ratio: pressure between 0.7*C and D must NOT age history.
|
|
528
|
+
await ctx.settings.update(namespace, { profile: 'cache-strict', autoCompact: { thresholdPercent: 90 } })
|
|
529
|
+
await ctx.plugin(ToolResultPruner, {
|
|
530
|
+
profile: 'cache-strict',
|
|
531
|
+
freshTriggerTokens: 100_000,
|
|
532
|
+
freshTargetTokens: 90_000,
|
|
533
|
+
aggregateTriggerTokens: 100_000,
|
|
534
|
+
aggregateTargetTokens: 90_000,
|
|
535
|
+
}).await()
|
|
536
|
+
const delayed = Session.create(SessionId('public-autocompact-90-delayed'))
|
|
537
|
+
appendToolTurn(delayed, 1, 'delayed gate evidence '.repeat(600), true)
|
|
538
|
+
appendToolTurn(delayed, 2, 'newest protected result', true)
|
|
539
|
+
const delayedTotal = measureForCompaction(ctx, delayed).totalTokens
|
|
540
|
+
const delayedWindow = Math.floor(delayedTotal / 0.72)
|
|
541
|
+
delayed.append('request/context', { provider: 'deepseek', model: MODEL, contextWindow: delayedWindow })
|
|
542
|
+
delayed.append('turn/start', { turn: 3 })
|
|
543
|
+
ctx.toolResultPruner.pruneSession(delayed, { stage: 'pressure' })
|
|
544
|
+
expect(rewrites(audit.records()).some(record =>
|
|
545
|
+
record.sessionId === String(delayed.id) && record.component === 'history')).toBe(false)
|
|
546
|
+
expect(audit.records()).toContainEqual(expect.objectContaining({
|
|
547
|
+
kind: 'component-evaluation',
|
|
548
|
+
sessionId: String(delayed.id),
|
|
549
|
+
component: 'history',
|
|
550
|
+
reason: 'below-micro-deadline',
|
|
551
|
+
triggerTokens: Math.floor(Math.floor(delayedWindow * 90 / 100) * 0.875),
|
|
552
|
+
}))
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
it('ages History earlier when the frozen Auto Compact threshold is low', async () => {
|
|
556
|
+
const ctx = await runtimeContext()
|
|
557
|
+
await ctx.plugin(TestSettings).await()
|
|
558
|
+
await ctx.plugin(SelectorHost).await()
|
|
559
|
+
const audit = captureAudit(ctx)
|
|
560
|
+
const namespace = settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
|
|
561
|
+
|
|
562
|
+
// A low threshold pulls the micro-compact deadline below the old fixed
|
|
563
|
+
// 0.7 ratio: pressure between D and 0.7*C must age history now.
|
|
564
|
+
await ctx.settings.update(namespace, { profile: 'cache-strict', autoCompact: { thresholdPercent: 50 } })
|
|
565
|
+
await ctx.plugin(ToolResultPruner, {
|
|
566
|
+
profile: 'cache-strict',
|
|
567
|
+
freshTriggerTokens: 100_000,
|
|
568
|
+
freshTargetTokens: 90_000,
|
|
569
|
+
aggregateTriggerTokens: 100_000,
|
|
570
|
+
aggregateTargetTokens: 90_000,
|
|
571
|
+
historyTriggerTokens: 400,
|
|
572
|
+
historyKeepRecentToolCalls: 0,
|
|
573
|
+
historyKeepRecentTokens: 1,
|
|
574
|
+
historyMinReclaimTokens: 1,
|
|
575
|
+
}).await()
|
|
576
|
+
const early = Session.create(SessionId('public-autocompact-50-early'))
|
|
577
|
+
appendToolTurn(early, 1, 'early gate evidence '.repeat(600), true)
|
|
578
|
+
appendToolTurn(early, 2, 'newest protected result', true)
|
|
579
|
+
const earlyTotal = measureForCompaction(ctx, early).totalTokens
|
|
580
|
+
const earlyWindow = Math.floor(earlyTotal / 0.6)
|
|
581
|
+
early.append('request/context', { provider: 'deepseek', model: MODEL, contextWindow: earlyWindow })
|
|
582
|
+
early.append('turn/start', { turn: 3 })
|
|
583
|
+
ctx.toolResultPruner.pruneSession(early, { stage: 'pressure' })
|
|
584
|
+
|
|
585
|
+
expect(rewrites(audit.records()).some(record =>
|
|
586
|
+
record.sessionId === String(early.id) && record.component === 'history')).toBe(true)
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
it('freezes the generation-owned threshold over later global settings changes', async () => {
|
|
590
|
+
const ctx = await runtimeContext()
|
|
591
|
+
await ctx.plugin(TestSettings).await()
|
|
592
|
+
await ctx.plugin(SelectorHost).await()
|
|
593
|
+
const audit = captureAudit(ctx)
|
|
594
|
+
const namespace = settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
|
|
595
|
+
// The preset overlay captured 70% into this generation's deployment
|
|
596
|
+
// config; the user then moved the global setting to 90 before the first
|
|
597
|
+
// prune. Auto Compact and micro compact must both stay on 70%.
|
|
598
|
+
await ctx.plugin(ToolResultPruner, {
|
|
599
|
+
profile: 'balanced',
|
|
600
|
+
autoCompactThresholdPercent: 70,
|
|
601
|
+
}).await()
|
|
602
|
+
const session = Session.create(SessionId('public-generation-owned-threshold'))
|
|
603
|
+
appendToolTurn(session, 1, 'generation-owned threshold evidence', false)
|
|
604
|
+
session.append('request/context', { provider: 'deepseek', model: MODEL, contextWindow: 1_000_000 })
|
|
605
|
+
await ctx.settings.update(namespace, { profile: 'balanced', autoCompact: { thresholdPercent: 90 } })
|
|
606
|
+
ctx.toolResultPruner.pruneSession(session, { stage: 'fresh', freshTurn: 1, freshStep: 1 })
|
|
607
|
+
|
|
608
|
+
const resolved = audit.records().find(record =>
|
|
609
|
+
record.kind === 'policy-resolved' && record.sessionId === String(session.id))
|
|
610
|
+
expect(resolved).toMatchObject({
|
|
611
|
+
coordination: {
|
|
612
|
+
thresholdPercent: 70,
|
|
613
|
+
autoCompactTokens: 700_000,
|
|
614
|
+
microDeadlineTokens: 612_500,
|
|
615
|
+
},
|
|
616
|
+
})
|
|
617
|
+
const frozen = audit.records().find(record =>
|
|
618
|
+
record.kind === 'policy-frozen' && record.sessionId === String(session.id))
|
|
619
|
+
expect(frozen).toMatchObject({
|
|
620
|
+
autoCompactThresholdSource: 'generation-config',
|
|
621
|
+
settings: { autoCompact: { thresholdPercent: 70 } },
|
|
622
|
+
})
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
it('fails open to a lossless frozen policy when stored settings are malformed', async () => {
|
|
626
|
+
const ctx = await runtimeContext()
|
|
627
|
+
await ctx.plugin(TestSettings).await()
|
|
628
|
+
await ctx.plugin(SelectorHost).await()
|
|
629
|
+
const audit = captureAudit(ctx)
|
|
630
|
+
const namespace = settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
|
|
631
|
+
// A hand-edited store can surface a document the schema rejects (unknown
|
|
632
|
+
// top-level key). The runtime must not fall back to a lossy-capable
|
|
633
|
+
// profile: the session freezes effectively off and keeps every original
|
|
634
|
+
// tool result.
|
|
635
|
+
const malformed = {
|
|
636
|
+
profile: 'off',
|
|
637
|
+
custom: structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY),
|
|
638
|
+
unrelated: true,
|
|
639
|
+
}
|
|
640
|
+
const originalGet = ctx.settings.get.bind(ctx.settings)
|
|
641
|
+
vi.spyOn(ctx.settings, 'get').mockImplementation((ns: unknown) =>
|
|
642
|
+
ns === undefined || String(ns) === String(namespace) ? structuredClone(malformed) : originalGet(ns as never))
|
|
643
|
+
await ctx.plugin(ToolResultPruner, {
|
|
644
|
+
profile: 'balanced',
|
|
645
|
+
freshTriggerTokens: 10,
|
|
646
|
+
freshTargetTokens: 8,
|
|
647
|
+
}).await()
|
|
648
|
+
const session = Session.create(SessionId('public-malformed-settings-fail-open'))
|
|
649
|
+
appendToolTurn(session, 1, 'malformed settings must not compress '.repeat(300), true)
|
|
650
|
+
session.append('turn/start', { turn: 2 })
|
|
651
|
+
|
|
652
|
+
const result = ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
|
|
653
|
+
|
|
654
|
+
expect(result.pruned).toHaveLength(0)
|
|
655
|
+
expect(rewrites(audit.records())).toHaveLength(0)
|
|
656
|
+
const frozen = audit.records().find(record =>
|
|
657
|
+
record.kind === 'policy-frozen' && record.sessionId === String(session.id))
|
|
658
|
+
expect(frozen).toMatchObject({
|
|
659
|
+
settingsInvalidFallback: 'lossless-off',
|
|
660
|
+
settings: { profile: 'off' },
|
|
661
|
+
})
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
it.each([
|
|
665
|
+
['null profile', { profile: null, custom: structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY) }],
|
|
666
|
+
['null custom', { profile: 'balanced', custom: null }],
|
|
667
|
+
['both sections null', { profile: null, custom: null }],
|
|
668
|
+
['own-property undefined profile', { profile: undefined, custom: structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY) }],
|
|
669
|
+
['own-property undefined custom', { profile: 'off', custom: undefined }],
|
|
670
|
+
['profile off plus malformed autoCompact', {
|
|
671
|
+
profile: 'off',
|
|
672
|
+
custom: structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY),
|
|
673
|
+
autoCompact: { thresholdPercent: 400 },
|
|
674
|
+
}],
|
|
675
|
+
])('fails open to a lossless frozen policy when the stored document carries %s', async (label, malformed) => {
|
|
676
|
+
const ctx = await runtimeContext()
|
|
677
|
+
await ctx.plugin(TestSettings).await()
|
|
678
|
+
await ctx.plugin(SelectorHost).await()
|
|
679
|
+
const audit = captureAudit(ctx)
|
|
680
|
+
const namespace = settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
|
|
681
|
+
// Schemastery `.default(...)` would silently replace these present-but-null
|
|
682
|
+
// sections with the balanced/default-v3 policy; the runtime must reject the
|
|
683
|
+
// document and freeze the session losslessly instead.
|
|
684
|
+
const originalGet = ctx.settings.get.bind(ctx.settings)
|
|
685
|
+
vi.spyOn(ctx.settings, 'get').mockImplementation((ns: unknown) =>
|
|
686
|
+
ns === undefined || String(ns) === String(namespace) ? structuredClone(malformed) : originalGet(ns as never))
|
|
687
|
+
await ctx.plugin(ToolResultPruner, {
|
|
688
|
+
profile: 'balanced',
|
|
689
|
+
freshTriggerTokens: 10,
|
|
690
|
+
freshTargetTokens: 8,
|
|
691
|
+
}).await()
|
|
692
|
+
const session = Session.create(SessionId(`public-null-settings-${label.replace(/\s+/gu, '-')}`))
|
|
693
|
+
appendToolTurn(session, 1, 'null settings must not compress '.repeat(300), true)
|
|
694
|
+
session.append('turn/start', { turn: 2 })
|
|
695
|
+
|
|
696
|
+
const result = ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
|
|
697
|
+
|
|
698
|
+
expect(result.pruned).toHaveLength(0)
|
|
699
|
+
expect(rewrites(audit.records())).toHaveLength(0)
|
|
700
|
+
const frozen = audit.records().find(record =>
|
|
701
|
+
record.kind === 'policy-frozen' && record.sessionId === String(session.id))
|
|
702
|
+
expect(frozen).toMatchObject({
|
|
703
|
+
settingsInvalidFallback: 'lossless-off',
|
|
704
|
+
settings: { profile: 'off' },
|
|
705
|
+
})
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
it('validates exotic Host settings before any clone can erase their prototype', async () => {
|
|
709
|
+
class AutoCompactDocument {
|
|
710
|
+
thresholdPercent = 73
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const ctx = await runtimeContext()
|
|
714
|
+
await ctx.plugin(TestSettings).await()
|
|
715
|
+
await ctx.plugin(SelectorHost).await()
|
|
716
|
+
const audit = captureAudit(ctx)
|
|
717
|
+
const namespace = settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
|
|
718
|
+
const malformed = {
|
|
719
|
+
profile: 'balanced' as const,
|
|
720
|
+
custom: structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY),
|
|
721
|
+
autoCompact: new AutoCompactDocument(),
|
|
722
|
+
}
|
|
723
|
+
const originalGet = ctx.settings.get.bind(ctx.settings)
|
|
724
|
+
vi.spyOn(ctx.settings, 'get').mockImplementation((ns: unknown) =>
|
|
725
|
+
ns === undefined || String(ns) === String(namespace) ? malformed : originalGet(ns as never))
|
|
726
|
+
await ctx.plugin(ToolResultPruner, {
|
|
727
|
+
profile: 'balanced',
|
|
728
|
+
freshTriggerTokens: 10,
|
|
729
|
+
freshTargetTokens: 8,
|
|
730
|
+
}).await()
|
|
731
|
+
const session = Session.create(SessionId('public-exotic-settings-before-clone'))
|
|
732
|
+
appendToolTurn(session, 1, 'exotic settings must not compress '.repeat(300), false)
|
|
733
|
+
|
|
734
|
+
const result = ctx.toolResultPruner.pruneSession(session, {
|
|
735
|
+
stage: 'fresh',
|
|
736
|
+
freshTurn: 1,
|
|
737
|
+
freshStep: 1,
|
|
738
|
+
})
|
|
739
|
+
|
|
740
|
+
expect(result.pruned).toHaveLength(0)
|
|
741
|
+
expect(rewrites(audit.records())).toHaveLength(0)
|
|
742
|
+
expect(audit.records()).toContainEqual(expect.objectContaining({
|
|
743
|
+
kind: 'policy-frozen',
|
|
744
|
+
sessionId: String(session.id),
|
|
745
|
+
settingsInvalidFallback: 'lossless-off',
|
|
746
|
+
settings: expect.objectContaining({ profile: 'off' }),
|
|
747
|
+
}))
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
it('emits a policy audit again when the route returns to a previous value', async () => {
|
|
751
|
+
const ctx = await runtimeContext()
|
|
752
|
+
const audit = captureAudit(ctx)
|
|
753
|
+
await ctx.plugin(ToolResultPruner, { profile: 'balanced' }).await()
|
|
754
|
+
const session = Session.create(SessionId('public-policy-audit-reroute-aba'))
|
|
755
|
+
const route = (model: string) => session.append('request/header', {
|
|
756
|
+
reason: 'change',
|
|
757
|
+
header: canonicalHeader({ config: { provider: 'deepseek', model } }),
|
|
758
|
+
})
|
|
759
|
+
const prune = (turn: number) => {
|
|
760
|
+
appendToolTurn(session, turn, `route evidence ${String(turn)}`, false)
|
|
761
|
+
ctx.toolResultPruner.pruneSession(session, { stage: 'fresh', freshTurn: turn, freshStep: 1 })
|
|
762
|
+
}
|
|
763
|
+
prune(1)
|
|
764
|
+
route('deepseek-v4-pro')
|
|
765
|
+
prune(2)
|
|
766
|
+
route(MODEL)
|
|
767
|
+
prune(3)
|
|
768
|
+
|
|
769
|
+
const resolved = audit.records().filter((record): record is Extract<CompressionAuditRecord, { kind: 'policy-resolved' }> =>
|
|
770
|
+
record.kind === 'policy-resolved' && record.sessionId === String(session.id))
|
|
771
|
+
expect(resolved).toHaveLength(3)
|
|
772
|
+
expect(resolved.map(record => record.route?.model)).toEqual([MODEL, 'deepseek-v4-pro', MODEL])
|
|
773
|
+
})
|
|
774
|
+
|
|
775
|
+
it('reports no bundled tokenizer for a DeepSeek model behind another provider', async () => {
|
|
776
|
+
const ctx = await runtimeContext()
|
|
777
|
+
const audit = captureAudit(ctx)
|
|
778
|
+
await ctx.plugin(ToolResultPruner, { profile: 'balanced' }).await()
|
|
779
|
+
const session = Session.create(SessionId('public-policy-audit-foreign-provider'))
|
|
780
|
+
appendToolTurn(session, 1, 'foreign provider evidence', false, undefined, 'openai', 'deepseek-v4-pro')
|
|
781
|
+
ctx.toolResultPruner.pruneSession(session, { stage: 'fresh', freshTurn: 1, freshStep: 1 })
|
|
782
|
+
|
|
783
|
+
expect(audit.records()).toContainEqual(expect.objectContaining({
|
|
784
|
+
kind: 'policy-resolved',
|
|
785
|
+
sessionId: String(session.id),
|
|
786
|
+
route: { provider: 'openai', model: 'deepseek-v4-pro' },
|
|
787
|
+
tokenizer: { repository: 'unavailable', revision: 'unavailable' },
|
|
788
|
+
}))
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
it('emits a fresh policy audit when the durable route changes mid-session', async () => {
|
|
792
|
+
const ctx = await runtimeContext()
|
|
793
|
+
const audit = captureAudit(ctx)
|
|
794
|
+
await ctx.plugin(ToolResultPruner, { profile: 'balanced' }).await()
|
|
795
|
+
const session = Session.create(SessionId('public-policy-audit-reroute'))
|
|
796
|
+
appendToolTurn(session, 1, 'first route evidence', false)
|
|
797
|
+
ctx.toolResultPruner.pruneSession(session, { stage: 'fresh', freshTurn: 1, freshStep: 1 })
|
|
798
|
+
session.append('request/header', {
|
|
799
|
+
reason: 'change',
|
|
800
|
+
header: canonicalHeader({ config: { provider: 'deepseek', model: 'deepseek-v4-pro' } }),
|
|
801
|
+
})
|
|
802
|
+
appendToolTurn(session, 2, 'second route evidence', false, undefined, 'deepseek', 'deepseek-v4-pro')
|
|
803
|
+
ctx.toolResultPruner.pruneSession(session, { stage: 'fresh', freshTurn: 2, freshStep: 1 })
|
|
804
|
+
|
|
805
|
+
const resolved = audit.records().filter(record =>
|
|
806
|
+
record.kind === 'policy-resolved' && record.sessionId === String(session.id))
|
|
807
|
+
expect(resolved).toHaveLength(2)
|
|
808
|
+
expect(resolved[0]).toMatchObject({ route: { model: MODEL } })
|
|
809
|
+
expect(resolved[1]).toMatchObject({ route: { model: 'deepseek-v4-pro' } })
|
|
810
|
+
})
|
|
811
|
+
|
|
812
|
+
it('audits the Auto Compact coordination block on policy resolution', async () => {
|
|
813
|
+
const ctx = await runtimeContext()
|
|
814
|
+
await ctx.plugin(TestSettings).await()
|
|
815
|
+
await ctx.plugin(SelectorHost).await()
|
|
816
|
+
const audit = captureAudit(ctx)
|
|
817
|
+
const namespace = settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
|
|
818
|
+
await ctx.settings.update(namespace, { autoCompact: { thresholdPercent: 73 } })
|
|
819
|
+
await ctx.plugin(ToolResultPruner, { profile: 'balanced' }).await()
|
|
820
|
+
const session = Session.create(SessionId('public-autocompact-coordination'))
|
|
821
|
+
appendToolTurn(session, 1, 'coordination audit evidence', false)
|
|
822
|
+
session.append('request/context', { provider: 'deepseek', model: MODEL, contextWindow: 1_000_000 })
|
|
823
|
+
ctx.toolResultPruner.pruneSession(session, { stage: 'fresh', freshTurn: 1, freshStep: 1 })
|
|
824
|
+
|
|
825
|
+
const resolved = audit.records().find(record =>
|
|
826
|
+
record.kind === 'policy-resolved' && record.sessionId === String(session.id))
|
|
827
|
+
expect(resolved).toMatchObject({
|
|
828
|
+
policy: {
|
|
829
|
+
autoCompactTokens: 730_000,
|
|
830
|
+
microDeadlineTokens: 638_750,
|
|
831
|
+
historyTriggerTokens: 456_250,
|
|
832
|
+
},
|
|
833
|
+
contextWindowTokens: 1_000_000,
|
|
834
|
+
coordination: {
|
|
835
|
+
thresholdPercent: 73,
|
|
836
|
+
autoCompactTokens: 730_000,
|
|
837
|
+
microDeadlineTokens: 638_750,
|
|
838
|
+
paramSource: 'auto-compact-linked',
|
|
839
|
+
},
|
|
840
|
+
route: { provider: 'deepseek', model: MODEL },
|
|
841
|
+
tokenizer: {
|
|
842
|
+
repository: 'deepseek-ai/DeepSeek-V4-Pro',
|
|
843
|
+
revision: DEEPSEEK_V4_TOKENIZER_ARTIFACT.revision,
|
|
844
|
+
},
|
|
845
|
+
})
|
|
846
|
+
|
|
847
|
+
// Deployment config overriding every linked History watermark must audit
|
|
848
|
+
// itself as deployment-override, not as linkage-derived.
|
|
849
|
+
const overrideCtx = await runtimeContext()
|
|
850
|
+
await overrideCtx.plugin(TestSettings).await()
|
|
851
|
+
await overrideCtx.plugin(SelectorHost).await()
|
|
852
|
+
const overrideAudit = captureAudit(overrideCtx)
|
|
853
|
+
await overrideCtx.settings.update(namespace, { autoCompact: { thresholdPercent: 73 } })
|
|
854
|
+
await overrideCtx.plugin(ToolResultPruner, {
|
|
855
|
+
profile: 'balanced',
|
|
856
|
+
historyTriggerTokens: 123_456,
|
|
857
|
+
historyKeepRecentTokens: 12_345,
|
|
858
|
+
historyMinReclaimTokens: 1_234,
|
|
859
|
+
}).await()
|
|
860
|
+
const overrideSession = Session.create(SessionId('public-autocompact-deployment-override'))
|
|
861
|
+
appendToolTurn(overrideSession, 1, 'deployment override evidence', false)
|
|
862
|
+
overrideSession.append('request/context', { provider: 'deepseek', model: MODEL, contextWindow: 1_000_000 })
|
|
863
|
+
overrideCtx.toolResultPruner.pruneSession(overrideSession, { stage: 'fresh', freshTurn: 1, freshStep: 1 })
|
|
864
|
+
expect(overrideAudit.records()).toContainEqual(expect.objectContaining({
|
|
865
|
+
kind: 'policy-resolved',
|
|
866
|
+
sessionId: String(overrideSession.id),
|
|
867
|
+
coordination: expect.objectContaining({
|
|
868
|
+
microDeadlineTokens: 638_750,
|
|
869
|
+
paramSource: 'deployment-override',
|
|
870
|
+
}),
|
|
871
|
+
}))
|
|
872
|
+
|
|
873
|
+
// A partial override of the linked watermarks reads as mixed.
|
|
874
|
+
const mixedCtx = await runtimeContext()
|
|
875
|
+
await mixedCtx.plugin(TestSettings).await()
|
|
876
|
+
await mixedCtx.plugin(SelectorHost).await()
|
|
877
|
+
const mixedAudit = captureAudit(mixedCtx)
|
|
878
|
+
await mixedCtx.settings.update(namespace, { autoCompact: { thresholdPercent: 73 } })
|
|
879
|
+
await mixedCtx.plugin(ToolResultPruner, {
|
|
880
|
+
profile: 'balanced',
|
|
881
|
+
historyTriggerTokens: 123_456,
|
|
882
|
+
}).await()
|
|
883
|
+
const mixedSession = Session.create(SessionId('public-autocompact-mixed-source'))
|
|
884
|
+
appendToolTurn(mixedSession, 1, 'mixed source evidence', false)
|
|
885
|
+
mixedSession.append('request/context', { provider: 'deepseek', model: MODEL, contextWindow: 1_000_000 })
|
|
886
|
+
mixedCtx.toolResultPruner.pruneSession(mixedSession, { stage: 'fresh', freshTurn: 1, freshStep: 1 })
|
|
887
|
+
expect(mixedAudit.records()).toContainEqual(expect.objectContaining({
|
|
888
|
+
kind: 'policy-resolved',
|
|
889
|
+
sessionId: String(mixedSession.id),
|
|
890
|
+
coordination: expect.objectContaining({
|
|
891
|
+
microDeadlineTokens: 638_750,
|
|
892
|
+
paramSource: 'mixed',
|
|
893
|
+
}),
|
|
894
|
+
}))
|
|
895
|
+
|
|
896
|
+
// A route without a verified bundled tokenizer reports the unavailable
|
|
897
|
+
// identity instead of inventing one.
|
|
898
|
+
const unknown = Session.create(SessionId('public-autocompact-unknown-route'))
|
|
899
|
+
appendToolTurn(unknown, 1, 'unknown route evidence', false, undefined, 'deepseek', 'some-other-model')
|
|
900
|
+
ctx.toolResultPruner.pruneSession(unknown, { stage: 'fresh', freshTurn: 1, freshStep: 1 })
|
|
901
|
+
expect(audit.records()).toContainEqual(expect.objectContaining({
|
|
902
|
+
kind: 'policy-resolved',
|
|
903
|
+
sessionId: String(unknown.id),
|
|
904
|
+
route: { provider: 'deepseek', model: 'some-other-model' },
|
|
905
|
+
tokenizer: { repository: 'unavailable', revision: 'unavailable' },
|
|
906
|
+
}))
|
|
907
|
+
})
|
|
908
|
+
|
|
909
|
+
it('freezes the Auto Compact threshold per Session and applies new values only to new Sessions', async () => {
|
|
910
|
+
const ctx = await runtimeContext()
|
|
911
|
+
await ctx.plugin(TestSettings).await()
|
|
912
|
+
await ctx.plugin(SelectorHost).await()
|
|
913
|
+
const audit = captureAudit(ctx)
|
|
914
|
+
const namespace = settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
|
|
915
|
+
await ctx.plugin(ToolResultPruner, { profile: 'balanced' }).await()
|
|
916
|
+
|
|
917
|
+
const first = Session.create(SessionId('public-autocompact-freeze-first'))
|
|
918
|
+
appendToolTurn(first, 1, 'frozen threshold evidence', false)
|
|
919
|
+
first.append('request/context', { provider: 'deepseek', model: MODEL, contextWindow: 1_000_000 })
|
|
920
|
+
ctx.toolResultPruner.pruneSession(first, { stage: 'fresh', freshTurn: 1, freshStep: 1 })
|
|
921
|
+
|
|
922
|
+
await ctx.settings.update(namespace, { autoCompact: { thresholdPercent: 73 } })
|
|
923
|
+
|
|
924
|
+
const second = Session.create(SessionId('public-autocompact-freeze-second'))
|
|
925
|
+
appendToolTurn(second, 1, 'new threshold evidence', false)
|
|
926
|
+
second.append('request/context', { provider: 'deepseek', model: MODEL, contextWindow: 1_000_000 })
|
|
927
|
+
ctx.toolResultPruner.pruneSession(second, { stage: 'fresh', freshTurn: 1, freshStep: 1 })
|
|
928
|
+
|
|
929
|
+
// The already-observed session keeps its frozen 80% policy even after
|
|
930
|
+
// another prune; only the new session adopts 73%.
|
|
931
|
+
first.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
|
932
|
+
appendToolTurn(first, 2, 'post-change frozen evidence', false)
|
|
933
|
+
first.append('request/context', { provider: 'deepseek', model: MODEL, contextWindow: 1_000_000 })
|
|
934
|
+
ctx.toolResultPruner.pruneSession(first, { stage: 'fresh', freshTurn: 2, freshStep: 1 })
|
|
935
|
+
|
|
936
|
+
const resolvedFor = (sessionId: string) => audit.records().find(record =>
|
|
937
|
+
record.kind === 'policy-resolved' && record.sessionId === sessionId)
|
|
938
|
+
expect(resolvedFor(String(first.id))).toMatchObject({
|
|
939
|
+
coordination: { thresholdPercent: 80, autoCompactTokens: 800_000, microDeadlineTokens: 700_000 },
|
|
940
|
+
})
|
|
941
|
+
expect(resolvedFor(String(second.id))).toMatchObject({
|
|
942
|
+
coordination: { thresholdPercent: 73, autoCompactTokens: 730_000, microDeadlineTokens: 638_750 },
|
|
943
|
+
})
|
|
944
|
+
expect(audit.records().filter(record =>
|
|
945
|
+
record.kind === 'policy-resolved'
|
|
946
|
+
&& record.sessionId === String(first.id)
|
|
947
|
+
&& record.coordination?.thresholdPercent === 73)).toHaveLength(0)
|
|
948
|
+
})
|
|
949
|
+
|
|
950
|
+
it('ages History through the full-request last-chance gate even below the profile trigger', async () => {
|
|
951
|
+
const ctx = await runtimeContext()
|
|
952
|
+
const audit = captureAudit(ctx)
|
|
953
|
+
await ctx.plugin(ToolResultPruner, {
|
|
954
|
+
profile: 'balanced',
|
|
955
|
+
freshTriggerTokens: 100_000,
|
|
956
|
+
freshTargetTokens: 90_000,
|
|
957
|
+
aggregateTriggerTokens: 100_000,
|
|
958
|
+
aggregateTargetTokens: 90_000,
|
|
959
|
+
historyTriggerTokens: 100_000,
|
|
960
|
+
historyKeepRecentToolCalls: 0,
|
|
961
|
+
historyKeepRecentTokens: 1,
|
|
962
|
+
historyMinReclaimTokens: 1,
|
|
963
|
+
}).await()
|
|
964
|
+
const session = Session.create(SessionId('public-history-last-chance'))
|
|
965
|
+
session.append('turn/start', { turn: 1 })
|
|
966
|
+
session.append('request/header', {
|
|
967
|
+
reason: 'initial',
|
|
968
|
+
header: canonicalHeader({ config: { provider: 'deepseek', model: MODEL } }),
|
|
969
|
+
})
|
|
970
|
+
// Non-tool prose carries the full request past the deadline while the
|
|
971
|
+
// tool results alone stay far below the linked profile trigger.
|
|
972
|
+
session.append('user/message', createUserMessage({
|
|
973
|
+
content: [{ type: 'text', text: 'pressure prose '.repeat(1_000) }],
|
|
974
|
+
source: { kind: 'user' },
|
|
975
|
+
}), { surfaceOp: 'append' })
|
|
976
|
+
session.append('step/start', { turn: 1, step: 1 })
|
|
977
|
+
session.append('assistant/message', {
|
|
978
|
+
turn: 1,
|
|
979
|
+
step: 1,
|
|
980
|
+
message: createMessage({
|
|
981
|
+
role: 'assistant',
|
|
982
|
+
content: [{ type: 'tool-call', id: CallId('last-chance-old'), name: 'bash', arguments: '{}' }],
|
|
983
|
+
source: { kind: 'model', provider: 'deepseek', model: MODEL },
|
|
984
|
+
}),
|
|
985
|
+
}, { surfaceOp: 'append' })
|
|
986
|
+
session.append('tool/call', { turn: 1, step: 1, callId: CallId('last-chance-old'), name: 'bash', arguments: '{}' })
|
|
987
|
+
session.append('tool/result', {
|
|
988
|
+
turn: 1,
|
|
989
|
+
step: 1,
|
|
990
|
+
message: createToolResultMessage({
|
|
991
|
+
callId: CallId('last-chance-old'),
|
|
992
|
+
content: [{ type: 'text', text: 'last-chance aging evidence '.repeat(700) }],
|
|
993
|
+
isError: false,
|
|
994
|
+
}),
|
|
995
|
+
}, { surfaceOp: 'append' })
|
|
996
|
+
session.append('step/end', { turn: 1, step: 1 })
|
|
997
|
+
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
|
998
|
+
appendToolTurn(session, 2, 'newest protected result', true)
|
|
999
|
+
// 80% default on a 10,000-token window: A = 8000, linked H = 5000, D = 7000.
|
|
1000
|
+
session.append('request/context', { provider: 'deepseek', model: MODEL, contextWindow: 10_000 })
|
|
1001
|
+
session.append('turn/start', { turn: 3 })
|
|
1002
|
+
|
|
1003
|
+
const before = measureForCompaction(ctx, session)
|
|
1004
|
+
const toolResultTokens = before.measuredNodes
|
|
1005
|
+
.filter(node => session.events[node.seq]?.type === 'tool/result')
|
|
1006
|
+
.reduce((sum, node) => sum + (node.count.kind === 'exact-tokenizer' ? node.count.tokens : 0), 0)
|
|
1007
|
+
// Deadline D = 7000 is reached by the complete request, not by the tool
|
|
1008
|
+
// results, which stay far below every trigger in force.
|
|
1009
|
+
expect(before.totalTokens).toBeGreaterThanOrEqual(7_000)
|
|
1010
|
+
expect(toolResultTokens).toBeLessThan(7_000)
|
|
1011
|
+
|
|
1012
|
+
const result = ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
|
|
1013
|
+
|
|
1014
|
+
const aged = rewrites(audit.records()).filter(record =>
|
|
1015
|
+
record.sessionId === String(session.id) && record.component === 'history')
|
|
1016
|
+
expect(aged.length).toBeGreaterThanOrEqual(1)
|
|
1017
|
+
expect(result.pruned.length).toBeGreaterThanOrEqual(1)
|
|
1018
|
+
})
|
|
1019
|
+
|
|
1020
|
+
it.each(['balanced', 'adaptive'] as const)(
|
|
1021
|
+
'skips a linked History batch that cannot reach its reclaim target in %s mode',
|
|
1022
|
+
async (profile) => {
|
|
1023
|
+
const ctx = await runtimeContext()
|
|
1024
|
+
const audit = captureAudit(ctx)
|
|
1025
|
+
await ctx.plugin(ToolResultPruner, {
|
|
1026
|
+
profile,
|
|
1027
|
+
freshTriggerTokens: 100_000,
|
|
1028
|
+
freshTargetTokens: 90_000,
|
|
1029
|
+
aggregateTriggerTokens: 100_000,
|
|
1030
|
+
aggregateTargetTokens: 90_000,
|
|
1031
|
+
historyTriggerTokens: 100,
|
|
1032
|
+
historyKeepRecentToolCalls: 0,
|
|
1033
|
+
historyKeepRecentTokens: 1,
|
|
1034
|
+
historyMinReclaimTokens: 1,
|
|
1035
|
+
}).await()
|
|
1036
|
+
|
|
1037
|
+
// Below the deadline: the excess above the trigger sits mostly in the
|
|
1038
|
+
// protected newest result, so the only reclaimable batch is far too small.
|
|
1039
|
+
const below = Session.create(SessionId(`public-history-insufficient-reclaim-${profile}`))
|
|
1040
|
+
appendToolTurn(below, 1, 'unreachable reclaim evidence '.repeat(400), true)
|
|
1041
|
+
appendToolTurn(below, 2, 'newest protected bulk '.repeat(600), true)
|
|
1042
|
+
below.append('request/context', { provider: 'deepseek', model: MODEL, contextWindow: 10_000 })
|
|
1043
|
+
below.append('turn/start', { turn: 3 })
|
|
1044
|
+
const belowResult = ctx.toolResultPruner.pruneSession(below, { stage: 'pressure' })
|
|
1045
|
+
expect(belowResult.pruned).toHaveLength(0)
|
|
1046
|
+
expect(rewrites(audit.records()).some(record =>
|
|
1047
|
+
record.sessionId === String(below.id) && record.component === 'history')).toBe(false)
|
|
1048
|
+
expect(audit.records()).toContainEqual(expect.objectContaining({
|
|
1049
|
+
kind: 'component-evaluation',
|
|
1050
|
+
sessionId: String(below.id),
|
|
1051
|
+
component: 'history',
|
|
1052
|
+
status: 'skipped',
|
|
1053
|
+
reason: 'insufficient-reclaim',
|
|
1054
|
+
reclaimTokens: expect.any(Number),
|
|
1055
|
+
requiredTokens: expect.any(Number),
|
|
1056
|
+
}))
|
|
1057
|
+
|
|
1058
|
+
// Past the deadline with tool tokens below the trigger: the last-chance
|
|
1059
|
+
// gate engages planning, and the skip names the deadline target.
|
|
1060
|
+
const past = Session.create(SessionId(`public-history-deadline-unreachable-${profile}`))
|
|
1061
|
+
past.append('turn/start', { turn: 1 })
|
|
1062
|
+
past.append('request/header', {
|
|
1063
|
+
reason: 'initial',
|
|
1064
|
+
header: canonicalHeader({ config: { provider: 'deepseek', model: MODEL } }),
|
|
1065
|
+
})
|
|
1066
|
+
past.append('user/message', createUserMessage({
|
|
1067
|
+
content: [{ type: 'text', text: 'deadline pressure prose '.repeat(2_000) }],
|
|
1068
|
+
source: { kind: 'user' },
|
|
1069
|
+
}), { surfaceOp: 'append' })
|
|
1070
|
+
appendToolTurn(past, 1, 'deadline unreachable evidence '.repeat(400), true)
|
|
1071
|
+
appendToolTurn(past, 2, 'newest protected bulk '.repeat(600), true)
|
|
1072
|
+
past.append('request/context', { provider: 'deepseek', model: MODEL, contextWindow: 10_000 })
|
|
1073
|
+
past.append('turn/start', { turn: 2 })
|
|
1074
|
+
const pastResult = ctx.toolResultPruner.pruneSession(past, { stage: 'pressure' })
|
|
1075
|
+
expect(pastResult.pruned).toHaveLength(0)
|
|
1076
|
+
expect(rewrites(audit.records()).some(record =>
|
|
1077
|
+
record.sessionId === String(past.id) && record.component === 'history')).toBe(false)
|
|
1078
|
+
expect(audit.records()).toContainEqual(expect.objectContaining({
|
|
1079
|
+
kind: 'component-evaluation',
|
|
1080
|
+
sessionId: String(past.id),
|
|
1081
|
+
component: 'history',
|
|
1082
|
+
status: 'skipped',
|
|
1083
|
+
reason: 'cannot-reach-deadline-target',
|
|
1084
|
+
triggerTokens: 7_000,
|
|
1085
|
+
}))
|
|
1086
|
+
},
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1089
|
+
it.each(['balanced', 'adaptive'] as const)(
|
|
1090
|
+
'audits below-profile-trigger in %s mode when the tool-result total sits under the History trigger',
|
|
1091
|
+
async (profile) => {
|
|
1092
|
+
const ctx = await runtimeContext()
|
|
1093
|
+
const audit = captureAudit(ctx)
|
|
1094
|
+
await ctx.plugin(ToolResultPruner, {
|
|
1095
|
+
profile,
|
|
1096
|
+
freshTriggerTokens: 100_000,
|
|
1097
|
+
freshTargetTokens: 90_000,
|
|
1098
|
+
aggregateTriggerTokens: 100_000,
|
|
1099
|
+
aggregateTargetTokens: 90_000,
|
|
1100
|
+
historyTriggerTokens: 100_000,
|
|
1101
|
+
historyKeepRecentToolCalls: 0,
|
|
1102
|
+
historyKeepRecentTokens: 1,
|
|
1103
|
+
historyMinReclaimTokens: 1,
|
|
1104
|
+
}).await()
|
|
1105
|
+
const session = Session.create(SessionId(`public-history-below-profile-trigger-${profile}`))
|
|
1106
|
+
appendToolTurn(session, 1, 'small routine evidence', true)
|
|
1107
|
+
session.append('turn/start', { turn: 2 })
|
|
1108
|
+
ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
|
|
1109
|
+
expect(audit.records()).toContainEqual(expect.objectContaining({
|
|
1110
|
+
kind: 'component-evaluation',
|
|
1111
|
+
sessionId: String(session.id),
|
|
1112
|
+
component: 'history',
|
|
1113
|
+
status: 'skipped',
|
|
1114
|
+
reason: 'below-profile-trigger',
|
|
1115
|
+
triggerTokens: 100_000,
|
|
1116
|
+
}))
|
|
1117
|
+
},
|
|
1118
|
+
)
|
|
1119
|
+
|
|
1120
|
+
it.each(['balanced', 'adaptive'] as const)(
|
|
1121
|
+
'audits protected-working-set in %s mode when every safe candidate is inside the protected tail',
|
|
1122
|
+
async (profile) => {
|
|
1123
|
+
const ctx = await runtimeContext()
|
|
1124
|
+
const audit = captureAudit(ctx)
|
|
1125
|
+
await ctx.plugin(ToolResultPruner, {
|
|
1126
|
+
profile,
|
|
1127
|
+
freshTriggerTokens: 100_000,
|
|
1128
|
+
freshTargetTokens: 90_000,
|
|
1129
|
+
aggregateTriggerTokens: 100_000,
|
|
1130
|
+
aggregateTargetTokens: 90_000,
|
|
1131
|
+
historyTriggerTokens: 100,
|
|
1132
|
+
historyKeepRecentToolCalls: 10,
|
|
1133
|
+
historyKeepRecentTokens: 1_000_000,
|
|
1134
|
+
historyMinReclaimTokens: 1,
|
|
1135
|
+
}).await()
|
|
1136
|
+
const session = Session.create(SessionId(`public-history-protected-working-set-${profile}`))
|
|
1137
|
+
appendToolTurn(session, 1, 'protected working set evidence '.repeat(300), true)
|
|
1138
|
+
appendToolTurn(session, 2, 'newest protected result '.repeat(300), true)
|
|
1139
|
+
session.append('turn/start', { turn: 3 })
|
|
1140
|
+
const result = ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
|
|
1141
|
+
expect(result.pruned).toHaveLength(0)
|
|
1142
|
+
expect(audit.records()).toContainEqual(expect.objectContaining({
|
|
1143
|
+
kind: 'component-evaluation',
|
|
1144
|
+
sessionId: String(session.id),
|
|
1145
|
+
component: 'history',
|
|
1146
|
+
status: 'skipped',
|
|
1147
|
+
reason: 'protected-working-set',
|
|
1148
|
+
}))
|
|
1149
|
+
},
|
|
1150
|
+
)
|
|
1151
|
+
|
|
1152
|
+
it.each(['balanced', 'adaptive'] as const)(
|
|
1153
|
+
'audits no-safe-candidates in %s mode when the only tool results are recovery-tool output',
|
|
1154
|
+
async (profile) => {
|
|
1155
|
+
const ctx = await runtimeContext()
|
|
1156
|
+
const audit = captureAudit(ctx)
|
|
1157
|
+
await ctx.plugin(ToolResultPruner, {
|
|
1158
|
+
profile,
|
|
1159
|
+
freshTriggerTokens: 100_000,
|
|
1160
|
+
freshTargetTokens: 90_000,
|
|
1161
|
+
aggregateTriggerTokens: 100_000,
|
|
1162
|
+
aggregateTargetTokens: 90_000,
|
|
1163
|
+
historyTriggerTokens: 100,
|
|
1164
|
+
historyKeepRecentToolCalls: 0,
|
|
1165
|
+
historyKeepRecentTokens: 1,
|
|
1166
|
+
historyMinReclaimTokens: 1,
|
|
1167
|
+
}).await()
|
|
1168
|
+
const session = Session.create(SessionId(`public-history-no-safe-candidates-${profile}`))
|
|
1169
|
+
// One ordinary turn (protected by nothing) plus one recovery-tool output:
|
|
1170
|
+
// filtering only unsafe candidates leaves the ordinary result, so this
|
|
1171
|
+
// first pass must NOT read as no-safe-candidates.
|
|
1172
|
+
appendToolTurn(session, 1, 'ordinary reclaimable evidence '.repeat(300), true)
|
|
1173
|
+
const turn = 2
|
|
1174
|
+
const callId = CallId('recovery-call')
|
|
1175
|
+
session.append('turn/start', { turn })
|
|
1176
|
+
session.append('step/start', { turn, step: 1 })
|
|
1177
|
+
session.append('assistant/message', {
|
|
1178
|
+
turn,
|
|
1179
|
+
step: 1,
|
|
1180
|
+
message: createMessage({
|
|
1181
|
+
role: 'assistant',
|
|
1182
|
+
content: [{ type: 'tool-call', id: callId, name: 'context_compression_retrieve', arguments: '{}' }],
|
|
1183
|
+
source: { kind: 'model', provider: 'deepseek', model: MODEL },
|
|
1184
|
+
}),
|
|
1185
|
+
}, { surfaceOp: 'append' })
|
|
1186
|
+
session.append('tool/call', { turn, step: 1, callId, name: 'context_compression_retrieve', arguments: '{}' })
|
|
1187
|
+
session.append('tool/result', {
|
|
1188
|
+
turn,
|
|
1189
|
+
step: 1,
|
|
1190
|
+
message: createToolResultMessage({
|
|
1191
|
+
callId,
|
|
1192
|
+
content: [{ type: 'text', text: 'recovered original group '.repeat(300) }],
|
|
1193
|
+
isError: false,
|
|
1194
|
+
}),
|
|
1195
|
+
}, { surfaceOp: 'append' })
|
|
1196
|
+
session.append('step/end', { turn, step: 1 })
|
|
1197
|
+
session.append('turn/end', { turn, reason: { kind: 'completed' } })
|
|
1198
|
+
session.append('turn/start', { turn: 3 })
|
|
1199
|
+
ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
|
|
1200
|
+
// The ordinary result remains a safe candidate. Balanced commits it;
|
|
1201
|
+
// Adaptive may reject its real plan on cost, but neither profile may
|
|
1202
|
+
// misreport this mixed set as no-safe-candidates.
|
|
1203
|
+
expect(audit.records().some(record => record.kind === 'component-evaluation'
|
|
1204
|
+
&& record.sessionId === String(session.id)
|
|
1205
|
+
&& record.component === 'history'
|
|
1206
|
+
&& record.reason === 'no-safe-candidates')).toBe(false)
|
|
1207
|
+
if (profile === 'balanced') {
|
|
1208
|
+
expect(rewrites(audit.records()).some(record =>
|
|
1209
|
+
record.sessionId === String(session.id) && record.component === 'history')).toBe(true)
|
|
1210
|
+
} else {
|
|
1211
|
+
expect(audit.records()).toContainEqual(expect.objectContaining({
|
|
1212
|
+
kind: 'component-evaluation',
|
|
1213
|
+
sessionId: String(session.id),
|
|
1214
|
+
component: 'history',
|
|
1215
|
+
reason: 'adaptive-cost-rejected',
|
|
1216
|
+
}))
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
const onlyUnsafe = Session.create(SessionId(`public-history-no-safe-candidates-only-${profile}`))
|
|
1220
|
+
onlyUnsafe.append('turn/start', { turn: 1 })
|
|
1221
|
+
onlyUnsafe.append('request/header', {
|
|
1222
|
+
reason: 'initial',
|
|
1223
|
+
header: canonicalHeader({ config: { provider: 'deepseek', model: MODEL } }),
|
|
1224
|
+
})
|
|
1225
|
+
onlyUnsafe.append('step/start', { turn: 1, step: 1 })
|
|
1226
|
+
onlyUnsafe.append('assistant/message', {
|
|
1227
|
+
turn: 1,
|
|
1228
|
+
step: 1,
|
|
1229
|
+
message: createMessage({
|
|
1230
|
+
role: 'assistant',
|
|
1231
|
+
content: [{ type: 'tool-call', id: callId, name: 'context_compression_retrieve', arguments: '{}' }],
|
|
1232
|
+
source: { kind: 'model', provider: 'deepseek', model: MODEL },
|
|
1233
|
+
}),
|
|
1234
|
+
}, { surfaceOp: 'append' })
|
|
1235
|
+
onlyUnsafe.append('tool/call', { turn: 1, step: 1, callId, name: 'context_compression_retrieve', arguments: '{}' })
|
|
1236
|
+
onlyUnsafe.append('tool/result', {
|
|
1237
|
+
turn: 1,
|
|
1238
|
+
step: 1,
|
|
1239
|
+
message: createToolResultMessage({
|
|
1240
|
+
callId,
|
|
1241
|
+
content: [{ type: 'text', text: 'only recovery output lives here '.repeat(300) }],
|
|
1242
|
+
isError: false,
|
|
1243
|
+
}),
|
|
1244
|
+
}, { surfaceOp: 'append' })
|
|
1245
|
+
onlyUnsafe.append('step/end', { turn: 1, step: 1 })
|
|
1246
|
+
onlyUnsafe.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
|
1247
|
+
onlyUnsafe.append('turn/start', { turn: 2 })
|
|
1248
|
+
ctx.toolResultPruner.pruneSession(onlyUnsafe, { stage: 'pressure' })
|
|
1249
|
+
expect(audit.records()).toContainEqual(expect.objectContaining({
|
|
1250
|
+
kind: 'component-evaluation',
|
|
1251
|
+
sessionId: String(onlyUnsafe.id),
|
|
1252
|
+
component: 'history',
|
|
1253
|
+
status: 'skipped',
|
|
1254
|
+
reason: 'no-safe-candidates',
|
|
1255
|
+
}))
|
|
1256
|
+
},
|
|
1257
|
+
)
|
|
1258
|
+
|
|
1259
|
+
it('audits adaptive-cost-rejected when the adaptive estimate refuses the batch', async () => {
|
|
1260
|
+
const ctx = await runtimeContext()
|
|
1261
|
+
const audit = captureAudit(ctx)
|
|
1262
|
+
await ctx.plugin(ToolResultPruner, {
|
|
1263
|
+
profile: 'adaptive',
|
|
1264
|
+
freshTriggerTokens: 100_000,
|
|
1265
|
+
freshTargetTokens: 90_000,
|
|
1266
|
+
aggregateTriggerTokens: 100_000,
|
|
1267
|
+
aggregateTargetTokens: 90_000,
|
|
1268
|
+
historyTriggerTokens: 100,
|
|
1269
|
+
historyKeepRecentToolCalls: 0,
|
|
1270
|
+
historyKeepRecentTokens: 1,
|
|
1271
|
+
historyMinReclaimTokens: 1,
|
|
1272
|
+
}).await()
|
|
1273
|
+
const session = Session.create(SessionId('public-history-adaptive-cost-rejected'))
|
|
1274
|
+
appendToolTurn(session, 1, 'adaptive rejected evidence '.repeat(300), true)
|
|
1275
|
+
appendToolTurn(session, 2, 'newest adaptive working-set evidence', true)
|
|
1276
|
+
session.append('turn/start', { turn: 3 })
|
|
1277
|
+
ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
|
|
1278
|
+
expect(audit.records()).toContainEqual(expect.objectContaining({
|
|
1279
|
+
kind: 'component-evaluation',
|
|
1280
|
+
sessionId: String(session.id),
|
|
1281
|
+
component: 'history',
|
|
1282
|
+
status: 'skipped',
|
|
1283
|
+
reason: 'adaptive-cost-rejected',
|
|
1284
|
+
historyMode: 'adaptive',
|
|
1285
|
+
}))
|
|
1286
|
+
})
|
|
1287
|
+
|
|
1288
|
+
it('preserves below-profile-trigger when Adaptive planning never forms a batch', async () => {
|
|
1289
|
+
const ctx = await runtimeContext()
|
|
1290
|
+
const audit = captureAudit(ctx)
|
|
1291
|
+
await ctx.plugin(ToolResultPruner, {
|
|
1292
|
+
profile: 'adaptive',
|
|
1293
|
+
freshTriggerTokens: 100_000,
|
|
1294
|
+
freshTargetTokens: 90_000,
|
|
1295
|
+
aggregateTriggerTokens: 100_000,
|
|
1296
|
+
aggregateTargetTokens: 90_000,
|
|
1297
|
+
historyTriggerTokens: 100_000,
|
|
1298
|
+
historyKeepRecentToolCalls: 0,
|
|
1299
|
+
historyKeepRecentTokens: 1,
|
|
1300
|
+
historyMinReclaimTokens: 1,
|
|
1301
|
+
}).await()
|
|
1302
|
+
const session = Session.create(SessionId('public-history-adaptive-below-trigger'))
|
|
1303
|
+
appendToolTurn(session, 1, 'small adaptive evidence', true)
|
|
1304
|
+
session.append('turn/start', { turn: 2 })
|
|
1305
|
+
ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
|
|
1306
|
+
expect(audit.records()).toContainEqual(expect.objectContaining({
|
|
1307
|
+
kind: 'component-evaluation',
|
|
1308
|
+
sessionId: String(session.id),
|
|
1309
|
+
component: 'history',
|
|
1310
|
+
status: 'skipped',
|
|
1311
|
+
reason: 'below-profile-trigger',
|
|
1312
|
+
historyMode: 'adaptive',
|
|
1313
|
+
triggerTokens: 100_000,
|
|
1314
|
+
}))
|
|
1315
|
+
})
|
|
1316
|
+
|
|
1317
|
+
it('preserves exact-tokenizer-unavailable before Adaptive cost authority', async () => {
|
|
1318
|
+
const ctx = await runtimeContext()
|
|
1319
|
+
const audit = captureAudit(ctx)
|
|
1320
|
+
await ctx.plugin(ToolResultPruner, {
|
|
1321
|
+
profile: 'adaptive',
|
|
1322
|
+
freshTriggerTokens: 100_000,
|
|
1323
|
+
freshTargetTokens: 90_000,
|
|
1324
|
+
aggregateTriggerTokens: 100_000,
|
|
1325
|
+
aggregateTargetTokens: 90_000,
|
|
1326
|
+
historyTriggerTokens: 100,
|
|
1327
|
+
historyKeepRecentToolCalls: 0,
|
|
1328
|
+
historyKeepRecentTokens: 1,
|
|
1329
|
+
historyMinReclaimTokens: 1,
|
|
1330
|
+
}).await()
|
|
1331
|
+
const session = Session.create(SessionId('public-history-adaptive-exact-unavailable'))
|
|
1332
|
+
appendToolTurn(
|
|
1333
|
+
session,
|
|
1334
|
+
1,
|
|
1335
|
+
'unknown adaptive model evidence '.repeat(300),
|
|
1336
|
+
true,
|
|
1337
|
+
undefined,
|
|
1338
|
+
'deepseek',
|
|
1339
|
+
'unsupported-public-model',
|
|
1340
|
+
)
|
|
1341
|
+
session.append('turn/start', { turn: 2 })
|
|
1342
|
+
ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
|
|
1343
|
+
expect(audit.records()).toContainEqual(expect.objectContaining({
|
|
1344
|
+
kind: 'component-evaluation',
|
|
1345
|
+
sessionId: String(session.id),
|
|
1346
|
+
component: 'history',
|
|
1347
|
+
status: 'skipped',
|
|
1348
|
+
reason: 'exact-tokenizer-unavailable',
|
|
1349
|
+
historyMode: 'adaptive',
|
|
1350
|
+
measurementKind: 'unavailable',
|
|
1351
|
+
}))
|
|
1352
|
+
})
|
|
1353
|
+
|
|
1354
|
+
it('audits exact-tokenizer-unavailable for History when counts are not exact', async () => {
|
|
1355
|
+
const ctx = await runtimeContext()
|
|
1356
|
+
const audit = captureAudit(ctx)
|
|
1357
|
+
await ctx.plugin(ToolResultPruner, {
|
|
1358
|
+
profile: 'balanced',
|
|
1359
|
+
freshTriggerTokens: 100_000,
|
|
1360
|
+
freshTargetTokens: 90_000,
|
|
1361
|
+
aggregateTriggerTokens: 100_000,
|
|
1362
|
+
aggregateTargetTokens: 90_000,
|
|
1363
|
+
historyTriggerTokens: 100,
|
|
1364
|
+
historyKeepRecentToolCalls: 0,
|
|
1365
|
+
historyKeepRecentTokens: 1,
|
|
1366
|
+
historyMinReclaimTokens: 1,
|
|
1367
|
+
}).await()
|
|
1368
|
+
const session = Session.create(SessionId('public-history-exact-unavailable'))
|
|
1369
|
+
appendToolTurn(session, 1, 'unknown model history evidence '.repeat(300), true, undefined, 'deepseek', 'unsupported-public-model')
|
|
1370
|
+
session.append('turn/start', { turn: 2 })
|
|
1371
|
+
ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
|
|
1372
|
+
expect(audit.records()).toContainEqual(expect.objectContaining({
|
|
1373
|
+
kind: 'component-evaluation',
|
|
1374
|
+
sessionId: String(session.id),
|
|
1375
|
+
component: 'history',
|
|
1376
|
+
status: 'skipped',
|
|
1377
|
+
reason: 'exact-tokenizer-unavailable',
|
|
1378
|
+
measurementKind: 'unavailable',
|
|
1379
|
+
}))
|
|
1380
|
+
})
|
|
1381
|
+
|
|
1382
|
+
it('audits recovery-tool-unavailable when a committed batch cannot land without the recovery tool', async () => {
|
|
1383
|
+
const ctx = new Context()
|
|
1384
|
+
activeContexts.push(ctx)
|
|
1385
|
+
await ctx.plugin(SessionStore)
|
|
1386
|
+
await ctx.plugin(SystemPrompt)
|
|
1387
|
+
await ctx.plugin(TokenMeter)
|
|
1388
|
+
const audit = captureAudit(ctx)
|
|
1389
|
+
await ctx.plugin(ToolResultPruner, {
|
|
1390
|
+
profile: 'balanced',
|
|
1391
|
+
freshTriggerTokens: 100_000,
|
|
1392
|
+
freshTargetTokens: 90_000,
|
|
1393
|
+
aggregateTriggerTokens: 100_000,
|
|
1394
|
+
aggregateTargetTokens: 90_000,
|
|
1395
|
+
historyTriggerTokens: 100,
|
|
1396
|
+
historyKeepRecentToolCalls: 0,
|
|
1397
|
+
historyKeepRecentTokens: 1,
|
|
1398
|
+
historyMinReclaimTokens: 1,
|
|
1399
|
+
}).await()
|
|
1400
|
+
const session = Session.create(SessionId('public-history-recovery-tool-unavailable'))
|
|
1401
|
+
// The newest result stays inside the protected tail; the older one plans a
|
|
1402
|
+
// committed batch that cannot land without the recovery tool.
|
|
1403
|
+
appendToolTurn(session, 1, 'committed batch without recovery tool '.repeat(300), true)
|
|
1404
|
+
appendToolTurn(session, 2, 'newest protected result', true)
|
|
1405
|
+
session.append('turn/start', { turn: 3 })
|
|
1406
|
+
const result = ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
|
|
1407
|
+
expect(result.pruned).toHaveLength(0)
|
|
1408
|
+
expect(audit.records()).toContainEqual(expect.objectContaining({
|
|
1409
|
+
kind: 'component-evaluation',
|
|
1410
|
+
sessionId: String(session.id),
|
|
1411
|
+
component: 'history',
|
|
1412
|
+
status: 'skipped',
|
|
1413
|
+
reason: 'recovery-tool-unavailable',
|
|
1414
|
+
}))
|
|
1415
|
+
})
|
|
1416
|
+
|
|
1417
|
+
it('still commits unlinked Custom History batches at the minimum reclaim', async () => {
|
|
1418
|
+
const ctx = await runtimeContext()
|
|
1419
|
+
await ctx.plugin(TestSettings).await()
|
|
1420
|
+
await ctx.plugin(SelectorHost).await()
|
|
1421
|
+
const audit = captureAudit(ctx)
|
|
1422
|
+
// A protected token tail larger than the trigger leaves the linked-style
|
|
1423
|
+
// whole-excess demand unreachable; Custom stays manual and must still
|
|
1424
|
+
// commit once a batch passes its explicit minimum reclaim.
|
|
1425
|
+
const custom = structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY) as CustomCompressionPolicy
|
|
1426
|
+
if (custom.version !== 3) throw new Error('unlinked Custom fixture requires policy v3')
|
|
1427
|
+
custom.fresh = { enabled: true, trigger: 1_000_000, target: 900_000 }
|
|
1428
|
+
custom.aggregate = { enabled: true, trigger: 1_000_000, target: 900_000 }
|
|
1429
|
+
custom.history = {
|
|
1430
|
+
enabled: true,
|
|
1431
|
+
trigger: 20_000,
|
|
1432
|
+
keepRecentToolCalls: 2,
|
|
1433
|
+
keepRecentTokens: 30_000,
|
|
1434
|
+
minReclaim: 1_000,
|
|
1435
|
+
}
|
|
1436
|
+
custom.prefixPolicy = 'pressure-break'
|
|
1437
|
+
custom.tailTrim = { enabled: false, trigger: 700_000 }
|
|
1438
|
+
await ctx.settings.update(settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
|
|
1439
|
+
profile: 'custom',
|
|
1440
|
+
custom,
|
|
1441
|
+
})
|
|
1442
|
+
await ctx.plugin(ToolResultPruner, { profile: 'custom' }).await()
|
|
1443
|
+
|
|
1444
|
+
const session = Session.create(SessionId('public-custom-min-reclaim-commit'))
|
|
1445
|
+
// Five results of ~14k tokens: the 30k token tail plus 2 newest calls
|
|
1446
|
+
// protect the newest three, leaving two old results reclaimable.
|
|
1447
|
+
appendToolTurn(session, 1, 'custom old reclaimable evidence '.repeat(3_000), true)
|
|
1448
|
+
appendToolTurn(session, 2, 'custom old reclaimable evidence '.repeat(3_000), true)
|
|
1449
|
+
appendToolTurn(session, 3, 'custom newer reclaimable evidence '.repeat(3_000), true)
|
|
1450
|
+
appendToolTurn(session, 4, 'custom newer reclaimable evidence '.repeat(3_000), true)
|
|
1451
|
+
appendToolTurn(session, 5, 'custom newest protected evidence '.repeat(3_000), true)
|
|
1452
|
+
session.append('turn/start', { turn: 6 })
|
|
1453
|
+
|
|
1454
|
+
const result = ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
|
|
1455
|
+
|
|
1456
|
+
expect(result.pruned.length).toBeGreaterThanOrEqual(1)
|
|
1457
|
+
expect(rewrites(audit.records()).some(record =>
|
|
1458
|
+
record.sessionId === String(session.id) && record.component === 'history')).toBe(true)
|
|
1459
|
+
})
|
|
1460
|
+
|
|
1461
|
+
it('distinguishes inactive and active History capacity-pressure from durable route capacity', async () => {
|
|
1462
|
+
const ctx = await runtimeContext()
|
|
1463
|
+
const audit = captureAudit(ctx)
|
|
1464
|
+
await ctx.plugin(ToolResultPruner, {
|
|
1465
|
+
profile: 'cache-strict',
|
|
1466
|
+
freshTriggerTokens: 100_000,
|
|
1467
|
+
freshTargetTokens: 90_000,
|
|
1468
|
+
aggregateTriggerTokens: 100_000,
|
|
1469
|
+
aggregateTargetTokens: 90_000,
|
|
1470
|
+
historyTriggerTokens: 400,
|
|
1471
|
+
historyKeepRecentToolCalls: 0,
|
|
1472
|
+
historyKeepRecentTokens: 1,
|
|
1473
|
+
historyMinReclaimTokens: 1,
|
|
1474
|
+
}).await()
|
|
1475
|
+
|
|
1476
|
+
const belowCapacityGate = Session.create(SessionId('public-history-capacity-inactive'))
|
|
1477
|
+
appendToolTurn(belowCapacityGate, 1, 'capacity inactive evidence '.repeat(600), true)
|
|
1478
|
+
appendToolTurn(belowCapacityGate, 2, 'newest protected result', true)
|
|
1479
|
+
belowCapacityGate.append('request/context', {
|
|
1480
|
+
provider: 'deepseek',
|
|
1481
|
+
model: MODEL,
|
|
1482
|
+
contextWindow: 1_000_000,
|
|
1483
|
+
})
|
|
1484
|
+
belowCapacityGate.append('turn/start', { turn: 3 })
|
|
1485
|
+
ctx.toolResultPruner.pruneSession(belowCapacityGate, { stage: 'pressure' })
|
|
1486
|
+
|
|
1487
|
+
expect(rewrites(audit.records()).some(record =>
|
|
1488
|
+
record.sessionId === String(belowCapacityGate.id) && record.component === 'history')).toBe(false)
|
|
1489
|
+
expect(audit.records()).toContainEqual(expect.objectContaining({
|
|
1490
|
+
kind: 'component-evaluation',
|
|
1491
|
+
sessionId: String(belowCapacityGate.id),
|
|
1492
|
+
component: 'history',
|
|
1493
|
+
status: 'skipped',
|
|
1494
|
+
reason: 'below-micro-deadline',
|
|
1495
|
+
historyMode: 'capacity-pressure',
|
|
1496
|
+
}))
|
|
1497
|
+
|
|
1498
|
+
const aboveCapacityGate = Session.create(SessionId('public-history-capacity-active'))
|
|
1499
|
+
appendToolTurn(aboveCapacityGate, 1, 'capacity active evidence '.repeat(600), true)
|
|
1500
|
+
appendToolTurn(aboveCapacityGate, 2, 'newest protected result', true)
|
|
1501
|
+
const capacityWindow = Math.floor(measureForCompaction(ctx, aboveCapacityGate).totalTokens / 0.75)
|
|
1502
|
+
aboveCapacityGate.append('request/context', {
|
|
1503
|
+
provider: 'deepseek',
|
|
1504
|
+
model: MODEL,
|
|
1505
|
+
contextWindow: capacityWindow,
|
|
1506
|
+
})
|
|
1507
|
+
aboveCapacityGate.append('turn/start', { turn: 3 })
|
|
1508
|
+
ctx.toolResultPruner.pruneSession(aboveCapacityGate, { stage: 'pressure' })
|
|
1509
|
+
|
|
1510
|
+
expect(rewrites(audit.records())).toContainEqual(expect.objectContaining({
|
|
1511
|
+
sessionId: String(aboveCapacityGate.id),
|
|
1512
|
+
component: 'history',
|
|
1513
|
+
stage: 'pressure',
|
|
1514
|
+
historyMode: 'capacity-pressure',
|
|
1515
|
+
}))
|
|
1516
|
+
})
|
|
1517
|
+
|
|
1518
|
+
it('runs Cache Strict capacity-pressure History through the real agent pre-step boundary', async () => {
|
|
1519
|
+
const ctx = new Context()
|
|
1520
|
+
activeContexts.push(ctx)
|
|
1521
|
+
await mountAgentLoopTestDependencies(ctx)
|
|
1522
|
+
await ctx.plugin(AgentLoop, { agents: [] })
|
|
1523
|
+
await ctx.plugin(TokenMeter)
|
|
1524
|
+
const audit = captureAudit(ctx)
|
|
1525
|
+
ctx.llm.registerAdapter(
|
|
1526
|
+
['deepseek'],
|
|
1527
|
+
new NativeSummaryAdapter(['Cache Strict pressure pass complete'], 100),
|
|
1528
|
+
)
|
|
1529
|
+
await ctx.plugin(ToolResultPruner, {
|
|
1530
|
+
profile: 'cache-strict',
|
|
1531
|
+
freshTriggerTokens: 100_000,
|
|
1532
|
+
freshTargetTokens: 90_000,
|
|
1533
|
+
aggregateTriggerTokens: 100_000,
|
|
1534
|
+
aggregateTargetTokens: 90_000,
|
|
1535
|
+
historyTriggerTokens: 400,
|
|
1536
|
+
historyKeepRecentToolCalls: 0,
|
|
1537
|
+
historyKeepRecentTokens: 1,
|
|
1538
|
+
historyMinReclaimTokens: 1,
|
|
1539
|
+
}).await()
|
|
1540
|
+
|
|
1541
|
+
const agent = ctx.agentLoop.create(SessionId('public-history-capacity-request-boundary'), {
|
|
1542
|
+
provider: 'deepseek',
|
|
1543
|
+
model: MODEL,
|
|
1544
|
+
})
|
|
1545
|
+
const { session } = agent
|
|
1546
|
+
appendToolTurn(session, 1, 'old capacity-pressure evidence '.repeat(600), true)
|
|
1547
|
+
appendToolTurn(session, 2, 'newest protected result', true)
|
|
1548
|
+
const boundaryTotal = measureForCompaction(ctx, session).totalTokens
|
|
1549
|
+
session.append('request/context', {
|
|
1550
|
+
provider: 'deepseek',
|
|
1551
|
+
model: MODEL,
|
|
1552
|
+
// Just under the frozen 80% deadline D = 0.7 * window, so the gate is
|
|
1553
|
+
// active while the reclaim target stays reachable for one old result.
|
|
1554
|
+
contextWindow: Math.floor(boundaryTotal / 0.72),
|
|
1555
|
+
})
|
|
1556
|
+
|
|
1557
|
+
agent.followup(createUserMessage({
|
|
1558
|
+
content: [{ type: 'text', text: 'run Cache Strict pressure pass' }],
|
|
1559
|
+
source: { kind: 'user' },
|
|
1560
|
+
}))
|
|
1561
|
+
await agent.whenIdle()
|
|
1562
|
+
|
|
1563
|
+
expect(rewrites(audit.records())).toContainEqual(expect.objectContaining({
|
|
1564
|
+
sessionId: String(session.id),
|
|
1565
|
+
component: 'history',
|
|
1566
|
+
stage: 'pressure',
|
|
1567
|
+
historyMode: 'capacity-pressure',
|
|
1568
|
+
}))
|
|
1569
|
+
})
|
|
1570
|
+
|
|
1571
|
+
it('audits enabled-but-below-trigger without claiming a rewrite', async () => {
|
|
1572
|
+
const ctx = await runtimeContext()
|
|
1573
|
+
const audit = captureAudit(ctx)
|
|
1574
|
+
await ctx.plugin(ToolResultPruner, {
|
|
1575
|
+
profile: 'balanced',
|
|
1576
|
+
freshTriggerTokens: 100_000,
|
|
1577
|
+
freshTargetTokens: 90_000,
|
|
1578
|
+
aggregateTriggerTokens: 100_000,
|
|
1579
|
+
aggregateTargetTokens: 90_000,
|
|
1580
|
+
historyTriggerTokens: 100_000,
|
|
1581
|
+
}).await()
|
|
1582
|
+
const session = Session.create(SessionId('public-below-trigger-audit'))
|
|
1583
|
+
appendToolTurn(session, 1, 'small evidence', false)
|
|
1584
|
+
|
|
1585
|
+
ctx.toolResultPruner.pruneSession(session, {
|
|
1586
|
+
stage: 'fresh',
|
|
1587
|
+
freshTurn: 1,
|
|
1588
|
+
freshStep: 1,
|
|
1589
|
+
})
|
|
1590
|
+
|
|
1591
|
+
expect(rewrites(audit.records())).toHaveLength(0)
|
|
1592
|
+
expect(audit.records()).toContainEqual(expect.objectContaining({
|
|
1593
|
+
kind: 'component-evaluation',
|
|
1594
|
+
component: 'fresh',
|
|
1595
|
+
status: 'skipped',
|
|
1596
|
+
reason: 'at-or-below-trigger',
|
|
1597
|
+
currentTokens: expect.any(Number),
|
|
1598
|
+
triggerTokens: 100_000,
|
|
1599
|
+
}))
|
|
1600
|
+
})
|
|
1601
|
+
|
|
1602
|
+
it('freezes the complete settings and deployment snapshot on first Session observation', async () => {
|
|
1603
|
+
const ctx = new Context()
|
|
1604
|
+
activeContexts.push(ctx)
|
|
1605
|
+
await ctx.plugin(TestSettings).await()
|
|
1606
|
+
await ctx.plugin(SelectorHost).await()
|
|
1607
|
+
await ctx.plugin(SessionStore)
|
|
1608
|
+
await ctx.plugin(SystemPrompt)
|
|
1609
|
+
await ctx.plugin(ToolRuntime)
|
|
1610
|
+
await ctx.plugin(TokenMeter)
|
|
1611
|
+
const audit = captureAudit(ctx)
|
|
1612
|
+
|
|
1613
|
+
const firstCustom = structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY) as CustomCompressionPolicy
|
|
1614
|
+
firstCustom.fresh = { enabled: true, trigger: 512, target: 256 }
|
|
1615
|
+
firstCustom.aggregate.enabled = false
|
|
1616
|
+
firstCustom.history.enabled = false
|
|
1617
|
+
if (firstCustom.version === 3) firstCustom.tailTrim.enabled = false
|
|
1618
|
+
const namespace = settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
|
|
1619
|
+
await ctx.settings.update(namespace, { profile: 'custom', custom: firstCustom })
|
|
1620
|
+
await ctx.plugin(ToolResultPruner, {
|
|
1621
|
+
profile: 'off',
|
|
1622
|
+
headChars: 6,
|
|
1623
|
+
tailChars: 4,
|
|
1624
|
+
}).await()
|
|
1625
|
+
|
|
1626
|
+
const sessionA = Session.create(SessionId('public-policy-freeze-a'))
|
|
1627
|
+
appendToolTurn(sessionA, 1, 'first frozen policy result '.repeat(800), false)
|
|
1628
|
+
expect(ctx.toolResultPruner.pruneSession(sessionA, {
|
|
1629
|
+
stage: 'fresh', freshTurn: 1, freshStep: 1,
|
|
1630
|
+
}).pruned).toHaveLength(1)
|
|
1631
|
+
|
|
1632
|
+
const secondCustom = structuredClone(firstCustom)
|
|
1633
|
+
secondCustom.fresh.enabled = false
|
|
1634
|
+
secondCustom.history.trigger += 123
|
|
1635
|
+
await ctx.settings.update(namespace, { custom: secondCustom })
|
|
1636
|
+
sessionA.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
|
1637
|
+
appendToolTurn(sessionA, 2, 'second frozen policy result '.repeat(800), false)
|
|
1638
|
+
expect(ctx.toolResultPruner.pruneSession(sessionA, {
|
|
1639
|
+
stage: 'fresh', freshTurn: 2, freshStep: 1,
|
|
1640
|
+
}).pruned).toHaveLength(1)
|
|
1641
|
+
|
|
1642
|
+
const sessionB = Session.create(SessionId('public-policy-freeze-b'))
|
|
1643
|
+
const keptB = appendToolTurn(sessionB, 1, 'new Session disabled Fresh '.repeat(800), false)
|
|
1644
|
+
expect(ctx.toolResultPruner.pruneSession(sessionB, {
|
|
1645
|
+
stage: 'fresh', freshTurn: 1, freshStep: 1,
|
|
1646
|
+
})).toEqual({ pruned: [], charsRemoved: 0, tokensRemoved: 0 })
|
|
1647
|
+
expect(sessionB.surface.nodes).toContain(keptB.resultSeq)
|
|
1648
|
+
|
|
1649
|
+
const frozen = audit.records().filter(record => record.kind === 'policy-frozen')
|
|
1650
|
+
expect(frozen).toHaveLength(2)
|
|
1651
|
+
expect(frozen.find(record => record.sessionId === String(sessionA.id))).toMatchObject({
|
|
1652
|
+
settingsSource: 'host-settings',
|
|
1653
|
+
settings: { profile: 'custom', custom: firstCustom },
|
|
1654
|
+
deploymentConfig: { profile: 'off', headChars: 6, tailChars: 4 },
|
|
1655
|
+
})
|
|
1656
|
+
expect(frozen.find(record => record.sessionId === String(sessionB.id))).toMatchObject({
|
|
1657
|
+
settingsSource: 'host-settings',
|
|
1658
|
+
settings: { profile: 'custom', custom: secondCustom },
|
|
1659
|
+
deploymentConfig: { profile: 'off', headChars: 6, tailChars: 4 },
|
|
1660
|
+
})
|
|
1661
|
+
})
|
|
1662
|
+
|
|
1663
|
+
it('publishes TailTrim through standard prune + user replacement and validates append roots', async () => {
|
|
1664
|
+
const ctx = new Context()
|
|
1665
|
+
activeContexts.push(ctx)
|
|
1666
|
+
await ctx.plugin(TestSettings).await()
|
|
1667
|
+
await ctx.plugin(SelectorHost).await()
|
|
1668
|
+
await ctx.plugin(SessionStore)
|
|
1669
|
+
await ctx.plugin(SystemPrompt)
|
|
1670
|
+
await ctx.plugin(ToolRuntime)
|
|
1671
|
+
await ctx.plugin(TokenMeter)
|
|
1672
|
+
const audit = captureAudit(ctx)
|
|
1673
|
+
|
|
1674
|
+
const policy = structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY) as CustomCompressionPolicy
|
|
1675
|
+
if (policy.version !== 3) throw new Error('TailTrim public protocol test requires policy v3')
|
|
1676
|
+
policy.fresh.enabled = false
|
|
1677
|
+
policy.aggregate.enabled = false
|
|
1678
|
+
policy.history.enabled = false
|
|
1679
|
+
// Isolate TailTrim: keep no History working set protected.
|
|
1680
|
+
policy.history.keepRecentToolCalls = 0
|
|
1681
|
+
policy.history.keepRecentTokens = 0
|
|
1682
|
+
policy.history.minReclaim = 1
|
|
1683
|
+
policy.tailTrim = { enabled: true, trigger: 8 }
|
|
1684
|
+
await ctx.settings.update(settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
|
|
1685
|
+
profile: 'custom',
|
|
1686
|
+
custom: policy,
|
|
1687
|
+
})
|
|
1688
|
+
await ctx.plugin(ToolResultPruner, { profile: 'off' }).await()
|
|
1689
|
+
const pruner = ctx.toolResultPruner
|
|
1690
|
+
const session = Session.create(SessionId('public-tailtrim'))
|
|
1691
|
+
appendToolTurn(session, 1, 'first completed group'.repeat(40), true)
|
|
1692
|
+
const candidate = appendToolTurn(session, 2, 'old candidate result'.repeat(400), true)
|
|
1693
|
+
session.append('turn/start', { turn: 3 })
|
|
1694
|
+
expect(ctx.tools.get('context_compression_retrieve')).toBeDefined()
|
|
1695
|
+
const before = measureForCompaction(ctx, session)
|
|
1696
|
+
expect(before.currentSurface.kind).toBe('exact-tokenizer')
|
|
1697
|
+
|
|
1698
|
+
pruner.pruneSession(session, { stage: 'pressure' })
|
|
1699
|
+
|
|
1700
|
+
const manifest = session.events.findLast(event => event.type === 'compaction/prune')
|
|
1701
|
+
expect(manifest?.type).toBe('compaction/prune')
|
|
1702
|
+
if (manifest?.type !== 'compaction/prune') throw new Error('TailTrim did not publish a standard prune')
|
|
1703
|
+
const publication = validatePublishedTailTrim(session, manifest.seq)
|
|
1704
|
+
expect(publication).not.toBeNull()
|
|
1705
|
+
expect(publication?.manifest.data.shadowedSeqs).toEqual([
|
|
1706
|
+
candidate.assistantSeq,
|
|
1707
|
+
candidate.resultSeq,
|
|
1708
|
+
])
|
|
1709
|
+
expect(publication?.replacement.type).toBe('user/message')
|
|
1710
|
+
expect(publication?.ref).toBe(`session://${String(session.id)}/tailtrim/${String(manifest.seq)}`)
|
|
1711
|
+
expect(ctx.tools.get('context_compression_retrieve')).toBeDefined()
|
|
1712
|
+
expect(rewrites(audit.records())).toContainEqual(expect.objectContaining({
|
|
1713
|
+
component: 'tail-trim',
|
|
1714
|
+
stage: 'pressure',
|
|
1715
|
+
reducer: 'pair-preserving-tail-trim',
|
|
1716
|
+
manifestSeq: manifest.seq,
|
|
1717
|
+
tokensBefore: expect.any(Number),
|
|
1718
|
+
tokensAfter: expect.any(Number),
|
|
1719
|
+
}))
|
|
1720
|
+
|
|
1721
|
+
const recovered = await ctx.tools.execute({
|
|
1722
|
+
name: 'context_compression_retrieve',
|
|
1723
|
+
arguments: { ref: publication?.ref, max_lines: 20 },
|
|
1724
|
+
callId: CallId('tailtrim-retrieve-live'),
|
|
1725
|
+
signal: new AbortController().signal,
|
|
1726
|
+
agent: stubAgent(ctx, session),
|
|
1727
|
+
})
|
|
1728
|
+
expect(recovered.isError).toBe(false)
|
|
1729
|
+
const recoveredText = recovered.content
|
|
1730
|
+
.map(block => block.type === 'text' ? block.text : '')
|
|
1731
|
+
.join('\n')
|
|
1732
|
+
expect(recoveredText).toContain('kind: tailtrim-group')
|
|
1733
|
+
expect(recoveredText).toContain('old candidate result')
|
|
1734
|
+
|
|
1735
|
+
const persistedEvents = JSON.parse(JSON.stringify(session.events))
|
|
1736
|
+
const replay = Session.create(session.id, persistedEvents)
|
|
1737
|
+
const replayPublication = validatePublishedTailTrim(replay, manifest.seq)
|
|
1738
|
+
expect(replayPublication?.ref).toBe(publication?.ref)
|
|
1739
|
+
expect(replay.deriveMessages()).toEqual(session.deriveMessages())
|
|
1740
|
+
const replayRecovered = await ctx.tools.execute({
|
|
1741
|
+
name: 'context_compression_retrieve',
|
|
1742
|
+
arguments: { ref: replayPublication?.ref, max_lines: 20 },
|
|
1743
|
+
callId: CallId('tailtrim-retrieve-replay'),
|
|
1744
|
+
signal: new AbortController().signal,
|
|
1745
|
+
agent: stubAgent(ctx, replay),
|
|
1746
|
+
})
|
|
1747
|
+
expect(replayRecovered).toEqual(recovered)
|
|
1748
|
+
})
|
|
1749
|
+
|
|
1750
|
+
it('audits TailTrim threshold, tokenizer, safety-group, and min-reclaim skip paths', async () => {
|
|
1751
|
+
async function runScenario(options: {
|
|
1752
|
+
readonly id: string
|
|
1753
|
+
readonly expectedReason: string
|
|
1754
|
+
readonly trigger: number
|
|
1755
|
+
readonly minReclaim: number
|
|
1756
|
+
readonly candidateTurns: number
|
|
1757
|
+
readonly model?: string
|
|
1758
|
+
}): Promise<void> {
|
|
1759
|
+
const ctx = new Context()
|
|
1760
|
+
activeContexts.push(ctx)
|
|
1761
|
+
await ctx.plugin(TestSettings).await()
|
|
1762
|
+
await ctx.plugin(SelectorHost).await()
|
|
1763
|
+
await ctx.plugin(SessionStore)
|
|
1764
|
+
await ctx.plugin(SystemPrompt)
|
|
1765
|
+
await ctx.plugin(ToolRuntime)
|
|
1766
|
+
await ctx.plugin(TokenMeter)
|
|
1767
|
+
const audit = captureAudit(ctx)
|
|
1768
|
+
|
|
1769
|
+
const policy = structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY) as CustomCompressionPolicy
|
|
1770
|
+
if (policy.version !== 3) throw new Error('TailTrim negative-path test requires policy v3')
|
|
1771
|
+
policy.fresh.enabled = false
|
|
1772
|
+
policy.aggregate.enabled = false
|
|
1773
|
+
policy.history.enabled = false
|
|
1774
|
+
policy.history.trigger = Math.max(policy.history.trigger, options.minReclaim)
|
|
1775
|
+
policy.history.keepRecentToolCalls = 0
|
|
1776
|
+
policy.history.keepRecentTokens = 0
|
|
1777
|
+
policy.history.minReclaim = options.minReclaim
|
|
1778
|
+
policy.tailTrim = { enabled: true, trigger: options.trigger }
|
|
1779
|
+
await ctx.settings.update(settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
|
|
1780
|
+
profile: 'custom',
|
|
1781
|
+
custom: policy,
|
|
1782
|
+
})
|
|
1783
|
+
await ctx.plugin(ToolResultPruner, { profile: 'off' }).await()
|
|
1784
|
+
|
|
1785
|
+
const session = Session.create(SessionId(options.id))
|
|
1786
|
+
if (options.model !== undefined) {
|
|
1787
|
+
session.append('request/header', {
|
|
1788
|
+
reason: 'initial',
|
|
1789
|
+
header: canonicalHeader({ config: { provider: 'deepseek', model: options.model } }),
|
|
1790
|
+
})
|
|
1791
|
+
}
|
|
1792
|
+
for (let turn = 1; turn <= options.candidateTurns; turn++) {
|
|
1793
|
+
appendToolTurn(session, turn, `tailtrim negative ${options.id} `.repeat(200), true)
|
|
1794
|
+
}
|
|
1795
|
+
session.append('turn/start', { turn: options.candidateTurns + 1 })
|
|
1796
|
+
|
|
1797
|
+
ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
|
|
1798
|
+
|
|
1799
|
+
expect(rewrites(audit.records()).filter(record => record.component === 'tail-trim')).toHaveLength(0)
|
|
1800
|
+
expect(audit.records()).toContainEqual(expect.objectContaining({
|
|
1801
|
+
kind: 'component-evaluation',
|
|
1802
|
+
sessionId: String(session.id),
|
|
1803
|
+
component: 'tail-trim',
|
|
1804
|
+
status: 'skipped',
|
|
1805
|
+
reason: options.expectedReason,
|
|
1806
|
+
}))
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
await runScenario({
|
|
1810
|
+
id: 'public-tailtrim-below-trigger',
|
|
1811
|
+
expectedReason: 'at-or-below-trigger',
|
|
1812
|
+
trigger: 1_000_000,
|
|
1813
|
+
minReclaim: 1,
|
|
1814
|
+
candidateTurns: 2,
|
|
1815
|
+
})
|
|
1816
|
+
await runScenario({
|
|
1817
|
+
id: 'public-tailtrim-tokenizer-unavailable',
|
|
1818
|
+
expectedReason: 'exact-tokenizer-unavailable',
|
|
1819
|
+
trigger: 1,
|
|
1820
|
+
minReclaim: 1,
|
|
1821
|
+
candidateTurns: 2,
|
|
1822
|
+
model: 'unsupported-public-model',
|
|
1823
|
+
})
|
|
1824
|
+
await runScenario({
|
|
1825
|
+
id: 'public-tailtrim-first-group-protected',
|
|
1826
|
+
expectedReason: 'no-safe-eligible-tool-group',
|
|
1827
|
+
trigger: 1,
|
|
1828
|
+
minReclaim: 1,
|
|
1829
|
+
candidateTurns: 1,
|
|
1830
|
+
})
|
|
1831
|
+
await runScenario({
|
|
1832
|
+
id: 'public-tailtrim-min-reclaim',
|
|
1833
|
+
expectedReason: 'no-safe-eligible-tool-group',
|
|
1834
|
+
trigger: 1,
|
|
1835
|
+
minReclaim: 1_000_000,
|
|
1836
|
+
candidateTurns: 2,
|
|
1837
|
+
})
|
|
1838
|
+
})
|
|
1839
|
+
|
|
1840
|
+
it('fails open after an orphan prune and remains appendable after JSON restart', async () => {
|
|
1841
|
+
const ctx = new Context()
|
|
1842
|
+
activeContexts.push(ctx)
|
|
1843
|
+
await ctx.plugin(TestSettings).await()
|
|
1844
|
+
await ctx.plugin(SelectorHost).await()
|
|
1845
|
+
await ctx.plugin(SessionStore)
|
|
1846
|
+
await ctx.plugin(InvariantRegistry)
|
|
1847
|
+
await ctx.plugin(RuntimeInvariant)
|
|
1848
|
+
await ctx.plugin(SystemPrompt)
|
|
1849
|
+
await ctx.plugin(ToolRuntime)
|
|
1850
|
+
await ctx.plugin(TokenMeter)
|
|
1851
|
+
const audit = captureAudit(ctx)
|
|
1852
|
+
|
|
1853
|
+
const policy = structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY) as CustomCompressionPolicy
|
|
1854
|
+
if (policy.version !== 3) throw new Error('orphan recovery test requires policy v3')
|
|
1855
|
+
policy.fresh.enabled = false
|
|
1856
|
+
policy.aggregate.enabled = false
|
|
1857
|
+
policy.history.enabled = false
|
|
1858
|
+
policy.history.keepRecentToolCalls = 0
|
|
1859
|
+
policy.history.keepRecentTokens = 0
|
|
1860
|
+
policy.history.minReclaim = 1
|
|
1861
|
+
policy.tailTrim = { enabled: true, trigger: 8 }
|
|
1862
|
+
await ctx.settings.update(settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
|
|
1863
|
+
profile: 'custom',
|
|
1864
|
+
custom: policy,
|
|
1865
|
+
})
|
|
1866
|
+
await ctx.plugin(ToolResultPruner, { profile: 'off' }).await()
|
|
1867
|
+
const session = ctx.sessions.create(SessionId('public-tailtrim-orphan-recovery'))
|
|
1868
|
+
appendToolTurn(session, 1, 'first completed group'.repeat(40), true)
|
|
1869
|
+
appendToolTurn(session, 2, 'orphan source remains intact'.repeat(400), true)
|
|
1870
|
+
session.append('turn/start', { turn: 3 })
|
|
1871
|
+
const beforeSurface = [...session.surface.nodes]
|
|
1872
|
+
const originalAppend = session.append.bind(session)
|
|
1873
|
+
const append = vi.spyOn(session, 'append').mockImplementation((type: string, ...args: unknown[]) => {
|
|
1874
|
+
const options = args[1] as { surfaceOp?: unknown } | undefined
|
|
1875
|
+
if (type === 'user/message' && typeof options?.surfaceOp === 'object') {
|
|
1876
|
+
throw new Error('fixture replacement append failure')
|
|
1877
|
+
}
|
|
1878
|
+
return Reflect.apply(
|
|
1879
|
+
originalAppend as (...values: unknown[]) => unknown,
|
|
1880
|
+
undefined,
|
|
1881
|
+
[type, ...args],
|
|
1882
|
+
) as never
|
|
1883
|
+
})
|
|
1884
|
+
|
|
1885
|
+
expect(() => ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })).not.toThrow()
|
|
1886
|
+
append.mockRestore()
|
|
1887
|
+
expect(session.events.at(-1)?.type).toBe('compaction/prune')
|
|
1888
|
+
expect(session.surface.nodes).toEqual(beforeSurface)
|
|
1889
|
+
expect(rewrites(audit.records())).toHaveLength(0)
|
|
1890
|
+
expect(audit.records()).toContainEqual(expect.objectContaining({
|
|
1891
|
+
kind: 'failure',
|
|
1892
|
+
operation: 'publication',
|
|
1893
|
+
component: 'tail-trim',
|
|
1894
|
+
manifestSeq: session.events.length - 1,
|
|
1895
|
+
}))
|
|
1896
|
+
|
|
1897
|
+
expect(() => session.append('turn/end', {
|
|
1898
|
+
turn: 3,
|
|
1899
|
+
reason: { kind: 'completed' },
|
|
1900
|
+
})).not.toThrow()
|
|
1901
|
+
const persisted = JSON.parse(JSON.stringify(session.events))
|
|
1902
|
+
|
|
1903
|
+
const resumedCtx = new Context()
|
|
1904
|
+
activeContexts.push(resumedCtx)
|
|
1905
|
+
await resumedCtx.plugin(SessionStore)
|
|
1906
|
+
await resumedCtx.plugin(InvariantRegistry)
|
|
1907
|
+
await resumedCtx.plugin(RuntimeInvariant)
|
|
1908
|
+
const resumed = resumedCtx.sessions.create(session.id, { seed: persisted })
|
|
1909
|
+
expect(resumed.surface.nodes).toEqual(beforeSurface)
|
|
1910
|
+
expect(() => resumed.append('turn/start', { turn: 4 })).not.toThrow()
|
|
1911
|
+
})
|
|
1912
|
+
|
|
1913
|
+
it('still rejects a malformed adjacent replacement instead of treating it as an orphan', async () => {
|
|
1914
|
+
const ctx = new Context()
|
|
1915
|
+
activeContexts.push(ctx)
|
|
1916
|
+
await ctx.plugin(SessionStore)
|
|
1917
|
+
await ctx.plugin(InvariantRegistry)
|
|
1918
|
+
await ctx.plugin(RuntimeInvariant)
|
|
1919
|
+
const session = ctx.sessions.create(SessionId('public-malformed-companion'))
|
|
1920
|
+
const source = appendToolTurn(session, 1, 'source remains intact', false)
|
|
1921
|
+
session.append('compaction/prune', {
|
|
1922
|
+
shadowedRange: { start: source.resultSeq, end: source.resultSeq },
|
|
1923
|
+
shadowedSeqs: [source.resultSeq],
|
|
1924
|
+
shadowedTokenCount: 1,
|
|
1925
|
+
})
|
|
1926
|
+
const result = session.events[source.resultSeq]
|
|
1927
|
+
if (result?.type !== 'tool/result') throw new Error('missing source result')
|
|
1928
|
+
const before = session.seq
|
|
1929
|
+
|
|
1930
|
+
expect(() => session.append('tool/result', result.data, {
|
|
1931
|
+
surfaceOp: { op: 'replace', start: source.assistantSeq, end: source.assistantSeq },
|
|
1932
|
+
sourceEventSeqs: [source.resultSeq],
|
|
1933
|
+
})).toThrow(/sourceEventSeqs|does not replace compaction\/prune range/)
|
|
1934
|
+
expect(session.seq).toBe(before)
|
|
1935
|
+
})
|
|
1936
|
+
|
|
1937
|
+
it('observes committed Native auto-compact separately from plugin rewrites', async () => {
|
|
1938
|
+
const ctx = await runtimeContext()
|
|
1939
|
+
const audit = captureAudit(ctx)
|
|
1940
|
+
await ctx.plugin(ToolResultPruner, { profile: 'off' }).await()
|
|
1941
|
+
const session = Session.create(SessionId('public-native-auto-audit'))
|
|
1942
|
+
const summary = session.append('compaction/summary', {
|
|
1943
|
+
compactionId: CompactionId('public-native-auto'),
|
|
1944
|
+
summary: [{ type: 'text', text: 'summary' }],
|
|
1945
|
+
shadowedRange: { start: 0, end: 0 },
|
|
1946
|
+
shadowedSeqs: [],
|
|
1947
|
+
shadowedTokenCount: 321,
|
|
1948
|
+
provider: 'deepseek',
|
|
1949
|
+
model: MODEL,
|
|
1950
|
+
})
|
|
1951
|
+
|
|
1952
|
+
ctx.emit('session/event', session, summary)
|
|
1953
|
+
|
|
1954
|
+
expect(rewrites(audit.records())).toHaveLength(0)
|
|
1955
|
+
expect(audit.records()).toContainEqual(expect.objectContaining({
|
|
1956
|
+
kind: 'native-auto-compact',
|
|
1957
|
+
manifestEventType: 'compaction/summary',
|
|
1958
|
+
manifestSeq: summary.seq,
|
|
1959
|
+
provider: 'deepseek',
|
|
1960
|
+
model: MODEL,
|
|
1961
|
+
tokensBefore: 321,
|
|
1962
|
+
tokensAfter: null,
|
|
1963
|
+
}))
|
|
1964
|
+
})
|
|
1965
|
+
|
|
1966
|
+
it('triggers Native auto-compact through the real AgentLoop pre-step boundary', async () => {
|
|
1967
|
+
const ctx = new Context()
|
|
1968
|
+
activeContexts.push(ctx)
|
|
1969
|
+
await mountAgentLoopTestDependencies(ctx)
|
|
1970
|
+
await ctx.plugin(AgentLoop, { agents: [] })
|
|
1971
|
+
await ctx.plugin(TokenMeter)
|
|
1972
|
+
const audit = captureAudit(ctx)
|
|
1973
|
+
ctx.llm.registerAdapter(
|
|
1974
|
+
['deepseek'],
|
|
1975
|
+
new NativeSummaryAdapter([
|
|
1976
|
+
'first native turn complete',
|
|
1977
|
+
'native compact summary',
|
|
1978
|
+
'second native turn complete',
|
|
1979
|
+
], 1_000),
|
|
1980
|
+
)
|
|
1981
|
+
await ctx.plugin(ToolResultPruner, { profile: 'off' }).await()
|
|
1982
|
+
void new BasicCompactionEngine(ctx, {
|
|
1983
|
+
auto: true,
|
|
1984
|
+
thresholdRatio: 0.3,
|
|
1985
|
+
retainTokens: 0,
|
|
1986
|
+
maxTokens: 100,
|
|
1987
|
+
compactionRetries: 0,
|
|
1988
|
+
})
|
|
1989
|
+
const agent = ctx.agentLoop.create(SessionId('public-native-auto-real'), {
|
|
1990
|
+
provider: 'deepseek',
|
|
1991
|
+
model: MODEL,
|
|
1992
|
+
})
|
|
1993
|
+
|
|
1994
|
+
agent.followup(createUserMessage({
|
|
1995
|
+
content: [{ type: 'text', text: 'native pressure evidence '.repeat(500) }],
|
|
1996
|
+
source: { kind: 'user' },
|
|
1997
|
+
}))
|
|
1998
|
+
await agent.whenIdle()
|
|
1999
|
+
agent.followup(createUserMessage({
|
|
2000
|
+
content: [{ type: 'text', text: 'continue after native compaction' }],
|
|
2001
|
+
source: { kind: 'user' },
|
|
2002
|
+
}))
|
|
2003
|
+
await agent.whenIdle()
|
|
2004
|
+
|
|
2005
|
+
expect(rewrites(audit.records())).toHaveLength(0)
|
|
2006
|
+
const summary = agent.session.events.findLast(event => event.type === 'compaction/summary')
|
|
2007
|
+
expect(summary?.type).toBe('compaction/summary')
|
|
2008
|
+
expect(audit.records()).toContainEqual(expect.objectContaining({
|
|
2009
|
+
kind: 'native-auto-compact',
|
|
2010
|
+
manifestEventType: 'compaction/summary',
|
|
2011
|
+
manifestSeq: summary?.seq,
|
|
2012
|
+
provider: 'deepseek',
|
|
2013
|
+
model: MODEL,
|
|
2014
|
+
tokensBefore: expect.any(Number),
|
|
2015
|
+
tokensAfter: null,
|
|
2016
|
+
}))
|
|
2017
|
+
})
|
|
2018
|
+
|
|
2019
|
+
it('runs Fresh, Aggregate, History, TailTrim and Native in one non-isolated audited session', async () => {
|
|
2020
|
+
const ctx = new Context()
|
|
2021
|
+
activeContexts.push(ctx)
|
|
2022
|
+
await ctx.plugin(TestSettings).await()
|
|
2023
|
+
await ctx.plugin(SelectorHost).await()
|
|
2024
|
+
await ctx.plugin(LlmRuntime)
|
|
2025
|
+
await ctx.plugin(SessionStore)
|
|
2026
|
+
await ctx.plugin(SystemPrompt)
|
|
2027
|
+
await ctx.plugin(ToolRuntime)
|
|
2028
|
+
await ctx.plugin(TokenMeter)
|
|
2029
|
+
const audit = captureAudit(ctx)
|
|
2030
|
+
|
|
2031
|
+
const policy = structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY) as CustomCompressionPolicy
|
|
2032
|
+
if (policy.version !== 3) throw new Error('full-pipeline public E2E requires policy v3')
|
|
2033
|
+
policy.fresh = { enabled: true, trigger: 512, target: 256 }
|
|
2034
|
+
policy.aggregate = { enabled: true, trigger: 1_000, target: 400 }
|
|
2035
|
+
policy.history = {
|
|
2036
|
+
enabled: true,
|
|
2037
|
+
// The strict required-reclaim gate demands one batch pull tool tokens
|
|
2038
|
+
// back under this trigger: it must sit above the large protected tail
|
|
2039
|
+
// (so the batch is reachable) while the residual total stays far above
|
|
2040
|
+
// the auto-compact threshold (half the measured total) afterwards.
|
|
2041
|
+
trigger: 7_800,
|
|
2042
|
+
keepRecentToolCalls: 0,
|
|
2043
|
+
keepRecentTokens: 1,
|
|
2044
|
+
minReclaim: 1,
|
|
2045
|
+
}
|
|
2046
|
+
policy.prefixPolicy = 'pressure-break'
|
|
2047
|
+
policy.tailTrim = { enabled: true, trigger: 8 }
|
|
2048
|
+
await ctx.settings.update(settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
|
|
2049
|
+
profile: 'custom',
|
|
2050
|
+
custom: policy,
|
|
2051
|
+
})
|
|
2052
|
+
await ctx.plugin(ToolResultPruner, { profile: 'off' }).await()
|
|
2053
|
+
|
|
2054
|
+
const session = ctx.sessions.create(SessionId('public-full-pipeline-e2e'))
|
|
2055
|
+
appendToolTurn(session, 1, 'fresh full pipeline '.repeat(600), false, 'run Fresh')
|
|
2056
|
+
ctx.toolResultPruner.pruneSession(session, {
|
|
2057
|
+
stage: 'fresh',
|
|
2058
|
+
freshTurn: 1,
|
|
2059
|
+
freshStep: 1,
|
|
2060
|
+
})
|
|
2061
|
+
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
|
2062
|
+
|
|
2063
|
+
appendToolBatchTurn(
|
|
2064
|
+
session,
|
|
2065
|
+
2,
|
|
2066
|
+
Array.from({ length: 30 }, (_, index) => `aggregate-${String(index)} `.repeat(20)),
|
|
2067
|
+
false,
|
|
2068
|
+
'run Aggregate',
|
|
2069
|
+
)
|
|
2070
|
+
ctx.toolResultPruner.pruneSession(session, {
|
|
2071
|
+
stage: 'fresh',
|
|
2072
|
+
freshTurn: 2,
|
|
2073
|
+
freshStep: 1,
|
|
2074
|
+
})
|
|
2075
|
+
session.append('turn/end', { turn: 2, reason: { kind: 'completed' } })
|
|
2076
|
+
// Leave one old, still-original result for the pressure-stage History
|
|
2077
|
+
// reducer, then add a newer working-set result that remains protected.
|
|
2078
|
+
appendToolTurn(session, 3, 'history full pipeline '.repeat(600), true, 'run History')
|
|
2079
|
+
appendToolTurn(session, 4, 'recent protected working-set context '.repeat(1200), true, 'retain recent context')
|
|
2080
|
+
session.append('turn/start', { turn: 5 })
|
|
2081
|
+
ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
|
|
2082
|
+
|
|
2083
|
+
const beforeNative = ctx.tokenMeter.measure(session).totalTokens
|
|
2084
|
+
expect(beforeNative).toBeGreaterThan(2)
|
|
2085
|
+
ctx.llm.registerAdapter(
|
|
2086
|
+
['deepseek'],
|
|
2087
|
+
new NativeSummaryAdapter(['full-pipeline native summary'], beforeNative),
|
|
2088
|
+
)
|
|
2089
|
+
void new BasicCompactionEngine(ctx, {
|
|
2090
|
+
auto: true,
|
|
2091
|
+
thresholdRatio: 0.5,
|
|
2092
|
+
retainTokens: 0,
|
|
2093
|
+
maxTokens: 100,
|
|
2094
|
+
compactionRetries: 0,
|
|
2095
|
+
})
|
|
2096
|
+
const signal = new AbortController().signal
|
|
2097
|
+
const decision = await agentEvents(ctx, stubAgent(ctx, session)).waterfall(
|
|
2098
|
+
'agent/pre-step',
|
|
2099
|
+
{ messages: [], turn: 5, step: 1, signal },
|
|
2100
|
+
() => Promise.resolve({ kind: 'enter' as const, messages: [] }),
|
|
2101
|
+
)
|
|
2102
|
+
expect(decision).toEqual({ kind: 'enter', messages: [] })
|
|
2103
|
+
const summary = session.events.findLast(event => event.type === 'compaction/summary')
|
|
2104
|
+
expect(summary?.type).toBe('compaction/summary')
|
|
2105
|
+
|
|
2106
|
+
const records = audit.records()
|
|
2107
|
+
const rewriteRecords = rewrites(records)
|
|
2108
|
+
const components = rewriteRecords.map(record => record.component)
|
|
2109
|
+
expect(components).toContain('fresh')
|
|
2110
|
+
expect(components).toContain('aggregate')
|
|
2111
|
+
expect(components).toContain('history')
|
|
2112
|
+
expect(components).toContain('tail-trim')
|
|
2113
|
+
expect(records).toContainEqual(expect.objectContaining({
|
|
2114
|
+
kind: 'native-auto-compact',
|
|
2115
|
+
manifestSeq: summary?.seq,
|
|
2116
|
+
tokensBefore: expect.any(Number),
|
|
2117
|
+
}))
|
|
2118
|
+
|
|
2119
|
+
const firstIndex = (component: CompressionRewriteAuditRecord['component']) =>
|
|
2120
|
+
records.findIndex(record => record.kind === 'rewrite' && record.component === component)
|
|
2121
|
+
expect(firstIndex('fresh')).toBeLessThan(firstIndex('aggregate'))
|
|
2122
|
+
expect(firstIndex('aggregate')).toBeLessThan(firstIndex('history'))
|
|
2123
|
+
expect(firstIndex('history')).toBeLessThan(firstIndex('tail-trim'))
|
|
2124
|
+
expect(records.findIndex(record => record.kind === 'native-auto-compact'))
|
|
2125
|
+
.toBeGreaterThan(firstIndex('tail-trim'))
|
|
2126
|
+
expect(session.events.some(event => event.type === ('compaction/group-trim' as string))).toBe(false)
|
|
2127
|
+
})
|
|
2128
|
+
|
|
2129
|
+
it('compresses text tool results exactly in a vision session that also carries a user image', async () => {
|
|
2130
|
+
const ctx = await runtimeContext()
|
|
2131
|
+
const audit = captureAudit(ctx)
|
|
2132
|
+
await ctx.plugin(ToolResultPruner, {
|
|
2133
|
+
profile: 'balanced',
|
|
2134
|
+
freshTriggerTokens: 1_000_000,
|
|
2135
|
+
freshTargetTokens: 900_000,
|
|
2136
|
+
aggregateTriggerTokens: 100,
|
|
2137
|
+
aggregateTargetTokens: 64,
|
|
2138
|
+
historyTriggerTokens: 600,
|
|
2139
|
+
historyKeepRecentToolCalls: 0,
|
|
2140
|
+
historyKeepRecentTokens: 1,
|
|
2141
|
+
historyMinReclaimTokens: 1,
|
|
2142
|
+
}).await()
|
|
2143
|
+
const session = Session.create(SessionId('public-vision-text-session'))
|
|
2144
|
+
session.append('turn/start', { turn: 1 })
|
|
2145
|
+
session.append('request/header', {
|
|
2146
|
+
reason: 'initial',
|
|
2147
|
+
header: canonicalHeader({ config: { provider: 'deepseek', model: VISION_MODEL } }),
|
|
2148
|
+
})
|
|
2149
|
+
session.append('user/message', createUserMessage({
|
|
2150
|
+
content: [
|
|
2151
|
+
imageBlock(640, 480),
|
|
2152
|
+
{ type: 'text', text: 'describe the attachment and run the tools' },
|
|
2153
|
+
],
|
|
2154
|
+
source: { kind: 'user' },
|
|
2155
|
+
}), { surfaceOp: 'append' })
|
|
2156
|
+
session.append('step/start', { turn: 1, step: 1 })
|
|
2157
|
+
session.append('assistant/message', {
|
|
2158
|
+
turn: 1,
|
|
2159
|
+
step: 1,
|
|
2160
|
+
message: createMessage({
|
|
2161
|
+
role: 'assistant',
|
|
2162
|
+
content: [{ type: 'tool-call', id: CallId('vision-call-1'), name: 'bash', arguments: '{}' }],
|
|
2163
|
+
source: { kind: 'model', provider: 'deepseek', model: VISION_MODEL },
|
|
2164
|
+
}),
|
|
2165
|
+
}, { surfaceOp: 'append' })
|
|
2166
|
+
session.append('tool/call', { turn: 1, step: 1, callId: CallId('vision-call-1'), name: 'bash', arguments: '{}' })
|
|
2167
|
+
session.append('tool/result', {
|
|
2168
|
+
turn: 1,
|
|
2169
|
+
step: 1,
|
|
2170
|
+
message: createToolResultMessage({
|
|
2171
|
+
callId: CallId('vision-call-1'),
|
|
2172
|
+
content: [{ type: 'text', text: 'vision fresh evidence '.repeat(1_000) }],
|
|
2173
|
+
isError: false,
|
|
2174
|
+
}),
|
|
2175
|
+
}, { surfaceOp: 'append' })
|
|
2176
|
+
session.append('step/end', { turn: 1, step: 1 })
|
|
2177
|
+
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
|
2178
|
+
// One older text result outside the fresh coordinates keeps History above
|
|
2179
|
+
// its trigger after Fresh has already shrunk the turn-1 result.
|
|
2180
|
+
appendToolTurn(session, 2, 'vision history evidence '.repeat(300), true, undefined, 'deepseek', VISION_MODEL)
|
|
2181
|
+
appendToolTurn(session, 3, 'recent protected result', true, undefined, 'deepseek', VISION_MODEL)
|
|
2182
|
+
session.append('turn/start', { turn: 4 })
|
|
2183
|
+
|
|
2184
|
+
const fresh = ctx.toolResultPruner.pruneSession(session, { stage: 'fresh', freshTurn: 1, freshStep: 1 })
|
|
2185
|
+
const pressure = ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
|
|
2186
|
+
|
|
2187
|
+
expect(fresh.pruned).toHaveLength(1)
|
|
2188
|
+
expect(pressure.pruned.length).toBeGreaterThanOrEqual(1)
|
|
2189
|
+
const visionRewrites = rewrites(audit.records())
|
|
2190
|
+
.filter(record => record.sessionId === String(session.id))
|
|
2191
|
+
expect(visionRewrites.length).toBeGreaterThanOrEqual(2)
|
|
2192
|
+
for (const record of visionRewrites) {
|
|
2193
|
+
expect(record.tokenizerId).toBe('deepseek-ai/DeepSeek-V4-Flash-Vision-Exp')
|
|
2194
|
+
expect(record.tokenizerRevision).toBe('6821d6ad3681a4b137b066b76094fa82ebd0a380')
|
|
2195
|
+
}
|
|
2196
|
+
// Fresh is disabled by its trigger; Aggregate is the pre-compression stage
|
|
2197
|
+
// that lands on the vision route for the oversized new result.
|
|
2198
|
+
expect(visionRewrites.some(record => record.component === 'fresh')).toBe(false)
|
|
2199
|
+
expect(visionRewrites.some(record => record.component === 'aggregate')).toBe(true)
|
|
2200
|
+
expect(visionRewrites.some(record => record.component === 'history')).toBe(true)
|
|
2201
|
+
})
|
|
2202
|
+
|
|
2203
|
+
it('keeps an image-bearing tool result fail-open in a vision session', async () => {
|
|
2204
|
+
const ctx = await runtimeContext()
|
|
2205
|
+
const audit = captureAudit(ctx)
|
|
2206
|
+
await ctx.plugin(ToolResultPruner, {
|
|
2207
|
+
profile: 'balanced',
|
|
2208
|
+
freshTriggerTokens: 10,
|
|
2209
|
+
freshTargetTokens: 8,
|
|
2210
|
+
aggregateTriggerTokens: 10,
|
|
2211
|
+
aggregateTargetTokens: 8,
|
|
2212
|
+
historyTriggerTokens: 10,
|
|
2213
|
+
historyKeepRecentToolCalls: 0,
|
|
2214
|
+
historyKeepRecentTokens: 1,
|
|
2215
|
+
historyMinReclaimTokens: 1,
|
|
2216
|
+
}).await()
|
|
2217
|
+
const session = Session.create(SessionId('public-vision-image-tool-result'))
|
|
2218
|
+
session.append('turn/start', { turn: 1 })
|
|
2219
|
+
session.append('request/header', {
|
|
2220
|
+
reason: 'initial',
|
|
2221
|
+
header: canonicalHeader({ config: { provider: 'deepseek', model: VISION_MODEL } }),
|
|
2222
|
+
})
|
|
2223
|
+
session.append('step/start', { turn: 1, step: 1 })
|
|
2224
|
+
session.append('assistant/message', {
|
|
2225
|
+
turn: 1,
|
|
2226
|
+
step: 1,
|
|
2227
|
+
message: createMessage({
|
|
2228
|
+
role: 'assistant',
|
|
2229
|
+
content: [{ type: 'tool-call', id: CallId('vision-image-call'), name: 'screenshot', arguments: '{}' }],
|
|
2230
|
+
source: { kind: 'model', provider: 'deepseek', model: VISION_MODEL },
|
|
2231
|
+
}),
|
|
2232
|
+
}, { surfaceOp: 'append' })
|
|
2233
|
+
session.append('tool/call', { turn: 1, step: 1, callId: CallId('vision-image-call'), name: 'screenshot', arguments: '{}' })
|
|
2234
|
+
const imageResult = session.append('tool/result', {
|
|
2235
|
+
turn: 1,
|
|
2236
|
+
step: 1,
|
|
2237
|
+
message: createToolResultMessage({
|
|
2238
|
+
callId: CallId('vision-image-call'),
|
|
2239
|
+
content: [
|
|
2240
|
+
{ type: 'text', text: 'screenshot captured' },
|
|
2241
|
+
imageBlock(800, 600),
|
|
2242
|
+
],
|
|
2243
|
+
isError: false,
|
|
2244
|
+
}),
|
|
2245
|
+
}, { surfaceOp: 'append' })
|
|
2246
|
+
session.append('step/end', { turn: 1, step: 1 })
|
|
2247
|
+
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
|
2248
|
+
session.append('turn/start', { turn: 2 })
|
|
2249
|
+
|
|
2250
|
+
const fresh = ctx.toolResultPruner.pruneSession(session, { stage: 'fresh', freshTurn: 1, freshStep: 1 })
|
|
2251
|
+
const pressure = ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
|
|
2252
|
+
|
|
2253
|
+
expect(fresh.pruned).toHaveLength(0)
|
|
2254
|
+
expect(pressure.pruned).toHaveLength(0)
|
|
2255
|
+
// The original image-bearing result stays on the surface untouched.
|
|
2256
|
+
const original = session.events[imageResult.seq]
|
|
2257
|
+
expect(original?.type).toBe('tool/result')
|
|
2258
|
+
expect(audit.records()).toContainEqual(expect.objectContaining({
|
|
2259
|
+
kind: 'component-evaluation',
|
|
2260
|
+
sessionId: String(session.id),
|
|
2261
|
+
component: 'fresh',
|
|
2262
|
+
status: 'skipped',
|
|
2263
|
+
reason: 'exact-tokenizer-unavailable',
|
|
2264
|
+
}))
|
|
2265
|
+
expect(audit.records()).toContainEqual(expect.objectContaining({
|
|
2266
|
+
kind: 'component-evaluation',
|
|
2267
|
+
sessionId: String(session.id),
|
|
2268
|
+
component: 'history',
|
|
2269
|
+
status: 'skipped',
|
|
2270
|
+
reason: 'exact-tokenizer-unavailable',
|
|
2271
|
+
}))
|
|
2272
|
+
})
|
|
2273
|
+
|
|
2274
|
+
it('counts image nodes as estimates while retaining exact text-only siblings', async () => {
|
|
2275
|
+
const ctx = await runtimeContext()
|
|
2276
|
+
const session = Session.create(SessionId('public-vision-image-surface'))
|
|
2277
|
+
session.append('turn/start', { turn: 1 })
|
|
2278
|
+
session.append('request/header', {
|
|
2279
|
+
reason: 'initial',
|
|
2280
|
+
header: canonicalHeader({ config: { provider: 'deepseek', model: VISION_MODEL } }),
|
|
2281
|
+
})
|
|
2282
|
+
const userMessage = session.append('user/message', createUserMessage({
|
|
2283
|
+
content: [
|
|
2284
|
+
{ type: 'text', text: 'look at this' },
|
|
2285
|
+
imageBlock(640, 480),
|
|
2286
|
+
{ type: 'text', text: 'and describe it' },
|
|
2287
|
+
],
|
|
2288
|
+
source: { kind: 'user' },
|
|
2289
|
+
}), { surfaceOp: 'append' })
|
|
2290
|
+
const textOnly = session.append('user/message', createUserMessage({
|
|
2291
|
+
content: [{ type: 'text', text: 'still exactly measurable' }],
|
|
2292
|
+
source: { kind: 'user' },
|
|
2293
|
+
}), { surfaceOp: 'append' })
|
|
2294
|
+
|
|
2295
|
+
const view = measureForCompaction(ctx, session)
|
|
2296
|
+
|
|
2297
|
+
// The exact request expansion is not publicly verifiable, so a mixed
|
|
2298
|
+
// text/image node carries a bounded estimate and cannot authorize an exact
|
|
2299
|
+
// rewrite proof.
|
|
2300
|
+
const node = view.measuredNodes.find(entry => entry.seq === userMessage.seq)
|
|
2301
|
+
expect(node?.count).toMatchObject({
|
|
2302
|
+
kind: 'tokenizer-estimate',
|
|
2303
|
+
estimatorId: DEEPSEEK_VISION_IMAGE_ESTIMATOR.id,
|
|
2304
|
+
estimatorRevision: DEEPSEEK_VISION_IMAGE_ESTIMATOR.revision,
|
|
2305
|
+
})
|
|
2306
|
+
if (node?.count.kind !== 'tokenizer-estimate') throw new Error('image node must carry an estimate')
|
|
2307
|
+
expect(node.count.upperBoundTokens).toBeGreaterThanOrEqual(node.count.tokens)
|
|
2308
|
+
expect(view.currentSurface.kind).toBe('tokenizer-estimate')
|
|
2309
|
+
// Text-only siblings keep their exact counts.
|
|
2310
|
+
expect(view.measuredNodes.find(entry => entry.seq === textOnly.seq)?.count).toMatchObject({
|
|
2311
|
+
kind: 'exact-tokenizer',
|
|
2312
|
+
tokenizerId: 'deepseek-ai/DeepSeek-V4-Flash-Vision-Exp',
|
|
2313
|
+
})
|
|
2314
|
+
// The official arithmetic is attached as an INTRINSIC diagnostic (the
|
|
2315
|
+
// padding extremes on intrinsic dimensions), explicitly not a request
|
|
2316
|
+
// bound: the adapter may still re-project the image.
|
|
2317
|
+
const grid = deepSeekVisionImageGrid(640, 480)
|
|
2318
|
+
expect(node.intrinsicImageBlockEstimate).toMatchObject({
|
|
2319
|
+
paddingMinimumTokens: deepSeekVisionImageBlockTokens(grid.nLlmH, grid.nLlmW, 3),
|
|
2320
|
+
paddingMaximumTokens: deepSeekVisionImageBlockTokens(grid.nLlmH, grid.nLlmW, 0),
|
|
2321
|
+
})
|
|
2322
|
+
expect(view.intrinsicImageBlockEstimateTokens).toBe(node.intrinsicImageBlockEstimate?.paddingMinimumTokens)
|
|
2323
|
+
})
|
|
2324
|
+
|
|
2325
|
+
it('estimates over-budget image metadata instead of making the surface unavailable', async () => {
|
|
2326
|
+
const ctx = await runtimeContext()
|
|
2327
|
+
const session = Session.create(SessionId('public-vision-image-over-budget'))
|
|
2328
|
+
session.append('turn/start', { turn: 1 })
|
|
2329
|
+
session.append('request/header', {
|
|
2330
|
+
reason: 'initial',
|
|
2331
|
+
header: canonicalHeader({ config: { provider: 'deepseek', model: VISION_MODEL } }),
|
|
2332
|
+
})
|
|
2333
|
+
const userMessage = session.append('user/message', createUserMessage({
|
|
2334
|
+
content: [imageBlock(4096, 4096)],
|
|
2335
|
+
source: { kind: 'user' },
|
|
2336
|
+
}), { surfaceOp: 'append' })
|
|
2337
|
+
|
|
2338
|
+
const view = measureForCompaction(ctx, session)
|
|
2339
|
+
|
|
2340
|
+
const node = view.measuredNodes.find(entry => entry.seq === userMessage.seq)
|
|
2341
|
+
expect(node?.count).toMatchObject({
|
|
2342
|
+
kind: 'tokenizer-estimate',
|
|
2343
|
+
upperBoundTokens: DEEPSEEK_VISION_PROJECTION.visionMaxNTokens,
|
|
2344
|
+
})
|
|
2345
|
+
expect(view.currentSurface.kind).toBe('tokenizer-estimate')
|
|
2346
|
+
})
|
|
2347
|
+
|
|
2348
|
+
it('keeps empty content nodes exact instead of fail-opening the surface', async () => {
|
|
2349
|
+
const ctx = await runtimeContext()
|
|
2350
|
+
const session = Session.create(SessionId('public-empty-content-node'))
|
|
2351
|
+
session.append('turn/start', { turn: 1 })
|
|
2352
|
+
session.append('request/header', {
|
|
2353
|
+
reason: 'initial',
|
|
2354
|
+
header: canonicalHeader({ config: { provider: 'deepseek', model: MODEL } }),
|
|
2355
|
+
})
|
|
2356
|
+
const empty = session.append('user/message', createUserMessage({
|
|
2357
|
+
content: [],
|
|
2358
|
+
source: { kind: 'user' },
|
|
2359
|
+
}), { surfaceOp: 'append' })
|
|
2360
|
+
const present = session.append('user/message', createUserMessage({
|
|
2361
|
+
content: [{ type: 'text', text: 'still measurable' }],
|
|
2362
|
+
source: { kind: 'user' },
|
|
2363
|
+
}), { surfaceOp: 'append' })
|
|
2364
|
+
|
|
2365
|
+
const view = measureForCompaction(ctx, session)
|
|
2366
|
+
|
|
2367
|
+
expect(view.measuredNodes.find(node => node.seq === empty.seq)?.count).toMatchObject({
|
|
2368
|
+
kind: 'exact-tokenizer',
|
|
2369
|
+
tokens: 0,
|
|
2370
|
+
})
|
|
2371
|
+
expect(view.measuredNodes.find(node => node.seq === present.seq)?.count).toMatchObject({
|
|
2372
|
+
kind: 'exact-tokenizer',
|
|
2373
|
+
})
|
|
2374
|
+
expect(view.currentSurface.kind).toBe('exact-tokenizer')
|
|
2375
|
+
})
|
|
2376
|
+
|
|
2377
|
+
it('uses the fixed image-token fallback for malformed metadata', async () => {
|
|
2378
|
+
const ctx = await runtimeContext()
|
|
2379
|
+
const session = Session.create(SessionId('public-vision-image-malformed-dims'))
|
|
2380
|
+
session.append('turn/start', { turn: 1 })
|
|
2381
|
+
session.append('request/header', {
|
|
2382
|
+
reason: 'initial',
|
|
2383
|
+
header: canonicalHeader({ config: { provider: 'deepseek', model: VISION_MODEL } }),
|
|
2384
|
+
})
|
|
2385
|
+
const userMessage = session.append('user/message', createUserMessage({
|
|
2386
|
+
content: [imageBlock(640.5, 480)],
|
|
2387
|
+
source: { kind: 'user' },
|
|
2388
|
+
}), { surfaceOp: 'append' })
|
|
2389
|
+
const zeroSize = session.append('user/message', createUserMessage({
|
|
2390
|
+
content: [imageBlock(0, 480)],
|
|
2391
|
+
source: { kind: 'user' },
|
|
2392
|
+
}), { surfaceOp: 'append' })
|
|
2393
|
+
|
|
2394
|
+
const view = measureForCompaction(ctx, session)
|
|
2395
|
+
|
|
2396
|
+
expect(view.measuredNodes.find(node => node.seq === userMessage.seq)?.count).toEqual({
|
|
2397
|
+
kind: 'tokenizer-estimate',
|
|
2398
|
+
tokens: DEEPSEEK_VISION_DEFAULT_IMAGE_TOKENS,
|
|
2399
|
+
upperBoundTokens: DEEPSEEK_VISION_PROJECTION.visionMaxNTokens,
|
|
2400
|
+
estimatorId: DEEPSEEK_VISION_IMAGE_ESTIMATOR.id,
|
|
2401
|
+
estimatorRevision: DEEPSEEK_VISION_IMAGE_ESTIMATOR.revision,
|
|
2402
|
+
})
|
|
2403
|
+
expect(view.measuredNodes.find(node => node.seq === zeroSize.seq)?.count).toEqual({
|
|
2404
|
+
kind: 'tokenizer-estimate',
|
|
2405
|
+
tokens: DEEPSEEK_VISION_DEFAULT_IMAGE_TOKENS,
|
|
2406
|
+
upperBoundTokens: DEEPSEEK_VISION_PROJECTION.visionMaxNTokens,
|
|
2407
|
+
estimatorId: DEEPSEEK_VISION_IMAGE_ESTIMATOR.id,
|
|
2408
|
+
estimatorRevision: DEEPSEEK_VISION_IMAGE_ESTIMATOR.revision,
|
|
2409
|
+
})
|
|
2410
|
+
expect(view.currentSurface).toMatchObject({
|
|
2411
|
+
kind: 'tokenizer-estimate',
|
|
2412
|
+
tokens: DEEPSEEK_VISION_DEFAULT_IMAGE_TOKENS * 2,
|
|
2413
|
+
upperBoundTokens: DEEPSEEK_VISION_PROJECTION.visionMaxNTokens * 2,
|
|
2414
|
+
})
|
|
2415
|
+
})
|
|
2416
|
+
|
|
2417
|
+
it('keeps image content unavailable for non-vision models', async () => {
|
|
2418
|
+
const ctx = await runtimeContext()
|
|
2419
|
+
const session = Session.create(SessionId('public-text-model-image-node'))
|
|
2420
|
+
session.append('turn/start', { turn: 1 })
|
|
2421
|
+
session.append('request/header', {
|
|
2422
|
+
reason: 'initial',
|
|
2423
|
+
header: canonicalHeader({ config: { provider: 'deepseek', model: MODEL } }),
|
|
2424
|
+
})
|
|
2425
|
+
const userMessage = session.append('user/message', createUserMessage({
|
|
2426
|
+
content: [imageBlock(640, 480)],
|
|
2427
|
+
source: { kind: 'user' },
|
|
2428
|
+
}), { surfaceOp: 'append' })
|
|
2429
|
+
|
|
2430
|
+
const view = measureForCompaction(ctx, session)
|
|
2431
|
+
|
|
2432
|
+
expect(view.measuredNodes.find(entry => entry.seq === userMessage.seq)?.count).toMatchObject({
|
|
2433
|
+
kind: 'unavailable',
|
|
2434
|
+
})
|
|
2435
|
+
})
|
|
2436
|
+
|
|
2437
|
+
it('keeps the vision image estimator unavailable behind a foreign provider', async () => {
|
|
2438
|
+
const ctx = await runtimeContext()
|
|
2439
|
+
const session = Session.create(SessionId('public-foreign-provider-vision-image-node'))
|
|
2440
|
+
session.append('turn/start', { turn: 1 })
|
|
2441
|
+
session.append('request/header', {
|
|
2442
|
+
reason: 'initial',
|
|
2443
|
+
header: canonicalHeader({ config: { provider: 'openai', model: VISION_MODEL } }),
|
|
2444
|
+
})
|
|
2445
|
+
const userMessage = session.append('user/message', createUserMessage({
|
|
2446
|
+
content: [imageBlock(640, 480)],
|
|
2447
|
+
source: { kind: 'user' },
|
|
2448
|
+
}), { surfaceOp: 'append' })
|
|
2449
|
+
|
|
2450
|
+
const view = measureForCompaction(ctx, session)
|
|
2451
|
+
|
|
2452
|
+
expect(view.measuredNodes.find(entry => entry.seq === userMessage.seq)?.count).toMatchObject({
|
|
2453
|
+
kind: 'unavailable',
|
|
2454
|
+
})
|
|
2455
|
+
expect(view.currentSurface).toMatchObject({ kind: 'unavailable' })
|
|
2456
|
+
})
|
|
2457
|
+
|
|
2458
|
+
it('never TailTrims a tool group whose results carry images', async () => {
|
|
2459
|
+
const ctx = await runtimeContext()
|
|
2460
|
+
await ctx.plugin(TestSettings).await()
|
|
2461
|
+
await ctx.plugin(SelectorHost).await()
|
|
2462
|
+
const audit = captureAudit(ctx)
|
|
2463
|
+
await ctx.settings.update(settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
|
|
2464
|
+
profile: 'custom',
|
|
2465
|
+
custom: {
|
|
2466
|
+
version: 3,
|
|
2467
|
+
unit: 'tokens',
|
|
2468
|
+
fresh: { enabled: true, trigger: 1_000_000, target: 900_000 },
|
|
2469
|
+
aggregate: { enabled: true, trigger: 1_000_000, target: 900_000 },
|
|
2470
|
+
history: { enabled: false, trigger: 900_000, keepRecentToolCalls: 0, keepRecentTokens: 0, minReclaim: 1 },
|
|
2471
|
+
prefixPolicy: 'preserve',
|
|
2472
|
+
tailTrim: { enabled: true, trigger: 10 },
|
|
2473
|
+
},
|
|
2474
|
+
})
|
|
2475
|
+
await ctx.plugin(ToolResultPruner, { profile: 'custom' }).await()
|
|
2476
|
+
const session = Session.create(SessionId('public-vision-tailtrim-image-group'))
|
|
2477
|
+
session.append('turn/start', { turn: 1 })
|
|
2478
|
+
session.append('request/header', {
|
|
2479
|
+
reason: 'initial',
|
|
2480
|
+
header: canonicalHeader({ config: { provider: 'deepseek', model: VISION_MODEL } }),
|
|
2481
|
+
})
|
|
2482
|
+
session.append('step/start', { turn: 1, step: 1 })
|
|
2483
|
+
session.append('assistant/message', {
|
|
2484
|
+
turn: 1,
|
|
2485
|
+
step: 1,
|
|
2486
|
+
message: createMessage({
|
|
2487
|
+
role: 'assistant',
|
|
2488
|
+
content: [{ type: 'tool-call', id: CallId('vision-tailtrim-call'), name: 'screenshot', arguments: '{}' }],
|
|
2489
|
+
source: { kind: 'model', provider: 'deepseek', model: VISION_MODEL },
|
|
2490
|
+
}),
|
|
2491
|
+
}, { surfaceOp: 'append' })
|
|
2492
|
+
session.append('tool/call', { turn: 1, step: 1, callId: CallId('vision-tailtrim-call'), name: 'screenshot', arguments: '{}' })
|
|
2493
|
+
session.append('tool/result', {
|
|
2494
|
+
turn: 1,
|
|
2495
|
+
step: 1,
|
|
2496
|
+
message: createToolResultMessage({
|
|
2497
|
+
callId: CallId('vision-tailtrim-call'),
|
|
2498
|
+
content: [
|
|
2499
|
+
{ type: 'text', text: 'screenshot captured for tail trim' },
|
|
2500
|
+
imageBlock(320, 240),
|
|
2501
|
+
],
|
|
2502
|
+
isError: false,
|
|
2503
|
+
}),
|
|
2504
|
+
}, { surfaceOp: 'append' })
|
|
2505
|
+
session.append('step/end', { turn: 1, step: 1 })
|
|
2506
|
+
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
|
2507
|
+
session.append('turn/start', { turn: 2 })
|
|
2508
|
+
|
|
2509
|
+
const result = ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
|
|
2510
|
+
|
|
2511
|
+
expect(result.pruned).toHaveLength(0)
|
|
2512
|
+
expect(rewrites(audit.records()).some(record =>
|
|
2513
|
+
record.sessionId === String(session.id) && record.component === 'tail-trim')).toBe(false)
|
|
2514
|
+
expect(session.events.some(event =>
|
|
2515
|
+
event.type === 'tool/result'
|
|
2516
|
+
&& event.data.message.content.some(block => block.type === 'tool-result'
|
|
2517
|
+
&& block.content.some(inner => inner.type === 'image')))).toBe(true)
|
|
2518
|
+
// Either fail-open gate is acceptable: the protected-set scan refuses the
|
|
2519
|
+
// image-bearing candidate, and the group scan independently refuses it.
|
|
2520
|
+
const tailTrimSkips = audit.records().filter((record): record is Extract<CompressionAuditRecord, { kind: 'component-evaluation' }> =>
|
|
2521
|
+
record.kind === 'component-evaluation'
|
|
2522
|
+
&& record.sessionId === String(session.id)
|
|
2523
|
+
&& record.component === 'tail-trim'
|
|
2524
|
+
&& record.status === 'skipped')
|
|
2525
|
+
expect(tailTrimSkips.length).toBeGreaterThan(0)
|
|
2526
|
+
expect(tailTrimSkips.every(record =>
|
|
2527
|
+
record.reason === 'no-safe-eligible-tool-group'
|
|
2528
|
+
|| record.reason === 'exact-tokenizer-unavailable-in-protected-set'
|
|
2529
|
+
|| record.reason === 'exact-tokenizer-unavailable')).toBe(true)
|
|
2530
|
+
})
|
|
2531
|
+
})
|