@agenticmail/enterprise 0.5.94 → 0.5.97

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.
@@ -1,8 +1,18 @@
1
1
  /**
2
- * AgenticMail Agent Tools — Memory
2
+ * AgenticMail Agent Tools — Memory (DB-backed)
3
3
  *
4
- * Persistent memory/notes system for agents with size limits,
5
- * atomic writes, and entry count protection.
4
+ * Persistent, evolving memory system for agents. Uses the enterprise
5
+ * AgentMemoryManager (Postgres-backed) when available, with file-based
6
+ * fallback for local/dev environments.
7
+ *
8
+ * Designed so agents can build expertise over time — like a human
9
+ * employee who learns from every interaction, correction, and reflection.
10
+ *
11
+ * Tools:
12
+ * memory — CRUD: set/get/search/list/delete key-value memories
13
+ * memory_reflect — Record a self-reflection or lesson learned
14
+ * memory_context — Get relevant memories for a topic (for prompt injection)
15
+ * memory_stats — View memory statistics and health
6
16
  */
7
17
 
8
18
  import fs from 'node:fs/promises';
@@ -11,13 +21,16 @@ import crypto from 'node:crypto';
11
21
  import type { AnyAgentTool, ToolCreationOptions } from '../types.js';
12
22
  import { readStringParam, readNumberParam, jsonResult, textResult, errorResult } from '../common.js';
13
23
  import { MemorySearchIndex } from '../../lib/text-search.js';
24
+ import type { AgentMemoryManager, MemoryCategory, MemoryImportance, MemorySource, AgentMemoryEntry } from '../../engine/agent-memory.js';
25
+
26
+ // ── Types ──
14
27
 
15
28
  const MEMORY_ACTIONS = ['set', 'get', 'search', 'list', 'delete'] as const;
16
29
  type MemoryAction = (typeof MEMORY_ACTIONS)[number];
17
30
 
18
- const DEFAULT_MAX_ENTRIES = 1000;
19
- const DEFAULT_MAX_VALUE_SIZE = 100 * 1024; // 100KB per entry
20
- const DEFAULT_MAX_STORE_SIZE = 10 * 1024 * 1024; // 10MB total
31
+ const DEFAULT_MAX_ENTRIES = 2000;
32
+ const DEFAULT_MAX_VALUE_SIZE = 100 * 1024;
33
+ const DEFAULT_MAX_STORE_SIZE = 10 * 1024 * 1024;
21
34
 
22
35
  type MemoryEntry = {
23
36
  key: string;
@@ -27,94 +40,124 @@ type MemoryEntry = {
27
40
  updatedAt: string;
28
41
  };
29
42
 
30
- type MemoryStore = {
31
- entries: Record<string, MemoryEntry>;
32
- };
43
+ type MemoryStore = { entries: Record<string, MemoryEntry> };
44
+
45
+ // ── File-based fallback (for local/dev) ──
33
46
 
34
47
  async function loadMemoryStore(storePath: string): Promise<MemoryStore> {
35
48
  try {
36
49
  var content = await fs.readFile(storePath, 'utf-8');
37
50
  return JSON.parse(content) as MemoryStore;
38
- } catch {
39
- return { entries: {} };
40
- }
51
+ } catch { return { entries: {} }; }
41
52
  }
42
53
 
43
54
  async function saveMemoryStore(storePath: string, store: MemoryStore): Promise<void> {
44
55
  var dir = path.dirname(storePath);
45
56
  await fs.mkdir(dir, { recursive: true });
46
57
  var data = JSON.stringify(store, null, 2);
47
-
48
- // Check total store size
49
58
  var storeSize = Buffer.byteLength(data, 'utf-8');
50
59
  if (storeSize > DEFAULT_MAX_STORE_SIZE) {
51
60
  throw new Error('Memory store exceeds maximum size (' + Math.round(storeSize / 1024 / 1024) + 'MB). Delete some entries first.');
52
61
  }
53
-
54
- // Atomic write: write to temp file then rename
55
62
  var tmpPath = storePath + '.tmp.' + crypto.randomBytes(4).toString('hex');
56
63
  try {
57
64
  await fs.writeFile(tmpPath, data, 'utf-8');
58
65
  await fs.rename(tmpPath, storePath);
59
66
  } catch (err) {
60
- // Cleanup temp file on failure
61
67
  try { await fs.unlink(tmpPath); } catch { /* ignore */ }
62
68
  throw err;
63
69
  }
64
70
  }
