@animalabs/context-manager 0.5.3 → 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.
Files changed (90) hide show
  1. package/dist/src/adaptive/index.d.ts +6 -0
  2. package/dist/src/adaptive/index.d.ts.map +1 -1
  3. package/dist/src/adaptive/index.js +8 -0
  4. package/dist/src/adaptive/index.js.map +1 -1
  5. package/dist/src/adaptive/kv-cache-sim.d.ts +128 -0
  6. package/dist/src/adaptive/kv-cache-sim.d.ts.map +1 -0
  7. package/dist/src/adaptive/kv-cache-sim.js +0 -0
  8. package/dist/src/adaptive/kv-cache-sim.js.map +1 -0
  9. package/dist/src/adaptive/kv-control.d.ts +152 -0
  10. package/dist/src/adaptive/kv-control.d.ts.map +1 -0
  11. package/dist/src/adaptive/kv-control.js +368 -0
  12. package/dist/src/adaptive/kv-control.js.map +1 -0
  13. package/dist/src/adaptive/kv-replay.d.ts +76 -0
  14. package/dist/src/adaptive/kv-replay.d.ts.map +1 -0
  15. package/dist/src/adaptive/kv-replay.js +175 -0
  16. package/dist/src/adaptive/kv-replay.js.map +1 -0
  17. package/dist/src/adaptive/render-offsets.d.ts +61 -0
  18. package/dist/src/adaptive/render-offsets.d.ts.map +1 -0
  19. package/dist/src/adaptive/render-offsets.js +88 -0
  20. package/dist/src/adaptive/render-offsets.js.map +1 -0
  21. package/dist/src/adaptive/strategies/kv-stable.d.ts +48 -0
  22. package/dist/src/adaptive/strategies/kv-stable.d.ts.map +1 -0
  23. package/dist/src/adaptive/strategies/kv-stable.js +102 -0
  24. package/dist/src/adaptive/strategies/kv-stable.js.map +1 -0
  25. package/dist/src/adaptive/summary-tree.d.ts +98 -0
  26. package/dist/src/adaptive/summary-tree.d.ts.map +1 -0
  27. package/dist/src/adaptive/summary-tree.js +214 -0
  28. package/dist/src/adaptive/summary-tree.js.map +1 -0
  29. package/dist/src/context-log.d.ts.map +1 -1
  30. package/dist/src/context-log.js +7 -2
  31. package/dist/src/context-log.js.map +1 -1
  32. package/dist/src/message-store.js +1 -1
  33. package/dist/src/message-store.js.map +1 -1
  34. package/dist/src/strategies/autobiographical.d.ts +68 -39
  35. package/dist/src/strategies/autobiographical.d.ts.map +1 -1
  36. package/dist/src/strategies/autobiographical.js +301 -213
  37. package/dist/src/strategies/autobiographical.js.map +1 -1
  38. package/dist/src/types/strategy.d.ts +40 -1
  39. package/dist/src/types/strategy.d.ts.map +1 -1
  40. package/dist/src/types/strategy.js +2 -0
  41. package/dist/src/types/strategy.js.map +1 -1
  42. package/dist/test/adaptive/kv-cache-sim.test.d.ts +16 -0
  43. package/dist/test/adaptive/kv-cache-sim.test.d.ts.map +1 -0
  44. package/dist/test/adaptive/kv-cache-sim.test.js +173 -0
  45. package/dist/test/adaptive/kv-cache-sim.test.js.map +1 -0
  46. package/dist/test/adaptive/kv-control.test.d.ts +14 -0
  47. package/dist/test/adaptive/kv-control.test.d.ts.map +1 -0
  48. package/dist/test/adaptive/kv-control.test.js +130 -0
  49. package/dist/test/adaptive/kv-control.test.js.map +1 -0
  50. package/dist/test/adaptive/kv-replay.test.d.ts +15 -0
  51. package/dist/test/adaptive/kv-replay.test.d.ts.map +1 -0
  52. package/dist/test/adaptive/kv-replay.test.js +112 -0
  53. package/dist/test/adaptive/kv-replay.test.js.map +1 -0
  54. package/dist/test/adaptive/kv-stable-strategy.test.d.ts +10 -0
  55. package/dist/test/adaptive/kv-stable-strategy.test.d.ts.map +1 -0
  56. package/dist/test/adaptive/kv-stable-strategy.test.js +89 -0
  57. package/dist/test/adaptive/kv-stable-strategy.test.js.map +1 -0
  58. package/dist/test/adaptive/render-offsets.test.d.ts +11 -0
  59. package/dist/test/adaptive/render-offsets.test.d.ts.map +1 -0
  60. package/dist/test/adaptive/render-offsets.test.js +85 -0
  61. package/dist/test/adaptive/render-offsets.test.js.map +1 -0
  62. package/dist/test/adaptive/summary-tree.test.d.ts +5 -0
  63. package/dist/test/adaptive/summary-tree.test.d.ts.map +1 -0
  64. package/dist/test/adaptive/summary-tree.test.js +72 -0
  65. package/dist/test/adaptive/summary-tree.test.js.map +1 -0
  66. package/dist/test/image-strip-render-stats.test.d.ts +2 -0
  67. package/dist/test/image-strip-render-stats.test.d.ts.map +1 -0
  68. package/dist/test/image-strip-render-stats.test.js +130 -0
  69. package/dist/test/image-strip-render-stats.test.js.map +1 -0
  70. package/dist/test/image-stripping.test.d.ts +13 -0
  71. package/dist/test/image-stripping.test.d.ts.map +1 -0
  72. package/dist/test/image-stripping.test.js +104 -0
  73. package/dist/test/image-stripping.test.js.map +1 -0
  74. package/dist/test/render-stats.test.d.ts +2 -0
  75. package/dist/test/render-stats.test.d.ts.map +1 -0
  76. package/dist/test/render-stats.test.js +91 -0
  77. package/dist/test/render-stats.test.js.map +1 -0
  78. package/dist/tsconfig.tsbuildinfo +1 -1
  79. package/package.json +2 -2
  80. package/src/adaptive/index.ts +48 -0
  81. package/src/adaptive/kv-cache-sim.ts +0 -0
  82. package/src/adaptive/kv-control.ts +497 -0
  83. package/src/adaptive/kv-replay.ts +238 -0
  84. package/src/adaptive/render-offsets.ts +116 -0
  85. package/src/adaptive/strategies/kv-stable.ts +123 -0
  86. package/src/adaptive/summary-tree.ts +252 -0
  87. package/src/context-log.ts +7 -2
  88. package/src/message-store.ts +1 -1
  89. package/src/strategies/autobiographical.ts +314 -263
  90. package/src/types/strategy.ts +40 -1
