@psiclawops/hypermem 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.
Files changed (94) hide show
  1. package/ARCHITECTURE.md +296 -0
  2. package/LICENSE +190 -0
  3. package/README.md +243 -0
  4. package/dist/background-indexer.d.ts +117 -0
  5. package/dist/background-indexer.d.ts.map +1 -0
  6. package/dist/background-indexer.js +732 -0
  7. package/dist/compaction-fence.d.ts +89 -0
  8. package/dist/compaction-fence.d.ts.map +1 -0
  9. package/dist/compaction-fence.js +153 -0
  10. package/dist/compositor.d.ts +139 -0
  11. package/dist/compositor.d.ts.map +1 -0
  12. package/dist/compositor.js +1109 -0
  13. package/dist/cross-agent.d.ts +57 -0
  14. package/dist/cross-agent.d.ts.map +1 -0
  15. package/dist/cross-agent.js +254 -0
  16. package/dist/db.d.ts +131 -0
  17. package/dist/db.d.ts.map +1 -0
  18. package/dist/db.js +398 -0
  19. package/dist/desired-state-store.d.ts +100 -0
  20. package/dist/desired-state-store.d.ts.map +1 -0
  21. package/dist/desired-state-store.js +212 -0
  22. package/dist/doc-chunk-store.d.ts +115 -0
  23. package/dist/doc-chunk-store.d.ts.map +1 -0
  24. package/dist/doc-chunk-store.js +278 -0
  25. package/dist/doc-chunker.d.ts +99 -0
  26. package/dist/doc-chunker.d.ts.map +1 -0
  27. package/dist/doc-chunker.js +324 -0
  28. package/dist/episode-store.d.ts +48 -0
  29. package/dist/episode-store.d.ts.map +1 -0
  30. package/dist/episode-store.js +135 -0
  31. package/dist/fact-store.d.ts +57 -0
  32. package/dist/fact-store.d.ts.map +1 -0
  33. package/dist/fact-store.js +175 -0
  34. package/dist/fleet-store.d.ts +144 -0
  35. package/dist/fleet-store.d.ts.map +1 -0
  36. package/dist/fleet-store.js +276 -0
  37. package/dist/hybrid-retrieval.d.ts +60 -0
  38. package/dist/hybrid-retrieval.d.ts.map +1 -0
  39. package/dist/hybrid-retrieval.js +340 -0
  40. package/dist/index.d.ts +611 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +1042 -0
  43. package/dist/knowledge-graph.d.ts +110 -0
  44. package/dist/knowledge-graph.d.ts.map +1 -0
  45. package/dist/knowledge-graph.js +305 -0
  46. package/dist/knowledge-store.d.ts +72 -0
  47. package/dist/knowledge-store.d.ts.map +1 -0
  48. package/dist/knowledge-store.js +241 -0
  49. package/dist/library-schema.d.ts +22 -0
  50. package/dist/library-schema.d.ts.map +1 -0
  51. package/dist/library-schema.js +717 -0
  52. package/dist/message-store.d.ts +76 -0
  53. package/dist/message-store.d.ts.map +1 -0
  54. package/dist/message-store.js +273 -0
  55. package/dist/preference-store.d.ts +54 -0
  56. package/dist/preference-store.d.ts.map +1 -0
  57. package/dist/preference-store.js +109 -0
  58. package/dist/preservation-gate.d.ts +82 -0
  59. package/dist/preservation-gate.d.ts.map +1 -0
  60. package/dist/preservation-gate.js +150 -0
  61. package/dist/provider-translator.d.ts +40 -0
  62. package/dist/provider-translator.d.ts.map +1 -0
  63. package/dist/provider-translator.js +349 -0
  64. package/dist/rate-limiter.d.ts +76 -0
  65. package/dist/rate-limiter.d.ts.map +1 -0
  66. package/dist/rate-limiter.js +179 -0
  67. package/dist/redis.d.ts +188 -0
  68. package/dist/redis.d.ts.map +1 -0
  69. package/dist/redis.js +534 -0
  70. package/dist/schema.d.ts +15 -0
  71. package/dist/schema.d.ts.map +1 -0
  72. package/dist/schema.js +203 -0
  73. package/dist/secret-scanner.d.ts +51 -0
  74. package/dist/secret-scanner.d.ts.map +1 -0
  75. package/dist/secret-scanner.js +248 -0
  76. package/dist/seed.d.ts +108 -0
  77. package/dist/seed.d.ts.map +1 -0
  78. package/dist/seed.js +177 -0
  79. package/dist/system-store.d.ts +73 -0
  80. package/dist/system-store.d.ts.map +1 -0
  81. package/dist/system-store.js +182 -0
  82. package/dist/topic-store.d.ts +45 -0
  83. package/dist/topic-store.d.ts.map +1 -0
  84. package/dist/topic-store.js +136 -0
  85. package/dist/types.d.ts +329 -0
  86. package/dist/types.d.ts.map +1 -0
  87. package/dist/types.js +9 -0
  88. package/dist/vector-store.d.ts +132 -0
  89. package/dist/vector-store.d.ts.map +1 -0
  90. package/dist/vector-store.js +498 -0
  91. package/dist/work-store.d.ts +112 -0
  92. package/dist/work-store.d.ts.map +1 -0
  93. package/dist/work-store.js +273 -0
  94. package/package.json +57 -0
