@agenticmail/enterprise 0.5.95 → 0.5.98
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/dist/chunk-7AXTPSIN.js +16106 -0
- package/dist/chunk-CDBIJ42S.js +898 -0
- package/dist/chunk-CWAFR2JH.js +2191 -0
- package/dist/chunk-F7HD443H.js +898 -0
- package/dist/chunk-FJPRGEUN.js +16106 -0
- package/dist/chunk-HBZS3OKV.js +9255 -0
- package/dist/chunk-NWL4TLF4.js +2194 -0
- package/dist/chunk-PC7IMFOA.js +2194 -0
- package/dist/chunk-QC2XKXAY.js +2391 -0
- package/dist/chunk-TIV6GMQ5.js +898 -0
- package/dist/cli.js +1 -1
- package/dist/db-adapter-65QMSGCB.js +7 -0
- package/dist/index.js +5 -5
- package/dist/routes-XTQKNAQS.js +6938 -0
- package/dist/runtime-IEUW4STA.js +49 -0
- package/dist/runtime-Y7S2UQHD.js +49 -0
- package/dist/server-4DJAKGYA.js +12 -0
- package/dist/server-C7K5DKUM.js +12 -0
- package/dist/server-UHM4CFFF.js +12 -0
- package/dist/setup-CFXJICEP.js +20 -0
- package/dist/setup-CRY7S2BG.js +20 -0
- package/dist/setup-QLGEBVYU.js +20 -0
- package/package.json +1 -1
- package/src/agent-tools/index.ts +20 -2
- package/src/agent-tools/tools/memory.ts +377 -91
- package/src/engine/db-adapter.ts +1 -1
- package/src/engine/deployer.ts +186 -61
- package/src/runtime/index.ts +31 -5
- package/src/runtime/types.ts +2 -0
- package/src/server.ts +4 -1
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AgenticMail Agent Tools — Memory
|
|
2
|
+
* AgenticMail Agent Tools — Memory (DB-backed)
|
|
3
3
|
*
|
|
4
|
-
* Persistent memory
|
|
5
|
-
*
|
|
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 =
|
|
19
|
-
const DEFAULT_MAX_VALUE_SIZE = 100 * 1024;
|
|
20
|
-
const DEFAULT_MAX_STORE_SIZE = 10 * 1024 * 1024;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
113
|
+
return bestCategory;
|
|
102
114
|
}
|
|
103
115
|
|
|
104
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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: '
|
|
130
|
-
tags: { type: 'string', description: 'Comma-separated tags (for set).' },
|
|
131
|
-
|
|
132
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
188
|
-
|
|
189
|
-
|
|
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(
|
|
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
|
}
|
package/src/engine/db-adapter.ts
CHANGED
|
@@ -307,7 +307,7 @@ export class EngineDatabase {
|
|
|
307
307
|
async upsertPermissionProfile(orgId: string, profile: AgentPermissionProfile): Promise<void> {
|
|
308
308
|
await this.db.run(`
|
|
309
309
|
INSERT INTO permission_profiles (id, org_id, name, description, config, is_preset, created_at, updated_at)
|
|
310
|
-
VALUES (?, ?, ?, ?, ?,
|
|
310
|
+
VALUES (?, ?, ?, ?, ?, false, ?, ?)
|
|
311
311
|
ON CONFLICT(id) DO UPDATE SET
|
|
312
312
|
name = excluded.name, description = excluded.description,
|
|
313
313
|
config = excluded.config, updated_at = excluded.updated_at
|