@bolloon/bolloon-agent 0.4.7 → 0.4.9
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 +62 -0
- package/dist/web/server.js +5 -9
- 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
|
@@ -831,6 +831,41 @@ async function processInput(input, comm) {
|
|
|
831
831
|
}
|
|
832
832
|
catch { /* DID 生成失败不阻塞创建 */ }
|
|
833
833
|
const channels = await updateChannels((chs) => [...chs, ch]);
|
|
834
|
+
// 2026-08-12 (TaskFix): CLI 创建的 agent 同步写 agents.json + 关联 channelId —
|
|
835
|
+
// 与 server 创建 channel 逻辑对齐. 否则 CLI 创建的 agent 不在 agents.json,
|
|
836
|
+
// 重启后 server 的 healMissingChannels 只能从 agents.json 恢复 → 该 agent 永远消失.
|
|
837
|
+
try {
|
|
838
|
+
const agentsPath = path.join(process.env.HOME || '/tmp', '.bolloon', 'agents', 'agents.json');
|
|
839
|
+
await fs.mkdir(path.dirname(agentsPath), { recursive: true });
|
|
840
|
+
let arr = [];
|
|
841
|
+
try {
|
|
842
|
+
arr = JSON.parse(await fs.readFile(agentsPath, 'utf-8'));
|
|
843
|
+
}
|
|
844
|
+
catch { }
|
|
845
|
+
if (!Array.isArray(arr))
|
|
846
|
+
arr = [];
|
|
847
|
+
const exists = arr.find((a) => a && a.id === agentId);
|
|
848
|
+
if (exists) {
|
|
849
|
+
exists.channelId = id;
|
|
850
|
+
exists.name = name.trim();
|
|
851
|
+
exists.lastActive = new Date().toISOString();
|
|
852
|
+
}
|
|
853
|
+
else {
|
|
854
|
+
arr.push({
|
|
855
|
+
id: agentId,
|
|
856
|
+
name: name.trim(),
|
|
857
|
+
did: `did:local:${id}`,
|
|
858
|
+
description: `Agent ${name} (auto-registered from channel ${id})`,
|
|
859
|
+
status: 'active',
|
|
860
|
+
createdAt: new Date().toISOString(),
|
|
861
|
+
lastActive: new Date().toISOString(),
|
|
862
|
+
channelId: id,
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
await fs.writeFile(agentsPath, JSON.stringify(arr, null, 2), 'utf-8');
|
|
866
|
+
console.log(`[创建频道] agent 同步进 agents.json: ${agentId} → channel ${id}`);
|
|
867
|
+
}
|
|
868
|
+
catch { /* agents.json 写失败不阻塞创建 */ }
|
|
834
869
|
// 刷新 store 缓存 (updateChannels 走了 server-storage, store 内存还是旧的)
|
|
835
870
|
await store.load();
|
|
836
871
|
await store.setActive(id);
|
|
@@ -1912,6 +1947,33 @@ async function processInput(input, comm) {
|
|
|
1912
1947
|
}
|
|
1913
1948
|
});
|
|
1914
1949
|
}
|
|
1950
|
+
// 2026-08-12 (TaskM2, hermes sync 模式): CLI 对话结束后同步记忆 — 每轮 compressSessionToMemory
|
|
1951
|
+
// 把本会话消息压缩成摘要 (≥4 新消息触发), 供后续运行时 recallMemory 自动召回 (跨 session 记忆).
|
|
1952
|
+
// Web 模式 server.ts 已有, CLI 之前缺失 → CLI 下无摘要可召回. 失败静默, 不阻塞对话.
|
|
1953
|
+
if (cliActiveChannelId) {
|
|
1954
|
+
setImmediate(async () => {
|
|
1955
|
+
try {
|
|
1956
|
+
const { compressSessionToMemory } = await import('./bootstrap/memory-compressor.js');
|
|
1957
|
+
const channelForMem = String(cliActiveChannelId || '');
|
|
1958
|
+
let sessionId = 'default';
|
|
1959
|
+
try {
|
|
1960
|
+
const { getIdentityStore } = await import('./agents/agent-identity-store.js');
|
|
1961
|
+
const store = getIdentityStore();
|
|
1962
|
+
await store.load();
|
|
1963
|
+
const ch = store.rawChannels.find((c) => c.id === channelForMem);
|
|
1964
|
+
if (ch && ch.currentSessionId)
|
|
1965
|
+
sessionId = String(ch.currentSessionId);
|
|
1966
|
+
}
|
|
1967
|
+
catch { /* 读 sessionId 失败用 default */ }
|
|
1968
|
+
await compressSessionToMemory({
|
|
1969
|
+
agentId: getCliAgentId(),
|
|
1970
|
+
channelId: channelForMem,
|
|
1971
|
+
sessionId,
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
catch { /* 记忆压缩失败静默 */ }
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1915
1977
|
// 更新状态栏: 上下文进度 (2026-08-06: 每轮按当前 messageHistory 重算并写回 ContextManager,
|
|
1916
1978
|
// 保证状态栏按需更新 — 不依赖 pi-sdk loop 内部上报, 1s 定时器读到的一定是最新值)
|
|
1917
1979
|
// 2026-08-07 修复: pi-sdk loop 每轮已用 estimateHistoryTokens() 上报 ContextManager (pi-sdk.ts:1223),
|
package/dist/web/server.js
CHANGED
|
@@ -1731,7 +1731,6 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1731
1731
|
const { existsSync } = await import('fs');
|
|
1732
1732
|
const { readFile } = await import('fs/promises');
|
|
1733
1733
|
const agentsFile = `${process.env.HOME || '/tmp'}/.bolloon/agents/agents.json`;
|
|
1734
|
-
const sessionsDir = `${process.env.HOME || '/tmp'}/.bolloon/sessions/cache`;
|
|
1735
1734
|
if (!existsSync(agentsFile))
|
|
1736
1735
|
return 0;
|
|
1737
1736
|
const agentsRaw = await readFile(agentsFile, 'utf-8');
|
|
@@ -1744,14 +1743,11 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1744
1743
|
const cid = a && a.channelId;
|
|
1745
1744
|
if (!cid || knownIds.has(cid))
|
|
1746
1745
|
continue;
|
|
1747
|
-
//
|
|
1748
|
-
//
|
|
1749
|
-
//
|
|
1750
|
-
//
|
|
1751
|
-
|
|
1752
|
-
const hasSession = existsSync(getSessionCacheFile(cid, 'default')) || existsSync(`${sessionsDir}/${cid}.json`);
|
|
1753
|
-
if (!hasSession)
|
|
1754
|
-
continue;
|
|
1746
|
+
// 2026-08-12 (TaskFix): 放宽恢复条件 — 只要 agents.json 里 agent 关联了 channelId (非空),
|
|
1747
|
+
// 且 channels.json 缺失该 channel, 就恢复 channel stub.
|
|
1748
|
+
// 之前强制要求 session cache 文件存在, 导致"刚创建还没对话"的 agent (CLI /new agent 同步 agents.json 后)
|
|
1749
|
+
// 重启时 session 文件不存在 → 永不恢复 → "以前创建的智能体消失" bug.
|
|
1750
|
+
// 空 channelId (旧数据无关联 channel) 仍跳过, 避免给无关 agent 乱建 channel.
|
|
1755
1751
|
const restored = {
|
|
1756
1752
|
id: cid,
|
|
1757
1753
|
name: a.name || `Agent-${String(cid).slice(-6)}`,
|