@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/web/server.js
CHANGED
|
@@ -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 —— 新连接到来是真实业务活动
|