openclaw-amem 1.1.5 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/index.js +537 -557
- package/openclaw.plugin.json +1 -1
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -110,8 +110,8 @@ A memory plugin's job is to read configuration from the environment and send mem
|
|
|
110
110
|
|
|
111
111
|
What it actually does — all of it declared in [`openclaw.plugin.json`](openclaw.plugin.json):
|
|
112
112
|
|
|
113
|
-
- **Environment variables it reads** (its configuration surface, supplied by you): `AMEM_LLM_API_KEY`, `AMEM_LLM_BASE_URL`, `AMEM_LLM_MODEL`, `AMEM_COLLECTION`, `AMEM_DATA_DIR`, `AMEM_EVO_COUNTER_PATH`, `AMEM_REVIEW_DIR`, `AMEM_PROMPT_LOCALE`. No credential is bundled, hardcoded, or logged.
|
|
114
|
-
- **Network destinations**: only your **local Qdrant** (`http://localhost:6333`) and your configured **LLM endpoint** (Anthropic by default, or `AMEM_LLM_BASE_URL`). It sends memory text/embeddings there to store and evolve notes — its stated purpose. It does not phone home.
|
|
113
|
+
- **Environment variables it reads** (its configuration surface, supplied by you): `AMEM_LLM_PROVIDER`, `AMEM_LLM_API_KEY`, `AMEM_LLM_BASE_URL`, `AMEM_LLM_MODEL`, `AMEM_COLLECTION`, `AMEM_DATA_DIR`, `AMEM_EVO_COUNTER_PATH`, `AMEM_REVIEW_DIR`, `AMEM_PROMPT_LOCALE`. No credential is bundled, hardcoded, or logged.
|
|
114
|
+
- **Network destinations**: only your **local Qdrant** (`http://localhost:6333`) and your configured **LLM endpoint** (Anthropic by default, or any OpenAI-compatible endpoint via `AMEM_LLM_PROVIDER=openai` + `AMEM_LLM_BASE_URL`). It sends memory text/embeddings there to store and evolve notes — its stated purpose. It does not phone home.
|
|
115
115
|
- **Conversation content** is processed for memory only when you set `hooks.allowConversationAccess: true`. Keep Qdrant and review-output paths scoped to locations you control.
|
|
116
116
|
|
|
117
117
|
## Development
|
package/dist/index.js
CHANGED
|
@@ -122,468 +122,6 @@ var init_embedding = __esm({
|
|
|
122
122
|
}
|
|
123
123
|
});
|
|
124
124
|
|
|
125
|
-
// ../amem-core/src/evo-counter.ts
|
|
126
|
-
function counterFile() {
|
|
127
|
-
return process.env.AMEM_EVO_COUNTER_PATH || path2.join(getDataDir(), "amem_evo_cnt.json");
|
|
128
|
-
}
|
|
129
|
-
function getEvoCount() {
|
|
130
|
-
try {
|
|
131
|
-
const data = JSON.parse(fs.readFileSync(counterFile(), "utf-8"));
|
|
132
|
-
return data.count || 0;
|
|
133
|
-
} catch {
|
|
134
|
-
return 0;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
function incrementEvoCount() {
|
|
138
|
-
const count = getEvoCount() + 1;
|
|
139
|
-
fs.writeFileSync(counterFile(), JSON.stringify({ count, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
140
|
-
return count;
|
|
141
|
-
}
|
|
142
|
-
function shouldRunEvolution() {
|
|
143
|
-
const count = incrementEvoCount();
|
|
144
|
-
return count % EVO_THRESHOLD === 0;
|
|
145
|
-
}
|
|
146
|
-
var fs, path2, EVO_THRESHOLD;
|
|
147
|
-
var init_evo_counter = __esm({
|
|
148
|
-
"../amem-core/src/evo-counter.ts"() {
|
|
149
|
-
"use strict";
|
|
150
|
-
fs = __toESM(require("fs"), 1);
|
|
151
|
-
path2 = __toESM(require("path"), 1);
|
|
152
|
-
init_config();
|
|
153
|
-
EVO_THRESHOLD = 20;
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// ../amem-core/src/prompts.ts
|
|
158
|
-
var LOCALE, en, zh, templates, t;
|
|
159
|
-
var init_prompts = __esm({
|
|
160
|
-
"../amem-core/src/prompts.ts"() {
|
|
161
|
-
"use strict";
|
|
162
|
-
LOCALE = process.env.AMEM_PROMPT_LOCALE === "zh" ? "zh" : "en";
|
|
163
|
-
en = {
|
|
164
|
-
crudDecision: (userText, assistantText, memoryList) => `You are a memory management agent. Analyze the conversation and decide what memory operations are needed.
|
|
165
|
-
|
|
166
|
-
## Conversation
|
|
167
|
-
|
|
168
|
-
User: ${userText}
|
|
169
|
-
Assistant: ${assistantText}
|
|
170
|
-
|
|
171
|
-
## Existing relevant memories (identified by integer idx)
|
|
172
|
-
|
|
173
|
-
${memoryList}
|
|
174
|
-
|
|
175
|
-
## Task
|
|
176
|
-
|
|
177
|
-
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.
|
|
178
|
-
|
|
179
|
-
## Operation types
|
|
180
|
-
- NEW: Extract a brand new fact not present in existing memories
|
|
181
|
-
- UPDATE: New information refines or supersedes an existing memory; specify existingIdx
|
|
182
|
-
- DELETE: An existing memory is outdated, contradicted, or wrong; specify existingIdx, fact = original content
|
|
183
|
-
- NONE: Nothing worth recording, or information already fully captured
|
|
184
|
-
|
|
185
|
-
## Output format
|
|
186
|
-
|
|
187
|
-
Return a JSON array. Each item:
|
|
188
|
-
{"action": "NEW"|"UPDATE"|"DELETE"|"NONE", "fact": "fact content", "existingIdx": integer or omit, "reason": "optional"}
|
|
189
|
-
|
|
190
|
-
Return at most 3 operations. If nothing is worth recording, return [].
|
|
191
|
-
Return only the JSON array, no other text.
|
|
192
|
-
|
|
193
|
-
Examples:
|
|
194
|
-
|
|
195
|
-
1. New preference:
|
|
196
|
-
[{"action": "NEW", "fact": "User prefers TypeScript over JavaScript", "reason": "Explicitly stated tech preference"}]
|
|
197
|
-
|
|
198
|
-
2. Updating an existing memory (idx 0 was "User is evaluating React and Vue"):
|
|
199
|
-
[{"action": "UPDATE", "fact": "User decided to use React (dropped Vue)", "existingIdx": 0, "reason": "Decision finalized, update evaluation status"}]
|
|
200
|
-
|
|
201
|
-
3. Conversation is just "Sure, thanks" / "Got it" with no new info:
|
|
202
|
-
[]`,
|
|
203
|
-
shouldMerge: (contentA, contentB) => `You are a memory deduplication assistant. Determine whether two memories express essentially the same information.
|
|
204
|
-
|
|
205
|
-
Memory A: ${contentA}
|
|
206
|
-
Memory B: ${contentB}
|
|
207
|
-
|
|
208
|
-
Rules:
|
|
209
|
-
- If both memories express the same core fact (possibly different wording or granularity), return:
|
|
210
|
-
{"shouldMerge": true, "merged": "Concise merged statement preserving key details from both, more complete than either alone"}
|
|
211
|
-
- If the memories are complementary, on different topics, or contain different specific facts, return:
|
|
212
|
-
{"shouldMerge": false}
|
|
213
|
-
|
|
214
|
-
Return only JSON, no other text.
|
|
215
|
-
|
|
216
|
-
Examples:
|
|
217
|
-
|
|
218
|
-
1. Should merge (different granularity):
|
|
219
|
-
A: "Project uses PostgreSQL"
|
|
220
|
-
B: "Project's primary database is PostgreSQL 16, deployed on AWS RDS"
|
|
221
|
-
-> {"shouldMerge": true, "merged": "Project uses PostgreSQL 16 as primary database, deployed on AWS RDS"}
|
|
222
|
-
|
|
223
|
-
2. Should NOT merge (complementary but distinct):
|
|
224
|
-
A: "User prefers VS Code"
|
|
225
|
-
B: "User's VS Code uses One Dark Pro theme"
|
|
226
|
-
-> {"shouldMerge": false}`,
|
|
227
|
-
evolutionJudge: (oldContent, newContent) => `You are a memory evolution judge. Analyze the relationship between an old and new memory and return JSON.
|
|
228
|
-
|
|
229
|
-
Old memory: ${oldContent}
|
|
230
|
-
New memory: ${newContent}
|
|
231
|
-
|
|
232
|
-
Classification rules:
|
|
233
|
-
|
|
234
|
-
- EVOLVE: New content deepens or updates the old memory (e.g. "Considering Next.js" -> "Decided on Next.js 14 App Router")
|
|
235
|
-
Return: {"type": "EVOLVE", "mergedContent": "Merged content preserving the evolution trajectory"}
|
|
236
|
-
|
|
237
|
-
- CONFLICT: Old and new information directly contradict each other on the same attribute (e.g. "Uses MySQL as primary DB" vs "Migrated to PostgreSQL")
|
|
238
|
-
Return: {"type": "CONFLICT"}
|
|
239
|
-
|
|
240
|
-
- EXPAND: New information supplements the old memory on the same topic (e.g. "Handles backend dev" + "Backend uses Go and gRPC")
|
|
241
|
-
Return: {"type": "EXPAND", "mergedContent": "Merged content integrating both pieces of information"}
|
|
242
|
-
|
|
243
|
-
- NEW: Completely unrelated information, no substantive connection to the old memory
|
|
244
|
-
Return: {"type": "NEW"}
|
|
245
|
-
|
|
246
|
-
Return only JSON, no other text.`
|
|
247
|
-
};
|
|
248
|
-
zh = {
|
|
249
|
-
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
|
|
250
|
-
|
|
251
|
-
## \u5BF9\u8BDD\u5185\u5BB9
|
|
252
|
-
|
|
253
|
-
\u7528\u6237\uFF1A${userText}
|
|
254
|
-
\u52A9\u624B\uFF1A${assistantText}
|
|
255
|
-
|
|
256
|
-
## \u5DF2\u6709\u76F8\u5173\u8BB0\u5FC6\uFF08\u7528\u6574\u6570 idx \u6807\u8BC6\uFF09
|
|
257
|
-
|
|
258
|
-
${memoryList}
|
|
259
|
-
|
|
260
|
-
## \u4EFB\u52A1
|
|
261
|
-
|
|
262
|
-
\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
|
|
263
|
-
|
|
264
|
-
## \u64CD\u4F5C\u7C7B\u578B
|
|
265
|
-
- NEW\uFF1A\u63D0\u53D6\u5168\u65B0\u4E8B\u5B9E\uFF08\u5DF2\u6709\u8BB0\u5FC6\u4E2D\u6CA1\u6709\u7684\u4FE1\u606F\uFF09
|
|
266
|
-
- 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
|
|
267
|
-
- 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
|
|
268
|
-
- NONE\uFF1A\u4E0D\u503C\u5F97\u8BB0\u5F55\u6216\u5DF2\u6709\u5B8C\u5168\u76F8\u540C\u7684\u4FE1\u606F
|
|
269
|
-
|
|
270
|
-
## \u8F93\u51FA\u683C\u5F0F
|
|
271
|
-
|
|
272
|
-
\u8FD4\u56DE JSON \u6570\u7EC4\uFF0C\u6BCF\u6761\u683C\u5F0F\uFF1A
|
|
273
|
-
{"action": "NEW"|"UPDATE"|"DELETE"|"NONE", "fact": "\u4E8B\u5B9E\u5185\u5BB9", "existingIdx": \u6574\u6570\u6216\u7701\u7565, "reason": "\u539F\u56E0\uFF08\u53EF\u9009\uFF09"}
|
|
274
|
-
|
|
275
|
-
\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
|
|
276
|
-
\u53EA\u8FD4\u56DE JSON \u6570\u7EC4\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3002
|
|
277
|
-
|
|
278
|
-
\u793A\u4F8B\uFF1A
|
|
279
|
-
|
|
280
|
-
1. \u63D0\u53D6\u65B0\u504F\u597D\uFF1A
|
|
281
|
-
[{"action": "NEW", "fact": "\u7528\u6237\u504F\u597D TypeScript \u800C\u975E JavaScript", "reason": "\u660E\u786E\u8868\u8FBE\u7684\u6280\u672F\u504F\u597D"}]
|
|
282
|
-
|
|
283
|
-
2. \u66F4\u65B0\u5DF2\u6709\u8BB0\u5FC6\uFF08idx 0 \u539F\u4E3A"\u7528\u6237\u6B63\u5728\u8BC4\u4F30 React \u548C Vue"\uFF09\uFF1A
|
|
284
|
-
[{"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"}]
|
|
285
|
-
|
|
286
|
-
3. \u5BF9\u8BDD\u4EC5\u4E3A"\u597D\u7684\uFF0C\u8C22\u8C22"/"\u6CA1\u95EE\u9898"\u7B49\u786E\u8BA4\u8BED\uFF0C\u65E0\u65B0\u4FE1\u606F\uFF1A
|
|
287
|
-
[]`,
|
|
288
|
-
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
|
|
289
|
-
|
|
290
|
-
\u8BB0\u5FC6A\uFF1A${contentA}
|
|
291
|
-
\u8BB0\u5FC6B\uFF1A${contentB}
|
|
292
|
-
|
|
293
|
-
\u5224\u65AD\u89C4\u5219\uFF1A
|
|
294
|
-
- \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
|
|
295
|
-
{"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"}
|
|
296
|
-
- \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
|
|
297
|
-
{"shouldMerge": false}
|
|
298
|
-
|
|
299
|
-
\u53EA\u8FD4\u56DE JSON\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3002
|
|
300
|
-
|
|
301
|
-
\u793A\u4F8B\uFF1A
|
|
302
|
-
|
|
303
|
-
1. \u5E94\u5408\u5E76\uFF08\u7C92\u5EA6\u4E0D\u540C\uFF09\uFF1A
|
|
304
|
-
A: "\u9879\u76EE\u4F7F\u7528 PostgreSQL \u6570\u636E\u5E93"
|
|
305
|
-
B: "\u9879\u76EE\u7684\u4E3B\u6570\u636E\u5E93\u662F PostgreSQL 16\uFF0C\u90E8\u7F72\u5728 AWS RDS \u4E0A"
|
|
306
|
-
\u2192 {"shouldMerge": true, "merged": "\u9879\u76EE\u4F7F\u7528 PostgreSQL 16 \u4F5C\u4E3A\u4E3B\u6570\u636E\u5E93\uFF0C\u90E8\u7F72\u5728 AWS RDS \u4E0A"}
|
|
307
|
-
|
|
308
|
-
2. \u4E0D\u5E94\u5408\u5E76\uFF08\u4E92\u8865\u4F46\u4E0D\u540C\uFF09\uFF1A
|
|
309
|
-
A: "\u7528\u6237\u559C\u6B22\u7528 VS Code"
|
|
310
|
-
B: "\u7528\u6237\u7684 VS Code \u4F7F\u7528 One Dark Pro \u4E3B\u9898"
|
|
311
|
-
\u2192 {"shouldMerge": false}`,
|
|
312
|
-
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
|
|
313
|
-
|
|
314
|
-
\u65E7\u8BB0\u5FC6\uFF1A${oldContent}
|
|
315
|
-
\u65B0\u8BB0\u5FC6\uFF1A${newContent}
|
|
316
|
-
|
|
317
|
-
\u5224\u65AD\u89C4\u5219\uFF1A
|
|
318
|
-
|
|
319
|
-
- 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
|
|
320
|
-
\u8FD4\u56DE\uFF1A{"type": "EVOLVE", "mergedContent": "\u878D\u5408\u540E\u7684\u5B8C\u6574\u5185\u5BB9\uFF0C\u4FDD\u7559\u6F14\u5316\u8F68\u8FF9"}
|
|
321
|
-
|
|
322
|
-
- 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
|
|
323
|
-
\u8FD4\u56DE\uFF1A{"type": "CONFLICT"}
|
|
324
|
-
|
|
325
|
-
- 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
|
|
326
|
-
\u8FD4\u56DE\uFF1A{"type": "EXPAND", "mergedContent": "\u5408\u5E76\u540E\u7684\u5B8C\u6574\u5185\u5BB9\uFF0C\u6574\u5408\u53CC\u65B9\u4FE1\u606F"}
|
|
327
|
-
|
|
328
|
-
- 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
|
|
329
|
-
\u8FD4\u56DE\uFF1A{"type": "NEW"}
|
|
330
|
-
|
|
331
|
-
\u53EA\u8FD4\u56DE JSON\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3002`
|
|
332
|
-
};
|
|
333
|
-
templates = { en, zh };
|
|
334
|
-
t = templates[LOCALE];
|
|
335
|
-
}
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
// ../amem-core/src/llm.ts
|
|
339
|
-
async function llmCall(prompt, maxTokens = 500) {
|
|
340
|
-
try {
|
|
341
|
-
const isThinking = MODEL.includes("gemini") || MODEL.includes("pro-agent");
|
|
342
|
-
const effectiveMaxTokens = isThinking ? Math.max(maxTokens * 8, 4e3) : maxTokens;
|
|
343
|
-
const resp = await client.messages.create({
|
|
344
|
-
model: MODEL,
|
|
345
|
-
max_tokens: effectiveMaxTokens,
|
|
346
|
-
messages: [{ role: "user", content: prompt }]
|
|
347
|
-
});
|
|
348
|
-
for (const block of resp.content) {
|
|
349
|
-
if (block.type === "text") return block.text.trim();
|
|
350
|
-
}
|
|
351
|
-
return null;
|
|
352
|
-
} catch (e) {
|
|
353
|
-
console.error(`[amem] LLM call failed: ${e.message}`);
|
|
354
|
-
return null;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
function stripFences(raw) {
|
|
358
|
-
raw = raw.trim();
|
|
359
|
-
if (raw.startsWith("```")) {
|
|
360
|
-
const lines = raw.split("\n");
|
|
361
|
-
lines.shift();
|
|
362
|
-
if (lines[lines.length - 1] === "```") lines.pop();
|
|
363
|
-
raw = lines.join("\n").trim();
|
|
364
|
-
}
|
|
365
|
-
if (raw.startsWith('"') && raw.endsWith('"') || raw.startsWith("'") && raw.endsWith("'")) {
|
|
366
|
-
try {
|
|
367
|
-
raw = JSON.parse(raw);
|
|
368
|
-
} catch {
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
return raw;
|
|
372
|
-
}
|
|
373
|
-
async function llmConstructNote(content) {
|
|
374
|
-
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:
|
|
375
|
-
{
|
|
376
|
-
"keywords": ["keyword1", "keyword2"],
|
|
377
|
-
"tags": ["tag1", "tag2"],
|
|
378
|
-
"context": "one sentence summary in the same language as the input",
|
|
379
|
-
"category": "Technical|Business|Personal|Project|Research|System|General",
|
|
380
|
-
"note_type": "memory|knowledge",
|
|
381
|
-
"topics": ["Topic1", "Topic2"],
|
|
382
|
-
"confidence": "high|medium|low"
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
Category guide:
|
|
386
|
-
- Technical: code, tools, configuration, APIs, debugging
|
|
387
|
-
- Business: company, finance, compliance, contracts, invoices
|
|
388
|
-
- Personal: personal state, habits, preferences, emotions
|
|
389
|
-
- Project: project progress, decisions, milestones
|
|
390
|
-
- Research: research, literature, evaluation, comparison
|
|
391
|
-
- System: system services, monitoring, operations
|
|
392
|
-
- General: anything that does not fit the above
|
|
393
|
-
|
|
394
|
-
note_type guide:
|
|
395
|
-
- knowledge: books, methodologies, tools, domain knowledge, reference material \u2014 durable, no strong time component
|
|
396
|
-
- memory: events, decisions, preferences, states, observations \u2014 episodic, time-sensitive
|
|
397
|
-
|
|
398
|
-
topics guide (Story 26B):
|
|
399
|
-
- Only populate for knowledge notes (note_type=knowledge). For memory notes, return [].
|
|
400
|
-
- List 1-5 concise subject tags representing the main topics of this knowledge, e.g. ["TypeScript", "Qdrant", "Vector DB"].
|
|
401
|
-
|
|
402
|
-
confidence guide (Story 27):
|
|
403
|
-
- high: note_type is unambiguous \u2014 clearly episodic (event/decision/state) or clearly durable knowledge (tool doc/methodology)
|
|
404
|
-
- medium: some ambiguity \u2014 e.g. "learned X method" could be either memory or knowledge
|
|
405
|
-
- low: LLM is uncertain \u2014 vague, fragmentary, or mixed content
|
|
406
|
-
|
|
407
|
-
Text: ${content}`;
|
|
408
|
-
const raw = await llmCall(prompt, 400);
|
|
409
|
-
if (!raw)
|
|
410
|
-
return {
|
|
411
|
-
keywords: [],
|
|
412
|
-
tags: [],
|
|
413
|
-
context: "",
|
|
414
|
-
category: "General",
|
|
415
|
-
note_type: "memory",
|
|
416
|
-
topics: [],
|
|
417
|
-
confidence: "medium"
|
|
418
|
-
};
|
|
419
|
-
try {
|
|
420
|
-
const data = JSON.parse(stripFences(raw));
|
|
421
|
-
const rawCategory = typeof data.category === "string" ? data.category : "General";
|
|
422
|
-
const category = VALID_CATEGORIES.has(rawCategory) ? rawCategory : "General";
|
|
423
|
-
const note_type = data.note_type === "knowledge" ? "knowledge" : "memory";
|
|
424
|
-
const topics = note_type === "knowledge" && Array.isArray(data.topics) ? data.topics.filter((v) => typeof v === "string") : [];
|
|
425
|
-
const rawConfidence = typeof data.confidence === "string" ? data.confidence : "medium";
|
|
426
|
-
const confidence = VALID_CONFIDENCE.has(rawConfidence) ? rawConfidence : "medium";
|
|
427
|
-
return {
|
|
428
|
-
keywords: Array.isArray(data.keywords) ? data.keywords : [],
|
|
429
|
-
tags: Array.isArray(data.tags) ? data.tags : [],
|
|
430
|
-
context: typeof data.context === "string" ? data.context : "",
|
|
431
|
-
category,
|
|
432
|
-
note_type,
|
|
433
|
-
topics,
|
|
434
|
-
confidence
|
|
435
|
-
};
|
|
436
|
-
} catch (e) {
|
|
437
|
-
console.error(`[amem] Note construction parse failed: ${e.message}`);
|
|
438
|
-
return {
|
|
439
|
-
keywords: [],
|
|
440
|
-
tags: [],
|
|
441
|
-
context: "",
|
|
442
|
-
category: "General",
|
|
443
|
-
note_type: "memory",
|
|
444
|
-
topics: [],
|
|
445
|
-
confidence: "medium"
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
async function llmShouldLink(noteContent, candidateContent) {
|
|
450
|
-
const prompt = `Do these two memory notes have a meaningful relationship that would be useful to link?
|
|
451
|
-
Reply with only "yes" or "no".
|
|
452
|
-
|
|
453
|
-
Note A: ${noteContent}
|
|
454
|
-
Note B: ${candidateContent}`;
|
|
455
|
-
const raw = await llmCall(prompt, 10);
|
|
456
|
-
if (!raw) return false;
|
|
457
|
-
return raw.toLowerCase().startsWith("yes");
|
|
458
|
-
}
|
|
459
|
-
async function llmCrudDecision(userText, assistantText, existingMemories) {
|
|
460
|
-
const memoryList = existingMemories.length > 0 ? existingMemories.map((m) => `[${m.idx}] ${m.content}`).join("\n") : "(none)";
|
|
461
|
-
const prompt = t.crudDecision(userText.slice(0, 500), assistantText.slice(0, 500), memoryList);
|
|
462
|
-
try {
|
|
463
|
-
const raw = await llmCall(prompt, 400);
|
|
464
|
-
if (!raw) return [];
|
|
465
|
-
const match = raw.match(/\[.*\]/s);
|
|
466
|
-
if (!match) return [];
|
|
467
|
-
const parsed = JSON.parse(match[0]);
|
|
468
|
-
if (!Array.isArray(parsed)) return [];
|
|
469
|
-
const ops = [];
|
|
470
|
-
for (const item of parsed) {
|
|
471
|
-
if (!item || typeof item !== "object") continue;
|
|
472
|
-
const action = item.action;
|
|
473
|
-
if (!["NEW", "UPDATE", "DELETE", "NONE"].includes(action)) continue;
|
|
474
|
-
if (action === "NONE") continue;
|
|
475
|
-
const op = {
|
|
476
|
-
action,
|
|
477
|
-
fact: typeof item.fact === "string" ? item.fact : "",
|
|
478
|
-
reason: typeof item.reason === "string" ? item.reason : void 0
|
|
479
|
-
};
|
|
480
|
-
if (typeof item.existingIdx === "number") {
|
|
481
|
-
op.existingIdx = item.existingIdx;
|
|
482
|
-
}
|
|
483
|
-
ops.push(op);
|
|
484
|
-
}
|
|
485
|
-
return ops.slice(0, 3);
|
|
486
|
-
} catch (e) {
|
|
487
|
-
console.error(`[amem] llmCrudDecision failed: ${e.message}`);
|
|
488
|
-
return [];
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
async function llmShouldMerge(contentA, contentB) {
|
|
492
|
-
const prompt = t.shouldMerge(contentA, contentB);
|
|
493
|
-
const raw = await llmCall(prompt, 300);
|
|
494
|
-
if (!raw) return { shouldMerge: false };
|
|
495
|
-
try {
|
|
496
|
-
const data = JSON.parse(stripFences(raw));
|
|
497
|
-
if (typeof data.shouldMerge !== "boolean") return { shouldMerge: false };
|
|
498
|
-
if (data.shouldMerge && typeof data.merged === "string") {
|
|
499
|
-
return { shouldMerge: true, merged: data.merged };
|
|
500
|
-
}
|
|
501
|
-
return { shouldMerge: false };
|
|
502
|
-
} catch (e) {
|
|
503
|
-
console.error(`[amem] llmShouldMerge parse failed: ${e.message}`);
|
|
504
|
-
return { shouldMerge: false };
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
async function llmEvolutionJudge(oldContent, newContent) {
|
|
508
|
-
const prompt = t.evolutionJudge(oldContent, newContent);
|
|
509
|
-
const raw = await llmCall(prompt, 300);
|
|
510
|
-
if (!raw) return { type: "NEW" };
|
|
511
|
-
try {
|
|
512
|
-
const data = JSON.parse(stripFences(raw));
|
|
513
|
-
const type = VALID_EVOLUTION_TYPES.has(data.type) ? data.type : "NEW";
|
|
514
|
-
return {
|
|
515
|
-
type,
|
|
516
|
-
mergedContent: typeof data.mergedContent === "string" ? data.mergedContent : void 0
|
|
517
|
-
};
|
|
518
|
-
} catch (e) {
|
|
519
|
-
console.error(`[amem] llmEvolutionJudge parse failed: ${e.message}`);
|
|
520
|
-
return { type: "NEW" };
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
async function llmEvolveNote(content, linkedNotes) {
|
|
524
|
-
const linkedStr = linkedNotes.map((n) => `- ID: ${n.id}
|
|
525
|
-
Content: ${n.content}`).join("\n");
|
|
526
|
-
const prompt = `A memory note has gained new connections. Update its context, tags, and decide whether to strengthen connections with specific neighbors.
|
|
527
|
-
Reply with JSON only (no markdown):
|
|
528
|
-
{
|
|
529
|
-
"tags": ["tag1", "tag2", ...],
|
|
530
|
-
"context": "updated one sentence summary",
|
|
531
|
-
"should_strengthen": true|false,
|
|
532
|
-
"suggested_connections": ["neighbor_id_1", "neighbor_id_2", ...],
|
|
533
|
-
"tags_to_update": ["tag_1", ..., "tag_n"]
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
Guidelines:
|
|
537
|
-
- "tags" and "context" are for updating the original note based on new connections.
|
|
538
|
-
- "should_strengthen" is a decision whether this note should strengthen its connections to any of the newly linked notes (neighbors).
|
|
539
|
-
- "suggested_connections" must contain only IDs from the newly linked notes (neighbors) listed below.
|
|
540
|
-
- "tags_to_update" are updated tags for the original note itself if we strengthen connections.
|
|
541
|
-
|
|
542
|
-
Original note content: ${content}
|
|
543
|
-
|
|
544
|
-
Newly linked notes (neighbors):
|
|
545
|
-
${linkedStr}`;
|
|
546
|
-
const raw = await llmCall(prompt, 500);
|
|
547
|
-
if (!raw) return { tags: null, context: null, shouldStrengthen: false, suggestedConnections: [], tagsToUpdate: [] };
|
|
548
|
-
try {
|
|
549
|
-
const data = JSON.parse(stripFences(raw));
|
|
550
|
-
return {
|
|
551
|
-
tags: Array.isArray(data.tags) ? data.tags : null,
|
|
552
|
-
context: typeof data.context === "string" ? data.context : null,
|
|
553
|
-
shouldStrengthen: typeof data.should_strengthen === "boolean" ? data.should_strengthen : false,
|
|
554
|
-
suggestedConnections: Array.isArray(data.suggested_connections) ? data.suggested_connections.map(String) : [],
|
|
555
|
-
tagsToUpdate: Array.isArray(data.tags_to_update) ? data.tags_to_update.map(String) : []
|
|
556
|
-
};
|
|
557
|
-
} catch (e) {
|
|
558
|
-
console.error(`[amem] Evolution parse failed: ${e.message}`);
|
|
559
|
-
return { tags: null, context: null, shouldStrengthen: false, suggestedConnections: [], tagsToUpdate: [] };
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
var import_sdk, client, MODEL, VALID_CONFIDENCE, VALID_CATEGORIES, VALID_EVOLUTION_TYPES;
|
|
563
|
-
var init_llm = __esm({
|
|
564
|
-
"../amem-core/src/llm.ts"() {
|
|
565
|
-
"use strict";
|
|
566
|
-
import_sdk = __toESM(require("@anthropic-ai/sdk"), 1);
|
|
567
|
-
init_prompts();
|
|
568
|
-
client = new import_sdk.default({
|
|
569
|
-
...process.env.AMEM_LLM_API_KEY && { apiKey: process.env.AMEM_LLM_API_KEY },
|
|
570
|
-
...process.env.AMEM_LLM_BASE_URL && { baseURL: process.env.AMEM_LLM_BASE_URL }
|
|
571
|
-
});
|
|
572
|
-
MODEL = process.env.AMEM_LLM_MODEL ?? "claude-sonnet-4-6";
|
|
573
|
-
VALID_CONFIDENCE = /* @__PURE__ */ new Set(["high", "medium", "low"]);
|
|
574
|
-
VALID_CATEGORIES = /* @__PURE__ */ new Set([
|
|
575
|
-
"Technical",
|
|
576
|
-
"Business",
|
|
577
|
-
"Personal",
|
|
578
|
-
"Project",
|
|
579
|
-
"Research",
|
|
580
|
-
"System",
|
|
581
|
-
"General"
|
|
582
|
-
]);
|
|
583
|
-
VALID_EVOLUTION_TYPES = /* @__PURE__ */ new Set(["EVOLVE", "CONFLICT", "EXPAND", "NEW"]);
|
|
584
|
-
}
|
|
585
|
-
});
|
|
586
|
-
|
|
587
125
|
// ../amem-core/src/storage.ts
|
|
588
126
|
async function qdrant(method, path6, body) {
|
|
589
127
|
const res = await fetch(`${QDRANT_URL}${path6}`, {
|
|
@@ -601,10 +139,6 @@ async function pingQdrant() {
|
|
|
601
139
|
const res = await fetch(`${QDRANT_URL}/readyz`);
|
|
602
140
|
if (!res.ok) throw new Error(`Qdrant GET /readyz failed: ${res.status}`);
|
|
603
141
|
}
|
|
604
|
-
function resetCollectionReady() {
|
|
605
|
-
_collectionReady = false;
|
|
606
|
-
_collectionReadyMap.clear();
|
|
607
|
-
}
|
|
608
142
|
async function ensureCollection(collectionName) {
|
|
609
143
|
const col = collectionName || getCollection();
|
|
610
144
|
if (collectionName) {
|
|
@@ -942,57 +476,526 @@ function makeCrud(collectionName, modeBIsolated = false) {
|
|
|
942
476
|
function createStorageContext(collectionName, modeBIsolated = false) {
|
|
943
477
|
return makeCrud(collectionName || getCollection(), modeBIsolated);
|
|
944
478
|
}
|
|
945
|
-
async function addNote(note) {
|
|
946
|
-
return makeCrud(getCollection()).addNote(note);
|
|
947
|
-
}
|
|
948
479
|
async function getNote(id) {
|
|
949
480
|
return makeCrud(getCollection()).getNote(id);
|
|
950
481
|
}
|
|
951
482
|
async function updateNote(note) {
|
|
952
483
|
return makeCrud(getCollection()).updateNote(note);
|
|
953
484
|
}
|
|
954
|
-
async function
|
|
955
|
-
return makeCrud(getCollection()).
|
|
485
|
+
async function listNotes(agentId) {
|
|
486
|
+
return makeCrud(getCollection()).listNotes(agentId);
|
|
487
|
+
}
|
|
488
|
+
async function deleteNote(id) {
|
|
489
|
+
return makeCrud(getCollection()).deleteNote(id);
|
|
490
|
+
}
|
|
491
|
+
async function invalidateNote(id) {
|
|
492
|
+
return makeCrud(getCollection()).invalidateNote(id);
|
|
493
|
+
}
|
|
494
|
+
async function patchNotePayload(id, fields) {
|
|
495
|
+
return makeCrud(getCollection()).patchNotePayload(id, fields);
|
|
496
|
+
}
|
|
497
|
+
var QDRANT_URL, getCollection, VECTOR_DIM, _collectionReady, _collectionReadyMap;
|
|
498
|
+
var init_storage = __esm({
|
|
499
|
+
"../amem-core/src/storage.ts"() {
|
|
500
|
+
"use strict";
|
|
501
|
+
QDRANT_URL = "http://localhost:6333";
|
|
502
|
+
getCollection = () => process.env.AMEM_COLLECTION || "amem_notes";
|
|
503
|
+
VECTOR_DIM = 384;
|
|
504
|
+
_collectionReady = false;
|
|
505
|
+
_collectionReadyMap = /* @__PURE__ */ new Map();
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// ../amem-core/src/prompts.ts
|
|
510
|
+
var LOCALE, en, zh, templates, t;
|
|
511
|
+
var init_prompts = __esm({
|
|
512
|
+
"../amem-core/src/prompts.ts"() {
|
|
513
|
+
"use strict";
|
|
514
|
+
LOCALE = process.env.AMEM_PROMPT_LOCALE === "zh" ? "zh" : "en";
|
|
515
|
+
en = {
|
|
516
|
+
crudDecision: (userText, assistantText, memoryList) => `You are a memory management agent. Analyze the conversation and decide what memory operations are needed.
|
|
517
|
+
|
|
518
|
+
## Conversation
|
|
519
|
+
|
|
520
|
+
User: ${userText}
|
|
521
|
+
Assistant: ${assistantText}
|
|
522
|
+
|
|
523
|
+
## Existing relevant memories (identified by integer idx)
|
|
524
|
+
|
|
525
|
+
${memoryList}
|
|
526
|
+
|
|
527
|
+
## Task
|
|
528
|
+
|
|
529
|
+
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.
|
|
530
|
+
|
|
531
|
+
## Operation types
|
|
532
|
+
- NEW: Extract a brand new fact not present in existing memories
|
|
533
|
+
- UPDATE: New information refines or supersedes an existing memory; specify existingIdx
|
|
534
|
+
- DELETE: An existing memory is outdated, contradicted, or wrong; specify existingIdx, fact = original content
|
|
535
|
+
- NONE: Nothing worth recording, or information already fully captured
|
|
536
|
+
|
|
537
|
+
## Output format
|
|
538
|
+
|
|
539
|
+
Return a JSON array. Each item:
|
|
540
|
+
{"action": "NEW"|"UPDATE"|"DELETE"|"NONE", "fact": "fact content", "existingIdx": integer or omit, "reason": "optional"}
|
|
541
|
+
|
|
542
|
+
Return at most 3 operations. If nothing is worth recording, return [].
|
|
543
|
+
Return only the JSON array, no other text.
|
|
544
|
+
|
|
545
|
+
Examples:
|
|
546
|
+
|
|
547
|
+
1. New preference:
|
|
548
|
+
[{"action": "NEW", "fact": "User prefers TypeScript over JavaScript", "reason": "Explicitly stated tech preference"}]
|
|
549
|
+
|
|
550
|
+
2. Updating an existing memory (idx 0 was "User is evaluating React and Vue"):
|
|
551
|
+
[{"action": "UPDATE", "fact": "User decided to use React (dropped Vue)", "existingIdx": 0, "reason": "Decision finalized, update evaluation status"}]
|
|
552
|
+
|
|
553
|
+
3. Conversation is just "Sure, thanks" / "Got it" with no new info:
|
|
554
|
+
[]`,
|
|
555
|
+
shouldMerge: (contentA, contentB) => `You are a memory deduplication assistant. Determine whether two memories express essentially the same information.
|
|
556
|
+
|
|
557
|
+
Memory A: ${contentA}
|
|
558
|
+
Memory B: ${contentB}
|
|
559
|
+
|
|
560
|
+
Rules:
|
|
561
|
+
- If both memories express the same core fact (possibly different wording or granularity), return:
|
|
562
|
+
{"shouldMerge": true, "merged": "Concise merged statement preserving key details from both, more complete than either alone"}
|
|
563
|
+
- If the memories are complementary, on different topics, or contain different specific facts, return:
|
|
564
|
+
{"shouldMerge": false}
|
|
565
|
+
|
|
566
|
+
Return only JSON, no other text.
|
|
567
|
+
|
|
568
|
+
Examples:
|
|
569
|
+
|
|
570
|
+
1. Should merge (different granularity):
|
|
571
|
+
A: "Project uses PostgreSQL"
|
|
572
|
+
B: "Project's primary database is PostgreSQL 16, deployed on AWS RDS"
|
|
573
|
+
-> {"shouldMerge": true, "merged": "Project uses PostgreSQL 16 as primary database, deployed on AWS RDS"}
|
|
574
|
+
|
|
575
|
+
2. Should NOT merge (complementary but distinct):
|
|
576
|
+
A: "User prefers VS Code"
|
|
577
|
+
B: "User's VS Code uses One Dark Pro theme"
|
|
578
|
+
-> {"shouldMerge": false}`,
|
|
579
|
+
evolutionJudge: (oldContent, newContent) => `You are a memory evolution judge. Analyze the relationship between an old and new memory and return JSON.
|
|
580
|
+
|
|
581
|
+
Old memory: ${oldContent}
|
|
582
|
+
New memory: ${newContent}
|
|
583
|
+
|
|
584
|
+
Classification rules:
|
|
585
|
+
|
|
586
|
+
- EVOLVE: New content deepens or updates the old memory (e.g. "Considering Next.js" -> "Decided on Next.js 14 App Router")
|
|
587
|
+
Return: {"type": "EVOLVE", "mergedContent": "Merged content preserving the evolution trajectory"}
|
|
588
|
+
|
|
589
|
+
- CONFLICT: Old and new information directly contradict each other on the same attribute (e.g. "Uses MySQL as primary DB" vs "Migrated to PostgreSQL")
|
|
590
|
+
Return: {"type": "CONFLICT"}
|
|
591
|
+
|
|
592
|
+
- EXPAND: New information supplements the old memory on the same topic (e.g. "Handles backend dev" + "Backend uses Go and gRPC")
|
|
593
|
+
Return: {"type": "EXPAND", "mergedContent": "Merged content integrating both pieces of information"}
|
|
594
|
+
|
|
595
|
+
- NEW: Completely unrelated information, no substantive connection to the old memory
|
|
596
|
+
Return: {"type": "NEW"}
|
|
597
|
+
|
|
598
|
+
Return only JSON, no other text.`
|
|
599
|
+
};
|
|
600
|
+
zh = {
|
|
601
|
+
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
|
|
602
|
+
|
|
603
|
+
## \u5BF9\u8BDD\u5185\u5BB9
|
|
604
|
+
|
|
605
|
+
\u7528\u6237\uFF1A${userText}
|
|
606
|
+
\u52A9\u624B\uFF1A${assistantText}
|
|
607
|
+
|
|
608
|
+
## \u5DF2\u6709\u76F8\u5173\u8BB0\u5FC6\uFF08\u7528\u6574\u6570 idx \u6807\u8BC6\uFF09
|
|
609
|
+
|
|
610
|
+
${memoryList}
|
|
611
|
+
|
|
612
|
+
## \u4EFB\u52A1
|
|
613
|
+
|
|
614
|
+
\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
|
|
615
|
+
|
|
616
|
+
## \u64CD\u4F5C\u7C7B\u578B
|
|
617
|
+
- NEW\uFF1A\u63D0\u53D6\u5168\u65B0\u4E8B\u5B9E\uFF08\u5DF2\u6709\u8BB0\u5FC6\u4E2D\u6CA1\u6709\u7684\u4FE1\u606F\uFF09
|
|
618
|
+
- 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
|
|
619
|
+
- 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
|
|
620
|
+
- NONE\uFF1A\u4E0D\u503C\u5F97\u8BB0\u5F55\u6216\u5DF2\u6709\u5B8C\u5168\u76F8\u540C\u7684\u4FE1\u606F
|
|
621
|
+
|
|
622
|
+
## \u8F93\u51FA\u683C\u5F0F
|
|
623
|
+
|
|
624
|
+
\u8FD4\u56DE JSON \u6570\u7EC4\uFF0C\u6BCF\u6761\u683C\u5F0F\uFF1A
|
|
625
|
+
{"action": "NEW"|"UPDATE"|"DELETE"|"NONE", "fact": "\u4E8B\u5B9E\u5185\u5BB9", "existingIdx": \u6574\u6570\u6216\u7701\u7565, "reason": "\u539F\u56E0\uFF08\u53EF\u9009\uFF09"}
|
|
626
|
+
|
|
627
|
+
\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
|
|
628
|
+
\u53EA\u8FD4\u56DE JSON \u6570\u7EC4\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3002
|
|
629
|
+
|
|
630
|
+
\u793A\u4F8B\uFF1A
|
|
631
|
+
|
|
632
|
+
1. \u63D0\u53D6\u65B0\u504F\u597D\uFF1A
|
|
633
|
+
[{"action": "NEW", "fact": "\u7528\u6237\u504F\u597D TypeScript \u800C\u975E JavaScript", "reason": "\u660E\u786E\u8868\u8FBE\u7684\u6280\u672F\u504F\u597D"}]
|
|
634
|
+
|
|
635
|
+
2. \u66F4\u65B0\u5DF2\u6709\u8BB0\u5FC6\uFF08idx 0 \u539F\u4E3A"\u7528\u6237\u6B63\u5728\u8BC4\u4F30 React \u548C Vue"\uFF09\uFF1A
|
|
636
|
+
[{"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"}]
|
|
637
|
+
|
|
638
|
+
3. \u5BF9\u8BDD\u4EC5\u4E3A"\u597D\u7684\uFF0C\u8C22\u8C22"/"\u6CA1\u95EE\u9898"\u7B49\u786E\u8BA4\u8BED\uFF0C\u65E0\u65B0\u4FE1\u606F\uFF1A
|
|
639
|
+
[]`,
|
|
640
|
+
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
|
|
641
|
+
|
|
642
|
+
\u8BB0\u5FC6A\uFF1A${contentA}
|
|
643
|
+
\u8BB0\u5FC6B\uFF1A${contentB}
|
|
644
|
+
|
|
645
|
+
\u5224\u65AD\u89C4\u5219\uFF1A
|
|
646
|
+
- \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
|
|
647
|
+
{"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"}
|
|
648
|
+
- \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
|
|
649
|
+
{"shouldMerge": false}
|
|
650
|
+
|
|
651
|
+
\u53EA\u8FD4\u56DE JSON\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3002
|
|
652
|
+
|
|
653
|
+
\u793A\u4F8B\uFF1A
|
|
654
|
+
|
|
655
|
+
1. \u5E94\u5408\u5E76\uFF08\u7C92\u5EA6\u4E0D\u540C\uFF09\uFF1A
|
|
656
|
+
A: "\u9879\u76EE\u4F7F\u7528 PostgreSQL \u6570\u636E\u5E93"
|
|
657
|
+
B: "\u9879\u76EE\u7684\u4E3B\u6570\u636E\u5E93\u662F PostgreSQL 16\uFF0C\u90E8\u7F72\u5728 AWS RDS \u4E0A"
|
|
658
|
+
\u2192 {"shouldMerge": true, "merged": "\u9879\u76EE\u4F7F\u7528 PostgreSQL 16 \u4F5C\u4E3A\u4E3B\u6570\u636E\u5E93\uFF0C\u90E8\u7F72\u5728 AWS RDS \u4E0A"}
|
|
659
|
+
|
|
660
|
+
2. \u4E0D\u5E94\u5408\u5E76\uFF08\u4E92\u8865\u4F46\u4E0D\u540C\uFF09\uFF1A
|
|
661
|
+
A: "\u7528\u6237\u559C\u6B22\u7528 VS Code"
|
|
662
|
+
B: "\u7528\u6237\u7684 VS Code \u4F7F\u7528 One Dark Pro \u4E3B\u9898"
|
|
663
|
+
\u2192 {"shouldMerge": false}`,
|
|
664
|
+
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
|
|
665
|
+
|
|
666
|
+
\u65E7\u8BB0\u5FC6\uFF1A${oldContent}
|
|
667
|
+
\u65B0\u8BB0\u5FC6\uFF1A${newContent}
|
|
668
|
+
|
|
669
|
+
\u5224\u65AD\u89C4\u5219\uFF1A
|
|
670
|
+
|
|
671
|
+
- 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
|
|
672
|
+
\u8FD4\u56DE\uFF1A{"type": "EVOLVE", "mergedContent": "\u878D\u5408\u540E\u7684\u5B8C\u6574\u5185\u5BB9\uFF0C\u4FDD\u7559\u6F14\u5316\u8F68\u8FF9"}
|
|
673
|
+
|
|
674
|
+
- 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
|
|
675
|
+
\u8FD4\u56DE\uFF1A{"type": "CONFLICT"}
|
|
676
|
+
|
|
677
|
+
- 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
|
|
678
|
+
\u8FD4\u56DE\uFF1A{"type": "EXPAND", "mergedContent": "\u5408\u5E76\u540E\u7684\u5B8C\u6574\u5185\u5BB9\uFF0C\u6574\u5408\u53CC\u65B9\u4FE1\u606F"}
|
|
679
|
+
|
|
680
|
+
- 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
|
|
681
|
+
\u8FD4\u56DE\uFF1A{"type": "NEW"}
|
|
682
|
+
|
|
683
|
+
\u53EA\u8FD4\u56DE JSON\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3002`
|
|
684
|
+
};
|
|
685
|
+
templates = { en, zh };
|
|
686
|
+
t = templates[LOCALE];
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
// ../amem-core/src/llm.ts
|
|
691
|
+
function anthropic() {
|
|
692
|
+
return _anthropic ??= new import_sdk.default({
|
|
693
|
+
...process.env.AMEM_LLM_API_KEY && { apiKey: process.env.AMEM_LLM_API_KEY },
|
|
694
|
+
...process.env.AMEM_LLM_BASE_URL && { baseURL: process.env.AMEM_LLM_BASE_URL }
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
function openai() {
|
|
698
|
+
return _openai ??= new import_openai.default({
|
|
699
|
+
// AMEM_LLM_API_KEY first (engine convention), then the SDK's own
|
|
700
|
+
// OPENAI_API_KEY (the standard) — passing an explicit key blocks the SDK's
|
|
701
|
+
// env fallback, so read it here. Placeholder last, so keyless local servers
|
|
702
|
+
// (Ollama, vLLM) still work.
|
|
703
|
+
apiKey: process.env.AMEM_LLM_API_KEY || process.env.OPENAI_API_KEY || "sk-no-key-required",
|
|
704
|
+
...process.env.AMEM_LLM_BASE_URL && { baseURL: process.env.AMEM_LLM_BASE_URL }
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
async function llmCall(prompt, maxTokens = 500) {
|
|
708
|
+
const isThinking = MODEL.includes("gemini") || MODEL.includes("pro-agent");
|
|
709
|
+
const effectiveMaxTokens = isThinking ? Math.max(maxTokens * 8, 4e3) : maxTokens;
|
|
710
|
+
try {
|
|
711
|
+
return PROVIDER === "openai" ? await openaiCall(prompt, effectiveMaxTokens) : await anthropicCall(prompt, effectiveMaxTokens);
|
|
712
|
+
} catch (e) {
|
|
713
|
+
console.error(`[amem] LLM call failed: ${e.message}`);
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
async function anthropicCall(prompt, maxTokens) {
|
|
718
|
+
const resp = await anthropic().messages.create({
|
|
719
|
+
model: MODEL,
|
|
720
|
+
max_tokens: maxTokens,
|
|
721
|
+
messages: [{ role: "user", content: prompt }]
|
|
722
|
+
});
|
|
723
|
+
for (const block of resp.content) {
|
|
724
|
+
if (block.type === "text") return block.text.trim();
|
|
725
|
+
}
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
async function openaiCall(prompt, maxTokens) {
|
|
729
|
+
const isReasoning = /^o\d/.test(MODEL) || MODEL.startsWith("gpt-5");
|
|
730
|
+
const resp = await openai().chat.completions.create({
|
|
731
|
+
model: MODEL,
|
|
732
|
+
...isReasoning ? { max_completion_tokens: maxTokens } : { max_tokens: maxTokens },
|
|
733
|
+
messages: [{ role: "user", content: prompt }]
|
|
734
|
+
});
|
|
735
|
+
return resp.choices[0]?.message?.content?.trim() ?? null;
|
|
736
|
+
}
|
|
737
|
+
function stripFences(raw) {
|
|
738
|
+
raw = raw.trim();
|
|
739
|
+
if (raw.startsWith("```")) {
|
|
740
|
+
const lines = raw.split("\n");
|
|
741
|
+
lines.shift();
|
|
742
|
+
if (lines[lines.length - 1] === "```") lines.pop();
|
|
743
|
+
raw = lines.join("\n").trim();
|
|
744
|
+
}
|
|
745
|
+
if (raw.startsWith('"') && raw.endsWith('"') || raw.startsWith("'") && raw.endsWith("'")) {
|
|
746
|
+
try {
|
|
747
|
+
raw = JSON.parse(raw);
|
|
748
|
+
} catch {
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return raw;
|
|
752
|
+
}
|
|
753
|
+
async function llmConstructNote(content) {
|
|
754
|
+
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:
|
|
755
|
+
{
|
|
756
|
+
"keywords": ["keyword1", "keyword2"],
|
|
757
|
+
"tags": ["tag1", "tag2"],
|
|
758
|
+
"context": "one sentence summary in the same language as the input",
|
|
759
|
+
"category": "Technical|Business|Personal|Project|Research|System|General",
|
|
760
|
+
"note_type": "memory|knowledge",
|
|
761
|
+
"topics": ["Topic1", "Topic2"],
|
|
762
|
+
"confidence": "high|medium|low"
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
Category guide:
|
|
766
|
+
- Technical: code, tools, configuration, APIs, debugging
|
|
767
|
+
- Business: company, finance, compliance, contracts, invoices
|
|
768
|
+
- Personal: personal state, habits, preferences, emotions
|
|
769
|
+
- Project: project progress, decisions, milestones
|
|
770
|
+
- Research: research, literature, evaluation, comparison
|
|
771
|
+
- System: system services, monitoring, operations
|
|
772
|
+
- General: anything that does not fit the above
|
|
773
|
+
|
|
774
|
+
note_type guide:
|
|
775
|
+
- knowledge: books, methodologies, tools, domain knowledge, reference material \u2014 durable, no strong time component
|
|
776
|
+
- memory: events, decisions, preferences, states, observations \u2014 episodic, time-sensitive
|
|
777
|
+
|
|
778
|
+
topics guide (Story 26B):
|
|
779
|
+
- Only populate for knowledge notes (note_type=knowledge). For memory notes, return [].
|
|
780
|
+
- List 1-5 concise subject tags representing the main topics of this knowledge, e.g. ["TypeScript", "Qdrant", "Vector DB"].
|
|
781
|
+
|
|
782
|
+
confidence guide (Story 27):
|
|
783
|
+
- high: note_type is unambiguous \u2014 clearly episodic (event/decision/state) or clearly durable knowledge (tool doc/methodology)
|
|
784
|
+
- medium: some ambiguity \u2014 e.g. "learned X method" could be either memory or knowledge
|
|
785
|
+
- low: LLM is uncertain \u2014 vague, fragmentary, or mixed content
|
|
786
|
+
|
|
787
|
+
Text: ${content}`;
|
|
788
|
+
const raw = await llmCall(prompt, 400);
|
|
789
|
+
if (!raw)
|
|
790
|
+
return {
|
|
791
|
+
keywords: [],
|
|
792
|
+
tags: [],
|
|
793
|
+
context: "",
|
|
794
|
+
category: "General",
|
|
795
|
+
note_type: "memory",
|
|
796
|
+
topics: [],
|
|
797
|
+
confidence: "medium"
|
|
798
|
+
};
|
|
799
|
+
try {
|
|
800
|
+
const data = JSON.parse(stripFences(raw));
|
|
801
|
+
const rawCategory = typeof data.category === "string" ? data.category : "General";
|
|
802
|
+
const category = VALID_CATEGORIES.has(rawCategory) ? rawCategory : "General";
|
|
803
|
+
const note_type = data.note_type === "knowledge" ? "knowledge" : "memory";
|
|
804
|
+
const topics = note_type === "knowledge" && Array.isArray(data.topics) ? data.topics.filter((v) => typeof v === "string") : [];
|
|
805
|
+
const rawConfidence = typeof data.confidence === "string" ? data.confidence : "medium";
|
|
806
|
+
const confidence = VALID_CONFIDENCE.has(rawConfidence) ? rawConfidence : "medium";
|
|
807
|
+
return {
|
|
808
|
+
keywords: Array.isArray(data.keywords) ? data.keywords : [],
|
|
809
|
+
tags: Array.isArray(data.tags) ? data.tags : [],
|
|
810
|
+
context: typeof data.context === "string" ? data.context : "",
|
|
811
|
+
category,
|
|
812
|
+
note_type,
|
|
813
|
+
topics,
|
|
814
|
+
confidence
|
|
815
|
+
};
|
|
816
|
+
} catch (e) {
|
|
817
|
+
console.error(`[amem] Note construction parse failed: ${e.message}`);
|
|
818
|
+
return {
|
|
819
|
+
keywords: [],
|
|
820
|
+
tags: [],
|
|
821
|
+
context: "",
|
|
822
|
+
category: "General",
|
|
823
|
+
note_type: "memory",
|
|
824
|
+
topics: [],
|
|
825
|
+
confidence: "medium"
|
|
826
|
+
};
|
|
827
|
+
}
|
|
956
828
|
}
|
|
957
|
-
async function
|
|
958
|
-
|
|
829
|
+
async function llmShouldLink(noteContent, candidateContent) {
|
|
830
|
+
const prompt = `Do these two memory notes have a meaningful relationship that would be useful to link?
|
|
831
|
+
Reply with only "yes" or "no".
|
|
832
|
+
|
|
833
|
+
Note A: ${noteContent}
|
|
834
|
+
Note B: ${candidateContent}`;
|
|
835
|
+
const raw = await llmCall(prompt, 10);
|
|
836
|
+
if (!raw) return false;
|
|
837
|
+
return raw.toLowerCase().startsWith("yes");
|
|
959
838
|
}
|
|
960
|
-
async function
|
|
961
|
-
|
|
839
|
+
async function llmCrudDecision(userText, assistantText, existingMemories) {
|
|
840
|
+
const memoryList = existingMemories.length > 0 ? existingMemories.map((m) => `[${m.idx}] ${m.content}`).join("\n") : "(none)";
|
|
841
|
+
const prompt = t.crudDecision(userText.slice(0, 500), assistantText.slice(0, 500), memoryList);
|
|
842
|
+
try {
|
|
843
|
+
const raw = await llmCall(prompt, 400);
|
|
844
|
+
if (!raw) return [];
|
|
845
|
+
const match = raw.match(/\[.*\]/s);
|
|
846
|
+
if (!match) return [];
|
|
847
|
+
const parsed = JSON.parse(match[0]);
|
|
848
|
+
if (!Array.isArray(parsed)) return [];
|
|
849
|
+
const ops = [];
|
|
850
|
+
for (const item of parsed) {
|
|
851
|
+
if (!item || typeof item !== "object") continue;
|
|
852
|
+
const action = item.action;
|
|
853
|
+
if (!["NEW", "UPDATE", "DELETE", "NONE"].includes(action)) continue;
|
|
854
|
+
if (action === "NONE") continue;
|
|
855
|
+
const op = {
|
|
856
|
+
action,
|
|
857
|
+
fact: typeof item.fact === "string" ? item.fact : "",
|
|
858
|
+
reason: typeof item.reason === "string" ? item.reason : void 0
|
|
859
|
+
};
|
|
860
|
+
if (typeof item.existingIdx === "number") {
|
|
861
|
+
op.existingIdx = item.existingIdx;
|
|
862
|
+
}
|
|
863
|
+
ops.push(op);
|
|
864
|
+
}
|
|
865
|
+
return ops.slice(0, 3);
|
|
866
|
+
} catch (e) {
|
|
867
|
+
console.error(`[amem] llmCrudDecision failed: ${e.message}`);
|
|
868
|
+
return [];
|
|
869
|
+
}
|
|
962
870
|
}
|
|
963
|
-
async function
|
|
964
|
-
|
|
871
|
+
async function llmShouldMerge(contentA, contentB) {
|
|
872
|
+
const prompt = t.shouldMerge(contentA, contentB);
|
|
873
|
+
const raw = await llmCall(prompt, 300);
|
|
874
|
+
if (!raw) return { shouldMerge: false };
|
|
875
|
+
try {
|
|
876
|
+
const data = JSON.parse(stripFences(raw));
|
|
877
|
+
if (typeof data.shouldMerge !== "boolean") return { shouldMerge: false };
|
|
878
|
+
if (data.shouldMerge && typeof data.merged === "string") {
|
|
879
|
+
return { shouldMerge: true, merged: data.merged };
|
|
880
|
+
}
|
|
881
|
+
return { shouldMerge: false };
|
|
882
|
+
} catch (e) {
|
|
883
|
+
console.error(`[amem] llmShouldMerge parse failed: ${e.message}`);
|
|
884
|
+
return { shouldMerge: false };
|
|
885
|
+
}
|
|
965
886
|
}
|
|
966
|
-
async function
|
|
967
|
-
|
|
887
|
+
async function llmEvolutionJudge(oldContent, newContent) {
|
|
888
|
+
const prompt = t.evolutionJudge(oldContent, newContent);
|
|
889
|
+
const raw = await llmCall(prompt, 300);
|
|
890
|
+
if (!raw) return { type: "NEW" };
|
|
891
|
+
try {
|
|
892
|
+
const data = JSON.parse(stripFences(raw));
|
|
893
|
+
const type = VALID_EVOLUTION_TYPES.has(data.type) ? data.type : "NEW";
|
|
894
|
+
return {
|
|
895
|
+
type,
|
|
896
|
+
mergedContent: typeof data.mergedContent === "string" ? data.mergedContent : void 0
|
|
897
|
+
};
|
|
898
|
+
} catch (e) {
|
|
899
|
+
console.error(`[amem] llmEvolutionJudge parse failed: ${e.message}`);
|
|
900
|
+
return { type: "NEW" };
|
|
901
|
+
}
|
|
968
902
|
}
|
|
969
|
-
async function
|
|
970
|
-
|
|
903
|
+
async function llmEvolveNote(content, linkedNotes) {
|
|
904
|
+
const linkedStr = linkedNotes.map((n) => `- ID: ${n.id}
|
|
905
|
+
Content: ${n.content}`).join("\n");
|
|
906
|
+
const prompt = `A memory note has gained new connections. Update its context, tags, and decide whether to strengthen connections with specific neighbors.
|
|
907
|
+
Reply with JSON only (no markdown):
|
|
908
|
+
{
|
|
909
|
+
"tags": ["tag1", "tag2", ...],
|
|
910
|
+
"context": "updated one sentence summary",
|
|
911
|
+
"should_strengthen": true|false,
|
|
912
|
+
"suggested_connections": ["neighbor_id_1", "neighbor_id_2", ...],
|
|
913
|
+
"tags_to_update": ["tag_1", ..., "tag_n"]
|
|
971
914
|
}
|
|
972
|
-
|
|
973
|
-
|
|
915
|
+
|
|
916
|
+
Guidelines:
|
|
917
|
+
- "tags" and "context" are for updating the original note based on new connections.
|
|
918
|
+
- "should_strengthen" is a decision whether this note should strengthen its connections to any of the newly linked notes (neighbors).
|
|
919
|
+
- "suggested_connections" must contain only IDs from the newly linked notes (neighbors) listed below.
|
|
920
|
+
- "tags_to_update" are updated tags for the original note itself if we strengthen connections.
|
|
921
|
+
|
|
922
|
+
Original note content: ${content}
|
|
923
|
+
|
|
924
|
+
Newly linked notes (neighbors):
|
|
925
|
+
${linkedStr}`;
|
|
926
|
+
const raw = await llmCall(prompt, 500);
|
|
927
|
+
if (!raw) return { tags: null, context: null, shouldStrengthen: false, suggestedConnections: [], tagsToUpdate: [] };
|
|
928
|
+
try {
|
|
929
|
+
const data = JSON.parse(stripFences(raw));
|
|
930
|
+
return {
|
|
931
|
+
tags: Array.isArray(data.tags) ? data.tags : null,
|
|
932
|
+
context: typeof data.context === "string" ? data.context : null,
|
|
933
|
+
shouldStrengthen: typeof data.should_strengthen === "boolean" ? data.should_strengthen : false,
|
|
934
|
+
suggestedConnections: Array.isArray(data.suggested_connections) ? data.suggested_connections.map(String) : [],
|
|
935
|
+
tagsToUpdate: Array.isArray(data.tags_to_update) ? data.tags_to_update.map(String) : []
|
|
936
|
+
};
|
|
937
|
+
} catch (e) {
|
|
938
|
+
console.error(`[amem] Evolution parse failed: ${e.message}`);
|
|
939
|
+
return { tags: null, context: null, shouldStrengthen: false, suggestedConnections: [], tagsToUpdate: [] };
|
|
940
|
+
}
|
|
974
941
|
}
|
|
975
|
-
|
|
976
|
-
|
|
942
|
+
var import_sdk, import_openai, PROVIDER, MODEL, _anthropic, _openai, VALID_CONFIDENCE, VALID_CATEGORIES, VALID_EVOLUTION_TYPES;
|
|
943
|
+
var init_llm = __esm({
|
|
944
|
+
"../amem-core/src/llm.ts"() {
|
|
945
|
+
"use strict";
|
|
946
|
+
import_sdk = __toESM(require("@anthropic-ai/sdk"), 1);
|
|
947
|
+
import_openai = __toESM(require("openai"), 1);
|
|
948
|
+
init_prompts();
|
|
949
|
+
PROVIDER = (process.env.AMEM_LLM_PROVIDER ?? "anthropic").trim().toLowerCase();
|
|
950
|
+
if (PROVIDER !== "anthropic" && PROVIDER !== "openai") {
|
|
951
|
+
console.error(`[amem] unknown AMEM_LLM_PROVIDER "${PROVIDER}"; falling back to anthropic`);
|
|
952
|
+
}
|
|
953
|
+
MODEL = process.env.AMEM_LLM_MODEL ?? (PROVIDER === "openai" ? "gpt-4o-mini" : "claude-sonnet-4-6");
|
|
954
|
+
_anthropic = null;
|
|
955
|
+
_openai = null;
|
|
956
|
+
VALID_CONFIDENCE = /* @__PURE__ */ new Set(["high", "medium", "low"]);
|
|
957
|
+
VALID_CATEGORIES = /* @__PURE__ */ new Set([
|
|
958
|
+
"Technical",
|
|
959
|
+
"Business",
|
|
960
|
+
"Personal",
|
|
961
|
+
"Project",
|
|
962
|
+
"Research",
|
|
963
|
+
"System",
|
|
964
|
+
"General"
|
|
965
|
+
]);
|
|
966
|
+
VALID_EVOLUTION_TYPES = /* @__PURE__ */ new Set(["EVOLVE", "CONFLICT", "EXPAND", "NEW"]);
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
// ../amem-core/src/evo-counter.ts
|
|
971
|
+
function counterFile() {
|
|
972
|
+
return process.env.AMEM_EVO_COUNTER_PATH || path2.join(getDataDir(), "amem_evo_cnt.json");
|
|
977
973
|
}
|
|
978
|
-
|
|
979
|
-
|
|
974
|
+
function getEvoCount() {
|
|
975
|
+
try {
|
|
976
|
+
const data = JSON.parse(fs.readFileSync(counterFile(), "utf-8"));
|
|
977
|
+
return data.count || 0;
|
|
978
|
+
} catch {
|
|
979
|
+
return 0;
|
|
980
|
+
}
|
|
980
981
|
}
|
|
981
|
-
|
|
982
|
-
|
|
982
|
+
function incrementEvoCount() {
|
|
983
|
+
const count = getEvoCount() + 1;
|
|
984
|
+
fs.writeFileSync(counterFile(), JSON.stringify({ count, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
985
|
+
return count;
|
|
983
986
|
}
|
|
984
|
-
|
|
985
|
-
|
|
987
|
+
function shouldRunEvolution() {
|
|
988
|
+
const count = incrementEvoCount();
|
|
989
|
+
return count % EVO_THRESHOLD === 0;
|
|
986
990
|
}
|
|
987
|
-
var
|
|
988
|
-
var
|
|
989
|
-
"../amem-core/src/
|
|
991
|
+
var fs, path2, EVO_THRESHOLD;
|
|
992
|
+
var init_evo_counter = __esm({
|
|
993
|
+
"../amem-core/src/evo-counter.ts"() {
|
|
990
994
|
"use strict";
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
_collectionReadyMap = /* @__PURE__ */ new Map();
|
|
995
|
+
fs = __toESM(require("fs"), 1);
|
|
996
|
+
path2 = __toESM(require("path"), 1);
|
|
997
|
+
init_config();
|
|
998
|
+
EVO_THRESHOLD = 20;
|
|
996
999
|
}
|
|
997
1000
|
});
|
|
998
1001
|
|
|
@@ -1790,64 +1793,37 @@ var src_exports = {};
|
|
|
1790
1793
|
__export(src_exports, {
|
|
1791
1794
|
addEpisodic: () => addEpisodic,
|
|
1792
1795
|
addMemory: () => addMemory,
|
|
1793
|
-
addNote: () => addNote,
|
|
1794
|
-
bm25Score: () => bm25Score,
|
|
1795
|
-
buildBM25: () => buildBM25,
|
|
1796
1796
|
checkQuality: () => checkQuality,
|
|
1797
1797
|
configure: () => configure,
|
|
1798
1798
|
consolidateMemories: () => consolidateMemories,
|
|
1799
|
-
cosineSimilarity: () => cosineSimilarity,
|
|
1800
|
-
countNotes: () => countNotes,
|
|
1801
1799
|
createStorageContext: () => createStorageContext,
|
|
1802
1800
|
deleteNote: () => deleteNote,
|
|
1803
1801
|
encode: () => encode,
|
|
1804
1802
|
ensureCollection: () => ensureCollection,
|
|
1805
|
-
findByHash: () => findByHash,
|
|
1806
1803
|
generateReviewBatch: () => generateReviewBatch,
|
|
1807
|
-
getDataDir: () => getDataDir,
|
|
1808
|
-
getEvoCount: () => getEvoCount,
|
|
1809
1804
|
getNote: () => getNote,
|
|
1810
|
-
getNotesByDatePrefix: () => getNotesByDatePrefix,
|
|
1811
|
-
incrementEvoCount: () => incrementEvoCount,
|
|
1812
1805
|
invalidateNote: () => invalidateNote,
|
|
1813
1806
|
isModelLoaded: () => isModelLoaded,
|
|
1814
1807
|
listMemories: () => listMemories,
|
|
1815
1808
|
listNotes: () => listNotes,
|
|
1816
|
-
llmCall: () => llmCall,
|
|
1817
|
-
llmConstructNote: () => llmConstructNote,
|
|
1818
1809
|
llmCrudDecision: () => llmCrudDecision,
|
|
1819
|
-
llmEvolutionJudge: () => llmEvolutionJudge,
|
|
1820
|
-
llmEvolveNote: () => llmEvolveNote,
|
|
1821
|
-
llmShouldLink: () => llmShouldLink,
|
|
1822
|
-
llmShouldMerge: () => llmShouldMerge,
|
|
1823
1810
|
loadModel: () => loadModel,
|
|
1824
1811
|
mergeSimilarNotes: () => mergeSimilarNotes,
|
|
1825
1812
|
patchNotePayload: () => patchNotePayload,
|
|
1826
1813
|
pingQdrant: () => pingQdrant,
|
|
1827
|
-
queryByEmbedding: () => queryByEmbedding,
|
|
1828
|
-
replaceLinkReferences: () => replaceLinkReferences,
|
|
1829
|
-
resetCollectionReady: () => resetCollectionReady,
|
|
1830
|
-
rrfMerge: () => rrfMerge,
|
|
1831
1814
|
scanLowQuality: () => scanLowQuality,
|
|
1832
1815
|
searchMemory: () => searchMemory,
|
|
1833
|
-
|
|
1834
|
-
simpleTokenize: () => simpleTokenize,
|
|
1835
|
-
t: () => t,
|
|
1836
|
-
updateNote: () => updateNote,
|
|
1837
|
-
updateNoteContent: () => updateNoteContent,
|
|
1838
|
-
updateNoteLinks: () => updateNoteLinks
|
|
1816
|
+
updateNote: () => updateNote
|
|
1839
1817
|
});
|
|
1840
1818
|
var init_src = __esm({
|
|
1841
1819
|
"../amem-core/src/index.ts"() {
|
|
1842
1820
|
"use strict";
|
|
1843
1821
|
init_config();
|
|
1844
1822
|
init_embedding();
|
|
1845
|
-
init_evo_counter();
|
|
1846
|
-
init_llm();
|
|
1847
1823
|
init_memory();
|
|
1848
|
-
init_prompts();
|
|
1849
1824
|
init_quality();
|
|
1850
1825
|
init_storage();
|
|
1826
|
+
init_llm();
|
|
1851
1827
|
}
|
|
1852
1828
|
});
|
|
1853
1829
|
|
|
@@ -1907,6 +1883,28 @@ function hookNeverFiredWarning(now = Date.now()) {
|
|
|
1907
1883
|
return HOOK_WARNING_TEXT;
|
|
1908
1884
|
}
|
|
1909
1885
|
|
|
1886
|
+
// src/scope.ts
|
|
1887
|
+
function parseAgentIdFromSessionKey(sessionKey) {
|
|
1888
|
+
if (!sessionKey) return void 0;
|
|
1889
|
+
const parts = sessionKey.split(":").filter(Boolean);
|
|
1890
|
+
if (parts.length >= 3 && parts[0] === "agent") return parts[1] || void 0;
|
|
1891
|
+
return void 0;
|
|
1892
|
+
}
|
|
1893
|
+
function resolveAgentId(ctx, pluginConfig) {
|
|
1894
|
+
return ctx?.agentId ?? parseAgentIdFromSessionKey(ctx?.sessionKey) ?? pluginConfig.agentId ?? "main";
|
|
1895
|
+
}
|
|
1896
|
+
function buildScope(rawAgentId, pluginConfig, createStorageContext2) {
|
|
1897
|
+
const agentCfg = pluginConfig.agents?.[rawAgentId] ?? {};
|
|
1898
|
+
const effectiveAgentId = agentCfg.agentId ?? rawAgentId;
|
|
1899
|
+
const effectiveCollection = agentCfg.collection ?? pluginConfig.collection ?? void 0;
|
|
1900
|
+
const modeBIsolated = !!agentCfg.collection;
|
|
1901
|
+
return {
|
|
1902
|
+
agentId: effectiveAgentId,
|
|
1903
|
+
collection: effectiveCollection,
|
|
1904
|
+
storageCtx: createStorageContext2(effectiveCollection, modeBIsolated)
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1910
1908
|
// src/index.ts
|
|
1911
1909
|
init_src();
|
|
1912
1910
|
var _config = {};
|
|
@@ -1916,27 +1914,9 @@ function register(api) {
|
|
|
1916
1914
|
_config = api.pluginConfig || {};
|
|
1917
1915
|
const pluginConfig = _config;
|
|
1918
1916
|
configure({ dataDir: path5.join(os2.homedir(), ".openclaw") });
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
if (parts.length >= 3 && parts[0] === "agent") return parts[1] || void 0;
|
|
1923
|
-
return void 0;
|
|
1924
|
-
}
|
|
1925
|
-
function resolveAgentId(ctx) {
|
|
1926
|
-
return ctx?.agentId ?? parseAgentIdFromSessionKey(ctx?.sessionKey) ?? pluginConfig.agentId ?? "main";
|
|
1927
|
-
}
|
|
1928
|
-
function buildScope(rawAgentId) {
|
|
1929
|
-
const agentCfg = pluginConfig.agents?.[rawAgentId] ?? {};
|
|
1930
|
-
const effectiveAgentId = agentCfg.agentId ?? rawAgentId;
|
|
1931
|
-
const effectiveCollection = agentCfg.collection ?? pluginConfig.collection ?? void 0;
|
|
1932
|
-
const modeBIsolated = !!agentCfg.collection;
|
|
1933
|
-
return {
|
|
1934
|
-
agentId: effectiveAgentId,
|
|
1935
|
-
collection: effectiveCollection,
|
|
1936
|
-
storageCtx: createStorageContext(effectiveCollection, modeBIsolated)
|
|
1937
|
-
};
|
|
1938
|
-
}
|
|
1939
|
-
const defaultScope = buildScope(resolveAgentId());
|
|
1917
|
+
const resolveAgentId2 = (ctx) => resolveAgentId(ctx, pluginConfig);
|
|
1918
|
+
const buildScope2 = (rawAgentId) => buildScope(rawAgentId, pluginConfig, createStorageContext);
|
|
1919
|
+
const defaultScope = buildScope2(resolveAgentId2());
|
|
1940
1920
|
const dbPath = path5.join(os2.homedir(), ".openclaw", "amem_db");
|
|
1941
1921
|
logger.info(
|
|
1942
1922
|
`openclaw-amem: registered (native TS, Qdrant, default agent_id=${defaultScope.agentId}, default collection=${pluginConfig.collection ?? "amem_notes (default)"}, per-agent scope resolved per call)`
|
|
@@ -1957,7 +1937,7 @@ function register(api) {
|
|
|
1957
1937
|
runtime: {
|
|
1958
1938
|
async getMemorySearchManager(params) {
|
|
1959
1939
|
try {
|
|
1960
|
-
const scope =
|
|
1940
|
+
const scope = buildScope2(resolveAgentId2(params));
|
|
1961
1941
|
return {
|
|
1962
1942
|
manager: {
|
|
1963
1943
|
status() {
|
|
@@ -2007,7 +1987,7 @@ function register(api) {
|
|
|
2007
1987
|
}
|
|
2008
1988
|
},
|
|
2009
1989
|
resolveMemoryBackendConfig(params) {
|
|
2010
|
-
const scope =
|
|
1990
|
+
const scope = buildScope2(resolveAgentId2(params));
|
|
2011
1991
|
return { backend: "amem-qdrant", baseUrl: "", userId: scope.agentId };
|
|
2012
1992
|
},
|
|
2013
1993
|
async closeAllMemorySearchManagers() {
|
|
@@ -2020,7 +2000,7 @@ function register(api) {
|
|
|
2020
2000
|
if (typeof api.registerTool === "function") {
|
|
2021
2001
|
api.registerTool(
|
|
2022
2002
|
(ctx) => {
|
|
2023
|
-
const scope =
|
|
2003
|
+
const scope = buildScope2(resolveAgentId2(ctx));
|
|
2024
2004
|
return {
|
|
2025
2005
|
name: "memory_search",
|
|
2026
2006
|
label: "Memory Search (A-MEM)",
|
|
@@ -2077,7 +2057,7 @@ ${text}${hookWarning}` }],
|
|
|
2077
2057
|
);
|
|
2078
2058
|
api.registerTool(
|
|
2079
2059
|
(ctx) => {
|
|
2080
|
-
const scope =
|
|
2060
|
+
const scope = buildScope2(resolveAgentId2(ctx));
|
|
2081
2061
|
return {
|
|
2082
2062
|
name: "memory_add",
|
|
2083
2063
|
label: "Memory Add (A-MEM)",
|
|
@@ -2113,7 +2093,7 @@ ${text}${hookWarning}` }],
|
|
|
2113
2093
|
);
|
|
2114
2094
|
api.registerTool(
|
|
2115
2095
|
(ctx) => {
|
|
2116
|
-
const scope =
|
|
2096
|
+
const scope = buildScope2(resolveAgentId2(ctx));
|
|
2117
2097
|
return {
|
|
2118
2098
|
name: "memory_list",
|
|
2119
2099
|
label: "Memory List (A-MEM)",
|
|
@@ -2143,7 +2123,7 @@ ${text}${hookWarning}` }],
|
|
|
2143
2123
|
);
|
|
2144
2124
|
api.registerTool(
|
|
2145
2125
|
(ctx) => {
|
|
2146
|
-
const scope =
|
|
2126
|
+
const scope = buildScope2(resolveAgentId2(ctx));
|
|
2147
2127
|
return {
|
|
2148
2128
|
name: "memory_consolidate",
|
|
2149
2129
|
label: "Memory Consolidate (A-MEM)",
|
|
@@ -2176,7 +2156,7 @@ ${text}${hookWarning}` }],
|
|
|
2176
2156
|
);
|
|
2177
2157
|
api.registerTool(
|
|
2178
2158
|
(ctx) => {
|
|
2179
|
-
const scope =
|
|
2159
|
+
const scope = buildScope2(resolveAgentId2(ctx));
|
|
2180
2160
|
return {
|
|
2181
2161
|
name: "memory_quality_scan",
|
|
2182
2162
|
label: "Memory Quality Scan (A-MEM)",
|
|
@@ -2224,7 +2204,7 @@ ${text}${hookWarning}` }],
|
|
|
2224
2204
|
"agent_end",
|
|
2225
2205
|
async (event, ctx) => {
|
|
2226
2206
|
markHookFired();
|
|
2227
|
-
const scope =
|
|
2207
|
+
const scope = buildScope2(resolveAgentId2(ctx));
|
|
2228
2208
|
const agentId = scope.agentId;
|
|
2229
2209
|
const storageCtx = scope.storageCtx;
|
|
2230
2210
|
logger.info(
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "openclaw-amem",
|
|
3
3
|
"name": "Memory (A-MEM v2)",
|
|
4
4
|
"description": "OpenClaw memory plugin implementing A-MEM — memories evolve, not just accumulate. Graph linking, hybrid retrieval, LLM-driven evolution. No Python.",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.2.0",
|
|
6
6
|
"kind": "memory",
|
|
7
7
|
"openclaw": {
|
|
8
8
|
"compat": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-amem",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "OpenClaw memory plugin implementing A-MEM — memories evolve, not just accumulate. Graph linking, hybrid retrieval, LLM-driven evolution. No Python.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -21,16 +21,17 @@
|
|
|
21
21
|
"@node-rs/jieba": "^2.0.1",
|
|
22
22
|
"@qdrant/js-client-rest": "^1.18.0",
|
|
23
23
|
"@types/uuid": "^11.0.0",
|
|
24
|
+
"openai": "^6.48.0",
|
|
24
25
|
"uuid": "^14.0.0"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
28
|
"@eslint/js": "^10.0.1",
|
|
28
29
|
"@types/node": "^26.1.1",
|
|
29
|
-
"eslint": "^10.
|
|
30
|
-
"prettier": "^3.
|
|
30
|
+
"eslint": "^10.7.0",
|
|
31
|
+
"prettier": "^3.9.5",
|
|
31
32
|
"tsup": "^8.4.0",
|
|
32
|
-
"tsx": "^4.
|
|
33
|
-
"typescript": "^
|
|
33
|
+
"tsx": "^4.23.1",
|
|
34
|
+
"typescript": "^6.0.3",
|
|
34
35
|
"typescript-eslint": "^8.60.1",
|
|
35
36
|
"vitest": "^4.1.10"
|
|
36
37
|
},
|
|
@@ -73,6 +74,7 @@
|
|
|
73
74
|
"format": "prettier --check \"src/**/*.ts\"",
|
|
74
75
|
"format:fix": "prettier --write \"src/**/*.ts\"",
|
|
75
76
|
"test": "vitest run",
|
|
77
|
+
"test:unit": "vitest run test/unit",
|
|
76
78
|
"test:watch": "vitest",
|
|
77
79
|
"check": "npm run format && npm run lint && npm run test"
|
|
78
80
|
}
|