@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.
Files changed (110) 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/index.d.ts +1 -1
  33. package/dist/src/index.d.ts.map +1 -1
  34. package/dist/src/index.js +1 -1
  35. package/dist/src/index.js.map +1 -1
  36. package/dist/src/message-store.js +1 -1
  37. package/dist/src/message-store.js.map +1 -1
  38. package/dist/src/normalize-tool-messages.d.ts +33 -0
  39. package/dist/src/normalize-tool-messages.d.ts.map +1 -1
  40. package/dist/src/normalize-tool-messages.js +59 -0
  41. package/dist/src/normalize-tool-messages.js.map +1 -1
  42. package/dist/src/strategies/autobiographical.d.ts +101 -39
  43. package/dist/src/strategies/autobiographical.d.ts.map +1 -1
  44. package/dist/src/strategies/autobiographical.js +398 -222
  45. package/dist/src/strategies/autobiographical.js.map +1 -1
  46. package/dist/src/types/strategy.d.ts +40 -1
  47. package/dist/src/types/strategy.d.ts.map +1 -1
  48. package/dist/src/types/strategy.js +2 -0
  49. package/dist/src/types/strategy.js.map +1 -1
  50. package/dist/test/adaptive/kv-cache-sim.test.d.ts +16 -0
  51. package/dist/test/adaptive/kv-cache-sim.test.d.ts.map +1 -0
  52. package/dist/test/adaptive/kv-cache-sim.test.js +173 -0
  53. package/dist/test/adaptive/kv-cache-sim.test.js.map +1 -0
  54. package/dist/test/adaptive/kv-control.test.d.ts +14 -0
  55. package/dist/test/adaptive/kv-control.test.d.ts.map +1 -0
  56. package/dist/test/adaptive/kv-control.test.js +130 -0
  57. package/dist/test/adaptive/kv-control.test.js.map +1 -0
  58. package/dist/test/adaptive/kv-replay.test.d.ts +15 -0
  59. package/dist/test/adaptive/kv-replay.test.d.ts.map +1 -0
  60. package/dist/test/adaptive/kv-replay.test.js +112 -0
  61. package/dist/test/adaptive/kv-replay.test.js.map +1 -0
  62. package/dist/test/adaptive/kv-stable-strategy.test.d.ts +10 -0
  63. package/dist/test/adaptive/kv-stable-strategy.test.d.ts.map +1 -0
  64. package/dist/test/adaptive/kv-stable-strategy.test.js +89 -0
  65. package/dist/test/adaptive/kv-stable-strategy.test.js.map +1 -0
  66. package/dist/test/adaptive/render-offsets.test.d.ts +11 -0
  67. package/dist/test/adaptive/render-offsets.test.d.ts.map +1 -0
  68. package/dist/test/adaptive/render-offsets.test.js +85 -0
  69. package/dist/test/adaptive/render-offsets.test.js.map +1 -0
  70. package/dist/test/adaptive/summary-tree.test.d.ts +5 -0
  71. package/dist/test/adaptive/summary-tree.test.d.ts.map +1 -0
  72. package/dist/test/adaptive/summary-tree.test.js +72 -0
  73. package/dist/test/adaptive/summary-tree.test.js.map +1 -0
  74. package/dist/test/chunker-tool-boundary.test.d.ts +12 -0
  75. package/dist/test/chunker-tool-boundary.test.d.ts.map +1 -0
  76. package/dist/test/chunker-tool-boundary.test.js +89 -0
  77. package/dist/test/chunker-tool-boundary.test.js.map +1 -0
  78. package/dist/test/compression-shape-invariants.test.d.ts +22 -0
  79. package/dist/test/compression-shape-invariants.test.d.ts.map +1 -0
  80. package/dist/test/compression-shape-invariants.test.js +312 -0
  81. package/dist/test/compression-shape-invariants.test.js.map +1 -0
  82. package/dist/test/image-strip-render-stats.test.d.ts +2 -0
  83. package/dist/test/image-strip-render-stats.test.d.ts.map +1 -0
  84. package/dist/test/image-strip-render-stats.test.js +130 -0
  85. package/dist/test/image-strip-render-stats.test.js.map +1 -0
  86. package/dist/test/image-stripping.test.d.ts +13 -0
  87. package/dist/test/image-stripping.test.d.ts.map +1 -0
  88. package/dist/test/image-stripping.test.js +104 -0
  89. package/dist/test/image-stripping.test.js.map +1 -0
  90. package/dist/test/normalize-tool-messages.test.js +61 -1
  91. package/dist/test/normalize-tool-messages.test.js.map +1 -1
  92. package/dist/test/render-stats.test.d.ts +2 -0
  93. package/dist/test/render-stats.test.d.ts.map +1 -0
  94. package/dist/test/render-stats.test.js +91 -0
  95. package/dist/test/render-stats.test.js.map +1 -0
  96. package/dist/tsconfig.tsbuildinfo +1 -1
  97. package/package.json +2 -2
  98. package/src/adaptive/index.ts +48 -0
  99. package/src/adaptive/kv-cache-sim.ts +0 -0
  100. package/src/adaptive/kv-control.ts +497 -0
  101. package/src/adaptive/kv-replay.ts +238 -0
  102. package/src/adaptive/render-offsets.ts +116 -0
  103. package/src/adaptive/strategies/kv-stable.ts +123 -0
  104. package/src/adaptive/summary-tree.ts +252 -0
  105. package/src/context-log.ts +7 -2
  106. package/src/index.ts +1 -1
  107. package/src/message-store.ts +1 -1
  108. package/src/normalize-tool-messages.ts +61 -0
  109. package/src/strategies/autobiographical.ts +414 -271
  110. 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,12 @@
