@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,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
|
+
}
|