@bolloon/bolloon-agent 0.6.5 → 0.6.6

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.
Files changed (169) hide show
  1. package/dist/agents/chain/chain-settlement.js +0 -1
  2. package/dist/agents/chat-segmenter.js +4 -16
  3. package/dist/agents/contacts/human-directory.js +155 -0
  4. package/dist/agents/contacts/tools.js +79 -0
  5. package/dist/agents/experience-review.js +0 -3
  6. package/dist/agents/goal-flywheel/goal-change.js +3 -4
  7. package/dist/agents/goal-resume.js +0 -4
  8. package/dist/agents/intent-classifier.js +1 -1
  9. package/dist/agents/iteration-budget.js +12 -0
  10. package/dist/agents/knowledge-organizer.js +0 -2
  11. package/dist/agents/lesson-to-skill.js +0 -1
  12. package/dist/agents/loop-review.js +2 -2
  13. package/dist/agents/memory-store.js +359 -0
  14. package/dist/agents/opportunity-web-search.js +39 -0
  15. package/dist/agents/p2p-chat-tools.js +1 -2
  16. package/dist/agents/parse-tool-call.js +9 -22
  17. package/dist/agents/pi-harness.js +1 -2
  18. package/dist/agents/pi-sdk-tools.js +180 -5
  19. package/dist/agents/pi-sdk.js +143 -9
  20. package/dist/agents/process-runner.js +28 -9
  21. package/dist/agents/react-loop.js +9 -17
  22. package/dist/agents/runner-resolver.js +4 -3
  23. package/dist/agents/skill-loader.js +22 -15
  24. package/dist/agents/skills-manager.js +1 -25
  25. package/dist/agents/workflow-pivot-loop.js +152 -61
  26. package/dist/bootstrap/context-collector.js +2 -4
  27. package/dist/bootstrap/context-hierarchy.js +13 -1
  28. package/dist/cli/ink-app.js +44 -17
  29. package/dist/cli/intent-command.js +22 -12
  30. package/dist/cli/loading-tui.js +8 -2
  31. package/dist/cli/log-gate.js +40 -8
  32. package/dist/cli/mention-data.js +1 -0
  33. package/dist/cli/reply-hygiene.js +22 -0
  34. package/dist/cli/trace-line.js +61 -2
  35. package/dist/electron-build/agents/agent-gateway.js.map +1 -1
  36. package/dist/electron-build/agents/chain/chain-indexer.js.map +1 -1
  37. package/dist/electron-build/agents/chain/chain-settlement.js +0 -1
  38. package/dist/electron-build/agents/chain/chain-settlement.js.map +1 -1
  39. package/dist/electron-build/agents/contacts/chain.js.map +1 -1
  40. package/dist/electron-build/agents/contacts/human-directory.js +204 -0
  41. package/dist/electron-build/agents/contacts/human-directory.js.map +1 -0
  42. package/dist/electron-build/agents/contacts/policy.js.map +1 -1
  43. package/dist/electron-build/agents/contacts/tools.js +112 -0
  44. package/dist/electron-build/agents/contacts/tools.js.map +1 -1
  45. package/dist/electron-build/agents/experience-review.js +0 -3
  46. package/dist/electron-build/agents/experience-review.js.map +1 -1
  47. package/dist/electron-build/agents/external-events.js.map +1 -1
  48. package/dist/electron-build/agents/goal-flywheel/goal-change.js +3 -4
  49. package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -1
  50. package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -1
  51. package/dist/electron-build/agents/goal-resume.js +0 -4
  52. package/dist/electron-build/agents/goal-resume.js.map +1 -1
  53. package/dist/electron-build/agents/intent-classifier.js +1 -1
  54. package/dist/electron-build/agents/lesson-to-skill.js +0 -1
  55. package/dist/electron-build/agents/lesson-to-skill.js.map +1 -1
  56. package/dist/electron-build/agents/loop-review.js +2 -2
  57. package/dist/electron-build/agents/loop-review.js.map +1 -1
  58. package/dist/electron-build/agents/memory-recall.js.map +1 -1
  59. package/dist/electron-build/agents/memory-store.js +404 -0
  60. package/dist/electron-build/agents/memory-store.js.map +1 -0
  61. package/dist/electron-build/agents/parse-tool-call.js +9 -22
  62. package/dist/electron-build/agents/parse-tool-call.js.map +1 -1
  63. package/dist/electron-build/agents/pi-harness.js +1 -2
  64. package/dist/electron-build/agents/pi-harness.js.map +1 -1
  65. package/dist/electron-build/agents/pi-sdk-tools.js +180 -5
  66. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  67. package/dist/electron-build/agents/pi-sdk.js +143 -9
  68. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  69. package/dist/electron-build/agents/pre-tool-validator.js.map +1 -1
  70. package/dist/electron-build/agents/process-runner.js +28 -9
  71. package/dist/electron-build/agents/process-runner.js.map +1 -1
  72. package/dist/electron-build/agents/runner-resolver.js +4 -3
  73. package/dist/electron-build/agents/runner-resolver.js.map +1 -1
  74. package/dist/electron-build/agents/skill-loader.js +22 -16
  75. package/dist/electron-build/agents/skill-loader.js.map +1 -1
  76. package/dist/electron-build/agents/skill-supervisor-link.js.map +1 -1
  77. package/dist/electron-build/agents/skills-manager.js +0 -24
  78. package/dist/electron-build/agents/skills-manager.js.map +1 -1
  79. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  80. package/dist/electron-build/agents/workflow-engine.js.map +1 -1
  81. package/dist/electron-build/agents/workflow-pivot-loop.js +155 -62
  82. package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
  83. package/dist/electron-build/agents/x402/goal-run-bridge.js.map +1 -1
  84. package/dist/electron-build/bollharness-integration/gate-state-machine.js.map +1 -1
  85. package/dist/electron-build/bootstrap/context-collector.js +2 -4
  86. package/dist/electron-build/bootstrap/context-collector.js.map +1 -1
  87. package/dist/electron-build/bootstrap/context-hierarchy.js +13 -1
  88. package/dist/electron-build/bootstrap/context-hierarchy.js.map +1 -1
  89. package/dist/electron-build/electron-preload.js.map +1 -1
  90. package/dist/electron-build/external-engines/delegate.js.map +1 -1
  91. package/dist/electron-build/kernel/channel-actor.js +31 -2
  92. package/dist/electron-build/kernel/channel-actor.js.map +1 -1
  93. package/dist/electron-build/kernel/model-runtime.js +8 -1
  94. package/dist/electron-build/kernel/model-runtime.js.map +1 -1
  95. package/dist/electron-build/llm/config-store.js +20 -0
  96. package/dist/electron-build/llm/config-store.js.map +1 -1
  97. package/dist/electron-build/llm/embedding-adapter.js +75 -0
  98. package/dist/electron-build/llm/embedding-adapter.js.map +1 -0
  99. package/dist/electron-build/llm/pi-ai.js +96 -35
  100. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  101. package/dist/electron-build/network/agent-network.js.map +1 -1
  102. package/dist/electron-build/orbitdb/memory-sync.js +76 -0
  103. package/dist/electron-build/orbitdb/memory-sync.js.map +1 -0
  104. package/dist/electron-build/pi-ecosystem-judgment/adaptive-scan.js.map +1 -1
  105. package/dist/electron-build/security/tool-gate.js +2 -0
  106. package/dist/electron-build/security/tool-gate.js.map +1 -1
  107. package/dist/electron-build/setup/setup-store.js +34 -6
  108. package/dist/electron-build/setup/setup-store.js.map +1 -1
  109. package/dist/electron-build/social/heartbeat.js +18 -5
  110. package/dist/electron-build/social/heartbeat.js.map +1 -1
  111. package/dist/electron-build/utils/update-manager.js.map +1 -1
  112. package/dist/index.js +250 -12
  113. package/dist/ios/bolloon-web.json +2 -2
  114. package/dist/ios/client.js +74 -35
  115. package/dist/ios/icons/icon-1400x1400.png +0 -0
  116. package/dist/ios/icons/icon-1400x1400.webp +0 -0
  117. package/dist/ios/index.html +106 -107
  118. package/dist/ios/mobile-agent.js +346 -45
  119. package/dist/ios/mobile-core.js +7303 -4590
  120. package/dist/ios/mobile-harness.js +1004 -0
  121. package/dist/ios/mobile-orbitdb.js +194 -0
  122. package/dist/ios/mobile-persona.js +80 -0
  123. package/dist/ios/mobile-privacy.js +2 -3
  124. package/dist/ios/mobile.css +286 -21
  125. package/dist/ios/mobile.html +106 -107
  126. package/dist/ios/mobile.js +300 -132
  127. package/dist/ios/routes-world.js +4 -1
  128. package/dist/ios/server.js +3 -1
  129. package/dist/ios/style.css +13 -5
  130. package/dist/ios/ui/message-renderer.js +4 -18
  131. package/dist/ios/ui/step-timeline.js +14 -1
  132. package/dist/kernel/channel-actor.js +30 -2
  133. package/dist/kernel/gate-scan.js +1 -1
  134. package/dist/kernel/index.js +33 -0
  135. package/dist/kernel/model-runtime.js +8 -1
  136. package/dist/kernel/plan.js +3 -3
  137. package/dist/kernel/roster.js +3 -3
  138. package/dist/llm/config-store.js +20 -0
  139. package/dist/llm/embedding-adapter.js +69 -0
  140. package/dist/llm/pi-ai.js +94 -34
  141. package/dist/orbitdb/memory-sync.js +70 -0
  142. package/dist/pi-ecosystem-judgment/adaptive-scan.js +1 -1
  143. package/dist/security/tool-gate.js +2 -0
  144. package/dist/setup/setup-store.js +34 -6
  145. package/dist/social/heartbeat.js +18 -5
  146. package/dist/web/bolloon-web.json +3 -3
  147. package/dist/web/client.js +74 -35
  148. package/dist/web/icons/icon-1400x1400.png +0 -0
  149. package/dist/web/icons/icon-1400x1400.webp +0 -0
  150. package/dist/web/index.html +37 -25
  151. package/dist/web/mobile-agent.js +351 -45
  152. package/dist/web/mobile-contacts.js +0 -4
  153. package/dist/web/mobile-core.js +7430 -4590
  154. package/dist/web/mobile-harness.js +1004 -0
  155. package/dist/web/mobile-orbitdb.js +194 -0
  156. package/dist/web/mobile-persona.js +80 -0
  157. package/dist/web/mobile-privacy.js +2 -3
  158. package/dist/web/mobile.css +286 -21
  159. package/dist/web/mobile.html +121 -107
  160. package/dist/web/mobile.js +409 -136
  161. package/dist/web/routes-contacts.js +94 -1
  162. package/dist/web/routes-world.js +4 -1
  163. package/dist/web/server.js +4 -2
  164. package/dist/web/style.css +13 -5
  165. package/dist/web/sw.js +1 -1
  166. package/dist/web/ui/message-renderer.js +4 -18
  167. package/dist/web/ui/step-timeline.js +14 -1
  168. package/package.json +3 -2
  169. package/scripts/build-web.ts +16 -1
