@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,125 @@
1
+ /**
2
+ * AnthropicTokenEstimator: Exact token counting via Anthropic API.
3
+ *
4
+ * Uses client.messages.countTokens() for exact counts.
5
+ */
6
+
7
+ import Anthropic from '@anthropic-ai/sdk';
8
+ import type { ContextBlock } from '../types/block.js';
9
+ import type { TokenEstimator, TokenEstimate } from './token-estimator.js';
10
+ import {
11
+ heuristicTokenCount,
12
+ serializeBlockForEstimation,
13
+ } from './token-estimator.js';
14
+
15
+ /**
16
+ * AnthropicTokenEstimator using API-based exact counting.
17
+ */
18
+ export class AnthropicTokenEstimator implements TokenEstimator {
19
+ private readonly client: Anthropic;
20
+ private readonly model: string;
21
+
22
+ /**
23
+ * Create an AnthropicTokenEstimator.
24
+ *
25
+ * @param client - Anthropic SDK client
26
+ * @param model - Model name (e.g., 'claude-sonnet-4-5')
27
+ */
28
+ constructor(client: Anthropic, model: string = 'claude-sonnet-4-5') {
29
+ this.client = client;
30
+ this.model = model;
31
+ }
32
+
33
+ /**
34
+ * Estimate tokens for a single block using Anthropic API.
35
+ *
36
+ * @param block - Block to estimate
37
+ * @returns Token estimate (exact confidence)
38
+ */
39
+ async estimateBlock(block: ContextBlock<unknown>): Promise<TokenEstimate> {
40
+ try {
41
+ // Serialize block to text
42
+ const text = serializeBlockForEstimation(block);
43
+
44
+ // Use Anthropic API for exact count
45
+ const result = await this.client.messages.countTokens({
46
+ model: this.model,
47
+ messages: [
48
+ {
49
+ role: 'user',
50
+ content: text,
51
+ },
52
+ ],
53
+ });
54
+
55
+ return {
56
+ tokens: result.input_tokens,
57
+ confidence: 'exact',
58
+ };
59
+ } catch (error) {
60
+ // Fallback to heuristic on API error
61
+ console.warn(
62
+ '[AnthropicTokenEstimator] API error, falling back to heuristic:',
63
+ error
64
+ );
65
+ const text = serializeBlockForEstimation(block);
66
+ return {
67
+ tokens: heuristicTokenCount(text),
68
+ confidence: 'low',
69
+ };
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Estimate tokens for multiple blocks.
75
+ * Batches API calls for efficiency.
76
+ *
77
+ * @param blocks - Blocks to estimate
78
+ * @returns Token estimate (exact confidence if all succeed)
79
+ */
80
+ async estimate(blocks: ContextBlock<unknown>[]): Promise<TokenEstimate> {
81
+ if (blocks.length === 0) {
82
+ return { tokens: 0, confidence: 'exact' };
83
+ }
84
+
85
+ try {
86
+ // Concatenate all block texts
87
+ const combinedText = blocks
88
+ .map((block) => serializeBlockForEstimation(block))
89
+ .join('\n\n');
90
+
91
+ // Single API call for all blocks
92
+ const result = await this.client.messages.countTokens({
93
+ model: this.model,
94
+ messages: [
95
+ {
96
+ role: 'user',
97
+ content: combinedText,
98
+ },
99
+ ],
100
+ });
101
+
102
+ return {
103
+ tokens: result.input_tokens,
104
+ confidence: 'exact',
105
+ };
106
+ } catch (error) {
107
+ // Fallback: sum individual heuristic estimates
108
+ console.warn(
109
+ '[AnthropicTokenEstimator] API error, falling back to heuristic:',
110
+ error
111
+ );
112
+
113
+ let totalTokens = 0;
114
+ for (const block of blocks) {
115
+ const text = serializeBlockForEstimation(block);
116
+ totalTokens += heuristicTokenCount(text);
117
+ }
118
+
119
+ return {
120
+ tokens: totalTokens,
121
+ confidence: 'low',
122
+ };
123
+ }
124
+ }
125
+ }
@@ -0,0 +1,295 @@
1
+ /**
2
+ * AttachmentResolver: Versioned resolution of attachments with provenance.
3
+ *
4
+ * Responsibilities:
5
+ * - Resolve attachment references to actual content
6
+ * - Support different resolution levels (metadata_only, extract, full)
7
+ * - Generate derived blocks with provenance tracking
8
+ * - Compute snapshot hashes for reproducibility
9
+ */
10
+
11
+ import type { ContextBlock, BlockKind } from '../types/block.js';
12
+ import type { AttachmentRef, AttachmentMeta, AttachmentMimeType } from '../types/attachment.js';
13
+ import { computeBlockHash } from '../types/hash.js';
14
+
15
+ /**
16
+ * Resolution level for attachments.
17
+ */
18
+ export type AttachmentResolutionLevel =
19
+ | 'metadata_only' // Only metadata (filename, size, type)
20
+ | 'extract' // Extract text/structured data (PDFs -> text, images -> OCR)
21
+ | 'full'; // Full content (base64-encoded for images)
22
+
23
+ /**
24
+ * Resolved attachment part (for multi-part content).
25
+ */
26
+ export interface AttachmentPart {
27
+ /** Part type */
28
+ type: 'text' | 'image' | 'json';
29
+
30
+ /** Text content (for text parts) */
31
+ text?: string;
32
+
33
+ /** Image content (base64-encoded, for image parts) */
34
+ image?: {
35
+ data: string;
36
+ mimeType: AttachmentMimeType;
37
+ };
38
+
39
+ /** JSON content (for json parts) */
40
+ json?: unknown;
41
+
42
+ /** Optional description */
43
+ description?: string;
44
+ }
45
+
46
+ /**
47
+ * Resolved attachment with content and derived blocks.
48
+ * Extended version with provenance tracking.
49
+ */
50
+ export interface ResolvedAttachmentWithProvenance extends AttachmentMeta {
51
+ /** Resolution level used */
52
+ level: AttachmentResolutionLevel;
53
+
54
+ /** Resolved content parts */
55
+ parts: AttachmentPart[];
56
+
57
+ /** Derived blocks (generated from attachment content) */
58
+ derivedBlocks: ContextBlock[];
59
+
60
+ /** Snapshot hash (for reproducibility) */
61
+ snapshotHash: string;
62
+
63
+ /** Resolver version (for debugging) */
64
+ resolverVersion: string;
65
+ }
66
+
67
+ /**
68
+ * Attachment resolver interface.
69
+ */
70
+ export interface AttachmentResolver {
71
+ /** Resolver identifier */
72
+ resolverId: string;
73
+
74
+ /** Resolver version */
75
+ version: string;
76
+
77
+ /**
78
+ * Resolve an attachment reference to actual content.
79
+ *
80
+ * @param ref - Attachment reference
81
+ * @param level - Resolution level
82
+ * @returns Resolved attachment with derived blocks
83
+ */
84
+ resolve(
85
+ ref: AttachmentRef,
86
+ level: AttachmentResolutionLevel
87
+ ): Promise<ResolvedAttachmentWithProvenance>;
88
+ }
89
+
90
+ /**
91
+ * Create a snapshot hash for a resolved attachment.
92
+ * Used for reproducibility and change detection.
93
+ *
94
+ * @param attachmentId - Attachment ID
95
+ * @param level - Resolution level
96
+ * @param parts - Resolved parts
97
+ * @param resolverVersion - Resolver version
98
+ * @returns Snapshot hash
99
+ */
100
+ export function createSnapshotHash(
101
+ attachmentId: string,
102
+ level: AttachmentResolutionLevel,
103
+ parts: AttachmentPart[],
104
+ resolverVersion: string
105
+ ): string {
106
+ const snapshot = {
107
+ attachmentId,
108
+ level,
109
+ parts: parts.map((part) => ({
110
+ type: part.type,
111
+ // Hash content without including full data
112
+ textHash: part.text ? computeBlockHash({ kind: 'reference' as BlockKind, sensitivity: 'public', codecId: 'text', codecVersion: '1.0.0' }, { text: part.text }) : undefined,
113
+ imageHash: part.image ? computeBlockHash({ kind: 'reference' as BlockKind, sensitivity: 'public', codecId: 'image', codecVersion: '1.0.0' }, { data: part.image.data, mimeType: part.image.mimeType }) : undefined,
114
+ jsonHash: part.json ? computeBlockHash({ kind: 'reference' as BlockKind, sensitivity: 'public', codecId: 'json', codecVersion: '1.0.0' }, part.json) : undefined,
115
+ })),
116
+ resolverVersion,
117
+ };
118
+
119
+ return computeBlockHash(
120
+ { kind: 'reference' as BlockKind, sensitivity: 'public', codecId: 'attachment-snapshot', codecVersion: '1.0.0' },
121
+ snapshot
122
+ );
123
+ }
124
+
125
+ /**
126
+ * Create derived blocks from resolved attachment parts.
127
+ *
128
+ * @param attachmentId - Attachment ID
129
+ * @param parts - Resolved parts
130
+ * @param parentHash - Parent block hash (for provenance)
131
+ * @returns Array of derived blocks
132
+ */
133
+ export function createDerivedBlocks(
134
+ attachmentId: string,
135
+ parts: AttachmentPart[],
136
+ parentHash: string
137
+ ): ContextBlock[] {
138
+ const blocks: ContextBlock[] = [];
139
+
140
+ for (let i = 0; i < parts.length; i++) {
141
+ const part = parts[i];
142
+
143
+ if (part.type === 'text' && part.text) {
144
+ const payload = {
145
+ text: part.text,
146
+ source: attachmentId,
147
+ partIndex: i,
148
+ description: part.description,
149
+ };
150
+
151
+ const blockHash = computeBlockHash(
152
+ { kind: 'reference', sensitivity: 'public', codecId: 'attachment-text', codecVersion: '1.0.0' },
153
+ payload
154
+ );
155
+
156
+ blocks.push({
157
+ blockHash,
158
+ meta: {
159
+ kind: 'reference',
160
+ sensitivity: 'public',
161
+ codecId: 'attachment-text',
162
+ codecVersion: '1.0.0',
163
+ createdAt: Math.floor(Date.now() / 1000),
164
+ source: attachmentId,
165
+ },
166
+ payload,
167
+ });
168
+ } else if (part.type === 'image' && part.image) {
169
+ const payload = {
170
+ data: part.image.data,
171
+ mimeType: part.image.mimeType,
172
+ source: attachmentId,
173
+ partIndex: i,
174
+ description: part.description,
175
+ };
176
+
177
+ const blockHash = computeBlockHash(
178
+ { kind: 'reference', sensitivity: 'public', codecId: 'attachment-image', codecVersion: '1.0.0' },
179
+ payload
180
+ );
181
+
182
+ blocks.push({
183
+ blockHash,
184
+ meta: {
185
+ kind: 'reference',
186
+ sensitivity: 'public',
187
+ codecId: 'attachment-image',
188
+ codecVersion: '1.0.0',
189
+ createdAt: Math.floor(Date.now() / 1000),
190
+ source: attachmentId,
191
+ },
192
+ payload,
193
+ });
194
+ } else if (part.type === 'json' && part.json) {
195
+ const payload = {
196
+ data: part.json,
197
+ source: attachmentId,
198
+ partIndex: i,
199
+ description: part.description,
200
+ };
201
+
202
+ const blockHash = computeBlockHash(
203
+ { kind: 'reference', sensitivity: 'public', codecId: 'attachment-json', codecVersion: '1.0.0' },
204
+ payload
205
+ );
206
+
207
+ blocks.push({
208
+ blockHash,
209
+ meta: {
210
+ kind: 'reference',
211
+ sensitivity: 'public',
212
+ codecId: 'attachment-json',
213
+ codecVersion: '1.0.0',
214
+ createdAt: Math.floor(Date.now() / 1000),
215
+ source: attachmentId,
216
+ },
217
+ payload,
218
+ });
219
+ }
220
+ }
221
+
222
+ return blocks;
223
+ }
224
+
225
+ /**
226
+ * Default attachment resolver (stub implementation).
227
+ * In production, this would integrate with actual storage backends (S3, GCS, etc.).
228
+ */
229
+ export class DefaultAttachmentResolver implements AttachmentResolver {
230
+ resolverId = 'default-resolver';
231
+ version = '1.0.0';
232
+
233
+ async resolve(
234
+ ref: AttachmentRef,
235
+ level: AttachmentResolutionLevel
236
+ ): Promise<ResolvedAttachmentWithProvenance> {
237
+ // Stub implementation - in production, this would:
238
+ // 1. Fetch attachment metadata from storage
239
+ // 2. Fetch content based on resolution level
240
+ // 3. Process content (extract text, OCR images, etc.)
241
+ // 4. Generate derived blocks
242
+
243
+ // For now, return a minimal resolved attachment
244
+ const parts: AttachmentPart[] = [];
245
+
246
+ if (level === 'metadata_only') {
247
+ // No content parts for metadata_only
248
+ } else if (level === 'extract') {
249
+ // Extract text/structured data
250
+ parts.push({
251
+ type: 'text',
252
+ text: `Extracted content from ${ref.attachmentId}`,
253
+ description: ref.description,
254
+ });
255
+ } else if (level === 'full') {
256
+ // Full content (would be base64-encoded image data in production)
257
+ parts.push({
258
+ type: 'text',
259
+ text: `Full content of ${ref.attachmentId}`,
260
+ description: ref.description,
261
+ });
262
+ }
263
+
264
+ const snapshotHash = createSnapshotHash(
265
+ ref.attachmentId,
266
+ level,
267
+ parts,
268
+ this.version
269
+ );
270
+
271
+ const derivedBlocks = createDerivedBlocks(
272
+ ref.attachmentId,
273
+ parts,
274
+ ref.attachmentId // Parent hash (would be actual block hash in production)
275
+ );
276
+
277
+ return {
278
+ // Metadata (would be fetched from storage in production)
279
+ attachmentId: ref.attachmentId,
280
+ mimeType: 'text/plain',
281
+ sizeBytes: 0,
282
+ filename: ref.attachmentId,
283
+ storage: 'local',
284
+ storagePath: '',
285
+ createdAt: Math.floor(Date.now() / 1000),
286
+
287
+ // Resolution results
288
+ level,
289
+ parts,
290
+ derivedBlocks,
291
+ snapshotHash,
292
+ resolverVersion: this.version,
293
+ };
294
+ }
295
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * AttachmentSelector: Token budget-aware attachment selection.
3
+ *
4
+ * Selects attachments based on policy-driven ranking with deterministic ordering.
5
+ */
6
+
7
+ import type { ResolvedAttachment, AttachmentRef } from '../types/attachment.js';
8
+ import type { AttachmentPolicy, AttachmentPurpose, RankingCriterion } from '../types/policy.js';
9
+ import type { TokenEstimator } from './token-estimator.js';
10
+
11
+ /**
12
+ * Attachment with selection metadata.
13
+ */
14
+ export interface RankedAttachment extends ResolvedAttachment {
15
+ /** Attachment purpose (for ranking) */
16
+ purpose: AttachmentPurpose;
17
+
18
+ /** User explicitly mentioned this attachment */
19
+ userMention: boolean;
20
+
21
+ /** Ranking score (lower = higher priority) */
22
+ rankScore: number;
23
+ }
24
+
25
+ /**
26
+ * Selected attachments result.
27
+ */
28
+ export interface SelectedAttachments {
29
+ /** Selected attachments (within budget) */
30
+ selected: ResolvedAttachment[];
31
+
32
+ /** Excluded attachments (over budget) */
33
+ excluded: AttachmentRef[];
34
+
35
+ /** Total tokens used by selected attachments */
36
+ tokensUsed: number;
37
+ }
38
+
39
+ /**
40
+ * Compute rank score based on selection strategy.
41
+ *
42
+ * @param attachment - Ranked attachment
43
+ * @param rankBy - Ranking criteria in priority order
44
+ * @param purposePriority - Purpose priority mapping
45
+ * @returns Rank score (lower = higher priority)
46
+ */
47
+ function computeRankScore(
48
+ attachment: RankedAttachment,
49
+ rankBy: RankingCriterion[],
50
+ purposePriority: Record<AttachmentPurpose, number>
51
+ ): number {
52
+ let score = 0;
53
+ let multiplier = 1000; // High multiplier for primary criterion
54
+
55
+ for (const criterion of rankBy) {
56
+ switch (criterion) {
57
+ case 'purpose':
58
+ score += purposePriority[attachment.purpose] * multiplier;
59
+ break;
60
+ case 'user_mention':
61
+ // User mention: 0 if mentioned, 1 if not
62
+ score += (attachment.userMention ? 0 : 1) * multiplier;
63
+ break;
64
+ case 'recency':
65
+ // More recent = lower score (inverted createdAt)
66
+ score += (Date.now() / 1000 - attachment.createdAt) * multiplier * 0.01;
67
+ break;
68
+ }
69
+
70
+ multiplier /= 100; // Reduce multiplier for next criterion
71
+ }
72
+
73
+ return score;
74
+ }
75
+
76
+ /**
77
+ * Estimate tokens for an attachment.
78
+ *
79
+ * @param attachment - Resolved attachment
80
+ * @returns Estimated tokens
81
+ */
82
+ function estimateAttachmentTokens(attachment: ResolvedAttachment): number {
83
+ // For text attachments, estimate from text content
84
+ if (attachment.text) {
85
+ // Rough estimate: 1 token per 4 characters
86
+ return Math.ceil(attachment.text.length / 4);
87
+ }
88
+
89
+ // For images, use a heuristic based on size
90
+ // (actual token count depends on image dimensions and provider)
91
+ if (attachment.mimeType.startsWith('image/')) {
92
+ // Rough estimate: ~85 tokens per 512x512 tile
93
+ // Assume average image is ~4 tiles
94
+ return 340;
95
+ }
96
+
97
+ // For PDFs and JSON, use a conservative estimate
98
+ if (attachment.mimeType === 'application/pdf' || attachment.mimeType === 'application/json') {
99
+ // Estimate based on content length if available
100
+ if (attachment.content) {
101
+ // Base64 content: approximate text length
102
+ const textLength = Math.floor(attachment.content.length * 0.75);
103
+ return Math.ceil(textLength / 4); // Rough token estimate
104
+ }
105
+ // Fallback: 500 tokens per attachment
106
+ return 500;
107
+ }
108
+
109
+ // Default fallback
110
+ return 100;
111
+ }
112
+
113
+ /**
114
+ * AttachmentSelector: Select attachments with policy enforcement.
115
+ */
116
+ export class AttachmentSelector {
117
+ constructor(
118
+ private readonly policy: AttachmentPolicy,
119
+ private readonly estimator: TokenEstimator
120
+ ) {}
121
+
122
+ /**
123
+ * Select attachments within token budget.
124
+ * Deterministic: same inputs → same selection order.
125
+ *
126
+ * @param attachments - Ranked attachments to select from
127
+ * @returns Selected attachments result
128
+ */
129
+ async selectAttachments(
130
+ attachments: RankedAttachment[]
131
+ ): Promise<SelectedAttachments> {
132
+ const { maxTokensTotal, selectionStrategy } = this.policy;
133
+ const { rankBy, purposePriority } = selectionStrategy;
134
+
135
+ // Use default purpose priority if not provided
136
+ const effectivePurposePriority = purposePriority ?? {
137
+ evidence: 1,
138
+ input: 2,
139
+ context: 3,
140
+ artifact: 4,
141
+ };
142
+
143
+ // Compute rank scores for all attachments
144
+ const scoredAttachments = attachments.map((attachment) => ({
145
+ ...attachment,
146
+ rankScore: computeRankScore(attachment, rankBy, effectivePurposePriority),
147
+ }));
148
+
149
+ // Sort by rank score (ascending - lower score = higher priority)
150
+ // Use attachmentId as tiebreaker for deterministic ordering
151
+ scoredAttachments.sort((a, b) => {
152
+ if (a.rankScore !== b.rankScore) {
153
+ return a.rankScore - b.rankScore;
154
+ }
155
+ return a.attachmentId.localeCompare(b.attachmentId);
156
+ });
157
+
158
+ // Select attachments within budget
159
+ const selected: ResolvedAttachment[] = [];
160
+ const excluded: AttachmentRef[] = [];
161
+ let tokensUsed = 0;
162
+
163
+ for (const attachment of scoredAttachments) {
164
+ // Estimate tokens for this attachment
165
+ const attachmentTokens = estimateAttachmentTokens(attachment);
166
+
167
+ // Check if adding this attachment would exceed budget
168
+ if (tokensUsed + attachmentTokens > maxTokensTotal) {
169
+ // Budget exceeded - exclude this attachment
170
+ excluded.push({
171
+ attachmentId: attachment.attachmentId,
172
+ description: attachment.filename,
173
+ });
174
+ continue;
175
+ }
176
+
177
+ // Include attachment
178
+ selected.push(attachment);
179
+ tokensUsed += attachmentTokens;
180
+ }
181
+
182
+ return {
183
+ selected,
184
+ excluded,
185
+ tokensUsed,
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Select attachments from raw list with purpose/mention metadata.
191
+ * Convenience method that wraps selectAttachments.
192
+ *
193
+ * @param attachments - Resolved attachments
194
+ * @param metadata - Per-attachment metadata
195
+ * @returns Selected attachments result
196
+ */
197
+ async selectFromList(
198
+ attachments: ResolvedAttachment[],
199
+ metadata: Map<string, { purpose: AttachmentPurpose; userMention: boolean }>
200
+ ): Promise<SelectedAttachments> {
201
+ // Build ranked attachments
202
+ const ranked: RankedAttachment[] = attachments.map((attachment) => {
203
+ const meta = metadata.get(attachment.attachmentId) ?? {
204
+ purpose: 'context' as const,
205
+ userMention: false,
206
+ };
207
+
208
+ return {
209
+ ...attachment,
210
+ purpose: meta.purpose,
211
+ userMention: meta.userMention,
212
+ rankScore: 0, // Will be computed in selectAttachments
213
+ };
214
+ });
215
+
216
+ return this.selectAttachments(ranked);
217
+ }
218
+ }