@aitytech/agentkits-memory 1.0.0 → 2.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/LICENSE +21 -0
- package/README.md +267 -149
- package/assets/agentkits-memory-add-memory.png +0 -0
- package/assets/agentkits-memory-memory-detail.png +0 -0
- package/assets/agentkits-memory-memory-list.png +0 -0
- package/assets/logo.svg +24 -0
- package/dist/better-sqlite3-backend.d.ts +192 -0
- package/dist/better-sqlite3-backend.d.ts.map +1 -0
- package/dist/better-sqlite3-backend.js +801 -0
- package/dist/better-sqlite3-backend.js.map +1 -0
- package/dist/cli/save.js +0 -0
- package/dist/cli/setup.d.ts +6 -2
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +289 -42
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/viewer.js +25 -56
- package/dist/cli/viewer.js.map +1 -1
- package/dist/cli/web-viewer.d.ts +14 -0
- package/dist/cli/web-viewer.d.ts.map +1 -0
- package/dist/cli/web-viewer.js +1769 -0
- package/dist/cli/web-viewer.js.map +1 -0
- package/dist/embeddings/embedding-cache.d.ts +131 -0
- package/dist/embeddings/embedding-cache.d.ts.map +1 -0
- package/dist/embeddings/embedding-cache.js +217 -0
- package/dist/embeddings/embedding-cache.js.map +1 -0
- package/dist/embeddings/index.d.ts +11 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +11 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/embeddings/local-embeddings.d.ts +140 -0
- package/dist/embeddings/local-embeddings.d.ts.map +1 -0
- package/dist/embeddings/local-embeddings.js +293 -0
- package/dist/embeddings/local-embeddings.js.map +1 -0
- package/dist/hooks/context.d.ts +6 -1
- package/dist/hooks/context.d.ts.map +1 -1
- package/dist/hooks/context.js +12 -2
- package/dist/hooks/context.js.map +1 -1
- package/dist/hooks/observation.d.ts +6 -1
- package/dist/hooks/observation.d.ts.map +1 -1
- package/dist/hooks/observation.js +12 -2
- package/dist/hooks/observation.js.map +1 -1
- package/dist/hooks/service.d.ts +1 -6
- package/dist/hooks/service.d.ts.map +1 -1
- package/dist/hooks/service.js +33 -85
- package/dist/hooks/service.js.map +1 -1
- package/dist/hooks/session-init.d.ts +6 -1
- package/dist/hooks/session-init.d.ts.map +1 -1
- package/dist/hooks/session-init.js +12 -2
- package/dist/hooks/session-init.js.map +1 -1
- package/dist/hooks/summarize.d.ts +6 -1
- package/dist/hooks/summarize.d.ts.map +1 -1
- package/dist/hooks/summarize.js +12 -2
- package/dist/hooks/summarize.js.map +1 -1
- package/dist/index.d.ts +10 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +172 -94
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +17 -3
- package/dist/mcp/server.js.map +1 -1
- package/dist/migration.js +3 -3
- package/dist/migration.js.map +1 -1
- package/dist/search/hybrid-search.d.ts +262 -0
- package/dist/search/hybrid-search.d.ts.map +1 -0
- package/dist/search/hybrid-search.js +688 -0
- package/dist/search/hybrid-search.js.map +1 -0
- package/dist/search/index.d.ts +13 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +13 -0
- package/dist/search/index.js.map +1 -0
- package/dist/search/token-economics.d.ts +161 -0
- package/dist/search/token-economics.d.ts.map +1 -0
- package/dist/search/token-economics.js +239 -0
- package/dist/search/token-economics.js.map +1 -0
- package/dist/types.d.ts +0 -68
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +23 -8
- package/src/__tests__/better-sqlite3-backend.test.ts +1466 -0
- package/src/__tests__/cache-manager.test.ts +499 -0
- package/src/__tests__/embedding-integration.test.ts +481 -0
- package/src/__tests__/hnsw-index.test.ts +727 -0
- package/src/__tests__/index.test.ts +432 -0
- package/src/better-sqlite3-backend.ts +1000 -0
- package/src/cli/setup.ts +358 -47
- package/src/cli/viewer.ts +28 -63
- package/src/cli/web-viewer.ts +1956 -0
- package/src/embeddings/__tests__/embedding-cache.test.ts +269 -0
- package/src/embeddings/__tests__/local-embeddings.test.ts +495 -0
- package/src/embeddings/embedding-cache.ts +318 -0
- package/src/embeddings/index.ts +20 -0
- package/src/embeddings/local-embeddings.ts +419 -0
- package/src/hooks/__tests__/handlers.test.ts +58 -17
- package/src/hooks/__tests__/integration.test.ts +77 -26
- package/src/hooks/context.ts +13 -2
- package/src/hooks/observation.ts +13 -2
- package/src/hooks/service.ts +39 -100
- package/src/hooks/session-init.ts +13 -2
- package/src/hooks/summarize.ts +13 -2
- package/src/index.ts +210 -116
- package/src/mcp/server.ts +20 -3
- package/src/search/__tests__/hybrid-search.test.ts +669 -0
- package/src/search/__tests__/token-economics.test.ts +276 -0
- package/src/search/hybrid-search.ts +968 -0
- package/src/search/index.ts +29 -0
- package/src/search/token-economics.ts +367 -0
- package/src/types.ts +0 -96
- package/src/__tests__/sqljs-backend.test.ts +0 -410
- package/src/migration.ts +0 -574
- package/src/sql.js.d.ts +0 -70
- package/src/sqljs-backend.ts +0 -789
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Offline Embeddings Service
|
|
3
|
+
*
|
|
4
|
+
* Provides 100% offline text embeddings using Transformers.js (WASM-based).
|
|
5
|
+
* No external API calls. Model is downloaded once and cached locally.
|
|
6
|
+
*
|
|
7
|
+
* @module @aitytech/agentkits-memory/embeddings
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { EmbeddingGenerator } from '../types.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Embedding provider type
|
|
14
|
+
*/
|
|
15
|
+
export type EmbeddingProvider = 'transformers' | 'mock';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Local embeddings configuration
|
|
19
|
+
*/
|
|
20
|
+
export interface LocalEmbeddingsConfig {
|
|
21
|
+
/** Provider to use (default: 'transformers') */
|
|
22
|
+
provider?: EmbeddingProvider;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Model ID for Transformers.js
|
|
26
|
+
* Default: 'Xenova/multilingual-e5-small' (100+ languages, optimized for retrieval)
|
|
27
|
+
* Alternative: 'Xenova/paraphrase-multilingual-MiniLM-L12-v2' (50+ languages)
|
|
28
|
+
* Alternative: 'Xenova/all-MiniLM-L6-v2' (English only, faster)
|
|
29
|
+
*/
|
|
30
|
+
modelId?: string;
|
|
31
|
+
|
|
32
|
+
/** Vector dimensions (default: 384) */
|
|
33
|
+
dimensions?: number;
|
|
34
|
+
|
|
35
|
+
/** Enable in-memory cache for repeated texts */
|
|
36
|
+
cacheEnabled?: boolean;
|
|
37
|
+
|
|
38
|
+
/** Maximum cache size (default: 1000) */
|
|
39
|
+
maxCacheSize?: number;
|
|
40
|
+
|
|
41
|
+
/** Show progress during model download */
|
|
42
|
+
showProgress?: boolean;
|
|
43
|
+
|
|
44
|
+
/** Custom cache directory for models */
|
|
45
|
+
cacheDir?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Embedding result with metadata
|
|
50
|
+
*/
|
|
51
|
+
export interface EmbeddingResult {
|
|
52
|
+
/** The embedding vector */
|
|
53
|
+
embedding: Float32Array;
|
|
54
|
+
|
|
55
|
+
/** Time taken in milliseconds */
|
|
56
|
+
timeMs: number;
|
|
57
|
+
|
|
58
|
+
/** Whether result was from cache */
|
|
59
|
+
cached: boolean;
|
|
60
|
+
|
|
61
|
+
/** Token count (approximate) */
|
|
62
|
+
tokenCount?: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Local embeddings service statistics
|
|
67
|
+
*/
|
|
68
|
+
export interface EmbeddingsStats {
|
|
69
|
+
/** Total embeddings generated */
|
|
70
|
+
totalEmbeddings: number;
|
|
71
|
+
|
|
72
|
+
/** Cache hits */
|
|
73
|
+
cacheHits: number;
|
|
74
|
+
|
|
75
|
+
/** Cache misses */
|
|
76
|
+
cacheMisses: number;
|
|
77
|
+
|
|
78
|
+
/** Average time per embedding (ms) */
|
|
79
|
+
avgTimeMs: number;
|
|
80
|
+
|
|
81
|
+
/** Total time spent (ms) */
|
|
82
|
+
totalTimeMs: number;
|
|
83
|
+
|
|
84
|
+
/** Model loaded */
|
|
85
|
+
modelLoaded: boolean;
|
|
86
|
+
|
|
87
|
+
/** Provider being used */
|
|
88
|
+
provider: EmbeddingProvider;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* In-memory LRU Cache for embeddings
|
|
93
|
+
*/
|
|
94
|
+
class InMemoryEmbeddingCache {
|
|
95
|
+
private cache = new Map<string, Float32Array>();
|
|
96
|
+
private accessOrder: string[] = [];
|
|
97
|
+
private maxSize: number;
|
|
98
|
+
|
|
99
|
+
constructor(maxSize: number) {
|
|
100
|
+
this.maxSize = maxSize;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get(key: string): Float32Array | undefined {
|
|
104
|
+
const value = this.cache.get(key);
|
|
105
|
+
if (value) {
|
|
106
|
+
// Move to end (most recently used)
|
|
107
|
+
const index = this.accessOrder.indexOf(key);
|
|
108
|
+
if (index > -1) {
|
|
109
|
+
this.accessOrder.splice(index, 1);
|
|
110
|
+
}
|
|
111
|
+
this.accessOrder.push(key);
|
|
112
|
+
}
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
set(key: string, value: Float32Array): void {
|
|
117
|
+
if (this.cache.has(key)) {
|
|
118
|
+
this.cache.set(key, value);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Evict if at capacity
|
|
123
|
+
while (this.cache.size >= this.maxSize && this.accessOrder.length > 0) {
|
|
124
|
+
const oldest = this.accessOrder.shift();
|
|
125
|
+
if (oldest) {
|
|
126
|
+
this.cache.delete(oldest);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.cache.set(key, value);
|
|
131
|
+
this.accessOrder.push(key);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
clear(): void {
|
|
135
|
+
this.cache.clear();
|
|
136
|
+
this.accessOrder = [];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
get size(): number {
|
|
140
|
+
return this.cache.size;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Mock embedding provider for testing
|
|
146
|
+
*/
|
|
147
|
+
function createMockEmbedding(text: string, dimensions: number): Float32Array {
|
|
148
|
+
const embedding = new Float32Array(dimensions);
|
|
149
|
+
// Generate deterministic pseudo-random values based on text hash
|
|
150
|
+
let hash = 0;
|
|
151
|
+
for (let i = 0; i < text.length; i++) {
|
|
152
|
+
hash = ((hash << 5) - hash) + text.charCodeAt(i);
|
|
153
|
+
hash = hash & hash;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (let i = 0; i < dimensions; i++) {
|
|
157
|
+
// Use hash to seed pseudo-random generation
|
|
158
|
+
hash = ((hash << 5) - hash) + i;
|
|
159
|
+
hash = hash & hash;
|
|
160
|
+
embedding[i] = (hash % 1000) / 1000 - 0.5;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Normalize
|
|
164
|
+
let norm = 0;
|
|
165
|
+
for (let i = 0; i < dimensions; i++) {
|
|
166
|
+
norm += embedding[i] * embedding[i];
|
|
167
|
+
}
|
|
168
|
+
norm = Math.sqrt(norm);
|
|
169
|
+
if (norm > 0) {
|
|
170
|
+
for (let i = 0; i < dimensions; i++) {
|
|
171
|
+
embedding[i] /= norm;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return embedding;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Local Embeddings Service
|
|
180
|
+
*
|
|
181
|
+
* Provides offline text embeddings using Transformers.js.
|
|
182
|
+
* Models are downloaded once and cached locally in ~/.cache/huggingface.
|
|
183
|
+
*/
|
|
184
|
+
export class LocalEmbeddingsService {
|
|
185
|
+
private config: Required<LocalEmbeddingsConfig>;
|
|
186
|
+
private cache: InMemoryEmbeddingCache | null = null;
|
|
187
|
+
private pipeline: any = null;
|
|
188
|
+
private modelLoading: Promise<void> | null = null;
|
|
189
|
+
private stats = {
|
|
190
|
+
totalEmbeddings: 0,
|
|
191
|
+
cacheHits: 0,
|
|
192
|
+
cacheMisses: 0,
|
|
193
|
+
totalTimeMs: 0,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
constructor(config: LocalEmbeddingsConfig = {}) {
|
|
197
|
+
this.config = {
|
|
198
|
+
provider: config.provider || 'transformers',
|
|
199
|
+
// Use multilingual-e5-small for best CJK support and retrieval accuracy
|
|
200
|
+
modelId: config.modelId || 'Xenova/multilingual-e5-small',
|
|
201
|
+
dimensions: config.dimensions || 384,
|
|
202
|
+
cacheEnabled: config.cacheEnabled ?? true,
|
|
203
|
+
maxCacheSize: config.maxCacheSize || 1000,
|
|
204
|
+
showProgress: config.showProgress ?? false,
|
|
205
|
+
cacheDir: config.cacheDir || '',
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
if (this.config.cacheEnabled) {
|
|
209
|
+
this.cache = new InMemoryEmbeddingCache(this.config.maxCacheSize);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Initialize the embeddings service (loads model)
|
|
215
|
+
*/
|
|
216
|
+
async initialize(): Promise<void> {
|
|
217
|
+
if (this.config.provider === 'mock') {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (this.pipeline) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (this.modelLoading) {
|
|
226
|
+
await this.modelLoading;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
this.modelLoading = this.loadModel();
|
|
231
|
+
await this.modelLoading;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private async loadModel(): Promise<void> {
|
|
235
|
+
try {
|
|
236
|
+
// Dynamic import for Transformers.js
|
|
237
|
+
const { pipeline } = await import('@xenova/transformers');
|
|
238
|
+
|
|
239
|
+
const progressCallback = this.config.showProgress
|
|
240
|
+
? (progress: { status: string; progress?: number }) => {
|
|
241
|
+
if (progress.status === 'progress' && progress.progress !== undefined) {
|
|
242
|
+
process.stderr.write(
|
|
243
|
+
`\rLoading model: ${Math.round(progress.progress)}%`
|
|
244
|
+
);
|
|
245
|
+
} else if (progress.status === 'done') {
|
|
246
|
+
process.stderr.write('\rModel loaded successfully. \n');
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
: undefined;
|
|
250
|
+
|
|
251
|
+
this.pipeline = await pipeline('feature-extraction', this.config.modelId, {
|
|
252
|
+
progress_callback: progressCallback,
|
|
253
|
+
});
|
|
254
|
+
} catch (error) {
|
|
255
|
+
// If Transformers.js is not available, fall back to mock
|
|
256
|
+
console.warn(
|
|
257
|
+
'Transformers.js not available, falling back to mock embeddings.',
|
|
258
|
+
'Install with: npm install @xenova/transformers'
|
|
259
|
+
);
|
|
260
|
+
this.config.provider = 'mock';
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Generate embedding for text
|
|
266
|
+
*/
|
|
267
|
+
async embed(text: string): Promise<EmbeddingResult> {
|
|
268
|
+
const startTime = performance.now();
|
|
269
|
+
|
|
270
|
+
// Check cache
|
|
271
|
+
if (this.cache) {
|
|
272
|
+
const cached = this.cache.get(text);
|
|
273
|
+
if (cached) {
|
|
274
|
+
this.stats.cacheHits++;
|
|
275
|
+
return {
|
|
276
|
+
embedding: cached,
|
|
277
|
+
timeMs: performance.now() - startTime,
|
|
278
|
+
cached: true,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
this.stats.cacheMisses++;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Generate embedding
|
|
285
|
+
let embedding: Float32Array;
|
|
286
|
+
|
|
287
|
+
if (this.config.provider === 'mock') {
|
|
288
|
+
embedding = createMockEmbedding(text, this.config.dimensions);
|
|
289
|
+
} else {
|
|
290
|
+
await this.initialize();
|
|
291
|
+
|
|
292
|
+
if (!this.pipeline) {
|
|
293
|
+
// Fall back to mock if model failed to load
|
|
294
|
+
embedding = createMockEmbedding(text, this.config.dimensions);
|
|
295
|
+
} else {
|
|
296
|
+
const output = await this.pipeline(text, {
|
|
297
|
+
pooling: 'mean',
|
|
298
|
+
normalize: true,
|
|
299
|
+
});
|
|
300
|
+
embedding = new Float32Array(output.data);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const timeMs = performance.now() - startTime;
|
|
305
|
+
|
|
306
|
+
// Update stats
|
|
307
|
+
this.stats.totalEmbeddings++;
|
|
308
|
+
this.stats.totalTimeMs += timeMs;
|
|
309
|
+
|
|
310
|
+
// Cache result
|
|
311
|
+
if (this.cache) {
|
|
312
|
+
this.cache.set(text, embedding);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
embedding,
|
|
317
|
+
timeMs,
|
|
318
|
+
cached: false,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Generate embeddings for multiple texts (batch)
|
|
324
|
+
*/
|
|
325
|
+
async embedBatch(texts: string[]): Promise<EmbeddingResult[]> {
|
|
326
|
+
// Process in parallel for better performance
|
|
327
|
+
return Promise.all(texts.map((text) => this.embed(text)));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Get embedding generator function compatible with ProjectMemoryService
|
|
332
|
+
*/
|
|
333
|
+
getGenerator(): EmbeddingGenerator {
|
|
334
|
+
return async (content: string): Promise<Float32Array> => {
|
|
335
|
+
const result = await this.embed(content);
|
|
336
|
+
return result.embedding;
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Get service statistics
|
|
342
|
+
*/
|
|
343
|
+
getStats(): EmbeddingsStats {
|
|
344
|
+
return {
|
|
345
|
+
totalEmbeddings: this.stats.totalEmbeddings,
|
|
346
|
+
cacheHits: this.stats.cacheHits,
|
|
347
|
+
cacheMisses: this.stats.cacheMisses,
|
|
348
|
+
avgTimeMs:
|
|
349
|
+
this.stats.totalEmbeddings > 0
|
|
350
|
+
? this.stats.totalTimeMs / this.stats.totalEmbeddings
|
|
351
|
+
: 0,
|
|
352
|
+
totalTimeMs: this.stats.totalTimeMs,
|
|
353
|
+
modelLoaded: this.pipeline !== null || this.config.provider === 'mock',
|
|
354
|
+
provider: this.config.provider,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Clear the embedding cache
|
|
360
|
+
*/
|
|
361
|
+
clearCache(): void {
|
|
362
|
+
if (this.cache) {
|
|
363
|
+
this.cache.clear();
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Get vector dimensions
|
|
369
|
+
*/
|
|
370
|
+
getDimensions(): number {
|
|
371
|
+
return this.config.dimensions;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Shutdown and cleanup
|
|
376
|
+
*/
|
|
377
|
+
async shutdown(): Promise<void> {
|
|
378
|
+
this.clearCache();
|
|
379
|
+
this.pipeline = null;
|
|
380
|
+
this.modelLoading = null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Create a local embeddings service with default configuration
|
|
386
|
+
*/
|
|
387
|
+
export function createLocalEmbeddings(
|
|
388
|
+
config?: LocalEmbeddingsConfig
|
|
389
|
+
): LocalEmbeddingsService {
|
|
390
|
+
return new LocalEmbeddingsService(config);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Create an embedding generator function for use with ProjectMemoryService
|
|
395
|
+
*
|
|
396
|
+
* @example
|
|
397
|
+
* ```typescript
|
|
398
|
+
* import { createEmbeddingGenerator } from '@aitytech/agentkits-memory/embeddings';
|
|
399
|
+
* import { ProjectMemoryService } from '@aitytech/agentkits-memory';
|
|
400
|
+
*
|
|
401
|
+
* const embeddingGenerator = await createEmbeddingGenerator();
|
|
402
|
+
*
|
|
403
|
+
* const memory = new ProjectMemoryService({
|
|
404
|
+
* projectPath: '/path/to/project',
|
|
405
|
+
* enableVectorIndex: true,
|
|
406
|
+
* embeddingGenerator,
|
|
407
|
+
* });
|
|
408
|
+
* ```
|
|
409
|
+
*/
|
|
410
|
+
export async function createEmbeddingGenerator(
|
|
411
|
+
config?: LocalEmbeddingsConfig
|
|
412
|
+
): Promise<EmbeddingGenerator> {
|
|
413
|
+
const service = new LocalEmbeddingsService(config);
|
|
414
|
+
await service.initialize();
|
|
415
|
+
return service.getGenerator();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Default export
|
|
419
|
+
export default LocalEmbeddingsService;
|
|
@@ -16,6 +16,9 @@ import { SummarizeHook, createSummarizeHook } from '../summarize.js';
|
|
|
16
16
|
|
|
17
17
|
const TEST_DIR = path.join(process.cwd(), '.test-hook-handlers');
|
|
18
18
|
|
|
19
|
+
// Track hooks for cleanup
|
|
20
|
+
let activeHooks: Array<{ shutdown: () => Promise<void> }> = [];
|
|
21
|
+
|
|
19
22
|
function createTestInput(overrides: Partial<NormalizedHookInput> = {}): NormalizedHookInput {
|
|
20
23
|
return {
|
|
21
24
|
sessionId: 'test-session-123',
|
|
@@ -26,25 +29,53 @@ function createTestInput(overrides: Partial<NormalizedHookInput> = {}): Normaliz
|
|
|
26
29
|
};
|
|
27
30
|
}
|
|
28
31
|
|
|
32
|
+
// Helper to track hooks for cleanup
|
|
33
|
+
function trackHook<T extends { shutdown: () => Promise<void> }>(hook: T): T {
|
|
34
|
+
activeHooks.push(hook);
|
|
35
|
+
return hook;
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
describe('Hook Handlers', () => {
|
|
30
39
|
beforeEach(() => {
|
|
40
|
+
activeHooks = [];
|
|
31
41
|
// Clean up test directory
|
|
32
42
|
if (existsSync(TEST_DIR)) {
|
|
33
|
-
|
|
43
|
+
try {
|
|
44
|
+
rmSync(TEST_DIR, { recursive: true });
|
|
45
|
+
} catch {
|
|
46
|
+
// Ignore errors on Windows
|
|
47
|
+
}
|
|
34
48
|
}
|
|
35
49
|
mkdirSync(TEST_DIR, { recursive: true });
|
|
36
50
|
});
|
|
37
51
|
|
|
38
|
-
afterEach(() => {
|
|
52
|
+
afterEach(async () => {
|
|
53
|
+
// Shutdown all hooks first (releases database locks)
|
|
54
|
+
for (const hook of activeHooks) {
|
|
55
|
+
try {
|
|
56
|
+
await hook.shutdown();
|
|
57
|
+
} catch {
|
|
58
|
+
// Ignore shutdown errors
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
activeHooks = [];
|
|
62
|
+
|
|
63
|
+
// Small delay for Windows file system
|
|
64
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
65
|
+
|
|
39
66
|
// Clean up test directory
|
|
40
67
|
if (existsSync(TEST_DIR)) {
|
|
41
|
-
|
|
68
|
+
try {
|
|
69
|
+
rmSync(TEST_DIR, { recursive: true });
|
|
70
|
+
} catch {
|
|
71
|
+
// Ignore errors on Windows - files may still be locked
|
|
72
|
+
}
|
|
42
73
|
}
|
|
43
74
|
});
|
|
44
75
|
|
|
45
76
|
describe('ContextHook', () => {
|
|
46
77
|
it('should return no context for new project', async () => {
|
|
47
|
-
const hook = createContextHook(TEST_DIR);
|
|
78
|
+
const hook = trackHook(createContextHook(TEST_DIR));
|
|
48
79
|
const input = createTestInput();
|
|
49
80
|
|
|
50
81
|
const result = await hook.execute(input);
|
|
@@ -63,7 +94,7 @@ describe('Hook Handlers', () => {
|
|
|
63
94
|
await service.shutdown();
|
|
64
95
|
|
|
65
96
|
// Run context hook
|
|
66
|
-
const hook = createContextHook(TEST_DIR);
|
|
97
|
+
const hook = trackHook(createContextHook(TEST_DIR));
|
|
67
98
|
const input = createTestInput({ sessionId: 'new-session' });
|
|
68
99
|
|
|
69
100
|
const result = await hook.execute(input);
|
|
@@ -92,7 +123,7 @@ describe('Hook Handlers', () => {
|
|
|
92
123
|
|
|
93
124
|
describe('SessionInitHook', () => {
|
|
94
125
|
it('should initialize a new session', async () => {
|
|
95
|
-
const hook = createSessionInitHook(TEST_DIR);
|
|
126
|
+
const hook = trackHook(createSessionInitHook(TEST_DIR));
|
|
96
127
|
const input = createTestInput({ prompt: 'Hello Claude' });
|
|
97
128
|
|
|
98
129
|
const result = await hook.execute(input);
|
|
@@ -100,6 +131,9 @@ describe('Hook Handlers', () => {
|
|
|
100
131
|
expect(result.continue).toBe(true);
|
|
101
132
|
expect(result.suppressOutput).toBe(true);
|
|
102
133
|
|
|
134
|
+
// Shutdown hook before verifying
|
|
135
|
+
await hook.shutdown();
|
|
136
|
+
|
|
103
137
|
// Verify session was created
|
|
104
138
|
const service = new MemoryHookService(TEST_DIR);
|
|
105
139
|
await service.initialize();
|
|
@@ -112,12 +146,14 @@ describe('Hook Handlers', () => {
|
|
|
112
146
|
|
|
113
147
|
it('should not overwrite existing session', async () => {
|
|
114
148
|
// Create initial session
|
|
115
|
-
const hook1 = createSessionInitHook(TEST_DIR);
|
|
149
|
+
const hook1 = trackHook(createSessionInitHook(TEST_DIR));
|
|
116
150
|
await hook1.execute(createTestInput({ prompt: 'First prompt' }));
|
|
151
|
+
await hook1.shutdown();
|
|
117
152
|
|
|
118
153
|
// Try to re-init with different prompt
|
|
119
|
-
const hook2 = createSessionInitHook(TEST_DIR);
|
|
154
|
+
const hook2 = trackHook(createSessionInitHook(TEST_DIR));
|
|
120
155
|
await hook2.execute(createTestInput({ prompt: 'Second prompt' }));
|
|
156
|
+
await hook2.shutdown();
|
|
121
157
|
|
|
122
158
|
// Verify original prompt preserved
|
|
123
159
|
const service = new MemoryHookService(TEST_DIR);
|
|
@@ -145,11 +181,12 @@ describe('Hook Handlers', () => {
|
|
|
145
181
|
describe('ObservationHook', () => {
|
|
146
182
|
it('should store observation', async () => {
|
|
147
183
|
// Initialize session first
|
|
148
|
-
const initHook = createSessionInitHook(TEST_DIR);
|
|
184
|
+
const initHook = trackHook(createSessionInitHook(TEST_DIR));
|
|
149
185
|
await initHook.execute(createTestInput());
|
|
186
|
+
await initHook.shutdown();
|
|
150
187
|
|
|
151
188
|
// Store observation
|
|
152
|
-
const hook = createObservationHook(TEST_DIR);
|
|
189
|
+
const hook = trackHook(createObservationHook(TEST_DIR));
|
|
153
190
|
const input = createTestInput({
|
|
154
191
|
toolName: 'Read',
|
|
155
192
|
toolInput: { file_path: '/path/to/file.ts' },
|
|
@@ -157,6 +194,7 @@ describe('Hook Handlers', () => {
|
|
|
157
194
|
});
|
|
158
195
|
|
|
159
196
|
const result = await hook.execute(input);
|
|
197
|
+
await hook.shutdown();
|
|
160
198
|
|
|
161
199
|
expect(result.continue).toBe(true);
|
|
162
200
|
expect(result.suppressOutput).toBe(true);
|
|
@@ -172,7 +210,7 @@ describe('Hook Handlers', () => {
|
|
|
172
210
|
});
|
|
173
211
|
|
|
174
212
|
it('should skip if no tool name', async () => {
|
|
175
|
-
const hook = createObservationHook(TEST_DIR);
|
|
213
|
+
const hook = trackHook(createObservationHook(TEST_DIR));
|
|
176
214
|
const input = createTestInput({ toolName: undefined });
|
|
177
215
|
|
|
178
216
|
const result = await hook.execute(input);
|
|
@@ -183,10 +221,11 @@ describe('Hook Handlers', () => {
|
|
|
183
221
|
|
|
184
222
|
it('should skip internal tools', async () => {
|
|
185
223
|
// Initialize session first
|
|
186
|
-
const initHook = createSessionInitHook(TEST_DIR);
|
|
224
|
+
const initHook = trackHook(createSessionInitHook(TEST_DIR));
|
|
187
225
|
await initHook.execute(createTestInput());
|
|
226
|
+
await initHook.shutdown();
|
|
188
227
|
|
|
189
|
-
const hook = createObservationHook(TEST_DIR);
|
|
228
|
+
const hook = trackHook(createObservationHook(TEST_DIR));
|
|
190
229
|
|
|
191
230
|
// Test skipped tools
|
|
192
231
|
for (const tool of ['TodoWrite', 'TodoRead', 'AskFollowupQuestion', 'AttemptCompletion']) {
|
|
@@ -196,6 +235,7 @@ describe('Hook Handlers', () => {
|
|
|
196
235
|
expect(result.continue).toBe(true);
|
|
197
236
|
expect(result.suppressOutput).toBe(true);
|
|
198
237
|
}
|
|
238
|
+
await hook.shutdown();
|
|
199
239
|
|
|
200
240
|
// Verify no observations stored
|
|
201
241
|
const service = new MemoryHookService(TEST_DIR);
|
|
@@ -207,7 +247,7 @@ describe('Hook Handlers', () => {
|
|
|
207
247
|
});
|
|
208
248
|
|
|
209
249
|
it('should create session if not exists', async () => {
|
|
210
|
-
const hook = createObservationHook(TEST_DIR);
|
|
250
|
+
const hook = trackHook(createObservationHook(TEST_DIR));
|
|
211
251
|
const input = createTestInput({
|
|
212
252
|
sessionId: 'new-session',
|
|
213
253
|
toolName: 'Read',
|
|
@@ -216,6 +256,7 @@ describe('Hook Handlers', () => {
|
|
|
216
256
|
});
|
|
217
257
|
|
|
218
258
|
const result = await hook.execute(input);
|
|
259
|
+
await hook.shutdown();
|
|
219
260
|
|
|
220
261
|
expect(result.continue).toBe(true);
|
|
221
262
|
|
|
@@ -251,8 +292,8 @@ describe('Hook Handlers', () => {
|
|
|
251
292
|
await service.storeObservation('test-session-123', 'test-project', 'Write', { file_path: 'b.ts' }, {}, TEST_DIR);
|
|
252
293
|
await service.shutdown();
|
|
253
294
|
|
|
254
|
-
// Run summarize hook
|
|
255
|
-
const hook = createSummarizeHook(TEST_DIR);
|
|
295
|
+
// Run summarize hook (it already calls shutdown internally)
|
|
296
|
+
const hook = trackHook(createSummarizeHook(TEST_DIR));
|
|
256
297
|
const input = createTestInput();
|
|
257
298
|
|
|
258
299
|
const result = await hook.execute(input);
|
|
@@ -272,7 +313,7 @@ describe('Hook Handlers', () => {
|
|
|
272
313
|
});
|
|
273
314
|
|
|
274
315
|
it('should handle non-existent session', async () => {
|
|
275
|
-
const hook = createSummarizeHook(TEST_DIR);
|
|
316
|
+
const hook = trackHook(createSummarizeHook(TEST_DIR));
|
|
276
317
|
const input = createTestInput({ sessionId: 'non-existent' });
|
|
277
318
|
|
|
278
319
|
const result = await hook.execute(input);
|