@hashgraphonline/conversational-agent 0.1.204 → 0.1.206

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 (87) hide show
  1. package/dist/cjs/base-agent.d.ts +6 -0
  2. package/dist/cjs/context/ReferenceContextManager.d.ts +84 -0
  3. package/dist/cjs/context/ReferenceResponseProcessor.d.ts +76 -0
  4. package/dist/cjs/conversational-agent.d.ts +34 -0
  5. package/dist/cjs/index.cjs +1 -1
  6. package/dist/cjs/index.cjs.map +1 -1
  7. package/dist/cjs/langchain-agent.d.ts +1 -0
  8. package/dist/cjs/mcp/ContentProcessor.d.ts +37 -0
  9. package/dist/cjs/mcp/MCPClientManager.d.ts +19 -1
  10. package/dist/cjs/memory/ContentStorage.d.ts +205 -0
  11. package/dist/cjs/memory/MemoryWindow.d.ts +114 -0
  12. package/dist/cjs/memory/ReferenceIdGenerator.d.ts +45 -0
  13. package/dist/cjs/memory/SmartMemoryManager.d.ts +201 -0
  14. package/dist/cjs/memory/TokenCounter.d.ts +61 -0
  15. package/dist/cjs/memory/index.d.ts +7 -0
  16. package/dist/cjs/plugins/hbar-transfer/TransferHbarTool.d.ts +18 -0
  17. package/dist/cjs/services/ContentStoreManager.d.ts +54 -0
  18. package/dist/cjs/types/content-reference.d.ts +213 -0
  19. package/dist/cjs/types/index.d.ts +4 -0
  20. package/dist/esm/index12.js +15 -2
  21. package/dist/esm/index12.js.map +1 -1
  22. package/dist/esm/index14.js +119 -95
  23. package/dist/esm/index14.js.map +1 -1
  24. package/dist/esm/index15.js +159 -114
  25. package/dist/esm/index15.js.map +1 -1
  26. package/dist/esm/index16.js +122 -81
  27. package/dist/esm/index16.js.map +1 -1
  28. package/dist/esm/index17.js +236 -0
  29. package/dist/esm/index17.js.map +1 -0
  30. package/dist/esm/index18.js +95 -0
  31. package/dist/esm/index18.js.map +1 -0
  32. package/dist/esm/index19.js +663 -0
  33. package/dist/esm/index19.js.map +1 -0
  34. package/dist/esm/index2.js +3 -1
  35. package/dist/esm/index2.js.map +1 -1
  36. package/dist/esm/index20.js +233 -0
  37. package/dist/esm/index20.js.map +1 -0
  38. package/dist/esm/index21.js +182 -0
  39. package/dist/esm/index21.js.map +1 -0
  40. package/dist/esm/index22.js +126 -0
  41. package/dist/esm/index22.js.map +1 -0
  42. package/dist/esm/index23.js +68 -0
  43. package/dist/esm/index23.js.map +1 -0
  44. package/dist/esm/index24.js +38 -0
  45. package/dist/esm/index24.js.map +1 -0
  46. package/dist/esm/index6.js +143 -84
  47. package/dist/esm/index6.js.map +1 -1
  48. package/dist/esm/index7.js.map +1 -1
  49. package/dist/esm/index8.js +69 -5
  50. package/dist/esm/index8.js.map +1 -1
  51. package/dist/types/base-agent.d.ts +6 -0
  52. package/dist/types/context/ReferenceContextManager.d.ts +84 -0
  53. package/dist/types/context/ReferenceResponseProcessor.d.ts +76 -0
  54. package/dist/types/conversational-agent.d.ts +34 -0
  55. package/dist/types/langchain-agent.d.ts +1 -0
  56. package/dist/types/mcp/ContentProcessor.d.ts +37 -0
  57. package/dist/types/mcp/MCPClientManager.d.ts +19 -1
  58. package/dist/types/memory/ContentStorage.d.ts +205 -0
  59. package/dist/types/memory/MemoryWindow.d.ts +114 -0
  60. package/dist/types/memory/ReferenceIdGenerator.d.ts +45 -0
  61. package/dist/types/memory/SmartMemoryManager.d.ts +201 -0
  62. package/dist/types/memory/TokenCounter.d.ts +61 -0
  63. package/dist/types/memory/index.d.ts +7 -0
  64. package/dist/types/plugins/hbar-transfer/TransferHbarTool.d.ts +18 -0
  65. package/dist/types/services/ContentStoreManager.d.ts +54 -0
  66. package/dist/types/types/content-reference.d.ts +213 -0
  67. package/dist/types/types/index.d.ts +4 -0
  68. package/package.json +30 -26
  69. package/src/base-agent.ts +6 -0
  70. package/src/context/ReferenceContextManager.ts +345 -0
  71. package/src/context/ReferenceResponseProcessor.ts +296 -0
  72. package/src/conversational-agent.ts +166 -92
  73. package/src/langchain-agent.ts +89 -2
  74. package/src/mcp/ContentProcessor.ts +317 -0
  75. package/src/mcp/MCPClientManager.ts +61 -1
  76. package/src/mcp/adapters/langchain.ts +9 -4
  77. package/src/memory/ContentStorage.ts +954 -0
  78. package/src/memory/MemoryWindow.ts +247 -0
  79. package/src/memory/ReferenceIdGenerator.ts +84 -0
  80. package/src/memory/SmartMemoryManager.ts +323 -0
  81. package/src/memory/TokenCounter.ts +152 -0
  82. package/src/memory/index.ts +8 -0
  83. package/src/plugins/hbar-transfer/TransferHbarTool.ts +19 -1
  84. package/src/plugins/hcs-10/HCS10Plugin.ts +5 -4
  85. package/src/services/ContentStoreManager.ts +199 -0
  86. package/src/types/content-reference.ts +281 -0
  87. package/src/types/index.ts +6 -0