65
71
 
66
- // ── Per-store BM25 search index (rebuilt on load, updated incrementally) ──
67
-
68
72
  var searchIndexCache = new Map<string, MemorySearchIndex>();
69
73
 
70
- function buildSearchIndex(storePath: string, entries: Record<string, MemoryEntry>): MemorySearchIndex {
71
- var index = new MemorySearchIndex();
72
- for (var entry of Object.values(entries)) {
73
- index.addDocument(entry.key, { title: entry.key, content: entry.value, tags: entry.tags });
74
- }
75
- searchIndexCache.set(storePath, index);
76
- return index;
77
- }
78
-
79
74
  function getSearchIndex(storePath: string, entries: Record<string, MemoryEntry>): MemorySearchIndex {
80
75
  var cached = searchIndexCache.get(storePath);
81
- // Rebuild if missing or entry count drifted (another process wrote the file)
82
76
  if (!cached || cached.docCount !== Object.keys(entries).length) {
83
- return buildSearchIndex(storePath, entries);
77
+ var index = new MemorySearchIndex();
78
+ for (var entry of Object.values(entries)) {
79
+ index.addDocument(entry.key, { title: entry.key, content: entry.value, tags: entry.tags });
80
+ }
81
+ searchIndexCache.set(storePath, index);
82
+ return index;
84
83
  }
85
84
  return cached;
86
85
  }
87
86
 
