@yhc3577/memory-new 0.1.0 → 0.1.3
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/index.js +1634 -28
- package/dist/index.js.map +4 -4
- package/dist/openclaw.plugin.json +100 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,54 +1,1660 @@
|
|
|
1
1
|
// index.ts
|
|
2
2
|
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
3
|
+
|
|
4
|
+
// src/store/storage.ts
|
|
5
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, appendFileSync } from "fs";
|
|
6
|
+
import { join, dirname } from "path";
|
|
7
|
+
var STORAGE_PATHS = {
|
|
8
|
+
l0: "memory/l0/",
|
|
9
|
+
l1: "memory/l1/",
|
|
10
|
+
l2: "memory/scenes/",
|
|
11
|
+
l3: "persona.md",
|
|
12
|
+
index: {
|
|
13
|
+
l2: "memory/scenes/index.json",
|
|
14
|
+
l3: "persona.md.meta"
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var StorageAdapter = class {
|
|
18
|
+
baseDir;
|
|
19
|
+
constructor(baseDir = "~/.openclaw/memory-new") {
|
|
20
|
+
this.baseDir = baseDir.replace("~", process.env.HOME || "/root");
|
|
21
|
+
mkdirSync(this.baseDir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
resolve(path) {
|
|
24
|
+
return join(this.baseDir, path);
|
|
25
|
+
}
|
|
26
|
+
// ========== File Operations ==========
|
|
27
|
+
async readFile(key) {
|
|
28
|
+
const filePath = this.resolve(key);
|
|
29
|
+
if (!existsSync(filePath)) return null;
|
|
30
|
+
return readFileSync(filePath, "utf-8");
|
|
31
|
+
}
|
|
32
|
+
async writeFile(key, content) {
|
|
33
|
+
const filePath = this.resolve(key);
|
|
34
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
35
|
+
writeFileSync(filePath, content, "utf-8");
|
|
36
|
+
}
|
|
37
|
+
async appendFile(key, content) {
|
|
38
|
+
const filePath = this.resolve(key);
|
|
39
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
40
|
+
appendFileSync(filePath, content, "utf-8");
|
|
41
|
+
}
|
|
42
|
+
async exists(key) {
|
|
43
|
+
return existsSync(this.resolve(key));
|
|
44
|
+
}
|
|
45
|
+
// ========== L0 Operations (JSONL per session) ==========
|
|
46
|
+
/**
|
|
47
|
+
* L0: Append message to session's JSONL file (TDB pattern: append-only)
|
|
48
|
+
*/
|
|
49
|
+
async appendL0(record) {
|
|
50
|
+
const sessionFile = `${STORAGE_PATHS.l0}${record.sessionKey}.jsonl`;
|
|
51
|
+
const line = JSON.stringify(record) + "\n";
|
|
52
|
+
await this.appendFile(sessionFile, line);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* L0: Batch read messages from session
|
|
56
|
+
*/
|
|
57
|
+
async readL0(sessionKey, limit = 100) {
|
|
58
|
+
const sessionFile = `${STORAGE_PATHS.l0}${sessionKey}.jsonl`;
|
|
59
|
+
const content = await this.readFile(sessionFile);
|
|
60
|
+
if (!content) return [];
|
|
61
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
62
|
+
const messages = lines.slice(-limit).map((line) => JSON.parse(line));
|
|
63
|
+
return messages;
|
|
64
|
+
}
|
|
65
|
+
// ========== L1 Operations (JSONL) ==========
|
|
66
|
+
/**
|
|
67
|
+
* L1: Append atomic memory to session's JSONL file
|
|
68
|
+
*/
|
|
69
|
+
async appendL1(record) {
|
|
70
|
+
const sessionFile = `${STORAGE_PATHS.l1}${record.sessionKey}.jsonl`;
|
|
71
|
+
const line = JSON.stringify(record) + "\n";
|
|
72
|
+
await this.appendFile(sessionFile, line);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* L1: Search memories by content (simple full-text scan)
|
|
76
|
+
*/
|
|
77
|
+
async searchL1(query, limit = 10) {
|
|
78
|
+
const content = await this.readFile(`${STORAGE_PATHS.l1}*.jsonl`);
|
|
79
|
+
if (!content) return [];
|
|
80
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
81
|
+
const allRecords = lines.map((line) => JSON.parse(line));
|
|
82
|
+
const queryLower = query.toLowerCase();
|
|
83
|
+
return allRecords.filter((r) => r.content.toLowerCase().includes(queryLower)).slice(0, limit);
|
|
84
|
+
}
|
|
85
|
+
// ========== L2 Operations (Markdown files) ==========
|
|
86
|
+
/**
|
|
87
|
+
* L2: Write scene block as Markdown file
|
|
88
|
+
*/
|
|
89
|
+
async writeScene(scene2) {
|
|
90
|
+
const sceneFile = `${STORAGE_PATHS.l2}${scene2.id}.md`;
|
|
91
|
+
const frontmatter = `---
|
|
92
|
+
id: ${scene2.id}
|
|
93
|
+
title: ${scene2.title}
|
|
94
|
+
summary: ${scene2.summary}
|
|
95
|
+
tags: ${JSON.stringify(scene2.tags)}
|
|
96
|
+
created_at: ${scene2.createdAt}
|
|
97
|
+
updated_at: ${scene2.updatedAt}
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
`;
|
|
101
|
+
await this.writeFile(sceneFile, frontmatter + scene2.content);
|
|
102
|
+
await this.updateSceneIndex(scene2);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* L2: Read scene block
|
|
106
|
+
*/
|
|
107
|
+
async readScene(sceneId) {
|
|
108
|
+
const content = await this.readFile(`${STORAGE_PATHS.l2}${sceneId}.md`);
|
|
109
|
+
if (!content) return null;
|
|
110
|
+
const lines = content.split("\n");
|
|
111
|
+
const frontmatterEnd = lines.findIndex((l) => l === "---", 1);
|
|
112
|
+
if (frontmatterEnd <= 1) return null;
|
|
113
|
+
const frontmatter = {};
|
|
114
|
+
for (let i = 1; i < frontmatterEnd; i++) {
|
|
115
|
+
const [key, ...valueParts] = lines[i].split(":");
|
|
116
|
+
if (key && valueParts.length > 0) {
|
|
117
|
+
frontmatter[key.trim()] = valueParts.join(":").trim();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
id: scene.id,
|
|
122
|
+
title: frontmatter.title || "",
|
|
123
|
+
content: lines.slice(frontmatterEnd + 1).join("\n"),
|
|
124
|
+
summary: frontmatter.summary || "",
|
|
125
|
+
tags: JSON.parse(frontmatter.tags || "[]"),
|
|
126
|
+
metadata: { layer: "L2", heat: 0, sourceRecords: [] },
|
|
127
|
+
createdAt: frontmatter.created_at || "",
|
|
128
|
+
updatedAt: frontmatter.updated_at || ""
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* L2: Maintain scene index for navigation
|
|
133
|
+
*/
|
|
134
|
+
async updateSceneIndex(scene2) {
|
|
135
|
+
const indexFile = STORAGE_PATHS.index.l2;
|
|
136
|
+
let index = {};
|
|
137
|
+
const existing = await this.readFile(indexFile);
|
|
138
|
+
if (existing) {
|
|
139
|
+
try {
|
|
140
|
+
index = JSON.parse(existing);
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
index[scene2.id] = {
|
|
145
|
+
id: scene2.id,
|
|
146
|
+
title: scene2.title,
|
|
147
|
+
summary: scene2.summary,
|
|
148
|
+
tags: scene2.tags
|
|
149
|
+
};
|
|
150
|
+
await this.writeFile(indexFile, JSON.stringify(index, null, 2));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* L2: Read scene index for navigation
|
|
154
|
+
*/
|
|
155
|
+
async readSceneIndex() {
|
|
156
|
+
const indexFile = STORAGE_PATHS.index.l2;
|
|
157
|
+
const content = await this.readFile(indexFile);
|
|
158
|
+
if (!content) return [];
|
|
159
|
+
try {
|
|
160
|
+
const index = JSON.parse(content);
|
|
161
|
+
return Object.values(index);
|
|
162
|
+
} catch {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// ========== L3 Operations (persona.md) ==========
|
|
167
|
+
/**
|
|
168
|
+
* L3: Write persona file
|
|
169
|
+
*/
|
|
170
|
+
async writePersona(persona) {
|
|
171
|
+
const frontmatter = `---
|
|
172
|
+
id: ${persona.id}
|
|
173
|
+
created_at: ${persona.createdAt}
|
|
174
|
+
updated_at: ${persona.updatedAt}
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
`;
|
|
178
|
+
await this.writeFile(STORAGE_PATHS.l3, frontmatter + persona.content);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* L3: Read persona file
|
|
182
|
+
*/
|
|
183
|
+
async readPersona() {
|
|
184
|
+
const content = await this.readFile(STORAGE_PATHS.l3);
|
|
185
|
+
if (!content) return null;
|
|
186
|
+
const lines = content.split("\n");
|
|
187
|
+
const frontmatterEnd = lines.findIndex((l) => l === "---", 1);
|
|
188
|
+
if (frontmatterEnd <= 1) return null;
|
|
189
|
+
const frontmatter = {};
|
|
190
|
+
for (let i = 1; i < frontmatterEnd; i++) {
|
|
191
|
+
const [key, ...valueParts] = lines[i].split(":");
|
|
192
|
+
if (key && valueParts.length > 0) {
|
|
193
|
+
frontmatter[key.trim()] = valueParts.join(":").trim();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
id: frontmatter.id || "",
|
|
198
|
+
content: lines.slice(frontmatterEnd + 1).join("\n"),
|
|
199
|
+
summary: "",
|
|
200
|
+
metadata: { layer: "L3", sourceScenes: [] },
|
|
201
|
+
createdAt: frontmatter.created_at || "",
|
|
202
|
+
updatedAt: frontmatter.updated_at || ""
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
var RECALL_LINE_SEPARATOR = "\n";
|
|
207
|
+
var MEMORY_TOOLS_GUIDE = `<memory-tools-guide>
|
|
208
|
+
## \u8BB0\u5FC6\u5DE5\u5177\u8C03\u7528\u6307\u5357
|
|
209
|
+
|
|
210
|
+
\u5F53\u4E0A\u65B9\u6CE8\u5165\u7684\u8BB0\u5FC6\u7247\u6BB5\u4E0D\u8DB3\u4EE5\u56DE\u7B54\u7528\u6237\u95EE\u9898\u65F6\uFF0C\u53EF\u4E3B\u52A8\u8C03\u7528\u4EE5\u4E0B\u5DE5\u5177\u83B7\u53D6\u66F4\u591A\u4FE1\u606F\uFF1A
|
|
211
|
+
|
|
212
|
+
- **memory_search**\uFF1A\u641C\u7D22\u7ED3\u6784\u5316\u8BB0\u5FC6\uFF08L1\uFF09\uFF0C\u9002\u7528\u4E8E\u56DE\u5FC6\u7528\u6237\u504F\u597D\u3001\u5386\u53F2\u4E8B\u4EF6\u8282\u70B9\u3001\u89C4\u5219\u7B49\u5173\u952E\u4FE1\u606F\u3002
|
|
213
|
+
- **memory_get**\uFF1A\u83B7\u53D6\u7279\u5B9A\u8BB0\u5FC6\u8BE6\u60C5\u3002
|
|
214
|
+
|
|
215
|
+
### \u8C03\u7528\u6B21\u6570\u9650\u5236
|
|
216
|
+
\u6BCF\u8F6E\u5BF9\u8BDD\u4E2D\uFF0C\u8BB0\u5FC6\u641C\u7D22\u5DE5\u5177**\u5408\u8BA1\u6700\u591A\u8C03\u7528 3 \u6B21**\u3002
|
|
217
|
+
</memory-tools-guide>`;
|
|
218
|
+
function generateSceneNavigation(scenes) {
|
|
219
|
+
if (scenes.length === 0) return "";
|
|
220
|
+
const lines = scenes.map(
|
|
221
|
+
(s) => `- [${s.title}](memory://scene/${s.id}): ${s.summary}`
|
|
222
|
+
);
|
|
223
|
+
return `## \u60C5\u5883\u5BFC\u822A
|
|
224
|
+
${lines.join(RECALL_LINE_SEPARATOR)}`;
|
|
225
|
+
}
|
|
226
|
+
var RecallEngine = class {
|
|
227
|
+
storage;
|
|
228
|
+
options;
|
|
229
|
+
constructor(storage, options = {}) {
|
|
230
|
+
this.storage = storage;
|
|
231
|
+
this.options = {
|
|
232
|
+
hybridSearch: options.hybridSearch ?? true,
|
|
233
|
+
semanticWeight: options.semanticWeight ?? 0.5,
|
|
234
|
+
bm25Weight: options.bm25Weight ?? 0.25,
|
|
235
|
+
entityBoostWeight: options.entityBoostWeight ?? 0.25
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Perform recall: search L1 + read L2 + read L3
|
|
240
|
+
* (Reference: TDB's performAutoRecallCore)
|
|
241
|
+
*
|
|
242
|
+
* Uses hybrid search when vector store is available via hybridSearch option.
|
|
243
|
+
*/
|
|
244
|
+
async recall(params) {
|
|
245
|
+
const { query, sessionKey, topK = 10, vectorStore } = params;
|
|
246
|
+
let memories = [];
|
|
247
|
+
let recallStrategy = "text";
|
|
248
|
+
if (vectorStore && this.options.hybridSearch) {
|
|
249
|
+
const searchResults = await vectorStore.hybridSearch({
|
|
250
|
+
query,
|
|
251
|
+
topK,
|
|
252
|
+
semanticWeight: this.options.semanticWeight,
|
|
253
|
+
bm25Weight: this.options.bm25Weight,
|
|
254
|
+
entityBoostWeight: this.options.entityBoostWeight
|
|
255
|
+
});
|
|
256
|
+
const matchedRecords = [];
|
|
257
|
+
for (const result of searchResults) {
|
|
258
|
+
const records = await this.storage.searchL1("", 100);
|
|
259
|
+
const record = records.find((r) => r.id === result.id);
|
|
260
|
+
if (record) {
|
|
261
|
+
matchedRecords.push(record);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
memories = matchedRecords;
|
|
265
|
+
recallStrategy = "hybrid";
|
|
266
|
+
} else {
|
|
267
|
+
memories = await this.storage.searchL1(query, topK);
|
|
268
|
+
recallStrategy = "text";
|
|
269
|
+
}
|
|
270
|
+
const sceneIndex = await this.storage.readSceneIndex();
|
|
271
|
+
const persona = await this.storage.readPersona();
|
|
272
|
+
let prependContext;
|
|
273
|
+
if (memories.length > 0) {
|
|
274
|
+
const memoryLines = memories.map(
|
|
275
|
+
(m) => `- [${m.type}] ${m.content}`
|
|
276
|
+
);
|
|
277
|
+
prependContext = `<relevant-memories>
|
|
278
|
+
\u4EE5\u4E0B\u662F\u4E0E\u5F53\u524D\u5BF9\u8BDD\u76F8\u5173\u7684\u8BB0\u5FC6\uFF1A
|
|
279
|
+
|
|
280
|
+
${memoryLines.join(RECALL_LINE_SEPARATOR)}
|
|
281
|
+
</relevant-memories>`;
|
|
282
|
+
}
|
|
283
|
+
const stableParts = [];
|
|
284
|
+
if (persona) {
|
|
285
|
+
stableParts.push(`<user-persona>
|
|
286
|
+
${persona.content}
|
|
287
|
+
</user-persona>`);
|
|
288
|
+
}
|
|
289
|
+
if (sceneIndex.length > 0) {
|
|
290
|
+
stableParts.push(`<scene-navigation>
|
|
291
|
+
${generateSceneNavigation(sceneIndex)}
|
|
292
|
+
</scene-navigation>`);
|
|
293
|
+
}
|
|
294
|
+
if (stableParts.length > 0 || prependContext) {
|
|
295
|
+
stableParts.push(MEMORY_TOOLS_GUIDE);
|
|
296
|
+
}
|
|
297
|
+
const appendSystemContext = stableParts.length > 0 ? stableParts.join("\n\n") : void 0;
|
|
298
|
+
return {
|
|
299
|
+
prependContext,
|
|
300
|
+
appendSystemContext,
|
|
301
|
+
recalledL1Memories: memories.map((m) => ({
|
|
302
|
+
content: m.content,
|
|
303
|
+
score: 0.5,
|
|
304
|
+
// TODO: calculate real score
|
|
305
|
+
type: m.type
|
|
306
|
+
})),
|
|
307
|
+
recalledL3Persona: persona?.content ?? null,
|
|
308
|
+
recallStrategy
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
var MemoryStore = class {
|
|
313
|
+
storage;
|
|
314
|
+
recall;
|
|
315
|
+
_vectorStore;
|
|
316
|
+
constructor(baseDir = "~/.openclaw/memory-new") {
|
|
317
|
+
this.storage = new StorageAdapter(baseDir);
|
|
318
|
+
this.recall = new RecallEngine(this.storage);
|
|
319
|
+
}
|
|
320
|
+
// ========== Vector Store (for semantic search) ==========
|
|
321
|
+
get vectorStore() {
|
|
322
|
+
return this._vectorStore;
|
|
323
|
+
}
|
|
324
|
+
setVectorStore(store) {
|
|
325
|
+
this._vectorStore = store;
|
|
326
|
+
}
|
|
327
|
+
// ========== L0 Operations ==========
|
|
328
|
+
async ingestMessage(message) {
|
|
329
|
+
const record = {
|
|
330
|
+
...message,
|
|
331
|
+
id: `l0_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
|
|
332
|
+
recordedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
333
|
+
};
|
|
334
|
+
await this.storage.appendL0(record);
|
|
335
|
+
return record;
|
|
336
|
+
}
|
|
337
|
+
async getMessages(sessionKey, limit = 100) {
|
|
338
|
+
return this.storage.readL0(sessionKey, limit);
|
|
339
|
+
}
|
|
340
|
+
// ========== L1 Operations ==========
|
|
341
|
+
async storeL1(record) {
|
|
342
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
343
|
+
const fullRecord = {
|
|
344
|
+
...record,
|
|
345
|
+
id: `l1_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
|
|
346
|
+
createdAt: now,
|
|
347
|
+
updatedAt: now,
|
|
348
|
+
version: 1
|
|
349
|
+
};
|
|
350
|
+
await this.storage.appendL1(fullRecord);
|
|
351
|
+
return fullRecord;
|
|
352
|
+
}
|
|
353
|
+
async searchL1(query, limit = 10) {
|
|
354
|
+
return this.storage.searchL1(query, limit);
|
|
355
|
+
}
|
|
356
|
+
// ========== L2 Operations ==========
|
|
357
|
+
async storeL2(scene2) {
|
|
358
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
359
|
+
const fullScene = {
|
|
360
|
+
...scene2,
|
|
361
|
+
id: `scene_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
|
|
362
|
+
createdAt: now,
|
|
363
|
+
updatedAt: now
|
|
364
|
+
};
|
|
365
|
+
await this.storage.writeScene(fullScene);
|
|
366
|
+
return fullScene;
|
|
367
|
+
}
|
|
368
|
+
async getScene(sceneId) {
|
|
369
|
+
return this.storage.readScene(sceneId);
|
|
370
|
+
}
|
|
371
|
+
async getSceneIndex() {
|
|
372
|
+
return this.storage.readSceneIndex();
|
|
373
|
+
}
|
|
374
|
+
// ========== L3 Operations ==========
|
|
375
|
+
async storeL3(persona) {
|
|
376
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
377
|
+
const fullPersona = {
|
|
378
|
+
...persona,
|
|
379
|
+
id: `persona_${Date.now()}`,
|
|
380
|
+
createdAt: now,
|
|
381
|
+
updatedAt: now
|
|
382
|
+
};
|
|
383
|
+
await this.storage.writePersona(fullPersona);
|
|
384
|
+
return fullPersona;
|
|
385
|
+
}
|
|
386
|
+
async getPersona() {
|
|
387
|
+
return this.storage.readPersona();
|
|
388
|
+
}
|
|
389
|
+
// ========== Recall ==========
|
|
390
|
+
async recallMemories(query, sessionKey, userId, agentId, topK) {
|
|
391
|
+
return this.recall.recall({ query, sessionKey, userId, agentId, topK });
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// src/vector/vector-store.ts
|
|
396
|
+
var VectorStore = class {
|
|
397
|
+
records = /* @__PURE__ */ new Map();
|
|
398
|
+
embeddingProvider = null;
|
|
399
|
+
dimension = 384;
|
|
400
|
+
constructor(dimension = 384) {
|
|
401
|
+
this.dimension = dimension;
|
|
402
|
+
}
|
|
403
|
+
setEmbeddingProvider(provider) {
|
|
404
|
+
this.embeddingProvider = provider;
|
|
405
|
+
}
|
|
406
|
+
// ========== Record Operations ==========
|
|
407
|
+
async add(record) {
|
|
408
|
+
const fullRecord = {
|
|
409
|
+
...record,
|
|
410
|
+
createdAt: Date.now()
|
|
411
|
+
};
|
|
412
|
+
this.records.set(record.id, fullRecord);
|
|
413
|
+
return fullRecord;
|
|
414
|
+
}
|
|
415
|
+
async get(id) {
|
|
416
|
+
return this.records.get(id) ?? null;
|
|
417
|
+
}
|
|
418
|
+
async delete(id) {
|
|
419
|
+
return this.records.delete(id);
|
|
420
|
+
}
|
|
421
|
+
async update(id, updates) {
|
|
422
|
+
const existing = this.records.get(id);
|
|
423
|
+
if (!existing) return null;
|
|
424
|
+
const updated = { ...existing, ...updates };
|
|
425
|
+
this.records.set(id, updated);
|
|
426
|
+
return updated;
|
|
427
|
+
}
|
|
428
|
+
// ========== Embedding Operations ==========
|
|
429
|
+
async embedContent(content) {
|
|
430
|
+
if (!this.embeddingProvider) {
|
|
431
|
+
return this.fallbackEmbed(content);
|
|
432
|
+
}
|
|
433
|
+
try {
|
|
434
|
+
const embedding = await this.embeddingProvider.embed(content, { inputType: "query" });
|
|
435
|
+
return embedding;
|
|
436
|
+
} catch (error) {
|
|
437
|
+
console.error("Embedding failed, using fallback:", error);
|
|
438
|
+
return this.fallbackEmbed(content);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
async embedBatch(contents) {
|
|
442
|
+
if (!this.embeddingProvider) {
|
|
443
|
+
return contents.map((c) => this.fallbackEmbed(c));
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
return await this.embeddingProvider.embedBatch(contents, { inputType: "document" });
|
|
447
|
+
} catch (error) {
|
|
448
|
+
console.error("Batch embedding failed, using fallback:", error);
|
|
449
|
+
return contents.map((c) => this.fallbackEmbed(c));
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// Fallback pseudo-embedding using hash (for testing without API)
|
|
453
|
+
fallbackEmbed(content) {
|
|
454
|
+
return this.generatePseudoEmbedding(content);
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Generate pseudo-embedding from text content.
|
|
458
|
+
* Used as fallback when no embedding provider is available.
|
|
459
|
+
*/
|
|
460
|
+
generatePseudoEmbedding(content) {
|
|
461
|
+
const embedding = new Array(this.dimension).fill(0);
|
|
462
|
+
let hash = 0;
|
|
463
|
+
for (let i = 0; i < content.length; i++) {
|
|
464
|
+
hash = (hash << 5) - hash + content.charCodeAt(i);
|
|
465
|
+
hash = hash & hash;
|
|
466
|
+
}
|
|
467
|
+
const seed = Math.abs(hash);
|
|
468
|
+
for (let i = 0; i < this.dimension; i++) {
|
|
469
|
+
const charCode = content.charCodeAt(i % content.length) || 1;
|
|
470
|
+
embedding[i] = Math.sin(seed * (i + 1) * charCode) * 0.5 + 0.5;
|
|
471
|
+
}
|
|
472
|
+
const magnitude = Math.sqrt(embedding.reduce((sum, v) => sum + v * v, 0));
|
|
473
|
+
if (magnitude > 0) {
|
|
474
|
+
for (let i = 0; i < embedding.length; i++) {
|
|
475
|
+
embedding[i] /= magnitude;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return embedding;
|
|
479
|
+
}
|
|
480
|
+
// ========== Vector Search ==========
|
|
481
|
+
async searchByVector(queryEmbedding, topK, minScore = 0) {
|
|
482
|
+
const results = [];
|
|
483
|
+
for (const record of this.records.values()) {
|
|
484
|
+
if (record.embedding.length !== queryEmbedding.length) continue;
|
|
485
|
+
const score = this.cosineSimilarity(queryEmbedding, record.embedding);
|
|
486
|
+
if (score >= minScore) {
|
|
487
|
+
results.push({
|
|
488
|
+
id: record.id,
|
|
489
|
+
content: record.content,
|
|
490
|
+
score,
|
|
491
|
+
metadata: record.metadata
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
results.sort((a, b) => b.score - a.score);
|
|
496
|
+
return results.slice(0, topK);
|
|
497
|
+
}
|
|
498
|
+
// ========== Hybrid Search ==========
|
|
499
|
+
async hybridSearch(options, getTextScore = bm25Score) {
|
|
500
|
+
const { query, topK, semanticWeight, bm25Weight, entityBoostWeight, minScore = 0.1 } = options;
|
|
501
|
+
let queryEmbedding = options.queryEmbedding;
|
|
502
|
+
if (!queryEmbedding) {
|
|
503
|
+
queryEmbedding = await this.embedContent(query);
|
|
504
|
+
}
|
|
505
|
+
const semanticResults = await this.searchByVector(queryEmbedding, topK * 2, 0);
|
|
506
|
+
const bm25Results = this.bm25Search(query, topK * 2);
|
|
507
|
+
const entities = this.extractEntities(query);
|
|
508
|
+
const entityBoostResults = this.entityBoostSearch(entities, topK * 2);
|
|
509
|
+
const scoreMap = /* @__PURE__ */ new Map();
|
|
510
|
+
for (const result of semanticResults) {
|
|
511
|
+
const semanticScore = result.score * semanticWeight;
|
|
512
|
+
scoreMap.set(result.id, {
|
|
513
|
+
id: result.id,
|
|
514
|
+
content: result.content,
|
|
515
|
+
score: semanticScore,
|
|
516
|
+
metadata: result.metadata
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
for (const result of bm25Results) {
|
|
520
|
+
const existing = scoreMap.get(result.id);
|
|
521
|
+
const bm25Contribution = result.score * bm25Weight;
|
|
522
|
+
if (existing) {
|
|
523
|
+
existing.score += bm25Contribution;
|
|
524
|
+
} else {
|
|
525
|
+
scoreMap.set(result.id, {
|
|
526
|
+
id: result.id,
|
|
527
|
+
content: result.content,
|
|
528
|
+
score: bm25Contribution,
|
|
529
|
+
metadata: result.metadata
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
for (const result of entityBoostResults) {
|
|
534
|
+
const existing = scoreMap.get(result.id);
|
|
535
|
+
const boostContribution = result.score * entityBoostWeight;
|
|
536
|
+
if (existing) {
|
|
537
|
+
existing.score += boostContribution;
|
|
538
|
+
} else {
|
|
539
|
+
scoreMap.set(result.id, {
|
|
540
|
+
id: result.id,
|
|
541
|
+
content: result.content,
|
|
542
|
+
score: boostContribution,
|
|
543
|
+
metadata: result.metadata
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const finalResults = Array.from(scoreMap.values()).filter((r) => r.score >= minScore).sort((a, b) => b.score - a.score).slice(0, topK);
|
|
548
|
+
return finalResults;
|
|
549
|
+
}
|
|
550
|
+
// ========== BM25 (in-memory simplified) ==========
|
|
551
|
+
bm25Search(query, topK) {
|
|
552
|
+
const queryTerms = query.toLowerCase().split(/\s+/);
|
|
553
|
+
const results = [];
|
|
554
|
+
const avgDocLen = this.getAverageDocLength();
|
|
555
|
+
const k1 = 1.5;
|
|
556
|
+
const b = 0.75;
|
|
557
|
+
for (const record of this.records.values()) {
|
|
558
|
+
const terms = record.content.toLowerCase().split(/\s+/);
|
|
559
|
+
let score = 0;
|
|
560
|
+
for (const term of queryTerms) {
|
|
561
|
+
const tf = terms.filter((t) => t === term).length;
|
|
562
|
+
if (tf > 0) {
|
|
563
|
+
const idf = Math.log((this.records.size + 1) / 2);
|
|
564
|
+
const docLen = terms.length;
|
|
565
|
+
const numerator = tf * (k1 + 1);
|
|
566
|
+
const denominator = tf + k1 * (1 - b + b * (docLen / avgDocLen));
|
|
567
|
+
score += idf * (numerator / denominator);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (score > 0) {
|
|
571
|
+
results.push({
|
|
572
|
+
id: record.id,
|
|
573
|
+
content: record.content,
|
|
574
|
+
score,
|
|
575
|
+
metadata: record.metadata
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
results.sort((a, b2) => b2.score - a.score);
|
|
580
|
+
return results.slice(0, topK);
|
|
581
|
+
}
|
|
582
|
+
getAverageDocLength() {
|
|
583
|
+
if (this.records.size === 0) return 1;
|
|
584
|
+
let total = 0;
|
|
585
|
+
for (const record of this.records.values()) {
|
|
586
|
+
total += record.content.split(/\s+/).length;
|
|
587
|
+
}
|
|
588
|
+
return total / this.records.size;
|
|
589
|
+
}
|
|
590
|
+
// ========== Entity Extraction & Boost ==========
|
|
591
|
+
extractEntities(query) {
|
|
592
|
+
const entities = [];
|
|
593
|
+
const capitalizedPattern = /[A-Z][a-z]+/g;
|
|
594
|
+
let match;
|
|
595
|
+
while ((match = capitalizedPattern.exec(query)) !== null) {
|
|
596
|
+
entities.push(match[0].toLowerCase());
|
|
597
|
+
}
|
|
598
|
+
const quotedPattern = /"([^"]+)"|'([^']+)'/g;
|
|
599
|
+
while ((match = quotedPattern.exec(query)) !== null) {
|
|
600
|
+
const entity = match[1] || match[2];
|
|
601
|
+
entities.push(entity.toLowerCase());
|
|
602
|
+
}
|
|
603
|
+
return [...new Set(entities)];
|
|
604
|
+
}
|
|
605
|
+
entityBoostSearch(entities, topK) {
|
|
606
|
+
if (entities.length === 0) return [];
|
|
607
|
+
const results = [];
|
|
608
|
+
for (const record of this.records.values()) {
|
|
609
|
+
const content = record.content.toLowerCase();
|
|
610
|
+
let matchCount = 0;
|
|
611
|
+
for (const entity of entities) {
|
|
612
|
+
if (content.includes(entity)) {
|
|
613
|
+
matchCount++;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (matchCount > 0) {
|
|
617
|
+
const score = matchCount / entities.length;
|
|
618
|
+
results.push({
|
|
619
|
+
id: record.id,
|
|
620
|
+
content: record.content,
|
|
621
|
+
score,
|
|
622
|
+
metadata: record.metadata
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
results.sort((a, b) => b.score - a.score);
|
|
627
|
+
return results.slice(0, topK);
|
|
628
|
+
}
|
|
629
|
+
// ========== Utilities ==========
|
|
630
|
+
cosineSimilarity(a, b) {
|
|
631
|
+
if (a.length !== b.length) return 0;
|
|
632
|
+
let dotProduct = 0;
|
|
633
|
+
let normA = 0;
|
|
634
|
+
let normB = 0;
|
|
635
|
+
for (let i = 0; i < a.length; i++) {
|
|
636
|
+
dotProduct += a[i] * b[i];
|
|
637
|
+
normA += a[i] * a[i];
|
|
638
|
+
normB += b[i] * b[i];
|
|
639
|
+
}
|
|
640
|
+
const denominator = Math.sqrt(normA) * Math.sqrt(normB);
|
|
641
|
+
if (denominator === 0) return 0;
|
|
642
|
+
return dotProduct / denominator;
|
|
643
|
+
}
|
|
644
|
+
async getAll() {
|
|
645
|
+
return Array.from(this.records.values());
|
|
646
|
+
}
|
|
647
|
+
async count() {
|
|
648
|
+
return this.records.size;
|
|
649
|
+
}
|
|
650
|
+
async clear() {
|
|
651
|
+
this.records.clear();
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
function bm25Score(content, query) {
|
|
655
|
+
const queryTerms = query.toLowerCase().split(/\s+/);
|
|
656
|
+
const terms = content.toLowerCase().split(/\s+/);
|
|
657
|
+
let score = 0;
|
|
658
|
+
for (const term of queryTerms) {
|
|
659
|
+
const tf = terms.filter((t) => t === term).length;
|
|
660
|
+
if (tf > 0) {
|
|
661
|
+
score += 1 + Math.log(1 + tf);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return score;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// src/pipeline/distillation.ts
|
|
668
|
+
var DEFAULT_DISTILLATION_CONFIG = {
|
|
669
|
+
l1: {
|
|
670
|
+
messageThreshold: 5,
|
|
671
|
+
idleSeconds: 60,
|
|
672
|
+
batchSize: 10,
|
|
673
|
+
enableDedup: true,
|
|
674
|
+
maxMemoriesPerSession: 50
|
|
675
|
+
},
|
|
676
|
+
l2: {
|
|
677
|
+
minIntervalMs: 15 * 60 * 1e3,
|
|
678
|
+
maxIntervalMs: 60 * 60 * 1e3,
|
|
679
|
+
topicThreshold: 3,
|
|
680
|
+
delayAfterL1Seconds: 90
|
|
681
|
+
},
|
|
682
|
+
l3: {
|
|
683
|
+
conditions: ["explicit_request", "cold_start", "restore", "first_scene", "threshold"],
|
|
684
|
+
importanceThreshold: 0.6
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
var L1_EXTRACTION_SYSTEM_PROMPT = `\u4F60\u662F\u4E13\u4E1A\u7684"\u5DE5\u4F5C\u60C5\u5883\u5207\u5206\u4E0E\u56E2\u961F\u5171\u4EAB\u8BB0\u5FC6\u63D0\u53D6\u4E13\u5BB6"\u3002
|
|
688
|
+
\u4F60\u7684\u4EFB\u52A1\u662F\u5206\u6790\u5DE5\u4F5C\u6D88\u606F\uFF0C\u5224\u65AD\u5DE5\u4F5C\u60C5\u5883\u5207\u6362\uFF0C\u5E76\u4ECE\u4E2D\u63D0\u53D6\u53EF\u5728\u56E2\u961F\u5185\u5171\u4EAB\u7684\u7ED3\u6784\u5316\u5DE5\u4F5C\u8BB0\u5FC6\u3002
|
|
689
|
+
|
|
690
|
+
## \u8F93\u51FA\u8981\u6C42
|
|
691
|
+
|
|
692
|
+
\u4E25\u683C\u6309\u4EE5\u4E0BJSON\u6570\u7EC4\u683C\u5F0F\u8F93\u51FA\uFF0C\u4E0D\u8981\u8F93\u51FA\u4EFB\u4F55\u989D\u5916\u7684 Markdown \u4EE3\u7801\u5757\u4FEE\u9970\u7B26\uFF08\u5982 \`\`\`json\uFF09\u6216\u89E3\u91CA\u6587\u672C\uFF1A
|
|
693
|
+
|
|
694
|
+
[
|
|
695
|
+
{
|
|
696
|
+
"scene_name": "\u60C5\u5883\u540D\u79F0\uFF08\u7B80\u6D01\uFF0C1-10\u4E2A\u5B57\uFF09",
|
|
697
|
+
"memories": [
|
|
698
|
+
{
|
|
699
|
+
"content": "\u8BB0\u5FC6\u5185\u5BB9\uFF08\u5B8C\u6574\u53E5\u5B50\uFF0C20-200\u5B57\uFF09",
|
|
700
|
+
"type": "persona | episodic | instruction",
|
|
701
|
+
"priority": \u4F18\u5148\u7EA7(0-100, \u8D8A\u9AD8\u8D8A\u91CD\u8981),
|
|
702
|
+
"source_message_ids": ["\u76F8\u5173\u6D88\u606FID"],
|
|
703
|
+
"metadata": {}
|
|
704
|
+
}
|
|
705
|
+
]
|
|
706
|
+
}
|
|
707
|
+
]
|
|
708
|
+
|
|
709
|
+
## Memory Type \u5B9A\u4E49
|
|
710
|
+
|
|
711
|
+
- **persona**: \u5173\u4E8E\u7528\u6237\u504F\u597D\u3001\u4E60\u60EF\u3001\u5DE5\u4F5C\u65B9\u5F0F\u7684\u8BB0\u5FC6\uFF08\u5982"\u7528\u6237\u559C\u6B22\u5728\u4E0A\u5348\u5904\u7406\u590D\u6742\u4EFB\u52A1"\uFF09
|
|
712
|
+
- **episodic**: \u5177\u4F53\u7684\u9879\u76EE\u4E8B\u4EF6\u3001\u51B3\u7B56\u3001\u8BA8\u8BBA\u8981\u70B9\uFF08\u5982"\u9879\u76EEX\u51B3\u5B9A\u4F7F\u7528\u5FAE\u670D\u52A1\u67B6\u6784"\uFF09
|
|
713
|
+
- **instruction**: \u7528\u6237\u7684\u660E\u786E\u6307\u4EE4\u6216\u9700\u6C42\uFF08\u5982"\u7528\u6237\u8981\u6C42\u6BCF\u5468\u4E94\u540C\u6B65\u8FDB\u5EA6"\uFF09
|
|
714
|
+
|
|
715
|
+
## \u573A\u666F\u5207\u6362\u5224\u65AD
|
|
716
|
+
|
|
717
|
+
\u5F53\u51FA\u73B0\u4EE5\u4E0B\u60C5\u51B5\u65F6\uFF0C\u5E94\u8BE5\u5207\u6362\u5230\u65B0\u573A\u666F\uFF1A
|
|
718
|
+
1. \u8BDD\u9898\u53D1\u751F\u5B9E\u8D28\u6027\u53D8\u5316
|
|
719
|
+
2. \u53C2\u4E0E\u4EBA\u5458\u53D1\u751F\u660E\u663E\u53D8\u5316
|
|
720
|
+
3. \u4EFB\u52A1\u76EE\u6807\u53D1\u751F\u5207\u6362
|
|
721
|
+
4. \u65F6\u95F4\u95F4\u9694\u8D85\u8FC730\u5206\u949F`;
|
|
722
|
+
function formatExtractionPrompt(newMessages, backgroundMessages, previousSceneName) {
|
|
723
|
+
const bgText = backgroundMessages.length > 0 ? backgroundMessages.map((m) => `[${m.id}] [${m.role}] [${new Date(m.timestamp).toISOString()}]: ${m.content}`).join("\n\n") : "\u65E0";
|
|
724
|
+
const newText = newMessages.map((m) => `[${m.id}] [${m.role}] [${new Date(m.timestamp).toISOString()}]: ${m.content}`).join("\n\n");
|
|
725
|
+
const userPrompt = `**\u8F93\u51FA\u8BED\u8A00**\uFF1A\u6839\u636E\u4E0B\u65B9"\u5F85\u63D0\u53D6\u7684\u65B0\u6D88\u606F"\u4E2D user \u53D1\u8A00\u7684\u4E3B\u5BFC\u8BED\u8A00\u4E66\u5199 \`scene_name\` \u548C memory \`content\`\u3002
|
|
726
|
+
|
|
727
|
+
\u3010\u4E0A\u4E00\u4E2A\u60C5\u5883\u3011\uFF1A${previousSceneName || "\u65E0"}
|
|
728
|
+
|
|
729
|
+
\u3010\u80CC\u666F\u5BF9\u8BDD\u3011\uFF08\u4EC5\u4F9B\u7406\u89E3\u4E0A\u4E0B\u6587\u63A8\u65AD\u5173\u7CFB/\u65F6\u95F4\uFF0C\u4E25\u7981\u4ECE\u4E2D\u63D0\u53D6\u8BB0\u5FC6\uFF09\uFF1A
|
|
730
|
+
${bgText}
|
|
731
|
+
|
|
732
|
+
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
733
|
+
|
|
734
|
+
\u3010\u5F85\u63D0\u53D6\u7684\u65B0\u6D88\u606F\u3011\uFF08\u52A1\u5FC5\u7ED3\u5408 timestamp \u63A8\u7B97\u65F6\u95F4\uFF0C\u53EA\u4ECE\u8FD9\u91CC\u63D0\u53D6\u8BB0\u5FC6\uFF01\uFF09\uFF1A
|
|
735
|
+
${newText}`;
|
|
736
|
+
return {
|
|
737
|
+
systemPrompt: L1_EXTRACTION_SYSTEM_PROMPT,
|
|
738
|
+
userPrompt
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
var DistillationPipeline = class {
|
|
742
|
+
config;
|
|
743
|
+
store;
|
|
744
|
+
vectorStore;
|
|
745
|
+
llmRunner;
|
|
746
|
+
subagentRunner;
|
|
747
|
+
lastL1At = null;
|
|
748
|
+
lastL2At = null;
|
|
749
|
+
lastL3At = null;
|
|
750
|
+
pendingL1Extraction = null;
|
|
751
|
+
// In-memory buffers (TDB uses VectorStore + JSONL)
|
|
752
|
+
messageBuffer = [];
|
|
753
|
+
constructor(config = DEFAULT_DISTILLATION_CONFIG, store, vectorStore, llmRunner) {
|
|
754
|
+
this.config = config;
|
|
755
|
+
this.store = store || new MemoryStore();
|
|
756
|
+
this.vectorStore = vectorStore || new VectorStore();
|
|
757
|
+
this.llmRunner = llmRunner;
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Set LLM runner for simple prompt-completion style extraction
|
|
761
|
+
*/
|
|
762
|
+
setLLMRunner(runner) {
|
|
763
|
+
this.llmRunner = runner;
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Set subagent runner for L1 extraction via OpenClaw subagent runtime
|
|
767
|
+
*/
|
|
768
|
+
setSubagentRunner(runner) {
|
|
769
|
+
this.subagentRunner = runner;
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Set vector store for embedding-based search
|
|
773
|
+
*/
|
|
774
|
+
setVectorStore(store) {
|
|
775
|
+
this.vectorStore = store;
|
|
776
|
+
}
|
|
777
|
+
// ============================================================================
|
|
778
|
+
// Ingestion (L0)
|
|
779
|
+
// ============================================================================
|
|
780
|
+
/**
|
|
781
|
+
* Ingest messages into L0 layer (TDB's captureAtomic pattern)
|
|
782
|
+
*/
|
|
783
|
+
async ingest(messages) {
|
|
784
|
+
const records = [];
|
|
785
|
+
for (const msg of messages) {
|
|
786
|
+
if (!shouldExtractL1(msg.content)) continue;
|
|
787
|
+
const record = await this.store.ingestMessage(msg);
|
|
788
|
+
records.push(record);
|
|
789
|
+
this.messageBuffer.push(record);
|
|
790
|
+
}
|
|
791
|
+
if (this.messageBuffer.length >= this.config.l1.messageThreshold) {
|
|
792
|
+
await this.triggerL1Extraction();
|
|
793
|
+
} else {
|
|
794
|
+
this.scheduleL1Extraction();
|
|
795
|
+
}
|
|
796
|
+
return records;
|
|
797
|
+
}
|
|
798
|
+
scheduleL1Extraction() {
|
|
799
|
+
if (this.pendingL1Extraction) return;
|
|
800
|
+
this.pendingL1Extraction = setTimeout(async () => {
|
|
801
|
+
this.pendingL1Extraction = null;
|
|
802
|
+
if (this.messageBuffer.length > 0) {
|
|
803
|
+
await this.distill("L1");
|
|
804
|
+
}
|
|
805
|
+
}, this.config.l1.idleSeconds * 1e3);
|
|
806
|
+
}
|
|
807
|
+
async triggerL1Extraction() {
|
|
808
|
+
if (this.pendingL1Extraction) {
|
|
809
|
+
clearTimeout(this.pendingL1Extraction);
|
|
810
|
+
this.pendingL1Extraction = null;
|
|
811
|
+
}
|
|
812
|
+
await this.distill("L1");
|
|
813
|
+
}
|
|
814
|
+
// ============================================================================
|
|
815
|
+
// Distillation
|
|
816
|
+
// ============================================================================
|
|
817
|
+
async distill(stage) {
|
|
818
|
+
switch (stage) {
|
|
819
|
+
case "L1":
|
|
820
|
+
return this.runL1Distillation();
|
|
821
|
+
case "L2":
|
|
822
|
+
return this.runL2Distillation();
|
|
823
|
+
case "L3":
|
|
824
|
+
return this.runL3Distillation();
|
|
825
|
+
default:
|
|
826
|
+
return { stage, produced: 0, errors: ["Unknown stage"] };
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* L1 Distillation: Extract atomic memories from L0 messages
|
|
831
|
+
* (Reference: TDB's extractL1Memories)
|
|
832
|
+
*/
|
|
833
|
+
async runL1Distillation() {
|
|
834
|
+
const errors = [];
|
|
835
|
+
let produced = 0;
|
|
836
|
+
try {
|
|
837
|
+
const messages = this.messageBuffer.slice(-this.config.l1.batchSize);
|
|
838
|
+
if (messages.length === 0) {
|
|
839
|
+
return { stage: "L1", produced: 0, errors: [] };
|
|
840
|
+
}
|
|
841
|
+
const lastScene = await this.getLastSceneName();
|
|
842
|
+
const maxNew = 5;
|
|
843
|
+
const newMessages = messages.slice(-maxNew);
|
|
844
|
+
const backgroundMessages = messages.slice(0, -maxNew);
|
|
845
|
+
const { systemPrompt, userPrompt } = formatExtractionPrompt(
|
|
846
|
+
newMessages,
|
|
847
|
+
backgroundMessages,
|
|
848
|
+
lastScene
|
|
849
|
+
);
|
|
850
|
+
let extractionOutput = "";
|
|
851
|
+
if (this.subagentRunner) {
|
|
852
|
+
const fullPrompt = `${systemPrompt}
|
|
853
|
+
|
|
854
|
+
${userPrompt}`;
|
|
855
|
+
extractionOutput = await this.subagentRunner(fullPrompt);
|
|
856
|
+
} else if (this.llmRunner) {
|
|
857
|
+
extractionOutput = await this.llmRunner(systemPrompt, userPrompt);
|
|
858
|
+
} else {
|
|
859
|
+
extractionOutput = this.fallbackExtract(messages, lastScene);
|
|
860
|
+
}
|
|
861
|
+
const extractedMemories = parseExtractionOutput(extractionOutput);
|
|
862
|
+
for (const mem of extractedMemories) {
|
|
863
|
+
await this.store.storeL1({
|
|
864
|
+
content: mem.content,
|
|
865
|
+
type: mem.type,
|
|
866
|
+
priority: mem.priority,
|
|
867
|
+
sceneName: mem.scene_name,
|
|
868
|
+
sourceMessageIds: mem.source_message_ids,
|
|
869
|
+
metadata: mem.metadata,
|
|
870
|
+
timestamps: messages.map((m) => new Date(m.timestamp).toISOString()),
|
|
871
|
+
sessionKey: messages[0]?.sessionKey || "default",
|
|
872
|
+
sessionId: messages[0]?.sessionId || "",
|
|
873
|
+
teamId: messages[0]?.teamId,
|
|
874
|
+
userId: messages[0]?.userId || "",
|
|
875
|
+
agentId: messages[0]?.agentId || ""
|
|
876
|
+
});
|
|
877
|
+
produced++;
|
|
878
|
+
}
|
|
879
|
+
this.messageBuffer = this.messageBuffer.slice(0, Math.max(0, this.messageBuffer.length - messages.length));
|
|
880
|
+
this.lastL1At = Date.now();
|
|
881
|
+
setTimeout(() => this.distill("L2"), this.config.l2.delayAfterL1Seconds * 1e3);
|
|
882
|
+
} catch (e) {
|
|
883
|
+
errors.push(String(e));
|
|
884
|
+
}
|
|
885
|
+
return { stage: "L1", produced, errors };
|
|
886
|
+
}
|
|
887
|
+
async getLastSceneName() {
|
|
888
|
+
const recent = await this.store.searchL1("", 1);
|
|
889
|
+
return recent[0]?.sceneName || "\u65E0";
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* L2 Distillation: Cluster L1 memories into scene blocks
|
|
893
|
+
* (Reference: TDB's SceneExtractor)
|
|
894
|
+
*/
|
|
895
|
+
async runL2Distillation() {
|
|
896
|
+
const errors = [];
|
|
897
|
+
let produced = 0;
|
|
898
|
+
try {
|
|
899
|
+
const now = Date.now();
|
|
900
|
+
if (this.lastL2At && now - this.lastL2At < this.config.l2.minIntervalMs) {
|
|
901
|
+
return { stage: "L2", produced: 0, errors: ["Too soon since last L2"] };
|
|
902
|
+
}
|
|
903
|
+
const l1Records = await this.store.searchL1("", 100);
|
|
904
|
+
if (l1Records.length < this.config.l2.topicThreshold) {
|
|
905
|
+
return { stage: "L2", produced: 0, errors: ["Not enough L1 records"] };
|
|
906
|
+
}
|
|
907
|
+
const sceneGroups = /* @__PURE__ */ new Map();
|
|
908
|
+
for (const record of l1Records) {
|
|
909
|
+
if (!sceneGroups.has(record.sceneName)) {
|
|
910
|
+
sceneGroups.set(record.sceneName, []);
|
|
911
|
+
}
|
|
912
|
+
sceneGroups.get(record.sceneName).push(record);
|
|
913
|
+
}
|
|
914
|
+
for (const [sceneName, records] of sceneGroups) {
|
|
915
|
+
if (records.length < this.config.l2.topicThreshold) continue;
|
|
916
|
+
const avgPriority = records.reduce((sum, r) => sum + r.priority, 0) / records.length;
|
|
917
|
+
const content = this.buildSceneContent(sceneName, records);
|
|
918
|
+
await this.store.storeL2({
|
|
919
|
+
title: sceneName,
|
|
920
|
+
content,
|
|
921
|
+
summary: `\u5E73\u5747\u4F18\u5148\u7EA7: ${avgPriority.toFixed(0)}`,
|
|
922
|
+
tags: [sceneName],
|
|
923
|
+
metadata: {
|
|
924
|
+
layer: "L2",
|
|
925
|
+
heat: records.length,
|
|
926
|
+
sourceRecords: records.map((r) => r.id)
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
produced++;
|
|
930
|
+
}
|
|
931
|
+
this.lastL2At = now;
|
|
932
|
+
setTimeout(() => this.distill("L3"), 5e3);
|
|
933
|
+
} catch (e) {
|
|
934
|
+
errors.push(String(e));
|
|
935
|
+
}
|
|
936
|
+
return { stage: "L2", produced, errors };
|
|
937
|
+
}
|
|
938
|
+
buildSceneContent(sceneName, records) {
|
|
939
|
+
const points = records.map((r) => `- [${r.type}] ${r.content}`).join("\n");
|
|
940
|
+
const avgPriority = records.reduce((sum, r) => sum + r.priority, 0) / records.length;
|
|
941
|
+
return `# ${sceneName}
|
|
942
|
+
|
|
943
|
+
## Key Points
|
|
944
|
+
${points}
|
|
945
|
+
|
|
946
|
+
## Summary
|
|
947
|
+
\u5E73\u5747\u4F18\u5148\u7EA7: ${avgPriority.toFixed(0)}
|
|
948
|
+
\u5171 ${records.length} \u6761\u76F8\u5173\u8BB0\u5FC6
|
|
949
|
+
|
|
950
|
+
---
|
|
951
|
+
Generated: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
952
|
+
`;
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* L3 Distillation: Build persona from high-value scenes
|
|
956
|
+
* (Reference: TDB's PersonaExtractor)
|
|
957
|
+
*/
|
|
958
|
+
async runL3Distillation() {
|
|
959
|
+
const errors = [];
|
|
960
|
+
let produced = 0;
|
|
961
|
+
try {
|
|
962
|
+
const l1Records = await this.store.searchL1("", 100);
|
|
963
|
+
if (l1Records.length === 0) {
|
|
964
|
+
return { stage: "L3", produced: 0, errors: ["No L1 records"] };
|
|
965
|
+
}
|
|
966
|
+
const avgImportance = l1Records.reduce((sum, r) => sum + r.priority, 0) / l1Records.length / 100;
|
|
967
|
+
if (avgImportance < this.config.l3.importanceThreshold) {
|
|
968
|
+
return { stage: "L3", produced: 0, errors: ["Avg importance below threshold"] };
|
|
969
|
+
}
|
|
970
|
+
const sceneIndex = await this.store.getSceneIndex();
|
|
971
|
+
const highValueScenes = sceneIndex.filter((s) => {
|
|
972
|
+
const avg = l1Records.filter((r) => r.sceneName === s.title).reduce((sum, r) => sum + r.priority, 0) / Math.max(1, l1Records.filter((r) => r.sceneName === s.title).length);
|
|
973
|
+
return avg >= this.config.l3.importanceThreshold * 100;
|
|
974
|
+
});
|
|
975
|
+
if (highValueScenes.length === 0) {
|
|
976
|
+
return { stage: "L3", produced: 0, errors: ["No high-value scenes"] };
|
|
977
|
+
}
|
|
978
|
+
const personaContent = this.buildPersona(highValueScenes, l1Records);
|
|
979
|
+
await this.store.storeL3({
|
|
980
|
+
content: personaContent,
|
|
981
|
+
summary: "Agent Self-Model",
|
|
982
|
+
metadata: {
|
|
983
|
+
layer: "L3",
|
|
984
|
+
sourceScenes: highValueScenes.map((s) => s.id)
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
produced = 1;
|
|
988
|
+
this.lastL3At = Date.now();
|
|
989
|
+
} catch (e) {
|
|
990
|
+
errors.push(String(e));
|
|
991
|
+
}
|
|
992
|
+
return { stage: "L3", produced, errors };
|
|
993
|
+
}
|
|
994
|
+
buildPersona(scenes, l1Records) {
|
|
995
|
+
const sceneContents = scenes.map((scene2) => {
|
|
996
|
+
const relatedMemories = l1Records.filter((r) => r.sceneName === scene2.title);
|
|
997
|
+
return `### ${scene2.title}
|
|
998
|
+
${relatedMemories.map((m) => `- ${m.content}`).join("\n")}`;
|
|
999
|
+
}).join("\n\n");
|
|
1000
|
+
const avgImportance = l1Records.reduce((sum, r) => sum + r.priority, 0) / l1Records.length / 100;
|
|
1001
|
+
return `# Agent Self-Model
|
|
1002
|
+
|
|
1003
|
+
## Core Knowledge
|
|
1004
|
+
${sceneContents}
|
|
1005
|
+
|
|
1006
|
+
## Behavioral Patterns
|
|
1007
|
+
- \u5171 ${scenes.length} \u4E2A\u9AD8\u4EF7\u503C\u573A\u666F
|
|
1008
|
+
- \u5E73\u5747\u91CD\u8981\u6027: ${avgImportance.toFixed(2)}
|
|
1009
|
+
|
|
1010
|
+
## Preferences
|
|
1011
|
+
(\u4ECE persona \u7C7B\u578B\u8BB0\u5FC6\u4E2D\u63D0\u53D6)
|
|
1012
|
+
|
|
1013
|
+
## Communication Style
|
|
1014
|
+
(\u4ECE\u4EA4\u4E92\u6A21\u5F0F\u4E2D\u5B66\u4E60)
|
|
1015
|
+
|
|
1016
|
+
---
|
|
1017
|
+
Generated: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1018
|
+
`;
|
|
1019
|
+
}
|
|
1020
|
+
// ============================================================================
|
|
1021
|
+
// Utilities
|
|
1022
|
+
// ============================================================================
|
|
1023
|
+
fallbackExtract(messages, previousSceneName) {
|
|
1024
|
+
const contents = messages.map((m) => m.content).join("\n");
|
|
1025
|
+
const facts = [];
|
|
1026
|
+
const patterns = [
|
|
1027
|
+
/(?:decided|决定)(.+)/gi,
|
|
1028
|
+
/(?:learned|学习)(.+)/gi,
|
|
1029
|
+
/(?:remember|记住)(.+)/gi
|
|
1030
|
+
];
|
|
1031
|
+
for (const pattern of patterns) {
|
|
1032
|
+
let match;
|
|
1033
|
+
while ((match = pattern.exec(contents)) !== null) {
|
|
1034
|
+
facts.push(match[1].trim());
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
if (facts.length === 0 && messages[0]) {
|
|
1038
|
+
const content = messages[0].content;
|
|
1039
|
+
if (content.length > 20) {
|
|
1040
|
+
facts.push(content.slice(0, 100));
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
const sceneName = previousSceneName || "General";
|
|
1044
|
+
return JSON.stringify([{
|
|
1045
|
+
scene_name: sceneName,
|
|
1046
|
+
memories: facts.slice(0, 3).map((content, i) => ({
|
|
1047
|
+
content,
|
|
1048
|
+
type: "episodic",
|
|
1049
|
+
priority: 50 + i * 10,
|
|
1050
|
+
source_message_ids: [messages[0]?.id || "unknown"],
|
|
1051
|
+
metadata: {}
|
|
1052
|
+
}))
|
|
1053
|
+
}]);
|
|
1054
|
+
}
|
|
1055
|
+
checkTriggers() {
|
|
1056
|
+
const now = Date.now();
|
|
1057
|
+
return {
|
|
1058
|
+
l1: this.messageBuffer.length >= this.config.l1.messageThreshold,
|
|
1059
|
+
l2: this.lastL2At ? now - this.lastL2At >= this.config.l2.minIntervalMs : this.messageBuffer.length >= this.config.l2.topicThreshold,
|
|
1060
|
+
l3: this.lastL3At === null
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
async getStats() {
|
|
1064
|
+
const l1Records = await this.store.searchL1("", 1e3);
|
|
1065
|
+
const l2Scenes = await this.store.getSceneIndex();
|
|
1066
|
+
const persona = await this.store.getPersona();
|
|
1067
|
+
return {
|
|
1068
|
+
l0Count: this.messageBuffer.length,
|
|
1069
|
+
l1Count: l1Records.length,
|
|
1070
|
+
l2Count: l2Scenes.length,
|
|
1071
|
+
l3Count: persona ? 1 : 0
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
function shouldExtractL1(content) {
|
|
1076
|
+
if (content.length < 10) return false;
|
|
1077
|
+
if (content.length > 1e4) return false;
|
|
1078
|
+
return true;
|
|
1079
|
+
}
|
|
1080
|
+
function parseExtractionOutput(output) {
|
|
1081
|
+
try {
|
|
1082
|
+
const jsonMatch = output.match(/\[[\s\S]*\]/);
|
|
1083
|
+
if (!jsonMatch) {
|
|
1084
|
+
const objMatch = output.match(/\{[\s\S]*\}/);
|
|
1085
|
+
if (objMatch) {
|
|
1086
|
+
return JSON.parse(objMatch[0]);
|
|
1087
|
+
}
|
|
1088
|
+
return [];
|
|
1089
|
+
}
|
|
1090
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
1091
|
+
if (parsed.scene_name) {
|
|
1092
|
+
return (parsed.memories || []).map((m) => ({
|
|
1093
|
+
content: m.content,
|
|
1094
|
+
type: normalizeMemoryType(m.type),
|
|
1095
|
+
priority: Math.min(100, Math.max(0, Number(m.priority) || 50)),
|
|
1096
|
+
source_message_ids: Array.isArray(m.source_message_ids) ? m.source_message_ids : [],
|
|
1097
|
+
metadata: m.metadata || {},
|
|
1098
|
+
scene_name: parsed.scene_name
|
|
1099
|
+
}));
|
|
1100
|
+
}
|
|
1101
|
+
const memories = [];
|
|
1102
|
+
for (const scene2 of parsed) {
|
|
1103
|
+
for (const m of scene2.memories || []) {
|
|
1104
|
+
memories.push({
|
|
1105
|
+
content: m.content,
|
|
1106
|
+
type: normalizeMemoryType(m.type),
|
|
1107
|
+
priority: Math.min(100, Math.max(0, Number(m.priority) || 50)),
|
|
1108
|
+
source_message_ids: Array.isArray(m.source_message_ids) ? m.source_message_ids : [],
|
|
1109
|
+
metadata: m.metadata || {},
|
|
1110
|
+
scene_name: scene2.scene_name
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
return memories;
|
|
1115
|
+
} catch (e) {
|
|
1116
|
+
console.error("Failed to parse LLM output:", e);
|
|
1117
|
+
return [];
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
function normalizeMemoryType(type) {
|
|
1121
|
+
const t = type?.toLowerCase();
|
|
1122
|
+
if (t === "persona" || t === "episodic" || t === "instruction") {
|
|
1123
|
+
return t;
|
|
1124
|
+
}
|
|
1125
|
+
return "episodic";
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// index.ts
|
|
1129
|
+
var DEFAULT_CONFIG = {
|
|
1130
|
+
enabled: true,
|
|
1131
|
+
layersEnabled: { L0: true, L1: true, L2: true, L3: false },
|
|
1132
|
+
decay: {
|
|
1133
|
+
ttl: { enabled: true, retentionDays: 30, safetyThreshold: 0.8, minRetainL0: 50, minRetainL1: 20 },
|
|
1134
|
+
importance: { enabled: true, baseHalflifeDays: 50 },
|
|
1135
|
+
accessFrequency: { enabled: true },
|
|
1136
|
+
stateMachine: { enabled: true }
|
|
1137
|
+
},
|
|
1138
|
+
teamMemory: {
|
|
1139
|
+
enabled: true,
|
|
1140
|
+
maxImportedAgents: 2,
|
|
1141
|
+
visibilityGate: true
|
|
1142
|
+
},
|
|
1143
|
+
retrieval: {
|
|
1144
|
+
hybridSearch: true,
|
|
1145
|
+
semanticWeight: 0.5,
|
|
1146
|
+
bm25Weight: 0.25,
|
|
1147
|
+
entityBoostWeight: 0.25,
|
|
1148
|
+
topK: 10,
|
|
1149
|
+
overFetch: 4
|
|
1150
|
+
},
|
|
1151
|
+
storage: {
|
|
1152
|
+
backend: "memory",
|
|
1153
|
+
dataDir: "~/.openclaw/memory-new"
|
|
1154
|
+
},
|
|
1155
|
+
llm: {
|
|
1156
|
+
enabled: false,
|
|
1157
|
+
// Disabled by default, use fallback extraction
|
|
1158
|
+
model: "default"
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
function deriveFreshness(engram, config) {
|
|
1162
|
+
if (!config.decay.importance.enabled) return "fresh";
|
|
1163
|
+
const ageDays = (Date.now() - engram.lastEffectiveAt) / (1e3 * 60 * 60 * 24);
|
|
1164
|
+
const halflife = config.decay.importance.baseHalflifeDays * Math.pow(engram.importance + 0.1, 1.5);
|
|
1165
|
+
if (ageDays <= halflife) return "fresh";
|
|
1166
|
+
if (ageDays <= halflife * 2) return "aging";
|
|
1167
|
+
if (ageDays <= halflife * 4) return "stale";
|
|
1168
|
+
return "forgotten";
|
|
1169
|
+
}
|
|
1170
|
+
function deriveHotness(retrievalCount, ageDays) {
|
|
1171
|
+
return 1 / (1 + Math.exp(-Math.log(1 + retrievalCount))) * Math.exp(-Math.LN2 * ageDays / 7);
|
|
1172
|
+
}
|
|
1173
|
+
function calculateScore(relevance, recency, importance, strength, hotness, weights = { relevance: 0.5, recency: 0.15, importance: 0.25, strength: 0.05, hotness: 0.05 }) {
|
|
1174
|
+
return weights.relevance * relevance + weights.recency * recency + weights.importance * importance + weights.strength * strength + weights.hotness * hotness;
|
|
1175
|
+
}
|
|
1176
|
+
function validateVisibilityTransition(from, to, config) {
|
|
1177
|
+
if (!config.teamMemory.visibilityGate) return true;
|
|
1178
|
+
if (from === to) return true;
|
|
1179
|
+
if (to === "private" && from !== "private") return false;
|
|
1180
|
+
if (from === "private") return true;
|
|
1181
|
+
return true;
|
|
1182
|
+
}
|
|
3
1183
|
var index_default = definePluginEntry({
|
|
4
1184
|
id: "memory_new",
|
|
5
1185
|
name: "Memory New",
|
|
6
|
-
description: "
|
|
1186
|
+
description: "Multi-layered memory system with L0\u2192L1\u2192L2\u2192L3 distillation, persistent storage, and recall",
|
|
7
1187
|
register(api) {
|
|
8
|
-
const config = api.pluginConfig ??
|
|
1188
|
+
const config = api.pluginConfig ?? DEFAULT_CONFIG;
|
|
1189
|
+
const storage = new StorageAdapter(config.storage.dataDir);
|
|
1190
|
+
const store = new MemoryStore(config.storage.dataDir);
|
|
1191
|
+
const recall = new RecallEngine(storage);
|
|
1192
|
+
const vectorStore = new VectorStore();
|
|
1193
|
+
const pipeline = new DistillationPipeline(DEFAULT_DISTILLATION_CONFIG, store, vectorStore);
|
|
1194
|
+
store.setVectorStore(vectorStore);
|
|
1195
|
+
if (config.llm?.enabled) {
|
|
1196
|
+
api.registerEmbeddingProvider({
|
|
1197
|
+
id: "memory-new-embedder",
|
|
1198
|
+
defaultModel: config.llm.model ?? "default",
|
|
1199
|
+
transport: "local",
|
|
1200
|
+
create: async (options) => {
|
|
1201
|
+
const provider = {
|
|
1202
|
+
id: "memory-new-embedder",
|
|
1203
|
+
model: options.model,
|
|
1204
|
+
dimensions: 384,
|
|
1205
|
+
maxInputTokens: 8192,
|
|
1206
|
+
embed: async (input) => {
|
|
1207
|
+
const text = typeof input === "string" ? input : input.text;
|
|
1208
|
+
return vectorStore.generatePseudoEmbedding(text);
|
|
1209
|
+
},
|
|
1210
|
+
embedBatch: async (inputs) => {
|
|
1211
|
+
return inputs.map((input) => {
|
|
1212
|
+
const text = typeof input === "string" ? input : input.text;
|
|
1213
|
+
return vectorStore.generatePseudoEmbedding(text);
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
return { provider, runtime: { id: "memory-new-embedder" } };
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
pipeline.setSubagentRunner(async (prompt) => {
|
|
1221
|
+
try {
|
|
1222
|
+
const result = await api.runtime.subagent.run({
|
|
1223
|
+
sessionKey: `memory-${Date.now()}`,
|
|
1224
|
+
message: prompt,
|
|
1225
|
+
model: config.llm?.model,
|
|
1226
|
+
disableTools: true
|
|
1227
|
+
});
|
|
1228
|
+
const waitResult = await api.runtime.subagent.waitForRun({ runId: result.runId, timeoutMs: 3e4 });
|
|
1229
|
+
const messages = await api.runtime.subagent.getSessionMessages({ sessionKey: result.sessionKey });
|
|
1230
|
+
const assistantMsg = messages.messages.find((m) => m.role === "assistant");
|
|
1231
|
+
return assistantMsg?.content?.[0]?.text ?? "";
|
|
1232
|
+
} catch (error) {
|
|
1233
|
+
api.logger.debug?.(`Subagent extraction failed: ${error}`);
|
|
1234
|
+
throw error;
|
|
1235
|
+
}
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
const sessionMessages = /* @__PURE__ */ new Map();
|
|
9
1239
|
api.registerCommand({
|
|
10
|
-
name: "
|
|
11
|
-
description: "Interact with
|
|
1240
|
+
name: "memory",
|
|
1241
|
+
description: "Interact with memory system",
|
|
12
1242
|
acceptsArgs: true,
|
|
13
1243
|
exposeSenderIsOwner: true,
|
|
14
1244
|
handler: async (ctx) => {
|
|
15
|
-
const
|
|
16
|
-
const action = (
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
1245
|
+
const args = ctx.args ?? "";
|
|
1246
|
+
const [action, ...rest] = args.trim().split(/\s+/);
|
|
1247
|
+
switch (action) {
|
|
1248
|
+
case "add": {
|
|
1249
|
+
const content = rest.join(" ");
|
|
1250
|
+
if (!content) return { text: "Usage: memory add <content>" };
|
|
1251
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1252
|
+
await store.storeL1({
|
|
1253
|
+
content,
|
|
1254
|
+
type: "episodic",
|
|
1255
|
+
priority: 50,
|
|
1256
|
+
sceneName: "User Added",
|
|
1257
|
+
sourceMessageIds: [],
|
|
1258
|
+
metadata: { source: "command" },
|
|
1259
|
+
timestamps: [now],
|
|
1260
|
+
sessionKey: ctx.sessionKey ?? "default",
|
|
1261
|
+
sessionId: ctx.sessionKey ?? "default",
|
|
1262
|
+
userId: ctx.senderIsOwner ? "owner" : "user",
|
|
1263
|
+
agentId: "self"
|
|
1264
|
+
});
|
|
1265
|
+
return { text: `Added: ${content.slice(0, 50)}...` };
|
|
1266
|
+
}
|
|
1267
|
+
case "search": {
|
|
1268
|
+
const query = rest.join(" ");
|
|
1269
|
+
if (!query) return { text: "Usage: memory search <query>" };
|
|
1270
|
+
const result = await recall.recall({
|
|
1271
|
+
query,
|
|
1272
|
+
sessionKey: ctx.sessionKey ?? "default",
|
|
1273
|
+
userId: ctx.senderIsOwner ? "owner" : "user",
|
|
1274
|
+
agentId: "self",
|
|
1275
|
+
topK: config.retrieval.topK
|
|
1276
|
+
});
|
|
1277
|
+
if (!result.prependContext && !result.appendSystemContext) {
|
|
1278
|
+
return { text: "No relevant memories found." };
|
|
1279
|
+
}
|
|
1280
|
+
return {
|
|
1281
|
+
text: `Found memories:
|
|
1282
|
+
|
|
1283
|
+
${result.prependContext ?? ""}
|
|
1284
|
+
|
|
1285
|
+
${result.appendSystemContext ?? ""}`
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
case "list": {
|
|
1289
|
+
const records = await store.searchL1("", 20);
|
|
1290
|
+
if (records.length === 0) return { text: "No memories stored." };
|
|
1291
|
+
const lines = records.map(
|
|
1292
|
+
(r) => `[${r.type}] ${r.content.slice(0, 60)}${r.content.length > 60 ? "..." : ""}`
|
|
1293
|
+
);
|
|
1294
|
+
return { text: `Recent ${records.length} memories:
|
|
1295
|
+
|
|
1296
|
+
${lines.join("\n")}` };
|
|
1297
|
+
}
|
|
1298
|
+
case "reinforce": {
|
|
1299
|
+
return { text: "Reinforce is not yet supported with persistent storage." };
|
|
1300
|
+
}
|
|
1301
|
+
case "decay": {
|
|
1302
|
+
return { text: "Decay is not yet fully implemented." };
|
|
1303
|
+
}
|
|
1304
|
+
case "stats": {
|
|
1305
|
+
const stats = await pipeline.getStats();
|
|
1306
|
+
return {
|
|
1307
|
+
text: `Memory Stats:
|
|
1308
|
+
L0 (buffered): ${stats.l0Count}
|
|
1309
|
+
L1 (stored): ${stats.l1Count}
|
|
1310
|
+
L2 (scenes): ${stats.l2Count}
|
|
1311
|
+
L3 (persona): ${stats.l3Count}`
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
case "config": {
|
|
1315
|
+
return { text: JSON.stringify(config, null, 2) };
|
|
1316
|
+
}
|
|
1317
|
+
default:
|
|
1318
|
+
return {
|
|
1319
|
+
text: `Memory New commands:
|
|
1320
|
+
memory add <content> - Add a memory
|
|
1321
|
+
memory search <query> - Search memories
|
|
1322
|
+
memory list - List recent memories
|
|
1323
|
+
memory stats - Show memory statistics
|
|
1324
|
+
memory config - Show configuration
|
|
1325
|
+
|
|
1326
|
+
Memory layers: L0 (raw) \u2192 L1 (atomic) \u2192 L2 (scene) \u2192 L3 (persona)`
|
|
1327
|
+
};
|
|
21
1328
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
1329
|
+
}
|
|
1330
|
+
});
|
|
1331
|
+
api.registerCommand({
|
|
1332
|
+
name: "team-memory",
|
|
1333
|
+
description: "Team memory operations",
|
|
1334
|
+
acceptsArgs: true,
|
|
1335
|
+
exposeSenderIsOwner: true,
|
|
1336
|
+
handler: async (ctx) => {
|
|
1337
|
+
if (!config.teamMemory.enabled) {
|
|
1338
|
+
return { text: "Team memory is disabled." };
|
|
1339
|
+
}
|
|
1340
|
+
const args = ctx.args ?? "";
|
|
1341
|
+
const [action, ...rest] = args.trim().split(/\s+/);
|
|
1342
|
+
switch (action) {
|
|
1343
|
+
case "share": {
|
|
1344
|
+
return { text: "Share is not yet implemented." };
|
|
1345
|
+
}
|
|
1346
|
+
case "import": {
|
|
1347
|
+
return { text: "Import is not yet implemented." };
|
|
1348
|
+
}
|
|
1349
|
+
case "status": {
|
|
1350
|
+
const stats = await pipeline.getStats();
|
|
1351
|
+
return {
|
|
1352
|
+
text: `Team memory status:
|
|
1353
|
+
Enabled: ${config.teamMemory.enabled}
|
|
1354
|
+
Max imported agents: ${config.teamMemory.maxImportedAgents}
|
|
1355
|
+
Visibility gate: ${config.teamMemory.visibilityGate}
|
|
1356
|
+
Total L1 memories: ${stats.l1Count}
|
|
1357
|
+
Total L2 scenes: ${stats.l2Count}`
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
default:
|
|
1361
|
+
return {
|
|
1362
|
+
text: `Team memory commands:
|
|
1363
|
+
team-memory status - Show team memory status`
|
|
1364
|
+
};
|
|
28
1365
|
}
|
|
29
|
-
return {
|
|
30
|
-
text: `Unknown action: ${action}
|
|
31
|
-
|
|
32
|
-
Type "memory_new help" for available commands.`
|
|
33
|
-
};
|
|
34
1366
|
}
|
|
35
1367
|
});
|
|
36
1368
|
api.on("before_prompt_build", async (event, ctx) => {
|
|
37
|
-
if (!config.enabled)
|
|
1369
|
+
if (!config.enabled) return void 0;
|
|
1370
|
+
const sessionKey = ctx.sessionKey ?? "default";
|
|
1371
|
+
const userId = "user";
|
|
1372
|
+
try {
|
|
1373
|
+
const query = event.prompt?.slice(0, 200) ?? "";
|
|
1374
|
+
const recallResult = await recall.recall({
|
|
1375
|
+
query,
|
|
1376
|
+
sessionKey,
|
|
1377
|
+
userId,
|
|
1378
|
+
agentId: "self",
|
|
1379
|
+
topK: config.retrieval.topK,
|
|
1380
|
+
vectorStore: config.retrieval.hybridSearch ? vectorStore : void 0
|
|
1381
|
+
});
|
|
1382
|
+
if (!recallResult.prependContext && !recallResult.appendSystemContext) {
|
|
1383
|
+
return void 0;
|
|
1384
|
+
}
|
|
1385
|
+
return {
|
|
1386
|
+
prependContext: recallResult.prependContext,
|
|
1387
|
+
appendContext: recallResult.appendSystemContext
|
|
1388
|
+
};
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
api.logger.debug?.(`Memory recall failed: ${error}`);
|
|
38
1391
|
return void 0;
|
|
39
1392
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
1393
|
+
});
|
|
1394
|
+
api.on("after_prompt_build", async (event, ctx) => {
|
|
1395
|
+
if (!config.enabled || !config.layersEnabled.L0) return;
|
|
1396
|
+
const sessionKey = ctx.sessionKey ?? "default";
|
|
1397
|
+
try {
|
|
1398
|
+
const messages = event.messages ?? [];
|
|
1399
|
+
if (messages.length > 0) {
|
|
1400
|
+
for (const msg of messages) {
|
|
1401
|
+
const l0Msg = {
|
|
1402
|
+
role: msg.role === "user" ? "user" : "assistant",
|
|
1403
|
+
content: msg.content?.slice(0, 1e4) ?? "",
|
|
1404
|
+
// Limit length
|
|
1405
|
+
timestamp: msg.timestamp ?? Date.now(),
|
|
1406
|
+
sessionKey,
|
|
1407
|
+
sessionId: sessionKey,
|
|
1408
|
+
userId: "user",
|
|
1409
|
+
agentId: "self"
|
|
1410
|
+
};
|
|
1411
|
+
await store.ingestMessage(l0Msg);
|
|
1412
|
+
}
|
|
1413
|
+
if (config.layersEnabled.L1 && config.llm?.enabled) {
|
|
1414
|
+
await pipeline.distill("L1");
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
} catch (error) {
|
|
1418
|
+
api.logger.debug?.(`Memory capture failed: ${error}`);
|
|
1419
|
+
}
|
|
44
1420
|
});
|
|
45
1421
|
api.on("agent_end", (event, ctx) => {
|
|
1422
|
+
if (!config.enabled) return;
|
|
46
1423
|
const runId = event.runId ?? ctx.runId;
|
|
47
|
-
api.logger.debug?.(`
|
|
1424
|
+
api.logger.debug?.(`Memory: agent ended for run ${runId}`);
|
|
1425
|
+
});
|
|
1426
|
+
api.on("session_end", async (event, ctx) => {
|
|
1427
|
+
if (!config.enabled) return;
|
|
1428
|
+
const sessionKey = ctx.sessionKey ?? "default";
|
|
1429
|
+
try {
|
|
1430
|
+
if (config.layersEnabled.L2) {
|
|
1431
|
+
await pipeline.distill("L2");
|
|
1432
|
+
}
|
|
1433
|
+
if (config.layersEnabled.L3) {
|
|
1434
|
+
await pipeline.distill("L3");
|
|
1435
|
+
}
|
|
1436
|
+
sessionMessages.delete(sessionKey);
|
|
1437
|
+
api.logger.debug?.(`Memory: session ended for ${sessionKey}`);
|
|
1438
|
+
} catch (error) {
|
|
1439
|
+
api.logger.debug?.(`Memory session cleanup failed: ${error}`);
|
|
1440
|
+
}
|
|
1441
|
+
});
|
|
1442
|
+
api.registerTool({
|
|
1443
|
+
name: "memory_search",
|
|
1444
|
+
description: "Search memory store using hybrid retrieval",
|
|
1445
|
+
parameters: {
|
|
1446
|
+
type: "object",
|
|
1447
|
+
properties: {
|
|
1448
|
+
query: { type: "string", description: "Search query" },
|
|
1449
|
+
limit: { type: "number", description: "Max results", default: 10 },
|
|
1450
|
+
visibility: {
|
|
1451
|
+
type: "string",
|
|
1452
|
+
enum: ["all", "public", "team", "private"],
|
|
1453
|
+
default: "all"
|
|
1454
|
+
}
|
|
1455
|
+
},
|
|
1456
|
+
required: ["query"]
|
|
1457
|
+
},
|
|
1458
|
+
execute: async (params, ctx) => {
|
|
1459
|
+
const result = await recall.recall({
|
|
1460
|
+
query: params.query,
|
|
1461
|
+
sessionKey: ctx.sessionKey ?? "default",
|
|
1462
|
+
userId: "user",
|
|
1463
|
+
agentId: "self",
|
|
1464
|
+
topK: params.limit ?? 10
|
|
1465
|
+
});
|
|
1466
|
+
return {
|
|
1467
|
+
memories: result.recalledL1Memories ?? [],
|
|
1468
|
+
prependContext: result.prependContext,
|
|
1469
|
+
appendContext: result.appendSystemContext
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1473
|
+
api.registerTool({
|
|
1474
|
+
name: "memory_store",
|
|
1475
|
+
description: "Store a new memory engram",
|
|
1476
|
+
parameters: {
|
|
1477
|
+
type: "object",
|
|
1478
|
+
properties: {
|
|
1479
|
+
content: { type: "string", description: "Memory content" },
|
|
1480
|
+
type: {
|
|
1481
|
+
type: "string",
|
|
1482
|
+
enum: ["persona", "episodic", "instruction"],
|
|
1483
|
+
default: "episodic",
|
|
1484
|
+
description: "Memory type"
|
|
1485
|
+
},
|
|
1486
|
+
priority: {
|
|
1487
|
+
type: "number",
|
|
1488
|
+
default: 50,
|
|
1489
|
+
description: "Priority 0-100"
|
|
1490
|
+
},
|
|
1491
|
+
sceneName: {
|
|
1492
|
+
type: "string",
|
|
1493
|
+
default: "General",
|
|
1494
|
+
description: "Scene name"
|
|
1495
|
+
}
|
|
1496
|
+
},
|
|
1497
|
+
required: ["content"]
|
|
1498
|
+
},
|
|
1499
|
+
execute: async (params, ctx) => {
|
|
1500
|
+
const record = await store.storeL1({
|
|
1501
|
+
content: params.content,
|
|
1502
|
+
type: params.type ?? "episodic",
|
|
1503
|
+
priority: params.priority ?? 50,
|
|
1504
|
+
sceneName: params.sceneName ?? "General",
|
|
1505
|
+
sourceMessageIds: [],
|
|
1506
|
+
metadata: { source: "tool" },
|
|
1507
|
+
timestamps: [(/* @__PURE__ */ new Date()).toISOString()],
|
|
1508
|
+
sessionKey: ctx.sessionKey ?? "default",
|
|
1509
|
+
sessionId: ctx.sessionKey ?? "default",
|
|
1510
|
+
userId: "user",
|
|
1511
|
+
agentId: "self"
|
|
1512
|
+
});
|
|
1513
|
+
return { engramId: record.id, stored: true };
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
api.registerTool({
|
|
1517
|
+
name: "memory_get",
|
|
1518
|
+
description: "Get a specific memory by ID",
|
|
1519
|
+
parameters: {
|
|
1520
|
+
type: "object",
|
|
1521
|
+
properties: {
|
|
1522
|
+
layer: {
|
|
1523
|
+
type: "string",
|
|
1524
|
+
enum: ["L0", "L1", "L2", "L3"],
|
|
1525
|
+
default: "L1",
|
|
1526
|
+
description: "Memory layer"
|
|
1527
|
+
},
|
|
1528
|
+
id: { type: "string", description: "Memory ID (for L2/L3)" }
|
|
1529
|
+
},
|
|
1530
|
+
required: []
|
|
1531
|
+
},
|
|
1532
|
+
execute: async (params, ctx) => {
|
|
1533
|
+
if (params.layer === "L2" && params.id) {
|
|
1534
|
+
const scene2 = await store.getScene(params.id);
|
|
1535
|
+
return scene2 ?? { error: "Scene not found" };
|
|
1536
|
+
}
|
|
1537
|
+
if (params.layer === "L3") {
|
|
1538
|
+
const persona = await store.getPersona();
|
|
1539
|
+
return persona ?? { error: "Persona not found" };
|
|
1540
|
+
}
|
|
1541
|
+
const records = await store.searchL1("", 10);
|
|
1542
|
+
return { records };
|
|
1543
|
+
}
|
|
1544
|
+
});
|
|
1545
|
+
api.registerTool({
|
|
1546
|
+
name: "memory_distill",
|
|
1547
|
+
description: "Trigger memory distillation manually",
|
|
1548
|
+
parameters: {
|
|
1549
|
+
type: "object",
|
|
1550
|
+
properties: {
|
|
1551
|
+
layer: {
|
|
1552
|
+
type: "string",
|
|
1553
|
+
enum: ["L1", "L2", "L3"],
|
|
1554
|
+
default: "L1",
|
|
1555
|
+
description: "Layer to distill"
|
|
1556
|
+
}
|
|
1557
|
+
},
|
|
1558
|
+
required: ["layer"]
|
|
1559
|
+
},
|
|
1560
|
+
execute: async (params, ctx) => {
|
|
1561
|
+
const layer = params.layer;
|
|
1562
|
+
const result = await pipeline.distill(layer);
|
|
1563
|
+
return {
|
|
1564
|
+
layer: result.stage,
|
|
1565
|
+
produced: result.produced,
|
|
1566
|
+
errors: result.errors
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
});
|
|
1570
|
+
api.registerHttpRoute({
|
|
1571
|
+
method: "GET",
|
|
1572
|
+
path: "/memory/team/status",
|
|
1573
|
+
handler: async (req, ctx) => {
|
|
1574
|
+
if (!config.teamMemory.enabled) {
|
|
1575
|
+
return { status: 403, body: { error: "Team memory disabled" } };
|
|
1576
|
+
}
|
|
1577
|
+
const stats = await pipeline.getStats();
|
|
1578
|
+
return {
|
|
1579
|
+
status: 200,
|
|
1580
|
+
body: {
|
|
1581
|
+
enabled: true,
|
|
1582
|
+
maxImportedAgents: config.teamMemory.maxImportedAgents,
|
|
1583
|
+
l1Count: stats.l1Count,
|
|
1584
|
+
l2Count: stats.l2Count
|
|
1585
|
+
}
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
});
|
|
1589
|
+
api.registerHttpRoute({
|
|
1590
|
+
method: "POST",
|
|
1591
|
+
path: "/memory/team/share",
|
|
1592
|
+
handler: async (req, ctx) => {
|
|
1593
|
+
if (!config.teamMemory.enabled) {
|
|
1594
|
+
return { status: 403, body: { error: "Team memory disabled" } };
|
|
1595
|
+
}
|
|
1596
|
+
return { status: 501, body: { error: "Not implemented" } };
|
|
1597
|
+
}
|
|
1598
|
+
});
|
|
1599
|
+
api.registerHttpRoute({
|
|
1600
|
+
method: "POST",
|
|
1601
|
+
path: "/memory/team/import",
|
|
1602
|
+
handler: async (req, ctx) => {
|
|
1603
|
+
if (!config.teamMemory.enabled) {
|
|
1604
|
+
return { status: 403, body: { error: "Team memory disabled" } };
|
|
1605
|
+
}
|
|
1606
|
+
return { status: 501, body: { error: "Not implemented" } };
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1609
|
+
api.logger.info?.("Memory New plugin registered with storage and pipeline");
|
|
1610
|
+
api.registerConfigMigration?.({
|
|
1611
|
+
id: "memory-new-default-config",
|
|
1612
|
+
migrate: (existingConfig) => {
|
|
1613
|
+
const current = existingConfig?.memory_new;
|
|
1614
|
+
if (!current) {
|
|
1615
|
+
return {
|
|
1616
|
+
memory_new: {
|
|
1617
|
+
enabled: true,
|
|
1618
|
+
layersEnabled: { L0: true, L1: true, L2: true, L3: false },
|
|
1619
|
+
llm: { enabled: false, model: "default" },
|
|
1620
|
+
retrieval: {
|
|
1621
|
+
hybridSearch: true,
|
|
1622
|
+
semanticWeight: 0.5,
|
|
1623
|
+
bm25Weight: 0.25,
|
|
1624
|
+
entityBoostWeight: 0.25,
|
|
1625
|
+
topK: 10,
|
|
1626
|
+
overFetch: 4
|
|
1627
|
+
},
|
|
1628
|
+
storage: { backend: "memory", dataDir: "~/.openclaw/memory-new" },
|
|
1629
|
+
decay: {
|
|
1630
|
+
ttl: { enabled: true, retentionDays: 30, safetyThreshold: 0.8, minRetainL0: 50, minRetainL1: 20 },
|
|
1631
|
+
importance: { enabled: true, baseHalflifeDays: 50 },
|
|
1632
|
+
accessFrequency: { enabled: true },
|
|
1633
|
+
stateMachine: { enabled: true }
|
|
1634
|
+
},
|
|
1635
|
+
teamMemory: { enabled: true, maxImportedAgents: 2, visibilityGate: true }
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
return {};
|
|
1640
|
+
}
|
|
1641
|
+
});
|
|
1642
|
+
api.registerAutoEnableProbe?.({
|
|
1643
|
+
id: "memory-new",
|
|
1644
|
+
check: async (config2) => {
|
|
1645
|
+
const hasConfig = config2?.plugins?.memory_new?.enabled === true;
|
|
1646
|
+
const hasStorageDir = false;
|
|
1647
|
+
return hasConfig || hasStorageDir;
|
|
1648
|
+
}
|
|
48
1649
|
});
|
|
49
1650
|
}
|
|
50
1651
|
});
|
|
51
|
-
var testing = {
|
|
1652
|
+
var testing = {
|
|
1653
|
+
deriveFreshness,
|
|
1654
|
+
deriveHotness,
|
|
1655
|
+
calculateScore,
|
|
1656
|
+
validateVisibilityTransition
|
|
1657
|
+
};
|
|
52
1658
|
export {
|
|
53
1659
|
index_default as default,
|
|
54
1660
|
testing
|