mark-improving-agent 2.3.4 → 2.3.5

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/VERSION CHANGED
@@ -1 +1 @@
1
- 2.3.4
1
+ 2.3.5
@@ -160,7 +160,7 @@ export function addToBinaryIndex(index, vectors, ids, metadata) {
160
160
  return {
161
161
  vectors: newVectors,
162
162
  indices: [...index.indices, ...ids.map((_, i) => index.indices.length + i)],
163
- metadata: [...index.metadata, ...metadata || ids.map(() => ({ id: '', importance: 0.5, timestamp: Date.now() }))],
163
+ metadata: [...index.metadata, ...metadata || ids.map((id, i) => ({ id, importance: 0.5, timestamp: Date.now() }))],
164
164
  config: index.config,
165
165
  };
166
166
  }
@@ -0,0 +1,420 @@
1
+ /**
2
+ * Budget-Aware Memory Retrieval System
3
+ *
4
+ * A deterministic, token-budget-conscious memory retrieval system that:
5
+ * - Respects max token budgets during retrieval
6
+ * - Provides sub-millisecond query latency
7
+ * - Ensures identical queries return identical results
8
+ * - Supports semantic caching for similar queries
9
+ *
10
+ * Based on: Doorman11991/budget-aware-mcp (https://github.com/Doorman11991/budget-aware-mcp)
11
+ *
12
+ * Key concepts:
13
+ * - Hop-based graph walk: expand outward from anchor symbols hop by hop
14
+ * - Token budget: retrieval stops when budget is exhausted
15
+ * - Deterministic ordering: alphabetical within each hop level
16
+ * - Semantic cache: similar queries hit cache instantly
17
+ *
18
+ * @module core/memory
19
+ * @fileoverview Budget-aware memory retrieval with token control
20
+ */
21
+ import { createLogger } from '../../utils/logger.js';
22
+ import { cosineSimilarity } from './embedder.js';
23
+ const logger = createLogger('[BudgetRetrieval]');
24
+ /**
25
+ * Trigram-based similarity for semantic caching
26
+ */
27
+ function extractTrigrams(text) {
28
+ const normalized = text.toLowerCase().replace(/[^a-z0-9\s]/g, ' ');
29
+ const words = normalized.split(/\s+/).filter(w => w.length > 0);
30
+ const trigrams = [];
31
+ for (const word of words) {
32
+ if (word.length >= 3) {
33
+ for (let i = 0; i <= word.length - 3; i++) {
34
+ trigrams.push(word.slice(i, i + 3));
35
+ }
36
+ }
37
+ }
38
+ return trigrams;
39
+ }
40
+ /**
41
+ * Jaccard similarity between two trigram sets
42
+ */
43
+ function trigramSimilarity(a, b) {
44
+ if (a.length === 0 || b.length === 0)
45
+ return 0;
46
+ const setA = new Set(a);
47
+ const setB = new Set(b);
48
+ const intersection = new Set([...setA].filter(x => setB.has(x)));
49
+ const union = new Set([...setA, ...setB]);
50
+ return intersection.size / union.size;
51
+ }
52
+ /**
53
+ * Estimate tokens for a memory entry (rough approximation)
54
+ */
55
+ function estimateTokens(entry) {
56
+ // Rough estimate: ~4 characters per token for typical English text
57
+ // Plus overhead for metadata
58
+ const contentTokens = Math.ceil(entry.content.length / 4);
59
+ const metadataTokens = 20; // tags, id, timestamps, etc.
60
+ return contentTokens + metadataTokens;
61
+ }
62
+ /**
63
+ * Create a deterministic hash from query
64
+ */
65
+ function hashQuery(query) {
66
+ const parts = [
67
+ query.query.toLowerCase().trim(),
68
+ query.anchorId || '',
69
+ query.tier || '',
70
+ (query.maxTokens || 0).toString(),
71
+ (query.tags || []).sort().join(','),
72
+ (query.importanceThreshold || 0).toString()
73
+ ];
74
+ // Simple hash function for determinism
75
+ const str = parts.join('|');
76
+ let hash = 0;
77
+ for (let i = 0; i < str.length; i++) {
78
+ const char = str.charCodeAt(i);
79
+ hash = ((hash << 5) - hash) + char;
80
+ hash = hash & hash; // Convert to 32-bit integer
81
+ }
82
+ return Math.abs(hash).toString(36);
83
+ }
84
+ /**
85
+ * Alphabetical sort for deterministic ordering within hop levels
86
+ */
87
+ function deterministicSort(entries) {
88
+ return entries.sort((a, b) => {
89
+ // First by hop level
90
+ if (a.hopLevel !== b.hopLevel) {
91
+ return a.hopLevel - b.hopLevel;
92
+ }
93
+ // Then alphabetically by content
94
+ return a.entry.content.localeCompare(b.entry.content);
95
+ });
96
+ }
97
+ /**
98
+ * Default configuration
99
+ */
100
+ export const DEFAULT_BUDGET_CONFIG = {
101
+ maxTokens: 4000,
102
+ tokensPerEntry: 200,
103
+ maxHops: 3,
104
+ enableCache: true,
105
+ cacheTTLMs: 5 * 60 * 1000, // 5 minutes
106
+ cacheSimilarityThreshold: 0.7,
107
+ enableImportanceFilter: true,
108
+ importanceThreshold: 0.3
109
+ };
110
+ /**
111
+ * Create a budget-aware retrieval engine
112
+ */
113
+ export function createBudgetRetrievalEngine(config = {}, embedder) {
114
+ const fullConfig = { ...DEFAULT_BUDGET_CONFIG, ...config };
115
+ logger.info('Creating BudgetRetrievalEngine', { ...fullConfig });
116
+ // Memory storage
117
+ const entries = new Map();
118
+ const entriesByTier = new Map();
119
+ const entriesByTag = new Map();
120
+ // Embedding index (if embedder provided)
121
+ const entryEmbeddings = new Map();
122
+ // Semantic cache
123
+ const cache = new Map();
124
+ let cacheStats = { hits: 0, misses: 0, hitRate: 0, avgSimilarity: 0 };
125
+ /**
126
+ * Check semantic cache for similar query
127
+ */
128
+ function checkCache(query) {
129
+ if (!fullConfig.enableCache)
130
+ return null;
131
+ const queryTrigrams = extractTrigrams(query.query);
132
+ const queryHash = hashQuery(query);
133
+ // Check exact match first
134
+ const exactEntry = cache.get(queryHash);
135
+ if (exactEntry) {
136
+ const age = Date.now() - exactEntry.timestamp;
137
+ if (age < fullConfig.cacheTTLMs) {
138
+ return exactEntry;
139
+ }
140
+ }
141
+ // Check similar queries
142
+ let bestMatch = null;
143
+ let bestSimilarity = 0;
144
+ for (const entry of cache.values()) {
145
+ const age = Date.now() - entry.timestamp;
146
+ if (age >= fullConfig.cacheTTLMs)
147
+ continue;
148
+ const similarity = trigramSimilarity(queryTrigrams, entry.trigrams);
149
+ if (similarity >= fullConfig.cacheSimilarityThreshold && similarity > bestSimilarity) {
150
+ bestSimilarity = similarity;
151
+ bestMatch = entry;
152
+ }
153
+ }
154
+ return bestMatch;
155
+ }
156
+ /**
157
+ * Store result in cache
158
+ */
159
+ function storeCache(query, result) {
160
+ if (!fullConfig.enableCache)
161
+ return;
162
+ const queryHash = hashQuery(query);
163
+ const trigrams = extractTrigrams(query.query);
164
+ cache.set(queryHash, {
165
+ queryHash,
166
+ queryText: query.query,
167
+ result,
168
+ timestamp: Date.now(),
169
+ trigrams
170
+ });
171
+ // Cleanup old entries
172
+ const now = Date.now();
173
+ for (const [key, entry] of cache.entries()) {
174
+ if (now - entry.timestamp > fullConfig.cacheTTLMs) {
175
+ cache.delete(key);
176
+ }
177
+ }
178
+ }
179
+ /**
180
+ * Calculate relevance score for an entry
181
+ */
182
+ function calculateRelevance(entry, query, anchorEntry) {
183
+ let relevance = 0;
184
+ // Base relevance from importance
185
+ relevance += entry.importance * 0.3;
186
+ // Text similarity if query provided
187
+ if (query.query && embedder) {
188
+ const queryEmb = embedder.embed(query.query);
189
+ const entryEmb = entryEmbeddings.get(entry.id);
190
+ if (queryEmb && entryEmb) {
191
+ const sim = cosineSimilarity(queryEmb, entryEmb);
192
+ relevance += sim * 0.4;
193
+ }
194
+ }
195
+ // Content match
196
+ const queryLower = query.query.toLowerCase();
197
+ const contentLower = entry.content.toLowerCase();
198
+ if (contentLower.includes(queryLower)) {
199
+ relevance += 0.3;
200
+ }
201
+ // Tag match
202
+ if (query.tags && query.tags.length > 0) {
203
+ const entryTags = new Set(entry.tags.map(t => t.toLowerCase()));
204
+ const matchCount = query.tags.filter(t => entryTags.has(t.toLowerCase())).length;
205
+ relevance += (matchCount / query.tags.length) * 0.2;
206
+ }
207
+ // Hop-based relevance from anchor
208
+ if (anchorEntry) {
209
+ // Entries referenced by anchor = hop 1
210
+ const anchor = anchorEntry;
211
+ const ent = entry;
212
+ if (anchor.references?.includes(ent.id)) {
213
+ return relevance + 0.5 - 0.1; // hop 1 bonus
214
+ }
215
+ // Entries referencing anchor = hop 1
216
+ if (ent.references?.includes(anchor.id)) {
217
+ return relevance + 0.5 - 0.1;
218
+ }
219
+ // Same session = hop 2
220
+ if (anchor.sessionId && anchor.sessionId === ent.sessionId) {
221
+ return relevance + 0.4 - 0.2;
222
+ }
223
+ }
224
+ return Math.min(1, relevance);
225
+ }
226
+ /**
227
+ * Perform budget-aware retrieval
228
+ */
229
+ async function retrieve(query) {
230
+ const startMs = Date.now();
231
+ const maxTokens = query.maxTokens || fullConfig.maxTokens;
232
+ logger.debug('Retrieving with budget', { query: query.query, maxTokens });
233
+ // Check cache first
234
+ const cached = checkCache(query);
235
+ if (cached) {
236
+ cacheStats.hits++;
237
+ cacheStats.avgSimilarity = (cacheStats.avgSimilarity * (cacheStats.hits - 1) + (cached.result.cacheSimilarity || 1)) / cacheStats.hits;
238
+ cacheStats.hitRate = cacheStats.hits / (cacheStats.hits + cacheStats.misses);
239
+ logger.debug('Cache hit', { similarity: cached.result.cacheSimilarity });
240
+ return {
241
+ ...cached.result,
242
+ cacheHit: true,
243
+ processingMs: Date.now() - startMs
244
+ };
245
+ }
246
+ cacheStats.misses++;
247
+ cacheStats.hitRate = cacheStats.hits / (cacheStats.hits + cacheStats.misses);
248
+ // Get anchor entry if specified
249
+ const anchorEntry = query.anchorId ? entries.get(query.anchorId) : undefined;
250
+ // Collect candidate entries
251
+ let candidateIds;
252
+ if (query.tier) {
253
+ const tierSet = entriesByTier.get(query.tier);
254
+ candidateIds = tierSet ? Array.from(tierSet) : [];
255
+ }
256
+ else {
257
+ candidateIds = Array.from(entries.keys());
258
+ }
259
+ // Filter by tags if specified
260
+ if (query.tags && query.tags.length > 0) {
261
+ candidateIds = candidateIds.filter(id => {
262
+ const entry = entries.get(id);
263
+ if (!entry)
264
+ return false;
265
+ const entryTags = new Set(entry.tags.map(t => t.toLowerCase()));
266
+ return query.tags.some(t => entryTags.has(t.toLowerCase()));
267
+ });
268
+ }
269
+ // Score and filter candidates
270
+ const scoredCandidates = [];
271
+ for (const id of candidateIds) {
272
+ const entry = entries.get(id);
273
+ if (!entry)
274
+ continue;
275
+ // Importance filter
276
+ if (fullConfig.enableImportanceFilter && entry.importance < fullConfig.importanceThreshold) {
277
+ continue;
278
+ }
279
+ if (query.importanceThreshold && entry.importance < query.importanceThreshold) {
280
+ continue;
281
+ }
282
+ const relevance = calculateRelevance(entry, query, anchorEntry);
283
+ const tokenEstimate = estimateTokens(entry);
284
+ // Determine hop level
285
+ let hopLevel = 0;
286
+ if (anchorEntry) {
287
+ if (id === anchorEntry.id) {
288
+ hopLevel = 0;
289
+ }
290
+ else if ((anchorEntry.references?.includes(id)) || (entry.references?.includes(anchorEntry.id))) {
291
+ hopLevel = 1;
292
+ }
293
+ else {
294
+ hopLevel = 2;
295
+ }
296
+ }
297
+ scoredCandidates.push({
298
+ entry,
299
+ relevance,
300
+ hopLevel,
301
+ tokenEstimate,
302
+ cumulativeTokens: 0 // Will be calculated after sorting
303
+ });
304
+ }
305
+ // Sort deterministically
306
+ const sorted = deterministicSort(scoredCandidates);
307
+ // Build budget-constrained result
308
+ const budget = {
309
+ maxTokens,
310
+ usedTokens: 0,
311
+ remainingTokens: maxTokens,
312
+ entriesSelected: 0
313
+ };
314
+ const selected = [];
315
+ for (const candidate of sorted) {
316
+ if (budget.remainingTokens < candidate.tokenEstimate) {
317
+ continue; // Can't afford this entry
318
+ }
319
+ if (candidate.hopLevel > fullConfig.maxHops) {
320
+ continue; // Exceeded max hops
321
+ }
322
+ candidate.cumulativeTokens = budget.usedTokens + candidate.tokenEstimate;
323
+ budget.usedTokens = candidate.cumulativeTokens;
324
+ budget.remainingTokens = maxTokens - budget.usedTokens;
325
+ budget.entriesSelected++;
326
+ selected.push(candidate);
327
+ }
328
+ const result = {
329
+ entries: selected,
330
+ tokensUsed: budget.usedTokens,
331
+ budgetRemaining: budget.remainingTokens,
332
+ cacheHit: false,
333
+ queryHash: hashQuery(query),
334
+ processingMs: Date.now() - startMs,
335
+ totalAvailable: candidateIds.length
336
+ };
337
+ // Store in cache
338
+ storeCache(query, result);
339
+ logger.debug('Retrieval complete', {
340
+ tokensUsed: result.tokensUsed,
341
+ entriesSelected: result.entries.length,
342
+ processingMs: result.processingMs,
343
+ cacheHit: result.cacheHit
344
+ });
345
+ return result;
346
+ }
347
+ /**
348
+ * Index a memory entry
349
+ */
350
+ function index(entry) {
351
+ entries.set(entry.id, entry);
352
+ // Index by tier
353
+ if (!entriesByTier.has(entry.tier)) {
354
+ entriesByTier.set(entry.tier, new Set());
355
+ }
356
+ entriesByTier.get(entry.tier).add(entry.id);
357
+ // Index by tags
358
+ for (const tag of entry.tags) {
359
+ if (!entriesByTag.has(tag)) {
360
+ entriesByTag.set(tag, new Set());
361
+ }
362
+ entriesByTag.get(tag).add(entry.id);
363
+ }
364
+ // Embed for semantic search
365
+ if (embedder) {
366
+ entryEmbeddings.set(entry.id, embedder.embed(entry.content));
367
+ }
368
+ }
369
+ /**
370
+ * Remove entry from index
371
+ */
372
+ function remove(id) {
373
+ const entry = entries.get(id);
374
+ if (!entry)
375
+ return;
376
+ entries.delete(id);
377
+ // Remove from tier index
378
+ entriesByTier.get(entry.tier)?.delete(id);
379
+ // Remove from tag indices
380
+ for (const tag of entry.tags) {
381
+ entriesByTag.get(tag)?.delete(id);
382
+ }
383
+ // Remove embedding
384
+ entryEmbeddings.delete(id);
385
+ }
386
+ /**
387
+ * Clear all entries
388
+ */
389
+ function clear() {
390
+ entries.clear();
391
+ entriesByTier.clear();
392
+ entriesByTag.clear();
393
+ entryEmbeddings.clear();
394
+ cache.clear();
395
+ logger.info('Budget retrieval engine cleared');
396
+ }
397
+ /**
398
+ * Get cache statistics
399
+ */
400
+ function getCacheStats() {
401
+ return { ...cacheStats };
402
+ }
403
+ /**
404
+ * Update configuration
405
+ */
406
+ function updateConfig(config) {
407
+ Object.assign(fullConfig, config);
408
+ logger.debug('Config updated', config);
409
+ }
410
+ return {
411
+ retrieve,
412
+ index,
413
+ remove,
414
+ clear,
415
+ getCacheStats,
416
+ updateConfig
417
+ };
418
+ }
419
+ // Export utilities for external use
420
+ export { extractTrigrams, trigramSimilarity, estimateTokens, hashQuery };
@@ -12,3 +12,4 @@ export { createHybridSearchEngine, createBM25Index, bm25Score, normalizeBM25Scor
12
12
  export { createPatternRecognizer } from './pattern-recognizer.js';
13
13
  export { createMemoryObserver } from './observer.js';
14
14
  export { createBinaryVectorCompressor, quantizeToBinary, hammingDistance, hammingToSimilarity, createBinaryIndex, addToBinaryIndex, searchBinaryIndex, getCompressionStats, serializeBinaryIndex, deserializeBinaryIndex, DEFAULT_BINARY_CONFIG, EMBEDDING_DIMENSIONS } from './binary-vector.js';
15
+ export { createBudgetRetrievalEngine, DEFAULT_BUDGET_CONFIG, extractTrigrams, trigramSimilarity, estimateTokens, hashQuery } from './budget-retrieval.js';
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '2.3.4';
1
+ export const VERSION = '2.3.5';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mark-improving-agent",
3
- "version": "2.3.4",
3
+ "version": "2.3.5",
4
4
  "description": "Self-evolving AI agent with permanent memory, identity continuity, and self-evolution — for AI agents that need to remember, learn, and evolve across sessions",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",