@@ -0,0 +1,954 @@
1
+ import type { BaseMessage } from '@langchain/core/messages';
2
+ import { ReferenceIdGenerator } from './ReferenceIdGenerator';
3
+ import {
4
+ ReferenceId,
5
+ ContentReference,
6
+ ContentMetadata,
7
+ ReferenceResolutionResult,
8
+ ContentReferenceConfig,
9
+ ContentReferenceStore,
10
+ ContentReferenceStats,
11
+ ContentReferenceError,
12
+ ContentType,
13
+ ContentSource,
14
+ ReferenceLifecycleState,
15
+ DEFAULT_CONTENT_REFERENCE_CONFIG
16
+ } from '../types/content-reference';
17
+
18
+ /**
19
+ * Stored message with metadata
20
+ */
21
+ interface StoredMessage {
22
+ message: BaseMessage;
23
+ storedAt: Date;
24
+ id: string;
25
+ }
26
+
27
+ /**
28
+ * Search options for message queries
29
+ */
30
+ interface SearchOptions {
31
+ /** Whether to perform case-sensitive search */
32
+ caseSensitive?: boolean;
33
+ /** Maximum number of results to return */
34
+ limit?: number;
35
+ /** Whether to use regex pattern matching */
36
+ useRegex?: boolean;
37
+ }
38
+
39
+ /**
40
+ * Result of storing messages
41
+ */
42
+ interface StoreResult {
43
+ /** Number of messages successfully stored */
44
+ stored: number;
45
+ /** Number of old messages dropped to make room */
46
+ dropped: number;
47
+ }
48
+
49
+ /**
50
+ * Storage statistics
51
+ */
52
+ export interface StorageStats {
53
+ /** Total number of messages currently stored */
54
+ totalMessages: number;
55
+ /** Maximum storage capacity */
56
+ maxStorageLimit: number;
57
+ /** Percentage of storage used */
58
+ usagePercentage: number;
59
+ /** Timestamp of oldest message */
60
+ oldestMessageTime: Date | undefined;
61
+ /** Timestamp of newest message */
62
+ newestMessageTime: Date | undefined;
63
+ }
64
+
65
+ /**
66
+ * Stored content with reference metadata
67
+ */
68
+ interface StoredContent {
69
+ /** The actual content buffer */
70
+ content: Buffer;
71
+
72
+ /** Complete metadata */
73
+ metadata: ContentMetadata;
74
+
75
+ /** Current lifecycle state */
76
+ state: ReferenceLifecycleState;
77
+
78
+ /** When this reference expires (if applicable) */
79
+ expiresAt?: Date;
80
+ }
81
+
82
+ /**
83
+ * Content storage for managing pruned conversation messages and large content references
84
+ * Provides searchable storage with time-based querying and automatic cleanup.
85
+ *
86
+ * Extended to support reference-based storage for large content to optimize context window usage.
87
+ */
88
+ export class ContentStorage implements ContentReferenceStore {
89
+ private messages: StoredMessage[] = [];
90
+ private maxStorage: number;
91
+ private idCounter: number = 0;
92
+
93
+ // Reference-based content storage
94
+ private contentStore: Map<ReferenceId, StoredContent> = new Map();
95
+ private referenceConfig: ContentReferenceConfig;
96
+ private cleanupTimer?: NodeJS.Timeout;
97
+ private referenceStats: Omit<ContentReferenceStats, 'performanceMetrics'> & {
98
+ performanceMetrics: ContentReferenceStats['performanceMetrics'] & {
99
+ creationTimes: number[];
100
+ resolutionTimes: number[];
101
+ cleanupTimes: number[];
102
+ };
103
+ };
104
+
105
+ // Default storage limit for messages
106
+ public static readonly DEFAULT_MAX_STORAGE = 1000;
107
+
108
+ constructor(
109
+ maxStorage: number = ContentStorage.DEFAULT_MAX_STORAGE,
110
+ referenceConfig?: Partial<ContentReferenceConfig>
111
+ ) {
112
+ this.maxStorage = maxStorage;
113
+
114
+ // Initialize reference-based storage
115
+ this.referenceConfig = { ...DEFAULT_CONTENT_REFERENCE_CONFIG, ...referenceConfig };
116
+ this.referenceStats = {
117
+ activeReferences: 0,
118
+ totalStorageBytes: 0,
119
+ recentlyCleanedUp: 0,
120
+ totalResolutions: 0,
121
+ failedResolutions: 0,
122
+ averageContentSize: 0,
123
+ storageUtilization: 0,
124
+ performanceMetrics: {
125
+ averageCreationTimeMs: 0,
126
+ averageResolutionTimeMs: 0,
127
+ averageCleanupTimeMs: 0,
128
+ creationTimes: [],
129
+ resolutionTimes: [],
130
+ cleanupTimes: []
131
+ }
132
+ };
133
+
134
+ // Start cleanup timer if enabled
135
+ if (this.referenceConfig.enableAutoCleanup) {
136
+ this.startReferenceCleanupTimer();
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Store messages in the content storage
142
+ * Automatically drops oldest messages if storage limit is exceeded
143
+ * @param messages - Messages to store
144
+ * @returns Result indicating how many messages were stored and dropped
145
+ */
146
+ storeMessages(messages: BaseMessage[]): StoreResult {
147
+ if (messages.length === 0) {
148
+ return { stored: 0, dropped: 0 };
149
+ }
150
+
151
+ const now = new Date();
152
+ let dropped = 0;
153
+
154
+ // Convert messages to stored format
155
+ const storedMessages: StoredMessage[] = messages.map(message => ({
156
+ message,
157
+ storedAt: now,
158
+ id: this.generateId()
159
+ }));
160
+
161
+ // Add new messages
162
+ this.messages.push(...storedMessages);
163
+
164
+ // Remove oldest messages if we exceed the limit
165
+ while (this.messages.length > this.maxStorage) {
166
+ this.messages.shift();
167
+ dropped++;
168
+ }
169
+
170
+ return {
171
+ stored: storedMessages.length,
172
+ dropped
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Get the most recent messages from storage
178
+ * @param count - Number of recent messages to retrieve
179
+ * @returns Array of recent messages in chronological order
180
+ */
181
+ getRecentMessages(count: number): BaseMessage[] {
182
+ if (count <= 0 || this.messages.length === 0) {
183
+ return [];
184
+ }
185
+
186
+ const startIndex = Math.max(0, this.messages.length - count);
187
+ return this.messages
188
+ .slice(startIndex)
189
+ .map(stored => stored.message);
190
+ }
191
+
192
+ /**
193
+ * Search for messages containing specific text or patterns
194
+ * @param query - Search term or regex pattern
195
+ * @param options - Search configuration options
196
+ * @returns Array of matching messages
197
+ */
198
+ searchMessages(query: string, options: SearchOptions = {}): BaseMessage[] {
199
+ if (!query || this.messages.length === 0) {
200
+ return [];
201
+ }
202
+
203
+ const {
204
+ caseSensitive = false,
205
+ limit,
206
+ useRegex = false
207
+ } = options;
208
+
209
+ let matches: BaseMessage[] = [];
210
+
211
+ if (useRegex) {
212
+ try {
213
+ const regex = new RegExp(query, caseSensitive ? 'g' : 'gi');
214
+ matches = this.messages
215
+ .filter(stored => regex.test(stored.message.content as string))
216
+ .map(stored => stored.message);
217
+ } catch (error) {
218
+ console.warn('Invalid regex pattern:', query, error);
219
+ return [];
220
+ }
221
+ } else {
222
+ const searchTerm = caseSensitive ? query : query.toLowerCase();
223
+ matches = this.messages
224
+ .filter(stored => {
225
+ const content = stored.message.content as string;
226
+ const searchContent = caseSensitive ? content : content.toLowerCase();
227
+ return searchContent.includes(searchTerm);
228
+ })
229
+ .map(stored => stored.message);
230
+ }
231
+
232
+ return limit ? matches.slice(0, limit) : matches;
233
+ }
234
+
235
+ /**
236
+ * Get messages from a specific time range
237
+ * @param startTime - Start of time range (inclusive)
238
+ * @param endTime - End of time range (inclusive)
239
+ * @returns Array of messages within the time range
240
+ */
241
+ getMessagesFromTimeRange(startTime: Date, endTime: Date): BaseMessage[] {
242
+ if (startTime > endTime || this.messages.length === 0) {
243
+ return [];
244
+ }
245
+
246
+ return this.messages
247
+ .filter(stored =>
248
+ stored.storedAt >= startTime && stored.storedAt <= endTime
249
+ )
250
+ .map(stored => stored.message);
251
+ }
252
+
253
+ /**
254
+ * Get storage statistics and usage information
255
+ * @returns Current storage statistics
256
+ */
257
+ getStorageStats(): StorageStats {
258
+ const totalMessages = this.messages.length;
259
+ const usagePercentage = totalMessages > 0
260
+ ? Math.round((totalMessages / this.maxStorage) * 100)
261
+ : 0;
262
+
263
+ let oldestMessageTime: Date | undefined;
264
+ let newestMessageTime: Date | undefined;
265
+
266
+ if (totalMessages > 0) {
267
+ oldestMessageTime = this.messages[0].storedAt;
268
+ newestMessageTime = this.messages[totalMessages - 1].storedAt;
269
+ }
270
+
271
+ return {
272
+ totalMessages,
273
+ maxStorageLimit: this.maxStorage,
274
+ usagePercentage,
275
+ oldestMessageTime,
276
+ newestMessageTime
277
+ };
278
+ }
279
+
280
+ /**
281
+ * Clear all stored messages
282
+ */
283
+ clear(): void {
284
+ this.messages = [];
285
+ this.idCounter = 0;
286
+ }
287
+
288
+ /**
289
+ * Get total number of stored messages
290
+ * @returns Number of messages currently in storage
291
+ */
292
+ getTotalStoredMessages(): number {
293
+ return this.messages.length;
294
+ }
295
+
296
+ /**
297
+ * Update the maximum storage limit
298
+ * @param newLimit - New maximum storage limit
299
+ */
300
+ updateStorageLimit(newLimit: number): void {
301
+ if (newLimit <= 0) {
302
+ throw new Error('Storage limit must be greater than 0');
303
+ }
304
+
305
+ this.maxStorage = newLimit;
306
+
307
+ // Prune messages if the new limit is smaller
308
+ while (this.messages.length > this.maxStorage) {
309
+ this.messages.shift();
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Get messages by message type
315
+ * @param messageType - Type of messages to retrieve ('human', 'ai', 'system', etc.)
316
+ * @param limit - Maximum number of messages to return
317
+ * @returns Array of messages of the specified type
318
+ */
319
+ getMessagesByType(messageType: string, limit?: number): BaseMessage[] {
320
+ const filtered = this.messages
321
+ .filter(stored => stored.message._getType() === messageType)
322
+ .map(stored => stored.message);
323
+
324
+ return limit ? filtered.slice(0, limit) : filtered;
325
+ }
326
+
327
+ /**
328
+ * Get the current storage configuration
329
+ * @returns Storage configuration object
330
+ */
331
+ getConfig() {
332
+ return {
333
+ maxStorage: this.maxStorage,
334
+ currentUsage: this.messages.length,
335
+ utilizationPercentage: (this.messages.length / this.maxStorage) * 100
336
+ };
337
+ }
338
+
339
+ /**
340
+ * Generate a unique ID for stored messages
341
+ * @returns Unique string identifier
342
+ */
343
+ private generateId(): string {
344
+ return `msg_${++this.idCounter}_${Date.now()}`;
345
+ }
346
+
347
+ /**
348
+ * Get messages stored within the last N minutes
349
+ * @param minutes - Number of minutes to look back
350
+ * @returns Array of messages from the last N minutes
351
+ */
352
+ getRecentMessagesByTime(minutes: number): BaseMessage[] {
353
+ if (minutes <= 0 || this.messages.length === 0) {
354
+ return [];
355
+ }
356
+
357
+ const cutoffTime = new Date(Date.now() - (minutes * 60 * 1000));
358
+
359
+ return this.messages
360
+ .filter(stored => stored.storedAt >= cutoffTime)
361
+ .map(stored => stored.message);
362
+ }
363
+
364
+ /**
365
+ * Export messages to a JSON-serializable format
366
+ * @returns Serializable representation of stored messages
367
+ */
368
+ exportMessages() {
369
+ return this.messages.map(stored => ({
370
+ content: stored.message.content,
371
+ type: stored.message._getType(),
372
+ storedAt: stored.storedAt.toISOString(),
373
+ id: stored.id
374
+ }));
375
+ }
376
+
377
+ // ========== Reference-Based Content Storage Methods ==========
378
+
379
+ /**
380
+ * Determine if content should be stored as a reference based on size
381
+ */
382
+ shouldUseReference(content: Buffer | string): boolean {
383
+ const size = Buffer.isBuffer(content) ? content.length : Buffer.byteLength(content, 'utf8');
384
+ return size > this.referenceConfig.sizeThresholdBytes;
385
+ }
386
+
387
+ /**
388
+ * Store content and return a reference if it exceeds the size threshold
389
+ * Otherwise returns null to indicate direct content should be used
390
+ */
391
+ async storeContentIfLarge(
392
+ content: Buffer | string,
393
+ metadata: {
394
+ contentType?: ContentType;
395
+ mimeType?: string;
396
+ source: ContentSource;
397
+ mcpToolName?: string;
398
+ fileName?: string;
399
+ tags?: string[];
400
+ customMetadata?: Record<string, unknown>;
401
+ }
402
+ ): Promise<ContentReference | null> {
403
+ const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content, 'utf8');
404
+
405
+ if (!this.shouldUseReference(buffer)) {
406
+ return null;
407
+ }
408
+
409
+ const storeMetadata: Omit<ContentMetadata, 'createdAt' | 'lastAccessedAt' | 'accessCount'> = {
410
+ contentType: metadata.contentType || this.detectContentType(buffer, metadata.mimeType),
411
+ sizeBytes: buffer.length,
412
+ source: metadata.source,
413
+ tags: []
414
+ };
415
+
416
+ if (metadata.mimeType !== undefined) {
417
+ storeMetadata.mimeType = metadata.mimeType;
418
+ }
419
+ if (metadata.mcpToolName !== undefined) {
420
+ storeMetadata.mcpToolName = metadata.mcpToolName;
421
+ }
422
+ if (metadata.fileName !== undefined) {
423
+ storeMetadata.fileName = metadata.fileName;
424
+ }
425
+ if (metadata.tags !== undefined) {
426
+ storeMetadata.tags = metadata.tags;
427
+ }
428
+ if (metadata.customMetadata !== undefined) {
429
+ storeMetadata.customMetadata = metadata.customMetadata;
430
+ }
431
+
432
+ return await this.storeContent(buffer, storeMetadata);
433
+ }
434
+
435
+ /**
436
+ * Store content and return a reference (implements ContentReferenceStore)
437
+ */
438
+ async storeContent(
439
+ content: Buffer,
440
+ metadata: Omit<ContentMetadata, 'createdAt' | 'lastAccessedAt' | 'accessCount'>
441
+ ): Promise<ContentReference> {
442
+ const startTime = Date.now();
443
+
444
+ try {
445
+ const now = new Date();
446
+ const referenceId = ReferenceIdGenerator.generateId(content);
447
+
448
+ const fullMetadata: ContentMetadata = {
449
+ ...metadata,
450
+ createdAt: now,
451
+ lastAccessedAt: now,
452
+ accessCount: 0
453
+ };
454
+
455
+ const storedContent: StoredContent = {
456
+ content,
457
+ metadata: fullMetadata,
458
+ state: 'active'
459
+ };
460
+
461
+ const expirationTime = this.calculateExpirationTime(metadata.source);
462
+ if (expirationTime !== undefined) {
463
+ storedContent.expiresAt = expirationTime;
464
+ }
465
+
466
+ this.contentStore.set(referenceId, storedContent);
467
+
468
+ // Update statistics
469
+ this.updateStatsAfterStore(content.length);
470
+
471
+ // Enforce storage limits after storing
472
+ await this.enforceReferenceStorageLimits();
473
+
474
+ // Create preview
475
+ const preview = this.createContentPreview(content, fullMetadata.contentType);
476
+
477
+ const referenceMetadata: Pick<ContentMetadata, 'contentType' | 'sizeBytes' | 'source' | 'fileName' | 'mimeType'> = {
478
+ contentType: fullMetadata.contentType,
479
+ sizeBytes: fullMetadata.sizeBytes,
480
+ source: fullMetadata.source
481
+ };
482
+
483
+ if (fullMetadata.fileName !== undefined) {
484
+ referenceMetadata.fileName = fullMetadata.fileName;
485
+ }
486
+ if (fullMetadata.mimeType !== undefined) {
487
+ referenceMetadata.mimeType = fullMetadata.mimeType;
488
+ }
489
+
490
+ const reference: ContentReference = {
491
+ referenceId,
492
+ state: 'active',
493
+ preview,
494
+ metadata: referenceMetadata,
495
+ createdAt: now,
496
+ format: 'ref://{id}' as const
497
+ };
498
+
499
+ // Record performance
500
+ const duration = Date.now() - startTime;
501
+ this.recordPerformanceMetric('creation', duration);
502
+
503
+ console.log(`[ContentStorage] Stored content with reference ID: ${referenceId} (${content.length} bytes)`);
504
+
505
+ return reference;
506
+ } catch (error) {
507
+ const duration = Date.now() - startTime;
508
+ this.recordPerformanceMetric('creation', duration);
509
+
510
+ console.error('[ContentStorage] Failed to store content:', error);
511
+ throw new ContentReferenceError(
512
+ `Failed to store content: ${error instanceof Error ? error.message : 'Unknown error'}`,
513
+ 'system_error',
514
+ undefined,
515
+ ['Try again', 'Check storage limits', 'Contact administrator']
516
+ );
517
+ }
518
+ }
519
+
520
+ /**
521
+ * Resolve a reference to its content (implements ContentReferenceStore)
522
+ */
523
+ async resolveReference(referenceId: ReferenceId): Promise<ReferenceResolutionResult> {
524
+ const startTime = Date.now();
525
+
526
+ try {
527
+ // Validate reference ID format
528
+ if (!ReferenceIdGenerator.isValidReferenceId(referenceId)) {
529
+ this.referenceStats.failedResolutions++;
530
+ return {
531
+ success: false,
532
+ error: 'Invalid reference ID format',
533
+ errorType: 'not_found',
534
+ suggestedActions: ['Check the reference ID format', 'Ensure the reference ID is complete']
535
+ };
536
+ }
537
+
538
+ const storedContent = this.contentStore.get(referenceId);
539
+
540
+ if (!storedContent) {
541
+ this.referenceStats.failedResolutions++;
542
+ return {
543
+ success: false,
544
+ error: 'Reference not found',
545
+ errorType: 'not_found',
546
+ suggestedActions: ['Verify the reference ID', 'Check if the content has expired', 'Request fresh content']
547
+ };
548
+ }
549
+
550
+ // Check if expired
551
+ if (storedContent.expiresAt && storedContent.expiresAt < new Date()) {
552
+ storedContent.state = 'expired';
553
+ this.referenceStats.failedResolutions++;
554
+ return {
555
+ success: false,
556
+ error: 'Reference has expired',
557
+ errorType: 'expired',
558
+ suggestedActions: ['Request fresh content', 'Use alternative content source']
559
+ };
560
+ }
561
+
562
+ // Check state
563
+ if (storedContent.state !== 'active') {
564
+ this.referenceStats.failedResolutions++;
565
+ return {
566
+ success: false,
567
+ error: `Reference is ${storedContent.state}`,
568
+ errorType: storedContent.state === 'expired' ? 'expired' : 'corrupted',
569
+ suggestedActions: ['Request fresh content', 'Check reference validity']
570
+ };
571
+ }
572
+
573
+ // Update access tracking
574
+ storedContent.metadata.lastAccessedAt = new Date();
575
+ storedContent.metadata.accessCount++;
576
+
577
+ // Update statistics
578
+ this.referenceStats.totalResolutions++;
579
+
580
+ // Record performance
581
+ const duration = Date.now() - startTime;
582
+ this.recordPerformanceMetric('resolution', duration);
583
+
584
+ console.log(`[ContentStorage] Resolved reference ${referenceId} (${storedContent.content.length} bytes, access count: ${storedContent.metadata.accessCount})`);
585
+
586
+ return {
587
+ success: true,
588
+ content: storedContent.content,
589
+ metadata: storedContent.metadata
590
+ };
591
+ } catch (error) {
592
+ const duration = Date.now() - startTime;
593
+ this.recordPerformanceMetric('resolution', duration);
594
+
595
+ this.referenceStats.failedResolutions++;
596
+ console.error(`[ContentStorage] Error resolving reference ${referenceId}:`, error);
597
+
598
+ return {
599
+ success: false,
600
+ error: `System error resolving reference: ${error instanceof Error ? error.message : 'Unknown error'}`,
601
+ errorType: 'system_error',
602
+ suggestedActions: ['Try again', 'Contact administrator']
603
+ };
604
+ }
605
+ }
606
+
607
+ /**
608
+ * Check if a reference exists and is valid
609
+ */
610
+ async hasReference(referenceId: ReferenceId): Promise<boolean> {
611
+ if (!ReferenceIdGenerator.isValidReferenceId(referenceId)) {
612
+ return false;
613
+ }
614
+
615
+ const storedContent = this.contentStore.get(referenceId);
616
+ if (!storedContent) {
617
+ return false;
618
+ }
619
+
620
+ // Check if expired
621
+ if (storedContent.expiresAt && storedContent.expiresAt < new Date()) {
622
+ storedContent.state = 'expired';
623
+ return false;
624
+ }
625
+
626
+ return storedContent.state === 'active';
627
+ }
628
+
629
+ /**
630
+ * Mark a reference for cleanup
631
+ */
632
+ async cleanupReference(referenceId: ReferenceId): Promise<boolean> {
633
+ const storedContent = this.contentStore.get(referenceId);
634
+ if (!storedContent) {
635
+ return false;
636
+ }
637
+
638
+ // Update statistics
639
+ this.referenceStats.totalStorageBytes -= storedContent.content.length;
640
+ this.referenceStats.activeReferences--;
641
+ this.referenceStats.recentlyCleanedUp++;
642
+
643
+ this.contentStore.delete(referenceId);
644
+
645
+ console.log(`[ContentStorage] Cleaned up reference ${referenceId} (${storedContent.content.length} bytes)`);
646
+ return true;
647
+ }
648
+
649
+ /**
650
+ * Get current reference storage statistics (implements ContentReferenceStore)
651
+ */
652
+ async getStats(): Promise<ContentReferenceStats> {
653
+ this.updateReferenceStorageStats();
654
+
655
+ return {
656
+ ...this.referenceStats,
657
+ performanceMetrics: {
658
+ averageCreationTimeMs: this.calculateAverage(this.referenceStats.performanceMetrics.creationTimes),
659
+ averageResolutionTimeMs: this.calculateAverage(this.referenceStats.performanceMetrics.resolutionTimes),
660
+ averageCleanupTimeMs: this.calculateAverage(this.referenceStats.performanceMetrics.cleanupTimes)
661
+ }
662
+ };
663
+ }
664
+
665
+ /**
666
+ * Update reference configuration
667
+ */
668
+ async updateConfig(config: Partial<ContentReferenceConfig>): Promise<void> {
669
+ this.referenceConfig = { ...this.referenceConfig, ...config };
670
+
671
+ // Restart cleanup timer if needed
672
+ if (this.cleanupTimer) {
673
+ clearInterval(this.cleanupTimer);
674
+ delete this.cleanupTimer;
675
+ }
676
+
677
+ if (this.referenceConfig.enableAutoCleanup) {
678
+ this.startReferenceCleanupTimer();
679
+ }
680
+
681
+ console.log('[ContentStorage] Reference configuration updated');
682
+ }
683
+
684
+ /**
685
+ * Perform cleanup based on current policies (implements ContentReferenceStore)
686
+ */
687
+ async performCleanup(): Promise<{ cleanedUp: number; errors: string[] }> {
688
+ const startTime = Date.now();
689
+ const errors: string[] = [];
690
+ let cleanedUp = 0;
691
+
692
+ try {
693
+ console.log('[ContentStorage] Starting reference cleanup process...');
694
+
695
+ const now = new Date();
696
+ const toCleanup: ReferenceId[] = [];
697
+
698
+ // Identify references for cleanup
699
+ for (const [referenceId, storedContent] of this.contentStore.entries()) {
700
+ let shouldCleanup = false;
701
+
702
+ // Check expiration
703
+ if (storedContent.expiresAt && storedContent.expiresAt < now) {
704
+ shouldCleanup = true;
705
+ storedContent.state = 'expired';
706
+ }
707
+
708
+ // Check age-based policies
709
+ const ageMs = now.getTime() - storedContent.metadata.createdAt.getTime();
710
+ const policy = this.getCleanupPolicy(storedContent.metadata.source);
711
+
712
+ if (ageMs > policy.maxAgeMs) {
713
+ shouldCleanup = true;
714
+ }
715
+
716
+ // Check if marked for cleanup
717
+ if (storedContent.state === 'cleanup_pending') {
718
+ shouldCleanup = true;
719
+ }
720
+
721
+ if (shouldCleanup) {
722
+ toCleanup.push(referenceId);
723
+ }
724
+ }
725
+
726
+ // Sort by priority (higher priority = cleanup first)
727
+ toCleanup.sort((a, b) => {
728
+ const aContent = this.contentStore.get(a)!;
729
+ const bContent = this.contentStore.get(b)!;
730
+ const aPriority = this.getCleanupPolicy(aContent.metadata.source).priority;
731
+ const bPriority = this.getCleanupPolicy(bContent.metadata.source).priority;
732
+ return bPriority - aPriority; // Higher priority first
733
+ });
734
+
735
+ // Perform cleanup
736
+ for (const referenceId of toCleanup) {
737
+ try {
738
+ const success = await this.cleanupReference(referenceId);
739
+ if (success) {
740
+ cleanedUp++;
741
+ }
742
+ } catch (error) {
743
+ errors.push(`Failed to cleanup ${referenceId}: ${error instanceof Error ? error.message : 'Unknown error'}`);
744
+ }
745
+ }
746
+
747
+ // Check storage limits and cleanup oldest if needed
748
+ if (this.contentStore.size > this.referenceConfig.maxReferences) {
749
+ const sortedByAge = Array.from(this.contentStore.entries())
750
+ .sort(([, a], [, b]) => a.metadata.lastAccessedAt.getTime() - b.metadata.lastAccessedAt.getTime());
751
+
752
+ const excessCount = this.contentStore.size - this.referenceConfig.maxReferences;
753
+ for (let i = 0; i < excessCount && i < sortedByAge.length; i++) {
754
+ const [referenceId] = sortedByAge[i];
755
+ try {
756
+ const success = await this.cleanupReference(referenceId);
757
+ if (success) {
758
+ cleanedUp++;
759
+ }
760
+ } catch (error) {
761
+ errors.push(`Failed to cleanup excess reference ${referenceId}: ${error instanceof Error ? error.message : 'Unknown error'}`);
762
+ }
763
+ }
764
+ }
765
+
766
+ const duration = Date.now() - startTime;
767
+ this.recordPerformanceMetric('cleanup', duration);
768
+
769
+ console.log(`[ContentStorage] Reference cleanup completed: ${cleanedUp} references cleaned up, ${errors.length} errors`);
770
+
771
+ return { cleanedUp, errors };
772
+ } catch (error) {
773
+ const duration = Date.now() - startTime;
774
+ this.recordPerformanceMetric('cleanup', duration);
775
+
776
+ const errorMessage = `Cleanup process failed: ${error instanceof Error ? error.message : 'Unknown error'}`;
777
+ console.error('[ContentStorage]', errorMessage);
778
+ errors.push(errorMessage);
779
+
780
+ return { cleanedUp, errors };
781
+ }
782
+ }
783
+
784
+ /**
785
+ * Get reference configuration for debugging
786
+ */
787
+ getReferenceConfig(): ContentReferenceConfig {
788
+ return { ...this.referenceConfig };
789
+ }
790
+
791
+ // ========== Private Reference Storage Helper Methods ==========
792
+
793
+ private async enforceReferenceStorageLimits(): Promise<void> {
794
+ // Check reference count limit
795
+ if (this.contentStore.size >= this.referenceConfig.maxReferences) {
796
+ await this.performCleanup();
797
+ }
798
+
799
+ // Check total storage size limit
800
+ if (this.referenceStats.totalStorageBytes >= this.referenceConfig.maxTotalStorageBytes) {
801
+ await this.performCleanup();
802
+ }
803
+ }
804
+
805
+ private calculateExpirationTime(source: ContentSource): Date | undefined {
806
+ const policy = this.getCleanupPolicy(source);
807
+ return new Date(Date.now() + policy.maxAgeMs);
808
+ }
809
+
810
+ private getCleanupPolicy(source: ContentSource) {
811
+ switch (source) {
812
+ case 'mcp_tool':
813
+ return this.referenceConfig.cleanupPolicies.recent;
814
+ case 'user_upload':
815
+ return this.referenceConfig.cleanupPolicies.userContent;
816
+ case 'agent_generated':
817
+ return this.referenceConfig.cleanupPolicies.agentGenerated;
818
+ default:
819
+ return this.referenceConfig.cleanupPolicies.default;
820
+ }
821
+ }
822
+
823
+ private detectContentType(content: Buffer, mimeType?: string): ContentType {
824
+ if (mimeType) {
825
+ if (mimeType === 'text/html') return 'html';
826
+ if (mimeType === 'text/markdown') return 'markdown';
827
+ if (mimeType === 'application/json') return 'json';
828
+ if (mimeType.startsWith('text/')) return 'text';
829
+ return 'binary';
830
+ }
831
+
832
+ // Simple content detection
833
+ const contentStr = content.toString('utf8', 0, Math.min(content.length, 1000));
834
+ if (contentStr.startsWith('{') || contentStr.startsWith('[')) return 'json';
835
+ if (contentStr.includes('<html>') || contentStr.includes('<!DOCTYPE')) return 'html';
836
+ if (contentStr.includes('#') && contentStr.includes('\n')) return 'markdown';
837
+
838
+ return 'text';
839
+ }
840
+
841
+ private createContentPreview(content: Buffer, contentType: ContentType): string {
842
+ const maxLength = 200;
843
+ let preview = content.toString('utf8', 0, Math.min(content.length, maxLength * 2));
844
+
845
+ // Clean up based on content type
846
+ if (contentType === 'html') {
847
+ // Remove all HTML tags and normalize whitespace
848
+ preview = preview
849
+ .replace(/<[^>]*>/g, '')
850
+ .replace(/\s+/g, ' ')
851
+ .trim();
852
+ } else if (contentType === 'json') {
853
+ try {
854
+ const parsed = JSON.parse(preview);
855
+ preview = JSON.stringify(parsed, null, 0);
856
+ } catch {
857
+ // Keep original if not valid JSON
858
+ }
859
+ }
860
+
861
+ preview = preview.trim();
862
+ if (preview.length > maxLength) {
863
+ preview = preview.substring(0, maxLength) + '...';
864
+ }
865
+
866
+ return preview || '[Binary content]';
867
+ }
868
+
869
+ private updateStatsAfterStore(sizeBytes: number): void {
870
+ this.referenceStats.activeReferences++;
871
+ this.referenceStats.totalStorageBytes += sizeBytes;
872
+ this.updateReferenceStorageStats();
873
+ }
874
+
875
+ private updateReferenceStorageStats(): void {
876
+ if (this.referenceStats.activeReferences > 0) {
877
+ this.referenceStats.averageContentSize = this.referenceStats.totalStorageBytes / this.referenceStats.activeReferences;
878
+ }
879
+
880
+ this.referenceStats.storageUtilization = (this.referenceStats.totalStorageBytes / this.referenceConfig.maxTotalStorageBytes) * 100;
881
+
882
+ // Find most accessed reference
883
+ let mostAccessedId: ReferenceId | undefined;
884
+ let maxAccess = 0;
885
+
886
+ for (const [referenceId, storedContent] of this.contentStore.entries()) {
887
+ if (storedContent.metadata.accessCount > maxAccess) {
888
+ maxAccess = storedContent.metadata.accessCount;
889
+ mostAccessedId = referenceId;
890
+ }
891
+ }
892
+
893
+ if (mostAccessedId !== undefined) {
894
+ this.referenceStats.mostAccessedReferenceId = mostAccessedId;
895
+ } else {
896
+ delete this.referenceStats.mostAccessedReferenceId;
897
+ }
898
+ }
899
+
900
+ private recordPerformanceMetric(type: 'creation' | 'resolution' | 'cleanup', timeMs: number): void {
901
+ const metrics = this.referenceStats.performanceMetrics;
902
+ const maxRecords = 100; // Keep last 100 measurements
903
+
904
+ switch (type) {
905
+ case 'creation':
906
+ metrics.creationTimes.push(timeMs);
907
+ if (metrics.creationTimes.length > maxRecords) {
908
+ metrics.creationTimes.shift();
909
+ }
910
+ break;
911
+ case 'resolution':
912
+ metrics.resolutionTimes.push(timeMs);
913
+ if (metrics.resolutionTimes.length > maxRecords) {
914
+ metrics.resolutionTimes.shift();
915
+ }
916
+ break;
917
+ case 'cleanup':
918
+ metrics.cleanupTimes.push(timeMs);
919
+ if (metrics.cleanupTimes.length > maxRecords) {
920
+ metrics.cleanupTimes.shift();
921
+ }
922
+ break;
923
+ }
924
+ }
925
+
926
+ private calculateAverage(times: number[]): number {
927
+ if (times.length === 0) return 0;
928
+ return times.reduce((sum, time) => sum + time, 0) / times.length;
929
+ }
930
+
931
+ private startReferenceCleanupTimer(): void {
932
+ this.cleanupTimer = setInterval(async () => {
933
+ try {
934
+ await this.performCleanup();
935
+ } catch (error) {
936
+ console.error('[ContentStorage] Error in scheduled reference cleanup:', error);
937
+ }
938
+ }, this.referenceConfig.cleanupIntervalMs);
939
+ }
940
+
941
+ /**
942
+ * Clean up resources (enhanced to include reference cleanup)
943
+ */
944
+ async dispose(): Promise<void> {
945
+ if (this.cleanupTimer) {
946
+ clearInterval(this.cleanupTimer);
947
+ delete this.cleanupTimer;
948
+ }
949
+
950
+ this.contentStore.clear();
951
+
952
+ this.clear();
953
+ }
954
+ }