1
+ /**
2
+ * Chunker invariant: a chunk must not close on a message containing a
3
+ * `tool_use` block, because the matching `tool_result` lives in the
4
+ * immediately-following user message and the Anthropic API rejects a
5
+ * compression request where a tool_use isn't followed by its result.
6
+ *
7
+ * Companion to `stripUnpairedToolBlocks` (the runtime safety net) — this
8
+ * verifies the upstream chunker behavior that avoids the situation in the
9
+ * first place.
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=chunker-tool-boundary.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunker-tool-boundary.test.d.ts","sourceRoot":"","sources":["../../test/chunker-tool-boundary.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Chunker invariant: a chunk must not close on a message containing a
3
+ * `tool_use` block, because the matching `tool_result` lives in the
4
+ * immediately-following user message and the Anthropic API rejects a
5
+ * compression request where a tool_use isn't followed by its result.
6
+ *
7
+ * Companion to `stripUnpairedToolBlocks` (the runtime safety net) — this
8
+ * verifies the upstream chunker behavior that avoids the situation in the
9
+ * first place.
10
+ */
11
+ import { describe, it, before, after } from 'node:test';
12
+ import assert from 'node:assert';
13
+ import { rmSync, existsSync } from 'node:fs';
14
+ import { ContextManager, AutobiographicalStrategy } from '../src/index.js';
15
+ const TEST_STORE_PATH = './test-chunker-tool-boundary';
16
+ function cleanup() {
17
+ if (existsSync(TEST_STORE_PATH)) {
18
+ rmSync(TEST_STORE_PATH, { recursive: true, force: true });
19
+ }
20
+ }
21
+ describe('Chunker: tool cycle boundary', () => {
22
+ before(() => cleanup());
23
+ after(() => cleanup());
24
+ it("defers chunk closure when the last message contains a tool_use", async () => {
25
+ cleanup();
26
+ const strategy = new AutobiographicalStrategy({
27
+ // Very small chunk threshold so we hit close-conditions quickly.
28
+ targetChunkTokens: 50,
29
+ // Disable head/recent windows so all messages are eligible for chunking.
30
+ headWindowTokens: 0,
31
+ recentWindowTokens: 0,
32
+ });
33
+ const manager = await ContextManager.open({
34
+ path: TEST_STORE_PATH,
35
+ strategy,
36
+ });
37
+ // Build a sequence where the message that would land at the chunk
38
+ // boundary contains a tool_use. The matching tool_result is the next
39
+ // message. The chunker should defer closing to include both.
40
+ //
41
+ // Messages (~30 tokens each so we cross 50 tokens after the 2nd):
42
+ // 0: user text
43
+ // 1: agent text
44
+ // 2: user text
45
+ // 3: agent text + tool_use(id=A) <- would close here (4 msgs, >50 tok)
46
+ // 4: user tool_result(id=A) <- chunk extended to include this
47
+ // 5: agent text
48
+ // 6: user text
49
+ // ...
50
+ const filler = (n) => 'word '.repeat(n);
51
+ manager.addMessage('user', [{ type: 'text', text: filler(30) }]);
52
+ manager.addMessage('agent', [{ type: 'text', text: filler(30) }]);
53
+ manager.addMessage('user', [{ type: 'text', text: filler(30) }]);
54
+ manager.addMessage('agent', [
55
+ { type: 'text', text: filler(20) },
56
+ { type: 'tool_use', id: 'A', name: 'fn', input: {} },
57
+ ]);
58
+ manager.addMessage('user', [
59
+ { type: 'tool_result', toolUseId: 'A', content: 'res' },
60
+ ]);
61
+ // Add more messages to ensure a second chunk forms (so the first
62
+ // chunk's boundary is unambiguous, not just "end of store").
63
+ for (let i = 0; i < 6; i++) {
64
+ manager.addMessage(i % 2 === 0 ? 'agent' : 'user', [
65
+ { type: 'text', text: filler(30) },
66
+ ]);
67
+ }
68
+ // Force a chunker rebuild by reading state via the strategy.
69
+ // ContextManager runs rebuildChunks during compile() — easier to call
70
+ // the internal directly.
71
+ const s = strategy;
72
+ s.rebuildChunks(manager.messageStore);
73
+ assert.ok(s.chunks.length >= 1, 'should have produced at least one chunk');
74
+ // For every closed chunk, the last message must NOT contain a tool_use.
75
+ for (const chunk of s.chunks) {
76
+ const last = chunk.messages[chunk.messages.length - 1];
77
+ const hasToolUse = last.content.some((b) => b.type === 'tool_use');
78
+ assert.ok(!hasToolUse, `chunk ending on message ${last.id} contains a trailing tool_use — boundary not deferred`);
79
+ }
80
+ // Specifically: the first chunk should have included message 4 (the
81
+ // tool_result), not stopped at message 3.
82
+ const firstChunk = s.chunks[0];
83
+ const firstChunkLastContent = firstChunk.messages[firstChunk.messages.length - 1].content;
84
+ const lastBlockIsToolResult = firstChunkLastContent.some((b) => b.type === 'tool_result');
85
+ assert.ok(lastBlockIsToolResult || firstChunk.messages.length >= 5, 'first chunk should have been extended to include the tool_result');
86
+ await manager.close();
87
+ });
88
+ });
89
+ //# sourceMappingURL=chunker-tool-boundary.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunker-tool-boundary.test.js","sourceRoot":"","sources":["../../test/chunker-tool-boundary.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
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
+ export {};
22
+ //# sourceMappingURL=compression-shape-invariants.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compression-shape-invariants.test.d.ts","sourceRoot":"","sources":["../../test/compression-shape-invariants.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG"}