@bolloon/bolloon-agent 0.3.7 → 0.3.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/README.md CHANGED
@@ -129,6 +129,37 @@ bolloon --help # 所有命令
129
129
 
130
130
  详细打包 / 桌面 / iOS 流程见 [docs/BUILD.md](docs/BUILD.md).
131
131
 
132
+ ### 从 npm / GitHub 安装(终端用户)
133
+
134
+ Bolloon 已发布到 npm,也可通过 GitHub 一键脚本安装(无需克隆仓库)。
135
+
136
+ **方式一:npm(各系统通用,需先装 Node.js LTS)**
137
+
138
+ ```bash
139
+ npm install -g @bolloon/bolloon-agent
140
+ bolloon --version
141
+ ```
142
+
143
+ **方式二:GitHub 一键脚本(macOS / Linux)**
144
+
145
+ ```bash
146
+ curl -fsSL https://raw.githubusercontent.com/logos-42/bolloon/master/scripts/install.sh | sh
147
+ ```
148
+
149
+ 脚本优先从 GitHub Releases 下载预编译包,未提供对应平台包时自动回退 `npm install -g`。
150
+
151
+ **各系统安装 Node.js(若尚未安装)**
152
+
153
+ | 系统 | 命令 |
154
+ |------|------|
155
+ | macOS(Homebrew) | `brew install node` |
156
+ | Linux(apt) | `sudo apt update && sudo apt install -y nodejs npm` |
157
+ | Linux(dnf / yum) | `sudo dnf install -y nodejs npm` |
158
+ | Windows(winget) | `winget install -e --id OpenJS.NodeJS.LTS` |
159
+ | Windows(Chocolatey) | `choco install nodejs` |
160
+
161
+ > 说明:bolloon 暂未上架 apt / yum / brew / winget / choco 原生仓库,统一经 npm 安装;上表命令仅用于安装 Node.js 运行时。
162
+
132
163
  ---
133
164
 
134
165
  ## 一、项目概述