88
- function searchEntries(
89
- storePath: string,
90
- entries: Record<string, MemoryEntry>,
91
- query: string,
92
- limit: number,
93
- ): MemoryEntry[] {
94
- var index = getSearchIndex(storePath, entries);
95
- var results = index.search(query);
96
- var out: MemoryEntry[] = [];
97
- for (var i = 0; i < Math.min(results.length, limit); i++) {
98
- var entry = entries[results[i].id];
99
- if (entry) out.push(entry);
87
+ // ── Category inference ──
88
+
89
+ const CATEGORY_KEYWORDS: Record<MemoryCategory, string[]> = {
90
+ org_knowledge: ['policy', 'procedure', 'rule', 'guideline', 'standard', 'protocol', 'compliance', 'regulation'],
91
+ interaction_pattern: ['user prefers', 'they like', 'communication style', 'when asked', 'pattern', 'usually', 'tends to'],
92
+ preference: ['prefer', 'favorite', 'always use', 'default', 'likes', 'dislikes', 'avoid'],
93
+ correction: ['wrong', 'mistake', 'corrected', 'actually', 'not that', 'should have', 'fix', 'error', 'learned that'],
94
+ skill: ['how to', 'technique', 'method', 'approach', 'workflow', 'process', 'tool', 'api'],
95
+ context: ['background', 'history', 'context', 'situation', 'project', 'team', 'department'],
96
+ reflection: ['realized', 'insight', 'lesson', 'takeaway', 'going forward', 'next time', 'reflection', 'learned'],
97
+ };
98
+
99
+ function inferCategory(title: string, content: string): MemoryCategory {
100
+ var text = (title + ' ' + content).toLowerCase();
101
+ var bestCategory: MemoryCategory = 'context';
102
+ var bestScore = 0;
103
+ for (var [cat, keywords] of Object.entries(CATEGORY_KEYWORDS)) {
104
+ var score = 0;
105
+ for (var kw of keywords) {
106
+ if (text.includes(kw)) score++;
107
+ }
108
+ if (score > bestScore) {
109
+ bestScore = score;
110
+ bestCategory = cat as MemoryCategory;
111
+ }
100
112
  }
101
- return out;
113
+ return bestCategory;
102
114
  }
103
115
 
104
- export function createMemoryTool(options?: ToolCreationOptions): AnyAgentTool | null {
116
+ function inferImportance(content: string, category: MemoryCategory): MemoryImportance {
117
+ if (category === 'correction') return 'high'; // corrections are always important — don't repeat mistakes
118
+ if (category === 'org_knowledge') return 'high';
119
+ var text = content.toLowerCase();
120
+ if (text.includes('critical') || text.includes('never') || text.includes('always') || text.includes('must')) return 'high';
121
+ if (text.includes('important') || text.includes('remember') || text.includes('key')) return 'normal';
122
+ return 'normal';
123
+ }
124
+
125
+ // ── Options interface ──
126
+
127
+ export interface MemoryToolOptions extends ToolCreationOptions {
128
+ /** DB-backed memory manager (enterprise) */
129
+ agentMemoryManager?: AgentMemoryManager;
130
+ /** Agent ID for DB-backed memory */
131
+ agentId?: string;
132
+ /** Org ID for DB-backed memory */
133
+ orgId?: string;
134
+ }
135
+
136
+ // ── Main memory tool ──
137
+
138
+ export function createMemoryTools(options?: MemoryToolOptions): AnyAgentTool[] {
105
139
  var memoryConfig = options?.config?.memory;
106
- if (memoryConfig?.enabled === false) return null;
140
+ if (memoryConfig?.enabled === false) return [];
107
141
 
142
+ var mgr = options?.agentMemoryManager;
143
+ var agentId = options?.agentId || 'default';
144
+ var orgId = options?.orgId || 'default';
145
+ var useDb = !!mgr;
146
+
147
+ // File-based fallback path
108
148
  var storePath = path.join(
109
149
  options?.workspaceDir || process.cwd(),
110
150
  '.agenticmail',
111
151
  'agent-memory.json',
112
152
  );
113
153
 
114
- return {
154
+ var tools: AnyAgentTool[] = [];
155
+
156
+ // ─── memory (CRUD) ───
157
+ tools.push({
115
158
  name: 'memory',
116
159
  label: 'Memory',
117
- description: 'Persistent memory for storing and retrieving notes, facts, and context across conversations. Supports set, get, search, list, and delete operations.',
160
+ description: 'Persistent memory for storing and retrieving knowledge across conversations. Use this to remember facts, preferences, lessons, corrections, and insights. Memories survive restarts and deployments.\n\nActions:\n- set: Store a memory (key + value + optional tags + optional category + optional importance)\n- get: Retrieve a memory by key\n- search: Full-text search across all memories (BM25F ranking)\n- list: List all memories (with optional category/importance filter)\n- delete: Remove a memory',
118
161
  category: 'memory',
119
162
  risk: 'low',
120
163
  parameters: {
@@ -125,17 +168,155 @@ export function createMemoryTool(options?: ToolCreationOptions): AnyAgentTool |
125
168
  description: 'Action: set, get, search, list, or delete.',
126
169
  enum: MEMORY_ACTIONS as unknown as string[],
127
170
  },
128
- key: { type: 'string', description: 'Memory key (for set/get/delete).' },
129
- value: { type: 'string', description: 'Value to store (for set).' },
130
- tags: { type: 'string', description: 'Comma-separated tags (for set).' },
131
- query: { type: 'string', description: 'Search query (for search).' },
132
- limit: { type: 'number', description: 'Max results for search/list.' },
171
+ key: { type: 'string', description: 'Memory key/title (for set/get/delete). Use descriptive keys like "user-prefers-concise-responses" or "api-endpoint-for-billing".' },
172
+ value: { type: 'string', description: 'Content to store (for set). Be detailed — future you needs to understand this without context.' },
173
+ tags: { type: 'string', description: 'Comma-separated tags (for set). E.g. "user-preference,communication"' },
174
+ category: { type: 'string', description: 'Category (for set/list filter). One of: org_knowledge, interaction_pattern, preference, correction, skill, context, reflection. Auto-inferred if omitted.', enum: ['org_knowledge', 'interaction_pattern', 'preference', 'correction', 'skill', 'context', 'reflection'] },
175
+ importance: { type: 'string', description: 'Importance (for set/list filter). One of: critical, high, normal, low. Auto-inferred if omitted.', enum: ['critical', 'high', 'normal', 'low'] },
176
+ query: { type: 'string', description: 'Search query (for search). Natural language works well.' },
177
+ limit: { type: 'number', description: 'Max results for search/list (default: 20).' },
133
178
  },
134
179
  required: ['action'],
135
180
  },
136
181
  execute: async function(_toolCallId, args) {
137
182
  var params = args as Record<string, unknown>;
138
183
  var action = readStringParam(params, 'action', { required: true }) as MemoryAction;
184
+
185
+ // ── DB-backed path ──
186
+ if (useDb) {
187
+ switch (action) {
188
+ case 'set': {
189
+ var key = readStringParam(params, 'key', { required: true });
190
+ var value = readStringParam(params, 'value', { required: true, trim: false });
191
+ var tagsRaw = readStringParam(params, 'tags') || '';
192
+ var tags = tagsRaw.split(',').map(t => t.trim()).filter(Boolean);
193
+ var category = (readStringParam(params, 'category') || inferCategory(key, value)) as MemoryCategory;
194
+ var importance = (readStringParam(params, 'importance') || inferImportance(value, category)) as MemoryImportance;
195
+
196
+ // Check if entry with same title exists (update instead of duplicate)
197
+ var existing = await mgr!.queryMemories({ agentId, category, query: key, limit: 5 });
198
+ var exactMatch = existing.find(e => e.title === key);
199
+
200
+ if (exactMatch) {
201
+ await mgr!.updateMemory(exactMatch.id, {
202
+ content: value,
203
+ tags,
204
+ category,
205
+ importance,
206
+ confidence: Math.min(1.0, exactMatch.confidence + 0.1), // boost confidence on update
207
+ });
208
+ return textResult('Updated memory: ' + key + ' [' + category + '/' + importance + '] (confidence: ' + Math.min(1.0, exactMatch.confidence + 0.1).toFixed(2) + ')');
209
+ }
210
+
211
+ await mgr!.createMemory({
212
+ agentId,
213
+ orgId,
214
+ category,
215
+ title: key,
216
+ content: value,
217
+ source: 'interaction' as MemorySource,
218
+ importance,
219
+ confidence: 0.8,
220
+ tags,
221
+ metadata: {},
222
+ });
223
+ return textResult('Stored memory: ' + key + ' [' + category + '/' + importance + ']');
224
+ }
225
+
226
+ case 'get': {
227
+ var key = readStringParam(params, 'key', { required: true });
228
+ var results = await mgr!.queryMemories({ agentId, query: key, limit: 5 });
229
+ var match = results.find(e => e.title === key) || results[0];
230
+ if (!match) return textResult('Memory not found: ' + key);
231
+ await mgr!.recordAccess(match.id);
232
+ return jsonResult({
233
+ key: match.title,
234
+ value: match.content,
235
+ category: match.category,
236
+ importance: match.importance,
237
+ confidence: match.confidence,
238
+ tags: match.tags,
239
+ accessCount: match.accessCount + 1,
240
+ createdAt: match.createdAt,
241
+ updatedAt: match.updatedAt,
242
+ });
243
+ }
244
+
245
+ case 'search': {
246
+ var query = readStringParam(params, 'query', { required: true });
247
+ var limit = readNumberParam(params, 'limit', { integer: true }) ?? 20;
248
+ var categoryFilter = readStringParam(params, 'category');
249
+ var importanceFilter = readStringParam(params, 'importance');
250
+ var results = await mgr!.queryMemories({
251
+ agentId,
252
+ query,
253
+ limit,
254
+ category: categoryFilter || undefined,
255
+ importance: importanceFilter || undefined,
256
+ });
257
+ if (results.length === 0) return textResult('No memories matching: ' + query);
258
+ // Record access for top results
259
+ for (var r of results.slice(0, 3)) { await mgr!.recordAccess(r.id); }
260
+ return jsonResult({
261
+ count: results.length,
262
+ results: results.map(e => ({
263
+ key: e.title,
264
+ value: e.content,
265
+ category: e.category,
266
+ importance: e.importance,
267
+ confidence: e.confidence,
268
+ tags: e.tags,
269
+ accessCount: e.accessCount,
270
+ updatedAt: e.updatedAt,
271
+ })),
272
+ });
273
+ }
274
+
275
+ case 'list': {
276
+ var limit = readNumberParam(params, 'limit', { integer: true }) ?? 20;
277
+ var categoryFilter = readStringParam(params, 'category');
278
+ var importanceFilter = readStringParam(params, 'importance');
279
+ var results = await mgr!.queryMemories({
280
+ agentId,
281
+ limit,
282
+ category: categoryFilter || undefined,
283
+ importance: importanceFilter || undefined,
284
+ });
285
+ var stats = await mgr!.getStats(agentId);
286
+ return jsonResult({
287
+ totalMemories: stats.totalEntries,
288
+ showing: results.length,
289
+ avgConfidence: stats.avgConfidence,
290
+ byCategory: stats.byCategory,
291
+ byImportance: stats.byImportance,
292
+ entries: results.map(e => ({
293
+ key: e.title,
294
+ category: e.category,
295
+ importance: e.importance,
296
+ confidence: e.confidence,
297
+ tags: e.tags,
298
+ accessCount: e.accessCount,
299
+ updatedAt: e.updatedAt,
300
+ preview: e.content.length > 120 ? e.content.slice(0, 120) + '...' : e.content,
301
+ })),
302
+ });
303
+ }
304
+
305
+ case 'delete': {
306
+ var key = readStringParam(params, 'key', { required: true });
307
+ var results = await mgr!.queryMemories({ agentId, query: key, limit: 5 });
308
+ var match = results.find(e => e.title === key);
309
+ if (!match) return textResult('Memory not found: ' + key);
310
+ await mgr!.deleteMemory(match.id);
311
+ return textResult('Deleted memory: ' + key);
312
+ }
313
+
314
+ default:
315
+ return errorResult('Unknown memory action: ' + action);
316
+ }
317
+ }
318
+
319
+ // ── File-based fallback ──
139
320
  var store = await loadMemoryStore(storePath);
140
321
 
141
322
  switch (action) {
@@ -143,79 +324,184 @@ export function createMemoryTool(options?: ToolCreationOptions): AnyAgentTool |
143
324
  var key = readStringParam(params, 'key', { required: true });
144
325
  var value = readStringParam(params, 'value', { required: true, trim: false });
145
326
  var tagsRaw = readStringParam(params, 'tags') || '';
146
- var tags = tagsRaw.split(',').map(function(t) { return t.trim(); }).filter(Boolean);
327
+ var tags = tagsRaw.split(',').map(t => t.trim()).filter(Boolean);
147
328
  var now = new Date().toISOString();
148
-
149
- // Value size limit
150
329
  var valueSize = Buffer.byteLength(value, 'utf-8');
151
- if (valueSize > DEFAULT_MAX_VALUE_SIZE) {
152
- return errorResult('Value too large: ' + Math.round(valueSize / 1024) + 'KB. Maximum is 100KB per entry.');
153
- }
154
-
155
- // Entry count limit (only for new entries)
330
+ if (valueSize > DEFAULT_MAX_VALUE_SIZE) return errorResult('Value too large: ' + Math.round(valueSize / 1024) + 'KB. Maximum is 100KB per entry.');
156
331
  var existing = store.entries[key];
157
- if (!existing && Object.keys(store.entries).length >= DEFAULT_MAX_ENTRIES) {
158
- return errorResult('Memory store full: ' + DEFAULT_MAX_ENTRIES + ' entries maximum. Delete some entries first.');
159
- }
160
-
161
- store.entries[key] = {
162
- key,
163
- value,
164
- tags,
165
- createdAt: existing?.createdAt || now,
166
- updatedAt: now,
167
- };
332
+ if (!existing && Object.keys(store.entries).length >= DEFAULT_MAX_ENTRIES) return errorResult('Memory store full: ' + DEFAULT_MAX_ENTRIES + ' entries maximum.');
333
+ store.entries[key] = { key, value, tags, createdAt: existing?.createdAt || now, updatedAt: now };
168
334
  await saveMemoryStore(storePath, store);
169
-
170
- // Keep BM25 index in sync
171
335
  var idx = getSearchIndex(storePath, store.entries);
172
- idx.addDocument(key, { title: key, content: value, tags: tags });
173
-
336
+ idx.addDocument(key, { title: key, content: value, tags });
174
337
  return textResult('Stored memory: ' + key);
175
338
  }
176
-
177
339
  case 'get': {
178
340
  var key = readStringParam(params, 'key', { required: true });
179
341
  var entry = store.entries[key];
180
342
  if (!entry) return textResult('Memory not found: ' + key);
181
343
  return jsonResult(entry);
182
344
  }
183
-
184
345
  case 'search': {
185
346
  var query = readStringParam(params, 'query', { required: true });
186
347
  var limit = readNumberParam(params, 'limit', { integer: true }) ?? 10;
187
- var results = searchEntries(storePath, store.entries, query, limit);
188
- if (results.length === 0) return textResult('No memories matching: ' + query);
189
- return jsonResult({ count: results.length, results });
348
+ var index = getSearchIndex(storePath, store.entries);
349
+ var results = index.search(query);
350
+ var out: MemoryEntry[] = [];
351
+ for (var i = 0; i < Math.min(results.length, limit); i++) {
352
+ var entry = store.entries[results[i].id];
353
+ if (entry) out.push(entry);
354
+ }
355
+ if (out.length === 0) return textResult('No memories matching: ' + query);
356
+ return jsonResult({ count: out.length, results: out });
190
357
  }
191
-
192
358
  case 'list': {
193
359
  var limit = readNumberParam(params, 'limit', { integer: true }) ?? 20;
194
360
  var keys = Object.keys(store.entries);
195
361
  var limited = keys.slice(0, limit);
196
- var entries = limited.map(function(k) {
197
- var e = store.entries[k];
198
- return { key: e.key, tags: e.tags, updatedAt: e.updatedAt };
199
- });
362
+ var entries = limited.map(k => { var e = store.entries[k]; return { key: e.key, tags: e.tags, updatedAt: e.updatedAt }; });
200
363
  return jsonResult({ count: keys.length, showing: limited.length, entries });
201
364
  }
202
-
203
365
  case 'delete': {
204
366
  var key = readStringParam(params, 'key', { required: true });
205
367
  if (!store.entries[key]) return textResult('Memory not found: ' + key);
206
368
  delete store.entries[key];
207
369
  await saveMemoryStore(storePath, store);
208
-
209
- // Keep BM25 index in sync
210
370
  var idx = searchIndexCache.get(storePath);
211
371
  if (idx) idx.removeDocument(key);
212
-
213
372
  return textResult('Deleted memory: ' + key);
214
373
  }
215
-
216
374
  default:
217
375
  return errorResult('Unknown memory action: ' + action);
218
376
  }
219
377
  },
220
- };
378
+ });
379
+
380
+ // ─── memory_reflect (self-reflection tool) ───
381
+ tools.push({
382
+ name: 'memory_reflect',
383
+ label: 'Self-Reflect',
384
+ description: 'Record a self-reflection, lesson learned, or insight from the current interaction. Use this after completing a task, receiving feedback, making a mistake, or discovering something useful. These reflections compound over time — they are how you grow and become an expert.\n\nExamples:\n- After a correction: "User prefers bullet points over paragraphs for reports"\n- After learning: "The billing API requires ISO 8601 dates, not Unix timestamps"\n- After a mistake: "Always confirm before sending emails to external addresses"\n- After success: "Breaking complex tasks into subtasks with status updates works well for this user"',
385
+ category: 'memory',
386
+ risk: 'low',
387
+ parameters: {
388
+ type: 'object',
389
+ properties: {
390
+ insight: { type: 'string', description: 'What you learned or realized. Be specific and actionable — future you needs to apply this.' },
391
+ category: { type: 'string', description: 'Type of insight.', enum: ['correction', 'skill', 'preference', 'interaction_pattern', 'reflection'] },
392
+ importance: { type: 'string', description: 'How important is this?', enum: ['critical', 'high', 'normal', 'low'] },
393
+ trigger: { type: 'string', description: 'What prompted this reflection? (optional — helps with future recall)' },
394
+ },
395
+ required: ['insight'],
396
+ },
397
+ execute: async function(_toolCallId, args) {
398
+ var params = args as Record<string, unknown>;
399
+ var insight = readStringParam(params, 'insight', { required: true });
400
+ var category = (readStringParam(params, 'category') || inferCategory('reflection', insight)) as MemoryCategory;
401
+ var importance = (readStringParam(params, 'importance') || inferImportance(insight, category)) as MemoryImportance;
402
+ var trigger = readStringParam(params, 'trigger') || '';
403
+
404
+ // Generate a descriptive title from the insight
405
+ var title = insight.length > 80 ? insight.slice(0, 77) + '...' : insight;
406
+ var content = insight;
407
+ if (trigger) content += '\n\nTrigger: ' + trigger;
408
+
409
+ if (useDb) {
410
+ await mgr!.createMemory({
411
+ agentId,
412
+ orgId,
413
+ category,
414
+ title,
415
+ content,
416
+ source: 'self_reflection' as MemorySource,
417
+ importance,
418
+ confidence: 0.9, // reflections start high — agent chose to record this
419
+ tags: ['reflection', category],
420
+ metadata: { trigger: trigger || undefined },
421
+ });
422
+ } else {
423
+ // File-based fallback
424
+ var store = await loadMemoryStore(storePath);
425
+ var now = new Date().toISOString();
426
+ var key = 'reflection-' + now.slice(0, 10) + '-' + crypto.randomBytes(3).toString('hex');
427
+ store.entries[key] = { key, value: content, tags: ['reflection', category], createdAt: now, updatedAt: now };
428
+ await saveMemoryStore(storePath, store);
429
+ }
430
+
431
+ return textResult('Reflection recorded [' + category + '/' + importance + ']: ' + title);
432
+ },
433
+ });
434
+
435
+ // ─── memory_context (get relevant context for a topic) ───
436
+ if (useDb) {
437
+ tools.push({
438
+ name: 'memory_context',
439
+ label: 'Memory Context',
440
+ description: 'Retrieve relevant memories for a given topic or task. Returns a curated, ranked summary of your most relevant knowledge — corrections, preferences, skills, and context. Use this at the start of complex tasks to recall what you know.\n\nThis is your "expertise retrieval" — the accumulated knowledge that makes you better at your job over time.',
441
+ category: 'memory',
442
+ risk: 'low',
443
+ parameters: {
444
+ type: 'object',
445
+ properties: {
446
+ topic: { type: 'string', description: 'What topic or task do you need context for? Natural language works best.' },
447
+ maxTokens: { type: 'number', description: 'Maximum token budget for context (default: 1500). Higher = more complete but uses more prompt space.' },
448
+ },
449
+ required: ['topic'],
450
+ },
451
+ execute: async function(_toolCallId, args) {
452
+ var params = args as Record<string, unknown>;
453
+ var topic = readStringParam(params, 'topic', { required: true });
454
+ var maxTokens = readNumberParam(params, 'maxTokens', { integer: true }) ?? 1500;
455
+
456
+ var context = await mgr!.generateMemoryContext(agentId, topic, maxTokens);
457
+ if (!context) return textResult('No relevant memories found for: ' + topic);
458
+ return textResult(context);
459
+ },
460
+ });
461
+ }
462
+
463
+ // ─── memory_stats ───
464
+ if (useDb) {
465
+ tools.push({
466
+ name: 'memory_stats',
467
+ label: 'Memory Stats',
468
+ description: 'View statistics about your memory: total entries, category breakdown, average confidence, and health metrics. Use this to understand what you know and identify gaps.',
469
+ category: 'memory',
470
+ risk: 'low',
471
+ parameters: {
472
+ type: 'object',
473
+ properties: {},
474
+ required: [],
475
+ },
476
+ execute: async function() {
477
+ var stats = await mgr!.getStats(agentId);
478
+ var recent = await mgr!.getRecentMemories(agentId, 24);
479
+ return jsonResult({
480
+ ...stats,
481
+ recentEntries24h: recent.length,
482
+ recentTopics: recent.slice(0, 5).map(e => e.title),
483
+ healthTips: generateHealthTips(stats),
484
+ });
485
+ },
486
+ });
487
+ }
488
+
489
+ return tools;
490
+ }
491
+
492
+ function generateHealthTips(stats: { totalEntries: number; byCategory: Record<string, number>; avgConfidence: number }): string[] {
493
+ var tips: string[] = [];
494
+ if (stats.totalEntries === 0) tips.push('Your memory is empty! Start recording interactions, preferences, and lessons learned.');
495
+ if (stats.totalEntries > 0 && !stats.byCategory['correction']) tips.push('No corrections recorded. When you make mistakes, use memory_reflect to learn from them.');
496
+ if (stats.totalEntries > 0 && !stats.byCategory['preference']) tips.push('No preferences recorded. Notice how users like things done and record those patterns.');
497
+ if (stats.totalEntries > 0 && !stats.byCategory['skill']) tips.push('No skills recorded. When you learn how to do something new, document the technique.');
498
+ if (stats.avgConfidence < 0.5 && stats.totalEntries > 10) tips.push('Average confidence is low (' + stats.avgConfidence.toFixed(2) + '). Review and reinforce your memories by accessing them.');
499
+ if (stats.totalEntries > 500) tips.push('Large memory (' + stats.totalEntries + ' entries). Consider reviewing and pruning outdated entries.');
500
+ return tips;
501
+ }
502
+
503
+ /** Legacy single-tool export for backward compatibility */
504
+ export function createMemoryTool(options?: ToolCreationOptions): AnyAgentTool | null {
505
+ var tools = createMemoryTools(options as MemoryToolOptions);
506
+ return tools.length > 0 ? tools[0] : null;
221
507
  }