@amemhq/core 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/LICENSE +21 -0
- package/README.md +125 -0
- package/dist/index.cjs +2369 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +502 -0
- package/dist/index.d.ts +502 -0
- package/dist/index.js +2298 -0
- package/dist/index.js.map +1 -0
- package/package.json +81 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2369 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
DEFAULT_CRUD_UPDATE_MIN_SIM: () => DEFAULT_CRUD_UPDATE_MIN_SIM,
|
|
34
|
+
DEFAULT_EMBEDDING_MODEL: () => DEFAULT_EMBEDDING_MODEL,
|
|
35
|
+
EmbeddingDimensionMismatchError: () => EmbeddingDimensionMismatchError,
|
|
36
|
+
addEpisodic: () => addEpisodic,
|
|
37
|
+
addMemory: () => addMemory,
|
|
38
|
+
canRead: () => canRead,
|
|
39
|
+
canWrite: () => canWrite,
|
|
40
|
+
checkQuality: () => checkQuality,
|
|
41
|
+
configure: () => configure,
|
|
42
|
+
configureLlm: () => configureLlm,
|
|
43
|
+
conflictSweep: () => conflictSweep,
|
|
44
|
+
consolidateMemories: () => consolidateMemories,
|
|
45
|
+
createStorageContext: () => createStorageContext,
|
|
46
|
+
deleteNote: () => deleteNote,
|
|
47
|
+
encode: () => encode,
|
|
48
|
+
ensureCollection: () => ensureCollection,
|
|
49
|
+
generateReviewBatch: () => generateReviewBatch,
|
|
50
|
+
getEmbeddingDim: () => getEmbeddingDim,
|
|
51
|
+
getEmbeddingModel: () => getEmbeddingModel,
|
|
52
|
+
getNote: () => getNote,
|
|
53
|
+
invalidateNote: () => invalidateNote,
|
|
54
|
+
isModelLoaded: () => isModelLoaded,
|
|
55
|
+
isPlausibleUpdateTarget: () => isPlausibleUpdateTarget,
|
|
56
|
+
listMemories: () => listMemories,
|
|
57
|
+
listNotes: () => listNotes,
|
|
58
|
+
llmCrudDecision: () => llmCrudDecision,
|
|
59
|
+
loadModel: () => loadModel,
|
|
60
|
+
mergeSimilarNotes: () => mergeSimilarNotes,
|
|
61
|
+
migrateCollection: () => migrateCollection,
|
|
62
|
+
patchNotePayload: () => patchNotePayload,
|
|
63
|
+
pingQdrant: () => pingQdrant,
|
|
64
|
+
resolveCrudUpdateMinSim: () => resolveCrudUpdateMinSim,
|
|
65
|
+
scanLowQuality: () => scanLowQuality,
|
|
66
|
+
searchMemory: () => searchMemory,
|
|
67
|
+
updateNote: () => updateNote
|
|
68
|
+
});
|
|
69
|
+
module.exports = __toCommonJS(index_exports);
|
|
70
|
+
|
|
71
|
+
// src/config.ts
|
|
72
|
+
var os = __toESM(require("os"), 1);
|
|
73
|
+
var path = __toESM(require("path"), 1);
|
|
74
|
+
var _dataDir = process.env.AMEM_DATA_DIR || path.join(os.homedir(), ".amem");
|
|
75
|
+
function configure(opts) {
|
|
76
|
+
if (opts.dataDir) _dataDir = opts.dataDir;
|
|
77
|
+
}
|
|
78
|
+
function getDataDir() {
|
|
79
|
+
return _dataDir;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/embedding.ts
|
|
83
|
+
var pipeline = null;
|
|
84
|
+
var extractor = null;
|
|
85
|
+
var loadedModelName = null;
|
|
86
|
+
var cachedDim = null;
|
|
87
|
+
var DEFAULT_EMBEDDING_MODEL = "Xenova/paraphrase-multilingual-MiniLM-L12-v2";
|
|
88
|
+
function getEmbeddingModel() {
|
|
89
|
+
return process.env.AMEM_EMBED_MODEL?.trim() || DEFAULT_EMBEDDING_MODEL;
|
|
90
|
+
}
|
|
91
|
+
async function getExtractor() {
|
|
92
|
+
const wanted = getEmbeddingModel();
|
|
93
|
+
if (extractor && loadedModelName === wanted) return extractor;
|
|
94
|
+
if (!pipeline) {
|
|
95
|
+
const mod = await import("@huggingface/transformers");
|
|
96
|
+
pipeline = mod.pipeline;
|
|
97
|
+
}
|
|
98
|
+
extractor = await pipeline("feature-extraction", wanted, {
|
|
99
|
+
revision: "main"
|
|
100
|
+
});
|
|
101
|
+
loadedModelName = wanted;
|
|
102
|
+
cachedDim = null;
|
|
103
|
+
return extractor;
|
|
104
|
+
}
|
|
105
|
+
async function getEmbeddingDim() {
|
|
106
|
+
if (cachedDim !== null && loadedModelName === getEmbeddingModel()) return cachedDim;
|
|
107
|
+
const probe = await encode("dimension probe");
|
|
108
|
+
cachedDim = probe.length;
|
|
109
|
+
return cachedDim;
|
|
110
|
+
}
|
|
111
|
+
function meanPoolingNormalize(output, attentionMask) {
|
|
112
|
+
const seqLen = output.length;
|
|
113
|
+
const dim = output[0].length;
|
|
114
|
+
const pooled = new Array(dim).fill(0);
|
|
115
|
+
let maskSum = 0;
|
|
116
|
+
for (let i = 0; i < seqLen; i++) {
|
|
117
|
+
const m = attentionMask[i];
|
|
118
|
+
maskSum += m;
|
|
119
|
+
for (let j = 0; j < dim; j++) {
|
|
120
|
+
pooled[j] += output[i][j] * m;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
for (let j = 0; j < dim; j++) {
|
|
124
|
+
pooled[j] /= Math.max(maskSum, 1e-9);
|
|
125
|
+
}
|
|
126
|
+
let norm = 0;
|
|
127
|
+
for (const v of pooled) norm += v * v;
|
|
128
|
+
norm = Math.sqrt(norm);
|
|
129
|
+
return pooled.map((v) => v / Math.max(norm, 1e-9));
|
|
130
|
+
}
|
|
131
|
+
async function encode(text) {
|
|
132
|
+
const ext = await getExtractor();
|
|
133
|
+
const result = await ext(text, { pooling: "mean", normalize: true });
|
|
134
|
+
if (result && result.data) {
|
|
135
|
+
return Array.from(result.data);
|
|
136
|
+
}
|
|
137
|
+
const tensor = result;
|
|
138
|
+
if (tensor.dims && tensor.dims.length === 3) {
|
|
139
|
+
const seqLen = tensor.dims[1];
|
|
140
|
+
const dim = tensor.dims[2];
|
|
141
|
+
const raw = [];
|
|
142
|
+
for (let i = 0; i < seqLen; i++) {
|
|
143
|
+
const row = [];
|
|
144
|
+
for (let j = 0; j < dim; j++) {
|
|
145
|
+
row.push(tensor.data[i * dim + j]);
|
|
146
|
+
}
|
|
147
|
+
raw.push(row);
|
|
148
|
+
}
|
|
149
|
+
return meanPoolingNormalize(raw, new Array(seqLen).fill(1));
|
|
150
|
+
}
|
|
151
|
+
throw new Error("Unexpected embedding output shape");
|
|
152
|
+
}
|
|
153
|
+
async function loadModel() {
|
|
154
|
+
await getExtractor();
|
|
155
|
+
}
|
|
156
|
+
function isModelLoaded() {
|
|
157
|
+
return extractor !== null;
|
|
158
|
+
}
|
|
159
|
+
function cosineSimilarity(a, b) {
|
|
160
|
+
let dot = 0;
|
|
161
|
+
for (let i = 0; i < a.length; i++) dot += a[i] * b[i];
|
|
162
|
+
return dot;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/memory.ts
|
|
166
|
+
var import_uuid = require("uuid");
|
|
167
|
+
var import_crypto = require("crypto");
|
|
168
|
+
var fs2 = __toESM(require("fs"), 1);
|
|
169
|
+
var path3 = __toESM(require("path"), 1);
|
|
170
|
+
|
|
171
|
+
// src/auth.ts
|
|
172
|
+
function canWrite(note, callerAgentId) {
|
|
173
|
+
return note.owner === callerAgentId || note.writers.includes(callerAgentId) || note.writers.includes("*");
|
|
174
|
+
}
|
|
175
|
+
function canRead(note, callerAgentId) {
|
|
176
|
+
return note.owner === callerAgentId || note.readers.includes(callerAgentId) || note.readers.includes("*");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/storage.ts
|
|
180
|
+
var QDRANT_URL = "http://localhost:6333";
|
|
181
|
+
var getCollection = () => process.env.AMEM_COLLECTION || "amem_notes";
|
|
182
|
+
var EmbeddingDimensionMismatchError = class extends Error {
|
|
183
|
+
constructor(collection, collectionDim, modelDim, model) {
|
|
184
|
+
super(
|
|
185
|
+
`Collection "${collection}" stores ${collectionDim}-dimension vectors, but the embedding model "${model}" produces ${modelDim}. Qdrant fixes a collection's vector size at creation and cannot change it, so writes and searches would both fail.
|
|
186
|
+
Either set AMEM_EMBED_MODEL back to the model this collection was built with, or migrate: build a new collection with the new model, backfill it, then point AMEM_COLLECTION at it. See docs/reference/embedding-models.md.`
|
|
187
|
+
);
|
|
188
|
+
this.collection = collection;
|
|
189
|
+
this.collectionDim = collectionDim;
|
|
190
|
+
this.modelDim = modelDim;
|
|
191
|
+
this.model = model;
|
|
192
|
+
this.name = "EmbeddingDimensionMismatchError";
|
|
193
|
+
}
|
|
194
|
+
collection;
|
|
195
|
+
collectionDim;
|
|
196
|
+
modelDim;
|
|
197
|
+
model;
|
|
198
|
+
};
|
|
199
|
+
async function qdrant(method, path5, body) {
|
|
200
|
+
const res = await fetch(`${QDRANT_URL}${path5}`, {
|
|
201
|
+
method,
|
|
202
|
+
headers: { "Content-Type": "application/json" },
|
|
203
|
+
body: body ? JSON.stringify(body) : void 0
|
|
204
|
+
});
|
|
205
|
+
const data = await res.json();
|
|
206
|
+
if (!res.ok || data.status && data.status !== "ok" && data.status !== "acknowledged") {
|
|
207
|
+
throw new Error(`Qdrant ${method} ${path5} failed: ${data.error || JSON.stringify(data)}`);
|
|
208
|
+
}
|
|
209
|
+
return data.result;
|
|
210
|
+
}
|
|
211
|
+
async function pingQdrant() {
|
|
212
|
+
const res = await fetch(`${QDRANT_URL}/readyz`);
|
|
213
|
+
if (!res.ok) throw new Error(`Qdrant GET /readyz failed: ${res.status}`);
|
|
214
|
+
}
|
|
215
|
+
var _collectionReady = false;
|
|
216
|
+
var _collectionReadyMap = /* @__PURE__ */ new Map();
|
|
217
|
+
async function ensureCollection(collectionName) {
|
|
218
|
+
const col = collectionName || getCollection();
|
|
219
|
+
if (collectionName) {
|
|
220
|
+
if (_collectionReadyMap.get(col)) return;
|
|
221
|
+
} else {
|
|
222
|
+
if (_collectionReady) return;
|
|
223
|
+
}
|
|
224
|
+
const markReady = () => {
|
|
225
|
+
if (collectionName) _collectionReadyMap.set(col, true);
|
|
226
|
+
else _collectionReady = true;
|
|
227
|
+
};
|
|
228
|
+
let existing = null;
|
|
229
|
+
try {
|
|
230
|
+
existing = await qdrant("GET", `/collections/${col}`);
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
if (existing) {
|
|
234
|
+
const collectionDim = existing.config?.params?.vectors?.size;
|
|
235
|
+
if (typeof collectionDim === "number") {
|
|
236
|
+
const modelDim = await getEmbeddingDim();
|
|
237
|
+
if (collectionDim !== modelDim) {
|
|
238
|
+
throw new EmbeddingDimensionMismatchError(col, collectionDim, modelDim, getEmbeddingModel());
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
markReady();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
const size = await getEmbeddingDim();
|
|
246
|
+
await qdrant("PUT", `/collections/${col}`, {
|
|
247
|
+
vectors: { size, distance: "Cosine" }
|
|
248
|
+
});
|
|
249
|
+
} catch (err) {
|
|
250
|
+
if (!(err instanceof Error) || !err.message.includes("already exists")) throw err;
|
|
251
|
+
}
|
|
252
|
+
await qdrant("PUT", `/collections/${col}/index`, {
|
|
253
|
+
field_name: "agent_id",
|
|
254
|
+
field_schema: "keyword"
|
|
255
|
+
});
|
|
256
|
+
await qdrant("PUT", `/collections/${col}/index`, {
|
|
257
|
+
field_name: "hash",
|
|
258
|
+
field_schema: "keyword"
|
|
259
|
+
});
|
|
260
|
+
await qdrant("PUT", `/collections/${col}/index`, {
|
|
261
|
+
field_name: "topics",
|
|
262
|
+
field_schema: "keyword"
|
|
263
|
+
});
|
|
264
|
+
await qdrant("PUT", `/collections/${col}/index`, {
|
|
265
|
+
field_name: "subjects",
|
|
266
|
+
field_schema: "keyword"
|
|
267
|
+
});
|
|
268
|
+
markReady();
|
|
269
|
+
}
|
|
270
|
+
async function scrollAllRaw(collection, limit = 1e4) {
|
|
271
|
+
const out = [];
|
|
272
|
+
let offset = void 0;
|
|
273
|
+
for (; ; ) {
|
|
274
|
+
const body = { with_payload: true, with_vector: true, limit };
|
|
275
|
+
if (offset !== void 0 && offset !== null) body.offset = offset;
|
|
276
|
+
const res = await qdrant("POST", `/collections/${collection}/points/scroll`, body);
|
|
277
|
+
out.push(...res.points);
|
|
278
|
+
offset = res.next_page_offset;
|
|
279
|
+
if (offset === void 0 || offset === null || res.points.length === 0) break;
|
|
280
|
+
}
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
283
|
+
async function countPointsRaw(collection) {
|
|
284
|
+
const res = await qdrant("POST", `/collections/${collection}/points/count`, { exact: true });
|
|
285
|
+
return res.count;
|
|
286
|
+
}
|
|
287
|
+
async function collectionDimRaw(collection) {
|
|
288
|
+
try {
|
|
289
|
+
const info = await qdrant("GET", `/collections/${collection}`);
|
|
290
|
+
return info.config?.params?.vectors?.size ?? null;
|
|
291
|
+
} catch {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async function createCollectionRaw(collection, size) {
|
|
296
|
+
await qdrant("PUT", `/collections/${collection}`, { vectors: { size, distance: "Cosine" } });
|
|
297
|
+
for (const field_name of ["agent_id", "hash", "topics", "subjects"]) {
|
|
298
|
+
await qdrant("PUT", `/collections/${collection}/index`, { field_name, field_schema: "keyword" });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async function upsertPointsRaw(collection, points) {
|
|
302
|
+
await qdrant("PUT", `/collections/${collection}/points?wait=true`, { points });
|
|
303
|
+
}
|
|
304
|
+
function noteToPoint(note) {
|
|
305
|
+
return {
|
|
306
|
+
id: note.id,
|
|
307
|
+
vector: note.embedding,
|
|
308
|
+
payload: {
|
|
309
|
+
content: note.content,
|
|
310
|
+
keywords: note.keywords,
|
|
311
|
+
tags: note.tags,
|
|
312
|
+
context: note.context,
|
|
313
|
+
links: note.links,
|
|
314
|
+
timestamp: note.timestamp,
|
|
315
|
+
agent_id: note.agent_id,
|
|
316
|
+
hash: note.hash,
|
|
317
|
+
// 13-A
|
|
318
|
+
retrieval_count: note.retrieval_count ?? 0,
|
|
319
|
+
last_accessed: note.last_accessed || note.timestamp,
|
|
320
|
+
// 13-B: stored as JSON string (Qdrant payload can't handle nested array-of-objects)
|
|
321
|
+
evolution_history: JSON.stringify(note.evolution_history ?? []),
|
|
322
|
+
// 13-E
|
|
323
|
+
category: note.category || "General",
|
|
324
|
+
is_active: note.is_active !== false,
|
|
325
|
+
// 26B
|
|
326
|
+
topics: note.topics ?? [],
|
|
327
|
+
// 26A
|
|
328
|
+
note_type: note.note_type || "memory",
|
|
329
|
+
// 29
|
|
330
|
+
pending_merge: note.pending_merge ?? false,
|
|
331
|
+
// 30
|
|
332
|
+
evolution_type: note.evolution_type || "",
|
|
333
|
+
conflict: note.conflict ?? false,
|
|
334
|
+
conflicts_with: note.conflicts_with ?? [],
|
|
335
|
+
conflict_reason: note.conflict_reason ?? "",
|
|
336
|
+
conflict_scanned_at: note.conflict_scanned_at ?? "",
|
|
337
|
+
subjects: note.subjects ?? [],
|
|
338
|
+
// 31
|
|
339
|
+
ephemeral: note.ephemeral ?? false,
|
|
340
|
+
low_quality: note.low_quality ?? false,
|
|
341
|
+
// 32
|
|
342
|
+
owner: note.owner || note.agent_id,
|
|
343
|
+
readers: note.readers ?? [note.agent_id],
|
|
344
|
+
writers: note.writers ?? [note.agent_id]
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function pointToNote(point) {
|
|
349
|
+
const p = point.payload;
|
|
350
|
+
const timestamp = p.timestamp || "";
|
|
351
|
+
let evolutionHistory = [];
|
|
352
|
+
try {
|
|
353
|
+
const raw = p.evolution_history;
|
|
354
|
+
if (typeof raw === "string" && raw.length > 0) {
|
|
355
|
+
evolutionHistory = JSON.parse(raw);
|
|
356
|
+
} else if (Array.isArray(raw)) {
|
|
357
|
+
evolutionHistory = raw;
|
|
358
|
+
}
|
|
359
|
+
} catch {
|
|
360
|
+
evolutionHistory = [];
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
id: String(point.id),
|
|
364
|
+
content: p.content || "",
|
|
365
|
+
keywords: p.keywords || [],
|
|
366
|
+
tags: p.tags || [],
|
|
367
|
+
context: p.context || "",
|
|
368
|
+
links: p.links || [],
|
|
369
|
+
timestamp,
|
|
370
|
+
agent_id: p.agent_id || "main",
|
|
371
|
+
embedding: point.vector || [],
|
|
372
|
+
hash: p.hash || "",
|
|
373
|
+
// 13-A
|
|
374
|
+
retrieval_count: typeof p.retrieval_count === "number" ? p.retrieval_count : 0,
|
|
375
|
+
last_accessed: p.last_accessed || timestamp,
|
|
376
|
+
// 13-B
|
|
377
|
+
evolution_history: evolutionHistory,
|
|
378
|
+
// 13-E
|
|
379
|
+
category: p.category || "General",
|
|
380
|
+
is_active: p.is_active !== false,
|
|
381
|
+
// 26A
|
|
382
|
+
note_type: p.note_type === "knowledge" ? "knowledge" : "memory",
|
|
383
|
+
// 26B
|
|
384
|
+
topics: Array.isArray(p.topics) ? p.topics : [],
|
|
385
|
+
// 29
|
|
386
|
+
pending_merge: p.pending_merge === true,
|
|
387
|
+
// 30
|
|
388
|
+
evolution_type: typeof p.evolution_type === "string" && ["EVOLVE", "CONFLICT", "EXPAND", "NEW"].includes(p.evolution_type) ? p.evolution_type : void 0,
|
|
389
|
+
conflict: p.conflict === true,
|
|
390
|
+
conflicts_with: Array.isArray(p.conflicts_with) ? p.conflicts_with.filter((v) => typeof v === "string") : [],
|
|
391
|
+
conflict_reason: typeof p.conflict_reason === "string" ? p.conflict_reason : "",
|
|
392
|
+
conflict_scanned_at: typeof p.conflict_scanned_at === "string" ? p.conflict_scanned_at : "",
|
|
393
|
+
subjects: Array.isArray(p.subjects) ? p.subjects.filter((v) => typeof v === "string") : [],
|
|
394
|
+
// 31
|
|
395
|
+
ephemeral: p.ephemeral === true,
|
|
396
|
+
low_quality: p.low_quality === true,
|
|
397
|
+
// 32
|
|
398
|
+
owner: p.owner || p.agent_id || "main",
|
|
399
|
+
readers: Array.isArray(p.readers) ? p.readers : [p.agent_id || "main"],
|
|
400
|
+
writers: Array.isArray(p.writers) ? p.writers : [p.agent_id || "main"]
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
function agentFilter(agentId, subject) {
|
|
404
|
+
const must = [
|
|
405
|
+
{
|
|
406
|
+
should: [
|
|
407
|
+
{ key: "agent_id", match: { value: agentId } },
|
|
408
|
+
{ key: "agent_id", match: { value: "shared" } }
|
|
409
|
+
]
|
|
410
|
+
}
|
|
411
|
+
];
|
|
412
|
+
if (subject !== void 0) {
|
|
413
|
+
must.push({
|
|
414
|
+
should: [{ key: "subjects", match: { value: subject } }, { is_empty: { key: "subjects" } }]
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
must,
|
|
419
|
+
must_not: [{ key: "is_active", match: { value: false } }]
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function makeCrud(collectionName, modeBIsolated = false) {
|
|
423
|
+
const col = collectionName;
|
|
424
|
+
function scopedAgentFilter(agentId, subject) {
|
|
425
|
+
if (modeBIsolated) {
|
|
426
|
+
const must = [];
|
|
427
|
+
if (subject !== void 0) {
|
|
428
|
+
must.push({
|
|
429
|
+
should: [{ key: "subjects", match: { value: subject } }, { is_empty: { key: "subjects" } }]
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
...must.length > 0 && { must },
|
|
434
|
+
must_not: [{ key: "is_active", match: { value: false } }]
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
return agentFilter(agentId, subject);
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
async addNote(note) {
|
|
441
|
+
await ensureCollection(col);
|
|
442
|
+
await qdrant("PUT", `/collections/${col}/points?wait=true`, {
|
|
443
|
+
points: [noteToPoint(note)]
|
|
444
|
+
});
|
|
445
|
+
},
|
|
446
|
+
/**
|
|
447
|
+
* Story 36: this is the one read that bypasses the agent filter — it fetches
|
|
448
|
+
* straight by UUID. Pass `readerAgentId` to enforce `readers`; an unreadable
|
|
449
|
+
* note comes back as `null` (indistinguishable from missing, so nothing leaks,
|
|
450
|
+
* and callers already handle null). Omitting it skips the check, preserving
|
|
451
|
+
* behaviour for internal callers that only ever hold their own ids.
|
|
452
|
+
*/
|
|
453
|
+
async getNote(id, readerAgentId) {
|
|
454
|
+
await ensureCollection(col);
|
|
455
|
+
try {
|
|
456
|
+
const result = await qdrant("POST", `/collections/${col}/points`, {
|
|
457
|
+
ids: [id],
|
|
458
|
+
with_payload: true,
|
|
459
|
+
with_vector: true
|
|
460
|
+
});
|
|
461
|
+
if (!result.length) return null;
|
|
462
|
+
const note = pointToNote(result[0]);
|
|
463
|
+
if (readerAgentId !== void 0 && !canRead(note, readerAgentId)) return null;
|
|
464
|
+
return note;
|
|
465
|
+
} catch {
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
async updateNote(note) {
|
|
470
|
+
await ensureCollection(col);
|
|
471
|
+
await qdrant("PUT", `/collections/${col}/points?wait=true`, {
|
|
472
|
+
points: [noteToPoint(note)]
|
|
473
|
+
});
|
|
474
|
+
},
|
|
475
|
+
async findByHash(hash, agentId) {
|
|
476
|
+
await ensureCollection(col);
|
|
477
|
+
const body = {
|
|
478
|
+
filter: {
|
|
479
|
+
must: [
|
|
480
|
+
{ key: "hash", match: { value: hash } },
|
|
481
|
+
{ key: "is_active", match: { value: true } },
|
|
482
|
+
...modeBIsolated ? [] : [
|
|
483
|
+
{
|
|
484
|
+
should: [
|
|
485
|
+
{ key: "agent_id", match: { value: agentId } },
|
|
486
|
+
{ key: "agent_id", match: { value: "shared" } }
|
|
487
|
+
]
|
|
488
|
+
}
|
|
489
|
+
]
|
|
490
|
+
]
|
|
491
|
+
},
|
|
492
|
+
with_payload: true,
|
|
493
|
+
with_vector: true,
|
|
494
|
+
limit: 1
|
|
495
|
+
};
|
|
496
|
+
const result = await qdrant("POST", `/collections/${col}/points/scroll`, body);
|
|
497
|
+
if (!result.points.length) return null;
|
|
498
|
+
return pointToNote(result.points[0]);
|
|
499
|
+
},
|
|
500
|
+
/**
|
|
501
|
+
* Story 33: pass `callerAgentId` to enforce the writers policy. Callers that
|
|
502
|
+
* hold the note already should prefer checking `canWrite` themselves; this
|
|
503
|
+
* fetch-then-check path exists for callers that only have an id (the plugin's
|
|
504
|
+
* CRUD hook). Returns false — without writing — when the caller may not write.
|
|
505
|
+
* Omitting `callerAgentId` skips the check, preserving existing behaviour for
|
|
506
|
+
* internal callers that are already scoped to their own notes.
|
|
507
|
+
*/
|
|
508
|
+
async updateNoteContent(id, content, embedding, hash, callerAgentId) {
|
|
509
|
+
await ensureCollection(col);
|
|
510
|
+
let existing = null;
|
|
511
|
+
if (callerAgentId !== void 0) {
|
|
512
|
+
existing = await this.getNote(id);
|
|
513
|
+
if (existing && !canWrite(existing, callerAgentId)) return false;
|
|
514
|
+
}
|
|
515
|
+
await qdrant("PUT", `/collections/${col}/points/vectors?wait=true`, {
|
|
516
|
+
points: [{ id, vector: embedding }]
|
|
517
|
+
});
|
|
518
|
+
const payload = { content, hash };
|
|
519
|
+
if (existing) {
|
|
520
|
+
const history = [
|
|
521
|
+
...existing.evolution_history ?? [],
|
|
522
|
+
{
|
|
523
|
+
triggeredBy: "",
|
|
524
|
+
triggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
525
|
+
oldContext: existing.context,
|
|
526
|
+
newContext: existing.context,
|
|
527
|
+
oldTags: existing.tags,
|
|
528
|
+
newTags: existing.tags,
|
|
529
|
+
action: "crud_update",
|
|
530
|
+
oldContent: existing.content
|
|
531
|
+
}
|
|
532
|
+
];
|
|
533
|
+
payload.evolution_history = JSON.stringify(history);
|
|
534
|
+
}
|
|
535
|
+
await qdrant("POST", `/collections/${col}/points/payload?wait=true`, {
|
|
536
|
+
payload,
|
|
537
|
+
points: [id]
|
|
538
|
+
});
|
|
539
|
+
return true;
|
|
540
|
+
},
|
|
541
|
+
async queryByEmbedding(embedding, topK, agentId, scoreThreshold = 0, subject) {
|
|
542
|
+
await ensureCollection(col);
|
|
543
|
+
const result = await qdrant("POST", `/collections/${col}/points/search`, {
|
|
544
|
+
vector: embedding,
|
|
545
|
+
limit: topK,
|
|
546
|
+
with_payload: true,
|
|
547
|
+
with_vector: true,
|
|
548
|
+
score_threshold: scoreThreshold,
|
|
549
|
+
filter: scopedAgentFilter(agentId, subject)
|
|
550
|
+
});
|
|
551
|
+
const queryResults = result.map((r) => ({
|
|
552
|
+
note: pointToNote(r),
|
|
553
|
+
score: r.score
|
|
554
|
+
}));
|
|
555
|
+
if (queryResults.length > 0) {
|
|
556
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
557
|
+
const ids = queryResults.map((r) => r.note.id);
|
|
558
|
+
const patches = queryResults.map((r) => ({
|
|
559
|
+
id: r.note.id,
|
|
560
|
+
retrieval_count: (r.note.retrieval_count || 0) + 1
|
|
561
|
+
}));
|
|
562
|
+
Promise.all([
|
|
563
|
+
qdrant("POST", `/collections/${col}/points/payload?wait=false`, {
|
|
564
|
+
payload: { last_accessed: now },
|
|
565
|
+
points: ids
|
|
566
|
+
}),
|
|
567
|
+
...patches.map(
|
|
568
|
+
(p) => qdrant("POST", `/collections/${col}/points/payload?wait=false`, {
|
|
569
|
+
payload: { retrieval_count: p.retrieval_count },
|
|
570
|
+
points: [p.id]
|
|
571
|
+
})
|
|
572
|
+
)
|
|
573
|
+
]).catch((err) => {
|
|
574
|
+
console.error(`[amem] retrieval tracking patch failed: ${err.message}`);
|
|
575
|
+
});
|
|
576
|
+
for (const r of queryResults) {
|
|
577
|
+
r.note.retrieval_count = (r.note.retrieval_count || 0) + 1;
|
|
578
|
+
r.note.last_accessed = now;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return queryResults;
|
|
582
|
+
},
|
|
583
|
+
async listNotes(agentId, subject) {
|
|
584
|
+
await ensureCollection(col);
|
|
585
|
+
const body = {
|
|
586
|
+
with_payload: true,
|
|
587
|
+
with_vector: true,
|
|
588
|
+
limit: 1e4
|
|
589
|
+
};
|
|
590
|
+
if (agentId) body.filter = scopedAgentFilter(agentId, subject);
|
|
591
|
+
const result = await qdrant("POST", `/collections/${col}/points/scroll`, body);
|
|
592
|
+
return result.points.map(pointToNote);
|
|
593
|
+
},
|
|
594
|
+
async deleteNote(id) {
|
|
595
|
+
await ensureCollection(col);
|
|
596
|
+
await qdrant("POST", `/collections/${col}/points/delete`, {
|
|
597
|
+
points: [id]
|
|
598
|
+
});
|
|
599
|
+
},
|
|
600
|
+
/** Story 33: see `updateNoteContent` — returns false, unwritten, when denied. */
|
|
601
|
+
async invalidateNote(id, callerAgentId) {
|
|
602
|
+
await ensureCollection(col);
|
|
603
|
+
if (callerAgentId !== void 0) {
|
|
604
|
+
const existing = await this.getNote(id);
|
|
605
|
+
if (existing && !canWrite(existing, callerAgentId)) return false;
|
|
606
|
+
}
|
|
607
|
+
await qdrant("POST", `/collections/${col}/points/payload?wait=true`, {
|
|
608
|
+
payload: { is_active: false },
|
|
609
|
+
points: [id]
|
|
610
|
+
});
|
|
611
|
+
return true;
|
|
612
|
+
},
|
|
613
|
+
async getNotesByDatePrefix(datePrefix, agentId) {
|
|
614
|
+
await ensureCollection(col);
|
|
615
|
+
const filterClauses = [{ key: "is_active", match: { value: true } }];
|
|
616
|
+
if (!modeBIsolated) {
|
|
617
|
+
filterClauses.push({
|
|
618
|
+
should: [
|
|
619
|
+
{ key: "agent_id", match: { value: agentId } },
|
|
620
|
+
{ key: "agent_id", match: { value: "shared" } }
|
|
621
|
+
]
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
const body = {
|
|
625
|
+
filter: { must: filterClauses },
|
|
626
|
+
with_payload: true,
|
|
627
|
+
with_vector: true,
|
|
628
|
+
limit: 1e4
|
|
629
|
+
};
|
|
630
|
+
const result = await qdrant("POST", `/collections/${col}/points/scroll`, body);
|
|
631
|
+
return result.points.map(pointToNote).filter((n) => n.timestamp.startsWith(datePrefix));
|
|
632
|
+
},
|
|
633
|
+
async countNotes(agentId) {
|
|
634
|
+
await ensureCollection(col);
|
|
635
|
+
const body = { exact: true };
|
|
636
|
+
if (agentId) body.filter = scopedAgentFilter(agentId);
|
|
637
|
+
const result = await qdrant("POST", `/collections/${col}/points/count`, body);
|
|
638
|
+
return result.count;
|
|
639
|
+
},
|
|
640
|
+
async updateNoteLinks(id, links) {
|
|
641
|
+
await ensureCollection(col);
|
|
642
|
+
await qdrant("POST", `/collections/${col}/points/payload?wait=true`, {
|
|
643
|
+
payload: { links },
|
|
644
|
+
points: [id]
|
|
645
|
+
});
|
|
646
|
+
},
|
|
647
|
+
async patchNotePayload(id, fields) {
|
|
648
|
+
await ensureCollection(col);
|
|
649
|
+
await qdrant("POST", `/collections/${col}/points/payload?wait=true`, {
|
|
650
|
+
payload: fields,
|
|
651
|
+
points: [id]
|
|
652
|
+
});
|
|
653
|
+
},
|
|
654
|
+
async replaceLinkReferences(oldId, newId, agentId) {
|
|
655
|
+
const notes = await this.listNotes(agentId);
|
|
656
|
+
for (const note of notes) {
|
|
657
|
+
if (!canWrite(note, agentId)) continue;
|
|
658
|
+
if (note.links.includes(oldId)) {
|
|
659
|
+
const newLinks = note.links.map((linkId) => linkId === oldId ? newId : linkId);
|
|
660
|
+
const filteredLinks = newLinks.filter((linkId) => linkId !== note.id);
|
|
661
|
+
const uniqueLinks = Array.from(new Set(filteredLinks));
|
|
662
|
+
await this.updateNoteLinks(note.id, uniqueLinks);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
function createStorageContext(collectionName, modeBIsolated = false) {
|
|
669
|
+
return makeCrud(collectionName || getCollection(), modeBIsolated);
|
|
670
|
+
}
|
|
671
|
+
async function getNote(id, readerAgentId) {
|
|
672
|
+
return makeCrud(getCollection()).getNote(id, readerAgentId);
|
|
673
|
+
}
|
|
674
|
+
async function updateNote(note) {
|
|
675
|
+
return makeCrud(getCollection()).updateNote(note);
|
|
676
|
+
}
|
|
677
|
+
async function listNotes(agentId, subject) {
|
|
678
|
+
return makeCrud(getCollection()).listNotes(agentId, subject);
|
|
679
|
+
}
|
|
680
|
+
async function deleteNote(id) {
|
|
681
|
+
return makeCrud(getCollection()).deleteNote(id);
|
|
682
|
+
}
|
|
683
|
+
async function invalidateNote(id, callerAgentId) {
|
|
684
|
+
return makeCrud(getCollection()).invalidateNote(id, callerAgentId);
|
|
685
|
+
}
|
|
686
|
+
async function patchNotePayload(id, fields) {
|
|
687
|
+
return makeCrud(getCollection()).patchNotePayload(id, fields);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// src/llm.ts
|
|
691
|
+
var import_sdk = __toESM(require("@anthropic-ai/sdk"), 1);
|
|
692
|
+
var import_openai = __toESM(require("openai"), 1);
|
|
693
|
+
|
|
694
|
+
// src/prompts.ts
|
|
695
|
+
var LOCALE = process.env.AMEM_PROMPT_LOCALE === "zh" ? "zh" : "en";
|
|
696
|
+
var en = {
|
|
697
|
+
crudDecision: (userText, assistantText, memoryList) => `You are a memory management agent. Analyze the conversation and decide what memory operations are needed.
|
|
698
|
+
|
|
699
|
+
## Conversation
|
|
700
|
+
|
|
701
|
+
User: ${userText}
|
|
702
|
+
Assistant: ${assistantText}
|
|
703
|
+
|
|
704
|
+
## Existing relevant memories (identified by integer idx)
|
|
705
|
+
|
|
706
|
+
${memoryList}
|
|
707
|
+
|
|
708
|
+
## Task
|
|
709
|
+
|
|
710
|
+
Extract only genuinely important long-term facts (decisions, preferences, account info, project status, key insights). Skip small talk, confirmations, and information already captured in existing memories.
|
|
711
|
+
|
|
712
|
+
## Operation types
|
|
713
|
+
- NEW: Extract a brand new fact not present in existing memories
|
|
714
|
+
- UPDATE: New information refines or supersedes an existing memory; specify existingIdx
|
|
715
|
+
- DELETE: An existing memory is outdated, contradicted, or wrong; specify existingIdx, fact = original content
|
|
716
|
+
- NONE: Nothing worth recording, or information already fully captured
|
|
717
|
+
|
|
718
|
+
## Output format
|
|
719
|
+
|
|
720
|
+
Return a JSON array. Each item:
|
|
721
|
+
{"action": "NEW"|"UPDATE"|"DELETE"|"NONE", "fact": "fact content", "existingIdx": integer or omit, "reason": "optional"}
|
|
722
|
+
|
|
723
|
+
Return at most 3 operations. If nothing is worth recording, return [].
|
|
724
|
+
Return only the JSON array, no other text.
|
|
725
|
+
|
|
726
|
+
Examples:
|
|
727
|
+
|
|
728
|
+
1. New preference:
|
|
729
|
+
[{"action": "NEW", "fact": "User prefers TypeScript over JavaScript", "reason": "Explicitly stated tech preference"}]
|
|
730
|
+
|
|
731
|
+
2. Updating an existing memory (idx 0 was "User is evaluating React and Vue"):
|
|
732
|
+
[{"action": "UPDATE", "fact": "User decided to use React (dropped Vue)", "existingIdx": 0, "reason": "Decision finalized, update evaluation status"}]
|
|
733
|
+
|
|
734
|
+
3. Conversation is just "Sure, thanks" / "Got it" with no new info:
|
|
735
|
+
[]`,
|
|
736
|
+
shouldMerge: (contentA, contentB) => `You are a memory deduplication assistant. Determine whether two memories express essentially the same information.
|
|
737
|
+
|
|
738
|
+
Memory A: ${contentA}
|
|
739
|
+
Memory B: ${contentB}
|
|
740
|
+
|
|
741
|
+
Rules:
|
|
742
|
+
- If both memories express the same core fact (possibly different wording or granularity), return:
|
|
743
|
+
{"shouldMerge": true, "merged": "Concise merged statement preserving key details from both, more complete than either alone"}
|
|
744
|
+
- If the memories are complementary, on different topics, or contain different specific facts, return:
|
|
745
|
+
{"shouldMerge": false}
|
|
746
|
+
|
|
747
|
+
Return only JSON, no other text.
|
|
748
|
+
|
|
749
|
+
Examples:
|
|
750
|
+
|
|
751
|
+
1. Should merge (different granularity):
|
|
752
|
+
A: "Project uses PostgreSQL"
|
|
753
|
+
B: "Project's primary database is PostgreSQL 16, deployed on AWS RDS"
|
|
754
|
+
-> {"shouldMerge": true, "merged": "Project uses PostgreSQL 16 as primary database, deployed on AWS RDS"}
|
|
755
|
+
|
|
756
|
+
2. Should NOT merge (complementary but distinct):
|
|
757
|
+
A: "User prefers VS Code"
|
|
758
|
+
B: "User's VS Code uses One Dark Pro theme"
|
|
759
|
+
-> {"shouldMerge": false}`,
|
|
760
|
+
evolutionJudge: (oldContent, newContent) => `You are a memory evolution judge. Analyze the relationship between an old and new memory and return JSON.
|
|
761
|
+
|
|
762
|
+
Old memory: ${oldContent}
|
|
763
|
+
New memory: ${newContent}
|
|
764
|
+
|
|
765
|
+
Classification rules:
|
|
766
|
+
|
|
767
|
+
- EVOLVE: New content deepens or updates the old memory (e.g. "Considering Next.js" -> "Decided on Next.js 14 App Router")
|
|
768
|
+
Return: {"type": "EVOLVE", "mergedContent": "Merged content preserving the evolution trajectory"}
|
|
769
|
+
|
|
770
|
+
- CONFLICT: Old and new information directly contradict each other on the same attribute (e.g. "Uses MySQL as primary DB" vs "Migrated to PostgreSQL")
|
|
771
|
+
Return: {"type": "CONFLICT"}
|
|
772
|
+
|
|
773
|
+
- EXPAND: New information supplements the old memory on the same topic (e.g. "Handles backend dev" + "Backend uses Go and gRPC")
|
|
774
|
+
Return: {"type": "EXPAND", "mergedContent": "Merged content integrating both pieces of information"}
|
|
775
|
+
|
|
776
|
+
- NEW: Completely unrelated information, no substantive connection to the old memory
|
|
777
|
+
Return: {"type": "NEW"}
|
|
778
|
+
|
|
779
|
+
Return only JSON, no other text.`,
|
|
780
|
+
conflictScan: (numberedNotes) => `You are auditing a person's memory store for CONTRADICTIONS.
|
|
781
|
+
|
|
782
|
+
Below are numbered memories. Find pairs that CANNOT both be true of the same person at the same time.
|
|
783
|
+
|
|
784
|
+
${numberedNotes}
|
|
785
|
+
|
|
786
|
+
What counts as a contradiction:
|
|
787
|
+
- The same attribute holding two incompatible values ("lives in Paris" vs "moved to Berlin")
|
|
788
|
+
- A stated preference or constraint that a later memory violates ("is vegetarian" vs "loved the steak")
|
|
789
|
+
- A fact that a later memory supersedes ("uses MySQL" vs "migrated to PostgreSQL")
|
|
790
|
+
|
|
791
|
+
What does NOT count \u2014 be strict, these are the common false positives:
|
|
792
|
+
- Additive facts. Two things can both be true ("has a dog named Buddy" + "adopted a second dog, Scout" is NOT a contradiction)
|
|
793
|
+
- Change over time that both memories already acknowledge
|
|
794
|
+
- Merely similar or related topics
|
|
795
|
+
- Different contexts (likes coffee at work, tea at home)
|
|
796
|
+
|
|
797
|
+
For each contradicting pair, also say which one is SUPERSEDED \u2014 the one that is
|
|
798
|
+
no longer true. Judge this from the WORDING, not from any assumed order: phrases
|
|
799
|
+
like "used to", "back in 2019", "moved last month", "switched to" tell you which
|
|
800
|
+
statement describes the past. The memories are NOT listed in chronological order,
|
|
801
|
+
and the number does not imply age.
|
|
802
|
+
|
|
803
|
+
If you cannot tell which one is superseded, set it to null. That is a normal and
|
|
804
|
+
useful answer \u2014 say null rather than guessing, because a wrong guess retires a
|
|
805
|
+
memory that is still true.
|
|
806
|
+
|
|
807
|
+
Return ONLY a JSON array. Empty array if nothing genuinely contradicts:
|
|
808
|
+
[{"a": 0, "b": 3, "superseded": 0, "reason": "one short sentence naming the incompatible attribute"}]
|
|
809
|
+
|
|
810
|
+
"superseded" must be either the value of "a", the value of "b", or null.
|
|
811
|
+
Use the numbers shown. Report a pair once. Prefer returning nothing over guessing.`
|
|
812
|
+
};
|
|
813
|
+
var zh = {
|
|
814
|
+
crudDecision: (userText, assistantText, memoryList) => `\u4F60\u662F\u4E00\u4E2A\u8BB0\u5FC6\u7BA1\u7406 agent\uFF0C\u8D1F\u8D23\u5206\u6790\u5BF9\u8BDD\u5185\u5BB9\u5E76\u51B3\u5B9A\u5982\u4F55\u64CD\u4F5C\u8BB0\u5FC6\u5E93\u3002
|
|
815
|
+
|
|
816
|
+
## \u5BF9\u8BDD\u5185\u5BB9
|
|
817
|
+
|
|
818
|
+
\u7528\u6237\uFF1A${userText}
|
|
819
|
+
\u52A9\u624B\uFF1A${assistantText}
|
|
820
|
+
|
|
821
|
+
## \u5DF2\u6709\u76F8\u5173\u8BB0\u5FC6\uFF08\u7528\u6574\u6570 idx \u6807\u8BC6\uFF09
|
|
822
|
+
|
|
823
|
+
${memoryList}
|
|
824
|
+
|
|
825
|
+
## \u4EFB\u52A1
|
|
826
|
+
|
|
827
|
+
\u5206\u6790\u4E0A\u8FF0\u5BF9\u8BDD\uFF0C\u51B3\u5B9A\u9700\u8981\u54EA\u4E9B\u8BB0\u5FC6\u64CD\u4F5C\u3002\u53EA\u63D0\u53D6\u771F\u6B63\u91CD\u8981\u7684\u957F\u671F\u4E8B\u5B9E\uFF08\u51B3\u7B56\u3001\u504F\u597D\u3001\u8D26\u53F7\u4FE1\u606F\u3001\u9879\u76EE\u72B6\u6001\u3001\u5173\u952E\u6D1E\u5BDF\uFF09\u3002\u8DF3\u8FC7\u95F2\u804A\u3001\u786E\u8BA4\u8BED\u3001\u91CD\u590D\u4FE1\u606F\u3002
|
|
828
|
+
|
|
829
|
+
## \u64CD\u4F5C\u7C7B\u578B
|
|
830
|
+
- NEW\uFF1A\u63D0\u53D6\u5168\u65B0\u4E8B\u5B9E\uFF08\u5DF2\u6709\u8BB0\u5FC6\u4E2D\u6CA1\u6709\u7684\u4FE1\u606F\uFF09
|
|
831
|
+
- UPDATE\uFF1A\u65B0\u4FE1\u606F\u66F4\u65B0\u4E86\u67D0\u6761\u5DF2\u6709\u8BB0\u5FC6\uFF0C\u7528 existingIdx \u6307\u5B9A\u8981\u66F4\u65B0\u7684\u6761\u76EE
|
|
832
|
+
- DELETE\uFF1A\u67D0\u6761\u5DF2\u6709\u8BB0\u5FC6\u5DF2\u7ECF\u8FC7\u65F6\u3001\u53D1\u751F\u51B2\u7A81\u6216\u9519\u8BEF\uFF0C\u7528 existingIdx \u6307\u5B9A\uFF0Cfact \u586B\u539F\u5185\u5BB9
|
|
833
|
+
- NONE\uFF1A\u4E0D\u503C\u5F97\u8BB0\u5F55\u6216\u5DF2\u6709\u5B8C\u5168\u76F8\u540C\u7684\u4FE1\u606F
|
|
834
|
+
|
|
835
|
+
## \u8F93\u51FA\u683C\u5F0F
|
|
836
|
+
|
|
837
|
+
\u8FD4\u56DE JSON \u6570\u7EC4\uFF0C\u6BCF\u6761\u683C\u5F0F\uFF1A
|
|
838
|
+
{"action": "NEW"|"UPDATE"|"DELETE"|"NONE", "fact": "\u4E8B\u5B9E\u5185\u5BB9", "existingIdx": \u6574\u6570\u6216\u7701\u7565, "reason": "\u539F\u56E0\uFF08\u53EF\u9009\uFF09"}
|
|
839
|
+
|
|
840
|
+
\u6BCF\u6B21\u6700\u591A\u8FD4\u56DE 3 \u6761\u64CD\u4F5C\u3002\u5982\u679C\u6CA1\u6709\u503C\u5F97\u64CD\u4F5C\u7684\u5185\u5BB9\uFF0C\u8FD4\u56DE []\u3002
|
|
841
|
+
\u53EA\u8FD4\u56DE JSON \u6570\u7EC4\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3002
|
|
842
|
+
|
|
843
|
+
\u793A\u4F8B\uFF1A
|
|
844
|
+
|
|
845
|
+
1. \u63D0\u53D6\u65B0\u504F\u597D\uFF1A
|
|
846
|
+
[{"action": "NEW", "fact": "\u7528\u6237\u504F\u597D TypeScript \u800C\u975E JavaScript", "reason": "\u660E\u786E\u8868\u8FBE\u7684\u6280\u672F\u504F\u597D"}]
|
|
847
|
+
|
|
848
|
+
2. \u66F4\u65B0\u5DF2\u6709\u8BB0\u5FC6\uFF08idx 0 \u539F\u4E3A"\u7528\u6237\u6B63\u5728\u8BC4\u4F30 React \u548C Vue"\uFF09\uFF1A
|
|
849
|
+
[{"action": "UPDATE", "fact": "\u7528\u6237\u51B3\u5B9A\u4F7F\u7528 React\uFF08\u653E\u5F03\u4E86 Vue\uFF09", "existingIdx": 0, "reason": "\u51B3\u7B56\u5DF2\u660E\u786E\uFF0C\u66F4\u65B0\u8BC4\u4F30\u72B6\u6001"}]
|
|
850
|
+
|
|
851
|
+
3. \u5BF9\u8BDD\u4EC5\u4E3A"\u597D\u7684\uFF0C\u8C22\u8C22"/"\u6CA1\u95EE\u9898"\u7B49\u786E\u8BA4\u8BED\uFF0C\u65E0\u65B0\u4FE1\u606F\uFF1A
|
|
852
|
+
[]`,
|
|
853
|
+
shouldMerge: (contentA, contentB) => `\u4F60\u662F\u4E00\u4E2A\u8BB0\u5FC6\u53BB\u91CD\u52A9\u624B\uFF0C\u8D1F\u8D23\u5224\u65AD\u4E24\u6761\u8BB0\u5FC6\u662F\u5426\u8868\u8FBE\u4E86\u672C\u8D28\u76F8\u540C\u7684\u4FE1\u606F\u3002
|
|
854
|
+
|
|
855
|
+
\u8BB0\u5FC6A\uFF1A${contentA}
|
|
856
|
+
\u8BB0\u5FC6B\uFF1A${contentB}
|
|
857
|
+
|
|
858
|
+
\u5224\u65AD\u89C4\u5219\uFF1A
|
|
859
|
+
- \u5982\u679C\u4E24\u6761\u8BB0\u5FC6\u8868\u8FBE\u7684\u662F\u672C\u8D28\u76F8\u540C\u7684\u4FE1\u606F\uFF08\u53EF\u80FD\u63AA\u8F9E\u4E0D\u540C\u3001\u7C92\u5EA6\u4E0D\u540C\uFF0C\u4F46\u6838\u5FC3\u4E8B\u5B9E\u4E00\u81F4\uFF09\uFF0C\u8FD4\u56DE JSON\uFF1A
|
|
860
|
+
{"shouldMerge": true, "merged": "\u5408\u5E76\u540E\u7684\u7B80\u6D01\u8868\u8FF0\uFF0C\u4FDD\u7559\u4E24\u6761\u8BB0\u5FC6\u7684\u5173\u952E\u4FE1\u606F\uFF0C\u6BD4\u4EFB\u4F55\u4E00\u6761\u90FD\u66F4\u5B8C\u6574"}
|
|
861
|
+
- \u5982\u679C\u4E24\u6761\u8BB0\u5FC6\u662F\u4E92\u8865\u4FE1\u606F\u3001\u4E0D\u540C\u4E3B\u9898\u3001\u6216\u5305\u542B\u4E0D\u540C\u7684\u5177\u4F53\u4E8B\u5B9E\uFF0C\u8FD4\u56DE JSON\uFF1A
|
|
862
|
+
{"shouldMerge": false}
|
|
863
|
+
|
|
864
|
+
\u53EA\u8FD4\u56DE JSON\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3002
|
|
865
|
+
|
|
866
|
+
\u793A\u4F8B\uFF1A
|
|
867
|
+
|
|
868
|
+
1. \u5E94\u5408\u5E76\uFF08\u7C92\u5EA6\u4E0D\u540C\uFF09\uFF1A
|
|
869
|
+
A: "\u9879\u76EE\u4F7F\u7528 PostgreSQL \u6570\u636E\u5E93"
|
|
870
|
+
B: "\u9879\u76EE\u7684\u4E3B\u6570\u636E\u5E93\u662F PostgreSQL 16\uFF0C\u90E8\u7F72\u5728 AWS RDS \u4E0A"
|
|
871
|
+
\u2192 {"shouldMerge": true, "merged": "\u9879\u76EE\u4F7F\u7528 PostgreSQL 16 \u4F5C\u4E3A\u4E3B\u6570\u636E\u5E93\uFF0C\u90E8\u7F72\u5728 AWS RDS \u4E0A"}
|
|
872
|
+
|
|
873
|
+
2. \u4E0D\u5E94\u5408\u5E76\uFF08\u4E92\u8865\u4F46\u4E0D\u540C\uFF09\uFF1A
|
|
874
|
+
A: "\u7528\u6237\u559C\u6B22\u7528 VS Code"
|
|
875
|
+
B: "\u7528\u6237\u7684 VS Code \u4F7F\u7528 One Dark Pro \u4E3B\u9898"
|
|
876
|
+
\u2192 {"shouldMerge": false}`,
|
|
877
|
+
evolutionJudge: (oldContent, newContent) => `\u4F60\u662F\u4E00\u4E2A\u8BB0\u5FC6\u6F14\u5316\u5224\u65AD\u52A9\u624B\u3002\u5206\u6790\u4EE5\u4E0B\u4E24\u6761\u8BB0\u5FC6\u7684\u5173\u7CFB\u5E76\u8FD4\u56DE JSON\u3002
|
|
878
|
+
|
|
879
|
+
\u65E7\u8BB0\u5FC6\uFF1A${oldContent}
|
|
880
|
+
\u65B0\u8BB0\u5FC6\uFF1A${newContent}
|
|
881
|
+
|
|
882
|
+
\u5224\u65AD\u89C4\u5219\uFF1A
|
|
883
|
+
|
|
884
|
+
- EVOLVE\uFF1A\u65B0\u5185\u5BB9\u662F\u5BF9\u65E7\u8BB0\u5FC6\u7684\u6DF1\u5316/\u66F4\u65B0\uFF08\u5982\u300C\u6B63\u5728\u8003\u8651\u7528 Next.js\u300D\u2192\u300C\u51B3\u5B9A\u7528 Next.js 14 App Router\u300D\uFF09
|
|
885
|
+
\u8FD4\u56DE\uFF1A{"type": "EVOLVE", "mergedContent": "\u878D\u5408\u540E\u7684\u5B8C\u6574\u5185\u5BB9\uFF0C\u4FDD\u7559\u6F14\u5316\u8F68\u8FF9"}
|
|
886
|
+
|
|
887
|
+
- CONFLICT\uFF1A\u65B0\u65E7\u4FE1\u606F\u5728\u540C\u4E00\u5C5E\u6027\u4E0A\u76F4\u63A5\u77DB\u76FE\uFF08\u5982\u300C\u4F7F\u7528 MySQL \u4F5C\u4E3A\u4E3B\u6570\u636E\u5E93\u300Dvs\u300C\u5DF2\u8FC1\u79FB\u5230 PostgreSQL\u300D\uFF09
|
|
888
|
+
\u8FD4\u56DE\uFF1A{"type": "CONFLICT"}
|
|
889
|
+
|
|
890
|
+
- EXPAND\uFF1A\u65B0\u4FE1\u606F\u662F\u5BF9\u65E7\u8BB0\u5FC6\u540C\u4E00\u4E3B\u9898\u7684\u8865\u5145\u6269\u5C55\uFF08\u5982\u300C\u8D1F\u8D23\u540E\u7AEF\u5F00\u53D1\u300D+\u300C\u540E\u7AEF\u4F7F\u7528 Go \u548C gRPC\u300D\uFF09
|
|
891
|
+
\u8FD4\u56DE\uFF1A{"type": "EXPAND", "mergedContent": "\u5408\u5E76\u540E\u7684\u5B8C\u6574\u5185\u5BB9\uFF0C\u6574\u5408\u53CC\u65B9\u4FE1\u606F"}
|
|
892
|
+
|
|
893
|
+
- NEW\uFF1A\u5168\u65B0\u4FE1\u606F\uFF0C\u4E0E\u65E7\u8BB0\u5FC6\u65E0\u5B9E\u8D28\u5173\u8054\uFF08\u5982\u300C\u559C\u6B22 dark mode\u300Dvs\u300C\u4E0B\u5468\u8981\u53BB\u51FA\u5DEE\u300D\uFF09
|
|
894
|
+
\u8FD4\u56DE\uFF1A{"type": "NEW"}
|
|
895
|
+
|
|
896
|
+
\u53EA\u8FD4\u56DE JSON\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3002`,
|
|
897
|
+
conflictScan: (numberedNotes) => `\u4F60\u5728\u5BA1\u8BA1\u4E00\u4E2A\u4EBA\u7684\u8BB0\u5FC6\u5E93\uFF0C\u627E\u51FA\u5176\u4E2D**\u4E92\u76F8\u77DB\u76FE**\u7684\u6761\u76EE\u3002
|
|
898
|
+
|
|
899
|
+
\u4E0B\u9762\u662F\u7F16\u53F7\u7684\u8BB0\u5FC6\u3002\u627E\u51FA\u90A3\u4E9B**\u4E0D\u53EF\u80FD\u540C\u65F6\u4E3A\u771F**\u7684\u914D\u5BF9\u3002
|
|
900
|
+
|
|
901
|
+
${numberedNotes}
|
|
902
|
+
|
|
903
|
+
\u7B97\u77DB\u76FE\u7684\u60C5\u51B5\uFF1A
|
|
904
|
+
- \u540C\u4E00\u5C5E\u6027\u4E0A\u51FA\u73B0\u4E92\u65A5\u7684\u503C\uFF08\u300C\u4F4F\u5728\u5DF4\u9ECE\u300Dvs\u300C\u642C\u5230\u4E86\u67CF\u6797\u300D\uFF09
|
|
905
|
+
- \u540E\u6765\u7684\u8BB0\u5FC6\u8FDD\u53CD\u4E86\u5148\u524D\u9648\u8FF0\u7684\u504F\u597D\u6216\u7EA6\u675F\uFF08\u300C\u5403\u7D20\u300Dvs\u300C\u90A3\u5757\u725B\u6392\u5F88\u597D\u5403\u300D\uFF09
|
|
906
|
+
- \u540E\u6765\u7684\u4E8B\u5B9E\u53D6\u4EE3\u4E86\u5148\u524D\u7684\uFF08\u300C\u7528 MySQL\u300Dvs\u300C\u5DF2\u8FC1\u79FB\u5230 PostgreSQL\u300D\uFF09
|
|
907
|
+
|
|
908
|
+
**\u4E0D\u7B97**\u77DB\u76FE \u2014\u2014 \u8BF7\u4E25\u683C\uFF0C\u4EE5\u4E0B\u662F\u6700\u5E38\u89C1\u7684\u8BEF\u5224\uFF1A
|
|
909
|
+
- \u7D2F\u52A0\u7684\u4E8B\u5B9E\u3002\u4E24\u8005\u53EF\u4EE5\u540C\u65F6\u6210\u7ACB\uFF08\u300C\u517B\u4E86\u4E00\u53EA\u72D7\u53EB Buddy\u300D+\u300C\u53C8\u9886\u517B\u4E86\u7B2C\u4E8C\u53EA\u53EB Scout\u300D**\u4E0D\u662F**\u77DB\u76FE\uFF09
|
|
910
|
+
- \u4E24\u6761\u8BB0\u5FC6\u672C\u8EAB\u5DF2\u7ECF\u4F53\u73B0\u4E86\u968F\u65F6\u95F4\u7684\u53D8\u5316
|
|
911
|
+
- \u53EA\u662F\u4E3B\u9898\u76F8\u4F3C\u6216\u76F8\u5173
|
|
912
|
+
- \u573A\u666F\u4E0D\u540C\uFF08\u5728\u516C\u53F8\u559D\u5496\u5561\uFF0C\u5728\u5BB6\u559D\u8336\uFF09
|
|
913
|
+
|
|
914
|
+
\u5BF9\u6BCF\u4E00\u5BF9\u77DB\u76FE\uFF0C\u8FD8\u8981\u6307\u51FA\u54EA\u4E00\u6761\u662F**\u5DF2\u5931\u6548\u7684**\uFF08\u4E0D\u518D\u4E3A\u771F\u7684\u90A3\u6761\uFF09\u3002\u8BF7\u4ECE**\u63AA\u8F9E**\u5224\u65AD\uFF0C\u4E0D\u8981\u5047\u8BBE\u987A\u5E8F\uFF1A
|
|
915
|
+
\u300C\u4EE5\u524D\u300D\u300C2019 \u5E74\u90A3\u4F1A\u513F\u300D\u300C\u4E0A\u4E2A\u6708\u642C\u4E86\u300D\u300C\u6539\u7528\u4E86\u300D\u8FD9\u7C7B\u8BF4\u6CD5\u80FD\u544A\u8BC9\u4F60\u54EA\u6761\u63CF\u8FF0\u7684\u662F\u8FC7\u53BB\u3002
|
|
916
|
+
\u8FD9\u4E9B\u8BB0\u5FC6**\u4E0D\u662F\u6309\u65F6\u95F4\u987A\u5E8F\u6392\u5217\u7684**\uFF0C\u7F16\u53F7\u4E5F\u4E0D\u4EE3\u8868\u65B0\u65E7\u3002
|
|
917
|
+
|
|
918
|
+
\u5982\u679C\u65E0\u6CD5\u5224\u65AD\u54EA\u6761\u5DF2\u5931\u6548\uFF0C\u5C31\u586B null\u3002\u8FD9\u662F\u4E00\u4E2A**\u6B63\u5E38\u4E14\u6709\u7528**\u7684\u56DE\u7B54 \u2014\u2014 \u5B81\u53EF\u586B null \u4E5F\u4E0D\u8981\u731C\uFF0C
|
|
919
|
+
\u56E0\u4E3A\u731C\u9519\u4F1A\u8BA9\u4E00\u6761**\u4ECD\u7136\u4E3A\u771F**\u7684\u8BB0\u5FC6\u88AB\u505C\u7528\u3002
|
|
920
|
+
|
|
921
|
+
\u53EA\u8FD4\u56DE JSON \u6570\u7EC4\u3002\u6CA1\u6709\u771F\u6B63\u77DB\u76FE\u5C31\u8FD4\u56DE\u7A7A\u6570\u7EC4\uFF1A
|
|
922
|
+
[{"a": 0, "b": 3, "superseded": 0, "reason": "\u4E00\u53E5\u8BDD\u8BF4\u660E\u662F\u54EA\u4E2A\u5C5E\u6027\u4E92\u65A5"}]
|
|
923
|
+
|
|
924
|
+
"superseded" \u53EA\u80FD\u662F "a" \u7684\u503C\u3001"b" \u7684\u503C\uFF0C\u6216 null\u3002
|
|
925
|
+
\u4F7F\u7528\u4E0A\u9762\u663E\u793A\u7684\u7F16\u53F7\u3002\u540C\u4E00\u5BF9\u53EA\u62A5\u4E00\u6B21\u3002**\u5B81\u53EF\u4E0D\u62A5\uFF0C\u4E5F\u4E0D\u8981\u731C\u3002**`
|
|
926
|
+
};
|
|
927
|
+
var templates = { en, zh };
|
|
928
|
+
var t = templates[LOCALE];
|
|
929
|
+
|
|
930
|
+
// src/llm.ts
|
|
931
|
+
var _override = {};
|
|
932
|
+
function configureLlm(cfg) {
|
|
933
|
+
_override = { ...cfg };
|
|
934
|
+
_anthropicClients.clear();
|
|
935
|
+
_openaiClients.clear();
|
|
936
|
+
}
|
|
937
|
+
var _warned = /* @__PURE__ */ new Set();
|
|
938
|
+
function warnOnce(key, message) {
|
|
939
|
+
if (_warned.has(key)) return;
|
|
940
|
+
_warned.add(key);
|
|
941
|
+
console.error(message);
|
|
942
|
+
}
|
|
943
|
+
function resolveProvider(role = "fast") {
|
|
944
|
+
const raw = role === "strong" ? process.env.AMEM_LLM_STRONG_PROVIDER || _override.strong?.provider || void 0 : void 0;
|
|
945
|
+
const p = (raw || process.env.AMEM_LLM_PROVIDER || _override.provider || "anthropic").trim().toLowerCase();
|
|
946
|
+
if (p !== "anthropic" && p !== "openai") {
|
|
947
|
+
warnOnce(`provider:${p}`, `[amem] unknown LLM provider "${p}"; falling back to anthropic`);
|
|
948
|
+
}
|
|
949
|
+
return p;
|
|
950
|
+
}
|
|
951
|
+
function resolveModel(role = "fast") {
|
|
952
|
+
const strong = role === "strong" ? process.env.AMEM_LLM_STRONG_MODEL || _override.strong?.model || void 0 : void 0;
|
|
953
|
+
return strong || process.env.AMEM_LLM_MODEL || _override.model || (resolveProvider(role) === "openai" ? "gpt-4o-mini" : "claude-sonnet-4-6");
|
|
954
|
+
}
|
|
955
|
+
function resolveBaseURL(role = "fast") {
|
|
956
|
+
const strong = role === "strong" ? process.env.AMEM_LLM_STRONG_BASE_URL || _override.strong?.baseURL || void 0 : void 0;
|
|
957
|
+
return strong || process.env.AMEM_LLM_BASE_URL || _override.baseURL || void 0;
|
|
958
|
+
}
|
|
959
|
+
function resolveCrudRole() {
|
|
960
|
+
const raw = (process.env.AMEM_LLM_CRUD_ROLE || _override.crudRole || "fast").trim().toLowerCase();
|
|
961
|
+
if (raw === "strong") return "strong";
|
|
962
|
+
if (raw !== "fast") {
|
|
963
|
+
warnOnce(`crudRole:${raw}`, `[amem] unknown AMEM_LLM_CRUD_ROLE "${raw}"; using fast`);
|
|
964
|
+
}
|
|
965
|
+
return "fast";
|
|
966
|
+
}
|
|
967
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
968
|
+
function resolveTimeoutMs() {
|
|
969
|
+
const envVal = Number(process.env.AMEM_LLM_TIMEOUT);
|
|
970
|
+
if (Number.isFinite(envVal) && envVal > 0) return envVal;
|
|
971
|
+
if (_override.timeoutMs && _override.timeoutMs > 0) return _override.timeoutMs;
|
|
972
|
+
return DEFAULT_TIMEOUT_MS;
|
|
973
|
+
}
|
|
974
|
+
var _anthropicClients = /* @__PURE__ */ new Map();
|
|
975
|
+
function anthropic(baseURL) {
|
|
976
|
+
const key = baseURL ?? "";
|
|
977
|
+
let client = _anthropicClients.get(key);
|
|
978
|
+
if (!client) {
|
|
979
|
+
client = new import_sdk.default({
|
|
980
|
+
...process.env.AMEM_LLM_API_KEY && { apiKey: process.env.AMEM_LLM_API_KEY },
|
|
981
|
+
...baseURL && { baseURL },
|
|
982
|
+
timeout: resolveTimeoutMs()
|
|
983
|
+
});
|
|
984
|
+
_anthropicClients.set(key, client);
|
|
985
|
+
}
|
|
986
|
+
return client;
|
|
987
|
+
}
|
|
988
|
+
var _openaiClients = /* @__PURE__ */ new Map();
|
|
989
|
+
function openai(baseURL) {
|
|
990
|
+
const key = baseURL ?? "";
|
|
991
|
+
let client = _openaiClients.get(key);
|
|
992
|
+
if (!client) {
|
|
993
|
+
client = new import_openai.default({
|
|
994
|
+
// AMEM_LLM_API_KEY first (engine convention), then the SDK's own
|
|
995
|
+
// OPENAI_API_KEY (the standard) — passing an explicit key blocks the SDK's
|
|
996
|
+
// env fallback, so read it here. Placeholder last, so keyless local servers
|
|
997
|
+
// (Ollama, vLLM) still work.
|
|
998
|
+
apiKey: process.env.AMEM_LLM_API_KEY || process.env.OPENAI_API_KEY || "sk-no-key-required",
|
|
999
|
+
...baseURL && { baseURL },
|
|
1000
|
+
timeout: resolveTimeoutMs()
|
|
1001
|
+
});
|
|
1002
|
+
_openaiClients.set(key, client);
|
|
1003
|
+
}
|
|
1004
|
+
return client;
|
|
1005
|
+
}
|
|
1006
|
+
async function llmCall(prompt, maxTokens = 500, role = "fast") {
|
|
1007
|
+
const provider = resolveProvider(role);
|
|
1008
|
+
const model = resolveModel(role);
|
|
1009
|
+
const baseURL = resolveBaseURL(role);
|
|
1010
|
+
const isThinking = model.includes("gemini") || model.includes("pro-agent");
|
|
1011
|
+
const effectiveMaxTokens = isThinking ? Math.max(maxTokens * 8, 4e3) : maxTokens;
|
|
1012
|
+
try {
|
|
1013
|
+
return provider === "openai" ? await openaiCall(prompt, model, effectiveMaxTokens, baseURL) : await anthropicCall(prompt, model, effectiveMaxTokens, baseURL);
|
|
1014
|
+
} catch (e) {
|
|
1015
|
+
console.error(`[amem] LLM call failed: ${e.message}`);
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
async function anthropicCall(prompt, model, maxTokens, baseURL) {
|
|
1020
|
+
const resp = await anthropic(baseURL).messages.create({
|
|
1021
|
+
model,
|
|
1022
|
+
max_tokens: maxTokens,
|
|
1023
|
+
messages: [{ role: "user", content: prompt }]
|
|
1024
|
+
});
|
|
1025
|
+
for (const block of resp.content) {
|
|
1026
|
+
if (block.type === "text") return block.text.trim();
|
|
1027
|
+
}
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
async function openaiCall(prompt, model, maxTokens, baseURL) {
|
|
1031
|
+
const isReasoning = /^o\d/.test(model) || model.startsWith("gpt-5");
|
|
1032
|
+
const resp = await openai(baseURL).chat.completions.create({
|
|
1033
|
+
model,
|
|
1034
|
+
...isReasoning ? { max_completion_tokens: maxTokens } : { max_tokens: maxTokens },
|
|
1035
|
+
messages: [{ role: "user", content: prompt }]
|
|
1036
|
+
});
|
|
1037
|
+
return resp.choices[0]?.message?.content?.trim() ?? null;
|
|
1038
|
+
}
|
|
1039
|
+
function stripReasoning(raw) {
|
|
1040
|
+
return raw.replace(/<think>[\s\S]*?<\/think>/gi, "").replace(/<\|(?:eot_id|im_start|im_end|begin_of_text|end_of_text|endoftext)\|>/g, "").trim();
|
|
1041
|
+
}
|
|
1042
|
+
function stripFences(raw) {
|
|
1043
|
+
raw = stripReasoning(raw);
|
|
1044
|
+
if (raw.startsWith("```")) {
|
|
1045
|
+
const lines = raw.split("\n");
|
|
1046
|
+
lines.shift();
|
|
1047
|
+
if (lines[lines.length - 1] === "```") lines.pop();
|
|
1048
|
+
raw = lines.join("\n").trim();
|
|
1049
|
+
}
|
|
1050
|
+
if (raw.startsWith('"') && raw.endsWith('"') || raw.startsWith("'") && raw.endsWith("'")) {
|
|
1051
|
+
try {
|
|
1052
|
+
raw = JSON.parse(raw);
|
|
1053
|
+
} catch {
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return raw;
|
|
1057
|
+
}
|
|
1058
|
+
function parseJsonLoose(raw) {
|
|
1059
|
+
const cleaned = stripFences(raw);
|
|
1060
|
+
try {
|
|
1061
|
+
return JSON.parse(cleaned);
|
|
1062
|
+
} catch (e) {
|
|
1063
|
+
const m = cleaned.match(/\{[\s\S]*\}/);
|
|
1064
|
+
if (m) return JSON.parse(m[0]);
|
|
1065
|
+
throw e;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
var VALID_CONFIDENCE = /* @__PURE__ */ new Set(["high", "medium", "low"]);
|
|
1069
|
+
var VALID_CATEGORIES = /* @__PURE__ */ new Set([
|
|
1070
|
+
"Technical",
|
|
1071
|
+
"Business",
|
|
1072
|
+
"Personal",
|
|
1073
|
+
"Project",
|
|
1074
|
+
"Research",
|
|
1075
|
+
"System",
|
|
1076
|
+
"General"
|
|
1077
|
+
]);
|
|
1078
|
+
async function llmConstructNote(content) {
|
|
1079
|
+
const prompt = `Analyze the following text and respond with valid JSON only (no markdown fences, no explanation, no comments). All string values must use standard double quotes and be properly escaped:
|
|
1080
|
+
{
|
|
1081
|
+
"keywords": ["keyword1", "keyword2"],
|
|
1082
|
+
"tags": ["tag1", "tag2"],
|
|
1083
|
+
"context": "one sentence summary in the same language as the input",
|
|
1084
|
+
"category": "Technical|Business|Personal|Project|Research|System|General",
|
|
1085
|
+
"note_type": "memory|knowledge",
|
|
1086
|
+
"topics": ["Topic1", "Topic2"],
|
|
1087
|
+
"confidence": "high|medium|low"
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
Category guide:
|
|
1091
|
+
- Technical: code, tools, configuration, APIs, debugging
|
|
1092
|
+
- Business: company, finance, compliance, contracts, invoices
|
|
1093
|
+
- Personal: personal state, habits, preferences, emotions
|
|
1094
|
+
- Project: project progress, decisions, milestones
|
|
1095
|
+
- Research: research, literature, evaluation, comparison
|
|
1096
|
+
- System: system services, monitoring, operations
|
|
1097
|
+
- General: anything that does not fit the above
|
|
1098
|
+
|
|
1099
|
+
note_type guide:
|
|
1100
|
+
- knowledge: books, methodologies, tools, domain knowledge, reference material \u2014 durable, no strong time component
|
|
1101
|
+
- memory: events, decisions, preferences, states, observations \u2014 episodic, time-sensitive
|
|
1102
|
+
|
|
1103
|
+
topics guide (Story 26B):
|
|
1104
|
+
- Only populate for knowledge notes (note_type=knowledge). For memory notes, return [].
|
|
1105
|
+
- List 1-5 concise subject tags representing the main topics of this knowledge, e.g. ["TypeScript", "Qdrant", "Vector DB"].
|
|
1106
|
+
|
|
1107
|
+
confidence guide (Story 27):
|
|
1108
|
+
- high: note_type is unambiguous \u2014 clearly episodic (event/decision/state) or clearly durable knowledge (tool doc/methodology)
|
|
1109
|
+
- medium: some ambiguity \u2014 e.g. "learned X method" could be either memory or knowledge
|
|
1110
|
+
- low: LLM is uncertain \u2014 vague, fragmentary, or mixed content
|
|
1111
|
+
|
|
1112
|
+
Text: ${content}`;
|
|
1113
|
+
const raw = await llmCall(prompt, 400);
|
|
1114
|
+
if (!raw)
|
|
1115
|
+
return {
|
|
1116
|
+
keywords: [],
|
|
1117
|
+
tags: [],
|
|
1118
|
+
context: "",
|
|
1119
|
+
category: "General",
|
|
1120
|
+
note_type: "memory",
|
|
1121
|
+
topics: [],
|
|
1122
|
+
confidence: "medium"
|
|
1123
|
+
};
|
|
1124
|
+
try {
|
|
1125
|
+
const data = parseJsonLoose(raw);
|
|
1126
|
+
const rawCategory = typeof data.category === "string" ? data.category : "General";
|
|
1127
|
+
const category = VALID_CATEGORIES.has(rawCategory) ? rawCategory : "General";
|
|
1128
|
+
const note_type = data.note_type === "knowledge" ? "knowledge" : "memory";
|
|
1129
|
+
const topics = note_type === "knowledge" && Array.isArray(data.topics) ? data.topics.filter((v) => typeof v === "string") : [];
|
|
1130
|
+
const rawConfidence = typeof data.confidence === "string" ? data.confidence : "medium";
|
|
1131
|
+
const confidence = VALID_CONFIDENCE.has(rawConfidence) ? rawConfidence : "medium";
|
|
1132
|
+
return {
|
|
1133
|
+
keywords: Array.isArray(data.keywords) ? data.keywords : [],
|
|
1134
|
+
tags: Array.isArray(data.tags) ? data.tags : [],
|
|
1135
|
+
context: typeof data.context === "string" ? data.context : "",
|
|
1136
|
+
category,
|
|
1137
|
+
note_type,
|
|
1138
|
+
topics,
|
|
1139
|
+
confidence
|
|
1140
|
+
};
|
|
1141
|
+
} catch (e) {
|
|
1142
|
+
console.error(`[amem] Note construction parse failed: ${e.message}`);
|
|
1143
|
+
return {
|
|
1144
|
+
keywords: [],
|
|
1145
|
+
tags: [],
|
|
1146
|
+
context: "",
|
|
1147
|
+
category: "General",
|
|
1148
|
+
note_type: "memory",
|
|
1149
|
+
topics: [],
|
|
1150
|
+
confidence: "medium"
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
async function llmShouldLink(noteContent, candidateContent) {
|
|
1155
|
+
const prompt = `Do these two memory notes have a meaningful relationship that would be useful to link?
|
|
1156
|
+
Reply with only "yes" or "no".
|
|
1157
|
+
|
|
1158
|
+
Note A: ${noteContent}
|
|
1159
|
+
Note B: ${candidateContent}`;
|
|
1160
|
+
const raw = await llmCall(prompt, 10);
|
|
1161
|
+
if (!raw) return false;
|
|
1162
|
+
return raw.toLowerCase().startsWith("yes");
|
|
1163
|
+
}
|
|
1164
|
+
async function llmCrudDecision(userText, assistantText, existingMemories) {
|
|
1165
|
+
const memoryList = existingMemories.length > 0 ? existingMemories.map((m) => `[${m.idx}] ${m.content}`).join("\n") : "(none)";
|
|
1166
|
+
const prompt = t.crudDecision(userText.slice(0, 500), assistantText.slice(0, 500), memoryList);
|
|
1167
|
+
try {
|
|
1168
|
+
const raw = await llmCall(prompt, 400, resolveCrudRole());
|
|
1169
|
+
if (!raw) return [];
|
|
1170
|
+
const match = stripReasoning(raw).match(/\[.*\]/s);
|
|
1171
|
+
if (!match) return [];
|
|
1172
|
+
const parsed = JSON.parse(match[0]);
|
|
1173
|
+
if (!Array.isArray(parsed)) return [];
|
|
1174
|
+
const ops = [];
|
|
1175
|
+
for (const item of parsed) {
|
|
1176
|
+
if (!item || typeof item !== "object") continue;
|
|
1177
|
+
const action = item.action;
|
|
1178
|
+
if (!["NEW", "UPDATE", "DELETE", "NONE"].includes(action)) continue;
|
|
1179
|
+
if (action === "NONE") continue;
|
|
1180
|
+
const op = {
|
|
1181
|
+
action,
|
|
1182
|
+
fact: typeof item.fact === "string" ? item.fact : "",
|
|
1183
|
+
reason: typeof item.reason === "string" ? item.reason : void 0
|
|
1184
|
+
};
|
|
1185
|
+
if (typeof item.existingIdx === "number") {
|
|
1186
|
+
op.existingIdx = item.existingIdx;
|
|
1187
|
+
}
|
|
1188
|
+
ops.push(op);
|
|
1189
|
+
}
|
|
1190
|
+
return ops.slice(0, 3);
|
|
1191
|
+
} catch (e) {
|
|
1192
|
+
console.error(`[amem] llmCrudDecision failed: ${e.message}`);
|
|
1193
|
+
return [];
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
async function llmShouldMerge(contentA, contentB) {
|
|
1197
|
+
const prompt = t.shouldMerge(contentA, contentB);
|
|
1198
|
+
const raw = await llmCall(prompt, 300, "strong");
|
|
1199
|
+
if (!raw) return { shouldMerge: false };
|
|
1200
|
+
try {
|
|
1201
|
+
const data = parseJsonLoose(raw);
|
|
1202
|
+
if (typeof data.shouldMerge !== "boolean") return { shouldMerge: false };
|
|
1203
|
+
if (data.shouldMerge && typeof data.merged === "string") {
|
|
1204
|
+
return { shouldMerge: true, merged: data.merged };
|
|
1205
|
+
}
|
|
1206
|
+
return { shouldMerge: false };
|
|
1207
|
+
} catch (e) {
|
|
1208
|
+
console.error(`[amem] llmShouldMerge parse failed: ${e.message}`);
|
|
1209
|
+
return { shouldMerge: false };
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
var VALID_EVOLUTION_TYPES = /* @__PURE__ */ new Set(["EVOLVE", "CONFLICT", "EXPAND", "NEW"]);
|
|
1213
|
+
async function llmEvolutionJudge(oldContent, newContent) {
|
|
1214
|
+
const prompt = t.evolutionJudge(oldContent, newContent);
|
|
1215
|
+
const raw = await llmCall(prompt, 300, "strong");
|
|
1216
|
+
if (!raw) return { type: "NEW" };
|
|
1217
|
+
try {
|
|
1218
|
+
const data = parseJsonLoose(raw);
|
|
1219
|
+
const type = VALID_EVOLUTION_TYPES.has(data.type) ? data.type : "NEW";
|
|
1220
|
+
return {
|
|
1221
|
+
type,
|
|
1222
|
+
mergedContent: typeof data.mergedContent === "string" ? data.mergedContent : void 0
|
|
1223
|
+
};
|
|
1224
|
+
} catch (e) {
|
|
1225
|
+
console.error(`[amem] llmEvolutionJudge parse failed: ${e.message}`);
|
|
1226
|
+
return { type: "NEW" };
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
async function llmEvolveNote(content, linkedNotes) {
|
|
1230
|
+
const linkedStr = linkedNotes.map((n) => `- ID: ${n.id}
|
|
1231
|
+
Content: ${n.content}`).join("\n");
|
|
1232
|
+
const prompt = `A memory note has gained new connections. Update its context, tags, and decide whether to strengthen connections with specific neighbors.
|
|
1233
|
+
Reply with JSON only (no markdown):
|
|
1234
|
+
{
|
|
1235
|
+
"tags": ["tag1", "tag2", ...],
|
|
1236
|
+
"context": "updated one sentence summary",
|
|
1237
|
+
"should_strengthen": true|false,
|
|
1238
|
+
"suggested_connections": ["neighbor_id_1", "neighbor_id_2", ...],
|
|
1239
|
+
"tags_to_update": ["tag_1", ..., "tag_n"]
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
Guidelines:
|
|
1243
|
+
- "tags" and "context" are for updating the original note based on new connections.
|
|
1244
|
+
- "should_strengthen" is a decision whether this note should strengthen its connections to any of the newly linked notes (neighbors).
|
|
1245
|
+
- "suggested_connections" must contain only IDs from the newly linked notes (neighbors) listed below.
|
|
1246
|
+
- "tags_to_update" are updated tags for the original note itself if we strengthen connections.
|
|
1247
|
+
|
|
1248
|
+
Original note content: ${content}
|
|
1249
|
+
|
|
1250
|
+
Newly linked notes (neighbors):
|
|
1251
|
+
${linkedStr}`;
|
|
1252
|
+
const raw = await llmCall(prompt, 500);
|
|
1253
|
+
if (!raw) return { tags: null, context: null, shouldStrengthen: false, suggestedConnections: [], tagsToUpdate: [] };
|
|
1254
|
+
try {
|
|
1255
|
+
const data = parseJsonLoose(raw);
|
|
1256
|
+
return {
|
|
1257
|
+
tags: Array.isArray(data.tags) ? data.tags : null,
|
|
1258
|
+
context: typeof data.context === "string" ? data.context : null,
|
|
1259
|
+
shouldStrengthen: typeof data.should_strengthen === "boolean" ? data.should_strengthen : false,
|
|
1260
|
+
suggestedConnections: Array.isArray(data.suggested_connections) ? data.suggested_connections.map(String) : [],
|
|
1261
|
+
tagsToUpdate: Array.isArray(data.tags_to_update) ? data.tags_to_update.map(String) : []
|
|
1262
|
+
};
|
|
1263
|
+
} catch (e) {
|
|
1264
|
+
console.error(`[amem] Evolution parse failed: ${e.message}`);
|
|
1265
|
+
return { tags: null, context: null, shouldStrengthen: false, suggestedConnections: [], tagsToUpdate: [] };
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
async function llmConflictScan(contents) {
|
|
1269
|
+
if (contents.length < 2) return [];
|
|
1270
|
+
const numbered = contents.map((c, i) => `[${i}] ${c}`).join("\n");
|
|
1271
|
+
try {
|
|
1272
|
+
const raw = await llmCall(t.conflictScan(numbered), 600, "strong");
|
|
1273
|
+
if (!raw) return [];
|
|
1274
|
+
const cleaned = stripReasoning(raw);
|
|
1275
|
+
const match = cleaned.match(/\[[\s\S]*\]/);
|
|
1276
|
+
if (!match) return [];
|
|
1277
|
+
const parsed = JSON.parse(match[0]);
|
|
1278
|
+
if (!Array.isArray(parsed)) return [];
|
|
1279
|
+
const pairs = [];
|
|
1280
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1281
|
+
for (const item of parsed) {
|
|
1282
|
+
if (!item || typeof item !== "object") continue;
|
|
1283
|
+
const { a, b } = item;
|
|
1284
|
+
if (typeof a !== "number" || typeof b !== "number") continue;
|
|
1285
|
+
if (!Number.isInteger(a) || !Number.isInteger(b)) continue;
|
|
1286
|
+
if (a < 0 || b < 0 || a >= contents.length || b >= contents.length) continue;
|
|
1287
|
+
if (a === b) continue;
|
|
1288
|
+
const key = a < b ? `${a}:${b}` : `${b}:${a}`;
|
|
1289
|
+
if (seen.has(key)) continue;
|
|
1290
|
+
seen.add(key);
|
|
1291
|
+
const rawSup = item.superseded;
|
|
1292
|
+
const supersededIndex = rawSup === a || rawSup === b ? rawSup : null;
|
|
1293
|
+
pairs.push({
|
|
1294
|
+
a,
|
|
1295
|
+
b,
|
|
1296
|
+
reason: typeof item.reason === "string" ? item.reason : "",
|
|
1297
|
+
supersededIndex
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
return pairs;
|
|
1301
|
+
} catch (e) {
|
|
1302
|
+
console.error(`[amem] llmConflictScan failed: ${e.message}`);
|
|
1303
|
+
return [];
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// src/evo-counter.ts
|
|
1308
|
+
var fs = __toESM(require("fs"), 1);
|
|
1309
|
+
var path2 = __toESM(require("path"), 1);
|
|
1310
|
+
function counterFile() {
|
|
1311
|
+
return process.env.AMEM_EVO_COUNTER_PATH || path2.join(getDataDir(), "amem_evo_cnt.json");
|
|
1312
|
+
}
|
|
1313
|
+
var EVO_THRESHOLD = 20;
|
|
1314
|
+
function getEvoCount() {
|
|
1315
|
+
try {
|
|
1316
|
+
const data = JSON.parse(fs.readFileSync(counterFile(), "utf-8"));
|
|
1317
|
+
return data.count || 0;
|
|
1318
|
+
} catch {
|
|
1319
|
+
return 0;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
function incrementEvoCount() {
|
|
1323
|
+
const count = getEvoCount() + 1;
|
|
1324
|
+
fs.writeFileSync(counterFile(), JSON.stringify({ count, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
1325
|
+
return count;
|
|
1326
|
+
}
|
|
1327
|
+
function shouldRunEvolution() {
|
|
1328
|
+
const count = incrementEvoCount();
|
|
1329
|
+
return count % EVO_THRESHOLD === 0;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// src/memory.ts
|
|
1333
|
+
var import_jieba = require("@node-rs/jieba");
|
|
1334
|
+
var logSafe = (id) => id.replace(/[\r\n]/g, "");
|
|
1335
|
+
var _jieba = null;
|
|
1336
|
+
function getJieba() {
|
|
1337
|
+
if (!_jieba) _jieba = new import_jieba.Jieba();
|
|
1338
|
+
return _jieba;
|
|
1339
|
+
}
|
|
1340
|
+
function simpleTokenize(text) {
|
|
1341
|
+
const hasChinese = /[\u4e00-\u9fff]/.test(text);
|
|
1342
|
+
if (hasChinese) {
|
|
1343
|
+
return getJieba().cut(text, true).map((t2) => t2.toLowerCase().trim()).filter((t2) => t2.length > 0 && /[\w\u4e00-\u9fff]/.test(t2));
|
|
1344
|
+
}
|
|
1345
|
+
return Array.from(text.toLowerCase().matchAll(/[\w]+/g)).map((m) => m[0]);
|
|
1346
|
+
}
|
|
1347
|
+
function buildBM25(notes) {
|
|
1348
|
+
const ids = notes.map((n) => n.id);
|
|
1349
|
+
const corpus = notes.map((n) => {
|
|
1350
|
+
const text = [n.content, ...n.keywords, ...n.tags].join(" ");
|
|
1351
|
+
return simpleTokenize(text);
|
|
1352
|
+
});
|
|
1353
|
+
const df = /* @__PURE__ */ new Map();
|
|
1354
|
+
for (const tokens of corpus) {
|
|
1355
|
+
for (const t2 of new Set(tokens)) df.set(t2, (df.get(t2) ?? 0) + 1);
|
|
1356
|
+
}
|
|
1357
|
+
const N = corpus.length;
|
|
1358
|
+
const idf = /* @__PURE__ */ new Map();
|
|
1359
|
+
df.forEach((freq, term) => {
|
|
1360
|
+
idf.set(term, Math.log((N - freq + 0.5) / (freq + 0.5) + 1));
|
|
1361
|
+
});
|
|
1362
|
+
const avgdl = corpus.reduce((s, t2) => s + t2.length, 0) / Math.max(N, 1);
|
|
1363
|
+
return { ids, corpus, idf, avgdl };
|
|
1364
|
+
}
|
|
1365
|
+
function bm25Score(state, queryTokens, k1 = 1.5, b = 0.75) {
|
|
1366
|
+
const scores = state.ids.map((id, i) => {
|
|
1367
|
+
const doc = state.corpus[i];
|
|
1368
|
+
const dl = doc.length;
|
|
1369
|
+
const tf = /* @__PURE__ */ new Map();
|
|
1370
|
+
for (const t2 of doc) tf.set(t2, (tf.get(t2) ?? 0) + 1);
|
|
1371
|
+
let score = 0;
|
|
1372
|
+
for (const t2 of queryTokens) {
|
|
1373
|
+
const f = tf.get(t2) ?? 0;
|
|
1374
|
+
if (f === 0) continue;
|
|
1375
|
+
const idfVal = state.idf.get(t2) ?? 0;
|
|
1376
|
+
score += idfVal * (f * (k1 + 1) / (f + k1 * (1 - b + b * (dl / state.avgdl))));
|
|
1377
|
+
}
|
|
1378
|
+
return [id, score];
|
|
1379
|
+
});
|
|
1380
|
+
return scores.sort((a, b2) => b2[1] - a[1]);
|
|
1381
|
+
}
|
|
1382
|
+
function rrfMerge(embIds, bm25Ids, k = 60) {
|
|
1383
|
+
const scores = /* @__PURE__ */ new Map();
|
|
1384
|
+
embIds.forEach((id, rank) => scores.set(id, (scores.get(id) ?? 0) + 1 / (k + rank + 1)));
|
|
1385
|
+
bm25Ids.forEach((id, rank) => scores.set(id, (scores.get(id) ?? 0) + 1 / (k + rank + 1)));
|
|
1386
|
+
return Array.from(scores.entries()).sort((a, b) => b[1] - a[1]);
|
|
1387
|
+
}
|
|
1388
|
+
function buildEmbedText(note) {
|
|
1389
|
+
let text = note.content;
|
|
1390
|
+
if (note.keywords.length) text += " " + note.keywords.join(" ");
|
|
1391
|
+
if (note.tags.length) text += " " + note.tags.join(" ");
|
|
1392
|
+
if (note.context) text += " " + note.context;
|
|
1393
|
+
return text;
|
|
1394
|
+
}
|
|
1395
|
+
var EPHEMERAL_SIGNALS = ["\u5F85\u8DD1", "\u7B49\u786E\u8BA4", "\u6628\u65E5", "\u660E\u5929\u5B8C\u6210"];
|
|
1396
|
+
function checkQuality(content) {
|
|
1397
|
+
const trimmed = content.trim();
|
|
1398
|
+
if (trimmed.length < 10) {
|
|
1399
|
+
return { ok: false, ephemeral: false, reason: `\u5185\u5BB9\u8FC7\u77ED\uFF08${trimmed.length} \u5B57\uFF0C\u6700\u5C11 10 \u5B57\uFF09` };
|
|
1400
|
+
}
|
|
1401
|
+
const ephemeral = EPHEMERAL_SIGNALS.some((w) => trimmed.includes(w));
|
|
1402
|
+
return { ok: true, ephemeral };
|
|
1403
|
+
}
|
|
1404
|
+
function defaultCtx() {
|
|
1405
|
+
return createStorageContext();
|
|
1406
|
+
}
|
|
1407
|
+
async function addMemory(content, agentId = "main", opts) {
|
|
1408
|
+
const scope = opts?.scope ?? "private";
|
|
1409
|
+
const subjects = opts?.subjects ?? [];
|
|
1410
|
+
const ctx = opts?.storageCtx ?? defaultCtx();
|
|
1411
|
+
const quality = checkQuality(content);
|
|
1412
|
+
if (!quality.ok) {
|
|
1413
|
+
throw new Error(`[quality] \u5199\u5165\u62D2\u7EDD: ${quality.reason}`);
|
|
1414
|
+
}
|
|
1415
|
+
const effectiveNoteAgentId = scope === "shared" ? "shared" : agentId;
|
|
1416
|
+
const hash = (0, import_crypto.createHash)("md5").update(content).digest("hex");
|
|
1417
|
+
const existingByHash = await ctx.findByHash(hash, agentId);
|
|
1418
|
+
if (existingByHash) {
|
|
1419
|
+
console.log(`[add] dedup: exact hash match, skipping (id=${existingByHash.id.slice(0, 8)})`);
|
|
1420
|
+
return existingByHash.id;
|
|
1421
|
+
}
|
|
1422
|
+
console.log("[add] Constructing note...");
|
|
1423
|
+
const { keywords, tags, context, category, note_type, topics } = await llmConstructNote(content);
|
|
1424
|
+
console.log(` keywords: ${keywords.join(", ")}`);
|
|
1425
|
+
console.log(` tags: ${tags.join(", ")}`);
|
|
1426
|
+
console.log(` context: ${context}`);
|
|
1427
|
+
console.log(` category: ${category}`);
|
|
1428
|
+
console.log(` note_type: ${note_type}`);
|
|
1429
|
+
console.log(` topics: ${topics.join(", ")}`);
|
|
1430
|
+
const fieldsText = buildEmbedText({ content, keywords, tags, context });
|
|
1431
|
+
const embedding = await encode(fieldsText);
|
|
1432
|
+
const topMatch = await ctx.queryByEmbedding(embedding, 1, agentId, 0);
|
|
1433
|
+
if (topMatch.length > 0 && topMatch[0].score >= 0.85) {
|
|
1434
|
+
if (canWrite(topMatch[0].note, agentId)) {
|
|
1435
|
+
console.log(`[add] dedup: high-sim match (sim=${topMatch[0].score.toFixed(3)}), updating existing`);
|
|
1436
|
+
await ctx.updateNoteContent(topMatch[0].note.id, content, embedding, hash);
|
|
1437
|
+
return topMatch[0].note.id;
|
|
1438
|
+
}
|
|
1439
|
+
console.log(
|
|
1440
|
+
`[add] dedup: high-sim match ${topMatch[0].note.id.slice(0, 8)} is not writable by ${logSafe(agentId)} \u2014 inserting a new note instead`
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
const pendingMerge = topMatch.length > 0 && topMatch[0].score >= 0.72 && topMatch[0].score < 0.85;
|
|
1444
|
+
if (pendingMerge) {
|
|
1445
|
+
console.log(`[add] dedup: borderline sim (sim=${topMatch[0].score.toFixed(3)}), marking pending_merge=true`);
|
|
1446
|
+
}
|
|
1447
|
+
const readers = scope === "shared" ? ["*"] : [agentId];
|
|
1448
|
+
const writers = [agentId];
|
|
1449
|
+
const note = {
|
|
1450
|
+
id: (0, import_uuid.v4)(),
|
|
1451
|
+
subjects,
|
|
1452
|
+
content,
|
|
1453
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1454
|
+
keywords,
|
|
1455
|
+
tags,
|
|
1456
|
+
context,
|
|
1457
|
+
embedding,
|
|
1458
|
+
links: [],
|
|
1459
|
+
agent_id: effectiveNoteAgentId,
|
|
1460
|
+
hash,
|
|
1461
|
+
// 13-A
|
|
1462
|
+
retrieval_count: 0,
|
|
1463
|
+
last_accessed: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1464
|
+
// 13-B
|
|
1465
|
+
evolution_history: [],
|
|
1466
|
+
// 13-E
|
|
1467
|
+
category,
|
|
1468
|
+
is_active: true,
|
|
1469
|
+
// 26A
|
|
1470
|
+
note_type,
|
|
1471
|
+
// 26B
|
|
1472
|
+
topics,
|
|
1473
|
+
// 29
|
|
1474
|
+
pending_merge: pendingMerge,
|
|
1475
|
+
// 30
|
|
1476
|
+
conflict: false,
|
|
1477
|
+
// 31
|
|
1478
|
+
ephemeral: quality.ephemeral,
|
|
1479
|
+
low_quality: false,
|
|
1480
|
+
// 32
|
|
1481
|
+
owner: agentId,
|
|
1482
|
+
readers,
|
|
1483
|
+
writers
|
|
1484
|
+
};
|
|
1485
|
+
await ctx.addNote(note);
|
|
1486
|
+
console.log(` saved note ${note.id}`);
|
|
1487
|
+
try {
|
|
1488
|
+
const total = await ctx.countNotes(agentId);
|
|
1489
|
+
if (total > 1) {
|
|
1490
|
+
const candidates = await ctx.queryByEmbedding(embedding, 6, agentId, 0);
|
|
1491
|
+
const linkedIds = [];
|
|
1492
|
+
const linkedContents = [];
|
|
1493
|
+
for (const { note: cand, score } of candidates) {
|
|
1494
|
+
if (cand.id === note.id) continue;
|
|
1495
|
+
if (score < 0.3) continue;
|
|
1496
|
+
console.log(` candidate ${cand.id.slice(0, 8)}... sim=${score.toFixed(3)}, asking LLM...`);
|
|
1497
|
+
const shouldLink = await llmShouldLink(content, cand.content);
|
|
1498
|
+
if (shouldLink) {
|
|
1499
|
+
linkedIds.push(cand.id);
|
|
1500
|
+
linkedContents.push(cand.content);
|
|
1501
|
+
console.log(` \u2192 linked!`);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
if (linkedIds.length > 0) {
|
|
1505
|
+
note.links = linkedIds;
|
|
1506
|
+
await ctx.updateNote(note);
|
|
1507
|
+
for (const lid of linkedIds) {
|
|
1508
|
+
const linked = await ctx.getNote(lid);
|
|
1509
|
+
if (linked && !linked.links.includes(note.id)) {
|
|
1510
|
+
if (!canWrite(linked, agentId)) {
|
|
1511
|
+
console.log(`[link] back-link into ${lid.slice(0, 8)} skipped \u2014 not writable by ${logSafe(agentId)}`);
|
|
1512
|
+
continue;
|
|
1513
|
+
}
|
|
1514
|
+
linked.links.push(note.id);
|
|
1515
|
+
await ctx.updateNote(linked);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
if (shouldRunEvolution()) {
|
|
1519
|
+
console.log(` [evo] threshold reached, running evolution for ${Math.min(linkedIds.length, 3)} linked notes`);
|
|
1520
|
+
for (const lid of linkedIds.slice(0, 3)) {
|
|
1521
|
+
const linked = await ctx.getNote(lid);
|
|
1522
|
+
if (!linked) continue;
|
|
1523
|
+
if (!canWrite(linked, agentId)) {
|
|
1524
|
+
console.log(` [evo] skipping ${lid.slice(0, 8)} \u2014 not writable by ${logSafe(agentId)}`);
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1527
|
+
const linkedNotes = [];
|
|
1528
|
+
for (const llid of linked.links.slice(0, 5)) {
|
|
1529
|
+
if (llid === note.id) continue;
|
|
1530
|
+
const ln = await ctx.getNote(llid, agentId);
|
|
1531
|
+
if (ln) linkedNotes.push({ id: ln.id, content: ln.content });
|
|
1532
|
+
}
|
|
1533
|
+
linkedNotes.push({ id: note.id, content });
|
|
1534
|
+
const oldTags = [...linked.tags];
|
|
1535
|
+
const oldContext = linked.context;
|
|
1536
|
+
const {
|
|
1537
|
+
tags: newTags,
|
|
1538
|
+
context: newContext,
|
|
1539
|
+
shouldStrengthen,
|
|
1540
|
+
suggestedConnections,
|
|
1541
|
+
tagsToUpdate
|
|
1542
|
+
} = await llmEvolveNote(linked.content, linkedNotes);
|
|
1543
|
+
let evolved = false;
|
|
1544
|
+
if (newTags !== null || newContext !== null) {
|
|
1545
|
+
if (newTags !== null) linked.tags = newTags;
|
|
1546
|
+
if (newContext !== null) linked.context = newContext;
|
|
1547
|
+
linked.evolution_history = linked.evolution_history || [];
|
|
1548
|
+
linked.evolution_history.push({
|
|
1549
|
+
triggeredBy: note.id,
|
|
1550
|
+
triggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1551
|
+
oldContext,
|
|
1552
|
+
newContext: newContext ?? oldContext,
|
|
1553
|
+
oldTags,
|
|
1554
|
+
newTags: newTags ?? oldTags,
|
|
1555
|
+
action: "update_neighbor"
|
|
1556
|
+
});
|
|
1557
|
+
evolved = true;
|
|
1558
|
+
}
|
|
1559
|
+
let noteChanged = false;
|
|
1560
|
+
let noteTagsChanged = false;
|
|
1561
|
+
if (shouldStrengthen && suggestedConnections.length > 0) {
|
|
1562
|
+
for (const targetId of suggestedConnections) {
|
|
1563
|
+
if (!note.links.includes(targetId)) {
|
|
1564
|
+
note.links.push(targetId);
|
|
1565
|
+
noteChanged = true;
|
|
1566
|
+
}
|
|
1567
|
+
const target = await ctx.getNote(targetId, agentId);
|
|
1568
|
+
if (target && !target.links.includes(note.id)) {
|
|
1569
|
+
if (canWrite(target, agentId)) {
|
|
1570
|
+
target.links.push(note.id);
|
|
1571
|
+
await ctx.updateNote(target);
|
|
1572
|
+
} else {
|
|
1573
|
+
console.log(` [evo] strengthen back-link into ${targetId.slice(0, 8)} skipped \u2014 not writable`);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
if (tagsToUpdate.length > 0) {
|
|
1578
|
+
note.tags = tagsToUpdate;
|
|
1579
|
+
noteChanged = true;
|
|
1580
|
+
noteTagsChanged = true;
|
|
1581
|
+
}
|
|
1582
|
+
linked.evolution_history = linked.evolution_history || [];
|
|
1583
|
+
linked.evolution_history.push({
|
|
1584
|
+
triggeredBy: note.id,
|
|
1585
|
+
triggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1586
|
+
oldContext: linked.context,
|
|
1587
|
+
newContext: linked.context,
|
|
1588
|
+
oldTags: [...linked.tags],
|
|
1589
|
+
newTags: [...linked.tags],
|
|
1590
|
+
action: "strengthen",
|
|
1591
|
+
suggestedConnections,
|
|
1592
|
+
tagsUpdated: tagsToUpdate
|
|
1593
|
+
});
|
|
1594
|
+
evolved = true;
|
|
1595
|
+
}
|
|
1596
|
+
if (noteChanged) {
|
|
1597
|
+
if (noteTagsChanged) {
|
|
1598
|
+
note.embedding = await encode(buildEmbedText(note));
|
|
1599
|
+
}
|
|
1600
|
+
await ctx.updateNote(note);
|
|
1601
|
+
}
|
|
1602
|
+
if (evolved) {
|
|
1603
|
+
if (newTags !== null || newContext !== null) {
|
|
1604
|
+
linked.embedding = await encode(buildEmbedText(linked));
|
|
1605
|
+
}
|
|
1606
|
+
await ctx.updateNote(linked);
|
|
1607
|
+
console.log(` evolved/strengthened note ${lid.slice(0, 8)}...`);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
} else {
|
|
1611
|
+
console.log(` [evo] threshold not reached, skipping evolution this round`);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
} catch (e) {
|
|
1616
|
+
console.error(`[warn] Link/Evolution phase failed: ${e.message}`);
|
|
1617
|
+
}
|
|
1618
|
+
console.log(`[done] Note added: ${note.id}`);
|
|
1619
|
+
return note.id;
|
|
1620
|
+
}
|
|
1621
|
+
async function addEpisodic(content, agentId = "main", opts) {
|
|
1622
|
+
const scope = opts?.scope ?? "private";
|
|
1623
|
+
const subjects = opts?.subjects ?? [];
|
|
1624
|
+
const ctx = opts?.storageCtx ?? defaultCtx();
|
|
1625
|
+
const quality = checkQuality(content);
|
|
1626
|
+
if (!quality.ok) {
|
|
1627
|
+
throw new Error(`[quality] \u5199\u5165\u62D2\u7EDD: ${quality.reason}`);
|
|
1628
|
+
}
|
|
1629
|
+
const embedding = await encode(content);
|
|
1630
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1631
|
+
const note = {
|
|
1632
|
+
id: (0, import_uuid.v4)(),
|
|
1633
|
+
subjects,
|
|
1634
|
+
content,
|
|
1635
|
+
timestamp: now,
|
|
1636
|
+
keywords: [],
|
|
1637
|
+
tags: [],
|
|
1638
|
+
context: "",
|
|
1639
|
+
embedding,
|
|
1640
|
+
links: [],
|
|
1641
|
+
agent_id: scope === "shared" ? "shared" : agentId,
|
|
1642
|
+
hash: (0, import_crypto.createHash)("md5").update(content).digest("hex"),
|
|
1643
|
+
retrieval_count: 0,
|
|
1644
|
+
last_accessed: now,
|
|
1645
|
+
evolution_history: [],
|
|
1646
|
+
category: "General",
|
|
1647
|
+
is_active: true,
|
|
1648
|
+
note_type: "memory",
|
|
1649
|
+
topics: [],
|
|
1650
|
+
pending_merge: false,
|
|
1651
|
+
conflict: false,
|
|
1652
|
+
ephemeral: quality.ephemeral,
|
|
1653
|
+
low_quality: false,
|
|
1654
|
+
owner: agentId,
|
|
1655
|
+
readers: scope === "shared" ? ["*"] : [agentId],
|
|
1656
|
+
writers: [agentId]
|
|
1657
|
+
};
|
|
1658
|
+
await ctx.addNote(note);
|
|
1659
|
+
return note.id;
|
|
1660
|
+
}
|
|
1661
|
+
async function searchMemory(query, topK = 5, agentId = "main", opts) {
|
|
1662
|
+
const useBfs = opts?.useBfs !== false;
|
|
1663
|
+
const subject = opts?.subject;
|
|
1664
|
+
const bfsSimThreshold = opts?.bfsSimThreshold ?? 0.25;
|
|
1665
|
+
const ctx = opts?.storageCtx ?? defaultCtx();
|
|
1666
|
+
const total = await ctx.countNotes(agentId);
|
|
1667
|
+
if (total === 0) return [];
|
|
1668
|
+
const queryEmbedding = await encode(query);
|
|
1669
|
+
const n = Math.min(Math.max(topK * 4, 20), total);
|
|
1670
|
+
const embResults = await ctx.queryByEmbedding(queryEmbedding, n, agentId, 0, subject);
|
|
1671
|
+
const allNotes = await ctx.listNotes(agentId, subject);
|
|
1672
|
+
const bm25State = buildBM25(allNotes);
|
|
1673
|
+
const queryTokens = simpleTokenize(query);
|
|
1674
|
+
const bm25Ranked = bm25Score(bm25State, queryTokens).slice(0, n);
|
|
1675
|
+
const merged = rrfMerge(
|
|
1676
|
+
embResults.map((r) => r.note.id),
|
|
1677
|
+
bm25Ranked.map((r) => r[0])
|
|
1678
|
+
);
|
|
1679
|
+
const now = Date.now();
|
|
1680
|
+
const noteMap = new Map(allNotes.map((n2) => [n2.id, n2]));
|
|
1681
|
+
const boostedMerged = merged.map(([id, rrfScore]) => {
|
|
1682
|
+
const note = noteMap.get(id);
|
|
1683
|
+
if (!note) return [id, rrfScore];
|
|
1684
|
+
if (note.note_type === "knowledge") return [id, rrfScore];
|
|
1685
|
+
const lastAccessed = new Date(note.last_accessed || note.timestamp).getTime();
|
|
1686
|
+
const ageDays = (now - lastAccessed) / 864e5;
|
|
1687
|
+
const recencyBoost = 1 + 0.05 * Math.log(1 + (note.retrieval_count || 0)) / (ageDays + 1);
|
|
1688
|
+
return [id, rrfScore * recencyBoost];
|
|
1689
|
+
});
|
|
1690
|
+
boostedMerged.sort((a, b) => b[1] - a[1]);
|
|
1691
|
+
const topIds = boostedMerged.slice(0, topK).map(([id]) => id);
|
|
1692
|
+
const BFS_MAX_HOPS = 2;
|
|
1693
|
+
const BFS_MAX_EXPAND = 8;
|
|
1694
|
+
const visitedIds = new Set(topIds);
|
|
1695
|
+
const bfsQueue = useBfs ? topIds.map((id) => ({ id, hop: 0 })) : [];
|
|
1696
|
+
const bfsExtra = [];
|
|
1697
|
+
while (bfsQueue.length > 0 && bfsExtra.length < BFS_MAX_EXPAND) {
|
|
1698
|
+
const item = bfsQueue.shift();
|
|
1699
|
+
if (item.hop >= BFS_MAX_HOPS) continue;
|
|
1700
|
+
const note = noteMap.get(item.id);
|
|
1701
|
+
if (!note) continue;
|
|
1702
|
+
for (const linkedId of note.links) {
|
|
1703
|
+
if (visitedIds.has(linkedId)) continue;
|
|
1704
|
+
visitedIds.add(linkedId);
|
|
1705
|
+
const linked = noteMap.get(linkedId);
|
|
1706
|
+
if (!linked || linked.is_active === false) continue;
|
|
1707
|
+
if (bfsSimThreshold > 0 && linked.embedding) {
|
|
1708
|
+
const sim = cosineSimilarity(queryEmbedding, linked.embedding);
|
|
1709
|
+
if (sim < bfsSimThreshold) continue;
|
|
1710
|
+
}
|
|
1711
|
+
bfsExtra.push(linkedId);
|
|
1712
|
+
bfsQueue.push({ id: linkedId, hop: item.hop + 1 });
|
|
1713
|
+
if (bfsExtra.length >= BFS_MAX_EXPAND) break;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
const topicsFilter = opts?.topicsFilter;
|
|
1717
|
+
const filteredTopIds = topicsFilter && topicsFilter.length > 0 ? topIds.filter((id) => {
|
|
1718
|
+
const note = noteMap.get(id);
|
|
1719
|
+
if (!note) return false;
|
|
1720
|
+
if (note.note_type !== "knowledge") return true;
|
|
1721
|
+
return topicsFilter.every((t2) => note.topics.map((s) => s.toLowerCase()).includes(t2.toLowerCase()));
|
|
1722
|
+
}) : topIds;
|
|
1723
|
+
const embSimMap = new Map(embResults.map((r) => [r.note.id, r.score]));
|
|
1724
|
+
const rrfMap = new Map(boostedMerged.map(([id, score]) => [id, score]));
|
|
1725
|
+
const results = [];
|
|
1726
|
+
for (const id of [...filteredTopIds, ...bfsExtra]) {
|
|
1727
|
+
const note = noteMap.get(id);
|
|
1728
|
+
if (!note) continue;
|
|
1729
|
+
results.push({
|
|
1730
|
+
id: note.id,
|
|
1731
|
+
content: note.content,
|
|
1732
|
+
context: note.context,
|
|
1733
|
+
tags: note.tags,
|
|
1734
|
+
keywords: note.keywords,
|
|
1735
|
+
links: note.links,
|
|
1736
|
+
timestamp: note.timestamp,
|
|
1737
|
+
similarity: embSimMap.get(id) ?? 0,
|
|
1738
|
+
rrf: rrfMap.get(id) ?? 0,
|
|
1739
|
+
topics: note.topics ?? [],
|
|
1740
|
+
note_type: note.note_type ?? "memory"
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
return results;
|
|
1744
|
+
}
|
|
1745
|
+
async function listMemories(agentId = "main", storageCtx) {
|
|
1746
|
+
const ctx = storageCtx ?? defaultCtx();
|
|
1747
|
+
const count = await ctx.countNotes(agentId);
|
|
1748
|
+
return { count };
|
|
1749
|
+
}
|
|
1750
|
+
function sleep(ms) {
|
|
1751
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1752
|
+
}
|
|
1753
|
+
async function mergeSimilarNotes(agentId, storageCtx) {
|
|
1754
|
+
const ctx = storageCtx ?? defaultCtx();
|
|
1755
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1756
|
+
const allNotes = await ctx.getNotesByDatePrefix(today, agentId);
|
|
1757
|
+
const notes = allNotes.filter((n) => n.agent_id !== "shared");
|
|
1758
|
+
const pendingNotes = notes.filter((n) => n.pending_merge === true);
|
|
1759
|
+
let evolvedCount = 0;
|
|
1760
|
+
for (const pendingNote of pendingNotes) {
|
|
1761
|
+
let bestSim = -1;
|
|
1762
|
+
let bestNeighbor = null;
|
|
1763
|
+
for (const other of notes) {
|
|
1764
|
+
if (other.id === pendingNote.id) continue;
|
|
1765
|
+
if (other.pending_merge) continue;
|
|
1766
|
+
if (!other.embedding.length || !pendingNote.embedding.length) continue;
|
|
1767
|
+
const sim = cosineSimilarity(pendingNote.embedding, other.embedding);
|
|
1768
|
+
if (sim > bestSim) {
|
|
1769
|
+
bestSim = sim;
|
|
1770
|
+
bestNeighbor = other;
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
if (!bestNeighbor) {
|
|
1774
|
+
await ctx.patchNotePayload(pendingNote.id, { pending_merge: false });
|
|
1775
|
+
continue;
|
|
1776
|
+
}
|
|
1777
|
+
const judgment = await llmEvolutionJudge(bestNeighbor.content, pendingNote.content);
|
|
1778
|
+
console.log(
|
|
1779
|
+
`[merge] evolution judgment: ${pendingNote.id.slice(0, 8)} \u2192 ${bestNeighbor.id.slice(0, 8)}: ${judgment.type}`
|
|
1780
|
+
);
|
|
1781
|
+
if (judgment.type === "EVOLVE") {
|
|
1782
|
+
const oldHistory = bestNeighbor.evolution_history || [];
|
|
1783
|
+
oldHistory.push({
|
|
1784
|
+
triggeredBy: pendingNote.id,
|
|
1785
|
+
triggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1786
|
+
oldContext: bestNeighbor.context,
|
|
1787
|
+
newContext: bestNeighbor.context,
|
|
1788
|
+
oldTags: [...bestNeighbor.tags],
|
|
1789
|
+
newTags: [...bestNeighbor.tags],
|
|
1790
|
+
action: "consolidate"
|
|
1791
|
+
});
|
|
1792
|
+
const mergedContent = judgment.mergedContent || pendingNote.content;
|
|
1793
|
+
const newEmbedding = await encode(buildEmbedText({ ...bestNeighbor, content: mergedContent }));
|
|
1794
|
+
const newHash = (0, import_crypto.createHash)("md5").update(mergedContent).digest("hex");
|
|
1795
|
+
await ctx.updateNoteContent(bestNeighbor.id, mergedContent, newEmbedding, newHash);
|
|
1796
|
+
await ctx.patchNotePayload(bestNeighbor.id, {
|
|
1797
|
+
evolution_history: JSON.stringify(oldHistory),
|
|
1798
|
+
evolution_type: "EVOLVE"
|
|
1799
|
+
});
|
|
1800
|
+
await ctx.deleteNote(pendingNote.id);
|
|
1801
|
+
evolvedCount++;
|
|
1802
|
+
} else if (judgment.type === "CONFLICT") {
|
|
1803
|
+
await ctx.patchNotePayload(pendingNote.id, { pending_merge: false, conflict: true, evolution_type: "CONFLICT" });
|
|
1804
|
+
await ctx.patchNotePayload(bestNeighbor.id, { conflict: true, evolution_type: "CONFLICT" });
|
|
1805
|
+
} else if (judgment.type === "EXPAND") {
|
|
1806
|
+
const oldHistory = bestNeighbor.evolution_history || [];
|
|
1807
|
+
oldHistory.push({
|
|
1808
|
+
triggeredBy: pendingNote.id,
|
|
1809
|
+
triggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1810
|
+
oldContext: bestNeighbor.context,
|
|
1811
|
+
newContext: bestNeighbor.context,
|
|
1812
|
+
oldTags: [...bestNeighbor.tags],
|
|
1813
|
+
newTags: [...bestNeighbor.tags],
|
|
1814
|
+
action: "consolidate"
|
|
1815
|
+
});
|
|
1816
|
+
const mergedContent = judgment.mergedContent || `${bestNeighbor.content}\uFF1B${pendingNote.content}`;
|
|
1817
|
+
const newEmbedding = await encode(buildEmbedText({ ...bestNeighbor, content: mergedContent }));
|
|
1818
|
+
const newHash = (0, import_crypto.createHash)("md5").update(mergedContent).digest("hex");
|
|
1819
|
+
await ctx.updateNoteContent(bestNeighbor.id, mergedContent, newEmbedding, newHash);
|
|
1820
|
+
await ctx.patchNotePayload(bestNeighbor.id, {
|
|
1821
|
+
evolution_history: JSON.stringify(oldHistory),
|
|
1822
|
+
evolution_type: "EXPAND"
|
|
1823
|
+
});
|
|
1824
|
+
await ctx.deleteNote(pendingNote.id);
|
|
1825
|
+
evolvedCount++;
|
|
1826
|
+
} else {
|
|
1827
|
+
await ctx.patchNotePayload(pendingNote.id, { pending_merge: false, evolution_type: "NEW" });
|
|
1828
|
+
}
|
|
1829
|
+
await sleep(200);
|
|
1830
|
+
}
|
|
1831
|
+
if (notes.length < 5) return evolvedCount;
|
|
1832
|
+
const pendingIds = new Set(pendingNotes.map((n) => n.id));
|
|
1833
|
+
const pairs = [];
|
|
1834
|
+
for (let i = 0; i < notes.length; i++) {
|
|
1835
|
+
if (pendingIds.has(notes[i].id)) continue;
|
|
1836
|
+
for (let j = i + 1; j < notes.length; j++) {
|
|
1837
|
+
if (pendingIds.has(notes[j].id)) continue;
|
|
1838
|
+
if (!notes[i].embedding.length || !notes[j].embedding.length) continue;
|
|
1839
|
+
const sim = cosineSimilarity(notes[i].embedding, notes[j].embedding);
|
|
1840
|
+
if (sim >= 0.8) {
|
|
1841
|
+
pairs.push({ i, j, sim });
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
if (pairs.length === 0) return evolvedCount;
|
|
1846
|
+
pairs.sort((a, b) => b.sim - a.sim);
|
|
1847
|
+
const topPairs = pairs.slice(0, 10);
|
|
1848
|
+
const deletedIds = /* @__PURE__ */ new Set();
|
|
1849
|
+
let mergedCount = 0;
|
|
1850
|
+
for (const { i, j } of topPairs) {
|
|
1851
|
+
const noteA = notes[i];
|
|
1852
|
+
const noteB = notes[j];
|
|
1853
|
+
if (deletedIds.has(noteA.id) || deletedIds.has(noteB.id)) continue;
|
|
1854
|
+
const result = await llmShouldMerge(noteA.content, noteB.content);
|
|
1855
|
+
if (result.shouldMerge && result.merged) {
|
|
1856
|
+
const [keepNote, dropNote] = noteA.content.length >= noteB.content.length ? [noteA, noteB] : [noteB, noteA];
|
|
1857
|
+
const newEmbedding = await encode(result.merged);
|
|
1858
|
+
const newHash = (0, import_crypto.createHash)("md5").update(result.merged).digest("hex");
|
|
1859
|
+
await ctx.updateNoteContent(keepNote.id, result.merged, newEmbedding, newHash);
|
|
1860
|
+
await ctx.deleteNote(dropNote.id);
|
|
1861
|
+
deletedIds.add(dropNote.id);
|
|
1862
|
+
mergedCount++;
|
|
1863
|
+
}
|
|
1864
|
+
await sleep(200);
|
|
1865
|
+
}
|
|
1866
|
+
return evolvedCount + mergedCount;
|
|
1867
|
+
}
|
|
1868
|
+
async function consolidateMemories(agentId, logger, storageCtx) {
|
|
1869
|
+
const ctx = storageCtx ?? defaultCtx();
|
|
1870
|
+
const log = {
|
|
1871
|
+
info: (msg) => logger ? logger.info(msg) : console.log(msg),
|
|
1872
|
+
warn: (msg) => logger ? logger.warn(msg) : console.warn(msg),
|
|
1873
|
+
error: (msg) => logger ? logger.error(msg) : console.error(msg)
|
|
1874
|
+
};
|
|
1875
|
+
log.info(`[Consolidation] Starting consolidation for agentId: ${agentId}`);
|
|
1876
|
+
const rawNotes = await ctx.listNotes(agentId);
|
|
1877
|
+
const allNotes = rawNotes.filter((n) => n.agent_id !== "shared");
|
|
1878
|
+
log.info(
|
|
1879
|
+
`[Consolidation] Loaded ${allNotes.length} active private notes (${rawNotes.length - allNotes.length} shared skipped).`
|
|
1880
|
+
);
|
|
1881
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1882
|
+
for (const note of allNotes) {
|
|
1883
|
+
if (note.note_type === "knowledge") continue;
|
|
1884
|
+
const category = note.category || "General";
|
|
1885
|
+
if (!groups.has(category)) {
|
|
1886
|
+
groups.set(category, []);
|
|
1887
|
+
}
|
|
1888
|
+
groups.get(category).push(note);
|
|
1889
|
+
}
|
|
1890
|
+
const candidates = [];
|
|
1891
|
+
for (const [category, groupNotes] of groups.entries()) {
|
|
1892
|
+
log.info(`[Consolidation] Category "${category}" has ${groupNotes.length} notes.`);
|
|
1893
|
+
for (let i = 0; i < groupNotes.length; i++) {
|
|
1894
|
+
for (let j = i + 1; j < groupNotes.length; j++) {
|
|
1895
|
+
const noteA = groupNotes[i];
|
|
1896
|
+
const noteB = groupNotes[j];
|
|
1897
|
+
if (!noteA.embedding.length || !noteB.embedding.length) continue;
|
|
1898
|
+
const sim = cosineSimilarity(noteA.embedding, noteB.embedding);
|
|
1899
|
+
if (sim >= 0.75) {
|
|
1900
|
+
candidates.push({ noteA, noteB, similarity: sim });
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
candidates.sort((a, b) => b.similarity - a.similarity);
|
|
1906
|
+
const topPairs = candidates.slice(0, 15);
|
|
1907
|
+
log.info(
|
|
1908
|
+
`[Consolidation] Found ${candidates.length} candidate pairs with similarity >= 0.75. Processing top ${topPairs.length}.`
|
|
1909
|
+
);
|
|
1910
|
+
const processedIds = /* @__PURE__ */ new Set();
|
|
1911
|
+
let mergedCount = 0;
|
|
1912
|
+
function logMergeToFile(keepId, dropId, mergedContent) {
|
|
1913
|
+
const logDir = path3.join(getDataDir(), "logs");
|
|
1914
|
+
const logFile = path3.join(logDir, "amem-consolidate.log");
|
|
1915
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1916
|
+
const logMsg = `[${timestamp}] Consolidated: KeepNote ${keepId} and DropNote ${dropId}. Merged length: ${mergedContent.length} chars.
|
|
1917
|
+
`;
|
|
1918
|
+
try {
|
|
1919
|
+
fs2.mkdirSync(logDir, { recursive: true });
|
|
1920
|
+
fs2.appendFileSync(logFile, logMsg, "utf8");
|
|
1921
|
+
} catch (err) {
|
|
1922
|
+
log.error(`[Consolidation] Failed to write log: ${err.message}`);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
for (const { noteA, noteB, similarity } of topPairs) {
|
|
1926
|
+
if (processedIds.has(noteA.id) || processedIds.has(noteB.id)) {
|
|
1927
|
+
log.info(
|
|
1928
|
+
`[Consolidation] Skipping pair (${noteA.id.slice(0, 8)}, ${noteB.id.slice(0, 8)}) as one or both already merged.`
|
|
1929
|
+
);
|
|
1930
|
+
continue;
|
|
1931
|
+
}
|
|
1932
|
+
log.info(
|
|
1933
|
+
`[Consolidation] Evaluating pair (${noteA.id.slice(0, 8)}, ${noteB.id.slice(0, 8)}) with sim ${similarity.toFixed(4)}...`
|
|
1934
|
+
);
|
|
1935
|
+
const mergeDecision = await llmShouldMerge(noteA.content, noteB.content);
|
|
1936
|
+
if (mergeDecision.shouldMerge && mergeDecision.merged) {
|
|
1937
|
+
log.info(` -> LLM decision: MERGE!`);
|
|
1938
|
+
const [keepNote, dropNote] = noteA.content.length >= noteB.content.length ? [noteA, noteB] : [noteB, noteA];
|
|
1939
|
+
log.info(
|
|
1940
|
+
` -> KeepNote: ${keepNote.id.slice(0, 8)} (len: ${keepNote.content.length}), DropNote: ${dropNote.id.slice(0, 8)} (len: ${dropNote.content.length})`
|
|
1941
|
+
);
|
|
1942
|
+
const oldContext = keepNote.context;
|
|
1943
|
+
const oldTags = [...keepNote.tags];
|
|
1944
|
+
keepNote.content = mergeDecision.merged;
|
|
1945
|
+
keepNote.tags = Array.from(/* @__PURE__ */ new Set([...keepNote.tags, ...dropNote.tags]));
|
|
1946
|
+
keepNote.keywords = Array.from(/* @__PURE__ */ new Set([...keepNote.keywords, ...dropNote.keywords]));
|
|
1947
|
+
keepNote.links = Array.from(/* @__PURE__ */ new Set([...keepNote.links, ...dropNote.links])).filter(
|
|
1948
|
+
(id) => id !== keepNote.id && id !== dropNote.id
|
|
1949
|
+
);
|
|
1950
|
+
keepNote.retrieval_count = (keepNote.retrieval_count || 0) + (dropNote.retrieval_count || 0);
|
|
1951
|
+
const keepAccessTime = new Date(keepNote.last_accessed || keepNote.timestamp).getTime();
|
|
1952
|
+
const dropAccessTime = new Date(dropNote.last_accessed || dropNote.timestamp).getTime();
|
|
1953
|
+
keepNote.last_accessed = keepAccessTime >= dropAccessTime ? keepNote.last_accessed || keepNote.timestamp : dropNote.last_accessed || dropNote.timestamp;
|
|
1954
|
+
const embedText = buildEmbedText(keepNote);
|
|
1955
|
+
keepNote.embedding = await encode(embedText);
|
|
1956
|
+
keepNote.hash = (0, import_crypto.createHash)("md5").update(keepNote.content).digest("hex");
|
|
1957
|
+
keepNote.evolution_history = keepNote.evolution_history || [];
|
|
1958
|
+
keepNote.evolution_history.push({
|
|
1959
|
+
triggeredBy: dropNote.id,
|
|
1960
|
+
triggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1961
|
+
oldContext,
|
|
1962
|
+
newContext: keepNote.context,
|
|
1963
|
+
oldTags,
|
|
1964
|
+
newTags: keepNote.tags,
|
|
1965
|
+
action: "consolidate"
|
|
1966
|
+
});
|
|
1967
|
+
await ctx.updateNote(keepNote);
|
|
1968
|
+
await ctx.invalidateNote(dropNote.id);
|
|
1969
|
+
await ctx.replaceLinkReferences(dropNote.id, keepNote.id, agentId);
|
|
1970
|
+
logMergeToFile(keepNote.id, dropNote.id, keepNote.content);
|
|
1971
|
+
processedIds.add(keepNote.id);
|
|
1972
|
+
processedIds.add(dropNote.id);
|
|
1973
|
+
mergedCount++;
|
|
1974
|
+
} else {
|
|
1975
|
+
log.info(` -> LLM decision: DO NOT MERGE.`);
|
|
1976
|
+
}
|
|
1977
|
+
await sleep(200);
|
|
1978
|
+
}
|
|
1979
|
+
log.info(`[Consolidation] Completed consolidation run. Merged ${mergedCount} pairs.`);
|
|
1980
|
+
return mergedCount;
|
|
1981
|
+
}
|
|
1982
|
+
function resolveConflictMode(override) {
|
|
1983
|
+
const raw = (process.env.AMEM_CONFLICT_MODE || override || "review").trim().toLowerCase();
|
|
1984
|
+
return raw === "auto" ? "auto" : "review";
|
|
1985
|
+
}
|
|
1986
|
+
var CONFLICT_BATCH_SIZE = 25;
|
|
1987
|
+
async function conflictSweep(agentId, opts) {
|
|
1988
|
+
const ctx = opts?.storageCtx ?? defaultCtx();
|
|
1989
|
+
const force = opts?.force === true;
|
|
1990
|
+
const mode = resolveConflictMode(opts?.mode);
|
|
1991
|
+
const log = opts?.logger?.info ?? ((m) => console.log(m));
|
|
1992
|
+
const raw = await ctx.listNotes(agentId);
|
|
1993
|
+
const notes = raw.filter((n) => n.agent_id !== "shared" && n.note_type !== "knowledge" && n.is_active !== false);
|
|
1994
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1995
|
+
for (const n of notes) {
|
|
1996
|
+
const c = n.category || "General";
|
|
1997
|
+
if (!groups.has(c)) groups.set(c, []);
|
|
1998
|
+
groups.get(c).push(n);
|
|
1999
|
+
}
|
|
2000
|
+
let pairsFound = 0;
|
|
2001
|
+
let retired = 0;
|
|
2002
|
+
let batchesScanned = 0;
|
|
2003
|
+
let batchesSkipped = 0;
|
|
2004
|
+
for (const [category, groupNotes] of groups.entries()) {
|
|
2005
|
+
groupNotes.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
|
|
2006
|
+
for (let start = 0; start < groupNotes.length; start += CONFLICT_BATCH_SIZE) {
|
|
2007
|
+
const batch = groupNotes.slice(start, start + CONFLICT_BATCH_SIZE);
|
|
2008
|
+
if (batch.length < 2) continue;
|
|
2009
|
+
if (!force && batch.every((n) => n.conflict_scanned_at)) {
|
|
2010
|
+
batchesSkipped++;
|
|
2011
|
+
continue;
|
|
2012
|
+
}
|
|
2013
|
+
batchesScanned++;
|
|
2014
|
+
const pairs = await llmConflictScan(batch.map((n) => n.content));
|
|
2015
|
+
for (const { a, b, reason, supersededIndex } of pairs) {
|
|
2016
|
+
const noteA = batch[a];
|
|
2017
|
+
const noteB = batch[b];
|
|
2018
|
+
if (!noteA || !noteB) continue;
|
|
2019
|
+
pairsFound++;
|
|
2020
|
+
await ctx.patchNotePayload(noteA.id, {
|
|
2021
|
+
conflict: true,
|
|
2022
|
+
evolution_type: "CONFLICT",
|
|
2023
|
+
conflicts_with: Array.from(/* @__PURE__ */ new Set([...noteA.conflicts_with ?? [], noteB.id])),
|
|
2024
|
+
conflict_reason: reason
|
|
2025
|
+
});
|
|
2026
|
+
await ctx.patchNotePayload(noteB.id, {
|
|
2027
|
+
conflict: true,
|
|
2028
|
+
evolution_type: "CONFLICT",
|
|
2029
|
+
conflicts_with: Array.from(/* @__PURE__ */ new Set([...noteB.conflicts_with ?? [], noteA.id])),
|
|
2030
|
+
conflict_reason: reason
|
|
2031
|
+
});
|
|
2032
|
+
log(`[conflict] ${category}: ${noteA.id.slice(0, 8)} \u2194 ${noteB.id.slice(0, 8)} \u2014 ${reason}`);
|
|
2033
|
+
if (mode === "auto") {
|
|
2034
|
+
const superseded = supersededIndex === a ? noteA : supersededIndex === b ? noteB : null;
|
|
2035
|
+
if (!superseded) {
|
|
2036
|
+
log(`[conflict] auto: no superseded side identified \u2014 marked only, nothing retired`);
|
|
2037
|
+
} else {
|
|
2038
|
+
const ok = await ctx.invalidateNote(superseded.id, agentId);
|
|
2039
|
+
if (ok) {
|
|
2040
|
+
retired++;
|
|
2041
|
+
log(`[conflict] auto-retired the superseded note ${superseded.id.slice(0, 8)}`);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
const scannedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2047
|
+
for (const n of batch) {
|
|
2048
|
+
await ctx.patchNotePayload(n.id, { conflict_scanned_at: scannedAt });
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
log(
|
|
2053
|
+
`[conflict] ${batchesScanned} batch(es) scanned, ${batchesSkipped} already up to date; ${pairsFound} pair(s) found, ${retired} retired`
|
|
2054
|
+
);
|
|
2055
|
+
return { scanned: notes.length, pairsFound, retired, batchesScanned, batchesSkipped };
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
// src/quality.ts
|
|
2059
|
+
var fs3 = __toESM(require("fs"), 1);
|
|
2060
|
+
var path4 = __toESM(require("path"), 1);
|
|
2061
|
+
var LOCALE2 = process.env.AMEM_PROMPT_LOCALE === "zh" ? "zh" : "en";
|
|
2062
|
+
async function scanLowQuality(agentId) {
|
|
2063
|
+
const notes = await listNotes(agentId);
|
|
2064
|
+
const now = Date.now();
|
|
2065
|
+
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
2066
|
+
const results = [];
|
|
2067
|
+
for (const note of notes) {
|
|
2068
|
+
if (!canWrite(note, agentId)) continue;
|
|
2069
|
+
const reasons = [];
|
|
2070
|
+
if (note.content.trim().length < 10) {
|
|
2071
|
+
reasons.push("too_short");
|
|
2072
|
+
}
|
|
2073
|
+
if (note.ephemeral === true) {
|
|
2074
|
+
const createdAt = new Date(note.timestamp).getTime();
|
|
2075
|
+
if (now - createdAt > SEVEN_DAYS_MS) {
|
|
2076
|
+
reasons.push("expired_ephemeral");
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
if (note.conflict === true) {
|
|
2080
|
+
reasons.push("pending_conflict");
|
|
2081
|
+
}
|
|
2082
|
+
if (reasons.length > 0) {
|
|
2083
|
+
if (!note.low_quality) {
|
|
2084
|
+
await patchNotePayload(note.id, { low_quality: true });
|
|
2085
|
+
}
|
|
2086
|
+
results.push({ note, reasons });
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
return results;
|
|
2090
|
+
}
|
|
2091
|
+
var DEFAULT_OUTPUT_DIR = process.env.AMEM_REVIEW_DIR || process.cwd();
|
|
2092
|
+
function nextBatchNumber(dir) {
|
|
2093
|
+
let max = 0;
|
|
2094
|
+
try {
|
|
2095
|
+
const files = fs3.readdirSync(dir);
|
|
2096
|
+
for (const f of files) {
|
|
2097
|
+
const match = f.match(/^amem-review-batch(\d+)\.md$/);
|
|
2098
|
+
if (match) {
|
|
2099
|
+
const n = parseInt(match[1], 10);
|
|
2100
|
+
if (n > max) max = n;
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
} catch {
|
|
2104
|
+
}
|
|
2105
|
+
return max + 1;
|
|
2106
|
+
}
|
|
2107
|
+
function reasonLabel(r) {
|
|
2108
|
+
if (LOCALE2 === "zh") {
|
|
2109
|
+
switch (r) {
|
|
2110
|
+
case "too_short":
|
|
2111
|
+
return "\u5185\u5BB9\u8FC7\u77ED\uFF08<10\u5B57\uFF09";
|
|
2112
|
+
case "expired_ephemeral":
|
|
2113
|
+
return "\u4E34\u65F6\u8BB0\u5FC6\u5DF2\u8FC7\u671F\uFF08>7\u5929\uFF09";
|
|
2114
|
+
case "pending_conflict":
|
|
2115
|
+
return "\u5B58\u5728\u51B2\u7A81\u6807\u8BB0";
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
switch (r) {
|
|
2119
|
+
case "too_short":
|
|
2120
|
+
return "Content too short (<10 chars)";
|
|
2121
|
+
case "expired_ephemeral":
|
|
2122
|
+
return "Ephemeral memory expired (>7 days)";
|
|
2123
|
+
case "pending_conflict":
|
|
2124
|
+
return "Pending conflict flag";
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
function severityBadge(reasons) {
|
|
2128
|
+
if (reasons.includes("too_short")) return "\u{1F534} LOW";
|
|
2129
|
+
if (reasons.includes("expired_ephemeral")) return "\u{1F7E1} EXPIRED";
|
|
2130
|
+
return "\u{1F7E0} CONFLICT";
|
|
2131
|
+
}
|
|
2132
|
+
async function generateReviewBatch(agentId, outputPath) {
|
|
2133
|
+
const root = path4.resolve(DEFAULT_OUTPUT_DIR);
|
|
2134
|
+
let filePath;
|
|
2135
|
+
let batchN;
|
|
2136
|
+
if (outputPath) {
|
|
2137
|
+
const name = path4.basename(outputPath);
|
|
2138
|
+
if (name !== outputPath || name === "" || name === "." || name === "..") {
|
|
2139
|
+
throw new Error(`[quality] outputPath \u5FC5\u987B\u662F\u7EAF\u6587\u4EF6\u540D\uFF08\u4E0D\u542B\u76EE\u5F55\uFF09: ${outputPath}`);
|
|
2140
|
+
}
|
|
2141
|
+
filePath = path4.join(root, name);
|
|
2142
|
+
batchN = 0;
|
|
2143
|
+
} else {
|
|
2144
|
+
batchN = nextBatchNumber(root);
|
|
2145
|
+
filePath = path4.join(root, `amem-review-batch${batchN}.md`);
|
|
2146
|
+
}
|
|
2147
|
+
const items = await scanLowQuality(agentId);
|
|
2148
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2149
|
+
const lines = [];
|
|
2150
|
+
const title = LOCALE2 === "zh" ? "A-MEM \u8D28\u91CF\u5BA1\u6838" : "A-MEM Quality Review";
|
|
2151
|
+
const genLabel = LOCALE2 === "zh" ? "\u751F\u6210\u65F6\u95F4" : "Generated";
|
|
2152
|
+
const countLabel = LOCALE2 === "zh" ? `\u5171 ${items.length} \u6761\u4F4E\u8D28\u91CF\u6761\u76EE` : `${items.length} low-quality item(s)`;
|
|
2153
|
+
const applyHint = LOCALE2 === "zh" ? "\u52FE\u9009\u540E\u4EA4\u7ED9\u52A9\u624B\u5904\u7406\u8FD9\u4E9B\u6761\u76EE" : "Tick your choices, then ask the assistant to act on them";
|
|
2154
|
+
lines.push(`# ${title} \u2014 Batch ${batchN || "custom"}`);
|
|
2155
|
+
lines.push("");
|
|
2156
|
+
lines.push(`> ${genLabel}\uFF1A${now} | ${countLabel}`);
|
|
2157
|
+
lines.push(`> ${applyHint}`);
|
|
2158
|
+
lines.push("");
|
|
2159
|
+
if (items.length === 0) {
|
|
2160
|
+
lines.push(LOCALE2 === "zh" ? "\u2705 \u6CA1\u6709\u53D1\u73B0\u4F4E\u8D28\u91CF\u6761\u76EE\u3002" : "\u2705 No low-quality items found.");
|
|
2161
|
+
}
|
|
2162
|
+
const byId = new Map(items.map((it) => [it.note.id, it.note]));
|
|
2163
|
+
const renderedPairs = /* @__PURE__ */ new Set();
|
|
2164
|
+
const pairLines = [];
|
|
2165
|
+
for (const { note } of items) {
|
|
2166
|
+
for (const otherId of note.conflicts_with ?? []) {
|
|
2167
|
+
const other = byId.get(otherId);
|
|
2168
|
+
if (!other) continue;
|
|
2169
|
+
const key = note.id < otherId ? `${note.id}:${otherId}` : `${otherId}:${note.id}`;
|
|
2170
|
+
if (renderedPairs.has(key)) continue;
|
|
2171
|
+
renderedPairs.add(key);
|
|
2172
|
+
const [newer, older] = Date.parse(note.timestamp) >= Date.parse(other.timestamp) ? [note, other] : [other, note];
|
|
2173
|
+
const zh2 = LOCALE2 === "zh";
|
|
2174
|
+
pairLines.push(`### \u{1F7E0} ${zh2 ? "\u51B2\u7A81" : "CONFLICT"} | ${newer.category || "General"}`);
|
|
2175
|
+
if (newer.conflict_reason) {
|
|
2176
|
+
pairLines.push(`**${zh2 ? "\u5224\u5B9A\u7406\u7531" : "Why"}\uFF1A** ${newer.conflict_reason}`);
|
|
2177
|
+
pairLines.push("");
|
|
2178
|
+
}
|
|
2179
|
+
pairLines.push(`| | ${zh2 ? "\u65F6\u95F4" : "When"} | ${zh2 ? "\u5185\u5BB9" : "Content"} |`);
|
|
2180
|
+
pairLines.push("| :-- | :-- | :-- |");
|
|
2181
|
+
pairLines.push(`| **A** | ${newer.timestamp.slice(0, 10)} | ${newer.content.replace(/\n/g, " ")} |`);
|
|
2182
|
+
pairLines.push(`| **B** | ${older.timestamp.slice(0, 10)} | ${older.content.replace(/\n/g, " ")} |`);
|
|
2183
|
+
pairLines.push("");
|
|
2184
|
+
pairLines.push(`\`A: ${newer.id}\``);
|
|
2185
|
+
pairLines.push(`\`B: ${older.id}\``);
|
|
2186
|
+
pairLines.push("");
|
|
2187
|
+
pairLines.push(
|
|
2188
|
+
zh2 ? `- [ ] \u2705 **A \u662F\u5F53\u524D\u72B6\u6001\uFF0C\u505C\u7528 B**\uFF08\u63A8\u8350\uFF1AA \u66F4\u65B0\uFF09` : `- [ ] \u2705 **A is current \u2014 retire B** (recommended: A is newer)`
|
|
2189
|
+
);
|
|
2190
|
+
pairLines.push(zh2 ? `- [ ] \u21A9\uFE0F B \u662F\u5F53\u524D\u72B6\u6001\uFF0C\u505C\u7528 A` : `- [ ] \u21A9\uFE0F B is current \u2014 retire A`);
|
|
2191
|
+
pairLines.push(zh2 ? `- [ ] \u{1F91D} \u4E24\u8005\u90FD\u6210\u7ACB\uFF08\u8BEF\u5224\uFF09` : `- [ ] \u{1F91D} Both hold \u2014 not a contradiction`);
|
|
2192
|
+
pairLines.push("");
|
|
2193
|
+
pairLines.push("---");
|
|
2194
|
+
pairLines.push("");
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
if (pairLines.length > 0) {
|
|
2198
|
+
lines.push(LOCALE2 === "zh" ? "## \u51B2\u7A81\uFF08\u6210\u5BF9\uFF0C\u4E00\u4E2A\u51B2\u7A81\u4E00\u4E2A\u51B3\u5B9A\uFF09" : "## Conflicts (paired \u2014 one decision each)");
|
|
2199
|
+
lines.push("");
|
|
2200
|
+
lines.push(...pairLines);
|
|
2201
|
+
lines.push(LOCALE2 === "zh" ? "## \u5176\u4F59\u6761\u76EE" : "## Other items");
|
|
2202
|
+
lines.push("");
|
|
2203
|
+
}
|
|
2204
|
+
for (let i = 0; i < items.length; i++) {
|
|
2205
|
+
const { note, reasons } = items[i];
|
|
2206
|
+
const badge = severityBadge(reasons);
|
|
2207
|
+
const reasonStr = reasons.map(reasonLabel).join("\u3001");
|
|
2208
|
+
const issueLabel = LOCALE2 === "zh" ? "\u95EE\u9898" : "Issue";
|
|
2209
|
+
const contentLabel = LOCALE2 === "zh" ? "\u5185\u5BB9" : "Content";
|
|
2210
|
+
const kwLabel = LOCALE2 === "zh" ? "\u5173\u952E\u8BCD" : "Keywords";
|
|
2211
|
+
const tagLabel = LOCALE2 === "zh" ? "\u6807\u7B7E" : "Tags";
|
|
2212
|
+
const keepLabel = LOCALE2 === "zh" ? "\u4FDD\u7559" : "Keep";
|
|
2213
|
+
const rewriteLabel = LOCALE2 === "zh" ? "\u6539\u5199" : "Rewrite";
|
|
2214
|
+
const deleteLabel = LOCALE2 === "zh" ? "\u5220\u9664" : "Delete";
|
|
2215
|
+
lines.push(`### [${i + 1}] ${badge} | ${note.category || "General"}`);
|
|
2216
|
+
lines.push(`\`${note.id}\``);
|
|
2217
|
+
lines.push("");
|
|
2218
|
+
lines.push(`**${issueLabel}\uFF1A** ${reasonStr}`);
|
|
2219
|
+
lines.push("");
|
|
2220
|
+
lines.push(`**${contentLabel}\uFF1A**`);
|
|
2221
|
+
lines.push("```");
|
|
2222
|
+
lines.push(note.content);
|
|
2223
|
+
lines.push("```");
|
|
2224
|
+
lines.push("");
|
|
2225
|
+
lines.push(`**${kwLabel}\uFF1A** ${note.keywords.join(", ")}`);
|
|
2226
|
+
lines.push(`**${tagLabel}\uFF1A** ${note.tags.join(", ")}`);
|
|
2227
|
+
lines.push("");
|
|
2228
|
+
lines.push(`- [ ] \u2705 ${keepLabel}`);
|
|
2229
|
+
lines.push(`- [ ] \u{1F527} ${rewriteLabel}`);
|
|
2230
|
+
lines.push(`- [ ] \u{1F5D1}\uFE0F ${deleteLabel}`);
|
|
2231
|
+
lines.push("");
|
|
2232
|
+
lines.push("---");
|
|
2233
|
+
lines.push("");
|
|
2234
|
+
}
|
|
2235
|
+
fs3.mkdirSync(path4.dirname(filePath), { recursive: true });
|
|
2236
|
+
fs3.writeFileSync(filePath, lines.join("\n"), "utf8");
|
|
2237
|
+
return filePath;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
// src/migrate.ts
|
|
2241
|
+
function missingDerivedFields(n) {
|
|
2242
|
+
return n.keywords.length === 0 || n.tags.length === 0;
|
|
2243
|
+
}
|
|
2244
|
+
async function migrateCollection(opts) {
|
|
2245
|
+
const { from, to } = opts;
|
|
2246
|
+
const refreshFields = opts.refreshFields !== false;
|
|
2247
|
+
const dryRun = opts.dryRun !== false;
|
|
2248
|
+
const log = opts.logger?.info ?? ((m) => console.log(m));
|
|
2249
|
+
const warn = opts.logger?.warn ?? ((m) => console.warn(m));
|
|
2250
|
+
if (from === to) throw new Error(`migrate: source and target are the same collection ("${from}")`);
|
|
2251
|
+
const model = getEmbeddingModel();
|
|
2252
|
+
const targetDim = await getEmbeddingDim();
|
|
2253
|
+
const sourceDim = await collectionDimRaw(from);
|
|
2254
|
+
if (sourceDim === null) throw new Error(`migrate: source collection "${from}" does not exist`);
|
|
2255
|
+
const points = await scrollAllRaw(from);
|
|
2256
|
+
const notes = points.map(pointToNote);
|
|
2257
|
+
const missingDerived = notes.filter(missingDerivedFields).length;
|
|
2258
|
+
log(
|
|
2259
|
+
`[migrate] ${from} (${sourceDim}d, ${notes.length} notes) \u2192 ${to} (${targetDim}d, ${model}); ${missingDerived} note(s) missing keywords/tags`
|
|
2260
|
+
);
|
|
2261
|
+
if (dryRun) {
|
|
2262
|
+
log("[migrate] dry run \u2014 nothing written. Pass dryRun: false to apply.");
|
|
2263
|
+
return { total: notes.length, missingDerived, refreshed: 0, migrated: 0, sourceDim, targetDim, model, dryRun: true };
|
|
2264
|
+
}
|
|
2265
|
+
const existingTargetDim = await collectionDimRaw(to);
|
|
2266
|
+
if (existingTargetDim === null) {
|
|
2267
|
+
await createCollectionRaw(to, targetDim);
|
|
2268
|
+
log(`[migrate] created ${to} at ${targetDim}d`);
|
|
2269
|
+
} else {
|
|
2270
|
+
if (existingTargetDim !== targetDim) {
|
|
2271
|
+
throw new Error(`migrate: target "${to}" exists at ${existingTargetDim}d but the model produces ${targetDim}d`);
|
|
2272
|
+
}
|
|
2273
|
+
const existingCount = await countPointsRaw(to);
|
|
2274
|
+
if (existingCount > 0) {
|
|
2275
|
+
throw new Error(`migrate: target "${to}" already holds ${existingCount} point(s); use an empty collection`);
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
let refreshed = 0;
|
|
2279
|
+
let migrated = 0;
|
|
2280
|
+
const BATCH = 64;
|
|
2281
|
+
let buffer = [];
|
|
2282
|
+
const flush = async () => {
|
|
2283
|
+
if (!buffer.length) return;
|
|
2284
|
+
await upsertPointsRaw(to, buffer);
|
|
2285
|
+
migrated += buffer.length;
|
|
2286
|
+
buffer = [];
|
|
2287
|
+
};
|
|
2288
|
+
for (const note of notes) {
|
|
2289
|
+
if (refreshFields && missingDerivedFields(note)) {
|
|
2290
|
+
try {
|
|
2291
|
+
const built = await llmConstructNote(note.content);
|
|
2292
|
+
if (note.keywords.length === 0) note.keywords = built.keywords;
|
|
2293
|
+
if (note.tags.length === 0) note.tags = built.tags;
|
|
2294
|
+
if (!note.context) note.context = built.context;
|
|
2295
|
+
refreshed++;
|
|
2296
|
+
} catch (e) {
|
|
2297
|
+
warn(`[migrate] re-extract failed for ${note.id.slice(0, 8)} \u2014 keeping as-is: ${e.message}`);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
const point = noteToPoint({ ...note, embedding: await encode(buildEmbedText(note)) });
|
|
2301
|
+
buffer.push(point);
|
|
2302
|
+
if (buffer.length >= BATCH) {
|
|
2303
|
+
await flush();
|
|
2304
|
+
log(`[migrate] ${migrated}/${notes.length}`);
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
await flush();
|
|
2308
|
+
const finalCount = await countPointsRaw(to);
|
|
2309
|
+
if (finalCount !== notes.length) {
|
|
2310
|
+
warn(`[migrate] target holds ${finalCount} point(s) but the source had ${notes.length} \u2014 check before switching`);
|
|
2311
|
+
}
|
|
2312
|
+
log(
|
|
2313
|
+
`[migrate] done: ${migrated} migrated, ${refreshed} re-extracted. "${from}" is untouched \u2014 switch with AMEM_COLLECTION=${to}, and keep the old one until you are satisfied.`
|
|
2314
|
+
);
|
|
2315
|
+
return { total: notes.length, missingDerived, refreshed, migrated, sourceDim, targetDim, model, dryRun: false };
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
// src/crud-guard.ts
|
|
2319
|
+
var DEFAULT_CRUD_UPDATE_MIN_SIM = 0.35;
|
|
2320
|
+
function resolveCrudUpdateMinSim(override) {
|
|
2321
|
+
const envVal = Number(process.env.AMEM_CRUD_UPDATE_MIN_SIM);
|
|
2322
|
+
if (Number.isFinite(envVal) && envVal >= 0) return envVal;
|
|
2323
|
+
if (override !== void 0 && Number.isFinite(override) && override >= 0) return override;
|
|
2324
|
+
return DEFAULT_CRUD_UPDATE_MIN_SIM;
|
|
2325
|
+
}
|
|
2326
|
+
function isPlausibleUpdateTarget(newEmbedding, targetEmbedding, minSimilarity) {
|
|
2327
|
+
if (!newEmbedding?.length || !targetEmbedding?.length) return false;
|
|
2328
|
+
if (newEmbedding.length !== targetEmbedding.length) return false;
|
|
2329
|
+
return cosineSimilarity(newEmbedding, targetEmbedding) >= resolveCrudUpdateMinSim(minSimilarity);
|
|
2330
|
+
}
|
|
2331
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2332
|
+
0 && (module.exports = {
|
|
2333
|
+
DEFAULT_CRUD_UPDATE_MIN_SIM,
|
|
2334
|
+
DEFAULT_EMBEDDING_MODEL,
|
|
2335
|
+
EmbeddingDimensionMismatchError,
|
|
2336
|
+
addEpisodic,
|
|
2337
|
+
addMemory,
|
|
2338
|
+
canRead,
|
|
2339
|
+
canWrite,
|
|
2340
|
+
checkQuality,
|
|
2341
|
+
configure,
|
|
2342
|
+
configureLlm,
|
|
2343
|
+
conflictSweep,
|
|
2344
|
+
consolidateMemories,
|
|
2345
|
+
createStorageContext,
|
|
2346
|
+
deleteNote,
|
|
2347
|
+
encode,
|
|
2348
|
+
ensureCollection,
|
|
2349
|
+
generateReviewBatch,
|
|
2350
|
+
getEmbeddingDim,
|
|
2351
|
+
getEmbeddingModel,
|
|
2352
|
+
getNote,
|
|
2353
|
+
invalidateNote,
|
|
2354
|
+
isModelLoaded,
|
|
2355
|
+
isPlausibleUpdateTarget,
|
|
2356
|
+
listMemories,
|
|
2357
|
+
listNotes,
|
|
2358
|
+
llmCrudDecision,
|
|
2359
|
+
loadModel,
|
|
2360
|
+
mergeSimilarNotes,
|
|
2361
|
+
migrateCollection,
|
|
2362
|
+
patchNotePayload,
|
|
2363
|
+
pingQdrant,
|
|
2364
|
+
resolveCrudUpdateMinSim,
|
|
2365
|
+
scanLowQuality,
|
|
2366
|
+
searchMemory,
|
|
2367
|
+
updateNote
|
|
2368
|
+
});
|
|
2369
|
+
//# sourceMappingURL=index.cjs.map
|