@diyor28/context 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/README.md +270 -0
  2. package/dist/__tests__/attachment-selector.test.d.ts +11 -0
  3. package/dist/__tests__/attachment-selector.test.d.ts.map +1 -0
  4. package/dist/__tests__/attachment-selector.test.js +449 -0
  5. package/dist/__tests__/attachment-selector.test.js.map +1 -0
  6. package/dist/__tests__/cache-breakpoints.test.d.ts +11 -0
  7. package/dist/__tests__/cache-breakpoints.test.d.ts.map +1 -0
  8. package/dist/__tests__/cache-breakpoints.test.js +398 -0
  9. package/dist/__tests__/cache-breakpoints.test.js.map +1 -0
  10. package/dist/__tests__/codecs.test.d.ts +7 -0
  11. package/dist/__tests__/codecs.test.d.ts.map +1 -0
  12. package/dist/__tests__/codecs.test.js +331 -0
  13. package/dist/__tests__/codecs.test.js.map +1 -0
  14. package/dist/__tests__/compactor.test.d.ts +11 -0
  15. package/dist/__tests__/compactor.test.d.ts.map +1 -0
  16. package/dist/__tests__/compactor.test.js +519 -0
  17. package/dist/__tests__/compactor.test.js.map +1 -0
  18. package/dist/__tests__/context-graph.test.d.ts +7 -0
  19. package/dist/__tests__/context-graph.test.d.ts.map +1 -0
  20. package/dist/__tests__/context-graph.test.js +262 -0
  21. package/dist/__tests__/context-graph.test.js.map +1 -0
  22. package/dist/__tests__/hash.test.d.ts +7 -0
  23. package/dist/__tests__/hash.test.d.ts.map +1 -0
  24. package/dist/__tests__/hash.test.js +228 -0
  25. package/dist/__tests__/hash.test.js.map +1 -0
  26. package/dist/__tests__/integration.test.d.ts +15 -0
  27. package/dist/__tests__/integration.test.d.ts.map +1 -0
  28. package/dist/__tests__/integration.test.js +728 -0
  29. package/dist/__tests__/integration.test.js.map +1 -0
  30. package/dist/__tests__/kind-order.test.d.ts +7 -0
  31. package/dist/__tests__/kind-order.test.d.ts.map +1 -0
  32. package/dist/__tests__/kind-order.test.js +243 -0
  33. package/dist/__tests__/kind-order.test.js.map +1 -0
  34. package/dist/__tests__/phase2-integration.test.d.ts +5 -0
  35. package/dist/__tests__/phase2-integration.test.d.ts.map +1 -0
  36. package/dist/__tests__/phase2-integration.test.js +222 -0
  37. package/dist/__tests__/phase2-integration.test.js.map +1 -0
  38. package/dist/__tests__/queries.test.d.ts +7 -0
  39. package/dist/__tests__/queries.test.d.ts.map +1 -0
  40. package/dist/__tests__/queries.test.js +254 -0
  41. package/dist/__tests__/queries.test.js.map +1 -0
  42. package/dist/__tests__/token-estimator.test.d.ts +7 -0
  43. package/dist/__tests__/token-estimator.test.d.ts.map +1 -0
  44. package/dist/__tests__/token-estimator.test.js +267 -0
  45. package/dist/__tests__/token-estimator.test.js.map +1 -0
  46. package/dist/adapters/anthropic-estimator.d.ts +38 -0
  47. package/dist/adapters/anthropic-estimator.d.ts.map +1 -0
  48. package/dist/adapters/anthropic-estimator.js +108 -0
  49. package/dist/adapters/anthropic-estimator.js.map +1 -0
  50. package/dist/adapters/attachment-resolver.d.ts +96 -0
  51. package/dist/adapters/attachment-resolver.d.ts.map +1 -0
  52. package/dist/adapters/attachment-resolver.js +176 -0
  53. package/dist/adapters/attachment-resolver.js.map +1 -0
  54. package/dist/adapters/attachment-selector.d.ts +59 -0
  55. package/dist/adapters/attachment-selector.d.ts.map +1 -0
  56. package/dist/adapters/attachment-selector.js +163 -0
  57. package/dist/adapters/attachment-selector.js.map +1 -0
  58. package/dist/adapters/gemini-estimator.d.ts +27 -0
  59. package/dist/adapters/gemini-estimator.d.ts.map +1 -0
  60. package/dist/adapters/gemini-estimator.js +80 -0
  61. package/dist/adapters/gemini-estimator.js.map +1 -0
  62. package/dist/adapters/index.d.ts +12 -0
  63. package/dist/adapters/index.d.ts.map +1 -0
  64. package/dist/adapters/index.js +28 -0
  65. package/dist/adapters/index.js.map +1 -0
  66. package/dist/adapters/memory-store.d.ts +139 -0
  67. package/dist/adapters/memory-store.d.ts.map +1 -0
  68. package/dist/adapters/memory-store.js +187 -0
  69. package/dist/adapters/memory-store.js.map +1 -0
  70. package/dist/adapters/openai-estimator.d.ts +35 -0
  71. package/dist/adapters/openai-estimator.d.ts.map +1 -0
  72. package/dist/adapters/openai-estimator.js +89 -0
  73. package/dist/adapters/openai-estimator.js.map +1 -0
  74. package/dist/adapters/summarizer.d.ts +121 -0
  75. package/dist/adapters/summarizer.d.ts.map +1 -0
  76. package/dist/adapters/summarizer.js +121 -0
  77. package/dist/adapters/summarizer.js.map +1 -0
  78. package/dist/adapters/token-estimator.d.ts +63 -0
  79. package/dist/adapters/token-estimator.d.ts.map +1 -0
  80. package/dist/adapters/token-estimator.js +37 -0
  81. package/dist/adapters/token-estimator.js.map +1 -0
  82. package/dist/builder/context-builder.d.ts +186 -0
  83. package/dist/builder/context-builder.d.ts.map +1 -0
  84. package/dist/builder/context-builder.js +305 -0
  85. package/dist/builder/context-builder.js.map +1 -0
  86. package/dist/builder/context-fork.d.ts +166 -0
  87. package/dist/builder/context-fork.d.ts.map +1 -0
  88. package/dist/builder/context-fork.js +282 -0
  89. package/dist/builder/context-fork.js.map +1 -0
  90. package/dist/builder/index.d.ts +6 -0
  91. package/dist/builder/index.d.ts.map +1 -0
  92. package/dist/builder/index.js +22 -0
  93. package/dist/builder/index.js.map +1 -0
  94. package/dist/codecs/base.d.ts +18 -0
  95. package/dist/codecs/base.d.ts.map +1 -0
  96. package/dist/codecs/base.js +39 -0
  97. package/dist/codecs/base.js.map +1 -0
  98. package/dist/codecs/conversation-history.codec.d.ts +81 -0
  99. package/dist/codecs/conversation-history.codec.d.ts.map +1 -0
  100. package/dist/codecs/conversation-history.codec.js +89 -0
  101. package/dist/codecs/conversation-history.codec.js.map +1 -0
  102. package/dist/codecs/index.d.ts +31 -0
  103. package/dist/codecs/index.d.ts.map +1 -0
  104. package/dist/codecs/index.js +71 -0
  105. package/dist/codecs/index.js.map +1 -0
  106. package/dist/codecs/redacted-stub.codec.d.ts +32 -0
  107. package/dist/codecs/redacted-stub.codec.d.ts.map +1 -0
  108. package/dist/codecs/redacted-stub.codec.js +64 -0
  109. package/dist/codecs/redacted-stub.codec.js.map +1 -0
  110. package/dist/codecs/structured-reference.codec.d.ts +40 -0
  111. package/dist/codecs/structured-reference.codec.d.ts.map +1 -0
  112. package/dist/codecs/structured-reference.codec.js +81 -0
  113. package/dist/codecs/structured-reference.codec.js.map +1 -0
  114. package/dist/codecs/system-rules.codec.d.ts +32 -0
  115. package/dist/codecs/system-rules.codec.d.ts.map +1 -0
  116. package/dist/codecs/system-rules.codec.js +62 -0
  117. package/dist/codecs/system-rules.codec.js.map +1 -0
  118. package/dist/codecs/tool-output.codec.d.ts +66 -0
  119. package/dist/codecs/tool-output.codec.d.ts.map +1 -0
  120. package/dist/codecs/tool-output.codec.js +95 -0
  121. package/dist/codecs/tool-output.codec.js.map +1 -0
  122. package/dist/codecs/tool-schema.codec.d.ts +36 -0
  123. package/dist/codecs/tool-schema.codec.d.ts.map +1 -0
  124. package/dist/codecs/tool-schema.codec.js +74 -0
  125. package/dist/codecs/tool-schema.codec.js.map +1 -0
  126. package/dist/codecs/unsafe-text.codec.d.ts +28 -0
  127. package/dist/codecs/unsafe-text.codec.d.ts.map +1 -0
  128. package/dist/codecs/unsafe-text.codec.js +63 -0
  129. package/dist/codecs/unsafe-text.codec.js.map +1 -0
  130. package/dist/graph/context-graph.d.ts +121 -0
  131. package/dist/graph/context-graph.d.ts.map +1 -0
  132. package/dist/graph/context-graph.js +166 -0
  133. package/dist/graph/context-graph.js.map +1 -0
  134. package/dist/graph/index.d.ts +8 -0
  135. package/dist/graph/index.d.ts.map +1 -0
  136. package/dist/graph/index.js +24 -0
  137. package/dist/graph/index.js.map +1 -0
  138. package/dist/graph/kind-order.d.ts +60 -0
  139. package/dist/graph/kind-order.d.ts.map +1 -0
  140. package/dist/graph/kind-order.js +113 -0
  141. package/dist/graph/kind-order.js.map +1 -0
  142. package/dist/graph/queries.d.ts +68 -0
  143. package/dist/graph/queries.d.ts.map +1 -0
  144. package/dist/graph/queries.js +240 -0
  145. package/dist/graph/queries.js.map +1 -0
  146. package/dist/graph/views.d.ts +90 -0
  147. package/dist/graph/views.d.ts.map +1 -0
  148. package/dist/graph/views.js +173 -0
  149. package/dist/graph/views.js.map +1 -0
  150. package/dist/index.d.ts +16 -0
  151. package/dist/index.d.ts.map +1 -0
  152. package/dist/index.js +40 -0
  153. package/dist/index.js.map +1 -0
  154. package/dist/pipeline/compactor.d.ts +128 -0
  155. package/dist/pipeline/compactor.d.ts.map +1 -0
  156. package/dist/pipeline/compactor.js +346 -0
  157. package/dist/pipeline/compactor.js.map +1 -0
  158. package/dist/pipeline/index.d.ts +6 -0
  159. package/dist/pipeline/index.d.ts.map +1 -0
  160. package/dist/pipeline/index.js +22 -0
  161. package/dist/pipeline/index.js.map +1 -0
  162. package/dist/pipeline/summarizer.d.ts +18 -0
  163. package/dist/pipeline/summarizer.d.ts.map +1 -0
  164. package/dist/pipeline/summarizer.js +68 -0
  165. package/dist/pipeline/summarizer.js.map +1 -0
  166. package/dist/policies/default-policy.d.ts +29 -0
  167. package/dist/policies/default-policy.d.ts.map +1 -0
  168. package/dist/policies/default-policy.js +58 -0
  169. package/dist/policies/default-policy.js.map +1 -0
  170. package/dist/policies/index.d.ts +5 -0
  171. package/dist/policies/index.d.ts.map +1 -0
  172. package/dist/policies/index.js +21 -0
  173. package/dist/policies/index.js.map +1 -0
  174. package/dist/providers/anthropic-compiler.d.ts +58 -0
  175. package/dist/providers/anthropic-compiler.d.ts.map +1 -0
  176. package/dist/providers/anthropic-compiler.js +182 -0
  177. package/dist/providers/anthropic-compiler.js.map +1 -0
  178. package/dist/providers/capabilities.d.ts +54 -0
  179. package/dist/providers/capabilities.d.ts.map +1 -0
  180. package/dist/providers/capabilities.js +87 -0
  181. package/dist/providers/capabilities.js.map +1 -0
  182. package/dist/providers/gemini-compiler.d.ts +51 -0
  183. package/dist/providers/gemini-compiler.d.ts.map +1 -0
  184. package/dist/providers/gemini-compiler.js +206 -0
  185. package/dist/providers/gemini-compiler.js.map +1 -0
  186. package/dist/providers/index.d.ts +8 -0
  187. package/dist/providers/index.d.ts.map +1 -0
  188. package/dist/providers/index.js +24 -0
  189. package/dist/providers/index.js.map +1 -0
  190. package/dist/providers/openai-compiler.d.ts +46 -0
  191. package/dist/providers/openai-compiler.d.ts.map +1 -0
  192. package/dist/providers/openai-compiler.js +149 -0
  193. package/dist/providers/openai-compiler.js.map +1 -0
  194. package/dist/types/attachment.d.ts +62 -0
  195. package/dist/types/attachment.d.ts.map +1 -0
  196. package/dist/types/attachment.js +6 -0
  197. package/dist/types/attachment.js.map +1 -0
  198. package/dist/types/block.d.ts +61 -0
  199. package/dist/types/block.d.ts.map +1 -0
  200. package/dist/types/block.js +8 -0
  201. package/dist/types/block.js.map +1 -0
  202. package/dist/types/codec.d.ts +58 -0
  203. package/dist/types/codec.d.ts.map +1 -0
  204. package/dist/types/codec.js +6 -0
  205. package/dist/types/codec.js.map +1 -0
  206. package/dist/types/compiled.d.ts +91 -0
  207. package/dist/types/compiled.d.ts.map +1 -0
  208. package/dist/types/compiled.js +6 -0
  209. package/dist/types/compiled.js.map +1 -0
  210. package/dist/types/hash.d.ts +24 -0
  211. package/dist/types/hash.d.ts.map +1 -0
  212. package/dist/types/hash.js +49 -0
  213. package/dist/types/hash.js.map +1 -0
  214. package/dist/types/index.d.ts +10 -0
  215. package/dist/types/index.d.ts.map +1 -0
  216. package/dist/types/index.js +26 -0
  217. package/dist/types/index.js.map +1 -0
  218. package/dist/types/policy.d.ts +128 -0
  219. package/dist/types/policy.d.ts.map +1 -0
  220. package/dist/types/policy.js +55 -0
  221. package/dist/types/policy.js.map +1 -0
  222. package/package.json +55 -0
  223. package/postcss.config.js +4 -0
  224. package/src/__tests__/attachment-selector.test.ts +559 -0
  225. package/src/__tests__/cache-breakpoints.test.ts +566 -0
  226. package/src/__tests__/codecs.test.ts +417 -0
  227. package/src/__tests__/compactor.test.ts +608 -0
  228. package/src/__tests__/context-graph.test.ts +383 -0
  229. package/src/__tests__/hash.test.ts +274 -0
  230. package/src/__tests__/integration.test.ts +866 -0
  231. package/src/__tests__/kind-order.test.ts +312 -0
  232. package/src/__tests__/phase2-integration.test.ts +253 -0
  233. package/src/__tests__/queries.test.ts +387 -0
  234. package/src/__tests__/token-estimator.test.ts +326 -0
  235. package/src/adapters/anthropic-estimator.ts +125 -0
  236. package/src/adapters/attachment-resolver.ts +295 -0
  237. package/src/adapters/attachment-selector.ts +218 -0
  238. package/src/adapters/gemini-estimator.ts +93 -0
  239. package/src/adapters/index.ts +12 -0
  240. package/src/adapters/memory-store.ts +299 -0
  241. package/src/adapters/openai-estimator.ts +105 -0
  242. package/src/adapters/summarizer.ts +250 -0
  243. package/src/adapters/token-estimator.ts +74 -0
  244. package/src/builder/context-builder.ts +467 -0
  245. package/src/builder/context-fork.ts +471 -0
  246. package/src/builder/index.ts +6 -0
  247. package/src/codecs/base.ts +36 -0
  248. package/src/codecs/conversation-history.codec.ts +108 -0
  249. package/src/codecs/index.ts +57 -0
  250. package/src/codecs/redacted-stub.codec.ts +76 -0
  251. package/src/codecs/structured-reference.codec.ts +96 -0
  252. package/src/codecs/system-rules.codec.ts +74 -0
  253. package/src/codecs/tool-output.codec.ts +109 -0
  254. package/src/codecs/tool-schema.codec.ts +87 -0
  255. package/src/codecs/unsafe-text.codec.ts +74 -0
  256. package/src/graph/context-graph.ts +205 -0
  257. package/src/graph/index.ts +8 -0
  258. package/src/graph/kind-order.ts +125 -0
  259. package/src/graph/queries.ts +306 -0
  260. package/src/graph/views.ts +255 -0
  261. package/src/index.ts +31 -0
  262. package/src/pipeline/compactor.ts +563 -0
  263. package/src/pipeline/index.ts +6 -0
  264. package/src/pipeline/summarizer.ts +76 -0
  265. package/src/policies/default-policy.ts +69 -0
  266. package/src/policies/index.ts +5 -0
  267. package/src/providers/anthropic-compiler.ts +294 -0
  268. package/src/providers/capabilities.ts +144 -0
  269. package/src/providers/gemini-compiler.ts +272 -0
  270. package/src/providers/index.ts +8 -0
  271. package/src/providers/openai-compiler.ts +191 -0
  272. package/src/types/attachment.ts +86 -0
  273. package/src/types/block.ts +84 -0
  274. package/src/types/codec.ts +68 -0
  275. package/src/types/compiled.ts +109 -0
  276. package/src/types/hash.ts +58 -0
  277. package/src/types/index.ts +10 -0
  278. package/src/types/policy.ts +194 -0
  279. package/tsconfig.json +21 -0
  280. package/vitest.config.ts +21 -0
