@bolloon/bolloon-agent 0.3.6 → 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 +31 -0
- package/dist/cli/loading-tui.js +386 -33
- package/dist/cli-entry.js +5 -10
- package/dist/electron/main.js +16 -0
- package/dist/electron/main.js.map +1 -1
- package/dist/index.js +101 -22
- package/dist/social/agent-heartbeat.js +457 -0
- package/dist/utils/auto-update.js +292 -112
- package/dist/utils/auto-update.js.map +1 -0
- package/dist/web/client.js +6 -1
- package/dist/web/server.js +166 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import * as ed25519 from '@noble/ed25519';
|
|
|
5
5
|
import { sha512 } from '@noble/hashes/sha2.js';
|
|
6
6
|
import * as fs from 'fs/promises';
|
|
7
7
|
import * as path from 'path';
|
|
8
|
+
import { spawn } from 'child_process';
|
|
8
9
|
import { documentReader } from './documents/reader.js';
|
|
9
10
|
import { initMinimax } from './constraints/index.js';
|
|
10
11
|
import { createAgentSession } from './agents/pi-sdk.js';
|
|
@@ -12,7 +13,8 @@ import { createSubAgentManager } from './agents/subagent-manager.js';
|
|
|
12
13
|
import { getGlobalSharedContext } from './social/global-shared-context.js';
|
|
13
14
|
import { createBollharnessIntegration } from './bollharness-integration/index.js';
|
|
14
15
|
import * as readline from 'readline';
|
|
15
|
-
|
|
16
|
+
import { printBanner, renderDashboard, renderDialog, renderUserMessage, renderAgentMessage, renderToolCall, flowConnector, termWidth } from './cli/loading-tui.js';
|
|
17
|
+
// 启动自动检查更新:后台、节流、检测到新版本自动安装(可被 --no-update / BOLLOON_SKIP_UPDATE 关闭)
|
|
16
18
|
import { createRequire } from 'module';
|
|
17
19
|
const _require = createRequire(import.meta.url);
|
|
18
20
|
const _BOLLOON_VERSION = (() => {
|
|
@@ -42,14 +44,7 @@ const HIDE_CURSOR = '\x1b[?25l';
|
|
|
42
44
|
const SHOW_CURSOR = '\x1b[?25h';
|
|
43
45
|
const s = {
|
|
44
46
|
banner: () => {
|
|
45
|
-
|
|
46
|
-
const pad = Math.max(0, 39 - 17 - verStr.length);
|
|
47
|
-
const spaces = ' '.repeat(pad);
|
|
48
|
-
console.log(`\n${CYAN}${BOLD}
|
|
49
|
-
╔═══════════════════════════════════════════╗
|
|
50
|
-
║ ${WHITE}🤖 Bolloon ${CYAN}${verStr}${spaces}║
|
|
51
|
-
║ ${WHITE}P2P AI Document Processor${CYAN} ║
|
|
52
|
-
╚═══════════════════════════════════════════╝${RESET}\n`);
|
|
47
|
+
printBanner(_BOLLOON_VERSION);
|
|
53
48
|
},
|
|
54
49
|
step: (num, total, text, status) => {
|
|
55
50
|
const check = status === 'ok' ? `${GREEN}✓` :
|
|
@@ -96,6 +91,7 @@ const s = {
|
|
|
96
91
|
},
|
|
97
92
|
dialog: async (title, promptText) => {
|
|
98
93
|
return new Promise((resolve) => {
|
|
94
|
+
console.log(renderDialog({ title, prompt: promptText }));
|
|
99
95
|
const rl = readline.createInterface({
|
|
100
96
|
input: process.stdin,
|
|
101
97
|
output: process.stdout
|
|
@@ -368,6 +364,30 @@ function startCLI(comm) {
|
|
|
368
364
|
}
|
|
369
365
|
readline.emitKeypressEvents(process.stdin);
|
|
370
366
|
process.stdout.write(CLEAR_LINE);
|
|
367
|
+
// ── Bolloon Agent 仪表盘 (ASCII 艺术字 + 品牌图标) ──
|
|
368
|
+
printBanner(_BOLLOON_VERSION);
|
|
369
|
+
let peerCount = 0;
|
|
370
|
+
try {
|
|
371
|
+
peerCount = comm.getConnections().length;
|
|
372
|
+
}
|
|
373
|
+
catch { /* */ }
|
|
374
|
+
const llmName = process.env.MINIMAX_API_KEY ? 'MiniMax'
|
|
375
|
+
: process.env.OPENAI_API_KEY ? 'OpenAI'
|
|
376
|
+
: process.env.ANTHROPIC_API_KEY ? 'Anthropic'
|
|
377
|
+
: process.env.DEEPSEEK_API_KEY ? 'DeepSeek'
|
|
378
|
+
: '未配置';
|
|
379
|
+
process.stdout.write(renderDashboard({
|
|
380
|
+
title: '系统状态',
|
|
381
|
+
rows: [
|
|
382
|
+
{ label: 'LLM Provider', status: llmName === '未配置' ? 'warn' : 'ok', detail: llmName },
|
|
383
|
+
{ label: 'P2P 节点', status: peerCount > 0 ? 'ok' : 'warn', detail: `${peerCount} 个` },
|
|
384
|
+
{ label: '输入方式', status: 'info', detail: '底部对话框' },
|
|
385
|
+
],
|
|
386
|
+
}) + '\n');
|
|
387
|
+
process.stdout.write(renderDialog({
|
|
388
|
+
title: 'Bolloon Agent',
|
|
389
|
+
prompt: '输入消息开始对话 · 输入 help 查看命令',
|
|
390
|
+
}) + '\n');
|
|
371
391
|
showBottomPrompt();
|
|
372
392
|
const promptTimer = setInterval(() => {
|
|
373
393
|
if (isRunning && promptVisible) {
|
|
@@ -462,16 +482,43 @@ async function processInput(input, comm) {
|
|
|
462
482
|
return;
|
|
463
483
|
}
|
|
464
484
|
try {
|
|
485
|
+
// 已发送消息框
|
|
486
|
+
process.stdout.write(renderUserMessage(trimmed) + '\n');
|
|
465
487
|
const a = await getAgent();
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
488
|
+
const boxW = Math.min(termWidth() - 2, 76);
|
|
489
|
+
const pending = [];
|
|
490
|
+
let firstEvent = true;
|
|
491
|
+
const clearThinking = () => {
|
|
492
|
+
if (firstEvent) {
|
|
493
|
+
process.stdout.write('\r' + ' '.repeat(30) + '\r');
|
|
494
|
+
firstEvent = false;
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
const onStream = (e) => {
|
|
498
|
+
if (e.type === 'step_start') {
|
|
499
|
+
pending.push({ tool: e.tool, args: e.args, t0: Date.now() });
|
|
500
|
+
}
|
|
501
|
+
else if (e.type === 'step_done' || e.type === 'step_error') {
|
|
502
|
+
const p = pending.shift();
|
|
503
|
+
clearThinking();
|
|
504
|
+
// 连接线只在第 2 个及之后的工具框前出现
|
|
505
|
+
if (!firstEvent)
|
|
506
|
+
process.stdout.write(flowConnector(boxW) + '\n');
|
|
507
|
+
process.stdout.write(renderToolCall({
|
|
508
|
+
tool: e.tool ?? p?.tool ?? '?',
|
|
509
|
+
args: p?.args,
|
|
510
|
+
status: e.type === 'step_done' ? 'ok' : 'error',
|
|
511
|
+
output: e.output,
|
|
512
|
+
error: e.error,
|
|
513
|
+
durationMs: p ? Date.now() - p.t0 : undefined,
|
|
514
|
+
width: boxW,
|
|
515
|
+
}) + '\n');
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
const response = await a.prompt(trimmed, { onStream });
|
|
519
|
+
clearThinking();
|
|
520
|
+
// 智能体回复框 (圆角)
|
|
521
|
+
process.stdout.write(renderAgentMessage(response) + '\n');
|
|
475
522
|
}
|
|
476
523
|
catch (e) {
|
|
477
524
|
if (!e.message?.includes('ERR_USE_AFTER_CLOSE') && !e.message?.includes('write after end')) {
|
|
@@ -1808,6 +1855,25 @@ function printHelp() {
|
|
|
1808
1855
|
// ---------------------------------------------------------------------------
|
|
1809
1856
|
// Entry point
|
|
1810
1857
|
// ---------------------------------------------------------------------------
|
|
1858
|
+
/**
|
|
1859
|
+
* 以相同参数重新启动当前 Node 进程(用于更新后自动应用新版本)。
|
|
1860
|
+
* 先 detached 拉起新进程,再退出旧进程。
|
|
1861
|
+
*/
|
|
1862
|
+
function restartCurrentProcess() {
|
|
1863
|
+
try {
|
|
1864
|
+
const entry = process.argv[1];
|
|
1865
|
+
const child = spawn(process.execPath, [entry, ...process.argv.slice(2)], {
|
|
1866
|
+
stdio: 'inherit',
|
|
1867
|
+
detached: true,
|
|
1868
|
+
env: { ...process.env },
|
|
1869
|
+
});
|
|
1870
|
+
child.unref();
|
|
1871
|
+
}
|
|
1872
|
+
catch {
|
|
1873
|
+
// 拉起失败则退回手动重启
|
|
1874
|
+
}
|
|
1875
|
+
process.exit(0);
|
|
1876
|
+
}
|
|
1811
1877
|
async function main() {
|
|
1812
1878
|
try {
|
|
1813
1879
|
const args = parseArgs();
|
|
@@ -1815,10 +1881,23 @@ async function main() {
|
|
|
1815
1881
|
printHelp();
|
|
1816
1882
|
process.exit(0);
|
|
1817
1883
|
}
|
|
1818
|
-
//
|
|
1819
|
-
//
|
|
1820
|
-
//
|
|
1821
|
-
//
|
|
1884
|
+
// 启动自动更新检查(后台执行,不阻塞主流程)。
|
|
1885
|
+
// 检测到新版本会自动安装;安装成功后自动重启以应用新版本。
|
|
1886
|
+
// 可用 --no-update / BOLLOON_SKIP_UPDATE 关闭,
|
|
1887
|
+
// 或用 config.json 的 autoUpdate:false / autoRestart:false / BOLLOON_AUTO_UPDATE=1 控制。
|
|
1888
|
+
// 手动检查: bolloon --update-check
|
|
1889
|
+
// 手动更新: bolloon --update-now [package]
|
|
1890
|
+
if (!args.updateCheck && !args.updateNow) {
|
|
1891
|
+
void (async () => {
|
|
1892
|
+
try {
|
|
1893
|
+
const { checkAndUpdate } = await import('./utils/auto-update.js');
|
|
1894
|
+
await checkAndUpdate({ onUpdated: restartCurrentProcess });
|
|
1895
|
+
}
|
|
1896
|
+
catch {
|
|
1897
|
+
// 自动更新失败不影响主程序启动
|
|
1898
|
+
}
|
|
1899
|
+
})();
|
|
1900
|
+
}
|
|
1822
1901
|
const mode = args.web ? 'web' : 'cli';
|
|
1823
1902
|
const isNonInteractive = !!(args.tool || args.prompt);
|
|
1824
1903
|
const originalLog = console.log;
|
|
@@ -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
|
+
}
|