@memtensor/memos-cloud-openclaw-plugin 0.1.15 → 0.1.16-beta.1
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/README_ZH.md +2 -2
- package/clawdbot.plugin.json +316 -316
- package/index.js +272 -39
- package/lib/memos-cloud-api.js +48 -14
- package/moltbot.plugin.json +316 -316
- package/openclaw.plugin.json +316 -316
- package/package.json +1 -1
- package/test/recall-hook-registration.test.mjs +56 -0
package/index.js
CHANGED
|
@@ -27,6 +27,27 @@ const API_KEY_HELP_URL = "https://memos-dashboard.openmem.net/cn/apikeys/";
|
|
|
27
27
|
const ENV_FILE_SEARCH_HINTS = ["~/.openclaw/.env", "~/.moltbot/.env", "~/.clawdbot/.env"];
|
|
28
28
|
const MEMOS_SOURCE = "openclaw";
|
|
29
29
|
|
|
30
|
+
// Heartbeat prompts are always injected at the very beginning of the user
|
|
31
|
+
// content by the host (OpenClaw). Anchoring at start prevents false positives
|
|
32
|
+
// when a legitimate user message happens to mention these phrases.
|
|
33
|
+
const HEARTBEAT_PROMPT_PATTERN =
|
|
34
|
+
/^\s*(?:Read HEARTBEAT\.md if it exists\b|\[OpenClaw heartbeat poll\])/i;
|
|
35
|
+
const SYSTEM_COMMAND_PATTERN = /^\/(?:new|reset|clear|stop|status|help|dock_|undock)\b/i;
|
|
36
|
+
const INTERNAL_SYSTEM_PROMPT_PATTERNS = [
|
|
37
|
+
/^A new session was started via \/new or \/reset\./i,
|
|
38
|
+
/^Based on this conversation, generate a short 1-2 word filename slug\b[\s\S]*\bReply with ONLY the slug\b/i,
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
function isHeartbeatPrompt(text) {
|
|
42
|
+
return typeof text === "string" && HEARTBEAT_PROMPT_PATTERN.test(text);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function isSystemCommandPrompt(text) {
|
|
46
|
+
if (typeof text !== "string") return false;
|
|
47
|
+
const prompt = text.trimStart();
|
|
48
|
+
return SYSTEM_COMMAND_PATTERN.test(prompt) || INTERNAL_SYSTEM_PROMPT_PATTERNS.some((pattern) => pattern.test(prompt));
|
|
49
|
+
}
|
|
50
|
+
|
|
30
51
|
function warnMissingApiKey(log, context) {
|
|
31
52
|
const heading = "[memos-cloud] Missing MEMOS_API_KEY (Token auth)";
|
|
32
53
|
const header = `${heading}${context ? `; ${context} skipped` : ""}. Configure it with:`;
|
|
@@ -178,48 +199,222 @@ export function buildAddMessagePayload(cfg, messages, ctx) {
|
|
|
178
199
|
return payload;
|
|
179
200
|
}
|
|
180
201
|
|
|
181
|
-
function
|
|
182
|
-
const
|
|
183
|
-
.
|
|
184
|
-
.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
202
|
+
function convertAssistantMessage(msg, cfg) {
|
|
203
|
+
const contentArr = Array.isArray(msg.content)
|
|
204
|
+
? msg.content
|
|
205
|
+
: msg.content
|
|
206
|
+
? [{ type: "text", text: String(msg.content) }]
|
|
207
|
+
: [];
|
|
208
|
+
|
|
209
|
+
const textContent = contentArr
|
|
210
|
+
.filter((c) => c?.type === "text")
|
|
211
|
+
.map((c) => c.text || "")
|
|
212
|
+
.filter(Boolean)
|
|
213
|
+
.join("\n");
|
|
214
|
+
|
|
215
|
+
const toolCallItems = contentArr.filter((c) => c?.type === "toolCall");
|
|
216
|
+
|
|
217
|
+
const result = { role: "assistant" };
|
|
218
|
+
|
|
219
|
+
if (textContent) {
|
|
220
|
+
result.content = truncate(textContent, cfg.maxMessageChars);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (cfg.includeToolMemory && toolCallItems.length > 0) {
|
|
224
|
+
result.tool_calls = toolCallItems.map((tc) => ({
|
|
225
|
+
id: tc.id,
|
|
226
|
+
type: "function",
|
|
227
|
+
function: {
|
|
228
|
+
name: tc.name,
|
|
229
|
+
arguments: typeof tc.arguments === "string" ? tc.arguments : JSON.stringify(tc.arguments ?? {}),
|
|
230
|
+
},
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!result.content && !result.tool_calls) return null;
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function safeStringify(value) {
|
|
239
|
+
try {
|
|
240
|
+
return JSON.stringify(value);
|
|
241
|
+
} catch {
|
|
242
|
+
return "";
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 把单个附件值(URL / data URI / 裸 base64)统一描述成可读 text:
|
|
247
|
+
// - http(s):// / 其它协议 URL:[<kind>: <url>]
|
|
248
|
+
// - data:<mediaType>;base64,...:[<kind> (<mediaType> base64, ~<size> chars)]
|
|
249
|
+
// - 其它(视为裸 base64):[<kind> (base64, ~<size> chars)]
|
|
250
|
+
function describeAttachment(kind, value) {
|
|
251
|
+
const dataMatch = /^data:([^;,]+)/i.exec(value);
|
|
252
|
+
if (dataMatch) {
|
|
253
|
+
return `[${kind} (${dataMatch[1] || kind} base64, ~${value.length} chars)]`;
|
|
254
|
+
}
|
|
255
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(value)) {
|
|
256
|
+
return `[${kind}: ${value}]`;
|
|
257
|
+
}
|
|
258
|
+
return `[${kind} (base64, ~${value.length} chars)]`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// MemOS 是文本记忆服务,召回路径上图片/文件 block 几乎只有文本价值。
|
|
262
|
+
// 这里把所有 block 一律归一成 [{type:"text", text}],但**保留 URL 文字本身**:
|
|
263
|
+
// - text block:透传文本(按 cfg.maxMessageChars 截头)
|
|
264
|
+
// - URL 形态:输出 "[image: <url>]" / "[file: <url>]",URL 作为可检索文字保留
|
|
265
|
+
// - data URI / base64 形态:输出 "[image (<media_type> base64, ~<size> chars)]" 元数据描述,永不 inline base64
|
|
266
|
+
// - 未识别 type:含 url 字段则 "[<type>: <url>]",否则 JSON.stringify 兜底
|
|
267
|
+
function normalizeToolResultContent(content, cfg) {
|
|
268
|
+
const blocks = [];
|
|
269
|
+
|
|
270
|
+
const pushText = (raw) => {
|
|
271
|
+
const text = truncate(String(raw ?? ""), cfg.maxMessageChars);
|
|
272
|
+
if (text) blocks.push({ type: "text", text });
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// 解析所有协议下的 image 类 block,提取出统一的"附件值"再交给 describeAttachment 描述。
|
|
276
|
+
// 覆盖:
|
|
277
|
+
// {type:"image_url", image_url:{url}} / {image_url:"<str>"} / 顶层 url (OpenAI 风格)
|
|
278
|
+
// {type:"image", data, media_type} / {type:"image", source:{data, media_type}} (Claude 风格)
|
|
279
|
+
// {type:"image", url} (少见)
|
|
280
|
+
const tryPushImageBlock = (block) => {
|
|
281
|
+
const claudeData =
|
|
282
|
+
(block.source && typeof block.source === "object" && block.source.data) || block.data || "";
|
|
283
|
+
if (claudeData) {
|
|
284
|
+
const mediaType =
|
|
285
|
+
(block.source && typeof block.source === "object" && block.source.media_type) ||
|
|
286
|
+
block.media_type ||
|
|
287
|
+
block.mimeType ||
|
|
288
|
+
"image";
|
|
289
|
+
pushText(describeAttachment("image", `data:${mediaType};base64,${String(claudeData)}`));
|
|
290
|
+
return true;
|
|
199
291
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
292
|
+
const url =
|
|
293
|
+
(block.image_url && typeof block.image_url === "object" && block.image_url.url) ||
|
|
294
|
+
(typeof block.image_url === "string" ? block.image_url : "") ||
|
|
295
|
+
block.url ||
|
|
296
|
+
"";
|
|
297
|
+
if (!url) return false;
|
|
298
|
+
pushText(describeAttachment("image", String(url)));
|
|
299
|
+
return true;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// MemOS schema 标准 file block:{type:"file", file:{file_data}},兼容顶层 file_data。
|
|
303
|
+
const tryPushFileBlock = (block) => {
|
|
304
|
+
const fileData =
|
|
305
|
+
(block.file && typeof block.file === "object" && block.file.file_data) ||
|
|
306
|
+
block.file_data ||
|
|
307
|
+
"";
|
|
308
|
+
if (!fileData) return false;
|
|
309
|
+
pushText(describeAttachment("file", String(fileData)));
|
|
310
|
+
return true;
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const tryPushTypedBlock = (block) => {
|
|
314
|
+
if (!block || typeof block !== "object") return false;
|
|
315
|
+
if (block.type === "text") {
|
|
316
|
+
pushText(block.text);
|
|
317
|
+
return true;
|
|
203
318
|
}
|
|
319
|
+
if (block.type === "image_url" || block.type === "image") return tryPushImageBlock(block);
|
|
320
|
+
if (block.type === "file") return tryPushFileBlock(block);
|
|
321
|
+
return false;
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
// 未识别 type:有 url 字段则给可读占位,否则整体 stringify。
|
|
325
|
+
const fallbackSerialize = (block) => {
|
|
326
|
+
if (
|
|
327
|
+
block &&
|
|
328
|
+
typeof block === "object" &&
|
|
329
|
+
typeof block.type === "string" &&
|
|
330
|
+
typeof block.url === "string" &&
|
|
331
|
+
block.url
|
|
332
|
+
) {
|
|
333
|
+
pushText(`[${block.type}: ${block.url}]`);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const serialized = safeStringify(block);
|
|
337
|
+
if (serialized) pushText(serialized);
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
if (content == null || content === "") return blocks;
|
|
341
|
+
|
|
342
|
+
if (typeof content === "string") {
|
|
343
|
+
pushText(content);
|
|
344
|
+
return blocks;
|
|
204
345
|
}
|
|
205
346
|
|
|
206
|
-
|
|
207
|
-
|
|
347
|
+
if (Array.isArray(content)) {
|
|
348
|
+
for (const block of content) {
|
|
349
|
+
if (block == null) continue;
|
|
350
|
+
if (typeof block === "string") {
|
|
351
|
+
pushText(block);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (typeof block !== "object") continue;
|
|
355
|
+
if (tryPushTypedBlock(block)) continue;
|
|
356
|
+
fallbackSerialize(block);
|
|
357
|
+
}
|
|
358
|
+
return blocks;
|
|
359
|
+
}
|
|
208
360
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (!msg || !msg.role) continue;
|
|
213
|
-
if (msg.role === "user") {
|
|
214
|
-
const content = stripOpenClawInjectedPrefix(extractText(msg.content));
|
|
215
|
-
if (content) results.push({ role: "user", content: truncate(content, cfg.maxMessageChars) });
|
|
361
|
+
if (typeof content === "object") {
|
|
362
|
+
if (!tryPushTypedBlock(content)) {
|
|
363
|
+
fallbackSerialize(content);
|
|
216
364
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
365
|
+
return blocks;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return blocks;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function convertToolResultMessage(msg, cfg) {
|
|
372
|
+
const toolCallId = msg.toolCallId || msg.tool_call_id;
|
|
373
|
+
if (!toolCallId) return null;
|
|
374
|
+
const blocks = normalizeToolResultContent(msg.content, cfg);
|
|
375
|
+
if (blocks.length === 0) return null;
|
|
376
|
+
return {
|
|
377
|
+
role: "tool",
|
|
378
|
+
tool_call_id: toolCallId,
|
|
379
|
+
content: blocks,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// 把 OpenClaw 的单条原始消息转成 MemOS /add/message 接受的形态。
|
|
384
|
+
// 三类 role 分发:user / assistant / toolResult,其它 role(system/...)直接丢弃返 null。
|
|
385
|
+
function convertSessionMessage(msg, cfg) {
|
|
386
|
+
if (!msg || !msg.role) return null;
|
|
387
|
+
if (msg.role === "user") {
|
|
388
|
+
const content = stripOpenClawInjectedPrefix(extractText(msg.content));
|
|
389
|
+
if (!content) return null;
|
|
390
|
+
return { role: "user", content: truncate(content, cfg.maxMessageChars) };
|
|
391
|
+
}
|
|
392
|
+
if (msg.role === "assistant" && cfg.includeAssistant) {
|
|
393
|
+
return convertAssistantMessage(msg, cfg);
|
|
394
|
+
}
|
|
395
|
+
if (msg.role === "toolResult" && cfg.includeToolMemory) {
|
|
396
|
+
return convertToolResultMessage(msg, cfg);
|
|
397
|
+
}
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function pickLastTurnMessages(messages, cfg) {
|
|
402
|
+
let lastUserIndex = -1;
|
|
403
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
404
|
+
if (messages[i]?.role === "user") {
|
|
405
|
+
lastUserIndex = i;
|
|
406
|
+
break;
|
|
220
407
|
}
|
|
221
408
|
}
|
|
222
|
-
return
|
|
409
|
+
if (lastUserIndex < 0) return [];
|
|
410
|
+
return messages
|
|
411
|
+
.slice(lastUserIndex)
|
|
412
|
+
.map((m) => convertSessionMessage(m, cfg))
|
|
413
|
+
.filter(Boolean);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function pickFullSessionMessages(messages, cfg) {
|
|
417
|
+
return messages.map((m) => convertSessionMessage(m, cfg)).filter(Boolean);
|
|
223
418
|
}
|
|
224
419
|
|
|
225
420
|
function truncate(text, maxLen) {
|
|
@@ -447,6 +642,9 @@ export default {
|
|
|
447
642
|
// Start 12-hour background update interval
|
|
448
643
|
startUpdateChecker(log);
|
|
449
644
|
|
|
645
|
+
// Detect the host CLI version once so every hook registration branch can reference it.
|
|
646
|
+
const hostVersion = detectHostVersion();
|
|
647
|
+
|
|
450
648
|
// Side effects below are only meaningful when the host CLI was actually
|
|
451
649
|
// launched to run the gateway (`openclaw gateway run|start|restart`).
|
|
452
650
|
// Other entry points (e.g. `plugins install`, `security audit`) also
|
|
@@ -458,11 +656,8 @@ export default {
|
|
|
458
656
|
// misleading "probe timed out" warning before the process exits.
|
|
459
657
|
// Gate them all in one place so the policy is explicit and discoverable.
|
|
460
658
|
if (isGatewayRuntimeStartup()) {
|
|
461
|
-
// Detect the host CLI version once so every branch below can reference it.
|
|
462
659
|
// `allowConversationAccess` hook policy was introduced in 2026.4.23;
|
|
463
660
|
// older hosts do not understand the field and don't need it patched in.
|
|
464
|
-
const hostVersion = detectHostVersion();
|
|
465
|
-
|
|
466
661
|
const HOOK_POLICY_MIN_VERSION = "2026.4.23";
|
|
467
662
|
const needsHookPolicy =
|
|
468
663
|
hostVersion === null ||
|
|
@@ -529,7 +724,17 @@ export default {
|
|
|
529
724
|
);
|
|
530
725
|
}
|
|
531
726
|
|
|
532
|
-
|
|
727
|
+
const runRecall = async (event, ctx) => {
|
|
728
|
+
// Skip system events: heartbeat, /new, /reset, and other commands
|
|
729
|
+
const prompt = event?.prompt || "";
|
|
730
|
+
const isHeartbeat = isHeartbeatPrompt(prompt);
|
|
731
|
+
const isSystemCommand = isSystemCommandPrompt(prompt);
|
|
732
|
+
|
|
733
|
+
if (isHeartbeat || isSystemCommand) {
|
|
734
|
+
log.info?.(`[memos-cloud] recall skipped: system event detected (heartbeat=${isHeartbeat}, command=${isSystemCommand}, prompt="${prompt.substring(0, 50)}...")`);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
|
|
533
738
|
if (!isAgentAllowed(cfg, ctx)) {
|
|
534
739
|
log.info?.(`[memos-cloud] recall skipped: agent "${ctx?.agentId}" not in allowedAgents [${cfg.allowedAgents?.join(", ")}]`);
|
|
535
740
|
return;
|
|
@@ -561,9 +766,37 @@ export default {
|
|
|
561
766
|
} catch (err) {
|
|
562
767
|
log.warn?.(`[memos-cloud] recall failed: ${String(err)}`);
|
|
563
768
|
}
|
|
564
|
-
}
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
// Recall mutates prompt context only, so the phase-specific replacement for
|
|
772
|
+
// legacy before_agent_start is before_prompt_build. Do not register both on
|
|
773
|
+
// new hosts, otherwise the same memory block can be injected twice.
|
|
774
|
+
const PROMPT_BUILD_HOOK_MIN_VERSION = "2026.5.7";
|
|
775
|
+
const usesBeforePromptBuild =
|
|
776
|
+
hostVersion !== null &&
|
|
777
|
+
compareVersionStrings(hostVersion, PROMPT_BUILD_HOOK_MIN_VERSION) >= 0;
|
|
778
|
+
|
|
779
|
+
if (usesBeforePromptBuild) {
|
|
780
|
+
api.on("before_prompt_build", runRecall);
|
|
781
|
+
} else {
|
|
782
|
+
api.on("before_agent_start", runRecall);
|
|
783
|
+
}
|
|
565
784
|
|
|
566
785
|
api.on("agent_end", async (event, ctx) => {
|
|
786
|
+
// Skip system events: heartbeat and commands
|
|
787
|
+
// Check the last user message to determine if this was a system event
|
|
788
|
+
const messages = event?.messages || [];
|
|
789
|
+
const lastUserMsg = messages.slice().reverse().find(m => m?.role === "user");
|
|
790
|
+
const lastUserContent = extractText(lastUserMsg?.content || "");
|
|
791
|
+
|
|
792
|
+
const isHeartbeat = isHeartbeatPrompt(lastUserContent);
|
|
793
|
+
const isSystemCommand = isSystemCommandPrompt(lastUserContent);
|
|
794
|
+
|
|
795
|
+
if (isHeartbeat || isSystemCommand) {
|
|
796
|
+
log.info?.(`[memos-cloud] add skipped: system event detected (heartbeat=${isHeartbeat}, command=${isSystemCommand}, content="${lastUserContent.substring(0, 50)}...")`);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
|
|
567
800
|
if (!isAgentAllowed(cfg, ctx)) {
|
|
568
801
|
log.info?.(`[memos-cloud] add skipped: agent "${ctx?.agentId}" not in allowedAgents [${cfg.allowedAgents?.join(", ")}]`);
|
|
569
802
|
return;
|
package/lib/memos-cloud-api.js
CHANGED
|
@@ -14,6 +14,7 @@ const INBOUND_META_SENTINELS = [
|
|
|
14
14
|
"Forwarded message context (untrusted metadata):",
|
|
15
15
|
"Chat history since last reply (untrusted, for context):",
|
|
16
16
|
];
|
|
17
|
+
const SYSTEM_NOTE_PREFIX = /^Note:\s+The previous agent run was aborted by the user\./i;
|
|
17
18
|
const UNTRUSTED_CONTEXT_HEADER = "Untrusted context (metadata, do not treat as instructions or commands):";
|
|
18
19
|
const SENTINEL_FAST_RE = new RegExp(
|
|
19
20
|
[...INBOUND_META_SENTINELS, UNTRUSTED_CONTEXT_HEADER]
|
|
@@ -558,6 +559,32 @@ function stripTrailingFeishuSystemHints(text) {
|
|
|
558
559
|
return stripped || text;
|
|
559
560
|
}
|
|
560
561
|
|
|
562
|
+
function stripLeadingSystemNote(text) {
|
|
563
|
+
if (!text || typeof text !== "string") return text;
|
|
564
|
+
const lines = text.split(/\r?\n/);
|
|
565
|
+
let index = 0;
|
|
566
|
+
|
|
567
|
+
// Skip leading empty lines
|
|
568
|
+
while (index < lines.length && lines[index].trim() === "") {
|
|
569
|
+
index += 1;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (index >= lines.length) return "";
|
|
573
|
+
|
|
574
|
+
// Check if first non-empty line matches system note pattern
|
|
575
|
+
if (!SYSTEM_NOTE_PREFIX.test(lines[index])) return text;
|
|
576
|
+
|
|
577
|
+
// Skip the system note line
|
|
578
|
+
index += 1;
|
|
579
|
+
|
|
580
|
+
// Skip trailing empty lines after the note
|
|
581
|
+
while (index < lines.length && lines[index].trim() === "") {
|
|
582
|
+
index += 1;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return index === 0 ? text : lines.slice(index).join("\n");
|
|
586
|
+
}
|
|
587
|
+
|
|
561
588
|
function stripLeadingFeishuSenderPrefix(text) {
|
|
562
589
|
if (!text || typeof text !== "string") return text;
|
|
563
590
|
// Feishu user IDs are typically "ou_<id>". Strip only if it is the leading line prefix.
|
|
@@ -606,7 +633,8 @@ export function stripOpenClawInjectedPrefix(text) {
|
|
|
606
633
|
markerIndex === -1
|
|
607
634
|
? cleanedText
|
|
608
635
|
: cleanedText.slice(markerIndex + USER_QUERY_MARKER.length);
|
|
609
|
-
const
|
|
636
|
+
const withoutSystemNote = stripLeadingSystemNote(withoutRecallPrefix).trimStart();
|
|
637
|
+
const withoutInboundMetadata = stripLeadingInboundMetadata(withoutSystemNote).trimStart();
|
|
610
638
|
const withoutMessageIdHints = stripLeadingMessageIdHints(withoutInboundMetadata).trimStart();
|
|
611
639
|
const withoutEnvelope = stripLeadingEnvelope(withoutMessageIdHints).trimStart();
|
|
612
640
|
const withoutTrailingSystemHints = stripTrailingFeishuSystemHints(withoutEnvelope).trimStart();
|
|
@@ -675,13 +703,12 @@ function wrapCodeBlock(lines, options = {}) {
|
|
|
675
703
|
function buildMemorySections(data, options = {}) {
|
|
676
704
|
const memoryList = data?.memory_detail_list ?? [];
|
|
677
705
|
const preferenceList = data?.preference_detail_list ?? [];
|
|
706
|
+
const toolMemoryList = data?.tool_memory_detail_list ?? [];
|
|
707
|
+
|
|
708
|
+
const threshold = options.relativity ?? 0;
|
|
678
709
|
|
|
679
710
|
const memoryLines = memoryList
|
|
680
|
-
.filter((item) =>
|
|
681
|
-
const score = item?.relativity ?? 1;
|
|
682
|
-
const threshold = options.relativity ?? 0;
|
|
683
|
-
return score > threshold;
|
|
684
|
-
})
|
|
711
|
+
.filter((item) => (item?.relativity ?? 1) > threshold)
|
|
685
712
|
.map((item) => {
|
|
686
713
|
const text = item?.memory_value || item?.memory_key || "";
|
|
687
714
|
return formatMemoryLine(item, text, options);
|
|
@@ -689,18 +716,22 @@ function buildMemorySections(data, options = {}) {
|
|
|
689
716
|
.filter(Boolean);
|
|
690
717
|
|
|
691
718
|
const preferenceLines = preferenceList
|
|
692
|
-
.filter((item) =>
|
|
693
|
-
const score = item?.relativity ?? 1;
|
|
694
|
-
const threshold = options.relativity ?? 0;
|
|
695
|
-
return score > threshold;
|
|
696
|
-
})
|
|
719
|
+
.filter((item) => (item?.relativity ?? 1) > threshold)
|
|
697
720
|
.map((item) => {
|
|
698
721
|
const text = item?.preference || "";
|
|
699
722
|
return formatPreferenceLine(item, text, options);
|
|
700
723
|
})
|
|
701
724
|
.filter(Boolean);
|
|
702
725
|
|
|
703
|
-
|
|
726
|
+
const toolMemoryLines = toolMemoryList
|
|
727
|
+
.filter((item) => (item?.relativity ?? 1) > threshold)
|
|
728
|
+
.map((item) => {
|
|
729
|
+
const text = item?.tool_value || "";
|
|
730
|
+
return formatMemoryLine(item, text, options);
|
|
731
|
+
})
|
|
732
|
+
.filter(Boolean);
|
|
733
|
+
|
|
734
|
+
return { memoryLines, preferenceLines, toolMemoryLines };
|
|
704
735
|
}
|
|
705
736
|
|
|
706
737
|
const STATIC_RECALL_SYSTEM_PROMPT = [
|
|
@@ -752,8 +783,8 @@ const STATIC_RECALL_SYSTEM_PROMPT = [
|
|
|
752
783
|
].join("\n");
|
|
753
784
|
|
|
754
785
|
function buildMemoryPrependBlock(data, options = {}) {
|
|
755
|
-
const { memoryLines, preferenceLines } = buildMemorySections(data, options);
|
|
756
|
-
const hasContent = memoryLines.length > 0 || preferenceLines.length > 0;
|
|
786
|
+
const { memoryLines, preferenceLines, toolMemoryLines } = buildMemorySections(data, options);
|
|
787
|
+
const hasContent = memoryLines.length > 0 || preferenceLines.length > 0 || toolMemoryLines.length > 0;
|
|
757
788
|
if (!hasContent) return "";
|
|
758
789
|
|
|
759
790
|
const memoriesBlock = [
|
|
@@ -761,6 +792,9 @@ function buildMemoryPrependBlock(data, options = {}) {
|
|
|
761
792
|
" <facts>",
|
|
762
793
|
...memoryLines,
|
|
763
794
|
" </facts>",
|
|
795
|
+
" <tool_memories>",
|
|
796
|
+
...toolMemoryLines,
|
|
797
|
+
" </tool_memories>",
|
|
764
798
|
" <preferences>",
|
|
765
799
|
...preferenceLines,
|
|
766
800
|
" </preferences>",
|