@@ -0,0 +1,608 @@
1
+ /**
2
+ * Unit tests for Compactor: tool output pruning and deduplication.
3
+ *
4
+ * Tests:
5
+ * - Tool output pruning produces normalized shape
6
+ * - Deduplication removes identical blockHash
7
+ * - History trimming keeps recent + error messages
8
+ * - Provenance metadata is preserved
9
+ */
10
+
11
+ import { describe, it, expect } from 'vitest';
12
+ import { compactView, DEFAULT_TOOL_OUTPUT_PRUNING } from '../pipeline/compactor.js';
13
+ import type { ContextBlock } from '../types/block.js';
14
+ import type { ContextView } from '../graph/views.js';
15
+ import { heuristicTokenCount, serializeBlockForEstimation, type TokenEstimator, type TokenEstimate } from '../adapters/token-estimator.js';
16
+
17
+ // Mock token estimator for tests
18
+ class MockTokenEstimator implements TokenEstimator {
19
+ async estimate(blocks: ContextBlock<unknown>[]): Promise<TokenEstimate> {
20
+ const totalTokens = blocks.reduce((sum, block) => {
21
+ const text = serializeBlockForEstimation(block);
22
+ return sum + heuristicTokenCount(text);
23
+ }, 0);
24
+ return { tokens: totalTokens, confidence: 'low' };
25
+ }
26
+
27
+ async estimateBlock(block: ContextBlock<unknown>): Promise<TokenEstimate> {
28
+ const text = serializeBlockForEstimation(block);
29
+ return { tokens: heuristicTokenCount(text), confidence: 'low' };
30
+ }
31
+ }
32
+
33
+ describe('Compactor', () => {
34
+ describe('Deduplication', () => {
35
+ it('should remove duplicate blocks by blockHash', async () => {
36
+ const duplicateBlock: ContextBlock<string> = createBlock('hash1', 'memory', 'data');
37
+ const blocks: ContextBlock[] = [
38
+ duplicateBlock,
39
+ duplicateBlock, // Duplicate
40
+ createBlock('hash2', 'memory', 'other'),
41
+ ];
42
+
43
+ const view = createTestView(blocks);
44
+ const estimator = new MockTokenEstimator();
45
+ const result = await compactView(view, { steps: ['dedupe'] }, estimator);
46
+
47
+ expect(result.blocks).toHaveLength(2);
48
+ expect(result.removedBlocks).toHaveLength(1);
49
+ expect(result.blocks.map((b) => b.blockHash)).toEqual(['hash1', 'hash2']);
50
+ });
51
+
52
+ it('should keep first occurrence of duplicate', async () => {
53
+ const block1: ContextBlock<any> = createBlock('hash1', 'memory', 'first');
54
+ const block2: ContextBlock<any> = createBlock('hash1', 'memory', 'duplicate');
55
+
56
+ const view = createTestView([block1, block2]);
57
+ const estimator = new MockTokenEstimator();
58
+ const result = await compactView(view, { steps: ['dedupe'] }, estimator);
59
+
60
+ expect(result.blocks).toHaveLength(1);
61
+ expect(result.blocks[0].payload).toBe('first');
62
+ });
63
+
64
+ it('should handle no duplicates', async () => {
65
+ const blocks: ContextBlock[] = [
66
+ createBlock('hash1', 'memory', 'data1'),
67
+ createBlock('hash2', 'memory', 'data2'),
68
+ createBlock('hash3', 'memory', 'data3'),
69
+ ];
70
+
71
+ const view = createTestView(blocks);
72
+ const estimator = new MockTokenEstimator();
73
+ const result = await compactView(view, { steps: ['dedupe'] }, estimator);
74
+
75
+ expect(result.blocks).toHaveLength(3);
76
+ expect(result.removedBlocks).toHaveLength(0);
77
+ });
78
+
79
+ it('should report dedupe step results', async () => {
80
+ const duplicateBlock: ContextBlock<string> = createBlock('hash1', 'memory', 'data');
81
+ const blocks: ContextBlock[] = [duplicateBlock, duplicateBlock, duplicateBlock];
82
+
83
+ const view = createTestView(blocks);
84
+ const estimator = new MockTokenEstimator();
85
+ const result = await compactView(view, { steps: ['dedupe'] }, estimator);
86
+
87
+ const dedupeReport = result.report.stepReports.find((r) => r.step === 'dedupe');
88
+ expect(dedupeReport).toBeDefined();
89
+ expect(dedupeReport!.blocksRemoved).toBe(2);
90
+ expect(dedupeReport!.lossy).toBe(false);
91
+ });
92
+ });
93
+
94
+ describe('Tool Output Pruning', () => {
95
+ it('should truncate large tool outputs', async () => {
96
+ const largeOutput = 'x'.repeat(1000);
97
+ const block: ContextBlock<any> = createBlock('hash1', 'tool_output', {
98
+ toolName: 'test_tool',
99
+ output: largeOutput,
100
+ });
101
+
102
+ const view = createTestView([block]);
103
+ const estimator = new MockTokenEstimator();
104
+ const result = await compactView(view, {
105
+ steps: ['tool_output_prune'],
106
+ toolOutputPruning: {
107
+ maxRawTailChars: 100,
108
+ preserveErrorTail: true,
109
+ maxOutputsPerTool: 3,
110
+ },
111
+ }, estimator);
112
+
113
+ // Should have a replacement block
114
+ expect(result.blocks).toHaveLength(1);
115
+ const truncated = result.blocks[0].payload as any;
116
+
117
+ expect(truncated._truncated).toBe(true);
118
+ expect(truncated.output.length).toBeLessThan(largeOutput.length);
119
+ expect(truncated.output).toContain('truncated');
120
+ });
121
+
122
+ it('should preserve error outputs even if large', async () => {
123
+ const largeErrorOutput = 'error: ' + 'x'.repeat(1000);
124
+ const block: ContextBlock<any> = createBlock('hash1', 'tool_output', {
125
+ toolName: 'test_tool',
126
+ output: largeErrorOutput,
127
+ error: true,
128
+ });
129
+
130
+ const view = createTestView([block]);
131
+ const estimator = new MockTokenEstimator();
132
+ const result = await compactView(view, {
133
+ steps: ['tool_output_prune'],
134
+ toolOutputPruning: {
135
+ maxRawTailChars: 100,
136
+ preserveErrorTail: true, // Keep errors
137
+ maxOutputsPerTool: 3,
138
+ },
139
+ }, estimator);
140
+
141
+ // Should NOT be truncated because it's an error
142
+ const output = result.blocks[0].payload as any;
143
+ expect(output._truncated).toBeUndefined();
144
+ expect(output.output).toBe(largeErrorOutput);
145
+ });
146
+
147
+ it('should keep only recent N outputs per tool', async () => {
148
+ const blocks: ContextBlock<any>[] = [
149
+ createToolOutputBlock('hash1', 'tool1', 'output1', 1000),
150
+ createToolOutputBlock('hash2', 'tool1', 'output2', 2000),
151
+ createToolOutputBlock('hash3', 'tool1', 'output3', 3000),
152
+ createToolOutputBlock('hash4', 'tool1', 'output4', 4000),
153
+ createToolOutputBlock('hash5', 'tool1', 'output5', 5000),
154
+ ];
155
+
156
+ const view = createTestView(blocks);
157
+ const estimator = new MockTokenEstimator();
158
+ const result = await compactView(view, {
159
+ steps: ['tool_output_prune'],
160
+ toolOutputPruning: {
161
+ maxRawTailChars: 500,
162
+ preserveErrorTail: true,
163
+ maxOutputsPerTool: 3, // Keep last 3
164
+ },
165
+ }, estimator);
166
+
167
+ // Should keep only 3 most recent
168
+ expect(result.blocks).toHaveLength(3);
169
+ expect(result.removedBlocks).toHaveLength(2);
170
+
171
+ // Check that most recent are kept
172
+ const hashes = result.blocks.map((b) => b.blockHash);
173
+ expect(hashes).toContain('hash3');
174
+ expect(hashes).toContain('hash4');
175
+ expect(hashes).toContain('hash5');
176
+ });
177
+
178
+ it('should handle multiple tool types independently', async () => {
179
+ const blocks: ContextBlock<any>[] = [
180
+ createToolOutputBlock('hash1', 'tool1', 'output1', 1000),
181
+ createToolOutputBlock('hash2', 'tool1', 'output2', 2000),
182
+ createToolOutputBlock('hash3', 'tool2', 'output3', 3000),
183
+ createToolOutputBlock('hash4', 'tool2', 'output4', 4000),
184
+ ];
185
+
186
+ const view = createTestView(blocks);
187
+ const estimator = new MockTokenEstimator();
188
+ const result = await compactView(view, {
189
+ steps: ['tool_output_prune'],
190
+ toolOutputPruning: {
191
+ maxRawTailChars: 500,
192
+ preserveErrorTail: true,
193
+ maxOutputsPerTool: 1, // Keep 1 per tool
194
+ },
195
+ }, estimator);
196
+
197
+ // Should keep 1 from each tool (most recent)
198
+ expect(result.blocks).toHaveLength(2);
199
+ expect(result.blocks.map((b) => b.blockHash).sort()).toEqual(['hash2', 'hash4']);
200
+ });
201
+
202
+ it('should preserve non-tool_output blocks', async () => {
203
+ const blocks: ContextBlock<any>[] = [
204
+ createBlock('hash1', 'pinned', 'system'),
205
+ createToolOutputBlock('hash2', 'tool1', 'output1', 1000),
206
+ createToolOutputBlock('hash3', 'tool1', 'output2', 2000),
207
+ createBlock('hash4', 'memory', 'memory'),
208
+ ];
209
+
210
+ const view = createTestView(blocks);
211
+ const estimator = new MockTokenEstimator();
212
+ const result = await compactView(view, {
213
+ steps: ['tool_output_prune'],
214
+ toolOutputPruning: {
215
+ maxRawTailChars: 500,
216
+ preserveErrorTail: true,
217
+ maxOutputsPerTool: 1,
218
+ },
219
+ }, estimator);
220
+
221
+ // Should keep all non-tool_output blocks + 1 tool output
222
+ expect(result.blocks).toHaveLength(3);
223
+ const kinds = result.blocks.map((b) => b.meta.kind);
224
+ expect(kinds).toContain('pinned');
225
+ expect(kinds).toContain('memory');
226
+ expect(kinds).toContain('tool_output');
227
+ });
228
+
229
+ it('should report pruning step results', async () => {
230
+ const largeOutput = 'x'.repeat(1000);
231
+ const block: ContextBlock<any> = createBlock('hash1', 'tool_output', {
232
+ toolName: 'test_tool',
233
+ output: largeOutput,
234
+ });
235
+
236
+ const view = createTestView([block]);
237
+ const estimator = new MockTokenEstimator();
238
+ const result = await compactView(view, {
239
+ steps: ['tool_output_prune'],
240
+ }, estimator);
241
+
242
+ const pruneReport = result.report.stepReports.find(
243
+ (r) => r.step === 'tool_output_prune'
244
+ );
245
+ expect(pruneReport).toBeDefined();
246
+ expect(pruneReport!.blocksReplaced).toBeGreaterThan(0);
247
+ expect(pruneReport!.lossy).toBe(true);
248
+ });
249
+
250
+ it('should use default config when not provided', async () => {
251
+ const largeOutput = 'x'.repeat(1000);
252
+ const block: ContextBlock<any> = createBlock('hash1', 'tool_output', {
253
+ toolName: 'test_tool',
254
+ output: largeOutput,
255
+ });
256
+
257
+ const view = createTestView([block]);
258
+ const estimator = new MockTokenEstimator();
259
+ const result = await compactView(view, {
260
+ steps: ['tool_output_prune'],
261
+ // No toolOutputPruning config - should use DEFAULT_TOOL_OUTPUT_PRUNING
262
+ }, estimator);
263
+
264
+ expect(result.blocks).toHaveLength(1);
265
+ const truncated = result.blocks[0].payload as any;
266
+ expect(truncated._truncated).toBe(true);
267
+ // Default maxRawTailChars is 500
268
+ expect(truncated.output.length).toBeLessThan(largeOutput.length);
269
+ });
270
+ });
271
+
272
+ describe('History Trimming', () => {
273
+ it('should keep recent N messages', async () => {
274
+ const blocks: ContextBlock<any>[] = [
275
+ createHistoryBlock('hash1', 1000),
276
+ createHistoryBlock('hash2', 2000),
277
+ createHistoryBlock('hash3', 3000),
278
+ createHistoryBlock('hash4', 4000),
279
+ createHistoryBlock('hash5', 5000),
280
+ ];
281
+
282
+ const view = createTestView(blocks);
283
+ const estimator = new MockTokenEstimator();
284
+ const result = await compactView(view, {
285
+ steps: ['history_trim'],
286
+ historyTrim: {
287
+ keepRecentMessages: 2,
288
+ keepErrorMessages: false,
289
+ },
290
+ }, estimator);
291
+
292
+ // Should keep 2 most recent
293
+ expect(result.blocks).toHaveLength(2);
294
+ expect(result.blocks.map((b) => b.blockHash)).toEqual(['hash4', 'hash5']);
295
+ });
296
+
297
+ it('should preserve error messages', async () => {
298
+ const blocks: ContextBlock<any>[] = [
299
+ createHistoryBlock('hash1', 1000, { hasError: true }),
300
+ createHistoryBlock('hash2', 2000, { hasError: false }),
301
+ createHistoryBlock('hash3', 3000, { hasError: false }),
302
+ createHistoryBlock('hash4', 4000, { hasError: false }),
303
+ ];
304
+
305
+ const view = createTestView(blocks);
306
+ const estimator = new MockTokenEstimator();
307
+ const result = await compactView(view, {
308
+ steps: ['history_trim'],
309
+ historyTrim: {
310
+ keepRecentMessages: 2, // Keep hash3, hash4
311
+ keepErrorMessages: true, // Also keep hash1 (error)
312
+ },
313
+ }, estimator);
314
+
315
+ // Should keep 2 recent + 1 error
316
+ expect(result.blocks).toHaveLength(3);
317
+ const hashes = result.blocks.map((b) => b.blockHash);
318
+ expect(hashes).toContain('hash1'); // Error message
319
+ expect(hashes).toContain('hash3'); // Recent
320
+ expect(hashes).toContain('hash4'); // Recent
321
+ });
322
+
323
+ it('should not remove error messages when keepErrorMessages is false', async () => {
324
+ const blocks: ContextBlock<any>[] = [
325
+ createHistoryBlock('hash1', 1000, { hasError: true }),
326
+ createHistoryBlock('hash2', 2000, { hasError: false }),
327
+ createHistoryBlock('hash3', 3000, { hasError: false }),
328
+ ];
329
+
330
+ const view = createTestView(blocks);
331
+ const estimator = new MockTokenEstimator();
332
+ const result = await compactView(view, {
333
+ steps: ['history_trim'],
334
+ historyTrim: {
335
+ keepRecentMessages: 1,
336
+ keepErrorMessages: false,
337
+ },
338
+ }, estimator);
339
+
340
+ // Should only keep 1 most recent (hash3)
341
+ expect(result.blocks).toHaveLength(1);
342
+ expect(result.blocks[0].blockHash).toBe('hash3');
343
+ });
344
+
345
+ it('should preserve non-history blocks', async () => {
346
+ const blocks: ContextBlock<any>[] = [
347
+ createBlock('hash1', 'pinned', 'system'),
348
+ createHistoryBlock('hash2', 1000),
349
+ createHistoryBlock('hash3', 2000),
350
+ createBlock('hash4', 'memory', 'memory'),
351
+ ];
352
+
353
+ const view = createTestView(blocks);
354
+ const estimator = new MockTokenEstimator();
355
+ const result = await compactView(view, {
356
+ steps: ['history_trim'],
357
+ historyTrim: {
358
+ keepRecentMessages: 1,
359
+ keepErrorMessages: false,
360
+ },
361
+ }, estimator);
362
+
363
+ // Should keep all non-history + 1 history
364
+ expect(result.blocks).toHaveLength(3);
365
+ const kinds = result.blocks.map((b) => b.meta.kind);
366
+ expect(kinds).toContain('pinned');
367
+ expect(kinds).toContain('memory');
368
+ expect(kinds).toContain('history');
369
+ });
370
+
371
+ it('should handle no history blocks', async () => {
372
+ const blocks: ContextBlock<any>[] = [
373
+ createBlock('hash1', 'pinned', 'system'),
374
+ createBlock('hash2', 'memory', 'memory'),
375
+ ];
376
+
377
+ const view = createTestView(blocks);
378
+ const estimator = new MockTokenEstimator();
379
+ const result = await compactView(view, {
380
+ steps: ['history_trim'],
381
+ }, estimator);
382
+
383
+ expect(result.blocks).toHaveLength(2);
384
+ expect(result.removedBlocks).toHaveLength(0);
385
+ });
386
+
387
+ it('should report trimming step results', async () => {
388
+ const blocks: ContextBlock<any>[] = [
389
+ createHistoryBlock('hash1', 1000),
390
+ createHistoryBlock('hash2', 2000),
391
+ createHistoryBlock('hash3', 3000),
392
+ ];
393
+
394
+ const view = createTestView(blocks);
395
+ const estimator = new MockTokenEstimator();
396
+ const result = await compactView(view, {
397
+ steps: ['history_trim'],
398
+ historyTrim: {
399
+ keepRecentMessages: 1,
400
+ keepErrorMessages: false,
401
+ },
402
+ }, estimator);
403
+
404
+ const trimReport = result.report.stepReports.find(
405
+ (r) => r.step === 'history_trim'
406
+ );
407
+ expect(trimReport).toBeDefined();
408
+ expect(trimReport!.blocksRemoved).toBe(2);
409
+ expect(trimReport!.lossy).toBe(true);
410
+ });
411
+ });
412
+
413
+ describe('Multiple Steps', () => {
414
+ it('should apply steps in order', async () => {
415
+ const duplicate: ContextBlock<string> = createBlock('hash1', 'memory', 'data');
416
+ const blocks: ContextBlock<any>[] = [
417
+ duplicate,
418
+ duplicate, // Will be removed by dedupe
419
+ createToolOutputBlock('hash2', 'tool1', 'x'.repeat(1000), 1000), // Will be truncated
420
+ createHistoryBlock('hash3', 1000), // Will be trimmed
421
+ createHistoryBlock('hash4', 2000),
422
+ ];
423
+
424
+ const view = createTestView(blocks);
425
+ const estimator = new MockTokenEstimator();
426
+ const result = await compactView(view, {
427
+ steps: ['dedupe', 'tool_output_prune', 'history_trim'],
428
+ toolOutputPruning: {
429
+ maxRawTailChars: 100,
430
+ preserveErrorTail: true,
431
+ maxOutputsPerTool: 3,
432
+ },
433
+ historyTrim: {
434
+ keepRecentMessages: 1,
435
+ keepErrorMessages: false,
436
+ },
437
+ }, estimator);
438
+
439
+ expect(result.report.stepsApplied).toEqual([
440
+ 'dedupe',
441
+ 'tool_output_prune',
442
+ 'history_trim',
443
+ ]);
444
+ expect(result.report.stepReports).toHaveLength(3);
445
+ });
446
+
447
+ it('should accumulate removed blocks', async () => {
448
+ const duplicate: ContextBlock<string> = createBlock('hash1', 'memory', 'data');
449
+ const blocks: ContextBlock<any>[] = [
450
+ duplicate,
451
+ duplicate,
452
+ createHistoryBlock('hash2', 1000),
453
+ createHistoryBlock('hash3', 2000),
454
+ ];
455
+
456
+ const view = createTestView(blocks);
457
+ const estimator = new MockTokenEstimator();
458
+ const result = await compactView(view, {
459
+ steps: ['dedupe', 'history_trim'],
460
+ historyTrim: {
461
+ keepRecentMessages: 1,
462
+ keepErrorMessages: false,
463
+ },
464
+ }, estimator);
465
+
466
+ // 1 removed by dedupe + 1 removed by trim
467
+ expect(result.removedBlocks).toHaveLength(2);
468
+ });
469
+ });
470
+
471
+ describe('Provenance Tracking', () => {
472
+ it('should add provenance to truncated tool outputs', async () => {
473
+ const largeOutput = 'x'.repeat(1000);
474
+ const block: ContextBlock<any> = {
475
+ blockHash: 'original-hash',
476
+ meta: {
477
+ kind: 'tool_output',
478
+ sensitivity: 'public',
479
+ codecId: 'tool-output',
480
+ codecVersion: '1.0.0',
481
+ createdAt: Date.now(),
482
+ source: 'test-source',
483
+ },
484
+ payload: {
485
+ toolName: 'test_tool',
486
+ output: largeOutput,
487
+ },
488
+ };
489
+
490
+ const view = createTestView([block]);
491
+ const estimator = new MockTokenEstimator();
492
+ const result = await compactView(view, {
493
+ steps: ['tool_output_prune'],
494
+ toolOutputPruning: {
495
+ maxRawTailChars: 100,
496
+ preserveErrorTail: true,
497
+ maxOutputsPerTool: 3,
498
+ },
499
+ }, estimator);
500
+
501
+ const replacement = result.blocks[0];
502
+ expect(replacement.meta.source).toContain('compacted');
503
+ expect(replacement.meta.tags).toContain('compacted:tool_output_prune');
504
+ });
505
+ });
506
+
507
+ describe('Compaction Report', () => {
508
+ it('should generate complete report', async () => {
509
+ const blocks: ContextBlock<any>[] = [
510
+ createBlock('hash1', 'memory', 'data'),
511
+ createBlock('hash1', 'memory', 'data'), // Duplicate
512
+ ];
513
+
514
+ const view = createTestView(blocks);
515
+ const estimator = new MockTokenEstimator();
516
+ const result = await compactView(view, {
517
+ steps: ['dedupe'],
518
+ }, estimator);
519
+
520
+ expect(result.report).toHaveProperty('beforeTokens');
521
+ expect(result.report).toHaveProperty('afterTokens');
522
+ expect(result.report).toHaveProperty('savedTokens');
523
+ expect(result.report.stepsApplied).toEqual(['dedupe']);
524
+ expect(result.report.stepReports).toHaveLength(1);
525
+ });
526
+ });
527
+ });
528
+
529
+ // Test helpers
530
+
531
+ function createBlock(
532
+ hash: string,
533
+ kind: 'pinned' | 'memory' | 'history' | 'tool_output',
534
+ payload: any
535
+ ): ContextBlock<any> {
536
+ return {
537
+ blockHash: hash,
538
+ meta: {
539
+ kind,
540
+ sensitivity: 'public',
541
+ codecId: 'test',
542
+ codecVersion: '1.0.0',
543
+ createdAt: Date.now(),
544
+ },
545
+ payload,
546
+ };
547
+ }
548
+
549
+ function createToolOutputBlock(
550
+ hash: string,
551
+ codecId: string,
552
+ output: string,
553
+ createdAt: number
554
+ ): ContextBlock<any> {
555
+ return {
556
+ blockHash: hash,
557
+ meta: {
558
+ kind: 'tool_output',
559
+ sensitivity: 'public',
560
+ codecId,
561
+ codecVersion: '1.0.0',
562
+ createdAt,
563
+ },
564
+ payload: {
565
+ toolName: codecId,
566
+ output,
567
+ },
568
+ };
569
+ }
570
+
571
+ function createHistoryBlock(
572
+ hash: string,
573
+ createdAt: number,
574
+ options: { hasError?: boolean } = {}
575
+ ): ContextBlock<any> {
576
+ return {
577
+ blockHash: hash,
578
+ meta: {
579
+ kind: 'history',
580
+ sensitivity: 'public',
581
+ codecId: 'conversation-history',
582
+ codecVersion: '1.0.0',
583
+ createdAt,
584
+ },
585
+ payload: {
586
+ messages: [
587
+ {
588
+ role: 'user',
589
+ content: 'Test',
590
+ ...(options.hasError && { error: 'Test error' }),
591
+ },
592
+ ],
593
+ },
594
+ };
595
+ }
596
+
597
+ function createTestView(blocks: ContextBlock[]): ContextView {
598
+ return {
599
+ blocks,
600
+ tokenEstimate: {
601
+ tokens: blocks.length * 100, // Simplified estimate
602
+ confidence: 'low',
603
+ truncated: false,
604
+ },
605
+ stablePrefixHash: 'test-hash',
606
+ createdAt: Date.now() / 1000,
607
+ };
608
+ }