@@ -0,0 +1,359 @@
1
+ /**
2
+ * memory-store.ts — 结构化记忆条目 (P0, 2026-10-14, decentralized-memory-design §2)
3
+ *
4
+ * 与 memory-recall.ts (会话摘要召回) 互补: 摘要文件是「整段会话的压缩」,
5
+ * 这里是「结构化单条记忆」—— kind / importance / refs 证据链, 可单独召回/删除/固化。
6
+ *
7
+ * P0 范围 (小步, 零新依赖):
8
+ * - MemoryEntry 模型 + 原子 JSONL 持久化 (~/.bolloon/memory/<agentId>/entries.jsonl)
9
+ * - save (自动 importance 启发式打分) / recall (关键词 + importance 加权) / remove
10
+ * - 无 embedding (P1 接), 无 OrbitDB 复制 (P3 接)
11
+ *
12
+ * 借鉴 Hermes (decentralized-memory-design §1.5):
13
+ * - 写入落盘, prompt 注入走「每轮召回」快照, 不实时重建 (冻结快照 ≠ 实时写)
14
+ * - 条目摘要进上下文, 全文按需拉取 (progressive disclosure)
15
+ */
16
+ import * as fs from 'fs/promises';
17
+ import * as os from 'os';
18
+ import * as path from 'path';
19
+ import { createHash } from 'node:crypto';
20
+ import { cosineSimilarity } from '../llm/embedding-adapter.js';
21
+ const home = () => process.env.HOME || os.homedir() || '/tmp';
22
+ export function memoryEntriesPath(agentId, homeDir) {
23
+ const base = homeDir || home();
24
+ return path.join(base, '.bolloon', 'memory', agentId, 'entries.jsonl');
25
+ }
26
+ /** 空内容/纯空白 → 拒绝存 (与 evaluateQuality 同口径: 空串不是记忆) */
27
+ function validContent(content) {
28
+ return String(content || '').trim().length >= 2;
29
+ }
30
+ /** importance 启发式: 长度 + 特定信号词 (P0 无 LLM 打分, 不引依赖) */
31
+ export function heuristicImportance(content, kind = 'fact', manual) {
32
+ if (typeof manual === 'number')
33
+ return Math.min(1, Math.max(0, manual));
34
+ const t = String(content || '');
35
+ let score = 0.3; // 基线
36
+ if (t.length > 100)
37
+ score += 0.2; // 长记忆 = 更可能是重要细节
38
+ if (/喜欢|偏好|讨厌|禁止|必须|永远|从不|重要|注意|记住|约定/.test(t))
39
+ score += 0.25; // 用户强语义
40
+ if (kind === 'preference' || kind === 'relationship')
41
+ score += 0.2;
42
+ if (kind === 'skill-feedback')
43
+ score += 0.15;
44
+ return Math.min(1, score);
45
+ }
46
+ /**
47
+ * P2: 价值衰减 (decay) — 长期未被召回的条目降权标记, 不删除。
48
+ * 规则:
49
+ * - 固化条目 (crystallized) 豁免 (用户确认过/多次命中, 不淡忘)
50
+ * - 从 lastRecalledAt (未召回过 = createdAt) 起算, 超过 days 天 → decayed=true + importance×factor
51
+ * - 被召回 → decayed 撤销 (recall 里已处理)
52
+ * 纯函数: 不改 entry, 返回更新后的副本 (无变化返回原引用)。
53
+ */
54
+ export function applyDecay(entry, nowIso, opts = {}) {
55
+ if (entry.crystallized)
56
+ return entry; // 固化豁免
57
+ const days = opts.days ?? 30;
58
+ const factor = opts.factor ?? 0.5;
59
+ const base = entry.lastRecalledAt || entry.createdAt;
60
+ const ms = Date.parse(nowIso) - Date.parse(base);
61
+ if (!Number.isFinite(ms) || ms < 0)
62
+ return entry;
63
+ const elapsedDays = ms / 86_400_000;
64
+ if (elapsedDays < days)
65
+ return entry;
66
+ // 幂等: 已淡化过就不再重复降 (decayed 标记本身是终态)
67
+ if (entry.decayed)
68
+ return entry;
69
+ return {
70
+ ...entry,
71
+ importance: Math.max(0.05, entry.importance * factor),
72
+ decayed: true,
73
+ updatedAt: nowIso,
74
+ };
75
+ }
76
+ /** P2: 全库跑一次 decay, 返回被淡化条数 (未调用过 save/recall 的旧条目也会被标) */
77
+ export function decayAll(entries, nowIso, opts = {}) {
78
+ let n = 0;
79
+ for (const [id, e] of entries) {
80
+ const after = applyDecay(e, nowIso, opts);
81
+ if (after !== e) {
82
+ entries.set(id, after);
83
+ n++;
84
+ }
85
+ }
86
+ return n;
87
+ }
88
+ /** 关键词提取 (与 memory-recall.tokenizeQuery 同口径: 中文 2-gram + 英文分词) */
89
+ function tokenize(text) {
90
+ const clean = String(text || '').toLowerCase().trim();
91
+ if (!clean)
92
+ return [];
93
+ const STOP = new Set(['的', '了', '是', '我', '你', '他', '她', '它', '我们', '你们', '在', '和', '与', '吗', '呢', '吧', '这', '那', '个', '请', '帮', '一下', '一个', '怎么', '如何', 'the', 'a', 'an', 'is', 'are', 'to', 'of', 'for', 'and']);
94
+ const out = [];
95
+ // 英文词
96
+ for (const w of clean.match(/[a-z][a-z0-9_]+/g) || []) {
97
+ if (!STOP.has(w))
98
+ out.push(w);
99
+ }
100
+ // 中文 2-gram
101
+ const cjk = (clean.match(/[\u4e00-\u9fff]+/g) || []).join('');
102
+ for (let i = 0; i < cjk.length - 1; i++) {
103
+ const g = cjk.slice(i, i + 2);
104
+ if (!STOP.has(g))
105
+ out.push(g);
106
+ }
107
+ return out;
108
+ }
109
+ /** 词 → 分数 (词面命中 1 + 干净长词加权, 不引 TF-IDF, P1 换向量) */
110
+ function scoreText(text, tokens) {
111
+ const t = String(text || '').toLowerCase();
112
+ let score = 0;
113
+ for (const tok of tokens) {
114
+ if (t.includes(tok))
115
+ score += tok.length >= 4 ? 1.2 : 1;
116
+ }
117
+ return score;
118
+ }
119
+ /**
120
+ * MemoryStore — 单 agent 的结构化记忆 (每实例对应一个 agentId, 不跨 agent)
121
+ *
122
+ * 文件锁: fcntl 不可用 (Windows) 时退化为无锁 (与 Hermes 同思路, 原子 rename 兜底)。
123
+ * 防并发写坏: 写入 → tmp 文件 → rename 原子替换。
124
+ */
125
+ export class MemoryStore {
126
+ agentId;
127
+ file;
128
+ entries = new Map();
129
+ constructor(agentId, homeDir) {
130
+ this.agentId = agentId;
131
+ this.file = memoryEntriesPath(agentId, homeDir);
132
+ }
133
+ async load() {
134
+ try {
135
+ const raw = await fs.readFile(this.file, 'utf8');
136
+ this.entries.clear();
137
+ for (const line of raw.split('\n')) {
138
+ if (!line.trim())
139
+ continue;
140
+ try {
141
+ const e = JSON.parse(line);
142
+ if (e?.id)
143
+ this.entries.set(e.id, e);
144
+ }
145
+ catch { /* 单行损坏跳过 (append-only, 不整文件作废) */ }
146
+ }
147
+ }
148
+ catch { /* 文件不存在 = 空记忆 */ }
149
+ }
150
+ async persist() {
151
+ const dir = path.dirname(this.file);
152
+ await fs.mkdir(dir, { recursive: true });
153
+ const tmp = `${this.file}.tmp-${process.pid}`;
154
+ const lines = [...this.entries.values()].map((e) => JSON.stringify(e));
155
+ await fs.writeFile(tmp, lines.join('\n') + (lines.length ? '\n' : ''));
156
+ await fs.rename(tmp, this.file); // 原子替换, 并发读不碰半截文件
157
+ }
158
+ async save(input, adapter) {
159
+ const content = String(input.content || '').trim();
160
+ if (!validContent(content))
161
+ throw new Error('记忆内容太短 (至少 2 个字符)');
162
+ const now = new Date().toISOString();
163
+ const id = createHash('sha256').update(`${this.agentId}|${content}`).digest('hex').slice(0, 32);
164
+ const existing = this.entries.get(id);
165
+ let vec = existing?.vec;
166
+ if (adapter && adapter.provider !== 'none') {
167
+ try {
168
+ vec = await adapter.embed(content); // 内容变 → 重算; 内容同 → 复用旧向量
169
+ }
170
+ catch {
171
+ // 向量化失败不阻塞保存 (记忆仍存, 召回走关键词)
172
+ }
173
+ }
174
+ const entry = {
175
+ id,
176
+ agentId: this.agentId,
177
+ content,
178
+ kind: input.kind || existing?.kind || 'fact',
179
+ importance: heuristicImportance(content, input.kind || existing?.kind, input.importance),
180
+ createdAt: existing?.createdAt || now,
181
+ updatedAt: now,
182
+ source: input.source || existing?.source || 'manual',
183
+ refs: input.refs || existing?.refs || [],
184
+ crystallized: existing?.crystallized || false,
185
+ recallCount: existing?.recallCount || 0,
186
+ lastRecalledAt: existing?.lastRecalledAt,
187
+ decayed: existing?.decayed,
188
+ vec,
189
+ };
190
+ this.entries.set(id, entry);
191
+ await this.persist();
192
+ return entry;
193
+ }
194
+ /**
195
+ * 召回:
196
+ * - queryVec 提供 且 条目有 vec → cosine 相似度 (语义召回), 按 importance 加权
197
+ * - 否则 → 关键词命中 (P0 行为), 完全兼容旧调用
198
+ * 命中后 recallCount++ 并落盘 (动态升权)
199
+ */
200
+ async recall(opts) {
201
+ if (this.entries.size === 0)
202
+ return [];
203
+ const limit = opts.limit || 3;
204
+ const hits = [];
205
+ const qv = opts.queryVec;
206
+ const useVec = !!qv && qv.length > 0;
207
+ if (useVec) {
208
+ const minCosine = opts.minCosine ?? 0;
209
+ for (const e of this.entries.values()) {
210
+ if (!e.vec || e.vec.length !== qv.length)
211
+ continue; // 无向量的旧条目跳过
212
+ const c = cosineSimilarity(qv, e.vec);
213
+ if (c < minCosine)
214
+ continue;
215
+ const weighted = c * (0.4 + 0.6 * e.importance); // importance 加权
216
+ if (weighted > 0)
217
+ hits.push({ entry: e, score: weighted });
218
+ }
219
+ }
220
+ else {
221
+ const tokens = tokenize(opts.query);
222
+ if (tokens.length === 0)
223
+ return [];
224
+ const minScore = opts.minScore ?? 0;
225
+ for (const e of this.entries.values()) {
226
+ const s = scoreText(e.content, tokens);
227
+ if (s <= minScore)
228
+ continue;
229
+ const weighted = Math.min(5, s) * (0.4 + 0.6 * e.importance); // importance 加权
230
+ hits.push({ entry: e, score: weighted });
231
+ }
232
+ }
233
+ hits.sort((a, b) => b.score - a.score);
234
+ const top = hits.slice(0, limit);
235
+ // 命中 → recallCount++ + lastRecalledAt 刷新 (动态升权 / decay 重置), 每 10 次 importance +0.05
236
+ if (top.length > 0) {
237
+ const nowIso = new Date().toISOString();
238
+ let changed = false;
239
+ for (const h of top) {
240
+ h.entry.recallCount++;
241
+ h.entry.lastRecalledAt = nowIso;
242
+ if (h.entry.decayed)
243
+ h.entry.decayed = false; // 被召回 → 淡化撤销
244
+ if (h.entry.recallCount % 10 === 0 && h.entry.importance < 1) {
245
+ h.entry.importance = Math.min(1, h.entry.importance + 0.05);
246
+ }
247
+ changed = true;
248
+ }
249
+ if (changed)
250
+ await this.persist().catch(() => { });
251
+ }
252
+ return top;
253
+ }
254
+ /** P2: 固化 — 多次召回或用户确认后调用; crystallized=true (进 08-Insights 由调用方负责) */
255
+ async crystallize(id) {
256
+ const e = this.entries.get(id);
257
+ if (!e)
258
+ return null;
259
+ const changed = !e.crystallized;
260
+ e.crystallized = true;
261
+ e.importance = Math.min(1, e.importance + 0.1); // 固化 → importance 加成
262
+ e.updatedAt = new Date().toISOString();
263
+ if (changed)
264
+ await this.persist();
265
+ return e;
266
+ }
267
+ /** P2: 自动固化阈值 — 召回 ≥ 5 次且 importance ≥ 0.6 的条目 (recall 后可调用, 返回新固化条数) */
268
+ async autoCrystallize() {
269
+ let n = 0;
270
+ for (const e of this.entries.values()) {
271
+ if (!e.crystallized && e.recallCount >= 5 && e.importance >= 0.6) {
272
+ e.crystallized = true;
273
+ e.importance = Math.min(1, e.importance + 0.1);
274
+ e.updatedAt = new Date().toISOString();
275
+ n++;
276
+ }
277
+ }
278
+ if (n > 0)
279
+ await this.persist();
280
+ return n;
281
+ }
282
+ /** P2: 统计 (decay 标定 / 审计用) */
283
+ stats() {
284
+ let crystallized = 0;
285
+ let decayed = 0;
286
+ for (const e of this.entries.values()) {
287
+ if (e.crystallized)
288
+ crystallized++;
289
+ if (e.decayed)
290
+ decayed++;
291
+ }
292
+ return { total: this.entries.size, crystallized, decayed };
293
+ }
294
+ /**
295
+ * P3: 合并远端条目 (OrbitDB 拉回的) 到本地, LWW (updatedAt 大者胜)。
296
+ * 返回被采纳的远端条数; 有采纳则落盘。同步前本地必须先 load()。
297
+ */
298
+ async mergeRemote(remote) {
299
+ let n = 0;
300
+ for (const r of remote) {
301
+ if (!r || !r.id || r.agentId !== this.agentId)
302
+ continue; // 归属校验: 只合并本 agent
303
+ const l = this.entries.get(r.id);
304
+ if (!l) {
305
+ this.entries.set(r.id, r);
306
+ n++;
307
+ }
308
+ else if (r.updatedAt > l.updatedAt) {
309
+ this.entries.set(r.id, r);
310
+ n++;
311
+ }
312
+ // 平手 / 本地更新 → 本地胜 (LWW)
313
+ }
314
+ if (n > 0)
315
+ await this.persist();
316
+ return n;
317
+ }
318
+ /** P3: 导出一份「可在其他节点重建」的条目快照 (不含运行时计数, 只含持久事实) */
319
+ exportEntries() {
320
+ return [...this.entries.values()].map((e) => ({ ...e, vec: undefined })); // vec 不跨节点 (维度依赖 adapter)
321
+ }
322
+ async remove(id) {
323
+ const existed = this.entries.delete(id);
324
+ if (existed)
325
+ await this.persist();
326
+ return existed;
327
+ }
328
+ async list(limit = 50) {
329
+ return [...this.entries.values()]
330
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
331
+ .slice(0, limit);
332
+ }
333
+ }
334
+ /** 单例 (per-agent 分桶, 与 getContextStore 同模式); 测试可传 homeDir */
335
+ const stores = new Map();
336
+ export function getMemoryStore(agentId, homeDir) {
337
+ const key = `${homeDir || home()}|${agentId}`;
338
+ let s = stores.get(key);
339
+ if (!s) {
340
+ s = new MemoryStore(agentId, homeDir);
341
+ stores.set(key, s);
342
+ }
343
+ return s;
344
+ }
345
+ /** 召回 → 拼注入文本 (progressive disclosure: 只带摘要级内容 + id, 全文按需 load) */
346
+ export function formatRecallBlock(hits) {
347
+ if (!hits.length)
348
+ return '';
349
+ const lines = hits.map((h, i) => {
350
+ const e = h.entry;
351
+ const tag = `${e.kind}${e.crystallized ? '★' : ''}`;
352
+ return `[${i + 1}] (${tag}, 权重 ${e.importance.toFixed(2)}) ${e.content.slice(0, 200)}`;
353
+ });
354
+ return `<memory-context>\n${lines.join('\n')}\n</memory-context>`;
355
+ }
356
+ /** 测试辅助: 清空分桶 (隔离 HOME 时用) */
357
+ export function resetMemoryStores() {
358
+ stores.clear();
359
+ }
@@ -158,8 +158,47 @@ export async function searchOpportunitiesForTopic(topic, opts = {}) {
158
158
  if (added >= limit)
159
159
  break;
160
160
  }
