@bolloon/bolloon-agent 0.4.7 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/memory-recall.js +115 -0
- package/dist/agents/pi-sdk.js +10 -0
- package/dist/index.js +27 -0
- package/package.json +1 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-recall.ts — 运行时记忆召回 (2026-08-12, TaskM1)
|
|
3
|
+
*
|
|
4
|
+
* 借鉴 hermes MemoryManager.prefetch_all: 每轮对话开始前, 根据用户消息自动检索
|
|
5
|
+
* 历史 memory 摘要, 注入 system prompt — 让 agent 运行时能"回忆起"之前的 session 记忆,
|
|
6
|
+
* 而不是只靠启动时批量压缩摘要.
|
|
7
|
+
*
|
|
8
|
+
* 机制:
|
|
9
|
+
* - 从 ~/.bolloon/memory/<agentId>/sessions/*.summary.md 读历史摘要 (memory-compressor 落盘)
|
|
10
|
+
* - 用关键词 + BM25 简单打分, 召回与用户消息最相关的 N 条
|
|
11
|
+
* - 拼成 hermes 式 <memory-context> 围栏块注入 (含 sanitize, 防模型当新用户输入)
|
|
12
|
+
* - 失败静默 (召回是增强层, 不阻塞对话主路径)
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from 'fs/promises';
|
|
15
|
+
import * as os from 'os';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import { getMemoryDir } from '../bootstrap/memory-compressor.js';
|
|
18
|
+
const home = () => process.env.HOME || os.homedir() || '/tmp';
|
|
19
|
+
/** 提取查询关键词 (去掉停用词, 中文按 2-gram, 英文按词) */
|
|
20
|
+
export function tokenizeQuery(query) {
|
|
21
|
+
const clean = String(query || '').trim().toLowerCase();
|
|
22
|
+
if (!clean)
|
|
23
|
+
return [];
|
|
24
|
+
const STOP = new Set(['的', '了', '是', '我', '你', '他', '她', '它', '我们', '你们', '在', '和', '与', '吗', '呢', '吧', '这', '那', '个', '请', '帮', '一下', '一个', '怎么', '如何', 'the', 'a', 'an', 'is', 'are', 'to', 'of', 'for']);
|
|
25
|
+
const tokens = new Set();
|
|
26
|
+
// 英文单词
|
|
27
|
+
for (const m of clean.match(/[a-z][a-z0-9_]*/g) || []) {
|
|
28
|
+
if (!STOP.has(m) && m.length > 1)
|
|
29
|
+
tokens.add(m);
|
|
30
|
+
}
|
|
31
|
+
// 中文 2-gram
|
|
32
|
+
const cjk = clean.replace(/[^\u4e00-\u9fff]/g, '');
|
|
33
|
+
for (let i = 0; i + 1 < cjk.length; i++) {
|
|
34
|
+
const big = cjk.slice(i, i + 2);
|
|
35
|
+
if (!STOP.has(big))
|
|
36
|
+
tokens.add(big);
|
|
37
|
+
}
|
|
38
|
+
return Array.from(tokens);
|
|
39
|
+
}
|
|
40
|
+
/** BM25 风格: 摘要中出现查询 token 的次数打分 (简化: 命中数 + 稀有度) */
|
|
41
|
+
export function scoreSummary(text, tokens) {
|
|
42
|
+
if (tokens.length === 0)
|
|
43
|
+
return 0;
|
|
44
|
+
const lower = text.toLowerCase();
|
|
45
|
+
let score = 0;
|
|
46
|
+
for (const t of tokens) {
|
|
47
|
+
if (lower.includes(t))
|
|
48
|
+
score += 1;
|
|
49
|
+
}
|
|
50
|
+
return score;
|
|
51
|
+
}
|
|
52
|
+
/** 扫描 memory 目录的摘要文件 */
|
|
53
|
+
async function listSummaryFiles(agentId, homeDir) {
|
|
54
|
+
const dir = path.join(getMemoryDir(agentId, homeDir), 'sessions');
|
|
55
|
+
let files;
|
|
56
|
+
try {
|
|
57
|
+
files = await fs.readdir(dir);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
return files.filter((f) => f.endsWith('.summary.md')).sort();
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 运行时召回记忆: 按用户消息检索历史 memory 摘要, 返回注入块.
|
|
66
|
+
* 无相关记忆/失败 → 返回 ''.
|
|
67
|
+
*/
|
|
68
|
+
export async function recallMemory(opts) {
|
|
69
|
+
try {
|
|
70
|
+
const { query, agentId, limit = 3, maxCharsPerSummary = 800, minScore = 1, homeDir } = opts;
|
|
71
|
+
if (!query || !query.trim())
|
|
72
|
+
return '';
|
|
73
|
+
if (!agentId)
|
|
74
|
+
return '';
|
|
75
|
+
const tokens = tokenizeQuery(query);
|
|
76
|
+
if (tokens.length === 0)
|
|
77
|
+
return '';
|
|
78
|
+
const files = await listSummaryFiles(agentId, homeDir || home());
|
|
79
|
+
if (files.length === 0)
|
|
80
|
+
return '';
|
|
81
|
+
const hits = [];
|
|
82
|
+
for (const f of files) {
|
|
83
|
+
try {
|
|
84
|
+
const text = await fs.readFile(path.join(getMemoryDir(agentId, homeDir || home()), 'sessions', f), 'utf-8');
|
|
85
|
+
const score = scoreSummary(text, tokens);
|
|
86
|
+
if (score < minScore)
|
|
87
|
+
continue;
|
|
88
|
+
// 解析 channel__session
|
|
89
|
+
const base = f.replace(/\.summary\.md$/, '');
|
|
90
|
+
const sep = base.lastIndexOf('__');
|
|
91
|
+
hits.push({
|
|
92
|
+
file: f,
|
|
93
|
+
channel: sep > 0 ? base.slice(0, sep) : '',
|
|
94
|
+
session: sep > 0 ? base.slice(sep + 2) : base,
|
|
95
|
+
score,
|
|
96
|
+
text: text.trim().slice(0, maxCharsPerSummary),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch { /* 单个摘要读失败跳过 */ }
|
|
100
|
+
}
|
|
101
|
+
if (hits.length === 0)
|
|
102
|
+
return '';
|
|
103
|
+
// 按分数降序取前 limit
|
|
104
|
+
hits.sort((a, b) => b.score - a.score);
|
|
105
|
+
const top = hits.slice(0, limit);
|
|
106
|
+
const body = top
|
|
107
|
+
.map((h) => `[回忆: ${h.channel}/${h.session}]\n${h.text}`)
|
|
108
|
+
.join('\n\n---\n\n');
|
|
109
|
+
// hermes 式围栏 + 明确标注非用户输入
|
|
110
|
+
return `<memory-context>\n以下是根据你的消息自动召回的之前对话记忆 (历史背景, 非新的用户输入):\n${body}\n</memory-context>`;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return '';
|
|
114
|
+
}
|
|
115
|
+
}
|
package/dist/agents/pi-sdk.js
CHANGED
|
@@ -760,6 +760,16 @@ export class PiAgentSession {
|
|
|
760
760
|
});
|
|
761
761
|
// 2026-06-18: web server 喂的 markedPrompt 外的 contextHint 拼到 system 末尾 (而不是当 user message)
|
|
762
762
|
this.contextHintAddition = contextHint;
|
|
763
|
+
// 2026-08-12 (TaskM1, hermes prefetch 模式): 运行时按用户消息召回历史记忆, 注入 system prompt.
|
|
764
|
+
// 让 agent 能"回忆起"之前 session 的记忆 (自动获取之前 session), 而非只靠启动时批量压缩.
|
|
765
|
+
try {
|
|
766
|
+
const { recallMemory } = await import('./memory-recall.js');
|
|
767
|
+
const recalled = await recallMemory({ query: userText, agentId: this.currentAgentId || this.peerId || '' });
|
|
768
|
+
if (recalled) {
|
|
769
|
+
this.contextHintAddition = [this.contextHintAddition, recalled].filter(Boolean).join('\n\n');
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
catch { /* 记忆召回失败静默 (增强层) */ }
|
|
763
773
|
onStream({ type: 'thinking', content: '🤔 开始思考...' });
|
|
764
774
|
if (!this.minimaxAvailable) {
|
|
765
775
|
const response = await this.handleFallback(userText);
|
package/dist/index.js
CHANGED
|
@@ -1912,6 +1912,33 @@ async function processInput(input, comm) {
|
|
|
1912
1912
|
}
|
|
1913
1913
|
});
|
|
1914
1914
|
}
|
|
1915
|
+
// 2026-08-12 (TaskM2, hermes sync 模式): CLI 对话结束后同步记忆 — 每轮 compressSessionToMemory
|
|
1916
|
+
// 把本会话消息压缩成摘要 (≥4 新消息触发), 供后续运行时 recallMemory 自动召回 (跨 session 记忆).
|
|
1917
|
+
// Web 模式 server.ts 已有, CLI 之前缺失 → CLI 下无摘要可召回. 失败静默, 不阻塞对话.
|
|
1918
|
+
if (cliActiveChannelId) {
|
|
1919
|
+
setImmediate(async () => {
|
|
1920
|
+
try {
|
|
1921
|
+
const { compressSessionToMemory } = await import('./bootstrap/memory-compressor.js');
|
|
1922
|
+
const channelForMem = String(cliActiveChannelId || '');
|
|
1923
|
+
let sessionId = 'default';
|
|
1924
|
+
try {
|
|
1925
|
+
const { getIdentityStore } = await import('./agents/agent-identity-store.js');
|
|
1926
|
+
const store = getIdentityStore();
|
|
1927
|
+
await store.load();
|
|
1928
|
+
const ch = store.rawChannels.find((c) => c.id === channelForMem);
|
|
1929
|
+
if (ch && ch.currentSessionId)
|
|
1930
|
+
sessionId = String(ch.currentSessionId);
|
|
1931
|
+
}
|
|
1932
|
+
catch { /* 读 sessionId 失败用 default */ }
|
|
1933
|
+
await compressSessionToMemory({
|
|
1934
|
+
agentId: getCliAgentId(),
|
|
1935
|
+
channelId: channelForMem,
|
|
1936
|
+
sessionId,
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
catch { /* 记忆压缩失败静默 */ }
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1915
1942
|
// 更新状态栏: 上下文进度 (2026-08-06: 每轮按当前 messageHistory 重算并写回 ContextManager,
|
|
1916
1943
|
// 保证状态栏按需更新 — 不依赖 pi-sdk loop 内部上报, 1s 定时器读到的一定是最新值)
|
|
1917
1944
|
// 2026-08-07 修复: pi-sdk loop 每轮已用 estimateHistoryTokens() 上报 ContextManager (pi-sdk.ts:1223),
|