memory-lancedb-pro 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +489 -0
- package/README_CN.md +406 -0
- package/cli.ts +611 -0
- package/index.ts +698 -0
- package/openclaw.plugin.json +385 -0
- package/package.json +38 -0
- package/skills/lesson/SKILL.md +28 -0
- package/src/adaptive-retrieval.ts +60 -0
- package/src/embedder.ts +354 -0
- package/src/migrate.ts +356 -0
- package/src/noise-filter.ts +78 -0
- package/src/retriever.ts +722 -0
- package/src/scopes.ts +374 -0
- package/src/store.ts +567 -0
- package/src/tools.ts +639 -0
package/src/tools.ts
ADDED
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Tool Definitions
|
|
3
|
+
* Memory management tools for AI agents
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Type } from "@sinclair/typebox";
|
|
7
|
+
import { stringEnum } from "openclaw/plugin-sdk";
|
|
8
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
9
|
+
import type { MemoryRetriever, RetrievalResult } from "./retriever.js";
|
|
10
|
+
import type { MemoryStore } from "./store.js";
|
|
11
|
+
import { isNoise } from "./noise-filter.js";
|
|
12
|
+
import type { MemoryScopeManager } from "./scopes.js";
|
|
13
|
+
import type { Embedder } from "./embedder.js";
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Types
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
export const MEMORY_CATEGORIES = ["preference", "fact", "decision", "entity", "other"] as const;
|
|
20
|
+
|
|
21
|
+
interface ToolContext {
|
|
22
|
+
retriever: MemoryRetriever;
|
|
23
|
+
store: MemoryStore;
|
|
24
|
+
scopeManager: MemoryScopeManager;
|
|
25
|
+
embedder: Embedder;
|
|
26
|
+
agentId?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Utility Functions
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
function clampInt(value: number, min: number, max: number): number {
|
|
34
|
+
if (!Number.isFinite(value)) return min;
|
|
35
|
+
return Math.min(max, Math.max(min, Math.floor(value)));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function clamp01(value: number, fallback = 0.7): number {
|
|
39
|
+
if (!Number.isFinite(value)) return fallback;
|
|
40
|
+
return Math.min(1, Math.max(0, value));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function sanitizeMemoryForSerialization(results: RetrievalResult[]) {
|
|
44
|
+
return results.map(r => ({
|
|
45
|
+
id: r.entry.id,
|
|
46
|
+
text: r.entry.text,
|
|
47
|
+
category: r.entry.category,
|
|
48
|
+
scope: r.entry.scope,
|
|
49
|
+
importance: r.entry.importance,
|
|
50
|
+
score: r.score,
|
|
51
|
+
sources: r.sources,
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Core Tools (Backward Compatible)
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
export function registerMemoryRecallTool(api: OpenClawPluginApi, context: ToolContext) {
|
|
60
|
+
api.registerTool(
|
|
61
|
+
{
|
|
62
|
+
name: "memory_recall",
|
|
63
|
+
label: "Memory Recall",
|
|
64
|
+
description: "Search through long-term memories using hybrid retrieval (vector + keyword search). Use when you need context about user preferences, past decisions, or previously discussed topics.",
|
|
65
|
+
parameters: Type.Object({
|
|
66
|
+
query: Type.String({ description: "Search query for finding relevant memories" }),
|
|
67
|
+
limit: Type.Optional(Type.Number({ description: "Max results to return (default: 5, max: 20)" })),
|
|
68
|
+
scope: Type.Optional(Type.String({ description: "Specific memory scope to search in (optional)" })),
|
|
69
|
+
category: Type.Optional(stringEnum(MEMORY_CATEGORIES)),
|
|
70
|
+
}),
|
|
71
|
+
async execute(_toolCallId, params) {
|
|
72
|
+
const { query, limit = 5, scope, category } = params as {
|
|
73
|
+
query: string;
|
|
74
|
+
limit?: number;
|
|
75
|
+
scope?: string;
|
|
76
|
+
category?: string;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const safeLimit = clampInt(limit, 1, 20);
|
|
81
|
+
|
|
82
|
+
// Determine accessible scopes
|
|
83
|
+
let scopeFilter = context.scopeManager.getAccessibleScopes(context.agentId);
|
|
84
|
+
if (scope) {
|
|
85
|
+
if (context.scopeManager.isAccessible(scope, context.agentId)) {
|
|
86
|
+
scopeFilter = [scope];
|
|
87
|
+
} else {
|
|
88
|
+
return {
|
|
89
|
+
content: [{ type: "text", text: `Access denied to scope: ${scope}` }],
|
|
90
|
+
details: { error: "scope_access_denied", requestedScope: scope },
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const results = await context.retriever.retrieve({
|
|
96
|
+
query,
|
|
97
|
+
limit: safeLimit,
|
|
98
|
+
scopeFilter,
|
|
99
|
+
category,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (results.length === 0) {
|
|
103
|
+
return {
|
|
104
|
+
content: [{ type: "text", text: "No relevant memories found." }],
|
|
105
|
+
details: { count: 0, query, scopes: scopeFilter },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const text = results
|
|
110
|
+
.map((r, i) => {
|
|
111
|
+
const sources = [];
|
|
112
|
+
if (r.sources.vector) sources.push("vector");
|
|
113
|
+
if (r.sources.bm25) sources.push("BM25");
|
|
114
|
+
if (r.sources.reranked) sources.push("reranked");
|
|
115
|
+
|
|
116
|
+
return `${i + 1}. [${r.entry.category}:${r.entry.scope}] ${r.entry.text} (${(r.score * 100).toFixed(0)}%${sources.length > 0 ? `, ${sources.join('+')}` : ''})`;
|
|
117
|
+
})
|
|
118
|
+
.join("\n");
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
content: [{ type: "text", text: `Found ${results.length} memories:\n\n${text}` }],
|
|
122
|
+
details: {
|
|
123
|
+
count: results.length,
|
|
124
|
+
memories: sanitizeMemoryForSerialization(results),
|
|
125
|
+
query,
|
|
126
|
+
scopes: scopeFilter,
|
|
127
|
+
retrievalMode: context.retriever.getConfig().mode,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return {
|
|
132
|
+
content: [{ type: "text", text: `Memory recall failed: ${error instanceof Error ? error.message : String(error)}` }],
|
|
133
|
+
details: { error: "recall_failed", message: String(error) },
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{ name: "memory_recall" }
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function registerMemoryStoreTool(api: OpenClawPluginApi, context: ToolContext) {
|
|
143
|
+
api.registerTool(
|
|
144
|
+
{
|
|
145
|
+
name: "memory_store",
|
|
146
|
+
label: "Memory Store",
|
|
147
|
+
description: "Save important information in long-term memory. Use for preferences, facts, decisions, and other notable information.",
|
|
148
|
+
parameters: Type.Object({
|
|
149
|
+
text: Type.String({ description: "Information to remember" }),
|
|
150
|
+
importance: Type.Optional(Type.Number({ description: "Importance score 0-1 (default: 0.7)" })),
|
|
151
|
+
category: Type.Optional(stringEnum(MEMORY_CATEGORIES)),
|
|
152
|
+
scope: Type.Optional(Type.String({ description: "Memory scope (optional, defaults to agent scope)" })),
|
|
153
|
+
}),
|
|
154
|
+
async execute(_toolCallId, params) {
|
|
155
|
+
const {
|
|
156
|
+
text,
|
|
157
|
+
importance = 0.7,
|
|
158
|
+
category = "other",
|
|
159
|
+
scope,
|
|
160
|
+
} = params as {
|
|
161
|
+
text: string;
|
|
162
|
+
importance?: number;
|
|
163
|
+
category?: string;
|
|
164
|
+
scope?: string;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
// Determine target scope
|
|
169
|
+
let targetScope = scope || context.scopeManager.getDefaultScope(context.agentId);
|
|
170
|
+
|
|
171
|
+
// Validate scope access
|
|
172
|
+
if (!context.scopeManager.isAccessible(targetScope, context.agentId)) {
|
|
173
|
+
return {
|
|
174
|
+
content: [{ type: "text", text: `Access denied to scope: ${targetScope}` }],
|
|
175
|
+
details: { error: "scope_access_denied", requestedScope: targetScope },
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Reject noise before wasting an embedding API call
|
|
180
|
+
if (isNoise(text)) {
|
|
181
|
+
return {
|
|
182
|
+
content: [{ type: "text", text: `Skipped: text detected as noise (greeting, boilerplate, or meta-question)` }],
|
|
183
|
+
details: { action: "noise_filtered", text: text.slice(0, 60) },
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const safeImportance = clamp01(importance, 0.7);
|
|
188
|
+
const vector = await context.embedder.embedPassage(text);
|
|
189
|
+
|
|
190
|
+
// Check for duplicates using raw vector similarity (bypasses importance/recency weighting)
|
|
191
|
+
const existing = await context.store.vectorSearch(vector, 1, 0.1, [targetScope]);
|
|
192
|
+
|
|
193
|
+
if (existing.length > 0 && existing[0].score > 0.98) {
|
|
194
|
+
return {
|
|
195
|
+
content: [
|
|
196
|
+
{
|
|
197
|
+
type: "text",
|
|
198
|
+
text: `Similar memory already exists: "${existing[0].entry.text}"`,
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
details: {
|
|
202
|
+
action: "duplicate",
|
|
203
|
+
existingId: existing[0].entry.id,
|
|
204
|
+
existingText: existing[0].entry.text,
|
|
205
|
+
existingScope: existing[0].entry.scope,
|
|
206
|
+
similarity: existing[0].score,
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const entry = await context.store.store({
|
|
212
|
+
text,
|
|
213
|
+
vector,
|
|
214
|
+
importance: safeImportance,
|
|
215
|
+
category: category as any,
|
|
216
|
+
scope: targetScope,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
content: [{ type: "text", text: `Stored: "${text.slice(0, 100)}${text.length > 100 ? '...' : ''}" in scope '${targetScope}'` }],
|
|
221
|
+
details: {
|
|
222
|
+
action: "created",
|
|
223
|
+
id: entry.id,
|
|
224
|
+
scope: entry.scope,
|
|
225
|
+
category: entry.category,
|
|
226
|
+
importance: entry.importance,
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
} catch (error) {
|
|
230
|
+
return {
|
|
231
|
+
content: [{ type: "text", text: `Memory storage failed: ${error instanceof Error ? error.message : String(error)}` }],
|
|
232
|
+
details: { error: "store_failed", message: String(error) },
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
{ name: "memory_store" }
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function registerMemoryForgetTool(api: OpenClawPluginApi, context: ToolContext) {
|
|
242
|
+
api.registerTool(
|
|
243
|
+
{
|
|
244
|
+
name: "memory_forget",
|
|
245
|
+
label: "Memory Forget",
|
|
246
|
+
description: "Delete specific memories. Supports both search-based and direct ID-based deletion.",
|
|
247
|
+
parameters: Type.Object({
|
|
248
|
+
query: Type.Optional(Type.String({ description: "Search query to find memory to delete" })),
|
|
249
|
+
memoryId: Type.Optional(Type.String({ description: "Specific memory ID to delete" })),
|
|
250
|
+
scope: Type.Optional(Type.String({ description: "Scope to search/delete from (optional)" })),
|
|
251
|
+
}),
|
|
252
|
+
async execute(_toolCallId, params) {
|
|
253
|
+
const { query, memoryId, scope } = params as {
|
|
254
|
+
query?: string;
|
|
255
|
+
memoryId?: string;
|
|
256
|
+
scope?: string;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
// Determine accessible scopes
|
|
261
|
+
let scopeFilter = context.scopeManager.getAccessibleScopes(context.agentId);
|
|
262
|
+
if (scope) {
|
|
263
|
+
if (context.scopeManager.isAccessible(scope, context.agentId)) {
|
|
264
|
+
scopeFilter = [scope];
|
|
265
|
+
} else {
|
|
266
|
+
return {
|
|
267
|
+
content: [{ type: "text", text: `Access denied to scope: ${scope}` }],
|
|
268
|
+
details: { error: "scope_access_denied", requestedScope: scope },
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (memoryId) {
|
|
274
|
+
const deleted = await context.store.delete(memoryId, scopeFilter);
|
|
275
|
+
if (deleted) {
|
|
276
|
+
return {
|
|
277
|
+
content: [{ type: "text", text: `Memory ${memoryId} forgotten.` }],
|
|
278
|
+
details: { action: "deleted", id: memoryId },
|
|
279
|
+
};
|
|
280
|
+
} else {
|
|
281
|
+
return {
|
|
282
|
+
content: [{ type: "text", text: `Memory ${memoryId} not found or access denied.` }],
|
|
283
|
+
details: { error: "not_found", id: memoryId },
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (query) {
|
|
289
|
+
const results = await context.retriever.retrieve({
|
|
290
|
+
query,
|
|
291
|
+
limit: 5,
|
|
292
|
+
scopeFilter,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
if (results.length === 0) {
|
|
296
|
+
return {
|
|
297
|
+
content: [{ type: "text", text: "No matching memories found." }],
|
|
298
|
+
details: { found: 0, query },
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (results.length === 1 && results[0].score > 0.9) {
|
|
303
|
+
const deleted = await context.store.delete(results[0].entry.id, scopeFilter);
|
|
304
|
+
if (deleted) {
|
|
305
|
+
return {
|
|
306
|
+
content: [{ type: "text", text: `Forgotten: "${results[0].entry.text}"` }],
|
|
307
|
+
details: { action: "deleted", id: results[0].entry.id },
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const list = results
|
|
313
|
+
.map(r => `- [${r.entry.id.slice(0, 8)}] ${r.entry.text.slice(0, 60)}${r.entry.text.length > 60 ? '...' : ''}`)
|
|
314
|
+
.join("\n");
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
content: [
|
|
318
|
+
{
|
|
319
|
+
type: "text",
|
|
320
|
+
text: `Found ${results.length} candidates. Specify memoryId to delete:\n${list}`,
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
details: {
|
|
324
|
+
action: "candidates",
|
|
325
|
+
candidates: sanitizeMemoryForSerialization(results),
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
content: [{ type: "text", text: "Provide either 'query' to search for memories or 'memoryId' to delete specific memory." }],
|
|
332
|
+
details: { error: "missing_param" },
|
|
333
|
+
};
|
|
334
|
+
} catch (error) {
|
|
335
|
+
return {
|
|
336
|
+
content: [{ type: "text", text: `Memory deletion failed: ${error instanceof Error ? error.message : String(error)}` }],
|
|
337
|
+
details: { error: "delete_failed", message: String(error) },
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
{ name: "memory_forget" }
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ============================================================================
|
|
347
|
+
// Update Tool
|
|
348
|
+
// ============================================================================
|
|
349
|
+
|
|
350
|
+
export function registerMemoryUpdateTool(api: OpenClawPluginApi, context: ToolContext) {
|
|
351
|
+
api.registerTool(
|
|
352
|
+
{
|
|
353
|
+
name: "memory_update",
|
|
354
|
+
label: "Memory Update",
|
|
355
|
+
description: "Update an existing memory in-place. Preserves original timestamp. Use when correcting outdated info or adjusting importance/category without losing creation date.",
|
|
356
|
+
parameters: Type.Object({
|
|
357
|
+
memoryId: Type.String({ description: "ID of the memory to update (full UUID or 8+ char prefix)" }),
|
|
358
|
+
text: Type.Optional(Type.String({ description: "New text content (triggers re-embedding)" })),
|
|
359
|
+
importance: Type.Optional(Type.Number({ description: "New importance score 0-1" })),
|
|
360
|
+
category: Type.Optional(stringEnum(MEMORY_CATEGORIES)),
|
|
361
|
+
}),
|
|
362
|
+
async execute(_toolCallId, params) {
|
|
363
|
+
const { memoryId, text, importance, category } = params as {
|
|
364
|
+
memoryId: string;
|
|
365
|
+
text?: string;
|
|
366
|
+
importance?: number;
|
|
367
|
+
category?: string;
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
if (!text && importance === undefined && !category) {
|
|
372
|
+
return {
|
|
373
|
+
content: [{ type: "text", text: "Nothing to update. Provide at least one of: text, importance, category." }],
|
|
374
|
+
details: { error: "no_updates" },
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Determine accessible scopes
|
|
379
|
+
const scopeFilter = context.scopeManager.getAccessibleScopes(context.agentId);
|
|
380
|
+
|
|
381
|
+
// Resolve memoryId: if it doesn't look like a UUID, try search
|
|
382
|
+
let resolvedId = memoryId;
|
|
383
|
+
const uuidLike = /^[0-9a-f]{8}(-[0-9a-f]{4}){0,4}/i.test(memoryId);
|
|
384
|
+
if (!uuidLike) {
|
|
385
|
+
// Treat as search query
|
|
386
|
+
const results = await context.retriever.retrieve({
|
|
387
|
+
query: memoryId,
|
|
388
|
+
limit: 3,
|
|
389
|
+
scopeFilter,
|
|
390
|
+
});
|
|
391
|
+
if (results.length === 0) {
|
|
392
|
+
return {
|
|
393
|
+
content: [{ type: "text", text: `No memory found matching "${memoryId}".` }],
|
|
394
|
+
details: { error: "not_found", query: memoryId },
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
if (results.length === 1 || results[0].score > 0.85) {
|
|
398
|
+
resolvedId = results[0].entry.id;
|
|
399
|
+
} else {
|
|
400
|
+
const list = results
|
|
401
|
+
.map(r => `- [${r.entry.id.slice(0, 8)}] ${r.entry.text.slice(0, 60)}${r.entry.text.length > 60 ? '...' : ''}`)
|
|
402
|
+
.join("\n");
|
|
403
|
+
return {
|
|
404
|
+
content: [{ type: "text", text: `Multiple matches. Specify memoryId:\n${list}` }],
|
|
405
|
+
details: { action: "candidates", candidates: sanitizeMemoryForSerialization(results) },
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// If text changed, re-embed; reject noise
|
|
411
|
+
let newVector: number[] | undefined;
|
|
412
|
+
if (text) {
|
|
413
|
+
if (isNoise(text)) {
|
|
414
|
+
return {
|
|
415
|
+
content: [{ type: "text", text: "Skipped: updated text detected as noise" }],
|
|
416
|
+
details: { action: "noise_filtered" },
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
newVector = await context.embedder.embedPassage(text);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const updates: Record<string, any> = {};
|
|
423
|
+
if (text) updates.text = text;
|
|
424
|
+
if (newVector) updates.vector = newVector;
|
|
425
|
+
if (importance !== undefined) updates.importance = clamp01(importance, 0.7);
|
|
426
|
+
if (category) updates.category = category;
|
|
427
|
+
|
|
428
|
+
const updated = await context.store.update(resolvedId, updates, scopeFilter);
|
|
429
|
+
|
|
430
|
+
if (!updated) {
|
|
431
|
+
return {
|
|
432
|
+
content: [{ type: "text", text: `Memory ${resolvedId.slice(0, 8)}... not found or access denied.` }],
|
|
433
|
+
details: { error: "not_found", id: resolvedId },
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
content: [{ type: "text", text: `Updated memory ${updated.id.slice(0, 8)}...: "${updated.text.slice(0, 80)}${updated.text.length > 80 ? '...' : ''}"` }],
|
|
439
|
+
details: {
|
|
440
|
+
action: "updated",
|
|
441
|
+
id: updated.id,
|
|
442
|
+
scope: updated.scope,
|
|
443
|
+
category: updated.category,
|
|
444
|
+
importance: updated.importance,
|
|
445
|
+
fieldsUpdated: Object.keys(updates),
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
} catch (error) {
|
|
449
|
+
return {
|
|
450
|
+
content: [{ type: "text", text: `Memory update failed: ${error instanceof Error ? error.message : String(error)}` }],
|
|
451
|
+
details: { error: "update_failed", message: String(error) },
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
{ name: "memory_update" }
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ============================================================================
|
|
461
|
+
// Management Tools (Optional)
|
|
462
|
+
// ============================================================================
|
|
463
|
+
|
|
464
|
+
export function registerMemoryStatsTool(api: OpenClawPluginApi, context: ToolContext) {
|
|
465
|
+
api.registerTool(
|
|
466
|
+
{
|
|
467
|
+
name: "memory_stats",
|
|
468
|
+
label: "Memory Statistics",
|
|
469
|
+
description: "Get statistics about memory usage, scopes, and categories.",
|
|
470
|
+
parameters: Type.Object({
|
|
471
|
+
scope: Type.Optional(Type.String({ description: "Specific scope to get stats for (optional)" })),
|
|
472
|
+
}),
|
|
473
|
+
async execute(_toolCallId, params) {
|
|
474
|
+
const { scope } = params as { scope?: string };
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
// Determine accessible scopes
|
|
478
|
+
let scopeFilter = context.scopeManager.getAccessibleScopes(context.agentId);
|
|
479
|
+
if (scope) {
|
|
480
|
+
if (context.scopeManager.isAccessible(scope, context.agentId)) {
|
|
481
|
+
scopeFilter = [scope];
|
|
482
|
+
} else {
|
|
483
|
+
return {
|
|
484
|
+
content: [{ type: "text", text: `Access denied to scope: ${scope}` }],
|
|
485
|
+
details: { error: "scope_access_denied", requestedScope: scope },
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const stats = await context.store.stats(scopeFilter);
|
|
491
|
+
const scopeManagerStats = context.scopeManager.getStats();
|
|
492
|
+
const retrievalConfig = context.retriever.getConfig();
|
|
493
|
+
|
|
494
|
+
const text = [
|
|
495
|
+
`Memory Statistics:`,
|
|
496
|
+
`• Total memories: ${stats.totalCount}`,
|
|
497
|
+
`• Available scopes: ${scopeManagerStats.totalScopes}`,
|
|
498
|
+
`• Retrieval mode: ${retrievalConfig.mode}`,
|
|
499
|
+
`• FTS support: ${context.store.hasFtsSupport ? 'Yes' : 'No'}`,
|
|
500
|
+
``,
|
|
501
|
+
`Memories by scope:`,
|
|
502
|
+
...Object.entries(stats.scopeCounts).map(([s, count]) => ` • ${s}: ${count}`),
|
|
503
|
+
``,
|
|
504
|
+
`Memories by category:`,
|
|
505
|
+
...Object.entries(stats.categoryCounts).map(([c, count]) => ` • ${c}: ${count}`),
|
|
506
|
+
].join('\n');
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
content: [{ type: "text", text }],
|
|
510
|
+
details: {
|
|
511
|
+
stats,
|
|
512
|
+
scopeManagerStats,
|
|
513
|
+
retrievalConfig: {
|
|
514
|
+
...retrievalConfig,
|
|
515
|
+
rerankApiKey: retrievalConfig.rerankApiKey ? "***" : undefined,
|
|
516
|
+
},
|
|
517
|
+
hasFtsSupport: context.store.hasFtsSupport,
|
|
518
|
+
},
|
|
519
|
+
};
|
|
520
|
+
} catch (error) {
|
|
521
|
+
return {
|
|
522
|
+
content: [{ type: "text", text: `Failed to get memory stats: ${error instanceof Error ? error.message : String(error)}` }],
|
|
523
|
+
details: { error: "stats_failed", message: String(error) },
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
{ name: "memory_stats" }
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export function registerMemoryListTool(api: OpenClawPluginApi, context: ToolContext) {
|
|
533
|
+
api.registerTool(
|
|
534
|
+
{
|
|
535
|
+
name: "memory_list",
|
|
536
|
+
label: "Memory List",
|
|
537
|
+
description: "List recent memories with optional filtering by scope and category.",
|
|
538
|
+
parameters: Type.Object({
|
|
539
|
+
limit: Type.Optional(Type.Number({ description: "Max memories to list (default: 10, max: 50)" })),
|
|
540
|
+
scope: Type.Optional(Type.String({ description: "Filter by specific scope (optional)" })),
|
|
541
|
+
category: Type.Optional(stringEnum(MEMORY_CATEGORIES)),
|
|
542
|
+
offset: Type.Optional(Type.Number({ description: "Number of memories to skip (default: 0)" })),
|
|
543
|
+
}),
|
|
544
|
+
async execute(_toolCallId, params) {
|
|
545
|
+
const {
|
|
546
|
+
limit = 10,
|
|
547
|
+
scope,
|
|
548
|
+
category,
|
|
549
|
+
offset = 0,
|
|
550
|
+
} = params as {
|
|
551
|
+
limit?: number;
|
|
552
|
+
scope?: string;
|
|
553
|
+
category?: string;
|
|
554
|
+
offset?: number;
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
try {
|
|
558
|
+
const safeLimit = clampInt(limit, 1, 50);
|
|
559
|
+
const safeOffset = clampInt(offset, 0, 1000);
|
|
560
|
+
|
|
561
|
+
// Determine accessible scopes
|
|
562
|
+
let scopeFilter = context.scopeManager.getAccessibleScopes(context.agentId);
|
|
563
|
+
if (scope) {
|
|
564
|
+
if (context.scopeManager.isAccessible(scope, context.agentId)) {
|
|
565
|
+
scopeFilter = [scope];
|
|
566
|
+
} else {
|
|
567
|
+
return {
|
|
568
|
+
content: [{ type: "text", text: `Access denied to scope: ${scope}` }],
|
|
569
|
+
details: { error: "scope_access_denied", requestedScope: scope },
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const entries = await context.store.list(scopeFilter, category, safeLimit, safeOffset);
|
|
575
|
+
|
|
576
|
+
if (entries.length === 0) {
|
|
577
|
+
return {
|
|
578
|
+
content: [{ type: "text", text: "No memories found." }],
|
|
579
|
+
details: { count: 0, filters: { scope, category, limit: safeLimit, offset: safeOffset } },
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const text = entries
|
|
584
|
+
.map((entry, i) => {
|
|
585
|
+
const date = new Date(entry.timestamp).toISOString().split('T')[0];
|
|
586
|
+
return `${safeOffset + i + 1}. [${entry.category}:${entry.scope}] ${entry.text.slice(0, 100)}${entry.text.length > 100 ? '...' : ''} (${date})`;
|
|
587
|
+
})
|
|
588
|
+
.join('\n');
|
|
589
|
+
|
|
590
|
+
return {
|
|
591
|
+
content: [{ type: "text", text: `Recent memories (showing ${entries.length}):\n\n${text}` }],
|
|
592
|
+
details: {
|
|
593
|
+
count: entries.length,
|
|
594
|
+
memories: entries.map(e => ({
|
|
595
|
+
id: e.id,
|
|
596
|
+
text: e.text,
|
|
597
|
+
category: e.category,
|
|
598
|
+
scope: e.scope,
|
|
599
|
+
importance: e.importance,
|
|
600
|
+
timestamp: e.timestamp,
|
|
601
|
+
})),
|
|
602
|
+
filters: { scope, category, limit: safeLimit, offset: safeOffset },
|
|
603
|
+
},
|
|
604
|
+
};
|
|
605
|
+
} catch (error) {
|
|
606
|
+
return {
|
|
607
|
+
content: [{ type: "text", text: `Failed to list memories: ${error instanceof Error ? error.message : String(error)}` }],
|
|
608
|
+
details: { error: "list_failed", message: String(error) },
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
{ name: "memory_list" }
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ============================================================================
|
|
618
|
+
// Tool Registration Helper
|
|
619
|
+
// ============================================================================
|
|
620
|
+
|
|
621
|
+
export function registerAllMemoryTools(
|
|
622
|
+
api: OpenClawPluginApi,
|
|
623
|
+
context: ToolContext,
|
|
624
|
+
options: {
|
|
625
|
+
enableManagementTools?: boolean;
|
|
626
|
+
} = {}
|
|
627
|
+
) {
|
|
628
|
+
// Core tools (always enabled)
|
|
629
|
+
registerMemoryRecallTool(api, context);
|
|
630
|
+
registerMemoryStoreTool(api, context);
|
|
631
|
+
registerMemoryForgetTool(api, context);
|
|
632
|
+
registerMemoryUpdateTool(api, context);
|
|
633
|
+
|
|
634
|
+
// Management tools (optional)
|
|
635
|
+
if (options.enableManagementTools) {
|
|
636
|
+
registerMemoryStatsTool(api, context);
|
|
637
|
+
registerMemoryListTool(api, context);
|
|
638
|
+
}
|
|
639
|
+
}
|