server-memory-enhanced 0.2.0 → 2.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.
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Constants for validation rules
3
+ * Centralized to follow DRY principle and make maintenance easier
4
+ */
5
+ /**
6
+ * Maximum length for observations in characters
7
+ * Per spec Section 2: Hard Limits on Observation Length
8
+ */
9
+ export const MAX_OBSERVATION_LENGTH = 150;
10
+ /**
11
+ * Maximum number of sentences allowed per observation
12
+ * Per spec Section 2: Hard Limits on Observation Length
13
+ */
14
+ export const MAX_SENTENCES = 2;
15
+ /**
16
+ * Minimum observation length in characters
17
+ */
18
+ export const MIN_OBSERVATION_LENGTH = 5;
19
+ /**
20
+ * Minimum entity name length
21
+ */
22
+ export const MIN_ENTITY_NAME_LENGTH = 1;
23
+ /**
24
+ * Maximum entity name length
25
+ */
26
+ export const MAX_ENTITY_NAME_LENGTH = 100;
27
+ /**
28
+ * Minimum entity type length
29
+ */
30
+ export const MIN_ENTITY_TYPE_LENGTH = 1;
31
+ /**
32
+ * Maximum entity type length
33
+ */
34
+ export const MAX_ENTITY_TYPE_LENGTH = 50;
35
+ /**
36
+ * Target average relations per entity for quality scoring
37
+ * Used to normalize relation count to 0-1 scale
38
+ */
39
+ export const TARGET_AVG_RELATIONS = 2;
40
+ /**
41
+ * Weight for relation score in quality calculation
42
+ */
43
+ export const RELATION_SCORE_WEIGHT = 0.7;
44
+ /**
45
+ * Weight for observation score in quality calculation
46
+ */
47
+ export const OBSERVATION_SCORE_WEIGHT = 0.3;
48
+ /**
49
+ * Maximum number of entities returned in analytics queries
50
+ */
51
+ export const ANALYTICS_RESULT_LIMIT = 10;
52
+ /**
53
+ * Sentence terminators used for counting sentences
54
+ */
55
+ export const SENTENCE_TERMINATORS = /[.!?]/;
@@ -0,0 +1,315 @@
1
+ /**
2
+ * JSONL Storage Adapter - implements file-based storage using JSON Lines format
3
+ */
4
+ import { promises as fs } from 'fs';
5
+ import path from 'path';
6
+ // Constants for file naming and types
7
+ const THREAD_FILE_PREFIX = 'thread-';
8
+ const THREAD_FILE_EXTENSION = '.jsonl';
9
+ const ENTITY_TYPE = 'entity';
10
+ const RELATION_TYPE = 'relation';
11
+ const FILE_NOT_FOUND_ERROR = 'ENOENT';
12
+ /**
13
+ * Type guard to check if an error is a FileSystemError
14
+ */
15
+ function isFileSystemError(error) {
16
+ return error instanceof Error && 'code' in error;
17
+ }
18
+ /**
19
+ * Check if a string field is valid (non-empty after trimming)
20
+ */
21
+ function isValidString(value) {
22
+ return typeof value === 'string' && value.trim().length > 0;
23
+ }
24
+ /**
25
+ * JSONL-based storage adapter for the knowledge graph
26
+ * Stores data in thread-specific JSONL files
27
+ *
28
+ * Responsibilities:
29
+ * - File I/O operations for JSONL format
30
+ * - Thread-based data organization
31
+ * - Data serialization/deserialization
32
+ */
33
+ export class JsonlStorageAdapter {
34
+ memoryDirPath;
35
+ constructor(memoryDirPath) {
36
+ this.memoryDirPath = memoryDirPath;
37
+ }
38
+ /**
39
+ * Get the file path for a specific thread
40
+ */
41
+ getThreadFilePath(agentThreadId) {
42
+ return path.join(this.memoryDirPath, `${THREAD_FILE_PREFIX}${agentThreadId}${THREAD_FILE_EXTENSION}`);
43
+ }
44
+ /**
45
+ * Check if an item is a valid entity
46
+ */
47
+ isValidEntity(item) {
48
+ return item.type === ENTITY_TYPE &&
49
+ isValidString(item.name) &&
50
+ isValidString(item.entityType) &&
51
+ Array.isArray(item.observations) &&
52
+ isValidString(item.agentThreadId) &&
53
+ isValidString(item.timestamp) &&
54
+ typeof item.confidence === 'number' &&
55
+ typeof item.importance === 'number';
56
+ }
57
+ /**
58
+ * Check if an item is a valid relation
59
+ */
60
+ isValidRelation(item) {
61
+ return item.type === RELATION_TYPE &&
62
+ isValidString(item.from) &&
63
+ isValidString(item.to) &&
64
+ isValidString(item.relationType) &&
65
+ isValidString(item.agentThreadId) &&
66
+ isValidString(item.timestamp) &&
67
+ typeof item.confidence === 'number' &&
68
+ typeof item.importance === 'number';
69
+ }
70
+ /**
71
+ * Parse and validate a single JSONL line
72
+ */
73
+ parseLine(line, filePath) {
74
+ try {
75
+ return JSON.parse(line);
76
+ }
77
+ catch (parseError) {
78
+ console.warn(`Skipping malformed JSON line in ${filePath} (line length: ${line.length} chars)`);
79
+ return null;
80
+ }
81
+ }
82
+ /**
83
+ * Convert JSONL item to Entity
84
+ */
85
+ toEntity(item) {
86
+ return {
87
+ name: item.name,
88
+ entityType: item.entityType,
89
+ observations: item.observations,
90
+ agentThreadId: item.agentThreadId,
91
+ timestamp: item.timestamp,
92
+ confidence: item.confidence,
93
+ importance: item.importance
94
+ };
95
+ }
96
+ /**
97
+ * Convert JSONL item to Relation
98
+ */
99
+ toRelation(item) {
100
+ return {
101
+ from: item.from,
102
+ to: item.to,
103
+ relationType: item.relationType,
104
+ agentThreadId: item.agentThreadId,
105
+ timestamp: item.timestamp,
106
+ confidence: item.confidence,
107
+ importance: item.importance
108
+ };
109
+ }
110
+ /**
111
+ * Process a single JSONL item and add to graph
112
+ */
113
+ processItem(item, graph, filePath) {
114
+ if (!item) {
115
+ return;
116
+ }
117
+ if (this.isValidEntity(item)) {
118
+ graph.entities.push(this.toEntity(item));
119
+ }
120
+ else if (this.isValidRelation(item)) {
121
+ graph.relations.push(this.toRelation(item));
122
+ }
123
+ else if (item.type === ENTITY_TYPE || item.type === RELATION_TYPE) {
124
+ console.warn(`Skipping ${item.type} with missing required fields in ${filePath}`);
125
+ }
126
+ }
127
+ /**
128
+ * Serialize an entity to JSONL format
129
+ */
130
+ serializeEntity(entity) {
131
+ return JSON.stringify({
132
+ type: ENTITY_TYPE,
133
+ name: entity.name,
134
+ entityType: entity.entityType,
135
+ observations: entity.observations,
136
+ agentThreadId: entity.agentThreadId,
137
+ timestamp: entity.timestamp,
138
+ confidence: entity.confidence,
139
+ importance: entity.importance
140
+ });
141
+ }
142
+ /**
143
+ * Serialize a relation to JSONL format
144
+ */
145
+ serializeRelation(relation) {
146
+ return JSON.stringify({
147
+ type: RELATION_TYPE,
148
+ from: relation.from,
149
+ to: relation.to,
150
+ relationType: relation.relationType,
151
+ agentThreadId: relation.agentThreadId,
152
+ timestamp: relation.timestamp,
153
+ confidence: relation.confidence,
154
+ importance: relation.importance
155
+ });
156
+ }
157
+ /**
158
+ * Get or create thread data in the map
159
+ */
160
+ getOrCreateThreadData(threadMap, threadId) {
161
+ let threadData = threadMap.get(threadId);
162
+ if (!threadData) {
163
+ threadData = { entities: [], relations: [] };
164
+ threadMap.set(threadId, threadData);
165
+ }
166
+ return threadData;
167
+ }
168
+ /**
169
+ * Check if error is a file not found error
170
+ */
171
+ isFileNotFoundError(error) {
172
+ return isFileSystemError(error) && error.code === FILE_NOT_FOUND_ERROR;
173
+ }
174
+ /**
175
+ * Extract thread ID from filename
176
+ */
177
+ extractThreadId(fileName) {
178
+ const match = fileName.match(new RegExp(`^${THREAD_FILE_PREFIX}(.+)${THREAD_FILE_EXTENSION}$`));
179
+ return match ? match[1] : null;
180
+ }
181
+ /**
182
+ * Load graph data from a single JSONL file
183
+ */
184
+ async loadGraphFromFile(filePath) {
185
+ try {
186
+ const data = await fs.readFile(filePath, "utf-8");
187
+ const lines = data.split("\n").filter(line => line.trim() !== "");
188
+ const graph = { entities: [], relations: [] };
189
+ for (const line of lines) {
190
+ const item = this.parseLine(line, filePath);
191
+ this.processItem(item, graph, filePath);
192
+ }
193
+ return graph;
194
+ }
195
+ catch (error) {
196
+ if (this.isFileNotFoundError(error)) {
197
+ return { entities: [], relations: [] };
198
+ }
199
+ throw error;
200
+ }
201
+ }
202
+ /**
203
+ * Get all thread file names in the memory directory
204
+ */
205
+ async getThreadFileNames() {
206
+ const files = await fs.readdir(this.memoryDirPath).catch(() => []);
207
+ return files.filter(f => f.startsWith(THREAD_FILE_PREFIX) && f.endsWith(THREAD_FILE_EXTENSION));
208
+ }
209
+ /**
210
+ * Merge multiple graphs into one
211
+ */
212
+ mergeGraphs(graphs) {
213
+ return graphs.reduce((acc, graph) => ({
214
+ entities: [...acc.entities, ...graph.entities],
215
+ relations: [...acc.relations, ...graph.relations]
216
+ }), { entities: [], relations: [] });
217
+ }
218
+ /**
219
+ * Load the complete knowledge graph from all thread files
220
+ */
221
+ async loadGraph() {
222
+ const threadFiles = await this.getThreadFileNames();
223
+ const graphs = await Promise.all(threadFiles.map(f => this.loadGraphFromFile(path.join(this.memoryDirPath, f))));
224
+ return this.mergeGraphs(graphs);
225
+ }
226
+ /**
227
+ * Delete empty thread file if it exists
228
+ */
229
+ async deleteThreadFileIfExists(threadFilePath) {
230
+ try {
231
+ await fs.unlink(threadFilePath);
232
+ }
233
+ catch (error) {
234
+ if (!this.isFileNotFoundError(error)) {
235
+ console.warn(`Failed to delete empty thread file ${threadFilePath}:`, error);
236
+ }
237
+ }
238
+ }
239
+ /**
240
+ * Serialize thread data to JSONL lines
241
+ */
242
+ serializeThreadData(threadData) {
243
+ return [
244
+ ...threadData.entities.map(e => this.serializeEntity(e)),
245
+ ...threadData.relations.map(r => this.serializeRelation(r))
246
+ ];
247
+ }
248
+ /**
249
+ * Save data for a specific thread
250
+ */
251
+ async saveGraphForThread(agentThreadId, threadData) {
252
+ const threadFilePath = this.getThreadFilePath(agentThreadId);
253
+ const lines = this.serializeThreadData(threadData);
254
+ if (lines.length === 0) {
255
+ await this.deleteThreadFileIfExists(threadFilePath);
256
+ return;
257
+ }
258
+ await fs.writeFile(threadFilePath, lines.join("\n"));
259
+ }
260
+ /**
261
+ * Group graph data by thread ID
262
+ */
263
+ groupByThread(graph) {
264
+ const threadMap = new Map();
265
+ for (const entity of graph.entities) {
266
+ const threadData = this.getOrCreateThreadData(threadMap, entity.agentThreadId);
267
+ threadData.entities.push(entity);
268
+ }
269
+ for (const relation of graph.relations) {
270
+ const threadData = this.getOrCreateThreadData(threadMap, relation.agentThreadId);
271
+ threadData.relations.push(relation);
272
+ }
273
+ return threadMap;
274
+ }
275
+ /**
276
+ * Save all thread data to their respective files
277
+ */
278
+ async saveAllThreads(threadMap) {
279
+ const savePromises = Array.from(threadMap.entries()).map(([threadId, data]) => this.saveGraphForThread(threadId, data));
280
+ await Promise.all(savePromises);
281
+ }
282
+ /**
283
+ * Clean up stale thread files that are no longer in the graph
284
+ */
285
+ async cleanupStaleThreadFiles(activeThreadIds) {
286
+ try {
287
+ const threadFiles = await this.getThreadFileNames();
288
+ const deletePromises = threadFiles.map(async (fileName) => {
289
+ const threadId = this.extractThreadId(fileName);
290
+ if (threadId && !activeThreadIds.has(threadId)) {
291
+ const filePath = path.join(this.memoryDirPath, fileName);
292
+ await this.deleteThreadFileIfExists(filePath);
293
+ }
294
+ });
295
+ await Promise.all(deletePromises);
296
+ }
297
+ catch (error) {
298
+ console.warn('Failed to clean up stale thread files:', error);
299
+ }
300
+ }
301
+ /**
302
+ * Save the complete knowledge graph to thread-specific files
303
+ */
304
+ async saveGraph(graph) {
305
+ const threadMap = this.groupByThread(graph);
306
+ await this.saveAllThreads(threadMap);
307
+ await this.cleanupStaleThreadFiles(new Set(threadMap.keys()));
308
+ }
309
+ /**
310
+ * Initialize the storage adapter (create memory directory if needed)
311
+ */
312
+ async initialize() {
313
+ await fs.mkdir(this.memoryDirPath, { recursive: true });
314
+ }
315
+ }