@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,563 @@
1
+ /**
2
+ * Compactor: View-based context compaction with provenance tracking.
3
+ *
4
+ * Responsibilities:
5
+ * - Prune tool outputs (keep errors, truncate large outputs)
6
+ * - Deduplicate identical blocks
7
+ * - Trim conversation history (keep recent + errors)
8
+ * - Summarize history (optional, needs Summarizer)
9
+ *
10
+ * IMPORTANT: View-based compaction never mutates the input graph.
11
+ * Creates replacement blocks with full provenance (derivedFrom, method, version).
12
+ */
13
+
14
+ import type { ContextBlock, BlockRef, BlockKind } from '../types/block.js';
15
+ import type { ContextView } from '../graph/views.js';
16
+ import { computeBlockHash } from '../types/hash.js';
17
+ import type { TokenEstimator } from '../adapters/token-estimator.js';
18
+
19
+ /**
20
+ * Compaction step identifier.
21
+ */
22
+ export type CompactionStep =
23
+ | 'dedupe' // Remove duplicate blocks
24
+ | 'tool_output_prune' // Prune large tool outputs
25
+ | 'history_trim' // Trim conversation history
26
+ | 'summarize_history'; // Summarize old history
27
+
28
+ /**
29
+ * History compaction summarizer interface.
30
+ * Implementations use LLM to condense old conversation history.
31
+ *
32
+ * Note: This is different from the structured Summarizer in adapters/summarizer.ts
33
+ * which is designed for schema-validated outputs with provenance tracking.
34
+ */
35
+ export interface HistorySummarizer {
36
+ /**
37
+ * Summarize a list of history blocks into a single summary block.
38
+ *
39
+ * @param blocks - History blocks to summarize
40
+ * @param targetTokens - Target token count for summary
41
+ * @returns Summary block replacing the input blocks
42
+ */
43
+ summarize(
44
+ blocks: ContextBlock[],
45
+ targetTokens: number
46
+ ): Promise<ContextBlock>;
47
+ }
48
+
49
+ /**
50
+ * Tool output pruning configuration.
51
+ */
52
+ export interface ToolOutputPruningConfig {
53
+ /** Maximum raw tail characters to keep (default: 500) */
54
+ maxRawTailChars: number;
55
+
56
+ /** Preserve error tail even if large (default: true) */
57
+ preserveErrorTail: boolean;
58
+
59
+ /** Maximum outputs per tool type (default: 3) */
60
+ maxOutputsPerTool: number;
61
+ }
62
+
63
+ /**
64
+ * Compaction configuration for the pipeline.
65
+ */
66
+ export interface PipelineCompactionConfig {
67
+ /** Compaction steps to apply (in order) */
68
+ steps: CompactionStep[];
69
+
70
+ /** Tool output pruning config */
71
+ toolOutputPruning?: ToolOutputPruningConfig;
72
+
73
+ /** History trim config */
74
+ historyTrim?: {
75
+ /** Keep most recent N messages */
76
+ keepRecentMessages: number;
77
+
78
+ /** Always keep messages with errors */
79
+ keepErrorMessages: boolean;
80
+ };
81
+
82
+ /** Summarization config (optional, requires HistorySummarizer) */
83
+ summarization?: {
84
+ /** Minimum messages to trigger summarization */
85
+ minMessages: number;
86
+
87
+ /** Summarizer to use for history compaction */
88
+ summarizer?: HistorySummarizer;
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Compaction result.
94
+ */
95
+ export interface CompactionResult {
96
+ /** Compacted blocks (includes replacements) */
97
+ blocks: ContextBlock[];
98
+
99
+ /** Blocks that were removed */
100
+ removedBlocks: ContextBlock[];
101
+
102
+ /** Compaction report */
103
+ report: CompactionReport;
104
+ }
105
+
106
+ /**
107
+ * Compaction report with token savings and lossiness flags.
108
+ */
109
+ export interface CompactionReport {
110
+ /** Tokens before compaction */
111
+ beforeTokens: number;
112
+
113
+ /** Tokens after compaction */
114
+ afterTokens: number;
115
+
116
+ /** Token savings */
117
+ savedTokens: number;
118
+
119
+ /** Compaction steps applied */
120
+ stepsApplied: CompactionStep[];
121
+
122
+ /** Step-specific reports */
123
+ stepReports: StepReport[];
124
+ }
125
+
126
+ /**
127
+ * Per-step compaction report.
128
+ */
129
+ export interface StepReport {
130
+ /** Step identifier */
131
+ step: CompactionStep;
132
+
133
+ /** Blocks removed */
134
+ blocksRemoved: number;
135
+
136
+ /** Blocks replaced */
137
+ blocksReplaced: number;
138
+
139
+ /** Tokens saved */
140
+ tokensSaved: number;
141
+
142
+ /** Lossy compaction (information lost) */
143
+ lossy: boolean;
144
+
145
+ /** Human-readable description */
146
+ description: string;
147
+ }
148
+
149
+ /**
150
+ * Default tool output pruning configuration.
151
+ */
152
+ export const DEFAULT_TOOL_OUTPUT_PRUNING: ToolOutputPruningConfig = {
153
+ maxRawTailChars: 500,
154
+ preserveErrorTail: true,
155
+ maxOutputsPerTool: 3,
156
+ };
157
+
158
+ /**
159
+ * Compact a context view.
160
+ * Returns a new set of blocks with replacements tracked via provenance.
161
+ *
162
+ * @param view - Context view to compact
163
+ * @param config - Compaction configuration
164
+ * @param estimator - Token estimator for calculating savings
165
+ * @returns Compaction result
166
+ */
167
+ export async function compactView(
168
+ view: ContextView,
169
+ config: PipelineCompactionConfig,
170
+ estimator: TokenEstimator
171
+ ): Promise<CompactionResult> {
172
+ let blocks = [...view.blocks];
173
+ const removedBlocks: ContextBlock[] = [];
174
+ const stepReports: StepReport[] = [];
175
+
176
+ // Apply compaction steps in order
177
+ for (const step of config.steps) {
178
+ const beforeCount = blocks.length;
179
+ const beforeTokens = view.tokenEstimate?.tokens ?? 0;
180
+
181
+ switch (step) {
182
+ case 'dedupe': {
183
+ const result = deduplicateBlocks(blocks);
184
+
185
+ // Calculate token savings from removed duplicates
186
+ const removedEstimate = result.removed.length > 0
187
+ ? await estimator.estimate(result.removed)
188
+ : { tokens: 0 };
189
+
190
+ blocks = result.blocks;
191
+ removedBlocks.push(...result.removed);
192
+
193
+ stepReports.push({
194
+ step,
195
+ blocksRemoved: result.removed.length,
196
+ blocksReplaced: 0,
197
+ tokensSaved: removedEstimate.tokens,
198
+ lossy: false,
199
+ description: `Removed ${result.removed.length} duplicate blocks`,
200
+ });
201
+ break;
202
+ }
203
+
204
+ case 'tool_output_prune': {
205
+ const pruningConfig = config.toolOutputPruning ?? DEFAULT_TOOL_OUTPUT_PRUNING;
206
+
207
+ // Estimate tokens before pruning
208
+ const toolOutputBlocks = blocks.filter((b) => b.meta.kind === 'tool_output');
209
+ const beforeEstimate = toolOutputBlocks.length > 0
210
+ ? await estimator.estimate(toolOutputBlocks)
211
+ : { tokens: 0 };
212
+
213
+ const result = pruneToolOutputs(blocks, pruningConfig);
214
+
215
+ // Estimate tokens after pruning (only tool output blocks)
216
+ const keptToolOutputs = result.blocks.filter((b) => b.meta.kind === 'tool_output');
217
+ const afterEstimate = keptToolOutputs.length > 0
218
+ ? await estimator.estimate(keptToolOutputs)
219
+ : { tokens: 0 };
220
+
221
+ blocks = result.blocks;
222
+ removedBlocks.push(...result.removed);
223
+
224
+ stepReports.push({
225
+ step,
226
+ blocksRemoved: result.removed.length,
227
+ blocksReplaced: result.replaced.length,
228
+ tokensSaved: beforeEstimate.tokens - afterEstimate.tokens,
229
+ lossy: result.replaced.length > 0,
230
+ description: `Pruned ${result.replaced.length} tool outputs, removed ${result.removed.length}`,
231
+ });
232
+ break;
233
+ }
234
+
235
+ case 'history_trim': {
236
+ const trimConfig = config.historyTrim ?? {
237
+ keepRecentMessages: 20,
238
+ keepErrorMessages: true,
239
+ };
240
+ const result = trimHistory(blocks, trimConfig);
241
+
242
+ // Calculate token savings from removed history blocks
243
+ const removedEstimate = result.removed.length > 0
244
+ ? await estimator.estimate(result.removed)
245
+ : { tokens: 0 };
246
+
247
+ blocks = result.blocks;
248
+ removedBlocks.push(...result.removed);
249
+
250
+ stepReports.push({
251
+ step,
252
+ blocksRemoved: result.removed.length,
253
+ blocksReplaced: 0,
254
+ tokensSaved: removedEstimate.tokens,
255
+ lossy: result.removed.length > 0,
256
+ description: `Trimmed ${result.removed.length} old history messages`,
257
+ });
258
+ break;
259
+ }
260
+
261
+ case 'summarize_history': {
262
+ const summaryConfig = config.summarization;
263
+
264
+ if (!summaryConfig?.summarizer) {
265
+ stepReports.push({
266
+ step,
267
+ blocksRemoved: 0,
268
+ blocksReplaced: 0,
269
+ tokensSaved: 0,
270
+ lossy: false,
271
+ description: 'Summarizer not configured, skipping',
272
+ });
273
+ break;
274
+ }
275
+
276
+ const historyBlocks = blocks.filter((b) => b.meta.kind === 'history');
277
+
278
+ if (historyBlocks.length < summaryConfig.minMessages) {
279
+ stepReports.push({
280
+ step,
281
+ blocksRemoved: 0,
282
+ blocksReplaced: 0,
283
+ tokensSaved: 0,
284
+ lossy: false,
285
+ description: `Insufficient history (${historyBlocks.length} < ${summaryConfig.minMessages})`,
286
+ });
287
+ break;
288
+ }
289
+
290
+ // Split into recent (keep) and old (summarize)
291
+ const keepCount = 10; // Keep last 10 messages raw
292
+ const recent = historyBlocks.slice(-keepCount);
293
+ const old = historyBlocks.slice(0, -keepCount);
294
+
295
+ if (old.length === 0) {
296
+ stepReports.push({
297
+ step,
298
+ blocksRemoved: 0,
299
+ blocksReplaced: 0,
300
+ tokensSaved: 0,
301
+ lossy: false,
302
+ description: 'No old history to summarize',
303
+ });
304
+ break;
305
+ }
306
+
307
+ // Estimate tokens before summarization
308
+ const beforeSummaryEstimate = await estimator.estimate(old);
309
+ const targetTokens = Math.floor(beforeSummaryEstimate.tokens * 0.3); // Compress to 30%
310
+
311
+ // Create summary block
312
+ const summaryBlock = await summaryConfig.summarizer.summarize(old, targetTokens);
313
+
314
+ // Estimate tokens for summary block
315
+ const afterSummaryEstimate = await estimator.estimateBlock(summaryBlock);
316
+
317
+ // Replace old blocks with summary
318
+ const nonHistoryBlocks = blocks.filter((b) => b.meta.kind !== 'history');
319
+ blocks = [...nonHistoryBlocks, summaryBlock, ...recent];
320
+ removedBlocks.push(...old);
321
+
322
+ stepReports.push({
323
+ step,
324
+ blocksRemoved: old.length,
325
+ blocksReplaced: 1,
326
+ tokensSaved: beforeSummaryEstimate.tokens - afterSummaryEstimate.tokens,
327
+ lossy: true,
328
+ description: `Summarized ${old.length} old messages into 1 summary block`,
329
+ });
330
+ break;
331
+ }
332
+ }
333
+ }
334
+
335
+ // Calculate total savings - recalculate tokens for compacted blocks
336
+ const beforeTokens = view.tokenEstimate?.tokens ?? 0;
337
+
338
+ // Recalculate tokens for compacted blocks
339
+ const afterEstimate = blocks.length > 0
340
+ ? await estimator.estimate(blocks)
341
+ : { tokens: 0 };
342
+ const afterTokens = afterEstimate.tokens;
343
+ const savedTokens = beforeTokens - afterTokens;
344
+
345
+ return {
346
+ blocks,
347
+ removedBlocks,
348
+ report: {
349
+ beforeTokens,
350
+ afterTokens,
351
+ savedTokens,
352
+ stepsApplied: config.steps,
353
+ stepReports,
354
+ },
355
+ };
356
+ }
357
+
358
+ /**
359
+ * Deduplicate identical blocks.
360
+ * Keeps first occurrence of each unique blockHash.
361
+ *
362
+ * @param blocks - Input blocks
363
+ * @returns Deduplicated blocks and removed duplicates
364
+ */
365
+ function deduplicateBlocks(blocks: ContextBlock[]): {
366
+ blocks: ContextBlock[];
367
+ removed: ContextBlock[];
368
+ } {
369
+ const seen = new Set<string>();
370
+ const deduplicated: ContextBlock[] = [];
371
+ const removed: ContextBlock[] = [];
372
+
373
+ for (const block of blocks) {
374
+ if (seen.has(block.blockHash)) {
375
+ removed.push(block);
376
+ } else {
377
+ seen.add(block.blockHash);
378
+ deduplicated.push(block);
379
+ }
380
+ }
381
+
382
+ return { blocks: deduplicated, removed };
383
+ }
384
+
385
+ /**
386
+ * Prune tool outputs.
387
+ * Keeps:
388
+ * - All error outputs (preserveErrorTail = true)
389
+ * - Last N outputs per tool type
390
+ * - Truncated version of large outputs (tail only)
391
+ *
392
+ * @param blocks - Input blocks
393
+ * @param config - Pruning configuration
394
+ * @returns Pruned blocks, removed blocks, and replaced blocks
395
+ */
396
+ function pruneToolOutputs(
397
+ blocks: ContextBlock[],
398
+ config: ToolOutputPruningConfig
399
+ ): {
400
+ blocks: ContextBlock[];
401
+ removed: ContextBlock[];
402
+ replaced: ContextBlock[];
403
+ } {
404
+ const toolOutputBlocks = blocks.filter((b) => b.meta.kind === 'tool_output');
405
+ const otherBlocks = blocks.filter((b) => b.meta.kind !== 'tool_output');
406
+
407
+ // Group by tool type (codecId)
408
+ const byTool = new Map<string, ContextBlock[]>();
409
+ for (const block of toolOutputBlocks) {
410
+ const toolId = block.meta.codecId;
411
+ if (!byTool.has(toolId)) {
412
+ byTool.set(toolId, []);
413
+ }
414
+ byTool.get(toolId)!.push(block);
415
+ }
416
+
417
+ const kept: ContextBlock[] = [];
418
+ const removed: ContextBlock[] = [];
419
+ const replaced: ContextBlock[] = [];
420
+
421
+ // For each tool type, keep recent outputs and prune old/large ones
422
+ for (const [toolId, toolBlocks] of byTool.entries()) {
423
+ // Sort by createdAt (most recent last)
424
+ const sorted = [...toolBlocks].sort((a, b) => a.meta.createdAt - b.meta.createdAt);
425
+
426
+ // Keep last N outputs
427
+ const toKeep = sorted.slice(-config.maxOutputsPerTool);
428
+ const toRemove = sorted.slice(0, -config.maxOutputsPerTool);
429
+
430
+ // Check if any outputs need truncation
431
+ for (const block of toKeep) {
432
+ const payload = block.payload as any;
433
+
434
+ // Check if output is large (needs truncation)
435
+ const isLarge = payload.output && typeof payload.output === 'string' && payload.output.length > config.maxRawTailChars;
436
+ const isError = payload.error || payload.status === 'error';
437
+
438
+ if (isLarge && (!isError || !config.preserveErrorTail)) {
439
+ // Create truncated replacement block
440
+ const truncatedPayload = {
441
+ ...payload,
442
+ output: truncateTail(payload.output, config.maxRawTailChars),
443
+ _truncated: true,
444
+ };
445
+
446
+ const replacementBlock = createReplacementBlock(
447
+ block,
448
+ truncatedPayload,
449
+ 'tool_output_prune',
450
+ '1.0.0'
451
+ );
452
+
453
+ kept.push(replacementBlock);
454
+ replaced.push(block);
455
+ } else {
456
+ kept.push(block);
457
+ }
458
+ }
459
+
460
+ removed.push(...toRemove);
461
+ }
462
+
463
+ return {
464
+ blocks: [...otherBlocks, ...kept],
465
+ removed,
466
+ replaced,
467
+ };
468
+ }
469
+
470
+ /**
471
+ * Trim conversation history.
472
+ * Keeps recent messages and optionally error messages.
473
+ *
474
+ * @param blocks - Input blocks
475
+ * @param config - Trim configuration
476
+ * @returns Trimmed blocks and removed blocks
477
+ */
478
+ function trimHistory(
479
+ blocks: ContextBlock[],
480
+ config: { keepRecentMessages: number; keepErrorMessages: boolean }
481
+ ): {
482
+ blocks: ContextBlock[];
483
+ removed: ContextBlock[];
484
+ } {
485
+ const historyBlocks = blocks.filter((b) => b.meta.kind === 'history');
486
+ const otherBlocks = blocks.filter((b) => b.meta.kind !== 'history');
487
+
488
+ if (historyBlocks.length === 0) {
489
+ return { blocks, removed: [] };
490
+ }
491
+
492
+ // Sort by createdAt
493
+ const sorted = [...historyBlocks].sort((a, b) => a.meta.createdAt - b.meta.createdAt);
494
+
495
+ // Keep recent messages
496
+ const recent = sorted.slice(-config.keepRecentMessages);
497
+ const old = sorted.slice(0, -config.keepRecentMessages);
498
+
499
+ // Optionally keep error messages from old
500
+ const errorMessages = config.keepErrorMessages
501
+ ? old.filter((b) => {
502
+ const payload = b.payload as any;
503
+ return payload.messages?.some((m: any) => m.error);
504
+ })
505
+ : [];
506
+
507
+ const kept = [...recent, ...errorMessages];
508
+ const removed = old.filter((b) => !errorMessages.includes(b));
509
+
510
+ return {
511
+ blocks: [...otherBlocks, ...kept],
512
+ removed,
513
+ };
514
+ }
515
+
516
+ /**
517
+ * Truncate text to tail characters with ellipsis.
518
+ *
519
+ * @param text - Text to truncate
520
+ * @param maxChars - Maximum characters to keep
521
+ * @returns Truncated text
522
+ */
523
+ function truncateTail(text: string, maxChars: number): string {
524
+ if (text.length <= maxChars) {
525
+ return text;
526
+ }
527
+
528
+ const tail = text.slice(-maxChars);
529
+ return `... [truncated ${text.length - maxChars} chars] ...\n${tail}`;
530
+ }
531
+
532
+ /**
533
+ * Create a replacement block with provenance.
534
+ *
535
+ * @param original - Original block
536
+ * @param newPayload - New payload
537
+ * @param method - Compaction method
538
+ * @param version - Compaction version
539
+ * @returns Replacement block
540
+ */
541
+ function createReplacementBlock<TPayload>(
542
+ original: ContextBlock<TPayload>,
543
+ newPayload: TPayload,
544
+ method: string,
545
+ version: string
546
+ ): ContextBlock<TPayload> {
547
+ // Compute new hash
548
+ const blockHash = computeBlockHash(
549
+ original.meta,
550
+ newPayload
551
+ );
552
+
553
+ // Create replacement block with provenance metadata
554
+ return {
555
+ blockHash,
556
+ meta: {
557
+ ...original.meta,
558
+ source: `${original.meta.source ?? 'unknown'}:compacted`,
559
+ tags: [...(original.meta.tags ?? []), `compacted:${method}`],
560
+ },
561
+ payload: newPayload,
562
+ };
563
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Pipeline exports (compaction, fork, etc.).
3
+ */
4
+
5
+ export * from './compactor.js';
6
+ export * from './summarizer.js';
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Summarizer implementations for history compaction.
3
+ *
4
+ * Uses LLM to condense old conversation history into compact summaries.
5
+ */
6
+
7
+ import Anthropic from '@anthropic-ai/sdk';
8
+ import type { ContextBlock } from '../types/block.js';
9
+ import type { HistorySummarizer } from './compactor.js';
10
+ import { computeBlockHash } from '../types/hash.js';
11
+
12
+ /**
13
+ * Anthropic-based history summarizer.
14
+ * Uses Claude Haiku for cost-effective summarization.
15
+ */
16
+ export class AnthropicSummarizer implements HistorySummarizer {
17
+ constructor(private anthropicClient: Anthropic) {}
18
+
19
+ async summarize(blocks: ContextBlock[], targetTokens: number): Promise<ContextBlock> {
20
+ // Render blocks to text (simplified - just stringify payload)
21
+ const messages = blocks.map((block) => {
22
+ const payload = block.payload as any;
23
+ if (payload.messages && Array.isArray(payload.messages)) {
24
+ return payload.messages
25
+ .map((m: any) => `${m.role}: ${JSON.stringify(m.content)}`)
26
+ .join('\n');
27
+ }
28
+ return JSON.stringify(payload);
29
+ });
30
+
31
+ const text = messages.join('\n\n');
32
+
33
+ // Call Anthropic with summarization prompt
34
+ const response = await this.anthropicClient.messages.create({
35
+ model: 'claude-haiku-4-5',
36
+ max_tokens: targetTokens,
37
+ messages: [
38
+ {
39
+ role: 'user',
40
+ content: `Summarize the following conversation history concisely, preserving key context and decisions:\n\n${text}`,
41
+ },
42
+ ],
43
+ });
44
+
45
+ const summary =
46
+ response.content[0].type === 'text' ? response.content[0].text : '';
47
+
48
+ // Create summary block with provenance
49
+ const summaryPayload = {
50
+ summary,
51
+ originalBlockCount: blocks.length,
52
+ summarizedAt: Date.now(),
53
+ };
54
+
55
+ const firstBlock = blocks[0];
56
+ const blockHash = computeBlockHash(
57
+ {
58
+ ...firstBlock.meta,
59
+ source: `${firstBlock.meta.source ?? 'unknown'}:summarized`,
60
+ },
61
+ summaryPayload
62
+ );
63
+
64
+ return {
65
+ blockHash,
66
+ meta: {
67
+ ...firstBlock.meta,
68
+ kind: 'history',
69
+ source: `${firstBlock.meta.source ?? 'unknown'}:summarized`,
70
+ tags: [...(firstBlock.meta.tags ?? []), 'summarized'],
71
+ createdAt: Date.now(),
72
+ },
73
+ payload: summaryPayload,
74
+ };
75
+ }
76
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Default context policy for general-purpose LLM conversations.
3
+ *
4
+ * This policy provides sensible defaults for:
5
+ * - Token budget: 180K context window with 8K completion reserve
6
+ * - Caching: Provider-native caching with breakpoints for pinned/reference content
7
+ * - Compaction: Tool output pruning + history summarization
8
+ * - Attachments: 10K token budget with purpose-based selection
9
+ * - Sensitivity: Public content only with redaction for restricted content
10
+ */
11
+
12
+ import type { ContextPolicy } from '../types/policy.js';
13
+ import {
14
+ DEFAULT_KIND_PRIORITIES,
15
+ DEFAULT_COMPACTION_CONFIG,
16
+ DEFAULT_SENSITIVITY_CONFIG,
17
+ DEFAULT_ATTACHMENT_POLICY,
18
+ } from '../types/policy.js';
19
+
20
+ /**
21
+ * Default context policy configuration.
22
+ *
23
+ * Use this as a starting point for most conversational workflows.
24
+ * Override specific properties as needed for your use case.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * import { DEFAULT_POLICY, ContextBuilder } from '@foundry/context';
29
+ *
30
+ * const builder = new ContextBuilder({
31
+ * ...DEFAULT_POLICY,
32
+ * contextWindow: 200_000, // Override for larger context
33
+ * });
34
+ * ```
35
+ */
36
+ export const DEFAULT_POLICY: ContextPolicy = {
37
+ // Provider configuration
38
+ provider: 'anthropic',
39
+ modelId: 'claude-sonnet-4-5',
40
+
41
+ // Token budget
42
+ contextWindow: 180_000,
43
+ completionReserve: 8_000,
44
+
45
+ // Overflow handling
46
+ overflowStrategy: 'compact',
47
+
48
+ // Block priorities
49
+ kindPriorities: DEFAULT_KIND_PRIORITIES,
50
+
51
+ // Compaction configuration
52
+ compaction: {
53
+ ...DEFAULT_COMPACTION_CONFIG,
54
+ // Tool output pruning
55
+ pruneToolOutputs: true,
56
+ maxToolOutputAge: 3600, // 1 hour
57
+ maxToolOutputsPerKind: 10,
58
+
59
+ // History summarization
60
+ summarizeHistory: true,
61
+ maxHistoryMessages: 20,
62
+ },
63
+
64
+ // Sensitivity filtering
65
+ sensitivity: DEFAULT_SENSITIVITY_CONFIG,
66
+
67
+ // Attachment selection
68
+ attachments: DEFAULT_ATTACHMENT_POLICY,
69
+ };