@mem9/mem9 0.3.6 → 0.3.7
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/index.ts +344 -18
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -12,6 +12,8 @@ import type {
|
|
|
12
12
|
} from "./types.js";
|
|
13
13
|
|
|
14
14
|
const DEFAULT_API_URL = "https://api.mem9.ai";
|
|
15
|
+
const MEM9_MEMORY_PATH_PREFIX = "mem9/";
|
|
16
|
+
const MEM9_MEMORY_PROVIDER = "mem9";
|
|
15
17
|
|
|
16
18
|
function jsonResult(data: unknown) {
|
|
17
19
|
// Older OpenClaw versions may assume tool results have a normalized
|
|
@@ -27,6 +29,69 @@ function jsonResult(data: unknown) {
|
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
function buildMemoryPath(id: string): string {
|
|
33
|
+
return `${MEM9_MEMORY_PATH_PREFIX}${id}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeMemoryLookup(value: string): string {
|
|
37
|
+
const trimmed = value.trim();
|
|
38
|
+
return trimmed.startsWith(MEM9_MEMORY_PATH_PREFIX)
|
|
39
|
+
? trimmed.slice(MEM9_MEMORY_PATH_PREFIX.length)
|
|
40
|
+
: trimmed;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeSnippet(text: string, maxLength = 240): string {
|
|
44
|
+
const flattened = text.replace(/\s+/g, " ").trim();
|
|
45
|
+
if (flattened.length <= maxLength) {
|
|
46
|
+
return flattened;
|
|
47
|
+
}
|
|
48
|
+
return `${flattened.slice(0, maxLength - 3)}...`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function countLines(text: string): number {
|
|
52
|
+
if (!text) {
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
return text.split(/\r?\n/).length;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildPromptSection(params: {
|
|
59
|
+
availableTools: Set<string>;
|
|
60
|
+
citationsMode?: string;
|
|
61
|
+
}): string[] {
|
|
62
|
+
const hasMemorySearch = params.availableTools.has("memory_search");
|
|
63
|
+
const hasMemoryGet = params.availableTools.has("memory_get");
|
|
64
|
+
|
|
65
|
+
if (!hasMemorySearch && !hasMemoryGet) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let toolGuidance: string;
|
|
70
|
+
if (hasMemorySearch && hasMemoryGet) {
|
|
71
|
+
toolGuidance =
|
|
72
|
+
"Before answering anything about prior work, decisions, dates, people, preferences, or todos: run `memory_search` first, then use `memory_get` to pull only the needed memory. If low confidence after search, say you checked.";
|
|
73
|
+
} else if (hasMemorySearch) {
|
|
74
|
+
toolGuidance =
|
|
75
|
+
"Before answering anything about prior work, decisions, dates, people, preferences, or todos: run `memory_search` first and answer from the matching results. If low confidence after search, say you checked.";
|
|
76
|
+
} else {
|
|
77
|
+
toolGuidance =
|
|
78
|
+
"Before answering anything about prior work, decisions, dates, people, preferences, or todos that already point to a specific memory: run `memory_get` to pull only the needed memory. If low confidence after reading it, say you checked.";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const lines = ["## Memory Recall", toolGuidance];
|
|
82
|
+
if (params.citationsMode === "off") {
|
|
83
|
+
lines.push(
|
|
84
|
+
"Citations are disabled: do not mention memory identifiers unless the user explicitly asks.",
|
|
85
|
+
);
|
|
86
|
+
} else {
|
|
87
|
+
lines.push(
|
|
88
|
+
"Citations: include the memory identifier only when it helps the user verify recalled memory.",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
lines.push("");
|
|
92
|
+
return lines;
|
|
93
|
+
}
|
|
94
|
+
|
|
30
95
|
interface MemoryCapability {
|
|
31
96
|
search: (query: string, opts?: { limit?: number }) => Promise<{ data: Memory[]; total: number }>;
|
|
32
97
|
store: (content: string, opts?: { tags?: string[]; source?: string }) => Promise<unknown>;
|
|
@@ -34,6 +99,71 @@ interface MemoryCapability {
|
|
|
34
99
|
remove: (id: string) => Promise<boolean>;
|
|
35
100
|
}
|
|
36
101
|
|
|
102
|
+
interface MemoryPromptSectionBuilder {
|
|
103
|
+
(params: { availableTools: Set<string>; citationsMode?: string }): string[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface MemorySearchResult {
|
|
107
|
+
path: string;
|
|
108
|
+
startLine: number;
|
|
109
|
+
endLine: number;
|
|
110
|
+
score: number;
|
|
111
|
+
snippet: string;
|
|
112
|
+
source: "memory";
|
|
113
|
+
citation?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface MemoryProviderStatus {
|
|
117
|
+
backend: "builtin" | "qmd";
|
|
118
|
+
provider: string;
|
|
119
|
+
requestedProvider?: string;
|
|
120
|
+
custom?: Record<string, unknown>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
interface MemorySearchManager {
|
|
124
|
+
search: (
|
|
125
|
+
query: string,
|
|
126
|
+
opts?: { maxResults?: number; minScore?: number; sessionKey?: string },
|
|
127
|
+
) => Promise<MemorySearchResult[]>;
|
|
128
|
+
readFile: (params: {
|
|
129
|
+
relPath: string;
|
|
130
|
+
from?: number;
|
|
131
|
+
lines?: number;
|
|
132
|
+
}) => Promise<{ text: string; path: string }>;
|
|
133
|
+
status: () => MemoryProviderStatus;
|
|
134
|
+
sync?: (params?: {
|
|
135
|
+
reason?: string;
|
|
136
|
+
force?: boolean;
|
|
137
|
+
sessionFiles?: string[];
|
|
138
|
+
progress?: (update: { completed: number; total: number; label?: string }) => void;
|
|
139
|
+
}) => Promise<void>;
|
|
140
|
+
probeEmbeddingAvailability: () => Promise<{ ok: boolean; error?: string }>;
|
|
141
|
+
probeVectorAvailability: () => Promise<boolean>;
|
|
142
|
+
close?: () => Promise<void>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
interface MemoryRuntime {
|
|
146
|
+
getMemorySearchManager: (params: {
|
|
147
|
+
cfg?: unknown;
|
|
148
|
+
agentId: string;
|
|
149
|
+
purpose?: "default" | "status";
|
|
150
|
+
}) => Promise<{ manager: MemorySearchManager | null; error?: string }>;
|
|
151
|
+
resolveMemoryBackendConfig: (params: { cfg?: unknown; agentId: string }) => {
|
|
152
|
+
backend: "builtin" | "qmd";
|
|
153
|
+
provider: string;
|
|
154
|
+
requestedProvider?: string;
|
|
155
|
+
custom?: Record<string, unknown>;
|
|
156
|
+
};
|
|
157
|
+
closeAllMemorySearchManagers?: () => Promise<void>;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
interface MemorySlotCapability {
|
|
161
|
+
promptBuilder?: MemoryPromptSectionBuilder;
|
|
162
|
+
runtime?: MemoryRuntime;
|
|
163
|
+
flushPlanResolver?: unknown;
|
|
164
|
+
publicArtifacts?: unknown;
|
|
165
|
+
}
|
|
166
|
+
|
|
37
167
|
interface OpenClawPluginApi {
|
|
38
168
|
pluginConfig?: unknown;
|
|
39
169
|
logger: {
|
|
@@ -44,6 +174,9 @@ interface OpenClawPluginApi {
|
|
|
44
174
|
factory: ToolFactory | (() => AnyAgentTool[]),
|
|
45
175
|
opts: { names: string[] }
|
|
46
176
|
) => void;
|
|
177
|
+
registerMemoryCapability?: (capability: MemorySlotCapability) => void;
|
|
178
|
+
registerMemoryPromptSection?: (builder: MemoryPromptSectionBuilder) => void;
|
|
179
|
+
registerMemoryRuntime?: (runtime: MemoryRuntime) => void;
|
|
47
180
|
registerCapability?: (slot: string, capability: MemoryCapability) => void;
|
|
48
181
|
on: (hookName: string, handler: (...args: unknown[]) => unknown, opts?: { priority?: number }) => void;
|
|
49
182
|
}
|
|
@@ -69,6 +202,146 @@ interface AnyAgentTool {
|
|
|
69
202
|
execute: (_id: string, params: unknown) => Promise<unknown>;
|
|
70
203
|
}
|
|
71
204
|
|
|
205
|
+
class Mem9MemorySearchManager implements MemorySearchManager {
|
|
206
|
+
constructor(
|
|
207
|
+
private backend: MemoryBackend,
|
|
208
|
+
private apiUrl: string,
|
|
209
|
+
) {}
|
|
210
|
+
|
|
211
|
+
async search(
|
|
212
|
+
query: string,
|
|
213
|
+
opts?: { maxResults?: number; minScore?: number; sessionKey?: string },
|
|
214
|
+
): Promise<MemorySearchResult[]> {
|
|
215
|
+
void opts?.sessionKey;
|
|
216
|
+
const result = await this.backend.search({
|
|
217
|
+
q: query,
|
|
218
|
+
limit: opts?.maxResults,
|
|
219
|
+
});
|
|
220
|
+
return (result.data ?? [])
|
|
221
|
+
.map((memory, index) => {
|
|
222
|
+
const startLine = 1;
|
|
223
|
+
const endLine = countLines(memory.content);
|
|
224
|
+
const score =
|
|
225
|
+
typeof memory.score === "number"
|
|
226
|
+
? memory.score
|
|
227
|
+
: Math.max(0, 1 - index / Math.max(result.data.length, 1));
|
|
228
|
+
return {
|
|
229
|
+
path: buildMemoryPath(memory.id),
|
|
230
|
+
startLine,
|
|
231
|
+
endLine,
|
|
232
|
+
score,
|
|
233
|
+
snippet: normalizeSnippet(memory.content),
|
|
234
|
+
source: "memory" as const,
|
|
235
|
+
citation: `${buildMemoryPath(memory.id)}#L${startLine}`,
|
|
236
|
+
};
|
|
237
|
+
})
|
|
238
|
+
.filter((entry) => opts?.minScore == null || entry.score >= opts.minScore);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async readFile(params: {
|
|
242
|
+
relPath: string;
|
|
243
|
+
from?: number;
|
|
244
|
+
lines?: number;
|
|
245
|
+
}): Promise<{ text: string; path: string }> {
|
|
246
|
+
const lookup = normalizeMemoryLookup(params.relPath);
|
|
247
|
+
const memory = await this.backend.get(lookup);
|
|
248
|
+
if (!memory) {
|
|
249
|
+
return { text: "", path: params.relPath };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const contentLines = memory.content.split(/\r?\n/);
|
|
253
|
+
const fromLine = Math.max(1, params.from ?? 1);
|
|
254
|
+
const lineCount = params.lines == null ? contentLines.length : Math.max(0, params.lines);
|
|
255
|
+
const sliced =
|
|
256
|
+
params.lines == null
|
|
257
|
+
? contentLines.slice(fromLine - 1)
|
|
258
|
+
: contentLines.slice(fromLine - 1, fromLine - 1 + lineCount);
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
text: sliced.join("\n"),
|
|
262
|
+
path: buildMemoryPath(memory.id),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
status(): MemoryProviderStatus {
|
|
267
|
+
return {
|
|
268
|
+
backend: "builtin",
|
|
269
|
+
provider: MEM9_MEMORY_PROVIDER,
|
|
270
|
+
requestedProvider: MEM9_MEMORY_PROVIDER,
|
|
271
|
+
custom: {
|
|
272
|
+
mode: "remote",
|
|
273
|
+
apiUrl: this.apiUrl,
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async sync(): Promise<void> {}
|
|
279
|
+
|
|
280
|
+
async probeEmbeddingAvailability(): Promise<{ ok: boolean; error?: string }> {
|
|
281
|
+
try {
|
|
282
|
+
await this.backend.search({ q: "mem9-healthcheck", limit: 1 });
|
|
283
|
+
return { ok: true };
|
|
284
|
+
} catch (err) {
|
|
285
|
+
return {
|
|
286
|
+
ok: false,
|
|
287
|
+
error: err instanceof Error ? err.message : String(err),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async probeVectorAvailability(): Promise<boolean> {
|
|
293
|
+
const probe = await this.probeEmbeddingAvailability();
|
|
294
|
+
return probe.ok;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async close(): Promise<void> {}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function createMemoryRuntime(params: {
|
|
301
|
+
apiUrl: string;
|
|
302
|
+
fallbackAgentId: string;
|
|
303
|
+
createBackend: (agentId: string) => MemoryBackend;
|
|
304
|
+
}): MemoryRuntime {
|
|
305
|
+
const managers = new Map<string, Mem9MemorySearchManager>();
|
|
306
|
+
|
|
307
|
+
const getOrCreateManager = (agentId: string): Mem9MemorySearchManager => {
|
|
308
|
+
const resolvedAgentId = agentId.trim() || params.fallbackAgentId;
|
|
309
|
+
const existing = managers.get(resolvedAgentId);
|
|
310
|
+
if (existing) {
|
|
311
|
+
return existing;
|
|
312
|
+
}
|
|
313
|
+
const next = new Mem9MemorySearchManager(
|
|
314
|
+
params.createBackend(resolvedAgentId),
|
|
315
|
+
params.apiUrl,
|
|
316
|
+
);
|
|
317
|
+
managers.set(resolvedAgentId, next);
|
|
318
|
+
return next;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
async getMemorySearchManager({ agentId }) {
|
|
323
|
+
return { manager: getOrCreateManager(agentId) };
|
|
324
|
+
},
|
|
325
|
+
resolveMemoryBackendConfig() {
|
|
326
|
+
return {
|
|
327
|
+
backend: "builtin",
|
|
328
|
+
provider: MEM9_MEMORY_PROVIDER,
|
|
329
|
+
requestedProvider: MEM9_MEMORY_PROVIDER,
|
|
330
|
+
custom: {
|
|
331
|
+
mode: "remote",
|
|
332
|
+
apiUrl: params.apiUrl,
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
},
|
|
336
|
+
async closeAllMemorySearchManagers() {
|
|
337
|
+
for (const manager of managers.values()) {
|
|
338
|
+
await manager.close?.();
|
|
339
|
+
}
|
|
340
|
+
managers.clear();
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
72
345
|
function buildTools(backend: MemoryBackend): AnyAgentTool[] {
|
|
73
346
|
return [
|
|
74
347
|
{
|
|
@@ -122,6 +395,10 @@ function buildTools(backend: MemoryBackend): AnyAgentTool[] {
|
|
|
122
395
|
type: "object",
|
|
123
396
|
properties: {
|
|
124
397
|
q: { type: "string", description: "Search query" },
|
|
398
|
+
query: {
|
|
399
|
+
type: "string",
|
|
400
|
+
description: "Search query alias for hosts that expect `query` instead of `q`",
|
|
401
|
+
},
|
|
125
402
|
tags: {
|
|
126
403
|
type: "string",
|
|
127
404
|
description: "Comma-separated tags to filter by (AND)",
|
|
@@ -141,9 +418,19 @@ function buildTools(backend: MemoryBackend): AnyAgentTool[] {
|
|
|
141
418
|
},
|
|
142
419
|
async execute(_id: string, params: unknown) {
|
|
143
420
|
try {
|
|
144
|
-
const input = (params ?? {}) as SearchInput;
|
|
421
|
+
const input = { ...((params ?? {}) as SearchInput) };
|
|
422
|
+
if (!input.q && typeof (params as { query?: unknown } | null)?.query === "string") {
|
|
423
|
+
input.q = (params as { query: string }).query;
|
|
424
|
+
}
|
|
145
425
|
const result = await backend.search(input);
|
|
146
|
-
return jsonResult({
|
|
426
|
+
return jsonResult({
|
|
427
|
+
ok: true,
|
|
428
|
+
...result,
|
|
429
|
+
data: (result.data ?? []).map((memory) => ({
|
|
430
|
+
...memory,
|
|
431
|
+
path: buildMemoryPath(memory.id),
|
|
432
|
+
})),
|
|
433
|
+
});
|
|
147
434
|
} catch (err) {
|
|
148
435
|
return jsonResult({
|
|
149
436
|
ok: false,
|
|
@@ -161,16 +448,31 @@ function buildTools(backend: MemoryBackend): AnyAgentTool[] {
|
|
|
161
448
|
type: "object",
|
|
162
449
|
properties: {
|
|
163
450
|
id: { type: "string", description: "Memory id (UUID)" },
|
|
451
|
+
path: {
|
|
452
|
+
type: "string",
|
|
453
|
+
description: "Memory lookup path alias, e.g. mem9/<id>",
|
|
454
|
+
},
|
|
164
455
|
},
|
|
165
|
-
required: [
|
|
456
|
+
required: [],
|
|
166
457
|
},
|
|
167
458
|
async execute(_id: string, params: unknown) {
|
|
168
459
|
try {
|
|
169
|
-
const
|
|
460
|
+
const raw = params as { id?: string; path?: string };
|
|
461
|
+
const lookup = typeof raw.id === "string" ? raw.id : raw.path;
|
|
462
|
+
if (!lookup) {
|
|
463
|
+
return jsonResult({ ok: false, error: "memory id or path is required" });
|
|
464
|
+
}
|
|
465
|
+
const id = normalizeMemoryLookup(lookup);
|
|
170
466
|
const result = await backend.get(id);
|
|
171
467
|
if (!result)
|
|
172
468
|
return jsonResult({ ok: false, error: "memory not found" });
|
|
173
|
-
return jsonResult({
|
|
469
|
+
return jsonResult({
|
|
470
|
+
ok: true,
|
|
471
|
+
data: {
|
|
472
|
+
...result,
|
|
473
|
+
path: buildMemoryPath(result.id),
|
|
474
|
+
},
|
|
475
|
+
});
|
|
174
476
|
} catch (err) {
|
|
175
477
|
return jsonResult({
|
|
176
478
|
ok: false,
|
|
@@ -250,6 +552,8 @@ const mnemoPlugin = {
|
|
|
250
552
|
name: "Mnemo Memory",
|
|
251
553
|
description:
|
|
252
554
|
"AI agent memory — server mode (mnemo-server) with hybrid vector + keyword search.",
|
|
555
|
+
// Static hint for hosts that inspect entry metadata before runtime registration.
|
|
556
|
+
capabilities: ["memory"],
|
|
253
557
|
|
|
254
558
|
register(api: OpenClawPluginApi) {
|
|
255
559
|
const cfg = (api.pluginConfig ?? {}) as PluginConfig;
|
|
@@ -285,30 +589,52 @@ const mnemoPlugin = {
|
|
|
285
589
|
|
|
286
590
|
const hookAgentId = cfg.agentName ?? "agent";
|
|
287
591
|
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
const backend = new LazyServerBackend(
|
|
592
|
+
const createBackend = (agentId: string): MemoryBackend =>
|
|
593
|
+
new LazyServerBackend(
|
|
291
594
|
effectiveApiUrl,
|
|
292
595
|
() => resolveAPIKey(agentId),
|
|
293
596
|
agentId,
|
|
294
597
|
);
|
|
598
|
+
|
|
599
|
+
const factory: ToolFactory = (ctx: ToolContext) => {
|
|
600
|
+
const agentId = ctx.agentId ?? cfg.agentName ?? "agent";
|
|
601
|
+
const backend = createBackend(agentId);
|
|
295
602
|
return buildTools(backend);
|
|
296
603
|
};
|
|
297
604
|
|
|
298
605
|
api.registerTool(factory, { names: toolNames });
|
|
299
606
|
|
|
300
607
|
// Shared lazy backend for hooks and capability registration.
|
|
301
|
-
const hookBackend =
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
hookAgentId,
|
|
305
|
-
|
|
608
|
+
const hookBackend = createBackend(hookAgentId);
|
|
609
|
+
const memoryRuntime = createMemoryRuntime({
|
|
610
|
+
apiUrl: effectiveApiUrl,
|
|
611
|
+
fallbackAgentId: hookAgentId,
|
|
612
|
+
createBackend,
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
// OpenClaw's memory slot API has evolved across versions:
|
|
616
|
+
// - current hosts prefer registerMemoryCapability({ promptBuilder, runtime })
|
|
617
|
+
// - 2026.4.2 uses registerMemoryPromptSection/registerMemoryRuntime
|
|
618
|
+
// - older compatibility shims may still expose registerCapability("memory", ...)
|
|
619
|
+
if (typeof api.registerMemoryCapability === "function") {
|
|
620
|
+
api.registerMemoryCapability({
|
|
621
|
+
promptBuilder: buildPromptSection,
|
|
622
|
+
runtime: memoryRuntime,
|
|
623
|
+
});
|
|
624
|
+
} else {
|
|
625
|
+
if (typeof api.registerMemoryPromptSection === "function") {
|
|
626
|
+
api.registerMemoryPromptSection(buildPromptSection);
|
|
627
|
+
}
|
|
628
|
+
if (typeof api.registerMemoryRuntime === "function") {
|
|
629
|
+
api.registerMemoryRuntime(memoryRuntime);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
306
632
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
633
|
+
if (
|
|
634
|
+
typeof api.registerMemoryCapability !== "function" &&
|
|
635
|
+
typeof api.registerMemoryRuntime !== "function" &&
|
|
636
|
+
typeof api.registerCapability === "function"
|
|
637
|
+
) {
|
|
312
638
|
api.registerCapability("memory", {
|
|
313
639
|
search: async (query, opts) => {
|
|
314
640
|
const result = await hookBackend.search({ q: query, limit: opts?.limit });
|
package/package.json
CHANGED