161
+ // 2026-10-05 降级 (leo: 输入后要足够多机会): 引擎被限流/返回空时,
162
+ // 从已有搜索库按关键词相关度返回匹配旧结果 — 保证输入后总有内容 (不假装, 都是真 URL)。
163
+ if (added === 0) {
164
+ try {
165
+ const existing = await listSearchOpportunities();
166
+ const topicTags = buildTags(topic);
167
+ const related = existing
168
+ .map((o) => {
169
+ const tags = buildTags(`${o.title} ${o.summary} ${o.query || ''}`);
170
+ const hit = topicTags.filter((t) => tags.includes(t)).length;
171
+ return { o, hit };
172
+ })
173
+ .filter((x) => x.hit > 0)
174
+ .sort((a, b) => b.hit - a.hit)
175
+ .slice(0, limit);
176
+ for (const { o } of related) {
177
+ results.push({ ...o, query: `${topic} (相关)` });
178
+ }
179
+ if (results.length > 0) {
180
+ return { ok: true, added: 0, total: results.length, results, error: '引擎限流, 已返回相关旧结果' };
181
+ }
182
+ }
183
+ catch { /* 降级失败不影响 */ }
184
+ }
161
185
  return { ok: true, added, total: results.length, results };
162
186
  }
187
+ /** 提取关键词标签 (分词: ASCII 词 + CJK 二元片) — 用于相关度匹配 (导出供测试) */
188
+ export function buildTags(text) {
189
+ const src = String(text || '').toLowerCase();
190
+ const words = src.match(/[a-z][a-z0-9_-]{2,}/g) || [];
191
+ const cjk = src.match(/[\u4e00-\u9fff]+/g) || [];
192
+ const cjkBigrams = cjk.flatMap((s) => {
193
+ if (s.length <= 2)
194
+ return [s];
195
+ const out = [];
196
+ for (let i = 0; i + 1 < s.length; i += 1)
197
+ out.push(s.slice(i, i + 2));
198
+ return out;
199
+ });
200
+ return Array.from(new Set([...words, ...cjkBigrams]));
201
+ }
163
202
  /** 列出已搜索到的机会 (最近在前) */
