@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.
- package/README.md +270 -0
- package/dist/__tests__/attachment-selector.test.d.ts +11 -0
- package/dist/__tests__/attachment-selector.test.d.ts.map +1 -0
- package/dist/__tests__/attachment-selector.test.js +449 -0
- package/dist/__tests__/attachment-selector.test.js.map +1 -0
- package/dist/__tests__/cache-breakpoints.test.d.ts +11 -0
- package/dist/__tests__/cache-breakpoints.test.d.ts.map +1 -0
- package/dist/__tests__/cache-breakpoints.test.js +398 -0
- package/dist/__tests__/cache-breakpoints.test.js.map +1 -0
- package/dist/__tests__/codecs.test.d.ts +7 -0
- package/dist/__tests__/codecs.test.d.ts.map +1 -0
- package/dist/__tests__/codecs.test.js +331 -0
- package/dist/__tests__/codecs.test.js.map +1 -0
- package/dist/__tests__/compactor.test.d.ts +11 -0
- package/dist/__tests__/compactor.test.d.ts.map +1 -0
- package/dist/__tests__/compactor.test.js +519 -0
- package/dist/__tests__/compactor.test.js.map +1 -0
- package/dist/__tests__/context-graph.test.d.ts +7 -0
- package/dist/__tests__/context-graph.test.d.ts.map +1 -0
- package/dist/__tests__/context-graph.test.js +262 -0
- package/dist/__tests__/context-graph.test.js.map +1 -0
- package/dist/__tests__/hash.test.d.ts +7 -0
- package/dist/__tests__/hash.test.d.ts.map +1 -0
- package/dist/__tests__/hash.test.js +228 -0
- package/dist/__tests__/hash.test.js.map +1 -0
- package/dist/__tests__/integration.test.d.ts +15 -0
- package/dist/__tests__/integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration.test.js +728 -0
- package/dist/__tests__/integration.test.js.map +1 -0
- package/dist/__tests__/kind-order.test.d.ts +7 -0
- package/dist/__tests__/kind-order.test.d.ts.map +1 -0
- package/dist/__tests__/kind-order.test.js +243 -0
- package/dist/__tests__/kind-order.test.js.map +1 -0
- package/dist/__tests__/phase2-integration.test.d.ts +5 -0
- package/dist/__tests__/phase2-integration.test.d.ts.map +1 -0
- package/dist/__tests__/phase2-integration.test.js +222 -0
- package/dist/__tests__/phase2-integration.test.js.map +1 -0
- package/dist/__tests__/queries.test.d.ts +7 -0
- package/dist/__tests__/queries.test.d.ts.map +1 -0
- package/dist/__tests__/queries.test.js +254 -0
- package/dist/__tests__/queries.test.js.map +1 -0
- package/dist/__tests__/token-estimator.test.d.ts +7 -0
- package/dist/__tests__/token-estimator.test.d.ts.map +1 -0
- package/dist/__tests__/token-estimator.test.js +267 -0
- package/dist/__tests__/token-estimator.test.js.map +1 -0
- package/dist/adapters/anthropic-estimator.d.ts +38 -0
- package/dist/adapters/anthropic-estimator.d.ts.map +1 -0
- package/dist/adapters/anthropic-estimator.js +108 -0
- package/dist/adapters/anthropic-estimator.js.map +1 -0
- package/dist/adapters/attachment-resolver.d.ts +96 -0
- package/dist/adapters/attachment-resolver.d.ts.map +1 -0
- package/dist/adapters/attachment-resolver.js +176 -0
- package/dist/adapters/attachment-resolver.js.map +1 -0
- package/dist/adapters/attachment-selector.d.ts +59 -0
- package/dist/adapters/attachment-selector.d.ts.map +1 -0
- package/dist/adapters/attachment-selector.js +163 -0
- package/dist/adapters/attachment-selector.js.map +1 -0
- package/dist/adapters/gemini-estimator.d.ts +27 -0
- package/dist/adapters/gemini-estimator.d.ts.map +1 -0
- package/dist/adapters/gemini-estimator.js +80 -0
- package/dist/adapters/gemini-estimator.js.map +1 -0
- package/dist/adapters/index.d.ts +12 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +28 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/memory-store.d.ts +139 -0
- package/dist/adapters/memory-store.d.ts.map +1 -0
- package/dist/adapters/memory-store.js +187 -0
- package/dist/adapters/memory-store.js.map +1 -0
- package/dist/adapters/openai-estimator.d.ts +35 -0
- package/dist/adapters/openai-estimator.d.ts.map +1 -0
- package/dist/adapters/openai-estimator.js +89 -0
- package/dist/adapters/openai-estimator.js.map +1 -0
- package/dist/adapters/summarizer.d.ts +121 -0
- package/dist/adapters/summarizer.d.ts.map +1 -0
- package/dist/adapters/summarizer.js +121 -0
- package/dist/adapters/summarizer.js.map +1 -0
- package/dist/adapters/token-estimator.d.ts +63 -0
- package/dist/adapters/token-estimator.d.ts.map +1 -0
- package/dist/adapters/token-estimator.js +37 -0
- package/dist/adapters/token-estimator.js.map +1 -0
- package/dist/builder/context-builder.d.ts +186 -0
- package/dist/builder/context-builder.d.ts.map +1 -0
- package/dist/builder/context-builder.js +305 -0
- package/dist/builder/context-builder.js.map +1 -0
- package/dist/builder/context-fork.d.ts +166 -0
- package/dist/builder/context-fork.d.ts.map +1 -0
- package/dist/builder/context-fork.js +282 -0
- package/dist/builder/context-fork.js.map +1 -0
- package/dist/builder/index.d.ts +6 -0
- package/dist/builder/index.d.ts.map +1 -0
- package/dist/builder/index.js +22 -0
- package/dist/builder/index.js.map +1 -0
- package/dist/codecs/base.d.ts +18 -0
- package/dist/codecs/base.d.ts.map +1 -0
- package/dist/codecs/base.js +39 -0
- package/dist/codecs/base.js.map +1 -0
- package/dist/codecs/conversation-history.codec.d.ts +81 -0
- package/dist/codecs/conversation-history.codec.d.ts.map +1 -0
- package/dist/codecs/conversation-history.codec.js +89 -0
- package/dist/codecs/conversation-history.codec.js.map +1 -0
- package/dist/codecs/index.d.ts +31 -0
- package/dist/codecs/index.d.ts.map +1 -0
- package/dist/codecs/index.js +71 -0
- package/dist/codecs/index.js.map +1 -0
- package/dist/codecs/redacted-stub.codec.d.ts +32 -0
- package/dist/codecs/redacted-stub.codec.d.ts.map +1 -0
- package/dist/codecs/redacted-stub.codec.js +64 -0
- package/dist/codecs/redacted-stub.codec.js.map +1 -0
- package/dist/codecs/structured-reference.codec.d.ts +40 -0
- package/dist/codecs/structured-reference.codec.d.ts.map +1 -0
- package/dist/codecs/structured-reference.codec.js +81 -0
- package/dist/codecs/structured-reference.codec.js.map +1 -0
- package/dist/codecs/system-rules.codec.d.ts +32 -0
- package/dist/codecs/system-rules.codec.d.ts.map +1 -0
- package/dist/codecs/system-rules.codec.js +62 -0
- package/dist/codecs/system-rules.codec.js.map +1 -0
- package/dist/codecs/tool-output.codec.d.ts +66 -0
- package/dist/codecs/tool-output.codec.d.ts.map +1 -0
- package/dist/codecs/tool-output.codec.js +95 -0
- package/dist/codecs/tool-output.codec.js.map +1 -0
- package/dist/codecs/tool-schema.codec.d.ts +36 -0
- package/dist/codecs/tool-schema.codec.d.ts.map +1 -0
- package/dist/codecs/tool-schema.codec.js +74 -0
- package/dist/codecs/tool-schema.codec.js.map +1 -0
- package/dist/codecs/unsafe-text.codec.d.ts +28 -0
- package/dist/codecs/unsafe-text.codec.d.ts.map +1 -0
- package/dist/codecs/unsafe-text.codec.js +63 -0
- package/dist/codecs/unsafe-text.codec.js.map +1 -0
- package/dist/graph/context-graph.d.ts +121 -0
- package/dist/graph/context-graph.d.ts.map +1 -0
- package/dist/graph/context-graph.js +166 -0
- package/dist/graph/context-graph.js.map +1 -0
- package/dist/graph/index.d.ts +8 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +24 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/kind-order.d.ts +60 -0
- package/dist/graph/kind-order.d.ts.map +1 -0
- package/dist/graph/kind-order.js +113 -0
- package/dist/graph/kind-order.js.map +1 -0
- package/dist/graph/queries.d.ts +68 -0
- package/dist/graph/queries.d.ts.map +1 -0
- package/dist/graph/queries.js +240 -0
- package/dist/graph/queries.js.map +1 -0
- package/dist/graph/views.d.ts +90 -0
- package/dist/graph/views.d.ts.map +1 -0
- package/dist/graph/views.js +173 -0
- package/dist/graph/views.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/pipeline/compactor.d.ts +128 -0
- package/dist/pipeline/compactor.d.ts.map +1 -0
- package/dist/pipeline/compactor.js +346 -0
- package/dist/pipeline/compactor.js.map +1 -0
- package/dist/pipeline/index.d.ts +6 -0
- package/dist/pipeline/index.d.ts.map +1 -0
- package/dist/pipeline/index.js +22 -0
- package/dist/pipeline/index.js.map +1 -0
- package/dist/pipeline/summarizer.d.ts +18 -0
- package/dist/pipeline/summarizer.d.ts.map +1 -0
- package/dist/pipeline/summarizer.js +68 -0
- package/dist/pipeline/summarizer.js.map +1 -0
- package/dist/policies/default-policy.d.ts +29 -0
- package/dist/policies/default-policy.d.ts.map +1 -0
- package/dist/policies/default-policy.js +58 -0
- package/dist/policies/default-policy.js.map +1 -0
- package/dist/policies/index.d.ts +5 -0
- package/dist/policies/index.d.ts.map +1 -0
- package/dist/policies/index.js +21 -0
- package/dist/policies/index.js.map +1 -0
- package/dist/providers/anthropic-compiler.d.ts +58 -0
- package/dist/providers/anthropic-compiler.d.ts.map +1 -0
- package/dist/providers/anthropic-compiler.js +182 -0
- package/dist/providers/anthropic-compiler.js.map +1 -0
- package/dist/providers/capabilities.d.ts +54 -0
- package/dist/providers/capabilities.d.ts.map +1 -0
- package/dist/providers/capabilities.js +87 -0
- package/dist/providers/capabilities.js.map +1 -0
- package/dist/providers/gemini-compiler.d.ts +51 -0
- package/dist/providers/gemini-compiler.d.ts.map +1 -0
- package/dist/providers/gemini-compiler.js +206 -0
- package/dist/providers/gemini-compiler.js.map +1 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +24 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/openai-compiler.d.ts +46 -0
- package/dist/providers/openai-compiler.d.ts.map +1 -0
- package/dist/providers/openai-compiler.js +149 -0
- package/dist/providers/openai-compiler.js.map +1 -0
- package/dist/types/attachment.d.ts +62 -0
- package/dist/types/attachment.d.ts.map +1 -0
- package/dist/types/attachment.js +6 -0
- package/dist/types/attachment.js.map +1 -0
- package/dist/types/block.d.ts +61 -0
- package/dist/types/block.d.ts.map +1 -0
- package/dist/types/block.js +8 -0
- package/dist/types/block.js.map +1 -0
- package/dist/types/codec.d.ts +58 -0
- package/dist/types/codec.d.ts.map +1 -0
- package/dist/types/codec.js +6 -0
- package/dist/types/codec.js.map +1 -0
- package/dist/types/compiled.d.ts +91 -0
- package/dist/types/compiled.d.ts.map +1 -0
- package/dist/types/compiled.js +6 -0
- package/dist/types/compiled.js.map +1 -0
- package/dist/types/hash.d.ts +24 -0
- package/dist/types/hash.d.ts.map +1 -0
- package/dist/types/hash.js +49 -0
- package/dist/types/hash.js.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +26 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/policy.d.ts +128 -0
- package/dist/types/policy.d.ts.map +1 -0
- package/dist/types/policy.js +55 -0
- package/dist/types/policy.js.map +1 -0
- package/package.json +55 -0
- package/postcss.config.js +4 -0
- package/src/__tests__/attachment-selector.test.ts +559 -0
- package/src/__tests__/cache-breakpoints.test.ts +566 -0
- package/src/__tests__/codecs.test.ts +417 -0
- package/src/__tests__/compactor.test.ts +608 -0
- package/src/__tests__/context-graph.test.ts +383 -0
- package/src/__tests__/hash.test.ts +274 -0
- package/src/__tests__/integration.test.ts +866 -0
- package/src/__tests__/kind-order.test.ts +312 -0
- package/src/__tests__/phase2-integration.test.ts +253 -0
- package/src/__tests__/queries.test.ts +387 -0
- package/src/__tests__/token-estimator.test.ts +326 -0
- package/src/adapters/anthropic-estimator.ts +125 -0
- package/src/adapters/attachment-resolver.ts +295 -0
- package/src/adapters/attachment-selector.ts +218 -0
- package/src/adapters/gemini-estimator.ts +93 -0
- package/src/adapters/index.ts +12 -0
- package/src/adapters/memory-store.ts +299 -0
- package/src/adapters/openai-estimator.ts +105 -0
- package/src/adapters/summarizer.ts +250 -0
- package/src/adapters/token-estimator.ts +74 -0
- package/src/builder/context-builder.ts +467 -0
- package/src/builder/context-fork.ts +471 -0
- package/src/builder/index.ts +6 -0
- package/src/codecs/base.ts +36 -0
- package/src/codecs/conversation-history.codec.ts +108 -0
- package/src/codecs/index.ts +57 -0
- package/src/codecs/redacted-stub.codec.ts +76 -0
- package/src/codecs/structured-reference.codec.ts +96 -0
- package/src/codecs/system-rules.codec.ts +74 -0
- package/src/codecs/tool-output.codec.ts +109 -0
- package/src/codecs/tool-schema.codec.ts +87 -0
- package/src/codecs/unsafe-text.codec.ts +74 -0
- package/src/graph/context-graph.ts +205 -0
- package/src/graph/index.ts +8 -0
- package/src/graph/kind-order.ts +125 -0
- package/src/graph/queries.ts +306 -0
- package/src/graph/views.ts +255 -0
- package/src/index.ts +31 -0
- package/src/pipeline/compactor.ts +563 -0
- package/src/pipeline/index.ts +6 -0
- package/src/pipeline/summarizer.ts +76 -0
- package/src/policies/default-policy.ts +69 -0
- package/src/policies/index.ts +5 -0
- package/src/providers/anthropic-compiler.ts +294 -0
- package/src/providers/capabilities.ts +144 -0
- package/src/providers/gemini-compiler.ts +272 -0
- package/src/providers/index.ts +8 -0
- package/src/providers/openai-compiler.ts +191 -0
- package/src/types/attachment.ts +86 -0
- package/src/types/block.ts +84 -0
- package/src/types/codec.ts +68 -0
- package/src/types/compiled.ts +109 -0
- package/src/types/hash.ts +58 -0
- package/src/types/index.ts +10 -0
- package/src/types/policy.ts +194 -0
- package/tsconfig.json +21 -0
- 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
|
+
}
|