@bolloon/bolloon-agent 0.3.46 → 0.3.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/knowledge-organizer.js +470 -0
- package/dist/agents/pi-sdk-tools.js +5 -3
- package/dist/agents/skill-organizer.js +322 -0
- package/dist/bootstrap/context-os.js +19 -10
- package/dist/cli/ink-app.js +15 -1
- package/dist/cli/loading-tui.js +18 -10
- package/dist/index.js +163 -30
- package/dist/social/agent-heartbeat.js +62 -1
- package/dist/web/server.js +43 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { getGlobalSharedContext } from './social/global-shared-context.js';
|
|
|
16
16
|
import { createBollharnessIntegration } from './bollharness-integration/index.js';
|
|
17
17
|
import * as readline from 'readline';
|
|
18
18
|
import { printBanner, renderDialog, renderUserMessage, renderAgentMessage, renderMessageBox, renderToolCallListItem, termWidth } from './cli/loading-tui.js';
|
|
19
|
-
import { startInk, stopInk, inkAppendLine as appendLine, inkSetStatus, inkSetThinking } from './cli/ink-app.js';
|
|
19
|
+
import { startInk, stopInk, inkAppendLine as appendLine, inkSetStatus, inkSetThinking, inkSetTransient } from './cli/ink-app.js';
|
|
20
20
|
// 启动自动检查更新:后台、节流、检测到新版本自动安装(可被 --no-update / BOLLOON_SKIP_UPDATE 关闭)
|
|
21
21
|
import { createRequire } from 'module';
|
|
22
22
|
const _require = createRequire(import.meta.url);
|
|
@@ -271,12 +271,44 @@ async function bootstrapIroh(keypair, name) {
|
|
|
271
271
|
// Agent 懒加载
|
|
272
272
|
// ---------------------------------------------------------------------------
|
|
273
273
|
let agent = null;
|
|
274
|
+
/** 2026-08-09: agent 当前绑定的 channel id (null = 默认 harness 身份) — 切换时据此重建 */
|
|
275
|
+
let agentBoundChannelId = null;
|
|
274
276
|
let harness = null;
|
|
275
277
|
let hybridMessenger = null;
|
|
276
278
|
let agentIdentity = null;
|
|
277
279
|
async function getAgent() {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
+
// 2026-08-09: agent 身份绑定当前 active channel — 切换 / 新建 channel 后重建.
|
|
281
|
+
// 旧实现: agent 全局单例 + peerId:'harness' 固定, 切 channel 身份不变 (bug).
|
|
282
|
+
// 新实现: channel 有 agentId/did/publicKey/persona 时按 channel 建 session,
|
|
283
|
+
// agentIdentity 同步更新, loadSessionKey 回灌该 channel 的历史.
|
|
284
|
+
const targetChannelId = cliActiveChannelId || null;
|
|
285
|
+
if (agent && agentBoundChannelId === targetChannelId)
|
|
286
|
+
return agent;
|
|
287
|
+
// 读取当前 active channel 的持久身份
|
|
288
|
+
let chIdentity = null;
|
|
289
|
+
if (targetChannelId) {
|
|
290
|
+
try {
|
|
291
|
+
const { getIdentityStore } = await import('./agents/agent-identity-store.js');
|
|
292
|
+
const store = getIdentityStore();
|
|
293
|
+
await store.load();
|
|
294
|
+
const ch = store.rawChannels.find((c) => c.id === targetChannelId);
|
|
295
|
+
if (ch)
|
|
296
|
+
chIdentity = ch;
|
|
297
|
+
}
|
|
298
|
+
catch { /* 读不到就退默认 */ }
|
|
299
|
+
}
|
|
300
|
+
let identityDoc;
|
|
301
|
+
if (chIdentity?.did && chIdentity.publicKey) {
|
|
302
|
+
// channel 已有持久 DID → 用 channel 身份
|
|
303
|
+
identityDoc = {
|
|
304
|
+
did: chIdentity.did,
|
|
305
|
+
name: chIdentity.persona?.name || chIdentity.name || 'agent',
|
|
306
|
+
publicKey: chIdentity.publicKey,
|
|
307
|
+
createdAt: Date.now(),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
else if (agentIdentity) {
|
|
311
|
+
identityDoc = {
|
|
280
312
|
did: agentIdentity.did,
|
|
281
313
|
name: agentIdentity.name,
|
|
282
314
|
publicKey: agentIdentity.publicKey,
|
|
@@ -285,15 +317,39 @@ async function getAgent() {
|
|
|
285
317
|
p2pChannel: agentIdentity.p2pChannel,
|
|
286
318
|
cid: agentIdentity.cid,
|
|
287
319
|
ipnsName: agentIdentity.ipnsName
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
identityDoc = undefined;
|
|
324
|
+
}
|
|
325
|
+
const loadSessionKey = targetChannelId
|
|
326
|
+
? `${targetChannelId}:${chIdentity?.currentSessionId || 'default'}`
|
|
327
|
+
: undefined;
|
|
328
|
+
agent = await createAgentSession({
|
|
329
|
+
cwd: process.cwd(),
|
|
330
|
+
peerId: targetChannelId ?? 'harness',
|
|
331
|
+
identityDoc,
|
|
332
|
+
// 2026-08-09: 透传 channel.agentId → persona docs 按 agent 加载 (身份真正变化)
|
|
333
|
+
agentId: chIdentity?.agentId || (targetChannelId ? undefined : agentIdentity?.name),
|
|
334
|
+
loadSessionKey,
|
|
335
|
+
});
|
|
336
|
+
agentBoundChannelId = targetChannelId;
|
|
337
|
+
// 同步 agentIdentity (状态栏 / 身份引用)
|
|
338
|
+
if (chIdentity) {
|
|
339
|
+
agentIdentity = {
|
|
340
|
+
did: chIdentity.did || agentIdentity?.did || '',
|
|
341
|
+
name: chIdentity.persona?.name || chIdentity.name || 'agent',
|
|
342
|
+
publicKey: chIdentity.publicKey || agentIdentity?.publicKey || '',
|
|
343
|
+
peerId: targetChannelId ?? undefined,
|
|
344
|
+
};
|
|
294
345
|
}
|
|
295
346
|
return agent;
|
|
296
347
|
}
|
|
348
|
+
/** 强制重建 agent (切 channel / 新建 agent 后调用) */
|
|
349
|
+
function invalidateAgent() {
|
|
350
|
+
agent = null;
|
|
351
|
+
agentBoundChannelId = null;
|
|
352
|
+
}
|
|
297
353
|
// ---------------------------------------------------------------------------
|
|
298
354
|
// Dispatch
|
|
299
355
|
// ---------------------------------------------------------------------------
|
|
@@ -367,6 +423,8 @@ let cliStartTime = 0;
|
|
|
367
423
|
let cliModelName = '…';
|
|
368
424
|
let cliAgentName = '…';
|
|
369
425
|
let cliActiveChannelId = null;
|
|
426
|
+
// 2026-08-10: CLI 自动整理心跳 (与社交心跳并列, 独立于 server) — 退出时 stop
|
|
427
|
+
let cliOrganizeHeartbeat = null;
|
|
370
428
|
function fmtDuration(ms) {
|
|
371
429
|
const s = Math.floor(ms / 1000);
|
|
372
430
|
if (s < 60)
|
|
@@ -503,6 +561,59 @@ async function startCLI(comm) {
|
|
|
503
561
|
catch { /* 降级: getCliCtxUsage 返回 0/1M */ }
|
|
504
562
|
const initialStatus = `${C_ACCENT}${cliModelName}${RESET}${C_DIM} │${RESET} ${cliAgentName} ${C_DIM}│${RESET} ⏱ 0s${C_DIM} │${RESET} ${buildContextBar(getCliCtxUsage())}`;
|
|
505
563
|
startInk((text) => { processInput(text, comm); }, initialStatus, getStatus);
|
|
564
|
+
// 2026-08-10: 自动整理心跳 (CLI 侧, 与社交心跳并列) — 启动后立即"固定看一下 skills view"
|
|
565
|
+
// (扫描遗留 skills), 之后按周期 (默认 30min, env BOLLOON_ORGANIZE_HEARTBEAT_MS) 完整进化经验.
|
|
566
|
+
// 显示走 transient 颜文字行: 触发时显示, 结束后清空 (显示为空).
|
|
567
|
+
try {
|
|
568
|
+
const { startOrganizeHeartbeat } = await import('./agents/skill-organizer.js');
|
|
569
|
+
let firstOrganizeScan = true; // 启动第一轮只做快速遗留扫描 (无 LLM), 不阻塞启动
|
|
570
|
+
cliOrganizeHeartbeat = startOrganizeHeartbeat({
|
|
571
|
+
intervalMs: Number(process.env.BOLLOON_ORGANIZE_HEARTBEAT_MS) || 30 * 60_000,
|
|
572
|
+
onStart: () => inkSetTransient(`${C_DIM}(`・ω・´) 自动整理经验中...${RESET}`),
|
|
573
|
+
onEnd: (r) => {
|
|
574
|
+
inkSetTransient(null); // 结束后去除显示效果 (显示为空)
|
|
575
|
+
if (r && r.leftovers.length > 0) {
|
|
576
|
+
appendLine(`${C_DIM}🧹 发现 ${r.leftovers.length} 个遗留 skills: ${r.leftovers.slice(0, 5).map(l => l.name).join(', ')}${RESET}`);
|
|
577
|
+
}
|
|
578
|
+
if (r && r.evolved.length > 0) {
|
|
579
|
+
appendLine(`${C_OK}✨ 经验进化: ${r.evolved.join(', ')}${RESET}`);
|
|
580
|
+
}
|
|
581
|
+
// 2026-08-10: 知识层整理汇总 (Context OS/社交/智能体/judgeness/项目/画像/日志/目标)
|
|
582
|
+
const kSections = (r?.knowledge?.sections || []).filter(s => s.handled > 0 || s.error);
|
|
583
|
+
if (kSections.length > 0) {
|
|
584
|
+
appendLine(`${C_DIM}🧠 知识整理: ${kSections.map(s => s.error ? `${s.label}✗` : `${s.label}✓`).join(' ')}${RESET}`);
|
|
585
|
+
}
|
|
586
|
+
},
|
|
587
|
+
onError: () => inkSetTransient(null),
|
|
588
|
+
run: async () => {
|
|
589
|
+
// 启动第一轮 (firstOrganizeScan=true) 只做快速扫描 — 不拿 LLM, 立即执行.
|
|
590
|
+
// 后续周期轮才取 agent LLM 做完整经验进化 (2026-08-10: getAgent 在无 LLM 环境可能
|
|
591
|
+
// 长时间挂起 → 8s 超时降级为仅扫描)
|
|
592
|
+
let llm;
|
|
593
|
+
const needEvolve = !firstOrganizeScan;
|
|
594
|
+
if (needEvolve) {
|
|
595
|
+
try {
|
|
596
|
+
const a = await Promise.race([
|
|
597
|
+
getAgent().catch(() => null),
|
|
598
|
+
new Promise((res) => setTimeout(() => res(null), 8000)),
|
|
599
|
+
]);
|
|
600
|
+
if (a && typeof a.promptStream === 'function') {
|
|
601
|
+
llm = (p) => a.promptStream(p, () => { }, undefined, cliActiveChannelId || undefined);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
catch { /* 无 agent → 仅扫描 */ }
|
|
605
|
+
}
|
|
606
|
+
const { runAutoOrganize } = await import('./agents/skill-organizer.js');
|
|
607
|
+
const evolve = needEvolve && !!llm;
|
|
608
|
+
firstOrganizeScan = false;
|
|
609
|
+
return runAutoOrganize({ llm, source: 'cli:organize-heartbeat', evolve });
|
|
610
|
+
},
|
|
611
|
+
});
|
|
612
|
+
// 启动即跑一轮: 每次打开后固定看一下 skills view (遗留扫描, 快, 不阻塞)
|
|
613
|
+
// 延迟 3s 等 Ink 挂载完成 (global __inkAppend/__inkSetTransient 注册) — 否则首轮显示丢失
|
|
614
|
+
setTimeout(() => { cliOrganizeHeartbeat?.runOnce().catch(() => { }); }, 3000);
|
|
615
|
+
}
|
|
616
|
+
catch { /* 自动整理启动失败不阻塞 CLI */ }
|
|
506
617
|
// Wait on a promise that resolves on Ctrl+C / 双击 Esc
|
|
507
618
|
// (ink-app 的 requestExit 调 __inkRequestExit → resolve, 清理后 process.exit)
|
|
508
619
|
let cliExitResolve = () => { };
|
|
@@ -512,6 +623,10 @@ async function startCLI(comm) {
|
|
|
512
623
|
delete globalThis.__inkRequestExit;
|
|
513
624
|
stopInk();
|
|
514
625
|
appendLine(`\n${CYAN}👋 再见!${RESET}`);
|
|
626
|
+
try {
|
|
627
|
+
cliOrganizeHeartbeat?.stop();
|
|
628
|
+
}
|
|
629
|
+
catch { /* 非致命 */ }
|
|
515
630
|
comm.stop();
|
|
516
631
|
process.exit(0);
|
|
517
632
|
}
|
|
@@ -575,6 +690,13 @@ async function processInput(input, comm) {
|
|
|
575
690
|
await store.setActive(r.channel.id);
|
|
576
691
|
cliAgentName = r.identity.name;
|
|
577
692
|
cliActiveChannelId = r.channel.id;
|
|
693
|
+
// 2026-08-09: 切 channel 必须重建 agent session — 否则身份/记忆停留在旧 channel (bug 修复)
|
|
694
|
+
invalidateAgent();
|
|
695
|
+
// 立即重建 (提前建好, 避免下次输入才卡顿; 失败不阻塞切换)
|
|
696
|
+
try {
|
|
697
|
+
await getAgent();
|
|
698
|
+
}
|
|
699
|
+
catch { /* 非致命, 下次输入时再试 */ }
|
|
578
700
|
inkSetStatus(getStatus()); // 触发状态栏立即重绘 (无需等 1s 定时器)
|
|
579
701
|
const extra = prev && prev.name !== r.identity.name ? ` (从 ${prev.name} 切换)` : '';
|
|
580
702
|
appendLine(`${C_ACCENT}→ 当前智能体: ${r.identity.name}${RESET}${extra}`);
|
|
@@ -599,38 +721,47 @@ async function processInput(input, comm) {
|
|
|
599
721
|
const { getIdentityStore } = await import('./agents/agent-identity-store.js');
|
|
600
722
|
const store = getIdentityStore();
|
|
601
723
|
await store.load();
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
const
|
|
605
|
-
const
|
|
606
|
-
let channels = [];
|
|
607
|
-
try {
|
|
608
|
-
const parsed = JSON.parse(await readFile(channelsPath, 'utf-8'));
|
|
609
|
-
channels = Array.isArray(parsed) ? parsed : parsed?.channels || [];
|
|
610
|
-
}
|
|
611
|
-
catch { /* 首次无文件 */ }
|
|
612
|
-
const dupName = channels.find((c) => c.name === name.trim());
|
|
724
|
+
// 2026-08-09: 复用 server-storage updateChannels 原子写 (互斥锁) — 旧实现裸 readFile→push→writeFile
|
|
725
|
+
// 与 Web server 并发写 channels.json 互相覆盖 → 创建的 agent 重启后丢失 (bug 修复)
|
|
726
|
+
const { updateChannels } = await import('./web/server-storage.js');
|
|
727
|
+
const dupName = store.rawChannels.find((c) => c.name === name.trim());
|
|
613
728
|
if (dupName) {
|
|
614
729
|
appendLine(`${C_ERROR}同名智能体已存在: '${dupName.name}' (id=${dupName.id})${RESET}`);
|
|
615
730
|
return;
|
|
616
731
|
}
|
|
617
732
|
const id = `ch_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
|
|
733
|
+
const agentId = `agent-${name.trim().toLowerCase().replace(/\s+/g, '-')}`;
|
|
618
734
|
const ch = {
|
|
619
735
|
id,
|
|
620
736
|
name: name.trim(),
|
|
621
|
-
agentId
|
|
737
|
+
agentId,
|
|
622
738
|
createdAt: new Date().toISOString(),
|
|
623
739
|
updatedAt: new Date().toISOString(),
|
|
624
740
|
currentSessionId: 'default',
|
|
625
741
|
};
|
|
626
742
|
if (personaHint)
|
|
627
743
|
ch.persona = { name: name.trim(), description: personaHint };
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
744
|
+
// 2026-08-09: 立即生成该 agent 的持久 DID 身份 (agent-keys/<agentId>.json) —
|
|
745
|
+
// 与 server fixOneChannelDID 对齐, 保证 CLI 新建的 agent 身份稳定且归属用户 DID
|
|
746
|
+
try {
|
|
747
|
+
const { loadOrCreateAgentIdentity } = await import('./agents/agent-identity.js');
|
|
748
|
+
const idt = loadOrCreateAgentIdentity(agentId);
|
|
749
|
+
ch.did = idt.did;
|
|
750
|
+
ch.publicKey = idt.publicKey;
|
|
751
|
+
}
|
|
752
|
+
catch { /* DID 生成失败不阻塞创建 */ }
|
|
753
|
+
const channels = await updateChannels((chs) => [...chs, ch]);
|
|
754
|
+
// 刷新 store 缓存 (updateChannels 走了 server-storage, store 内存还是旧的)
|
|
755
|
+
await store.load();
|
|
631
756
|
await store.setActive(id);
|
|
632
757
|
cliAgentName = name.trim();
|
|
633
758
|
cliActiveChannelId = id;
|
|
759
|
+
// 2026-08-09: 新建 agent 后立即重建 session — 否则新 agent 身份不加载 (bug 修复)
|
|
760
|
+
invalidateAgent();
|
|
761
|
+
try {
|
|
762
|
+
await getAgent();
|
|
763
|
+
}
|
|
764
|
+
catch { /* 非致命 */ }
|
|
634
765
|
inkSetStatus(getStatus());
|
|
635
766
|
appendLine(`${C_OK}✓ 已创建智能体 channel: ${name.trim()}${RESET} (${C_DIM}${id}${RESET})${personaHint ? `\n ${C_DIM}persona: ${personaHint}${RESET}` : ''}`);
|
|
636
767
|
}
|
|
@@ -1544,18 +1675,20 @@ async function processInput(input, comm) {
|
|
|
1544
1675
|
appendLine(renderAgentMessage(response));
|
|
1545
1676
|
// 停止思考动画
|
|
1546
1677
|
inkSetThinking(false);
|
|
1547
|
-
// 2026-08-04: run-end 经验整理 — 连续成功工具 ≥2 自动写 skill 候选
|
|
1678
|
+
// 2026-08-04: run-end 经验整理 — 连续成功工具 ≥2 自动写 skill 候选
|
|
1679
|
+
// 2026-08-10: 显示改走 transient 行 (颜文字位置): 开始时显示, 结束后清空 (显示为空),
|
|
1680
|
+
// 不再追加 ✨ 消息行 → 不残留显示效果
|
|
1548
1681
|
if (runEndOkSteps.length >= 2) {
|
|
1549
|
-
|
|
1682
|
+
inkSetTransient(`${C_DIM}(`・ω・´) 整理本轮经验中... ${runEndOkSteps.length} 个工具调用${RESET}`);
|
|
1550
1683
|
setImmediate(async () => {
|
|
1551
1684
|
try {
|
|
1552
1685
|
const { writeRunEndSkillCandidates } = await import('./agents/skill-writer.js');
|
|
1553
|
-
|
|
1554
|
-
if (r.wrote) {
|
|
1555
|
-
appendLine(`${C_OK}✨ (◕‿◕) 经验候选已写入: ${r.names}${RESET}`);
|
|
1556
|
-
}
|
|
1686
|
+
await writeRunEndSkillCandidates(runEndOkSteps, 'cli:interactive');
|
|
1557
1687
|
}
|
|
1558
1688
|
catch { /* 非致命, 静默 */ }
|
|
1689
|
+
finally {
|
|
1690
|
+
inkSetTransient(null); // 结束后去除显示效果 (显示为空)
|
|
1691
|
+
}
|
|
1559
1692
|
});
|
|
1560
1693
|
}
|
|
1561
1694
|
// 更新状态栏: 上下文进度 (2026-08-06: 每轮按当前 messageHistory 重算并写回 ContextManager,
|
|
@@ -50,6 +50,8 @@ const DEFAULTS = {
|
|
|
50
50
|
backoffFactor: 2,
|
|
51
51
|
maxSocialIntervalMs: 30 * 60_000,
|
|
52
52
|
goalReevalMs: 60 * 60_000,
|
|
53
|
+
// 2026-08-10: 自动整理心跳
|
|
54
|
+
organizeIntervalMs: 30 * 60_000,
|
|
53
55
|
};
|
|
54
56
|
const MAX_BACKOFF_LEVEL = 6;
|
|
55
57
|
export class AgentHeartbeat {
|
|
@@ -58,6 +60,9 @@ export class AgentHeartbeat {
|
|
|
58
60
|
lastInitiated = new Map();
|
|
59
61
|
beaconTimer = null;
|
|
60
62
|
socialTimer = null;
|
|
63
|
+
// 2026-08-10: 自动整理心跳 timer + 重入锁 (上一轮没跑完不重复触发)
|
|
64
|
+
organizeTimer = null;
|
|
65
|
+
organizeRunning = false;
|
|
61
66
|
started = false;
|
|
62
67
|
// === 生命周期状态 ===
|
|
63
68
|
phase = 'BOOTSTRAP';
|
|
@@ -78,6 +83,10 @@ export class AgentHeartbeat {
|
|
|
78
83
|
onPeerAlive: options.onPeerAlive,
|
|
79
84
|
onActivity: options.onActivity,
|
|
80
85
|
onLifecycleChange: options.onLifecycleChange,
|
|
86
|
+
organizeEnabled: options.organizeEnabled ?? true,
|
|
87
|
+
organizeIntervalMs: options.organizeIntervalMs ?? DEFAULTS.organizeIntervalMs,
|
|
88
|
+
organize: options.organize,
|
|
89
|
+
onOrganizeEvent: options.onOrganizeEvent,
|
|
81
90
|
beaconIntervalMs: options.beaconIntervalMs ?? DEFAULTS.beaconIntervalMs,
|
|
82
91
|
socialIntervalMs: options.socialIntervalMs ?? DEFAULTS.socialIntervalMs,
|
|
83
92
|
cooldownMs: options.cooldownMs ?? DEFAULTS.cooldownMs,
|
|
@@ -110,10 +119,15 @@ export class AgentHeartbeat {
|
|
|
110
119
|
if (this.isSocialEnabled()) {
|
|
111
120
|
this.scheduleSocial();
|
|
112
121
|
}
|
|
122
|
+
// 2026-08-10: 自动整理心跳 — 与社交独立, 社交关闭也照跑
|
|
123
|
+
if (this.opts.organizeEnabled && this.opts.organize) {
|
|
124
|
+
this.scheduleOrganize();
|
|
125
|
+
}
|
|
113
126
|
// 立即发一次 beacon, 让对端尽快看到自己
|
|
114
127
|
this.tickBeacon().catch(() => { });
|
|
115
128
|
console.log(`[heartbeat] 社交心跳已启动 (beacon=${this.opts.beaconIntervalMs}ms` +
|
|
116
|
-
`${this.isSocialEnabled() ? `, social=${this.opts.socialIntervalMs}ms, cooldown=${this.opts.cooldownMs}ms` : ', social=关闭'}
|
|
129
|
+
`${this.isSocialEnabled() ? `, social=${this.opts.socialIntervalMs}ms, cooldown=${this.opts.cooldownMs}ms` : ', social=关闭'}` +
|
|
130
|
+
`${this.opts.organizeEnabled && this.opts.organize ? `, organize=${this.opts.organizeIntervalMs}ms` : ', organize=关闭'} )`);
|
|
117
131
|
}
|
|
118
132
|
/** 优雅停止: 清理全部定时器 (供全局 runtime 的 SIGTERM/SIGINT 清理调用) */
|
|
119
133
|
stop() {
|
|
@@ -121,8 +135,11 @@ export class AgentHeartbeat {
|
|
|
121
135
|
clearInterval(this.beaconTimer);
|
|
122
136
|
if (this.socialTimer)
|
|
123
137
|
clearTimeout(this.socialTimer);
|
|
138
|
+
if (this.organizeTimer)
|
|
139
|
+
clearTimeout(this.organizeTimer);
|
|
124
140
|
this.beaconTimer = null;
|
|
125
141
|
this.socialTimer = null;
|
|
142
|
+
this.organizeTimer = null;
|
|
126
143
|
this.started = false;
|
|
127
144
|
this.setPhase('PAUSED');
|
|
128
145
|
console.log('[heartbeat] 社交心跳已停止 (定时器已清理)');
|
|
@@ -192,6 +209,50 @@ export class AgentHeartbeat {
|
|
|
192
209
|
});
|
|
193
210
|
}, this.currentSocialInterval());
|
|
194
211
|
}
|
|
212
|
+
// ===================== 自动整理心跳 (2026-08-10) =====================
|
|
213
|
+
// 与社交心跳并列的第三条心跳: 周期性整理 skills 经验 (候选进化) + 扫描遗留 skills.
|
|
214
|
+
// 与社交生命周期完全独立 — 社交关闭/退避 RESTING 不影响整理照跑.
|
|
215
|
+
/** 是否启用了自动整理 */
|
|
216
|
+
isOrganizeEnabled() {
|
|
217
|
+
return this.opts.enabled && this.opts.organizeEnabled && !!this.opts.organize;
|
|
218
|
+
}
|
|
219
|
+
scheduleOrganize() {
|
|
220
|
+
if (!this.started || !this.isOrganizeEnabled()) {
|
|
221
|
+
this.organizeTimer = null;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
this.organizeTimer = setTimeout(() => {
|
|
225
|
+
this.tickOrganize()
|
|
226
|
+
.catch((e) => console.warn('[heartbeat] organize tick 失败:', e?.message))
|
|
227
|
+
.finally(() => {
|
|
228
|
+
if (this.started && this.isOrganizeEnabled())
|
|
229
|
+
this.scheduleOrganize();
|
|
230
|
+
});
|
|
231
|
+
}, this.opts.organizeIntervalMs);
|
|
232
|
+
}
|
|
233
|
+
/** 跑一轮自动整理 (导出供测试/启动即跑: 每次打开后固定看一下 skills view) */
|
|
234
|
+
async tickOrganize() {
|
|
235
|
+
if (!this.isOrganizeEnabled() || this.organizeRunning)
|
|
236
|
+
return;
|
|
237
|
+
this.organizeRunning = true;
|
|
238
|
+
this.opts.onOrganizeEvent?.({ phase: 'start' });
|
|
239
|
+
try {
|
|
240
|
+
const self = await this.opts.self();
|
|
241
|
+
const r = await this.opts.organize({ self });
|
|
242
|
+
this.opts.onOrganizeEvent?.({
|
|
243
|
+
phase: 'end',
|
|
244
|
+
summary: r?.summary || (r?.done ? '完成' : ''),
|
|
245
|
+
});
|
|
246
|
+
return r;
|
|
247
|
+
}
|
|
248
|
+
catch (e) {
|
|
249
|
+
this.opts.onOrganizeEvent?.({ phase: 'error', error: e?.message || String(e) });
|
|
250
|
+
throw e;
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
this.organizeRunning = false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
195
256
|
/** 社交决策 tick: 先评估生命周期, 再决定是否对存活 peer 发起对话 */
|
|
196
257
|
async tickSocial() {
|
|
197
258
|
this.opts.onActivity?.();
|
package/dist/web/server.js
CHANGED
|
@@ -2332,6 +2332,49 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
2332
2332
|
ts: Date.now(),
|
|
2333
2333
|
}, 'p2p-global');
|
|
2334
2334
|
},
|
|
2335
|
+
// === 2026-08-10: 自动整理心跳 (与社交并列) ===
|
|
2336
|
+
// 周期性: ① 扫描 skills view 找遗留 skills ② 候选经验 LLM 完整进化 (不再只是记录工具).
|
|
2337
|
+
// 与社交生命周期独立 — 社交关闭 (BOLLOON_AGENT_HEARTBEAT_SOCIAL=0) 整理仍照跑.
|
|
2338
|
+
organizeEnabled: true,
|
|
2339
|
+
organizeIntervalMs: Number(process.env.BOLLOON_ORGANIZE_HEARTBEAT_MS) || 30 * 60_000,
|
|
2340
|
+
organize: async () => {
|
|
2341
|
+
try {
|
|
2342
|
+
watchdogRef?.recordActivity?.('agent-organize');
|
|
2343
|
+
}
|
|
2344
|
+
catch { }
|
|
2345
|
+
const { runAutoOrganize } = await import('../agents/skill-organizer.js');
|
|
2346
|
+
// 用第一个本地 channel 的 agent 做 LLM 完整经验进化 (拿不到则仅扫描)
|
|
2347
|
+
// 2026-08-10: getAgentForChannel 初始化可能挂起 → 8s 超时降级
|
|
2348
|
+
let llm;
|
|
2349
|
+
try {
|
|
2350
|
+
const channels = await loadChannels();
|
|
2351
|
+
const local = channels[0];
|
|
2352
|
+
if (local) {
|
|
2353
|
+
const agent = await Promise.race([
|
|
2354
|
+
getAgentForChannel(local.id, local.did || '', local.name, local.didDocRef).catch(() => null),
|
|
2355
|
+
new Promise((res) => setTimeout(() => res(null), 8000)),
|
|
2356
|
+
]);
|
|
2357
|
+
if (agent && typeof agent.promptStream === 'function') {
|
|
2358
|
+
llm = (p) => agent.promptStream(p, () => { }, undefined, local.id);
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
catch { /* 无 agent → 仅扫描 */ }
|
|
2363
|
+
const r = await runAutoOrganize({ llm, source: 'server:organize-heartbeat', evolve: !!llm });
|
|
2364
|
+
const kTotal = r.knowledge?.totalHandled ?? 0;
|
|
2365
|
+
return { done: true, summary: `进化 ${r.evolved.length} 个 skill, 遗留 ${r.leftovers.length} 个, 知识层 ${kTotal} 项` };
|
|
2366
|
+
},
|
|
2367
|
+
onOrganizeEvent: (evt) => {
|
|
2368
|
+
if (evt?.phase === 'start') {
|
|
2369
|
+
console.log('[heartbeat] 自动整理开始 (skills 遗留扫描 + 经验进化)');
|
|
2370
|
+
}
|
|
2371
|
+
else if (evt?.phase === 'end') {
|
|
2372
|
+
console.log(`[heartbeat] 自动整理完成${evt.summary ? `: ${evt.summary}` : ''}`);
|
|
2373
|
+
}
|
|
2374
|
+
else if (evt?.phase === 'error') {
|
|
2375
|
+
console.warn(`[heartbeat] 自动整理失败 (non-fatal): ${evt?.error || ''}`);
|
|
2376
|
+
}
|
|
2377
|
+
},
|
|
2335
2378
|
});
|
|
2336
2379
|
agentHeartbeat.start();
|
|
2337
2380
|
// 注册到全局, 让 24h HealthMonitor.checkHeartbeat 能观测到本智能体 (getDiscoveredAgents/isAntColonyEnabled)
|