@@ -0,0 +1,89 @@
1
+ /**
2
+ * End-to-end: KvStableStrategy driven through the real Picker.run loop.
3
+ *
4
+ * Asserts the controller policy works as a drop-in FoldingStrategy: the picker
5
+ * walks to exactly the planned frontier, the result fits the budget, pins/locks
6
+ * are honored, and the same `planControlledFrontier` the replay harness measures
7
+ * is what the picker reaches.
8
+ */
9
+ import { test } from 'node:test';
10
+ import assert from 'node:assert/strict';
11
+ import { Picker } from '../../src/adaptive/picker.js';
12
+ import { SummaryTree } from '../../src/adaptive/summary-tree.js';
13
+ import { renderLayout } from '../../src/adaptive/render-offsets.js';
14
+ import { planControlledFrontier } from '../../src/adaptive/kv-control.js';
15
+ import { KvStableStrategy } from '../../src/adaptive/strategies/kv-stable.js';
16
+ import { buildChronicleWithChain } from './harness.js';
17
+ const BUDGET = (totalBudget, slack = 0.1) => ({
18
+ totalBudget,
19
+ targetBudget: totalBudget * (1 - slack),
20
+ slack,
21
+ });
22
+ function inputsOf(ch) {
23
+ return {
24
+ chunks: ch.chunks, summaries: ch.summaries, recallPairTokens: ch.recallPairTokens,
25
+ headTokens: 0, tailTokens: 0, headChunkIds: new Set(), tailChunkIds: new Set(),
26
+ };
27
+ }
28
+ function sameMap(a, b) {
29
+ if (a.size !== b.size)
30
+ return false;
31
+ for (const [k, v] of a)
32
+ if (b.get(k) !== v)
33
+ return false;
34
+ return true;
35
+ }
36
+ test('kv-stable strategy: picker walks to exactly the planned frontier and fits budget', () => {
37
+ const ch = buildChronicleWithChain({
38
+ chunkCount: 48, tokensPerChunk: 1000, mergeThreshold: 6, recallPairTokens: 200,
39
+ });
40
+ const inputs = inputsOf(ch);
41
+ const tree = new SummaryTree(inputs);
42
+ const budget = BUDGET(15_000); // targetBudget 13_500
43
+ // Independent reference plan (same policy the strategy uses internally).
44
+ let now = 0;
45
+ for (const c of inputs.chunks)
46
+ if (c.sequence > now)
47
+ now = c.sequence;
48
+ const plan = planControlledFrontier(inputs, tree, {
49
+ previous: new Map(), foldAtTokens: budget.totalBudget, expandAtTokens: budget.targetBudget,
50
+ targetTokens: budget.targetBudget, windowTokens: budget.totalBudget, rawZone: new Set(), now, mergeThreshold: 6,
51
+ });
52
+ const strategy = new KvStableStrategy(inputs, {});
53
+ const result = new Picker(strategy).run(inputs, budget);
54
+ assert.ok(sameMap(result.finalResolutions, plan.resolutions), 'picker reaches the planned frontier');
55
+ assert.ok(result.finalTokens <= budget.totalBudget, `fits hard wall: ${result.finalTokens} ≤ 15000`);
56
+ assert.equal(renderLayout(inputs, tree, result.finalResolutions).totalTokens, result.finalTokens);
57
+ });
58
+ test('kv-stable strategy: converges to null and leaves a small budget untouched', () => {
59
+ const ch = buildChronicleWithChain({ chunkCount: 8, tokensPerChunk: 500, mergeThreshold: 6, recallPairTokens: 200 });
60
+ const inputs = inputsOf(ch); // 4k raw
61
+ const result = new Picker(new KvStableStrategy(inputs, {})).run(inputs, BUDGET(100_000));
62
+ for (const c of inputs.chunks) {
63
+ assert.equal(result.finalResolutions.get(c.id), 0, 'nothing folded under a generous budget');
64
+ }
65
+ });
66
+ test('kv-stable strategy: pinned chunks stay raw, locked chunks keep their resolution', () => {
67
+ const ch = buildChronicleWithChain({ chunkCount: 48, tokensPerChunk: 1000, mergeThreshold: 6, recallPairTokens: 200 });
68
+ ch.chunks[2].pinned = true; // old → would otherwise fold
69
+ ch.chunks[5].lockedByAgent = true;
70
+ ch.chunks[5].currentResolution = 1; // frozen at L1
71
+ const inputs = inputsOf(ch);
72
+ const result = new Picker(new KvStableStrategy(inputs, {})).run(inputs, BUDGET(15_000));
73
+ assert.equal(result.finalResolutions.get(ch.chunks[2].id), 0, 'pinned chunk stays raw');
74
+ assert.equal(result.finalResolutions.get(ch.chunks[5].id), 1, 'locked chunk keeps its resolution');
75
+ });
76
+ test('kv-stable strategy: a tighter reach cap holds the deep prefix raw (shallower divergence)', () => {
77
+ const ch = buildChronicleWithChain({ chunkCount: 60, tokensPerChunk: 1000, mergeThreshold: 6, recallPairTokens: 200 });
78
+ const inputs = inputsOf(ch);
79
+ const budget = BUDGET(20_000);
80
+ const tight = new Picker(new KvStableStrategy(inputs, { reachTokens: 8_000 })).run(inputs, budget);
81
+ const wide = new Picker(new KvStableStrategy(inputs, { reachTokens: 60_000 })).run(inputs, budget);
82
+ // Deepest level among the oldest third of chunks: a tight reach can't fold
83
+ // that far back, so the deep prefix stays shallower (more raw) than with wide.
84
+ const ordered = [...inputs.chunks].sort((a, b) => a.sequence - b.sequence);
85
+ const oldThird = ordered.slice(0, 20);
86
+ const deepest = (r) => Math.max(...oldThird.map((c) => r.finalResolutions.get(c.id) ?? 0));
87
+ assert.ok(deepest(tight) <= deepest(wide), `tight reach keeps the deep prefix shallower (${deepest(tight)} ≤ ${deepest(wide)})`);
88
+ });
89
+ //# sourceMappingURL=kv-stable-strategy.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kv-stable-strategy.test.js","sourceRoot":"","sources":["../../../test/adaptive/kv-stable-strategy.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Tests for render-offsets — the rendered-unit layout + KV-cost accounting.
3
+ *
4
+ * Two things matter:
5
+ * 1. `renderLayout().totalTokens` must equal the picker's own computeTokens
6
+ * model (parity), so the KV term is computed over the real render.
7
+ * 2. KV cost must be position-aware: a change deep in the old prefix costs far
8
+ * more than a change near the tail — the whole reason for the term.
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=render-offsets.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-offsets.test.d.ts","sourceRoot":"","sources":["../../../test/adaptive/render-offsets.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Tests for render-offsets — the rendered-unit layout + KV-cost accounting.
3
+ *
4
+ * Two things matter:
5
+ * 1. `renderLayout().totalTokens` must equal the picker's own computeTokens
6
+ * model (parity), so the KV term is computed over the real render.
7
+ * 2. KV cost must be position-aware: a change deep in the old prefix costs far
8
+ * more than a change near the tail — the whole reason for the term.
9
+ */
10
+ import { test } from 'node:test';
11
+ import assert from 'node:assert/strict';
12
+ import { Picker } from '../../src/adaptive/picker.js';
13
+ import { FlatProfileStrategy } from '../../src/adaptive/strategies/flat-profile.js';
14
+ import { SummaryTree } from '../../src/adaptive/summary-tree.js';
15
+ import { renderLayout, kvCost, earliestDivergenceIndex, } from '../../src/adaptive/render-offsets.js';
16
+ import { buildChronicleWithChain } from './harness.js';
17
+ const BUDGET = (totalBudget, slack = 0.1) => ({
18
+ totalBudget,
19
+ targetBudget: totalBudget * (1 - slack),
20
+ slack,
21
+ });
22
+ function inputsOf(ch) {
23
+ // Explicit empty head/tail. (computeHeadTail force-includes one tail chunk
24
+ // even at tailTokens:0, which would add the tail block to the recomputed
25
+ // suffix and muddy the position-aware assertion below.)
26
+ return {
27
+ chunks: ch.chunks,
28
+ summaries: ch.summaries,
29
+ recallPairTokens: ch.recallPairTokens,
30
+ headTokens: 0,
31
+ tailTokens: 0,
32
+ headChunkIds: new Set(),
33
+ tailChunkIds: new Set(),
34
+ };
35
+ }
36
+ function frontier(ids, level) {
37
+ return new Map(ids.map((id) => [id, level]));
38
+ }
39
+ test('render-offsets: totalTokens matches the picker computeTokens model (parity)', () => {
40
+ const ch = buildChronicleWithChain({
41
+ chunkCount: 36, tokensPerChunk: 1000, mergeThreshold: 6, recallPairTokens: 200,
42
+ });
43
+ const inputs = inputsOf(ch);
44
+ const result = new Picker(new FlatProfileStrategy()).run(inputs, BUDGET(10_000));
45
+ assert.ok(result.applied.length > 0, 'expected the picker to fold something');
46
+ const tree = new SummaryTree(inputs);
47
+ const layout = renderLayout(inputs, tree, result.finalResolutions);
48
+ assert.equal(layout.totalTokens, result.finalTokens);
49
+ });
50
+ test('render-offsets: kvCost is zero for an unchanged frontier', () => {
51
+ const ch = buildChronicleWithChain({
52
+ chunkCount: 36, tokensPerChunk: 1000, mergeThreshold: 6, recallPairTokens: 200,
53
+ });
54
+ const inputs = inputsOf(ch);
55
+ const tree = new SummaryTree(inputs);
56
+ const layout = renderLayout(inputs, tree, new Map());
57
+ assert.equal(kvCost(layout, layout), 0);
58
+ });
59
+ test('render-offsets: deep-old change costs more than a near-tail change (position-aware)', () => {
60
+ const ch = buildChronicleWithChain({
61
+ chunkCount: 36, tokensPerChunk: 1000, mergeThreshold: 6, recallPairTokens: 200,
62
+ });
63
+ const inputs = inputsOf(ch);
64
+ const tree = new SummaryTree(inputs);
65
+ const allRaw = renderLayout(inputs, tree, new Map());
66
+ const foldOldest = renderLayout(inputs, tree, frontier(['c-0000', 'c-0001', 'c-0002', 'c-0003', 'c-0004', 'c-0005'], 1));
67
+ const foldNewest = renderLayout(inputs, tree, frontier(['c-0030', 'c-0031', 'c-0032', 'c-0033', 'c-0034', 'c-0035'], 1));
68
+ const costOld = kvCost(allRaw, foldOldest);
69
+ const costNew = kvCost(allRaw, foldNewest);
70
+ assert.ok(costNew < costOld, `near-tail (${costNew}) must cost less than deep-old (${costOld})`);
71
+ assert.equal(costNew, 200, 'near-tail fold recomputes only the new recall pair');
72
+ });
73
+ test('render-offsets: earliestDivergenceIndex + kvCost prefix semantics', () => {
74
+ const u = (kind, key, tokens, offset) => ({ kind, key, tokens, offset });
75
+ const a = [u('head', 'head', 10, 0), u('raw', 'x', 5, 10), u('raw', 'y', 5, 15)];
76
+ const same = [u('head', 'head', 10, 0), u('raw', 'x', 5, 10), u('raw', 'y', 5, 15)];
77
+ assert.equal(earliestDivergenceIndex(a, same), -1);
78
+ const prefix = [u('head', 'head', 10, 0), u('raw', 'x', 5, 10)]; // strict prefix of a
79
+ assert.equal(earliestDivergenceIndex(a, prefix), 2);
80
+ assert.equal(kvCost({ units: a, totalTokens: 20 }, { units: prefix, totalTokens: 15 }), 0);
81
+ const diff = [u('head', 'head', 10, 0), u('raw', 'z', 5, 10), u('raw', 'y', 5, 15)];
82
+ assert.equal(earliestDivergenceIndex(a, diff), 1);
83
+ assert.equal(kvCost({ units: a, totalTokens: 20 }, { units: diff, totalTokens: 20 }), 10); // 20 − offset@1
84
+ });
85
+ //# sourceMappingURL=render-offsets.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-offsets.test.js","sourceRoot":"","sources":["../../../test/adaptive/render-offsets.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Tests for SummaryTree — the structural view the V2 best-fit DP walks.
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=summary-tree.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"summary-tree.test.d.ts","sourceRoot":"","sources":["../../../test/adaptive/summary-tree.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Tests for SummaryTree — the structural view the V2 best-fit DP walks.
3
+ */
4
+ import { test } from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import { SummaryTree, nodeTokens } from '../../src/adaptive/summary-tree.js';
7
+ import { MockChronicle, buildChronicleWithChain } from './harness.js';
8
+ function inputsOf(chronicle) {
9
+ const ht = chronicle.computeHeadTail({ headTokens: 0, tailTokens: 0 });
10
+ return {
11
+ chunks: chronicle.chunks,
12
+ summaries: chronicle.summaries,
13
+ recallPairTokens: chronicle.recallPairTokens,
14
+ ...ht,
15
+ };
16
+ }
17
+ test('summary-tree: two L1s, no L2 — roots are the L1 summaries', () => {
18
+ const ch = buildChronicleWithChain({
19
+ chunkCount: 12,
20
+ tokensPerChunk: 100,
21
+ mergeThreshold: 6,
22
+ recallPairTokens: 200,
23
+ });
24
+ const tree = new SummaryTree(inputsOf(ch));
25
+ assert.equal(tree.ancestorAt('c-0000', 1)?.id, 'L1-0');
26
+ assert.equal(tree.ancestorAt('c-0000', 2), null, 'no L2 produced');
27
+ assert.equal(tree.maxLevel('c-0000'), 1);
28
+ assert.deepEqual(tree.leavesUnder('L1-0'), [
29
+ 'c-0000', 'c-0001', 'c-0002', 'c-0003', 'c-0004', 'c-0005',
30
+ ]);
31
+ assert.equal(tree.recallTokens('L1-0'), 200);
32
+ const roots = tree.roots();
33
+ assert.deepEqual(roots.map((r) => (r.kind === 'summary' ? r.id : r.chunkId)), ['L1-0', 'L1-1']);
34
+ const l1 = tree.summary('L1-0');
35
+ const kids = tree.children(l1);
36
+ assert.equal(kids.length, 6);
37
+ assert.ok(kids.every((k) => k.kind === 'leaf'));
38
+ assert.equal(nodeTokens(l1), 200);
39
+ assert.equal(nodeTokens(kids[0]), 100);
40
+ });
41
+ test('summary-tree: full L1→L2 chain — single L2 root with summary children', () => {
42
+ const ch = buildChronicleWithChain({
43
+ chunkCount: 36,
44
+ tokensPerChunk: 100,
45
+ mergeThreshold: 6,
46
+ recallPairTokens: 200,
47
+ });
48
+ const tree = new SummaryTree(inputsOf(ch));
49
+ assert.equal(tree.ancestorAt('c-0000', 1)?.id, 'L1-0');
50
+ assert.equal(tree.ancestorAt('c-0000', 2)?.id, 'L2-0');
51
+ assert.equal(tree.maxLevel('c-0000'), 2);
52
+ const roots = tree.roots();
53
+ assert.equal(roots.length, 1);
54
+ assert.equal(roots[0].kind, 'summary');
55
+ assert.equal(roots[0].id, 'L2-0');
56
+ assert.equal(tree.leavesUnder('L2-0').length, 36);
57
+ const kids = tree.children(tree.summary('L2-0'));
58
+ assert.equal(kids.length, 6);
59
+ assert.ok(kids.every((k) => k.kind === 'summary'));
60
+ });
61
+ test('summary-tree: an uncovered leaf is its own root at level 0', () => {
62
+ const ch = new MockChronicle({ mergeThreshold: 6, recallPairTokens: 200 });
63
+ ch.addChunk({ id: 'a', rawTokens: 100 });
64
+ ch.addChunk({ id: 'b', rawTokens: 100 });
65
+ ch.produceL1(['a']); // only 'a' is covered
66
+ const tree = new SummaryTree(inputsOf(ch));
67
+ assert.equal(tree.maxLevel('b'), 0);
68
+ assert.equal(tree.ancestorAt('b', 1), null);
69
+ const rootKeys = tree.roots().map((r) => (r.kind === 'summary' ? r.id : r.chunkId));
70
+ assert.deepEqual(rootKeys, ['L1-0', 'b']); // ordered by source sequence
71
+ });
72
+ //# sourceMappingURL=summary-tree.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"summary-tree.test.js","sourceRoot":"","sources":["../../../test/adaptive/summary-tree.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=image-strip-render-stats.test.d.ts.map
@@ -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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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=render-stats.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-stats.test.d.ts","sourceRoot":"","sources":["../../test/render-stats.test.ts"],"names":[],"mappings":""}