smartcontext-proxy 0.1.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/PLAN.md +406 -0
- package/PROGRESS.md +60 -0
- package/README.md +99 -0
- package/SPEC.md +915 -0
- package/adapters/openclaw/embedding.d.ts +8 -0
- package/adapters/openclaw/embedding.js +16 -0
- package/adapters/openclaw/embedding.ts +15 -0
- package/adapters/openclaw/index.d.ts +18 -0
- package/adapters/openclaw/index.js +42 -0
- package/adapters/openclaw/index.ts +43 -0
- package/adapters/openclaw/session-importer.d.ts +22 -0
- package/adapters/openclaw/session-importer.js +99 -0
- package/adapters/openclaw/session-importer.ts +105 -0
- package/adapters/openclaw/storage.d.ts +26 -0
- package/adapters/openclaw/storage.js +177 -0
- package/adapters/openclaw/storage.ts +183 -0
- package/dist/adapters/openclaw/embedding.d.ts +8 -0
- package/dist/adapters/openclaw/embedding.js +16 -0
- package/dist/adapters/openclaw/index.d.ts +18 -0
- package/dist/adapters/openclaw/index.js +42 -0
- package/dist/adapters/openclaw/session-importer.d.ts +22 -0
- package/dist/adapters/openclaw/session-importer.js +99 -0
- package/dist/adapters/openclaw/storage.d.ts +26 -0
- package/dist/adapters/openclaw/storage.js +177 -0
- package/dist/config/auto-detect.d.ts +3 -0
- package/dist/config/auto-detect.js +48 -0
- package/dist/config/defaults.d.ts +2 -0
- package/dist/config/defaults.js +28 -0
- package/dist/config/schema.d.ts +30 -0
- package/dist/config/schema.js +3 -0
- package/dist/context/budget.d.ts +25 -0
- package/dist/context/budget.js +85 -0
- package/dist/context/canonical.d.ts +39 -0
- package/dist/context/canonical.js +12 -0
- package/dist/context/chunker.d.ts +9 -0
- package/dist/context/chunker.js +148 -0
- package/dist/context/optimizer.d.ts +31 -0
- package/dist/context/optimizer.js +163 -0
- package/dist/context/retriever.d.ts +29 -0
- package/dist/context/retriever.js +103 -0
- package/dist/daemon/process.d.ts +6 -0
- package/dist/daemon/process.js +76 -0
- package/dist/daemon/service.d.ts +2 -0
- package/dist/daemon/service.js +99 -0
- package/dist/embedding/ollama.d.ts +11 -0
- package/dist/embedding/ollama.js +72 -0
- package/dist/embedding/types.d.ts +6 -0
- package/dist/embedding/types.js +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +190 -0
- package/dist/metrics/collector.d.ts +43 -0
- package/dist/metrics/collector.js +72 -0
- package/dist/providers/anthropic.d.ts +15 -0
- package/dist/providers/anthropic.js +109 -0
- package/dist/providers/google.d.ts +13 -0
- package/dist/providers/google.js +40 -0
- package/dist/providers/ollama.d.ts +13 -0
- package/dist/providers/ollama.js +82 -0
- package/dist/providers/openai.d.ts +15 -0
- package/dist/providers/openai.js +115 -0
- package/dist/providers/types.d.ts +18 -0
- package/dist/providers/types.js +3 -0
- package/dist/proxy/router.d.ts +12 -0
- package/dist/proxy/router.js +46 -0
- package/dist/proxy/server.d.ts +25 -0
- package/dist/proxy/server.js +265 -0
- package/dist/proxy/stream.d.ts +8 -0
- package/dist/proxy/stream.js +32 -0
- package/dist/src/config/auto-detect.d.ts +3 -0
- package/dist/src/config/auto-detect.js +48 -0
- package/dist/src/config/defaults.d.ts +2 -0
- package/dist/src/config/defaults.js +28 -0
- package/dist/src/config/schema.d.ts +30 -0
- package/dist/src/config/schema.js +3 -0
- package/dist/src/context/budget.d.ts +25 -0
- package/dist/src/context/budget.js +85 -0
- package/dist/src/context/canonical.d.ts +39 -0
- package/dist/src/context/canonical.js +12 -0
- package/dist/src/context/chunker.d.ts +9 -0
- package/dist/src/context/chunker.js +148 -0
- package/dist/src/context/optimizer.d.ts +31 -0
- package/dist/src/context/optimizer.js +163 -0
- package/dist/src/context/retriever.d.ts +29 -0
- package/dist/src/context/retriever.js +103 -0
- package/dist/src/daemon/process.d.ts +6 -0
- package/dist/src/daemon/process.js +76 -0
- package/dist/src/daemon/service.d.ts +2 -0
- package/dist/src/daemon/service.js +99 -0
- package/dist/src/embedding/ollama.d.ts +11 -0
- package/dist/src/embedding/ollama.js +72 -0
- package/dist/src/embedding/types.d.ts +6 -0
- package/dist/src/embedding/types.js +3 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +190 -0
- package/dist/src/metrics/collector.d.ts +43 -0
- package/dist/src/metrics/collector.js +72 -0
- package/dist/src/providers/anthropic.d.ts +15 -0
- package/dist/src/providers/anthropic.js +109 -0
- package/dist/src/providers/google.d.ts +13 -0
- package/dist/src/providers/google.js +40 -0
- package/dist/src/providers/ollama.d.ts +13 -0
- package/dist/src/providers/ollama.js +82 -0
- package/dist/src/providers/openai.d.ts +15 -0
- package/dist/src/providers/openai.js +115 -0
- package/dist/src/providers/types.d.ts +18 -0
- package/dist/src/providers/types.js +3 -0
- package/dist/src/proxy/router.d.ts +12 -0
- package/dist/src/proxy/router.js +46 -0
- package/dist/src/proxy/server.d.ts +25 -0
- package/dist/src/proxy/server.js +265 -0
- package/dist/src/proxy/stream.d.ts +8 -0
- package/dist/src/proxy/stream.js +32 -0
- package/dist/src/storage/lancedb.d.ts +21 -0
- package/dist/src/storage/lancedb.js +158 -0
- package/dist/src/storage/types.d.ts +52 -0
- package/dist/src/storage/types.js +3 -0
- package/dist/src/test/context.test.d.ts +1 -0
- package/dist/src/test/context.test.js +141 -0
- package/dist/src/test/dashboard.test.d.ts +1 -0
- package/dist/src/test/dashboard.test.js +85 -0
- package/dist/src/test/proxy.test.d.ts +1 -0
- package/dist/src/test/proxy.test.js +188 -0
- package/dist/src/ui/dashboard.d.ts +2 -0
- package/dist/src/ui/dashboard.js +183 -0
- package/dist/storage/lancedb.d.ts +21 -0
- package/dist/storage/lancedb.js +158 -0
- package/dist/storage/types.d.ts +52 -0
- package/dist/storage/types.js +3 -0
- package/dist/test/context.test.d.ts +1 -0
- package/dist/test/context.test.js +141 -0
- package/dist/test/dashboard.test.d.ts +1 -0
- package/dist/test/dashboard.test.js +85 -0
- package/dist/test/proxy.test.d.ts +1 -0
- package/dist/test/proxy.test.js +188 -0
- package/dist/ui/dashboard.d.ts +2 -0
- package/dist/ui/dashboard.js +183 -0
- package/package.json +38 -0
- package/src/config/auto-detect.ts +51 -0
- package/src/config/defaults.ts +26 -0
- package/src/config/schema.ts +33 -0
- package/src/context/budget.ts +126 -0
- package/src/context/canonical.ts +50 -0
- package/src/context/chunker.ts +165 -0
- package/src/context/optimizer.ts +201 -0
- package/src/context/retriever.ts +123 -0
- package/src/daemon/process.ts +70 -0
- package/src/daemon/service.ts +103 -0
- package/src/embedding/ollama.ts +68 -0
- package/src/embedding/types.ts +6 -0
- package/src/index.ts +176 -0
- package/src/metrics/collector.ts +114 -0
- package/src/providers/anthropic.ts +117 -0
- package/src/providers/google.ts +42 -0
- package/src/providers/ollama.ts +87 -0
- package/src/providers/openai.ts +127 -0
- package/src/providers/types.ts +20 -0
- package/src/proxy/router.ts +48 -0
- package/src/proxy/server.ts +315 -0
- package/src/proxy/stream.ts +39 -0
- package/src/storage/lancedb.ts +169 -0
- package/src/storage/types.ts +47 -0
- package/src/test/context.test.ts +165 -0
- package/src/test/dashboard.test.ts +94 -0
- package/src/test/proxy.test.ts +218 -0
- package/src/ui/dashboard.ts +184 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_CONFIG = void 0;
|
|
4
|
+
exports.DEFAULT_CONFIG = {
|
|
5
|
+
proxy: {
|
|
6
|
+
port: 4800,
|
|
7
|
+
host: '127.0.0.1',
|
|
8
|
+
},
|
|
9
|
+
providers: {},
|
|
10
|
+
context: {
|
|
11
|
+
tier1_exchanges: 3,
|
|
12
|
+
tier2_max_chunks: 10,
|
|
13
|
+
tier2_min_score: 0.55,
|
|
14
|
+
tier3_token_reserve: 500,
|
|
15
|
+
recency_boost: 0.15,
|
|
16
|
+
filepath_boost: 0.20,
|
|
17
|
+
dedup_threshold: 0.92,
|
|
18
|
+
confidence_gate: 0.55,
|
|
19
|
+
response_reserve_tokens: 8192,
|
|
20
|
+
},
|
|
21
|
+
logging: {
|
|
22
|
+
level: 'info',
|
|
23
|
+
raw_logs: true,
|
|
24
|
+
metrics: true,
|
|
25
|
+
debug_headers: false,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=defaults.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface SmartContextConfig {
|
|
2
|
+
proxy: {
|
|
3
|
+
port: number;
|
|
4
|
+
host: string;
|
|
5
|
+
};
|
|
6
|
+
providers: Record<string, ProviderConfig>;
|
|
7
|
+
context: ContextConfig;
|
|
8
|
+
logging: LoggingConfig;
|
|
9
|
+
}
|
|
10
|
+
export interface ProviderConfig {
|
|
11
|
+
apiKey: string;
|
|
12
|
+
baseUrl?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ContextConfig {
|
|
15
|
+
tier1_exchanges: number;
|
|
16
|
+
tier2_max_chunks: number;
|
|
17
|
+
tier2_min_score: number;
|
|
18
|
+
tier3_token_reserve: number;
|
|
19
|
+
recency_boost: number;
|
|
20
|
+
filepath_boost: number;
|
|
21
|
+
dedup_threshold: number;
|
|
22
|
+
confidence_gate: number;
|
|
23
|
+
response_reserve_tokens: number;
|
|
24
|
+
}
|
|
25
|
+
export interface LoggingConfig {
|
|
26
|
+
level: 'error' | 'warn' | 'info' | 'debug';
|
|
27
|
+
raw_logs: boolean;
|
|
28
|
+
metrics: boolean;
|
|
29
|
+
debug_headers: boolean;
|
|
30
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ScoredChunk } from '../storage/types.js';
|
|
2
|
+
import type { CanonicalMessage } from './canonical.js';
|
|
3
|
+
export interface BudgetAllocation {
|
|
4
|
+
systemPromptTokens: number;
|
|
5
|
+
tier1Tokens: number;
|
|
6
|
+
tier2Budget: number;
|
|
7
|
+
tier3Reserve: number;
|
|
8
|
+
responseReserve: number;
|
|
9
|
+
totalAvailable: number;
|
|
10
|
+
}
|
|
11
|
+
export interface PackedContext {
|
|
12
|
+
systemPrompt?: string;
|
|
13
|
+
tier1Messages: CanonicalMessage[];
|
|
14
|
+
tier2Chunks: ScoredChunk[];
|
|
15
|
+
tier3Summary?: string;
|
|
16
|
+
allocation: BudgetAllocation;
|
|
17
|
+
originalTokens: number;
|
|
18
|
+
optimizedTokens: number;
|
|
19
|
+
savingsPercent: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function getModelContextLimit(model: string): number;
|
|
22
|
+
/**
|
|
23
|
+
* Allocate token budget across tiers and pack context.
|
|
24
|
+
*/
|
|
25
|
+
export declare function packContext(systemPrompt: string | undefined, messages: CanonicalMessage[], retrievedChunks: ScoredChunk[], model: string, tier1Exchanges: number, tier3Reserve: number, responseReserve: number): PackedContext;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getModelContextLimit = getModelContextLimit;
|
|
4
|
+
exports.packContext = packContext;
|
|
5
|
+
const canonical_js_1 = require("./canonical.js");
|
|
6
|
+
const chunker_js_1 = require("./chunker.js");
|
|
7
|
+
/** Known model context window sizes */
|
|
8
|
+
const MODEL_CONTEXT_LIMITS = {
|
|
9
|
+
'claude-opus-4-6': 200000,
|
|
10
|
+
'claude-sonnet-4-6': 200000,
|
|
11
|
+
'claude-haiku-4-5-20251001': 200000,
|
|
12
|
+
'claude-3-5-sonnet-20241022': 200000,
|
|
13
|
+
'gpt-4o': 128000,
|
|
14
|
+
'gpt-4o-mini': 128000,
|
|
15
|
+
'gpt-4-turbo': 128000,
|
|
16
|
+
'o1': 200000,
|
|
17
|
+
'o1-mini': 128000,
|
|
18
|
+
};
|
|
19
|
+
const DEFAULT_CONTEXT_LIMIT = 128000;
|
|
20
|
+
function getModelContextLimit(model) {
|
|
21
|
+
// Check exact match
|
|
22
|
+
if (MODEL_CONTEXT_LIMITS[model])
|
|
23
|
+
return MODEL_CONTEXT_LIMITS[model];
|
|
24
|
+
// Check prefix match
|
|
25
|
+
for (const [key, limit] of Object.entries(MODEL_CONTEXT_LIMITS)) {
|
|
26
|
+
if (model.startsWith(key))
|
|
27
|
+
return limit;
|
|
28
|
+
}
|
|
29
|
+
return DEFAULT_CONTEXT_LIMIT;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Allocate token budget across tiers and pack context.
|
|
33
|
+
*/
|
|
34
|
+
function packContext(systemPrompt, messages, retrievedChunks, model, tier1Exchanges, tier3Reserve, responseReserve) {
|
|
35
|
+
const contextLimit = getModelContextLimit(model);
|
|
36
|
+
// Calculate original tokens
|
|
37
|
+
const originalTokens = (0, chunker_js_1.estimateTokens)(systemPrompt || '') +
|
|
38
|
+
messages.reduce((sum, m) => sum + (0, chunker_js_1.estimateTokens)((0, canonical_js_1.getTextContent)(m)), 0);
|
|
39
|
+
const systemPromptTokens = (0, chunker_js_1.estimateTokens)(systemPrompt || '');
|
|
40
|
+
// Extract Tier 1: last N exchanges (user+assistant pairs)
|
|
41
|
+
const tier1Messages = [];
|
|
42
|
+
let exchangeCount = 0;
|
|
43
|
+
for (let i = messages.length - 1; i >= 0 && exchangeCount < tier1Exchanges; i--) {
|
|
44
|
+
tier1Messages.unshift(messages[i]);
|
|
45
|
+
if (messages[i].role === 'user')
|
|
46
|
+
exchangeCount++;
|
|
47
|
+
}
|
|
48
|
+
const tier1Tokens = tier1Messages.reduce((sum, m) => sum + (0, chunker_js_1.estimateTokens)((0, canonical_js_1.getTextContent)(m)), 0);
|
|
49
|
+
// Calculate available budget for Tier 2
|
|
50
|
+
const totalAvailable = contextLimit - systemPromptTokens - responseReserve;
|
|
51
|
+
const tier2Budget = Math.max(0, totalAvailable - tier1Tokens - tier3Reserve);
|
|
52
|
+
// Pack Tier 2 chunks greedily by score
|
|
53
|
+
const tier2Chunks = [];
|
|
54
|
+
let tier2Used = 0;
|
|
55
|
+
for (const chunk of retrievedChunks) {
|
|
56
|
+
if (tier2Used + chunk.metadata.tokenCount <= tier2Budget) {
|
|
57
|
+
tier2Chunks.push(chunk);
|
|
58
|
+
tier2Used += chunk.metadata.tokenCount;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Tier 3: summary placeholder (will be filled by summary system later)
|
|
62
|
+
const tier3Summary = undefined;
|
|
63
|
+
const optimizedTokens = systemPromptTokens + tier1Tokens + tier2Used;
|
|
64
|
+
const allocation = {
|
|
65
|
+
systemPromptTokens,
|
|
66
|
+
tier1Tokens,
|
|
67
|
+
tier2Budget,
|
|
68
|
+
tier3Reserve,
|
|
69
|
+
responseReserve,
|
|
70
|
+
totalAvailable,
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
systemPrompt,
|
|
74
|
+
tier1Messages,
|
|
75
|
+
tier2Chunks,
|
|
76
|
+
tier3Summary,
|
|
77
|
+
allocation,
|
|
78
|
+
originalTokens,
|
|
79
|
+
optimizedTokens,
|
|
80
|
+
savingsPercent: originalTokens > 0
|
|
81
|
+
? Math.round((1 - optimizedTokens / originalTokens) * 100)
|
|
82
|
+
: 0,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=budget.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface ContentBlock {
|
|
2
|
+
type: 'text' | 'image' | 'tool_use' | 'tool_result';
|
|
3
|
+
text?: string;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
export interface CanonicalMessage {
|
|
7
|
+
role: 'system' | 'user' | 'assistant' | 'tool';
|
|
8
|
+
content: string | ContentBlock[];
|
|
9
|
+
timestamp?: number;
|
|
10
|
+
metadata?: {
|
|
11
|
+
provider?: string;
|
|
12
|
+
model?: string;
|
|
13
|
+
tokens?: number;
|
|
14
|
+
files?: string[];
|
|
15
|
+
tools?: string[];
|
|
16
|
+
sessionId?: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export interface CanonicalRequest {
|
|
20
|
+
messages: CanonicalMessage[];
|
|
21
|
+
systemPrompt?: string;
|
|
22
|
+
model: string;
|
|
23
|
+
stream: boolean;
|
|
24
|
+
maxTokens?: number;
|
|
25
|
+
temperature?: number;
|
|
26
|
+
tools?: unknown[];
|
|
27
|
+
rawHeaders: Record<string, string>;
|
|
28
|
+
providerAuth: string;
|
|
29
|
+
}
|
|
30
|
+
export interface CanonicalResponse {
|
|
31
|
+
content: string | ContentBlock[];
|
|
32
|
+
model: string;
|
|
33
|
+
stopReason?: string;
|
|
34
|
+
usage?: {
|
|
35
|
+
inputTokens: number;
|
|
36
|
+
outputTokens: number;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export declare function getTextContent(msg: CanonicalMessage): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getTextContent = getTextContent;
|
|
4
|
+
function getTextContent(msg) {
|
|
5
|
+
if (typeof msg.content === 'string')
|
|
6
|
+
return msg.content;
|
|
7
|
+
return msg.content
|
|
8
|
+
.filter((b) => b.type === 'text' && b.text)
|
|
9
|
+
.map((b) => b.text)
|
|
10
|
+
.join('\n');
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=canonical.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CanonicalMessage } from './canonical.js';
|
|
2
|
+
import type { Chunk } from '../storage/types.js';
|
|
3
|
+
export declare function estimateTokens(text: string): number;
|
|
4
|
+
/**
|
|
5
|
+
* Chunk a conversation into indexable units.
|
|
6
|
+
* Each chunk = one user-assistant exchange pair.
|
|
7
|
+
* Long responses are split at paragraph boundaries.
|
|
8
|
+
*/
|
|
9
|
+
export declare function chunkConversation(messages: CanonicalMessage[], sessionId: string, baseTimestamp?: number): Chunk[];
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.estimateTokens = estimateTokens;
|
|
4
|
+
exports.chunkConversation = chunkConversation;
|
|
5
|
+
const canonical_js_1 = require("./canonical.js");
|
|
6
|
+
const node_crypto_1 = require("node:crypto");
|
|
7
|
+
const AVG_CHARS_PER_TOKEN = 4;
|
|
8
|
+
const MAX_CHUNK_TOKENS = 2000;
|
|
9
|
+
function estimateTokens(text) {
|
|
10
|
+
return Math.ceil(text.length / AVG_CHARS_PER_TOKEN);
|
|
11
|
+
}
|
|
12
|
+
/** Extract file paths mentioned in text */
|
|
13
|
+
function extractFilePaths(text) {
|
|
14
|
+
const patterns = [
|
|
15
|
+
/(?:^|\s)((?:\/[\w.-]+)+(?:\.\w+)?)/gm,
|
|
16
|
+
/(?:^|\s)((?:[\w.-]+\/)+[\w.-]+(?:\.\w+)?)/gm,
|
|
17
|
+
/`([^`]*\/[^`]*\.\w+)`/g,
|
|
18
|
+
];
|
|
19
|
+
const files = new Set();
|
|
20
|
+
for (const pattern of patterns) {
|
|
21
|
+
let match;
|
|
22
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
23
|
+
const p = match[1].trim();
|
|
24
|
+
if (p.length > 3 && p.includes('/') && !p.startsWith('http')) {
|
|
25
|
+
files.add(p);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return Array.from(files);
|
|
30
|
+
}
|
|
31
|
+
/** Extract tool names from text */
|
|
32
|
+
function extractTools(text) {
|
|
33
|
+
const tools = new Set();
|
|
34
|
+
const patterns = [
|
|
35
|
+
/tool[_\s]?(?:use|call|result)[:\s]+(\w+)/gi,
|
|
36
|
+
];
|
|
37
|
+
for (const pattern of patterns) {
|
|
38
|
+
let match;
|
|
39
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
40
|
+
tools.add(match[1]);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return Array.from(tools);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Chunk a conversation into indexable units.
|
|
47
|
+
* Each chunk = one user-assistant exchange pair.
|
|
48
|
+
* Long responses are split at paragraph boundaries.
|
|
49
|
+
*/
|
|
50
|
+
function chunkConversation(messages, sessionId, baseTimestamp) {
|
|
51
|
+
const chunks = [];
|
|
52
|
+
let exchangeIndex = 0;
|
|
53
|
+
for (let i = 0; i < messages.length; i++) {
|
|
54
|
+
const msg = messages[i];
|
|
55
|
+
if (msg.role !== 'user')
|
|
56
|
+
continue;
|
|
57
|
+
const userText = (0, canonical_js_1.getTextContent)(msg);
|
|
58
|
+
let assistantText = '';
|
|
59
|
+
for (let j = i + 1; j < messages.length; j++) {
|
|
60
|
+
if (messages[j].role === 'assistant') {
|
|
61
|
+
assistantText = (0, canonical_js_1.getTextContent)(messages[j]);
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const fullText = `User: ${userText}\n\nAssistant: ${assistantText}`;
|
|
66
|
+
const tokens = estimateTokens(fullText);
|
|
67
|
+
const files = extractFilePaths(fullText);
|
|
68
|
+
const tools = extractTools(fullText);
|
|
69
|
+
const summary = userText.slice(0, 100);
|
|
70
|
+
const timestamp = baseTimestamp ? baseTimestamp + exchangeIndex * 1000 : Date.now();
|
|
71
|
+
if (tokens <= MAX_CHUNK_TOKENS) {
|
|
72
|
+
chunks.push({
|
|
73
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
74
|
+
text: fullText,
|
|
75
|
+
embedding: [],
|
|
76
|
+
sessionId,
|
|
77
|
+
timestamp,
|
|
78
|
+
metadata: { files, tools, summary, tokenCount: tokens, exchangeIndex },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
const paragraphs = splitIntoParagraphs(fullText);
|
|
83
|
+
let current = '';
|
|
84
|
+
let partIndex = 0;
|
|
85
|
+
for (const para of paragraphs) {
|
|
86
|
+
if (estimateTokens(current + para) > MAX_CHUNK_TOKENS && current.length > 0) {
|
|
87
|
+
chunks.push({
|
|
88
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
89
|
+
text: current.trim(),
|
|
90
|
+
embedding: [],
|
|
91
|
+
sessionId,
|
|
92
|
+
timestamp,
|
|
93
|
+
metadata: {
|
|
94
|
+
files: extractFilePaths(current),
|
|
95
|
+
tools: extractTools(current),
|
|
96
|
+
summary: `${summary} (part ${partIndex + 1})`,
|
|
97
|
+
tokenCount: estimateTokens(current),
|
|
98
|
+
exchangeIndex,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
current = '';
|
|
102
|
+
partIndex++;
|
|
103
|
+
}
|
|
104
|
+
current += para + '\n\n';
|
|
105
|
+
}
|
|
106
|
+
if (current.trim()) {
|
|
107
|
+
chunks.push({
|
|
108
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
109
|
+
text: current.trim(),
|
|
110
|
+
embedding: [],
|
|
111
|
+
sessionId,
|
|
112
|
+
timestamp,
|
|
113
|
+
metadata: {
|
|
114
|
+
files: extractFilePaths(current),
|
|
115
|
+
tools: extractTools(current),
|
|
116
|
+
summary: `${summary} (part ${partIndex + 1})`,
|
|
117
|
+
tokenCount: estimateTokens(current),
|
|
118
|
+
exchangeIndex,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
exchangeIndex++;
|
|
124
|
+
}
|
|
125
|
+
return chunks;
|
|
126
|
+
}
|
|
127
|
+
function splitIntoParagraphs(text) {
|
|
128
|
+
const parts = [];
|
|
129
|
+
let inCodeBlock = false;
|
|
130
|
+
let current = '';
|
|
131
|
+
for (const line of text.split('\n')) {
|
|
132
|
+
if (line.startsWith('```')) {
|
|
133
|
+
inCodeBlock = !inCodeBlock;
|
|
134
|
+
}
|
|
135
|
+
if (!inCodeBlock && line.trim() === '' && current.trim()) {
|
|
136
|
+
parts.push(current.trim());
|
|
137
|
+
current = '';
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
current += line + '\n';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (current.trim()) {
|
|
144
|
+
parts.push(current.trim());
|
|
145
|
+
}
|
|
146
|
+
return parts;
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=chunker.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { CanonicalRequest, CanonicalMessage } from './canonical.js';
|
|
2
|
+
import { type RetrievalResult } from './retriever.js';
|
|
3
|
+
import { type PackedContext } from './budget.js';
|
|
4
|
+
import type { EmbeddingAdapter } from '../embedding/types.js';
|
|
5
|
+
import type { StorageAdapter } from '../storage/types.js';
|
|
6
|
+
import type { ContextConfig } from '../config/schema.js';
|
|
7
|
+
export interface OptimizationResult {
|
|
8
|
+
optimizedMessages: CanonicalMessage[];
|
|
9
|
+
systemPrompt?: string;
|
|
10
|
+
packed: PackedContext;
|
|
11
|
+
retrieval: RetrievalResult | null;
|
|
12
|
+
passThrough: boolean;
|
|
13
|
+
reason?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare class ContextOptimizer {
|
|
16
|
+
private embedding;
|
|
17
|
+
private storage;
|
|
18
|
+
private config;
|
|
19
|
+
private retriever;
|
|
20
|
+
constructor(embedding: EmbeddingAdapter, storage: StorageAdapter, config: ContextConfig);
|
|
21
|
+
/**
|
|
22
|
+
* Optimize context for a request.
|
|
23
|
+
* Returns pass-through if optimization isn't beneficial.
|
|
24
|
+
*/
|
|
25
|
+
optimize(request: CanonicalRequest): Promise<OptimizationResult>;
|
|
26
|
+
/**
|
|
27
|
+
* Index a completed exchange for future retrieval.
|
|
28
|
+
* Called asynchronously after response completes.
|
|
29
|
+
*/
|
|
30
|
+
indexExchange(messages: CanonicalMessage[], sessionId: string): Promise<void>;
|
|
31
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ContextOptimizer = void 0;
|
|
4
|
+
const canonical_js_1 = require("./canonical.js");
|
|
5
|
+
const chunker_js_1 = require("./chunker.js");
|
|
6
|
+
const retriever_js_1 = require("./retriever.js");
|
|
7
|
+
const budget_js_1 = require("./budget.js");
|
|
8
|
+
class ContextOptimizer {
|
|
9
|
+
embedding;
|
|
10
|
+
storage;
|
|
11
|
+
config;
|
|
12
|
+
retriever;
|
|
13
|
+
constructor(embedding, storage, config) {
|
|
14
|
+
this.embedding = embedding;
|
|
15
|
+
this.storage = storage;
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.retriever = new retriever_js_1.Retriever(embedding, storage, config);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Optimize context for a request.
|
|
21
|
+
* Returns pass-through if optimization isn't beneficial.
|
|
22
|
+
*/
|
|
23
|
+
async optimize(request) {
|
|
24
|
+
const { messages, systemPrompt, model } = request;
|
|
25
|
+
// Calculate original size
|
|
26
|
+
const originalTokens = (0, chunker_js_1.estimateTokens)(systemPrompt || '') +
|
|
27
|
+
messages.reduce((sum, m) => sum + (0, chunker_js_1.estimateTokens)((0, canonical_js_1.getTextContent)(m)), 0);
|
|
28
|
+
// Don't optimize short conversations (< tier1 threshold)
|
|
29
|
+
const exchangeCount = messages.filter((m) => m.role === 'user').length;
|
|
30
|
+
if (exchangeCount <= this.config.tier1_exchanges) {
|
|
31
|
+
return {
|
|
32
|
+
optimizedMessages: messages,
|
|
33
|
+
systemPrompt,
|
|
34
|
+
packed: {
|
|
35
|
+
systemPrompt,
|
|
36
|
+
tier1Messages: messages,
|
|
37
|
+
tier2Chunks: [],
|
|
38
|
+
allocation: {
|
|
39
|
+
systemPromptTokens: (0, chunker_js_1.estimateTokens)(systemPrompt || ''),
|
|
40
|
+
tier1Tokens: originalTokens - (0, chunker_js_1.estimateTokens)(systemPrompt || ''),
|
|
41
|
+
tier2Budget: 0,
|
|
42
|
+
tier3Reserve: 0,
|
|
43
|
+
responseReserve: this.config.response_reserve_tokens,
|
|
44
|
+
totalAvailable: originalTokens,
|
|
45
|
+
},
|
|
46
|
+
originalTokens,
|
|
47
|
+
optimizedTokens: originalTokens,
|
|
48
|
+
savingsPercent: 0,
|
|
49
|
+
},
|
|
50
|
+
retrieval: null,
|
|
51
|
+
passThrough: true,
|
|
52
|
+
reason: 'conversation too short for optimization',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// Extract query from last user message
|
|
56
|
+
const lastUserMsg = [...messages].reverse().find((m) => m.role === 'user');
|
|
57
|
+
const queryText = lastUserMsg ? (0, canonical_js_1.getTextContent)(lastUserMsg) : '';
|
|
58
|
+
// Extract mentioned file paths from query
|
|
59
|
+
const filePathPattern = /(?:[\w.-]+\/)+[\w.-]+(?:\.\w+)?/g;
|
|
60
|
+
const mentionedFiles = queryText.match(filePathPattern) || [];
|
|
61
|
+
// Generate session ID from system prompt hash (stable across requests)
|
|
62
|
+
const sessionId = request.rawHeaders['x-smartcontext-session'] ||
|
|
63
|
+
`session-${simpleHash(systemPrompt || 'default')}`;
|
|
64
|
+
// Retrieve relevant chunks
|
|
65
|
+
let retrieval;
|
|
66
|
+
try {
|
|
67
|
+
retrieval = await this.retriever.retrieve(queryText, sessionId, mentionedFiles);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
// Graceful degradation: retrieval failed, pass through
|
|
71
|
+
return {
|
|
72
|
+
optimizedMessages: messages,
|
|
73
|
+
systemPrompt,
|
|
74
|
+
packed: {
|
|
75
|
+
systemPrompt,
|
|
76
|
+
tier1Messages: messages,
|
|
77
|
+
tier2Chunks: [],
|
|
78
|
+
allocation: {
|
|
79
|
+
systemPromptTokens: (0, chunker_js_1.estimateTokens)(systemPrompt || ''),
|
|
80
|
+
tier1Tokens: originalTokens - (0, chunker_js_1.estimateTokens)(systemPrompt || ''),
|
|
81
|
+
tier2Budget: 0,
|
|
82
|
+
tier3Reserve: 0,
|
|
83
|
+
responseReserve: this.config.response_reserve_tokens,
|
|
84
|
+
totalAvailable: originalTokens,
|
|
85
|
+
},
|
|
86
|
+
originalTokens,
|
|
87
|
+
optimizedTokens: originalTokens,
|
|
88
|
+
savingsPercent: 0,
|
|
89
|
+
},
|
|
90
|
+
retrieval: null,
|
|
91
|
+
passThrough: true,
|
|
92
|
+
reason: `retrieval failed: ${err}`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// Pack context with budget allocation
|
|
96
|
+
const packed = (0, budget_js_1.packContext)(systemPrompt, messages, retrieval.chunks, model, this.config.tier1_exchanges, this.config.tier3_token_reserve, this.config.response_reserve_tokens);
|
|
97
|
+
// If savings < 10%, not worth the risk — pass through
|
|
98
|
+
if (packed.savingsPercent < 10) {
|
|
99
|
+
return {
|
|
100
|
+
optimizedMessages: messages,
|
|
101
|
+
systemPrompt,
|
|
102
|
+
packed,
|
|
103
|
+
retrieval,
|
|
104
|
+
passThrough: true,
|
|
105
|
+
reason: `savings too low (${packed.savingsPercent}%)`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// Assemble optimized messages
|
|
109
|
+
const optimizedMessages = [];
|
|
110
|
+
// Add retrieved context as a system-injected context block
|
|
111
|
+
if (packed.tier2Chunks.length > 0) {
|
|
112
|
+
const contextBlock = packed.tier2Chunks
|
|
113
|
+
.map((c) => `[Context from ${new Date(c.timestamp).toISOString().slice(0, 19)}]\n${c.text}`)
|
|
114
|
+
.join('\n\n---\n\n');
|
|
115
|
+
optimizedMessages.push({
|
|
116
|
+
role: 'user',
|
|
117
|
+
content: `[Retrieved context from previous exchanges]\n\n${contextBlock}`,
|
|
118
|
+
metadata: { provider: 'smartcontext' },
|
|
119
|
+
});
|
|
120
|
+
optimizedMessages.push({
|
|
121
|
+
role: 'assistant',
|
|
122
|
+
content: 'I have reviewed the retrieved context from our previous exchanges. I\'ll use this information to provide more informed responses.',
|
|
123
|
+
metadata: { provider: 'smartcontext' },
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// Add Tier 1 messages (last N exchanges, verbatim)
|
|
127
|
+
optimizedMessages.push(...packed.tier1Messages);
|
|
128
|
+
return {
|
|
129
|
+
optimizedMessages,
|
|
130
|
+
systemPrompt,
|
|
131
|
+
packed,
|
|
132
|
+
retrieval,
|
|
133
|
+
passThrough: false,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Index a completed exchange for future retrieval.
|
|
138
|
+
* Called asynchronously after response completes.
|
|
139
|
+
*/
|
|
140
|
+
async indexExchange(messages, sessionId) {
|
|
141
|
+
const chunks = (0, chunker_js_1.chunkConversation)(messages, sessionId);
|
|
142
|
+
if (chunks.length === 0)
|
|
143
|
+
return;
|
|
144
|
+
// Embed all chunks
|
|
145
|
+
const texts = chunks.map((c) => c.text);
|
|
146
|
+
const embeddings = await this.embedding.embed(texts);
|
|
147
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
148
|
+
chunks[i].embedding = embeddings[i];
|
|
149
|
+
}
|
|
150
|
+
await this.storage.upsertChunks(chunks);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
exports.ContextOptimizer = ContextOptimizer;
|
|
154
|
+
function simpleHash(str) {
|
|
155
|
+
let hash = 0;
|
|
156
|
+
for (let i = 0; i < str.length; i++) {
|
|
157
|
+
const char = str.charCodeAt(i);
|
|
158
|
+
hash = ((hash << 5) - hash) + char;
|
|
159
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
160
|
+
}
|
|
161
|
+
return Math.abs(hash).toString(36);
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=optimizer.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { EmbeddingAdapter } from '../embedding/types.js';
|
|
2
|
+
import type { StorageAdapter, ScoredChunk } from '../storage/types.js';
|
|
3
|
+
import type { ContextConfig } from '../config/schema.js';
|
|
4
|
+
export interface RetrievalResult {
|
|
5
|
+
chunks: ScoredChunk[];
|
|
6
|
+
queryEmbeddingMs: number;
|
|
7
|
+
searchMs: number;
|
|
8
|
+
candidates: number;
|
|
9
|
+
aboveThreshold: number;
|
|
10
|
+
afterDedup: number;
|
|
11
|
+
topScore: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Retrieval pipeline:
|
|
15
|
+
* 1. Embed query
|
|
16
|
+
* 2. Vector search (top-K candidates)
|
|
17
|
+
* 3. Apply boosts (recency, file-path)
|
|
18
|
+
* 4. Dedup near-duplicates
|
|
19
|
+
* 5. Confidence gate
|
|
20
|
+
*/
|
|
21
|
+
export declare class Retriever {
|
|
22
|
+
private embedding;
|
|
23
|
+
private storage;
|
|
24
|
+
private config;
|
|
25
|
+
constructor(embedding: EmbeddingAdapter, storage: StorageAdapter, config: ContextConfig);
|
|
26
|
+
retrieve(queryText: string, currentSessionId: string, mentionedFiles: string[]): Promise<RetrievalResult>;
|
|
27
|
+
/** Remove near-duplicate chunks (same content, different IDs) */
|
|
28
|
+
private dedup;
|
|
29
|
+
}
|