@@ -0,0 +1,457 @@
1
+ /**
2
+ * agent-heartbeat.ts — 智能体社交心跳 + 生命周期 (2026-07-21, 重构于 2026-07-21)
3
+ *
4
+ * 目的: 让本地智能体拥有"心跳", 周期性宣告存活/能力, 并自主决定跟哪个远端智能体发起对话,
5
+ * 形成本地↔远端智能体的顺畅自动交流.
6
+ *
7
+ * ⚠️ 设计核心: 智能体生命周期 (避免"一直社交却毫无效果")
8
+ * 社交不是目的, 而是达成"目标"的手段. 因此本模块以一个 **目标驱动的状态机** 管理智能体:
9
+ *
10
+ * BOOTSTRAP ──start()──▶ DISCOVERING ──有存活 peer──▶ ENGAGING
11
+ * ▲ │ │
12
+ * │ │ 无目标/无存活 peer │ 目标达成 / 配额耗尽 / 无效果退避
13
+ * │ ▼ ▼
14
+ * └───────────────────── RESTING ◀─────────────────────┘
15
+ * │ (goalReevalMs 后重新评估, 重置配额再试一轮)
16
+ * ▼
17
+ * ENGAGING (新一轮)
18
+ *
19
+ * - 每个目标有配额 (maxInitiations) 与效果阈值 (effectThreshold): 达成即 RESTING, 不再闲聊.
20
+ * - 若连续多次发起却"毫无效果" (noEffectWindowMs 内无有效回复), 进入退避 RESTING (noEffectBackoffMs),
21
+ * 防止无限互 ping / 烧 LLM.
22
+ * - RESTING 不是消失: beacon 仍在发, 对端依旧能看到本智能体在线; 只是停止主动社交.
23
+ * - goalReevalMs 之后会重新评估目标 (重置配额) 再给一轮机会, 让智能体"活着"但不失控.
24
+ * - pause()/resume()/stop() 提供运行期控制; stop() 会清理全部定时器 (供全局 runtime 优雅关闭).
25
+ *
26
+ * 设计原则 (compile-first / 可测):
27
+ * - transport / decide / getPeers / self / getGoal / assessEffect 全部可注入, 不依赖真实网络或 LLM.
28
+ * - 生产环境: transport = p2p-outbox.sendOrQueue, decide = 本地 LLM 决策, getPeers = known_peers + remoteChannelCache.
29
+ * - 测试环境: 全部用 mock, 验证"beacon → 自主发起 → 远端回复(效果) → 目标达成 → RESTING".
30
+ *
31
+ * 协议 (复用 v3 P2P 的 {v:3, op, payload} 信封):
32
+ * - agent.heartbeat : 轻量 beacon, payload = {fromPublicKey, agentId, name, channels, ts}
33
+ * - agent.chat.send : 本地 agent 自主发起 (已有远端唤醒链路, server.ts:529 处理)
34
+ * - agent.chat.reply : 远端回复发回 (已有 SSE 链路, server.ts:1494 处理)
35
+ *
36
+ * 与全局 runtime 生命周期的集成:
37
+ * - stop() 清理定时器, 供 server.ts 的 cleanupAndExit (SIGTERM/SIGINT) 调用.
38
+ * - onActivity 回调喂给 Watchdog.recordActivity, 避免 24h 看门狗误判卡死.
39
+ * - 实例注册到 global.socialHeartbeat / global.agentHeartbeat, 供 HealthMonitor.checkHeartbeat 观测.
40
+ * - 暴露 getDiscoveredAgents() / isAntColonyEnabled() 兼容 HealthMonitor 契约.
41
+ */
42
+ const DEFAULTS = {
43
+ beaconIntervalMs: 30_000,
44
+ socialIntervalMs: 120_000,
45
+ cooldownMs: 10 * 60_000,
46
+ liveWindowMs: 3 * 30_000,
47
+ minAttemptsBeforeBackoff: 3,
48
+ noEffectWindowMs: 10 * 60_000,
49
+ noEffectBackoffMs: 30 * 60_000,
50
+ backoffFactor: 2,
51
+ maxSocialIntervalMs: 30 * 60_000,
52
+ goalReevalMs: 60 * 60_000,
53
+ };
54
+ const MAX_BACKOFF_LEVEL = 6;
55
+ export class AgentHeartbeat {
56
+ opts;
57
+ peerLiveness = new Map();
58
+ lastInitiated = new Map();
59
+ beaconTimer = null;
60
+ socialTimer = null;
61
+ started = false;
62
+ // === 生命周期状态 ===
63
+ phase = 'BOOTSTRAP';
64
+ goalRT = null;
65
+ /** 内置兜底目标 (无 getGoal 时使用, 带配额, 保证不会一直社交) */
66
+ builtinGoalRT = null;
67
+ backoffLevel = 0;
68
+ noEffectBackoffUntil = 0;
69
+ constructor(options) {
70
+ this.opts = {
71
+ self: options.self,
72
+ getPeers: options.getPeers,
73
+ transport: options.transport,
74
+ decide: options.decide,
75
+ getGoal: options.getGoal,
76
+ assessEffect: options.assessEffect,
77
+ onReply: options.onReply,
78
+ onPeerAlive: options.onPeerAlive,
79
+ onActivity: options.onActivity,
80
+ onLifecycleChange: options.onLifecycleChange,
81
+ beaconIntervalMs: options.beaconIntervalMs ?? DEFAULTS.beaconIntervalMs,
82
+ socialIntervalMs: options.socialIntervalMs ?? DEFAULTS.socialIntervalMs,
83
+ cooldownMs: options.cooldownMs ?? DEFAULTS.cooldownMs,
84
+ liveWindowMs: options.liveWindowMs ?? DEFAULTS.liveWindowMs,
85
+ minAttemptsBeforeBackoff: options.minAttemptsBeforeBackoff ?? DEFAULTS.minAttemptsBeforeBackoff,
86
+ noEffectWindowMs: options.noEffectWindowMs ?? DEFAULTS.noEffectWindowMs,
87
+ noEffectBackoffMs: options.noEffectBackoffMs ?? DEFAULTS.noEffectBackoffMs,
88
+ backoffFactor: options.backoffFactor ?? DEFAULTS.backoffFactor,
89
+ maxSocialIntervalMs: options.maxSocialIntervalMs ?? DEFAULTS.maxSocialIntervalMs,
90
+ goalReevalMs: options.goalReevalMs ?? DEFAULTS.goalReevalMs,
91
+ enabled: options.enabled ?? true,
92
+ socialEnabled: options.socialEnabled ?? (options.enabled ?? true),
93
+ };
94
+ }
95
+ isEnabled() {
96
+ return this.opts.enabled;
97
+ }
98
+ isSocialEnabled() {
99
+ return this.opts.enabled && this.opts.socialEnabled;
100
+ }
101
+ // ===================== 启动 / 停止 / 暂停 =====================
102
+ start() {
103
+ if (this.started || !this.opts.enabled)
104
+ return;
105
+ this.started = true;
106
+ this.setPhase('DISCOVERING');
107
+ this.beaconTimer = setInterval(() => {
108
+ this.tickBeacon().catch((e) => console.warn('[heartbeat] beacon tick 失败:', e?.message));
109
+ }, this.opts.beaconIntervalMs);
110
+ if (this.isSocialEnabled()) {
111
+ this.scheduleSocial();
112
+ }
113
+ // 立即发一次 beacon, 让对端尽快看到自己
114
+ this.tickBeacon().catch(() => { });
115
+ console.log(`[heartbeat] 社交心跳已启动 (beacon=${this.opts.beaconIntervalMs}ms` +
116
+ `${this.isSocialEnabled() ? `, social=${this.opts.socialIntervalMs}ms, cooldown=${this.opts.cooldownMs}ms` : ', social=关闭'} )`);
117
+ }
118
+ /** 优雅停止: 清理全部定时器 (供全局 runtime 的 SIGTERM/SIGINT 清理调用) */
119
+ stop() {
120
+ if (this.beaconTimer)
121
+ clearInterval(this.beaconTimer);
122
+ if (this.socialTimer)
123
+ clearTimeout(this.socialTimer);
124
+ this.beaconTimer = null;
125
+ this.socialTimer = null;
126
+ this.started = false;
127
+ this.setPhase('PAUSED');
128
+ console.log('[heartbeat] 社交心跳已停止 (定时器已清理)');
129
+ }
130
+ /** 暂停社交循环 (beacon 仍发, 智能体依旧在线可见, 只是停止主动聊天) */
131
+ pause() {
132
+ if (!this.started)
133
+ return;
134
+ if (this.socialTimer)
135
+ clearTimeout(this.socialTimer);
136
+ this.socialTimer = null;
137
+ this.setPhase('PAUSED');
138
+ console.log('[heartbeat] 社交循环已暂停 (仅保留 beacon)');
139
+ }
140
+ /** 从 PAUSED 恢复社交循环 */
141
+ resume() {
142
+ if (!this.started) {
143
+ this.start();
144
+ return;
145
+ }
146
+ if (this.phase !== 'PAUSED')
147
+ return;
148
+ this.setPhase('DISCOVERING');
149
+ if (this.isSocialEnabled())
150
+ this.scheduleSocial();
151
+ console.log('[heartbeat] 社交循环已恢复');
152
+ }
153
+ // ===================== beacon =====================
154
+ /** 周期性 beacon: 向每个已知 peer 宣告存活 + 自身渠道/能力 */
155
+ async tickBeacon() {
156
+ const self = await this.opts.self();
157
+ const peers = await this.opts.getPeers();
158
+ const payload = {
159
+ fromPublicKey: self.publicKey,
160
+ agentId: self.agentId,
161
+ name: self.name,
162
+ channels: self.channels,
163
+ ts: Date.now(),
164
+ };
165
+ for (const p of peers) {
166
+ if (p.publicKey === self.publicKey)
167
+ continue;
168
+ const r = await this.opts.transport
169
+ .send(p.publicKey, 'agent.heartbeat', payload)
170
+ .catch(() => 'FAILED');
171
+ if (r !== 'FAILED')
172
+ this.peerLiveness.set(p.publicKey, Date.now());
173
+ }
174
+ }
175
+ // ===================== 社交决策 tick (生命周期感知) =====================
176
+ /** 自适应 social 间隔 (退避时指数增长, 有上限) */
177
+ currentSocialInterval() {
178
+ const mult = Math.pow(this.opts.backoffFactor, this.backoffLevel);
179
+ return Math.min(this.opts.socialIntervalMs * mult, this.opts.maxSocialIntervalMs);
180
+ }
181
+ scheduleSocial() {
182
+ if (!this.started || !this.isSocialEnabled() || this.phase === 'PAUSED') {
183
+ this.socialTimer = null;
184
+ return;
185
+ }
186
+ this.socialTimer = setTimeout(() => {
187
+ this.tickSocial()
188
+ .catch((e) => console.warn('[heartbeat] social tick 失败:', e?.message))
189
+ .finally(() => {
190
+ if (this.started && this.phase !== 'PAUSED')
191
+ this.scheduleSocial();
192
+ });
193
+ }, this.currentSocialInterval());
194
+ }
195
+ /** 社交决策 tick: 先评估生命周期, 再决定是否对存活 peer 发起对话 */
196
+ async tickSocial() {
197
+ this.opts.onActivity?.();
198
+ const self = await this.opts.self();
199
+ const allPeers = (await this.opts.getPeers()).filter((p) => p.publicKey !== self.publicKey);
200
+ const now = Date.now();
201
+ const livePeers = allPeers.filter((p) => {
202
+ const seen = this.peerLiveness.get(p.publicKey) ?? p.lastSeen ?? 0;
203
+ return now - seen <= this.opts.liveWindowMs;
204
+ });
205
+ const goalRT = await this.resolveGoal();
206
+ // —— 生命周期评估: 决定本 tick 进入哪个阶段 ——
207
+ const evalResult = this.evaluateLifecycle(goalRT, livePeers.length, now);
208
+ if (evalResult.backoff) {
209
+ this.backoffLevel = Math.min(this.backoffLevel + 1, MAX_BACKOFF_LEVEL);
210
+ this.noEffectBackoffUntil = now + this.opts.noEffectBackoffMs;
211
+ }
212
+ if (evalResult.resetBackoff) {
213
+ this.backoffLevel = 0;
214
+ this.noEffectBackoffUntil = 0;
215
+ }
216
+ this.setPhase(evalResult.nextPhase);
217
+ if (this.phase === 'RESTING' || this.phase === 'PAUSED') {
218
+ console.log(`[heartbeat] 生命周期=${this.phase}, 跳过本次社交` +
219
+ `${this.noEffectBackoffUntil > now ? ` (无效果退避至 ${new Date(this.noEffectBackoffUntil).toLocaleTimeString()})` : ''}` +
220
+ ` (goal=${goalRT?.goal.description ?? '无'})`);
221
+ this.emitLifecycle();
222
+ return;
223
+ }
224
+ if (!this.opts.decide) {
225
+ this.setPhase('DISCOVERING');
226
+ this.emitLifecycle();
227
+ return;
228
+ }
229
+ if (livePeers.length === 0) {
230
+ this.setPhase('DISCOVERING');
231
+ console.log('[heartbeat] social tick: 没有存活 peer, 仅保持发现');
232
+ this.emitLifecycle();
233
+ return;
234
+ }
235
+ this.setPhase('ENGAGING');
236
+ const decision = await this.opts.decide({ self, peers: livePeers, goal: goalRT?.goal }).catch(() => ({ initiate: false }));
237
+ // 决策时即可声明目标达成
238
+ if (decision?.goalAchieved && goalRT) {
239
+ goalRT.achieved = true;
240
+ this.setPhase('RESTING');
241
+ console.log(`[heartbeat] 决策判定目标已达成 → RESTING: ${goalRT.goal.description}`);
242
+ this.emitLifecycle();
243
+ return;
244
+ }
245
+ if (!decision?.initiate) {
246
+ this.emitLifecycle();
247
+ return;
248
+ }
249
+ const { targetPeerPublicKey, targetChannelId, message } = decision;
250
+ if (!targetPeerPublicKey || !targetChannelId || !message) {
251
+ this.emitLifecycle();
252
+ return;
253
+ }
254
+ // 冷却: 避免对同一 peer 刷屏 / 无限互 ping
255
+ const last = this.lastInitiated.get(targetPeerPublicKey) ?? 0;
256
+ if (now - last < this.opts.cooldownMs) {
257
+ this.emitLifecycle();
258
+ return;
259
+ }
260
+ // 目标配额: 本目标已发起够多次则进入 RESTING
261
+ if (goalRT && goalRT.initiationsUsed >= goalRT.goal.maxInitiations) {
262
+ this.setPhase('RESTING');
263
+ console.log(`[heartbeat] 目标配额用尽 (${goalRT.initiationsUsed}/${goalRT.goal.maxInitiations}) → RESTING`);
264
+ this.emitLifecycle();
265
+ return;
266
+ }
267
+ const r = await this.opts.transport
268
+ .send(targetPeerPublicKey, 'agent.chat.send', {
269
+ channelId: targetChannelId,
270
+ text: message,
271
+ fromPublicKey: self.publicKey,
272
+ })
273
+ .catch(() => 'FAILED');
274
+ if (r !== 'FAILED') {
275
+ this.lastInitiated.set(targetPeerPublicKey, now);
276
+ if (goalRT) {
277
+ goalRT.initiationsUsed++;
278
+ goalRT.lastInitiateAt = now;
279
+ }
280
+ console.log(`[heartbeat] 主动发起对话 → ${targetPeerPublicKey.slice(0, 8)}… (channel=${targetChannelId}): "${message.slice(0, 40)}..."` +
281
+ `${goalRT ? ` [目标 ${goalRT.initiationsUsed}/${goalRT.goal.maxInitiations}]` : ''}`);
282
+ }
283
+ else {
284
+ console.warn(`[heartbeat] 主动发起失败 (peer ${targetPeerPublicKey.slice(0, 8)}… 不在线?)`);
285
+ }
286
+ this.emitLifecycle();
287
+ }
288
+ // ===================== 生命周期评估 =====================
289
+ makeGoalRuntime(g) {
290
+ return {
291
+ goal: { ...g, createdAt: g.createdAt ?? Date.now() },
292
+ initiationsUsed: 0,
293
+ effectfulReplies: 0,
294
+ achieved: false,
295
+ startedAt: Date.now(),
296
+ };
297
+ }
298
+ /** 解析当前目标: 复用未达成/未过期的目标; 否则尝试 getGoal(); 再否则用内置 discovery 目标 (带配额) */
299
+ async resolveGoal() {
300
+ if (this.goalRT && !this.goalRT.achieved) {
301
+ const ttl = this.goalRT.goal.ttlMs ?? Infinity;
302
+ if (Date.now() - this.goalRT.startedAt < ttl)
303
+ return this.goalRT;
304
+ }
305
+ const g = await this.opts.getGoal?.();
306
+ if (g) {
307
+ this.goalRT = this.makeGoalRuntime(g);
308
+ return this.goalRT;
309
+ }
310
+ // 兜底: 内置 discovery 目标, 保证不会"一直社交" (有配额)
311
+ if (!this.builtinGoalRT || this.builtinGoalRT.achieved) {
312
+ this.builtinGoalRT = this.makeGoalRuntime({
313
+ id: 'builtin-discovery',
314
+ description: '与已知 peer 建立并维持协作关系',
315
+ maxInitiations: 5,
316
+ effectThreshold: 2,
317
+ });
318
+ }
319
+ this.goalRT = this.builtinGoalRT;
320
+ return this.goalRT;
321
+ }
322
+ /**
323
+ * 生命周期转移决策.
324
+ * 返回下一个阶段 + 是否触发退避 + 是否重置退避.
325
+ */
326
+ evaluateLifecycle(goalRT, liveCount, now) {
327
+ // 无目标 → 用内置 discovery 兜底; 这里 goalRT 总有值
328
+ if (!goalRT)
329
+ return { nextPhase: 'RESTING', backoff: false, resetBackoff: false };
330
+ // 目标已达成 → 休息
331
+ if (goalRT.achieved)
332
+ return { nextPhase: 'RESTING', backoff: false, resetBackoff: true };
333
+ // 配额耗尽 → 休息
334
+ if (goalRT.initiationsUsed >= goalRT.goal.maxInitiations) {
335
+ return { nextPhase: 'RESTING', backoff: false, resetBackoff: false };
336
+ }
337
+ // 无效果退避中 → 保持休息
338
+ if (now < this.noEffectBackoffUntil) {
339
+ return { nextPhase: 'RESTING', backoff: false, resetBackoff: false };
340
+ }
341
+ // 连续多次发起却长时间无效果 → 进入退避
342
+ const sinceEffect = goalRT.lastEffectAt ? now - goalRT.lastEffectAt : now - goalRT.startedAt;
343
+ if (goalRT.initiationsUsed >= this.opts.minAttemptsBeforeBackoff && sinceEffect > this.opts.noEffectWindowMs) {
344
+ return { nextPhase: 'RESTING', backoff: true, resetBackoff: false };
345
+ }
346
+ // RESTING 重新评估: 超过 goalReevalMs 且有存活 peer → 重置配额, 给新一轮机会
347
+ if (this.phase === 'RESTING' && liveCount > 0) {
348
+ const sinceLast = goalRT.lastInitiateAt ? now - goalRT.lastInitiateAt : now - goalRT.startedAt;
349
+ if (sinceLast > this.opts.goalReevalMs) {
350
+ goalRT.initiationsUsed = 0;
351
+ return { nextPhase: 'ENGAGING', backoff: false, resetBackoff: true };
352
+ }
353
+ }
354
+ return { nextPhase: liveCount > 0 ? 'ENGAGING' : 'DISCOVERING', backoff: false, resetBackoff: false };
355
+ }
356
+ // ===================== 入站处理 =====================
357
+ /** 入站处理: 由 server.ts 的 data 事件处理器在收到 agent.heartbeat / agent.chat.reply 时调用 */
358
+ handleIncoming(op, payload, fromPublicKey) {
359
+ if (op === 'agent.heartbeat') {
360
+ this.peerLiveness.set(fromPublicKey, Date.now());
361
+ const info = {
362
+ publicKey: fromPublicKey,
363
+ name: payload?.name,
364
+ agentId: payload?.agentId,
365
+ channels: Array.isArray(payload?.channels) ? payload.channels : [],
366
+ lastSeen: Date.now(),
367
+ };
368
+ this.opts.onPeerAlive?.(info);
369
+ return;
370
+ }
371
+ if (op === 'agent.chat.reply') {
372
+ this.opts.onReply?.({
373
+ fromPublicKey,
374
+ channelId: payload?.channelId || '',
375
+ text: payload?.text || '',
376
+ });
377
+ // 效果度量: 一条有效回复推进目标 → 累计效果, 解除退避; 达阈值则目标达成 → RESTING
378
+ const goalRT = this.goalRT;
379
+ if (goalRT && !goalRT.achieved) {
380
+ const assessment = this.opts.assessEffect
381
+ ? this.opts.assessEffect({ goal: goalRT.goal, fromPublicKey, replyText: payload?.text || '' })
382
+ : { advanced: !!(payload?.text && String(payload.text).trim().length > 0), achievedGoal: false };
383
+ if (assessment.advanced) {
384
+ goalRT.effectfulReplies++;
385
+ goalRT.lastEffectAt = Date.now();
386
+ this.backoffLevel = 0;
387
+ this.noEffectBackoffUntil = 0;
388
+ if (assessment.achievedGoal || goalRT.effectfulReplies >= goalRT.goal.effectThreshold) {
389
+ goalRT.achieved = true;
390
+ console.log(`[heartbeat] 目标达成 (效果 ${goalRT.effectfulReplies}/${goalRT.goal.effectThreshold}) → RESTING: ${goalRT.goal.description}`);
391
+ this.setPhase('RESTING');
392
+ }
393
+ }
394
+ }
395
+ return;
396
+ }
397
+ }
398
+ // ===================== 运行期控制 / 观测 =====================
399
+ /** 设定新目标 (owner 可通过 RPC 注入). 会重置运行期状态 */
400
+ setGoal(goal) {
401
+ this.goalRT = this.makeGoalRuntime(goal);
402
+ this.backoffLevel = 0;
403
+ this.noEffectBackoffUntil = 0;
404
+ if (this.started && this.phase === 'RESTING')
405
+ this.setPhase('DISCOVERING');
406
+ console.log(`[heartbeat] 设定新目标: ${goal.description} (配额 ${goal.maxInitiations}, 效果阈值 ${goal.effectThreshold})`);
407
+ this.emitLifecycle();
408
+ }
409
+ setPhase(p) {
410
+ if (this.phase === p)
411
+ return;
412
+ this.phase = p;
413
+ this.emitLifecycle();
414
+ }
415
+ emitLifecycle() {
416
+ this.opts.onLifecycleChange?.(this.phase, this.getLifecycle());
417
+ }
418
+ /** 当前生命周期快照 */
419
+ getLifecycle() {
420
+ return {
421
+ phase: this.phase,
422
+ started: this.started,
423
+ livePeers: this.peerLiveness.size,
424
+ backoffLevel: this.backoffLevel,
425
+ socialIntervalMs: this.currentSocialInterval(),
426
+ noEffectBackoffUntil: this.noEffectBackoffUntil,
427
+ goal: this.goalRT
428
+ ? {
429
+ id: this.goalRT.goal.id,
430
+ description: this.goalRT.goal.description,
431
+ initiationsUsed: this.goalRT.initiationsUsed,
432
+ maxInitiations: this.goalRT.goal.maxInitiations,
433
+ effectfulReplies: this.goalRT.effectfulReplies,
434
+ effectThreshold: this.goalRT.goal.effectThreshold,
435
+ achieved: this.goalRT.achieved,
436
+ }
437
+ : undefined,
438
+ };
439
+ }
440
+ /** 调试: 当前 peer 存活表 */
441
+ getLiveness() {
442
+ return Array.from(this.peerLiveness.entries()).map(([publicKey, lastSeen]) => ({ publicKey, lastSeen }));
443
+ }
444
+ /** 调试: 上次主动发起时间 */
445
+ getLastInitiated(publicKey) {
446
+ return this.lastInitiated.get(publicKey);
447
+ }
448
+ // ===================== 兼容 HealthMonitor 契约 =====================
449
+ /** 供 HealthMonitor.checkHeartbeat 观测已发现的智能体 */
450
+ getDiscoveredAgents() {
451
+ return this.getLiveness().map((l) => ({ publicKey: l.publicKey, lastSeen: l.lastSeen, channels: [] }));
452
+ }
453
+ /** 供 HealthMonitor.checkHeartbeat 观测社交是否开启 */
454
+ isAntColonyEnabled() {
455
+ return this.isSocialEnabled();
456
+ }
457
+ }
@@ -2588,7 +2588,12 @@ ${data.error || "channel not found"}`, "error");
2588
2588
  const allPreviews = container.querySelectorAll(".message-ai.preview");
2589
2589
  allPreviews.forEach((el) => el.remove());
2590
2590
  currentPreviewBubble = null;
2591
- addMessage2(data.content || "", "ai", true, container, lastUsedJudgmentIds || []);
2591
+ if (!MR_hasStreamingText()) {
2592
+ addMessage2(data.content || "", "ai", true, container, lastUsedJudgmentIds || []);
2593
+ } else {
2594
+ MR_replaceStreamingText?.(data.content || "");
2595
+ MR_finalizeTimelineAsMessage(getRendererCtx());
2596
+ }
2592
2597
  } else if (data.type === "reply-preview") {
2593
2598
  const previewContent = data.content || "";
2594
2599
  const oldPreviews = container.querySelectorAll(".message-ai.preview");
@@ -217,6 +217,8 @@ async function persistRemoteChannelCache() {
217
217
  loadRemoteChannelCacheFromDisk();
218
218
  // v3: P2PDirect 引用 (Hyperswarm 薄包装) - 模块级, 因为 web server 闭包里不可用
219
219
  let v3P2PRef = null;
220
+ // 2026-07-21: 智能体社交心跳实例 (beacon + 自主决策发起对话), data 事件处理器会引用它
221
+ let agentHeartbeat = null;
220
222
  // 2026-06-10: watchdog 提升到 module-level, 让 broadcast() / 模块级业务函数能埋点喂活动
221
223
  // 之前在 createWebServer 闭包内, 闭包外的 broadcast() 拿不到 → 误判 30min 无活动 → 自杀.
222
224
  let watchdogRef = null;
@@ -1239,6 +1241,13 @@ function cleanupAndExit(signal) {
1239
1241
  return;
1240
1242
  cleanupDone = true;
1241
1243
  console.log(`[server] 收到 ${signal}, 开始清理...`);
1244
+ // 优雅停止社交心跳: 清理 beacon/social 定时器, 防止进程退出前仍一直社交
1245
+ try {
1246
+ agentHeartbeat?.stop();
1247
+ }
1248
+ catch (e) {
1249
+ console.warn('[heartbeat] 停止失败:', e?.message);
1250
+ }
1242
1251
  try {
1243
1252
  fsSync.unlinkSync(LOCK_PATH);
1244
1253
  }
@@ -1415,6 +1424,11 @@ export async function createWebServer(port = 3000, options = {}) {
1415
1424
  }, 'p2p-global');
1416
1425
  return;
1417
1426
  }
1427
+ // 2026-07-21: 社交心跳 beacon — 远端智能体宣告存活/能力, 更新本地 liveness
1428
+ if (parsed.op === 'agent.heartbeat') {
1429
+ agentHeartbeat?.handleIncoming('agent.heartbeat', parsed.payload, evt.fromPublicKey);
1430
+ return;
1431
+ }
1418
1432
  // v3 新增: B 端收到 A 的 thinking (开始 + 流式 token)
1419
1433
  if (parsed.op === 'agent.chat.thinking') {
1420
1434
  const phase = parsed.payload?.phase;
@@ -1621,6 +1635,158 @@ export async function createWebServer(port = 3000, options = {}) {
1621
1635
  console.error('[v3-P2PDirect] 解析/处理消息失败:', err.message);
1622
1636
  }
1623
1637
  });
1638
+ // === 2026-07-21: 智能体社交心跳 (beacon + 自主决策发起对话) ===
1639
+ // beacon 周期向已知 peer 宣告存活/能力; social 循环让本地 agent 自主决定跟哪个远端智能体发起对话.
1640
+ // 远端唤醒/回复链路已存在 (agent.chat.send → server.ts:529 跑 LLM → agent.chat.reply → SSE remote-chat-reply).
1641
+ try {
1642
+ const { AgentHeartbeat } = await import('../social/agent-heartbeat.js');
1643
+ const socialOn = process.env.BOLLOON_AGENT_HEARTBEAT_SOCIAL !== '0';
1644
+ const myName = await (async () => {
1645
+ let n = process.env.BOLLOON_USER_NAME || process.env.USER || 'node';
1646
+ try {
1647
+ const { readFileSync, existsSync } = await import('fs');
1648
+ const cfgPath = `${process.env.HOME || '/tmp'}/.bolloon/config.json`;
1649
+ if (existsSync(cfgPath)) {
1650
+ const cfg = JSON.parse(readFileSync(cfgPath, 'utf-8'));
1651
+ if (cfg.userName)
1652
+ n = cfg.userName;
1653
+ }
1654
+ }
1655
+ catch { }
1656
+ return n;
1657
+ })();
1658
+ agentHeartbeat = new AgentHeartbeat({
1659
+ enabled: true,
1660
+ socialEnabled: socialOn,
1661
+ beaconIntervalMs: Number(process.env.BOLLOON_HEARTBEAT_BEACON_MS) || 30_000,
1662
+ socialIntervalMs: Number(process.env.BOLLOON_HEARTBEAT_SOCIAL_MS) || 120_000,
1663
+ cooldownMs: Number(process.env.BOLLOON_HEARTBEAT_COOLDOWN_MS) || 10 * 60_000,
1664
+ self: async () => {
1665
+ const channels = await loadChannels();
1666
+ const myPk = v3P2PRef?.getPublicKey() || '';
1667
+ return {
1668
+ publicKey: myPk,
1669
+ agentId: channels[0]?.agentId,
1670
+ name: myName,
1671
+ channels: channels.map((c) => ({ id: c.id, name: c.name })),
1672
+ };
1673
+ },
1674
+ getPeers: async () => {
1675
+ const { listPeers } = await import('../network/known-peers.js');
1676
+ const kp = await listPeers();
1677
+ const myPk = v3P2PRef?.getPublicKey() || '';
1678
+ const peers = [];
1679
+ for (const p of kp) {
1680
+ if (p.publicKey === myPk)
1681
+ continue;
1682
+ const cached = remoteChannelCache.get(p.publicKey) || [];
1683
+ peers.push({
1684
+ publicKey: p.publicKey,
1685
+ name: p.name,
1686
+ channels: cached.map((c) => ({ id: c.id, name: c.name })),
1687
+ });
1688
+ }
1689
+ return peers;
1690
+ },
1691
+ transport: {
1692
+ send: async (pk, op, payload) => {
1693
+ const { sendOrQueue } = await import('../network/p2p-outbox.js');
1694
+ return sendOrQueue(pk, op, payload, v3P2PRef);
1695
+ },
1696
+ },
1697
+ decide: socialOn ? llmSocialDecide : undefined,
1698
+ // 目标: 社交服务于"与网络中的其他智能体建立并维持协作". 配额/效果阈值防止一直社交.
1699
+ // owner 可通过 env BOLLOON_AGENT_GOAL 覆盖描述; 也可经 RPC setGoal 运行时注入.
1700
+ getGoal: async () => ({
1701
+ id: 'owner-collab',
1702
+ description: process.env.BOLLOON_AGENT_GOAL || '与网络中的其他智能体建立并维持协作关系, 主动分享进展并获取所需信息',
1703
+ maxInitiations: Number(process.env.BOLLOON_HEARTBEAT_GOAL_MAX) || 8,
1704
+ effectThreshold: Number(process.env.BOLLOON_HEARTBEAT_GOAL_EFFECT) || 3,
1705
+ }),
1706
+ // 效果度量: 远端回了非空且有实质内容的消息, 视为推进了目标 (生产可换 LLM 判定 achievedGoal)
1707
+ assessEffect: ({ replyText }) => {
1708
+ const t = (replyText || '').trim();
1709
+ return { advanced: t.length > 0, achievedGoal: false };
1710
+ },
1711
+ onPeerAlive: (peer) => {
1712
+ broadcast({
1713
+ type: 'peer-heartbeat',
1714
+ fromPublicKey: peer.publicKey,
1715
+ name: peer.name,
1716
+ channels: peer.channels,
1717
+ ts: Date.now(),
1718
+ }, 'p2p-global');
1719
+ },
1720
+ // 每次社交 tick 喂给 24h 看门狗, 防止误判卡死重启
1721
+ onActivity: () => {
1722
+ try {
1723
+ watchdogRef?.recordActivity?.('agent-heartbeat');
1724
+ }
1725
+ catch { }
1726
+ },
1727
+ // 生命周期阶段变化 → 推 SSE 给前端展示
1728
+ onLifecycleChange: (phase, snap) => {
1729
+ broadcast({
1730
+ type: 'agent-lifecycle',
1731
+ phase,
1732
+ snapshot: snap,
1733
+ ts: Date.now(),
1734
+ }, 'p2p-global');
1735
+ },
1736
+ });
1737
+ agentHeartbeat.start();
1738
+ // 注册到全局, 让 24h HealthMonitor.checkHeartbeat 能观测到本智能体 (getDiscoveredAgents/isAntColonyEnabled)
1739
+ global.socialHeartbeat = agentHeartbeat;
1740
+ global.agentHeartbeat = agentHeartbeat;
1741
+ }
1742
+ catch (hbErr) {
1743
+ console.warn('[heartbeat] 启动失败 (non-fatal):', hbErr?.message);
1744
+ }
1745
+ // 社交决策: 让本地 agent (用第一个本地 channel 的身份) 判断是否主动联络某 peer
1746
+ // 目标感知: ctx.goal 是当前要达成的目标, 决策应服务于它, 达成后可声明 goalAchieved 进入 RESTING
1747
+ async function llmSocialDecide(ctx) {
1748
+ try {
1749
+ const channels = await loadChannels();
1750
+ const local = channels[0];
1751
+ if (!local)
1752
+ return { initiate: false };
1753
+ const agent = await getAgentForChannel(local.id, local.did || '', local.name, local.didDocRef);
1754
+ const peerLines = ctx.peers
1755
+ .map((p) => `- ${p.name || p.publicKey.slice(0, 8)} (pk=${p.publicKey.slice(0, 12)}…): 渠道[${p.channels.map((c) => c.name).join(', ') || '无'}]`)
1756
+ .join('\n');
1757
+ const goalDesc = ctx.goal ? `当前目标: ${ctx.goal.description} (已发起 ${ctx.goal.initiationsUsed}/${ctx.goal.maxInitiations}, 有效回复 ${ctx.goal.effectfulReplies}/${ctx.goal.effectThreshold})` : '当前无明确目标';
1758
+ const prompt = `你是智能体「${ctx.self.name || '本地智能体'}」。你通过 P2P 网络认识以下其他智能体:
1759
+ ${peerLines}
1760
+
1761
+ ${goalDesc}
1762
+
1763
+ 规则:
1764
+ 1. 社交是为了达成上述目标, 不是闲聊。只在你有真正有价值的信息要分享/询问、且能推进目标时才主动发起。
1765
+ 2. 不要重复最近已经聊过的话题, 不要每条心跳都发消息, 保持克制。
1766
+ 3. 如果目标已经通过已有交流达成 (或你认为无需再聊), 输出 {"initiate": false, "goalAchieved": true}。
1767
+ 4. 如果决定发起, 选一个最合适的目标渠道 (用对方渠道的真实 id)。
1768
+
1769
+ 现在是否要主动联系其中某个智能体? 只输出一个 JSON 对象, 不要任何其他文字:
1770
+ {"initiate": true 或 false, "goalAchieved": true 或 false, "targetPeerPublicKey": "对方 pk", "targetChannelId": "对方渠道 id", "message": "你要说的话"}
1771
+ 若不想发起, 输出 {"initiate": false}。`;
1772
+ const raw = await agent.promptStream(prompt, () => { }, undefined, local.id);
1773
+ const m = raw.match(/\{[\s\S]*\}/);
1774
+ if (!m)
1775
+ return { initiate: false };
1776
+ const obj = JSON.parse(m[0]);
1777
+ return {
1778
+ initiate: !!obj.initiate,
1779
+ goalAchieved: !!obj.goalAchieved,
1780
+ targetPeerPublicKey: obj.targetPeerPublicKey,
1781
+ targetChannelId: obj.targetChannelId,
1782
+ message: obj.message,
1783
+ };
1784
+ }
1785
+ catch (err) {
1786
+ console.warn('[heartbeat] 社交决策 LLM 失败 (跳过本次发起):', err?.message);
1787
+ return { initiate: false };
1788
+ }
1789
+ }
1624
1790
  // 新连接进来 → 主动发我分享给 ta 的 channel 列表
1625
1791
  v3P2PRef.on('connection', (evt) => {
1626
1792
  // 2026-06-10: 喂 watchdog —— 新连接到来是真实业务活动
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli-entry.js",
@@ -48,30 +48,30 @@
48
48
  "src/constraint-runtime"
49
49
  ],
50
50
  "dependencies": {
51
- "@bolloon/bolloon-agent": "^0.3.6",
51
+ "@bolloon/bolloon-agent": "^0.3.3",
52
52
  "@bolloon/constraint-runtime": "0.1.0",
53
- "@capacitor/core": "^8.4.2",
53
+ "@capacitor/core": "^8.4.1",
54
54
  "@capacitor/ios": "^8.4.1",
55
55
  "@chainsafe/libp2p-noise": "^17.0.0",
56
56
  "@chainsafe/libp2p-yamux": "^8.0.1",
57
57
  "@diap/sdk": "^0.1.10",
58
58
  "@libp2p/autonat": "^3.0.20",
59
- "@libp2p/circuit-relay-v2": "^4.2.9",
59
+ "@libp2p/circuit-relay-v2": "^4.2.5",
60
60
  "@libp2p/dcutr": "^3.0.20",
61
61
  "@libp2p/identify": "^4.1.6",
62
62
  "@libp2p/kad-dht": "^16.2.6",
63
63
  "@libp2p/tcp": "^11.0.20",
64
- "@libp2p/upnp-nat": "^4.0.24",
64
+ "@libp2p/upnp-nat": "^4.0.20",
65
65
  "@multiformats/multiaddr": "^13.0.3",
66
- "@noble/hashes": "^2.2.0",
66
+ "@noble/hashes": "^1.3.0",
67
67
  "@rayhanadev/iroh": "^0.1.1",
68
68
  "b4a": "^1.8.1",
69
69
  "dotenv": "^17.4.2",
70
- "esbuild": "^0.28.1",
70
+ "esbuild": "^0.24.0",
71
71
  "express": "^5.2.1",
72
72
  "libp2p": "^3.3.0",
73
73
  "mammoth": "^1.6.0",
74
- "pdf-parse": "^2.4.5",
74
+ "pdf-parse": "^1.1.4",
75
75
  "platform": "^1.3.6",
76
76
  "react": "^18.3.0",
77
77
  "react-dom": "^18.3.0",