@animalabs/context-manager 0.5.2 → 0.5.4
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/dist/src/adaptive/index.d.ts +6 -0
- package/dist/src/adaptive/index.d.ts.map +1 -1
- package/dist/src/adaptive/index.js +8 -0
- package/dist/src/adaptive/index.js.map +1 -1
- package/dist/src/adaptive/kv-cache-sim.d.ts +128 -0
- package/dist/src/adaptive/kv-cache-sim.d.ts.map +1 -0
- package/dist/src/adaptive/kv-cache-sim.js +0 -0
- package/dist/src/adaptive/kv-cache-sim.js.map +1 -0
- package/dist/src/adaptive/kv-control.d.ts +152 -0
- package/dist/src/adaptive/kv-control.d.ts.map +1 -0
- package/dist/src/adaptive/kv-control.js +368 -0
- package/dist/src/adaptive/kv-control.js.map +1 -0
- package/dist/src/adaptive/kv-replay.d.ts +76 -0
- package/dist/src/adaptive/kv-replay.d.ts.map +1 -0
- package/dist/src/adaptive/kv-replay.js +175 -0
- package/dist/src/adaptive/kv-replay.js.map +1 -0
- package/dist/src/adaptive/render-offsets.d.ts +61 -0
- package/dist/src/adaptive/render-offsets.d.ts.map +1 -0
- package/dist/src/adaptive/render-offsets.js +88 -0
- package/dist/src/adaptive/render-offsets.js.map +1 -0
- package/dist/src/adaptive/strategies/kv-stable.d.ts +48 -0
- package/dist/src/adaptive/strategies/kv-stable.d.ts.map +1 -0
- package/dist/src/adaptive/strategies/kv-stable.js +102 -0
- package/dist/src/adaptive/strategies/kv-stable.js.map +1 -0
- package/dist/src/adaptive/summary-tree.d.ts +98 -0
- package/dist/src/adaptive/summary-tree.d.ts.map +1 -0
- package/dist/src/adaptive/summary-tree.js +214 -0
- package/dist/src/adaptive/summary-tree.js.map +1 -0
- package/dist/src/context-log.d.ts.map +1 -1
- package/dist/src/context-log.js +7 -2
- package/dist/src/context-log.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/message-store.js +1 -1
- package/dist/src/message-store.js.map +1 -1
- package/dist/src/normalize-tool-messages.d.ts +33 -0
- package/dist/src/normalize-tool-messages.d.ts.map +1 -1
- package/dist/src/normalize-tool-messages.js +59 -0
- package/dist/src/normalize-tool-messages.js.map +1 -1
- package/dist/src/strategies/autobiographical.d.ts +101 -39
- package/dist/src/strategies/autobiographical.d.ts.map +1 -1
- package/dist/src/strategies/autobiographical.js +398 -222
- package/dist/src/strategies/autobiographical.js.map +1 -1
- package/dist/src/types/strategy.d.ts +40 -1
- package/dist/src/types/strategy.d.ts.map +1 -1
- package/dist/src/types/strategy.js +2 -0
- package/dist/src/types/strategy.js.map +1 -1
- package/dist/test/adaptive/kv-cache-sim.test.d.ts +16 -0
- package/dist/test/adaptive/kv-cache-sim.test.d.ts.map +1 -0
- package/dist/test/adaptive/kv-cache-sim.test.js +173 -0
- package/dist/test/adaptive/kv-cache-sim.test.js.map +1 -0
- package/dist/test/adaptive/kv-control.test.d.ts +14 -0
- package/dist/test/adaptive/kv-control.test.d.ts.map +1 -0
- package/dist/test/adaptive/kv-control.test.js +130 -0
- package/dist/test/adaptive/kv-control.test.js.map +1 -0
- package/dist/test/adaptive/kv-replay.test.d.ts +15 -0
- package/dist/test/adaptive/kv-replay.test.d.ts.map +1 -0
- package/dist/test/adaptive/kv-replay.test.js +112 -0
- package/dist/test/adaptive/kv-replay.test.js.map +1 -0
- package/dist/test/adaptive/kv-stable-strategy.test.d.ts +10 -0
- package/dist/test/adaptive/kv-stable-strategy.test.d.ts.map +1 -0
- package/dist/test/adaptive/kv-stable-strategy.test.js +89 -0
- package/dist/test/adaptive/kv-stable-strategy.test.js.map +1 -0
- package/dist/test/adaptive/render-offsets.test.d.ts +11 -0
- package/dist/test/adaptive/render-offsets.test.d.ts.map +1 -0
- package/dist/test/adaptive/render-offsets.test.js +85 -0
- package/dist/test/adaptive/render-offsets.test.js.map +1 -0
- package/dist/test/adaptive/summary-tree.test.d.ts +5 -0
- package/dist/test/adaptive/summary-tree.test.d.ts.map +1 -0
- package/dist/test/adaptive/summary-tree.test.js +72 -0
- package/dist/test/adaptive/summary-tree.test.js.map +1 -0
- package/dist/test/chunker-tool-boundary.test.d.ts +12 -0
- package/dist/test/chunker-tool-boundary.test.d.ts.map +1 -0
- package/dist/test/chunker-tool-boundary.test.js +89 -0
- package/dist/test/chunker-tool-boundary.test.js.map +1 -0
- package/dist/test/compression-shape-invariants.test.d.ts +22 -0
- package/dist/test/compression-shape-invariants.test.d.ts.map +1 -0
- package/dist/test/compression-shape-invariants.test.js +312 -0
- package/dist/test/compression-shape-invariants.test.js.map +1 -0
- package/dist/test/image-strip-render-stats.test.d.ts +2 -0
- package/dist/test/image-strip-render-stats.test.d.ts.map +1 -0
- package/dist/test/image-strip-render-stats.test.js +130 -0
- package/dist/test/image-strip-render-stats.test.js.map +1 -0
- package/dist/test/image-stripping.test.d.ts +13 -0
- package/dist/test/image-stripping.test.d.ts.map +1 -0
- package/dist/test/image-stripping.test.js +104 -0
- package/dist/test/image-stripping.test.js.map +1 -0
- package/dist/test/normalize-tool-messages.test.js +61 -1
- package/dist/test/normalize-tool-messages.test.js.map +1 -1
- package/dist/test/render-stats.test.d.ts +2 -0
- package/dist/test/render-stats.test.d.ts.map +1 -0
- package/dist/test/render-stats.test.js +91 -0
- package/dist/test/render-stats.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/adaptive/index.ts +48 -0
- package/src/adaptive/kv-cache-sim.ts +0 -0
- package/src/adaptive/kv-control.ts +497 -0
- package/src/adaptive/kv-replay.ts +238 -0
- package/src/adaptive/render-offsets.ts +116 -0
- package/src/adaptive/strategies/kv-stable.ts +123 -0
- package/src/adaptive/summary-tree.ts +252 -0
- package/src/context-log.ts +7 -2
- package/src/index.ts +1 -1
- package/src/message-store.ts +1 -1
- package/src/normalize-tool-messages.ts +61 -0
- package/src/strategies/autobiographical.ts +414 -271
- package/src/types/strategy.ts +40 -1
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end shape invariants for the compression pipeline.
|
|
3
|
+
*
|
|
4
|
+
* The whole compression-bug family (5/6/7/8/9) was about the *shape* of the
|
|
5
|
+
* request sent to the Anthropic API, not the content of summaries. This
|
|
6
|
+
* suite exercises the full pipeline (message store → chunker → L1 compress
|
|
7
|
+
* → L2/L3 merges) against a mock membrane that:
|
|
8
|
+
*
|
|
9
|
+
* 1. Validates every request against the API's structural rules
|
|
10
|
+
* (tool_use/tool_result adjacency, placement in user vs. assistant
|
|
11
|
+
* turns) and throws if any rule is violated.
|
|
12
|
+
* 2. Returns a deterministic summary text at a realistic ~5:1 compression
|
|
13
|
+
* ratio so summary IDs and merge cascades behave predictably.
|
|
14
|
+
*
|
|
15
|
+
* If any of the patched call sites — `compressChunkHierarchical`,
|
|
16
|
+
* `executeMerge`, the chunker's tool-cycle-respecting close, or the
|
|
17
|
+
* runtime `splitMixedToolMessages` / `stripUnpairedToolBlocks` passes —
|
|
18
|
+
* regresses, the mock will throw with the same error shape the live API
|
|
19
|
+
* would return, and the test fails.
|
|
20
|
+
*/
|
|
21
|
+
import { describe, it, before, after } from 'node:test';
|
|
22
|
+
import assert from 'node:assert';
|
|
23
|
+
import { rmSync, existsSync } from 'node:fs';
|
|
24
|
+
import { ContextManager, AutobiographicalStrategy } from '../src/index.js';
|
|
25
|
+
const TEST_STORE_PATH = './test-compression-shape';
|
|
26
|
+
function cleanup() {
|
|
27
|
+
if (existsSync(TEST_STORE_PATH)) {
|
|
28
|
+
rmSync(TEST_STORE_PATH, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// API-shape validator: mirrors what the Anthropic API enforces.
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
function validateApiShape(messages) {
|
|
35
|
+
for (let i = 0; i < messages.length; i++) {
|
|
36
|
+
const msg = messages[i];
|
|
37
|
+
const isUser = msg.participant.toLowerCase() === 'user';
|
|
38
|
+
for (const block of msg.content) {
|
|
39
|
+
if (block.type === 'tool_result' && !isUser) {
|
|
40
|
+
throw new Error(`messages[${i}]: tool_result block in non-user turn (participant=${msg.participant})`);
|
|
41
|
+
}
|
|
42
|
+
if (block.type === 'tool_use') {
|
|
43
|
+
const id = block.id;
|
|
44
|
+
const next = messages[i + 1];
|
|
45
|
+
const matched = next?.content.some((b) => b.type === 'tool_result' &&
|
|
46
|
+
b.toolUseId === id);
|
|
47
|
+
if (!matched) {
|
|
48
|
+
throw new Error(`messages[${i}]: tool_use id=${id} was not followed by a matching tool_result in messages[${i + 1}]`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (block.type === 'tool_result') {
|
|
52
|
+
const tid = block.toolUseId;
|
|
53
|
+
const prev = messages[i - 1];
|
|
54
|
+
const matched = prev?.content.some((b) => b.type === 'tool_use' && b.id === tid);
|
|
55
|
+
if (!matched) {
|
|
56
|
+
throw new Error(`messages[${i}]: tool_result toolUseId=${tid} was not preceded by a matching tool_use in messages[${i - 1}]`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Mock membrane: validates shape, returns deterministic compressed text.
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
function createValidatingMembrane(compressionRatio = 5) {
|
|
66
|
+
const calls = [];
|
|
67
|
+
const membrane = {
|
|
68
|
+
complete: async (request) => {
|
|
69
|
+
validateApiShape(request.messages);
|
|
70
|
+
const inputChars = request.messages
|
|
71
|
+
.flatMap((m) => m.content)
|
|
72
|
+
.map((b) => b.text ?? '')
|
|
73
|
+
.join('').length;
|
|
74
|
+
const targetChars = Math.max(60, Math.floor(inputChars / compressionRatio));
|
|
75
|
+
const summary = `[mock summary inChars=${inputChars}] ` +
|
|
76
|
+
'x '.repeat(Math.max(0, Math.floor((targetChars - 30) / 2)));
|
|
77
|
+
calls.push({ messages: request.messages, outputChars: summary.length });
|
|
78
|
+
return {
|
|
79
|
+
content: [{ type: 'text', text: summary }],
|
|
80
|
+
usage: {
|
|
81
|
+
input_tokens: Math.ceil(inputChars / 4),
|
|
82
|
+
output_tokens: Math.ceil(summary.length / 4),
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
return { membrane, calls };
|
|
88
|
+
}
|
|
89
|
+
const t = (s) => ({ type: 'text', text: s });
|
|
90
|
+
const u = (id) => ({ type: 'tool_use', id, name: 'fn', input: {} });
|
|
91
|
+
const r = (id) => ({ type: 'tool_result', toolUseId: id, content: 'ok' });
|
|
92
|
+
async function drain(manager) {
|
|
93
|
+
// Cap iterations so a regression that fails to converge can't hang CI.
|
|
94
|
+
for (let i = 0; i < 500; i++) {
|
|
95
|
+
if (manager.isReady())
|
|
96
|
+
return;
|
|
97
|
+
await manager.tick();
|
|
98
|
+
}
|
|
99
|
+
throw new Error('drain: queue did not converge within 500 ticks');
|
|
100
|
+
}
|
|
101
|
+
describe('Compression pipeline: API shape invariants', () => {
|
|
102
|
+
before(() => cleanup());
|
|
103
|
+
after(() => cleanup());
|
|
104
|
+
it('L1 compression: chunker close lands on a tool_use without fix — must not throw', async () => {
|
|
105
|
+
cleanup();
|
|
106
|
+
const { membrane, calls } = createValidatingMembrane();
|
|
107
|
+
const strategy = new AutobiographicalStrategy({
|
|
108
|
+
targetChunkTokens: 80,
|
|
109
|
+
headWindowTokens: 0,
|
|
110
|
+
recentWindowTokens: 0,
|
|
111
|
+
hierarchical: true,
|
|
112
|
+
});
|
|
113
|
+
const manager = await ContextManager.open({
|
|
114
|
+
path: TEST_STORE_PATH,
|
|
115
|
+
strategy,
|
|
116
|
+
membrane: membrane,
|
|
117
|
+
});
|
|
118
|
+
// Force the chunker to want to close on a tool_use-bearing message:
|
|
119
|
+
// - 3 small messages (length 3, tokens ~30)
|
|
120
|
+
// - then a FAT agent message with a tool_use (length 4 → meets >=4,
|
|
121
|
+
// fat enough to push currentTokens past targetChunkTokens=80)
|
|
122
|
+
// - then the matching tool_result (would land in next chunk
|
|
123
|
+
// without the chunker-defer fix → orphan tool_use at boundary)
|
|
124
|
+
// Repeat this pattern many times so multiple boundaries are exercised.
|
|
125
|
+
const small = (n) => 'word '.repeat(n);
|
|
126
|
+
const fat = (n) => 'thinking '.repeat(n);
|
|
127
|
+
for (let i = 0; i < 12; i++) {
|
|
128
|
+
manager.addMessage('user', [t(small(8))]);
|
|
129
|
+
manager.addMessage('agent', [t(small(8))]);
|
|
130
|
+
manager.addMessage('user', [t(small(8))]);
|
|
131
|
+
manager.addMessage('agent', [t(fat(60)), u(`A${i}`)]);
|
|
132
|
+
manager.addMessage('user', [r(`A${i}`)]);
|
|
133
|
+
manager.addMessage('agent', [t(small(10))]);
|
|
134
|
+
}
|
|
135
|
+
await drain(manager);
|
|
136
|
+
assert.ok(calls.length > 0, 'expected at least one L1 compression call');
|
|
137
|
+
await manager.close();
|
|
138
|
+
});
|
|
139
|
+
it('L2/L3 merge cascade preserves shape across hundreds of summaries', async () => {
|
|
140
|
+
cleanup();
|
|
141
|
+
const { membrane, calls } = createValidatingMembrane();
|
|
142
|
+
const strategy = new AutobiographicalStrategy({
|
|
143
|
+
targetChunkTokens: 50,
|
|
144
|
+
headWindowTokens: 0,
|
|
145
|
+
recentWindowTokens: 0,
|
|
146
|
+
hierarchical: true,
|
|
147
|
+
mergeThreshold: 3, // L1→L2 every 3 L1s, L2→L3 every 9 L1s
|
|
148
|
+
});
|
|
149
|
+
const manager = await ContextManager.open({
|
|
150
|
+
path: TEST_STORE_PATH,
|
|
151
|
+
strategy,
|
|
152
|
+
membrane: membrane,
|
|
153
|
+
});
|
|
154
|
+
const filler = (n) => 'word '.repeat(n);
|
|
155
|
+
for (let i = 0; i < 80; i++) {
|
|
156
|
+
if (i % 5 === 0 && i > 0) {
|
|
157
|
+
manager.addMessage('agent', [t(filler(10)), u(`B${i}`)]);
|
|
158
|
+
manager.addMessage('user', [r(`B${i}`)]);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
manager.addMessage(i % 2 === 0 ? 'agent' : 'user', [t(filler(15))]);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
await drain(manager);
|
|
165
|
+
// We should have hit L1 + at least one L2 merge — easiest signal is the
|
|
166
|
+
// sheer call count plus the absence of validation throws.
|
|
167
|
+
const snap = strategy.getProgressSnapshot();
|
|
168
|
+
assert.ok(snap.summaryCounts.l1 > 0, 'expected L1 summaries');
|
|
169
|
+
assert.ok(snap.summaryCounts.l2 > 0, `expected at least one L2 merge (l1=${snap.summaryCounts.l1}, l2=${snap.summaryCounts.l2})`);
|
|
170
|
+
assert.ok(calls.length >= snap.summaryCounts.l1 + snap.summaryCounts.l2);
|
|
171
|
+
await manager.close();
|
|
172
|
+
});
|
|
173
|
+
it('Bug 10: raw middle dedup correctly handles L2+ summaries (no message leak)', async () => {
|
|
174
|
+
cleanup();
|
|
175
|
+
const { membrane, calls } = createValidatingMembrane();
|
|
176
|
+
const strategy = new AutobiographicalStrategy({
|
|
177
|
+
targetChunkTokens: 40,
|
|
178
|
+
headWindowTokens: 0,
|
|
179
|
+
recentWindowTokens: 0,
|
|
180
|
+
hierarchical: true,
|
|
181
|
+
mergeThreshold: 3,
|
|
182
|
+
});
|
|
183
|
+
const manager = await ContextManager.open({
|
|
184
|
+
path: TEST_STORE_PATH,
|
|
185
|
+
strategy,
|
|
186
|
+
membrane: membrane,
|
|
187
|
+
});
|
|
188
|
+
// 100 messages → enough chunks to drive a multi-level merge cascade
|
|
189
|
+
// (L1s → L2s → L3) so executeMerge runs with L2s in its prior
|
|
190
|
+
// frontier. Each message has a distinctive marker (`RAW-N`) so we
|
|
191
|
+
// can count how many leak into requests as raw content.
|
|
192
|
+
const filler = (n) => 'word '.repeat(n);
|
|
193
|
+
for (let i = 0; i < 100; i++) {
|
|
194
|
+
manager.addMessage(i % 2 === 0 ? 'agent' : 'user', [
|
|
195
|
+
t(`RAW-${i} ${filler(8)}`),
|
|
196
|
+
]);
|
|
197
|
+
}
|
|
198
|
+
await drain(manager);
|
|
199
|
+
const snap = strategy.getProgressSnapshot();
|
|
200
|
+
assert.ok(snap.summaryCounts.l2 >= 2, `expected >=2 L2 summaries to exercise the bug (got l2=${snap.summaryCounts.l2})`);
|
|
201
|
+
// Count raw conversation messages in each request. The expansion bug
|
|
202
|
+
// allowed ~all messages to leak in (the bug report saw 256 user +
|
|
203
|
+
// 262 agent for a 4234-msg conversation). With the fix, per-request
|
|
204
|
+
// raw-message count should be bounded by the merge target's leaf
|
|
205
|
+
// size — for mergeThreshold=3 with ~3-msg L1s, that's <= ~25.
|
|
206
|
+
const RAW_MSG_LIMIT = 40;
|
|
207
|
+
for (let i = 0; i < calls.length; i++) {
|
|
208
|
+
const call = calls[i];
|
|
209
|
+
let rawCount = 0;
|
|
210
|
+
for (const m of call.messages) {
|
|
211
|
+
for (const block of m.content) {
|
|
212
|
+
const text = block.text ?? '';
|
|
213
|
+
if (text.includes('RAW-'))
|
|
214
|
+
rawCount++;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
assert.ok(rawCount <= RAW_MSG_LIMIT, `call ${i}: ${rawCount} raw messages in request — raw middle is leaking already-summarized content (Bug 10)`);
|
|
218
|
+
}
|
|
219
|
+
await manager.close();
|
|
220
|
+
});
|
|
221
|
+
it('runtime splitMixedToolMessages handles bundled cycles in source data', async () => {
|
|
222
|
+
cleanup();
|
|
223
|
+
const { membrane, calls } = createValidatingMembrane();
|
|
224
|
+
const strategy = new AutobiographicalStrategy({
|
|
225
|
+
targetChunkTokens: 60,
|
|
226
|
+
headWindowTokens: 0,
|
|
227
|
+
recentWindowTokens: 0,
|
|
228
|
+
hierarchical: true,
|
|
229
|
+
});
|
|
230
|
+
const manager = await ContextManager.open({
|
|
231
|
+
path: TEST_STORE_PATH,
|
|
232
|
+
strategy,
|
|
233
|
+
membrane: membrane,
|
|
234
|
+
});
|
|
235
|
+
// Pre-Bug-8 import shape: tool_use AND tool_result bundled into one
|
|
236
|
+
// assistant message. The runtime split has to unpack these before the
|
|
237
|
+
// request reaches the membrane.
|
|
238
|
+
for (let i = 0; i < 20; i++) {
|
|
239
|
+
manager.addMessage('user', [t('q'.repeat(60))]);
|
|
240
|
+
manager.addMessage('agent', [
|
|
241
|
+
t('a'.repeat(40)),
|
|
242
|
+
u(`C${i}`),
|
|
243
|
+
r(`C${i}`),
|
|
244
|
+
t('done'),
|
|
245
|
+
]);
|
|
246
|
+
}
|
|
247
|
+
await drain(manager);
|
|
248
|
+
assert.ok(calls.length > 0);
|
|
249
|
+
await manager.close();
|
|
250
|
+
});
|
|
251
|
+
it('orphan tool_use at the very tail (no next message ever) does not crash', async () => {
|
|
252
|
+
cleanup();
|
|
253
|
+
const { membrane, calls } = createValidatingMembrane();
|
|
254
|
+
const strategy = new AutobiographicalStrategy({
|
|
255
|
+
targetChunkTokens: 50,
|
|
256
|
+
headWindowTokens: 0,
|
|
257
|
+
recentWindowTokens: 0,
|
|
258
|
+
hierarchical: true,
|
|
259
|
+
});
|
|
260
|
+
const manager = await ContextManager.open({
|
|
261
|
+
path: TEST_STORE_PATH,
|
|
262
|
+
strategy,
|
|
263
|
+
membrane: membrane,
|
|
264
|
+
});
|
|
265
|
+
// Build a sequence that ends on a tool_use with no following tool_result.
|
|
266
|
+
// The chunker's defer rule can't help here (there's no next message);
|
|
267
|
+
// stripUnpairedToolBlocks is the safety net.
|
|
268
|
+
const filler = (n) => 'word '.repeat(n);
|
|
269
|
+
for (let i = 0; i < 12; i++) {
|
|
270
|
+
manager.addMessage(i % 2 === 0 ? 'agent' : 'user', [t(filler(20))]);
|
|
271
|
+
}
|
|
272
|
+
manager.addMessage('agent', [t(filler(10)), u('orphan')]);
|
|
273
|
+
await drain(manager);
|
|
274
|
+
assert.ok(calls.length > 0);
|
|
275
|
+
await manager.close();
|
|
276
|
+
});
|
|
277
|
+
it('adaptive resolution variant uses the same compression sites — same shape guarantees', async () => {
|
|
278
|
+
cleanup();
|
|
279
|
+
const { membrane, calls } = createValidatingMembrane();
|
|
280
|
+
const strategy = new AutobiographicalStrategy({
|
|
281
|
+
targetChunkTokens: 50,
|
|
282
|
+
headWindowTokens: 0,
|
|
283
|
+
recentWindowTokens: 0,
|
|
284
|
+
hierarchical: true,
|
|
285
|
+
adaptiveResolution: true,
|
|
286
|
+
});
|
|
287
|
+
const manager = await ContextManager.open({
|
|
288
|
+
path: TEST_STORE_PATH,
|
|
289
|
+
strategy,
|
|
290
|
+
membrane: membrane,
|
|
291
|
+
});
|
|
292
|
+
const filler = (n) => 'word '.repeat(n);
|
|
293
|
+
for (let i = 0; i < 30; i++) {
|
|
294
|
+
if (i % 4 === 0 && i > 0) {
|
|
295
|
+
manager.addMessage('agent', [t(filler(10)), u(`D${i}`)]);
|
|
296
|
+
manager.addMessage('user', [r(`D${i}`)]);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
manager.addMessage(i % 2 === 0 ? 'agent' : 'user', [t(filler(20))]);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Adaptive path produces L1s lazily via the picker — trigger a compile
|
|
303
|
+
// (or two) so FoldOps run and enqueue compression work.
|
|
304
|
+
await manager.compile();
|
|
305
|
+
await drain(manager);
|
|
306
|
+
await manager.compile();
|
|
307
|
+
await drain(manager);
|
|
308
|
+
assert.ok(calls.length > 0, 'adaptive path produced no compression calls');
|
|
309
|
+
await manager.close();
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
//# sourceMappingURL=compression-shape-invariants.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compression-shape-invariants.test.js","sourceRoot":"","sources":["../../test/compression-shape-invariants.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-strip-render-stats.test.d.ts","sourceRoot":"","sources":["../../test/image-strip-render-stats.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression: image stripping must run BEFORE the render-stats snapshot is
|
|
3
|
+
* committed and BEFORE cache markers are placed, so that
|
|
4
|
+
*
|
|
5
|
+
* (a) RenderStats.total reflects the POST-strip context — the real bytes the
|
|
6
|
+
* agent receives — not the pre-strip size that still counts full image
|
|
7
|
+
* weight (≈1600 tok) for images replaced by a short text placeholder.
|
|
8
|
+
* Pre-fix, getRenderStats() over-reported by ~1592 tok per stripped image
|
|
9
|
+
* (and image stripping is on by default: maxLiveImages = 6).
|
|
10
|
+
*
|
|
11
|
+
* (b) cache markers and stripping coexist cleanly: marker positions are
|
|
12
|
+
* structural (content-independent, so deterministic across recompiles)
|
|
13
|
+
* and a stripped image never resurrects across turns — the live-image
|
|
14
|
+
* window is monotonic, so each image strips at most once (one bounded KV
|
|
15
|
+
* perturbation, not per-turn flicker).
|
|
16
|
+
*/
|
|
17
|
+
import { describe, it, before, after, beforeEach } from 'node:test';
|
|
18
|
+
import assert from 'node:assert';
|
|
19
|
+
import { rmSync, existsSync } from 'node:fs';
|
|
20
|
+
import { ContextManager, AutobiographicalStrategy } from '../src/index.js';
|
|
21
|
+
const TEST_STORE_PATH = './test-image-strip-render-stats';
|
|
22
|
+
const PLACEHOLDER = 'image dropped from live context';
|
|
23
|
+
function cleanup() {
|
|
24
|
+
if (existsSync(TEST_STORE_PATH))
|
|
25
|
+
rmSync(TEST_STORE_PATH, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
/** A message carrying one image plus a text label so we can locate it later. */
|
|
28
|
+
function imageMessage(label, tokenEstimate = 1600) {
|
|
29
|
+
return [
|
|
30
|
+
{ type: 'image', source: { type: 'base64', data: 'AAAA', mediaType: 'image/png' }, tokenEstimate },
|
|
31
|
+
{ type: 'text', text: label },
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
/** Re-derive rendered tokens from the COMPILED output using the strategy's
|
|
35
|
+
* default estimator (image -> tokenEstimate ?? 1600, text -> ceil(len/4)).
|
|
36
|
+
* The fixtures use only image + text blocks. */
|
|
37
|
+
function renderedTokens(messages) {
|
|
38
|
+
let t = 0;
|
|
39
|
+
for (const m of messages) {
|
|
40
|
+
for (const b of m.content) {
|
|
41
|
+
if (b.type === 'image')
|
|
42
|
+
t += b.tokenEstimate ?? 1600;
|
|
43
|
+
else if (b.type === 'text')
|
|
44
|
+
t += Math.ceil(b.text.length / 4);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return t;
|
|
48
|
+
}
|
|
49
|
+
function hasLabel(content, label) {
|
|
50
|
+
return content.some((b) => b.type === 'text' && b.text.includes(label));
|
|
51
|
+
}
|
|
52
|
+
describe('image stripping × render stats', () => {
|
|
53
|
+
before(cleanup);
|
|
54
|
+
after(cleanup);
|
|
55
|
+
beforeEach(cleanup);
|
|
56
|
+
it('(a) total reflects the post-strip size, not the pre-strip image weight', async () => {
|
|
57
|
+
// Default live-image policy is ON (maxLiveImages: 6). 10 images => 4 stripped.
|
|
58
|
+
const manager = await ContextManager.open({
|
|
59
|
+
path: TEST_STORE_PATH,
|
|
60
|
+
strategy: new AutobiographicalStrategy({
|
|
61
|
+
adaptiveResolution: true,
|
|
62
|
+
headWindowTokens: 0,
|
|
63
|
+
recentWindowTokens: 1_000_000, // keep everything verbatim; isolate the count cap
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
for (let i = 1; i <= 10; i++)
|
|
67
|
+
manager.addMessage('user', imageMessage(`img-${String(i).padStart(2, '0')}`));
|
|
68
|
+
const compiled = await manager.compile({ maxTokens: 1_000_000, reserveForResponse: 0 });
|
|
69
|
+
const live = compiled.messages.filter((m) => m.content.some((b) => b.type === 'image')).length;
|
|
70
|
+
const stripped = compiled.messages.filter((m) => hasLabel(m.content, PLACEHOLDER)).length;
|
|
71
|
+
assert.equal(live, 6, 'default maxLiveImages keeps 6 images live');
|
|
72
|
+
assert.equal(stripped, 4, 'the other 4 are stripped to placeholders');
|
|
73
|
+
const rs = manager.getRenderStats();
|
|
74
|
+
assert.ok(rs, 'stats present after compile');
|
|
75
|
+
// Decisive assertion: total == the ACTUAL rendered size. Pre-fix this was
|
|
76
|
+
// inflated by the 4 stripped images (~4 * 1592 tok).
|
|
77
|
+
const expected = renderedTokens(compiled.messages);
|
|
78
|
+
assert.equal(rs.total.tokens, expected, 'total.tokens equals the real rendered size');
|
|
79
|
+
// Internal invariant must still hold (total == sum of buckets).
|
|
80
|
+
const s = rs.summaries;
|
|
81
|
+
const bucketSum = rs.head.tokens + rs.tail.tokens + rs.middleRaw.tokens +
|
|
82
|
+
s.l1.tokens + s.l2.tokens + s.l3.tokens;
|
|
83
|
+
assert.equal(rs.total.tokens, bucketSum, 'total == sum of buckets');
|
|
84
|
+
for (const k of ['head', 'tail', 'middleRaw']) {
|
|
85
|
+
assert.ok(rs[k].tokens >= 0, `${k} tokens stay non-negative after the strip credit`);
|
|
86
|
+
}
|
|
87
|
+
// And total must be strictly below the pre-strip size — proves the credit bites.
|
|
88
|
+
const preStrip = expected + stripped * (1600 - Math.ceil('[image dropped from live context]'.length / 4));
|
|
89
|
+
assert.ok(rs.total.tokens < preStrip, 'total is the stripped size, not the pre-strip size');
|
|
90
|
+
manager.close();
|
|
91
|
+
});
|
|
92
|
+
it('(b) cache markers coexist with stripping; stripped images never resurrect', async () => {
|
|
93
|
+
const manager = await ContextManager.open({
|
|
94
|
+
path: TEST_STORE_PATH,
|
|
95
|
+
strategy: new AutobiographicalStrategy({
|
|
96
|
+
adaptiveResolution: true,
|
|
97
|
+
headWindowTokens: 0,
|
|
98
|
+
recentWindowTokens: 1_000_000,
|
|
99
|
+
maxLiveImages: 3,
|
|
100
|
+
imageStripDepthTokens: 0, // isolate the count cap
|
|
101
|
+
}),
|
|
102
|
+
});
|
|
103
|
+
for (let i = 1; i <= 6; i++)
|
|
104
|
+
manager.addMessage('user', imageMessage(`img-${String(i).padStart(2, '0')}`));
|
|
105
|
+
const c1 = await manager.compile({ maxTokens: 1_000_000, reserveForResponse: 0 });
|
|
106
|
+
const markers1 = c1.messages.flatMap((m, i) => (m.cacheBreakpoint ? [i] : []));
|
|
107
|
+
const stripped1 = c1.messages.filter((m) => hasLabel(m.content, PLACEHOLDER)).length;
|
|
108
|
+
assert.ok(markers1.length > 0, 'cache markers are placed');
|
|
109
|
+
assert.equal(stripped1, 3, 'oldest 3 of 6 images stripped under maxLiveImages:3');
|
|
110
|
+
// Re-compile the SAME store: marker placement is structural (head/tail
|
|
111
|
+
// membership + index), so positions are identical even though stripping
|
|
112
|
+
// mutated content. Guards against a future token-sensitive marker pass
|
|
113
|
+
// being fed pre-strip sizes.
|
|
114
|
+
const c1b = await manager.compile({ maxTokens: 1_000_000, reserveForResponse: 0 });
|
|
115
|
+
const markers1b = c1b.messages.flatMap((m, i) => (m.cacheBreakpoint ? [i] : []));
|
|
116
|
+
assert.deepEqual(markers1b, markers1, 'marker positions are deterministic across recompiles');
|
|
117
|
+
// Cross-turn: append a new image so the live window slides forward. Images
|
|
118
|
+
// that were already stripped must STAY stripped — monotonic, no flicker
|
|
119
|
+
// that would churn the cached prefix turn-to-turn.
|
|
120
|
+
manager.addMessage('user', imageMessage('img-07'));
|
|
121
|
+
const c2 = await manager.compile({ maxTokens: 1_000_000, reserveForResponse: 0 });
|
|
122
|
+
for (const label of ['img-01', 'img-02', 'img-03']) {
|
|
123
|
+
const e = c2.messages.find((m) => hasLabel(m.content, label));
|
|
124
|
+
assert.ok(e, `${label} still present`);
|
|
125
|
+
assert.ok(!e.content.some((b) => b.type === 'image'), `${label} stays stripped after a new turn`);
|
|
126
|
+
}
|
|
127
|
+
manager.close();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
//# sourceMappingURL=image-strip-render-stats.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-strip-render-stats.test.js","sourceRoot":"","sources":["../../test/image-strip-render-stats.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the live-image policy in AutobiographicalStrategy:
|
|
3
|
+
*
|
|
4
|
+
* (1) maxLiveImages — a hard count ceiling. Only the N most-recent images
|
|
5
|
+
* survive as real image blocks; older ones become text placeholders.
|
|
6
|
+
* (2) imageStripDepthTokens — a depth cutoff. Images deeper than D tokens
|
|
7
|
+
* from the newest message are stripped even though the surrounding text
|
|
8
|
+
* stays verbatim (so text horizon >> image horizon).
|
|
9
|
+
*
|
|
10
|
+
* Stripped images are replaced by "[image dropped from live context]".
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=image-stripping.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-stripping.test.d.ts","sourceRoot":"","sources":["../../test/image-stripping.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the live-image policy in AutobiographicalStrategy:
|
|
3
|
+
*
|
|
4
|
+
* (1) maxLiveImages — a hard count ceiling. Only the N most-recent images
|
|
5
|
+
* survive as real image blocks; older ones become text placeholders.
|
|
6
|
+
* (2) imageStripDepthTokens — a depth cutoff. Images deeper than D tokens
|
|
7
|
+
* from the newest message are stripped even though the surrounding text
|
|
8
|
+
* stays verbatim (so text horizon >> image horizon).
|
|
9
|
+
*
|
|
10
|
+
* Stripped images are replaced by "[image dropped from live context]".
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, before, after } from 'node:test';
|
|
13
|
+
import assert from 'node:assert';
|
|
14
|
+
import { rmSync, existsSync } from 'node:fs';
|
|
15
|
+
import { ContextManager, AutobiographicalStrategy } from '../src/index.js';
|
|
16
|
+
const TEST_STORE_PATH = './test-image-stripping';
|
|
17
|
+
function cleanup() {
|
|
18
|
+
if (existsSync(TEST_STORE_PATH))
|
|
19
|
+
rmSync(TEST_STORE_PATH, { recursive: true, force: true });
|
|
20
|
+
}
|
|
21
|
+
/** A message carrying one image plus a text label so we can locate it later. */
|
|
22
|
+
function imageMessage(label, tokenEstimate = 1600) {
|
|
23
|
+
return [
|
|
24
|
+
{ type: 'image', source: { type: 'base64', data: 'AAAA', mediaType: 'image/png' }, tokenEstimate },
|
|
25
|
+
{ type: 'text', text: label },
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
/** Find the compiled entry whose text contains `label`, and report whether it
|
|
29
|
+
* still holds a real image block or was reduced to the placeholder. */
|
|
30
|
+
function imageState(messages, label) {
|
|
31
|
+
const entry = messages.find((m) => m.content.some((b) => b.type === 'text' && b.text.includes(label)));
|
|
32
|
+
if (!entry)
|
|
33
|
+
return 'missing';
|
|
34
|
+
if (entry.content.some((b) => b.type === 'image'))
|
|
35
|
+
return 'image';
|
|
36
|
+
if (entry.content.some((b) => b.type === 'text' && b.text.includes('image dropped from live context')))
|
|
37
|
+
return 'stripped';
|
|
38
|
+
return 'missing';
|
|
39
|
+
}
|
|
40
|
+
describe('Live image policy', () => {
|
|
41
|
+
before(() => cleanup());
|
|
42
|
+
after(() => cleanup());
|
|
43
|
+
it('keeps only the maxLiveImages most-recent images (count cap)', async () => {
|
|
44
|
+
cleanup();
|
|
45
|
+
const strategy = new AutobiographicalStrategy({
|
|
46
|
+
headWindowTokens: 0,
|
|
47
|
+
recentWindowTokens: 1_000_000, // keep everything verbatim
|
|
48
|
+
hierarchical: true,
|
|
49
|
+
maxLiveImages: 2,
|
|
50
|
+
imageStripDepthTokens: 0, // disable depth strip — isolate the count cap
|
|
51
|
+
});
|
|
52
|
+
const manager = await ContextManager.open({ path: TEST_STORE_PATH, strategy });
|
|
53
|
+
for (let i = 1; i <= 4; i++)
|
|
54
|
+
manager.addMessage('user', imageMessage(`img-0${i}`));
|
|
55
|
+
const compiled = await manager.compile({ maxTokens: 1_000_000, reserveForResponse: 0 });
|
|
56
|
+
// Newest two (img-03, img-04) keep their image; older two are stripped.
|
|
57
|
+
assert.equal(imageState(compiled.messages, 'img-04'), 'image', 'newest image kept');
|
|
58
|
+
assert.equal(imageState(compiled.messages, 'img-03'), 'image', '2nd-newest image kept');
|
|
59
|
+
assert.equal(imageState(compiled.messages, 'img-02'), 'stripped', '3rd-newest stripped by count cap');
|
|
60
|
+
assert.equal(imageState(compiled.messages, 'img-01'), 'stripped', 'oldest stripped by count cap');
|
|
61
|
+
manager.close();
|
|
62
|
+
});
|
|
63
|
+
it('strips images deeper than imageStripDepthTokens (depth cutoff)', async () => {
|
|
64
|
+
cleanup();
|
|
65
|
+
const strategy = new AutobiographicalStrategy({
|
|
66
|
+
headWindowTokens: 0,
|
|
67
|
+
recentWindowTokens: 1_000_000, // text horizon stays huge
|
|
68
|
+
hierarchical: true,
|
|
69
|
+
maxLiveImages: 0, // unlimited count — isolate the depth cutoff
|
|
70
|
+
imageStripDepthTokens: 3500, // ~2 images (1600 each) fit within the window
|
|
71
|
+
});
|
|
72
|
+
const manager = await ContextManager.open({ path: TEST_STORE_PATH, strategy });
|
|
73
|
+
for (let i = 1; i <= 4; i++)
|
|
74
|
+
manager.addMessage('user', imageMessage(`img-0${i}`));
|
|
75
|
+
const compiled = await manager.compile({ maxTokens: 1_000_000, reserveForResponse: 0 });
|
|
76
|
+
// Newest fits inside the depth window; the oldest is well beyond it.
|
|
77
|
+
assert.equal(imageState(compiled.messages, 'img-04'), 'image', 'newest image within depth window');
|
|
78
|
+
assert.equal(imageState(compiled.messages, 'img-01'), 'stripped', 'oldest image beyond depth window');
|
|
79
|
+
// But the text of the stripped message is still present verbatim (text
|
|
80
|
+
// horizon is independent of the image horizon).
|
|
81
|
+
const oldestText = compiled.messages.find((m) => m.content.some((b) => b.type === 'text' && b.text.includes('img-01')));
|
|
82
|
+
assert.ok(oldestText, 'oldest message text survives even though its image was stripped');
|
|
83
|
+
manager.close();
|
|
84
|
+
});
|
|
85
|
+
it('leaves images untouched when the policy is disabled', async () => {
|
|
86
|
+
cleanup();
|
|
87
|
+
const strategy = new AutobiographicalStrategy({
|
|
88
|
+
headWindowTokens: 0,
|
|
89
|
+
recentWindowTokens: 1_000_000,
|
|
90
|
+
hierarchical: true,
|
|
91
|
+
maxLiveImages: 0,
|
|
92
|
+
imageStripDepthTokens: 0,
|
|
93
|
+
});
|
|
94
|
+
const manager = await ContextManager.open({ path: TEST_STORE_PATH, strategy });
|
|
95
|
+
for (let i = 1; i <= 3; i++)
|
|
96
|
+
manager.addMessage('user', imageMessage(`img-0${i}`));
|
|
97
|
+
const compiled = await manager.compile({ maxTokens: 1_000_000, reserveForResponse: 0 });
|
|
98
|
+
for (const label of ['img-01', 'img-02', 'img-03']) {
|
|
99
|
+
assert.equal(imageState(compiled.messages, label), 'image', `${label} kept when policy disabled`);
|
|
100
|
+
}
|
|
101
|
+
manager.close();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
//# sourceMappingURL=image-stripping.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-stripping.test.js","sourceRoot":"","sources":["../../test/image-stripping.test.ts"],"names":[],"mappings":"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"}
|