164
203
  export async function listSearchOpportunities() {
165
204
  try {
@@ -23,7 +23,7 @@ import * as path from 'path';
23
23
  import * as os from 'os';
24
24
  import { EventEmitter } from 'events';
25
25
  import { irohTransport as defaultIrohTransport } from '../network/iroh-transport.js';
26
- import { getRelevantValues, getValueProfile, loadAllJudgments } from '../pi-ecosystem-judgment/human-value-store.js';
26
+ import { getValueProfile, loadAllJudgments } from '../pi-ecosystem-judgment/human-value-store.js';
27
27
  function homeDir() {
28
28
  return process.env.HOME || process.env.USERPROFILE || os.homedir();
29
29
  }
@@ -189,7 +189,6 @@ export async function getInbox(peerDID) {
189
189
  async function buildValueHint(text) {
190
190
  try {
191
191
  const allJudgments = await loadAllJudgments();
192
- const relevant = await getRelevantValues(text, undefined);
193
192
  const profile = await getValueProfile('me');
194
193
  const profileHint = [
195
194
  `quality_focus=${profile.quality_focus.toFixed(2)}`,
@@ -522,21 +522,19 @@ export function parseToolCall(content, ctx) {
522
522
  return null;
523
523
  }
524
524
  /**
525
- * isFinalResponse — 临时判断 LLM 是否已完结 (含 <final gen>)
526
- * 2026-06-19/06-30 修: 先剥离 think, 避免思考块中提及 <final gen> 误杀
527
- * 含可解析 tool_call 则优先执行工具, 不算 final
525
+ * isFinalResponse — 判断 LLM 是否已完结。
526
+ * 2026-10-12 (leo 拍板: 全项目去掉 <final gen> 截止符): 旧实现只有在回复里找到
527
+ * `<final gen>` 标记才返回 true —— 模型中间轮次 echo 该文本就误判提前截止。
528
+ * 现在: 剥掉 think 块后**没有可执行工具调用** = 这一轮就是最终答复。
529
+ * (工具调用优先: 含可解析 tool_call 不算 final, 先执行工具。)
528
530
  */
529
531
  export function isFinalResponse(content, ctx) {
530
- const stripped = content.replace(/<think[\s\S]*?<\/think/g, '');
531
- if (!stripped.includes('<final gen>'))
532
- return false;
533
- // 用默认 ctx (无需 tool 名解析, 只看能否 parse 出来) - 但 isFinalResponse 不需要 tools 列表上下文.
534
- // 如果没有传 ctx, 使用 empty set (只解析不 resolve).
532
+ const stripped = content.replace(/<think[\s\S]*?<\/think>/g, '');
533
+ // 与 react-loop 的判定链一致: 有可解析的工具调用 → 优先执行工具, 不算 final
535
534
  if (ctx && parseToolCall(stripped, ctx))
536
535
  return false;
537
536
  if (!ctx) {
538
537
  // 试探性 parse (不 resolve name) — 如果至少能提取 name+args 模式, 就不是 final.
539
- // 这里粗略检测: stripped 中含 json "name": 或 <invoke name= 或 <function_calls> 任一
540
538
  if (/\{[\s\S]*?"name"\s*:/.test(stripped))
541
539
  return false;
542
540
  if (/<invoke\s+name=/.test(stripped))
@@ -546,23 +544,12 @@ export function isFinalResponse(content, ctx) {
546
544
  }
547
545
  return true;
548
546
  }
549
- /** extractFinalAnswer: 去除 <final gen> 标记 + tool call 噪声. */
547
+ /** extractFinalAnswer: 去除 tool call 噪声. (2026-10-12: <final gen> 标记移除, 不再剥除) */
550
548
  export function extractFinalAnswer(content) {
551
- const marker = '<final gen>';
552
- const markerIndex = content.indexOf(marker);
553
- if (markerIndex !== -1) {
554
- const after = content.substring(markerIndex + marker.length).trim();
555
- if (after) {
556
- content = after;
557
- }
558
- else {
559
- content = content.substring(0, markerIndex).trim();
560
- }
561
- }
562
549
  let cleaned = content
563
550
  .replace(/调用工具[::]\s*\w+\s*\([^)]*\)/g, '')
564
551
  .replace(/使用工具[::]\s*\w+\s*\([^)]*\)/g, '')
565
- .replace(/tool[_\w]*[::]\s*\w+\s*\([^)]*\)/gi, '')
552
+ .replace(/tool[_\\w]*[::]\s*\w+\s*\([^)]*\)/gi, '')
566
553
  .trim();
567
554
  return cleaned;
568
555
  }
@@ -29,7 +29,6 @@ export class PiAgentHarness {
29
29
  deps;
30
30
  failClosed;
31
31
  modelCalls = 0;
32
- degradedOnce = new Set();
33
32
  constructor(deps) {
34
33
  this.deps = deps;
35
34
  this.failClosed = deps.failClosed !== false;
@@ -256,7 +255,7 @@ export class PiAgentHarness {
256
255
  this.emit({ event: 'review', kind: 'error', failureKind: 'goal_review', reason: `审查器失效: ${err?.message}`, runId: state.runId, goalId: state.goalId });
257
256
  return {
258
257
  kind: 'continue-review',
259
- hint: `[目标审查降级] 审查器执行失败 (${String(err?.message || err).slice(0, 120)}), 按"未确认完成"处理: 请再次核对用户需求是否真的全部满足, 并列出证据。确认完成后再加 <final gen>。`,
258
+ hint: `[目标审查降级] 审查器执行失败 (${String(err?.message || err).slice(0, 120)}), 按"未确认完成"处理: 请再次核对用户需求是否真的全部满足, 并列出证据。确认完成后再给出最终回答。`,
260
259
  };
261
260
  }
262
261
  }