@@ -0,0 +1,150 @@
1
+ /**
2
+ * HyperMem Preservation Gate
3
+ *
4
+ * Verifies that a proposed compaction summary preserves the semantic
5
+ * content of its source messages by measuring geometric fidelity in
6
+ * embedding space.
7
+ *
8
+ * Before a summary replaces raw messages, it must pass two checks:
9
+ *
10
+ * 1. Centroid Alignment — the summary embedding must be close to the
11
+ * centroid of the source message embeddings (cos similarity).
12
+ *
13
+ * 2. Source Coverage — the summary must have positive cosine similarity
14
+ * with each individual source message (averaged).
15
+ *
16
+ * If the combined preservation score falls below the threshold, the
17
+ * summary is rejected. The caller should fall back to extractive
18
+ * compaction (concatenation/selection) rather than accepting a
19
+ * semantically drifted summary.
20
+ *
21
+ * This prevents the silent failure mode where a confident summarizer
22
+ * produces fluent text that has drifted away from the original meaning
23
+ * in vector space — making it unretrievable by the very system that
24
+ * will later search for it.
25
+ *
26
+ * Inspired by the Nomic-space preservation gate in openclaw-memory-libravdb
27
+ * (mathematics-v2.md §5.3), adapted for our Ollama + sqlite-vec stack.
28
+ */
29
+ import { generateEmbeddings } from './vector-store.js';
30
+ const DEFAULT_PRESERVATION_CONFIG = {
31
+ threshold: 0.65,
32
+ };
33
+ // ─── Math Utilities ─────────────────────────────────────────────
34
+ /**
35
+ * Cosine similarity between two Float32Arrays.
36
+ * Returns value in [-1, 1]. Handles zero-norm vectors gracefully (returns 0).
37
+ */
38
+ function cosineSimilarity(a, b) {
39
+ if (a.length !== b.length) {
40
+ throw new Error(`Vector dimension mismatch: ${a.length} vs ${b.length}`);
41
+ }
42
+ let dot = 0;
43
+ let normA = 0;
44
+ let normB = 0;
45
+ for (let i = 0; i < a.length; i++) {
46
+ dot += a[i] * b[i];
47
+ normA += a[i] * a[i];
48
+ normB += b[i] * b[i];
49
+ }
50
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
51
+ if (denom === 0)
52
+ return 0;
53
+ return dot / denom;
54
+ }
55
+ /**
56
+ * Compute the centroid (element-wise mean) of an array of vectors.
57
+ */
58
+ function computeCentroid(vectors) {
59
+ if (vectors.length === 0) {
60
+ throw new Error('Cannot compute centroid of empty vector set');
61
+ }
62
+ const dim = vectors[0].length;
63
+ const centroid = new Float32Array(dim);
64
+ for (const vec of vectors) {
65
+ for (let i = 0; i < dim; i++) {
66
+ centroid[i] += vec[i];
67
+ }
68
+ }
69
+ const n = vectors.length;
70
+ for (let i = 0; i < dim; i++) {
71
+ centroid[i] /= n;
72
+ }
73
+ return centroid;
74
+ }
75
+ // ─── Preservation Gate ──────────────────────────────────────────
76
+ /**
77
+ * Verify that a summary preserves its source content in embedding space.
78
+ *
79
+ * SYNCHRONOUS PATH — for when you already have pre-computed embeddings
80
+ * (e.g., from the background indexer or vector store cache).
81
+ *
82
+ * This is the preferred path: no network calls, no async, deterministic.
83
+ *
84
+ * @param summaryEmbedding - The embedding of the proposed summary
85
+ * @param sourceEmbeddings - Embeddings of the source messages being replaced
86
+ * @param config - Preservation threshold config
87
+ */
88
+ export function verifyPreservationFromVectors(summaryEmbedding, sourceEmbeddings, config = {}) {
89
+ const threshold = config.threshold ?? DEFAULT_PRESERVATION_CONFIG.threshold;
90
+ if (sourceEmbeddings.length === 0) {
91
+ return {
92
+ alignment: 0,
93
+ coverage: 0,
94
+ score: 0,
95
+ passed: false,
96
+ threshold,
97
+ };
98
+ }
99
+ // 1. Centroid alignment
100
+ const centroid = computeCentroid(sourceEmbeddings);
101
+ const alignment = cosineSimilarity(summaryEmbedding, centroid);
102
+ // 2. Source coverage (average positive cosine similarity)
103
+ let coverageSum = 0;
104
+ for (const src of sourceEmbeddings) {
105
+ coverageSum += Math.max(0, cosineSimilarity(summaryEmbedding, src));
106
+ }
107
+ const coverage = coverageSum / sourceEmbeddings.length;
108
+ // 3. Combined score, clamped to [0, 1]
109
+ const rawScore = (alignment + coverage) / 2;
110
+ const score = Math.max(0, Math.min(1, rawScore));
111
+ return {
112
+ alignment,
113
+ coverage,
114
+ score,
115
+ passed: score >= threshold,
116
+ threshold,
117
+ };
118
+ }
119
+ /**
120
+ * Verify that a summary preserves its source content in embedding space.
121
+ *
122
+ * ASYNC PATH — generates embeddings via Ollama on demand.
123
+ * Use when pre-computed embeddings aren't available.
124
+ *
125
+ * This makes N+1 embedding calls (1 for summary, N for sources if not cached).
126
+ * For batch compaction, prefer pre-computing embeddings and using the sync path.
127
+ *
128
+ * @param summaryText - The proposed summary text
129
+ * @param sourceTexts - The source message texts being replaced
130
+ * @param config - Preservation threshold and embedding config
131
+ */
132
+ export async function verifyPreservation(summaryText, sourceTexts, config = {}) {
133
+ const threshold = config.threshold ?? DEFAULT_PRESERVATION_CONFIG.threshold;
134
+ if (sourceTexts.length === 0) {
135
+ return {
136
+ alignment: 0,
137
+ coverage: 0,
138
+ score: 0,
139
+ passed: false,
140
+ threshold,
141
+ };
142
+ }
143
+ // Batch all texts into one embedding call for efficiency
144
+ const allTexts = [summaryText, ...sourceTexts];
145
+ const allEmbeddings = await generateEmbeddings(allTexts, config.embedding);
146
+ const summaryEmbedding = allEmbeddings[0];
147
+ const sourceEmbeddings = allEmbeddings.slice(1);
148
+ return verifyPreservationFromVectors(summaryEmbedding, sourceEmbeddings, config);
149
+ }
150
+ //# sourceMappingURL=preservation-gate.js.map
@@ -0,0 +1,40 @@
1
+ /**
2
+ * HyperMem Provider Translator
3
+ *
4
+ * Converts between provider-neutral (NeutralMessage) and provider-specific formats.
5
+ * This is the ONLY place where provider-specific formatting exists.
6
+ * Storage is always neutral. Translation happens at the send/receive boundary.
7
+ *
8
+ * This eliminates grafting/stripping entirely — tool calls are stored as structured
9
+ * data, and each provider gets the format it expects at send time.
10
+ */
11
+ import type { NeutralMessage, NeutralToolResult, ProviderMessage } from './types.js';
12
+ /**
13
+ * Generate a HyperMem-native tool call ID.
14
+ * These are provider-neutral and deterministic within a session.
15
+ */
16
+ export declare function generateToolCallId(): string;
17
+ /**
18
+ * Convert a provider-specific tool call ID to a HyperMem ID.
19
+ * Deterministic: same input always produces same output.
20
+ */
21
+ export declare function normalizeToolCallId(providerId: string): string;
22
+ export type ProviderType = 'anthropic' | 'openai' | 'openai-responses' | 'unknown';
23
+ export declare function detectProvider(providerString: string | null | undefined): ProviderType;
24
+ /**
25
+ * Convert neutral messages to provider-specific format.
26
+ */
27
+ export declare function toProviderFormat(messages: NeutralMessage[], provider: string | null | undefined): ProviderMessage[];
28
+ /**
29
+ * Convert a provider-specific response to neutral format.
30
+ */
31
+ export declare function fromProviderFormat(response: Record<string, unknown>, provider: string): NeutralMessage;
32
+ /**
33
+ * Convert a user message (from chat input) to neutral format.
34
+ */
35
+ export declare function userMessageToNeutral(content: string, metadata?: Record<string, unknown>): NeutralMessage;
36
+ /**
37
+ * Convert tool results to a neutral user message.
38
+ */
39
+ export declare function toolResultsToNeutral(results: NeutralToolResult[]): NeutralMessage;
40
+ //# sourceMappingURL=provider-translator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-translator.d.ts","sourceRoot":"","sources":["../src/provider-translator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EACV,cAAc,EAEd,iBAAiB,EACjB,eAAe,EAChB,MAAM,YAAY,CAAC;AAOpB;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAK3C;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAI9D;AAID,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,QAAQ,GAAG,kBAAkB,GAAG,SAAS,CAAC;AAEnF,wBAAgB,cAAc,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,YAAY,CAOtF;AAgMD;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,cAAc,EAAE,EAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAClC,eAAe,EAAE,CAcnB;AA8ED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,QAAQ,EAAE,MAAM,GACf,cAAc,CAYhB;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,cAAc,CAQxG;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,iBAAiB,EAAE,GAAG,cAAc,CAOjF"}
@@ -0,0 +1,349 @@
1
+ /**
2
+ * HyperMem Provider Translator
3
+ *
4
+ * Converts between provider-neutral (NeutralMessage) and provider-specific formats.
5
+ * This is the ONLY place where provider-specific formatting exists.
6
+ * Storage is always neutral. Translation happens at the send/receive boundary.
7
+ *
8
+ * This eliminates grafting/stripping entirely — tool calls are stored as structured
9
+ * data, and each provider gets the format it expects at send time.
10
+ */
11
+ import { createHash } from 'node:crypto';
12
+ // ─── ID Generation ───────────────────────────────────────────────
13
+ let idCounter = 0;
14
+ /**
15
+ * Generate a HyperMem-native tool call ID.
16
+ * These are provider-neutral and deterministic within a session.
17
+ */
18
+ export function generateToolCallId() {
19
+ idCounter++;
20
+ const timestamp = Date.now().toString(36);
21
+ const counter = idCounter.toString(36).padStart(4, '0');
22
+ return `hm_${timestamp}_${counter}`;
23
+ }
24
+ /**
25
+ * Convert a provider-specific tool call ID to a HyperMem ID.
26
+ * Deterministic: same input always produces same output.
27
+ */
28
+ export function normalizeToolCallId(providerId) {
29
+ if (providerId.startsWith('hm_'))
30
+ return providerId; // already normalized
31
+ const hash = createHash('sha256').update(providerId).digest('hex').substring(0, 12);
32
+ return `hm_${hash}`;
33
+ }
34
+ export function detectProvider(providerString) {
35
+ if (!providerString)
36
+ return 'unknown';
37
+ const lower = providerString.toLowerCase();
38
+ if (lower.includes('anthropic') || lower.includes('claude'))
39
+ return 'anthropic';
40
+ if (lower.includes('codex') || lower.includes('responses'))
41
+ return 'openai-responses';
42
+ if (lower.includes('openai') || lower.includes('gpt') || lower.includes('copilot'))
43
+ return 'openai';
44
+ return 'unknown';
45
+ }
46
+ // ─── To Provider Format ──────────────────────────────────────────
47
+ /**
48
+ * Convert neutral messages to Anthropic Messages API format.
49
+ *
50
+ * Prompt caching (DYNAMIC_BOUNDARY):
51
+ * Anthropic supports prompt caching via cache_control on content blocks.
52
+ * The last system message BEFORE the dynamicBoundary marker gets
53
+ * cache_control: {type: "ephemeral"} to mark the static/dynamic boundary.
54
+ *
55
+ * Static (cacheable): system prompt + identity — stable across sessions
56
+ * Dynamic (not cacheable): context block (facts/recall), conversation history
57
+ *
58
+ * This allows Anthropic to cache the static prefix and skip re-tokenizing it.
59
+ */
60
+ function toAnthropic(messages) {
61
+ const result = [];
62
+ // Find the last static system message index (before any dynamicBoundary message)
63
+ // so we can mark it with cache_control.
64
+ let lastStaticSystemIdx = -1;
65
+ for (let i = 0; i < messages.length; i++) {
66
+ const msg = messages[i];
67
+ if (msg.role === 'system' && !msg.metadata?.dynamicBoundary) {
68
+ lastStaticSystemIdx = i;
69
+ }
70
+ else if (msg.metadata?.dynamicBoundary) {
71
+ // Stop scanning — everything after the boundary marker is dynamic
72
+ break;
73
+ }
74
+ }
75
+ for (let i = 0; i < messages.length; i++) {
76
+ const msg = messages[i];
77
+ if (msg.role === 'system') {
78
+ // Anthropic system messages are handled separately (system parameter)
79
+ // Include them as-is; the gateway will extract them.
80
+ // Mark the last static system message as the cache boundary.
81
+ const isLastStatic = i === lastStaticSystemIdx;
82
+ const providerMsg = {
83
+ role: 'system',
84
+ content: msg.textContent || '',
85
+ };
86
+ if (isLastStatic) {
87
+ // Add cache_control as a hint to the gateway/Anthropic API.
88
+ // The gateway is responsible for lifting this into the correct API position.
89
+ providerMsg.cache_control = { type: 'ephemeral' };
90
+ }
91
+ result.push(providerMsg);
92
+ continue;
93
+ }
94
+ if (msg.role === 'assistant') {
95
+ const content = [];
96
+ if (msg.textContent) {
97
+ content.push({ type: 'text', text: msg.textContent });
98
+ }
99
+ if (msg.toolCalls) {
100
+ for (const tc of msg.toolCalls) {
101
+ // tc may be a NeutralToolCall { id, name, arguments: string }
102
+ // or a raw OpenClaw content block { type, id, name, input: object }
103
+ const rawTc = tc;
104
+ let input;
105
+ if (rawTc.input !== undefined) {
106
+ // Raw content block format — input is already an object
107
+ input = typeof rawTc.input === 'string' ? JSON.parse(rawTc.input) : rawTc.input;
108
+ }
109
+ else if (tc.arguments !== undefined) {
110
+ // NeutralToolCall format — arguments is a JSON string
111
+ input = typeof tc.arguments === 'string' ? JSON.parse(tc.arguments) : (tc.arguments ?? {});
112
+ }
113
+ else {
114
+ input = {};
115
+ }
116
+ content.push({
117
+ type: 'tool_use',
118
+ id: tc.id,
119
+ name: tc.name,
120
+ input,
121
+ });
122
+ }
123
+ }
124
+ result.push({
125
+ role: 'assistant',
126
+ content: content.length === 1 && typeof content[0] === 'object' && content[0].type === 'text'
127
+ ? msg.textContent || ''
128
+ : content,
129
+ });
130
+ continue;
131
+ }
132
+ if (msg.role === 'user') {
133
+ // Tool results go as user messages with tool_result content blocks
134
+ if (msg.toolResults && msg.toolResults.length > 0) {
135
+ const content = [];
136
+ for (const tr of msg.toolResults) {
137
+ content.push({
138
+ type: 'tool_result',
139
+ tool_use_id: tr.callId,
140
+ content: tr.content,
141
+ is_error: tr.isError || false,
142
+ });
143
+ }
144
+ result.push({ role: 'user', content });
145
+ }
146
+ else {
147
+ result.push({ role: 'user', content: msg.textContent || '' });
148
+ }
149
+ continue;
150
+ }
151
+ }
152
+ return result;
153
+ }
154
+ /**
155
+ * Convert neutral messages to OpenAI Chat Completions API format.
156
+ */
157
+ function toOpenAI(messages) {
158
+ const result = [];
159
+ for (const msg of messages) {
160
+ if (msg.role === 'system') {
161
+ result.push({ role: 'system', content: msg.textContent || '' });
162
+ continue;
163
+ }
164
+ if (msg.role === 'assistant') {
165
+ const providerMsg = {
166
+ role: 'assistant',
167
+ content: msg.textContent || null,
168
+ };
169
+ if (msg.toolCalls && msg.toolCalls.length > 0) {
170
+ providerMsg.tool_calls = msg.toolCalls.map(tc => {
171
+ // Handle both NeutralToolCall { arguments: string } and raw content block { input: object }
172
+ const rawTc = tc;
173
+ let args;
174
+ if (rawTc.input !== undefined) {
175
+ args = typeof rawTc.input === 'string' ? rawTc.input : JSON.stringify(rawTc.input);
176
+ }
177
+ else if (tc.arguments !== undefined) {
178
+ args = typeof tc.arguments === 'string' ? tc.arguments : JSON.stringify(tc.arguments);
179
+ }
180
+ else {
181
+ args = '{}';
182
+ }
183
+ return {
184
+ id: tc.id,
185
+ type: 'function',
186
+ function: {
187
+ name: tc.name,
188
+ arguments: args,
189
+ },
190
+ };
191
+ });
192
+ }
193
+ result.push(providerMsg);
194
+ continue;
195
+ }
196
+ if (msg.role === 'user') {
197
+ if (msg.toolResults && msg.toolResults.length > 0) {
198
+ // OpenAI tool results are separate "tool" role messages
199
+ for (const tr of msg.toolResults) {
200
+ result.push({
201
+ role: 'tool',
202
+ tool_call_id: tr.callId,
203
+ content: tr.content,
204
+ });
205
+ }
206
+ }
207
+ else {
208
+ result.push({ role: 'user', content: msg.textContent || '' });
209
+ }
210
+ continue;
211
+ }
212
+ }
213
+ return result;
214
+ }
215
+ /**
216
+ * Convert neutral messages to OpenAI Responses API format.
217
+ */
218
+ function toOpenAIResponses(messages) {
219
+ // Responses API uses a different item format
220
+ // For now, use the same as Chat Completions — the gateway handles the conversion
221
+ // This is a stub for when we need direct Responses API support
222
+ return toOpenAI(messages);
223
+ }
224
+ /**
225
+ * Convert neutral messages to provider-specific format.
226
+ */
227
+ export function toProviderFormat(messages, provider) {
228
+ const providerType = detectProvider(provider);
229
+ switch (providerType) {
230
+ case 'anthropic':
231
+ return toAnthropic(messages);
232
+ case 'openai':
233
+ return toOpenAI(messages);
234
+ case 'openai-responses':
235
+ return toOpenAIResponses(messages);
236
+ default:
237
+ // Default to OpenAI format as it's most widely compatible
238
+ return toOpenAI(messages);
239
+ }
240
+ }
241
+ // ─── From Provider Format ────────────────────────────────────────
242
+ /**
243
+ * Convert an Anthropic response to neutral format.
244
+ */
245
+ function fromAnthropic(response) {
246
+ const content = response.content;
247
+ let textContent = null;
248
+ let toolCalls = null;
249
+ if (typeof content === 'string') {
250
+ textContent = content;
251
+ }
252
+ else if (Array.isArray(content)) {
253
+ const textParts = [];
254
+ const tools = [];
255
+ for (const block of content) {
256
+ if (block.type === 'text') {
257
+ textParts.push(block.text);
258
+ }
259
+ else if (block.type === 'tool_use') {
260
+ tools.push({
261
+ id: normalizeToolCallId(block.id),
262
+ name: block.name,
263
+ arguments: JSON.stringify(block.input),
264
+ });
265
+ }
266
+ }
267
+ if (textParts.length > 0)
268
+ textContent = textParts.join('\n');
269
+ if (tools.length > 0)
270
+ toolCalls = tools;
271
+ }
272
+ return {
273
+ role: 'assistant',
274
+ textContent,
275
+ toolCalls,
276
+ toolResults: null,
277
+ metadata: {
278
+ originalProvider: 'anthropic',
279
+ stopReason: response.stop_reason,
280
+ model: response.model,
281
+ },
282
+ };
283
+ }
284
+ /**
285
+ * Convert an OpenAI response choice to neutral format.
286
+ */
287
+ function fromOpenAI(choice) {
288
+ const message = choice.message
289
+ || choice;
290
+ const textContent = message.content || null;
291
+ let toolCalls = null;
292
+ const rawToolCalls = message.tool_calls;
293
+ if (rawToolCalls && rawToolCalls.length > 0) {
294
+ toolCalls = rawToolCalls.map(tc => ({
295
+ id: normalizeToolCallId(tc.id),
296
+ name: tc.function.name,
297
+ arguments: tc.function.arguments,
298
+ }));
299
+ }
300
+ return {
301
+ role: 'assistant',
302
+ textContent,
303
+ toolCalls,
304
+ toolResults: null,
305
+ metadata: {
306
+ originalProvider: 'openai',
307
+ finishReason: message.finish_reason || choice.finish_reason,
308
+ },
309
+ };
310
+ }
311
+ /**
312
+ * Convert a provider-specific response to neutral format.
313
+ */
314
+ export function fromProviderFormat(response, provider) {
315
+ const providerType = detectProvider(provider);
316
+ switch (providerType) {
317
+ case 'anthropic':
318
+ return fromAnthropic(response);
319
+ case 'openai':
320
+ case 'openai-responses':
321
+ return fromOpenAI(response);
322
+ default:
323
+ return fromOpenAI(response);
324
+ }
325
+ }
326
+ /**
327
+ * Convert a user message (from chat input) to neutral format.
328
+ */
329
+ export function userMessageToNeutral(content, metadata) {
330
+ return {
331
+ role: 'user',
332
+ textContent: content,
333
+ toolCalls: null,
334
+ toolResults: null,
335
+ metadata,
336
+ };
337
+ }
338
+ /**
339
+ * Convert tool results to a neutral user message.
340
+ */
341
+ export function toolResultsToNeutral(results) {
342
+ return {
343
+ role: 'user',
344
+ textContent: null,
345
+ toolCalls: null,
346
+ toolResults: results,
347
+ };
348
+ }
349
+ //# sourceMappingURL=provider-translator.js.map
@@ -0,0 +1,76 @@
1
+ /**
2
+ * HyperMem Rate Limiter
3
+ *
4
+ * Token-bucket rate limiter for embedding API calls.
5
+ * Prevents hammering Ollama during bulk indexing.
6
+ *
7
+ * Strategy:
8
+ * - Burst: allow immediate calls up to bucket capacity
9
+ * - Sustained: refill tokens at a steady rate
10
+ * - Backpressure: when tokens exhausted, delay until available
11
+ * - Priority: high-priority requests (user-facing recall) get reserved tokens
12
+ *
13
+ * Usage:
14
+ * const limiter = new RateLimiter({ tokensPerSecond: 5, burstSize: 10 });
15
+ * await limiter.acquire(); // Waits if necessary
16
+ * const embeddings = await generateEmbeddings(texts);
17
+ */
18
+ export interface RateLimiterConfig {
19
+ /** Tokens refilled per second. Default: 5 */
20
+ tokensPerSecond: number;
21
+ /** Maximum burst capacity. Default: 10 */
22
+ burstSize: number;
23
+ /** Reserved tokens for high-priority requests. Default: 2 */
24
+ reservedHigh: number;
25
+ /** Maximum wait time before rejecting (ms). Default: 30000 (30s) */
26
+ maxWaitMs: number;
27
+ }
28
+ export type Priority = 'high' | 'normal' | 'low';
29
+ export declare class RateLimiter {
30
+ private tokens;
31
+ private lastRefill;
32
+ private readonly config;
33
+ private waitQueue;
34
+ private refillTimer;
35
+ private _totalAcquired;
36
+ private _totalWaited;
37
+ private _totalRejected;
38
+ constructor(config?: Partial<RateLimiterConfig>);
39
+ /**
40
+ * Acquire tokens. Blocks until tokens are available or maxWaitMs expires.
41
+ *
42
+ * @param count - Number of tokens to acquire (default 1)
43
+ * @param priority - Request priority (high gets reserved tokens)
44
+ * @throws Error if wait exceeds maxWaitMs
45
+ */
46
+ acquire(count?: number, priority?: Priority): Promise<void>;
47
+ /**
48
+ * Try to acquire tokens without waiting.
49
+ * Returns true if tokens were acquired, false if not.
50
+ */
51
+ tryAcquire(count?: number, priority?: Priority): boolean;
52
+ /**
53
+ * Get current limiter state.
54
+ */
55
+ get state(): {
56
+ availableTokens: number;
57
+ pendingRequests: number;
58
+ stats: {
59
+ acquired: number;
60
+ waited: number;
61
+ rejected: number;
62
+ };
63
+ };
64
+ /**
65
+ * Stop the refill timer.
66
+ */
67
+ destroy(): void;
68
+ private refill;
69
+ private processQueue;
70
+ }
71
+ /**
72
+ * Rate-limited embedding generator.
73
+ * Wraps generateEmbeddings with rate limiting.
74
+ */
75
+ export declare function createRateLimitedEmbedder(embedFn: (texts: string[]) => Promise<Float32Array[]>, limiter: RateLimiter): (texts: string[], priority?: Priority) => Promise<Float32Array[]>;
76
+ //# sourceMappingURL=rate-limiter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../src/rate-limiter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,iBAAiB;IAChC,6CAA6C;IAC7C,eAAe,EAAE,MAAM,CAAC;IACxB,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,YAAY,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;AASjD,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoB;IAC3C,OAAO,CAAC,SAAS,CAMT;IACR,OAAO,CAAC,WAAW,CAA+C;IAClE,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,cAAc,CAAK;gBAEf,MAAM,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC;IAS/C;;;;;;OAMG;IACG,OAAO,CAAC,KAAK,GAAE,MAAU,EAAE,QAAQ,GAAE,QAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAqC9E;;;OAGG;IACH,UAAU,CAAC,KAAK,GAAE,MAAU,EAAE,QAAQ,GAAE,QAAmB,GAAG,OAAO;IAgBrE;;OAEG;IACH,IAAI,KAAK,IAAI;QACX,eAAe,EAAE,MAAM,CAAC;QACxB,eAAe,EAAE,MAAM,CAAC;QACxB,KAAK,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAA;SAAE,CAAC;KAC/D,CAWA;IAED;;OAEG;IACH,OAAO,IAAI,IAAI;IAcf,OAAO,CAAC,MAAM;IAcd,OAAO,CAAC,YAAY;CAiCrB;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,YAAY,EAAE,CAAC,EACrD,OAAO,EAAE,WAAW,GACnB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,QAAQ,CAAC,EAAE,QAAQ,KAAK,OAAO,CAAC,YAAY,EAAE,CAAC,CASnE"}