@bolloon/bolloon-agent 0.4.24 → 0.4.25
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/execution-supervisor.js +391 -0
- package/dist/agents/goal-store.js +451 -0
- package/dist/agents/pi-harness.js +263 -0
- package/dist/agents/pi-sdk.js +568 -126
- package/dist/agents/run-store.js +772 -0
- package/dist/agents/runner-resolver.js +225 -0
- package/dist/agents/skills-manager.js +435 -0
- package/dist/agents/supervisor-host.js +249 -0
- package/dist/cron/tick-lock.js +1 -1
- package/dist/index.js +428 -22
- package/dist/ios/agent-delegate-server.js +58 -12
- package/dist/ios/icons/icon-1024x1024.png +0 -0
- package/dist/ios/icons/icon-1024x1024.webp +0 -0
- package/dist/ios/icons/icon-216x216.png +0 -0
- package/dist/ios/icons/icon-216x216.webp +0 -0
- package/dist/ios/index.html +21 -1
- package/dist/ios/manifest.json +1 -1
- package/dist/ios/mobile-agent.js +195 -1
- package/dist/ios/mobile-core.js +24876 -24723
- package/dist/ios/mobile.css +15 -0
- package/dist/ios/mobile.html +21 -1
- package/dist/ios/mobile.js +143 -0
- package/dist/ios/server.js +51 -4
- package/dist/web/icons/icon-1024x1024.png +0 -0
- package/dist/web/icons/icon-1024x1024.webp +0 -0
- package/dist/web/icons/icon-216x216.png +0 -0
- package/dist/web/icons/icon-216x216.webp +0 -0
- package/dist/web/manifest.json +1 -1
- package/dist/web/mobile-agent.js +2 -2
- package/dist/web/mobile-core.js +24884 -24726
- package/dist/web/mobile-privacy.js +185 -0
- package/dist/web/mobile.css +15 -0
- package/dist/web/mobile.html +21 -1
- package/dist/web/mobile.js +179 -6
- package/dist/web/server.js +386 -0
- package/package.json +2 -2
package/dist/web/server.js
CHANGED
|
@@ -1530,6 +1530,11 @@ async function getAgentForChannel(channelId, channelDid, channelName, channelDid
|
|
|
1530
1530
|
usePivotLoop: true,
|
|
1531
1531
|
}, true); // forceNew: true 强制创建新实例
|
|
1532
1532
|
channelSessions.set(sessionKey, session);
|
|
1533
|
+
// 2026-09-16: 标记运行表面 — 落盘 run 记录里能区分 web 运行 (CLI 默认 cli)
|
|
1534
|
+
try {
|
|
1535
|
+
session.setRunSurface?.('web');
|
|
1536
|
+
}
|
|
1537
|
+
catch { /* 老 session 无此方法则忽略 */ }
|
|
1533
1538
|
if (channelDid) {
|
|
1534
1539
|
console.log(`[Agent] 新建频道 ${channelId} session, DID = ${channelDid}, sessionId = ${currentSessionId}`);
|
|
1535
1540
|
}
|
|
@@ -1626,6 +1631,92 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1626
1631
|
catch (err) {
|
|
1627
1632
|
console.warn('[createWebServer] bootstrap 失败 (非致命):', err);
|
|
1628
1633
|
}
|
|
1634
|
+
// 2026-09-16: 孤儿运行对账 — 上次进程留下的 running 记录改判 interrupted。
|
|
1635
|
+
// 不做这步的话, 刷新/重启后 UI 会显示"还在跑"的幽灵运行 (最典型的假状态)。
|
|
1636
|
+
try {
|
|
1637
|
+
const { reconcileOrphans } = await import('../agents/run-store.js');
|
|
1638
|
+
const rec = await reconcileOrphans();
|
|
1639
|
+
if (rec.interrupted.length) {
|
|
1640
|
+
console.log(`[runs] 对账: ${rec.interrupted.length} 条上次中断的运行已标 interrupted (${rec.interrupted.slice(0, 3).join(', ')}${rec.interrupted.length > 3 ? '…' : ''})`);
|
|
1641
|
+
}
|
|
1642
|
+
if (rec.stillRunning.length)
|
|
1643
|
+
console.log(`[runs] 存活运行: ${rec.stillRunning.length} 条`);
|
|
1644
|
+
}
|
|
1645
|
+
catch (err) {
|
|
1646
|
+
console.warn('[runs] 对账失败 (非致命):', err?.message);
|
|
1647
|
+
}
|
|
1648
|
+
// 2026-09-16 (M2-B): ExecutionSupervisor —— 长期执行层。
|
|
1649
|
+
// 页面关掉、进程重启后由它把 Goal 继续下去; 跨进程排他靠 Goal lease (不是内存状态)。
|
|
1650
|
+
// Harness 管"这一段能不能安全执行", Supervisor 管"这个目标还要不要继续执行"。
|
|
1651
|
+
if (process.env.BOLLOON_SUPERVISOR !== '0') {
|
|
1652
|
+
try {
|
|
1653
|
+
const { getSupervisor } = await import('../agents/execution-supervisor.js');
|
|
1654
|
+
const { runSupervisorHost } = await import('../agents/supervisor-host.js');
|
|
1655
|
+
const tickMs = Number(process.env.BOLLOON_SUPERVISOR_TICK_MS) || 30_000;
|
|
1656
|
+
const leaseMs = Number(process.env.BOLLOON_SUPERVISOR_LEASE_MS) || 90_000;
|
|
1657
|
+
// 执行器**在每次执行前解析** (不写死): web 宿主按 Goal 的 channelId 找 channel agent。
|
|
1658
|
+
// 解析不出来 → 只诊断不执行, 且不写任何 Goal 状态。
|
|
1659
|
+
const resolver = async (req) => {
|
|
1660
|
+
const goal = req.goal;
|
|
1661
|
+
if (!goal?.channelId) {
|
|
1662
|
+
return { ok: false, kind: 'none', reason: 'Goal 没有 channelId: 无法在 web 进程内解析执行器' };
|
|
1663
|
+
}
|
|
1664
|
+
const agent = await getAgentForChannel(goal.channelId, goal.agentId || '', undefined, undefined);
|
|
1665
|
+
if (!agent)
|
|
1666
|
+
return { ok: false, kind: 'none', reason: 'channel agent 不可用' };
|
|
1667
|
+
const runner = async (r) => {
|
|
1668
|
+
if (r.kind === 'resume' && r.prevRunId && typeof agent.resumeRun === 'function') {
|
|
1669
|
+
const res = await agent.resumeRun(r.prevRunId);
|
|
1670
|
+
return { runId: r.prevRunId, status: res?.ok ? 'done' : 'failed', error: res?.ok ? undefined : res?.reason };
|
|
1671
|
+
}
|
|
1672
|
+
// 跨预算继续: 新 Run 必须挂在同一个 Goal 下 (setGoalId), 并带上上一个 Run 的非幂等守卫
|
|
1673
|
+
agent.setGoalId?.(goal.goalId);
|
|
1674
|
+
agent.setContinuationGuards?.(r.guards || []);
|
|
1675
|
+
const reply = await agent.prompt(r.instruction);
|
|
1676
|
+
// 注意: prompt 收尾会清空 currentRunId → 必须读 lastRunId (否则会拿上一条 run 做决策)
|
|
1677
|
+
const runId = agent.getLastRunId?.() || agent.getRunId?.();
|
|
1678
|
+
return { runId, status: 'done', reply: typeof reply === 'string' ? reply.slice(0, 500) : undefined };
|
|
1679
|
+
};
|
|
1680
|
+
return { ok: true, runner, kind: 'web' };
|
|
1681
|
+
};
|
|
1682
|
+
const sup = getSupervisor({
|
|
1683
|
+
tickIntervalMs: tickMs,
|
|
1684
|
+
leaseTtlMs: leaseMs,
|
|
1685
|
+
maxPerTick: Number(process.env.BOLLOON_SUPERVISOR_MAX_PER_TICK) || 1,
|
|
1686
|
+
log: (m) => console.log(m),
|
|
1687
|
+
onEvent: (e) => { try {
|
|
1688
|
+
broadcast({ type: 'supervisor', ...e });
|
|
1689
|
+
}
|
|
1690
|
+
catch { /* UI 广播失败不影响调度 */ } },
|
|
1691
|
+
resolver: resolver,
|
|
1692
|
+
});
|
|
1693
|
+
// 宿主层: 跨进程单 tick 互斥 + 宿主身份/心跳落盘 + 优雅停止
|
|
1694
|
+
const host = await runSupervisorHost({
|
|
1695
|
+
supervisor: sup,
|
|
1696
|
+
tickIntervalMs: tickMs,
|
|
1697
|
+
leaseTtlMs: leaseMs,
|
|
1698
|
+
runnerKind: 'web',
|
|
1699
|
+
log: (m) => console.log(m),
|
|
1700
|
+
});
|
|
1701
|
+
global.__bolloonSupervisorHost = host;
|
|
1702
|
+
console.log(`[supervisor] 长期执行层已启动 (owner=${host.state.owner}, worker=${host.state.workerId}, tick=${tickMs}ms, lease=${leaseMs}ms)`);
|
|
1703
|
+
const stopHost = (sig) => { void host.stop(`signal:${sig}`).finally(() => process.exit(0)); };
|
|
1704
|
+
process.once('SIGINT', () => stopHost('SIGINT'));
|
|
1705
|
+
process.once('SIGTERM', () => stopHost('SIGTERM'));
|
|
1706
|
+
}
|
|
1707
|
+
catch (err) {
|
|
1708
|
+
// 启动失败必须可观测 (不许静默降级成长时间不执行)
|
|
1709
|
+
const msg = `[supervisor] 启动失败: ${err?.message || err}`;
|
|
1710
|
+
console.error(msg);
|
|
1711
|
+
try {
|
|
1712
|
+
broadcast({ type: 'supervisor', kind: 'startup_failed', message: msg });
|
|
1713
|
+
}
|
|
1714
|
+
catch { /* ignore */ }
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
else {
|
|
1718
|
+
console.log('[supervisor] 长期执行层已关闭 (BOLLOON_SUPERVISOR=0)');
|
|
1719
|
+
}
|
|
1629
1720
|
// 2026-08-03 (Context OS P5): 初始化资产层 12+3 目录 (幂等, 每层 README 声明职责边界)
|
|
1630
1721
|
try {
|
|
1631
1722
|
const { ensureContextOsDirs } = await import('../bootstrap/context-os.js');
|
|
@@ -2439,6 +2530,19 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
2439
2530
|
console.log(`[cron] 调度器已启动 (每 ${Math.round((Number(process.env.BOLLOON_CRON_HEARTBEAT_MS) || 60_000) / 1000)}s 一轮, tick 带锁 + DND)`);
|
|
2440
2531
|
// 暴露给 /message 使用 (主任务闸门)
|
|
2441
2532
|
global.__bolloonMainTask = { enterMainTask, exitMainTask };
|
|
2533
|
+
// 2026-09-16: 运行失速巡检 (每 60s) — running 但长时间没新进展的运行标 stalled,
|
|
2534
|
+
// 让 UI/CLI 能说"这个运行卡住了", 而不是永远转圈 (crash 由启动时对账兜底)。
|
|
2535
|
+
setInterval(() => {
|
|
2536
|
+
void (async () => {
|
|
2537
|
+
try {
|
|
2538
|
+
const { superviseRuns } = await import('../agents/run-store.js');
|
|
2539
|
+
const r = await superviseRuns();
|
|
2540
|
+
if (r.stalled.length)
|
|
2541
|
+
console.warn(`[runs] 失速巡检: ${r.stalled.length} 条运行标 stalled (${r.stalled.slice(0, 3).join(', ')})`);
|
|
2542
|
+
}
|
|
2543
|
+
catch { /* 巡检失败不影响主流程 */ }
|
|
2544
|
+
})();
|
|
2545
|
+
}, 60_000);
|
|
2442
2546
|
}
|
|
2443
2547
|
catch (cronErr) {
|
|
2444
2548
|
console.warn('[cron] 调度器启动失败 (non-fatal):', cronErr?.message);
|
|
@@ -2832,6 +2936,288 @@ ${goalDesc}
|
|
|
2832
2936
|
res.status(400).json({ ok: false, severity: 'block', reason: e?.message ?? 'parse error' });
|
|
2833
2937
|
}
|
|
2834
2938
|
});
|
|
2939
|
+
// 2026-09-16: 运行记录 (持久化 harness) — 当前 + 历史 agent 运行。跨重载可读。
|
|
2940
|
+
// 2026-09-16 (2-G.1): Skills Manager —— 与 CLI `/skills` `/skill` 读同一份事实 (skills-registry.json + 技能目录)
|
|
2941
|
+
app.get('/api/skills', async (_req, res) => {
|
|
2942
|
+
try {
|
|
2943
|
+
const { getSkillsManager } = await import('../agents/skills-manager.js');
|
|
2944
|
+
const sm = getSkillsManager();
|
|
2945
|
+
const skills = await sm.view();
|
|
2946
|
+
res.json({ count: skills.length, registryPath: (await import('../agents/skills-manager.js')).registryPath(), skills, health: await sm.health() });
|
|
2947
|
+
}
|
|
2948
|
+
catch (err) {
|
|
2949
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
2950
|
+
}
|
|
2951
|
+
});
|
|
2952
|
+
app.get('/api/skills/health', async (_req, res) => {
|
|
2953
|
+
try {
|
|
2954
|
+
const { getSkillsManager } = await import('../agents/skills-manager.js');
|
|
2955
|
+
res.json(await getSkillsManager().health());
|
|
2956
|
+
}
|
|
2957
|
+
catch (err) {
|
|
2958
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
2959
|
+
}
|
|
2960
|
+
});
|
|
2961
|
+
app.get('/api/skills/:name', async (req, res) => {
|
|
2962
|
+
try {
|
|
2963
|
+
const { getSkillsManager } = await import('../agents/skills-manager.js');
|
|
2964
|
+
const s = await getSkillsManager().inspect(String(req.params.name));
|
|
2965
|
+
if (!s) {
|
|
2966
|
+
res.status(404).json({ error: `没有这个技能: ${req.params.name}` });
|
|
2967
|
+
return;
|
|
2968
|
+
}
|
|
2969
|
+
res.json({ skill: s });
|
|
2970
|
+
}
|
|
2971
|
+
catch (err) {
|
|
2972
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
2973
|
+
}
|
|
2974
|
+
});
|
|
2975
|
+
app.post('/api/skills/import', async (req, res) => {
|
|
2976
|
+
try {
|
|
2977
|
+
const ref = String(req.body?.ref || '').trim();
|
|
2978
|
+
if (!ref) {
|
|
2979
|
+
res.status(400).json({ error: '要 body {ref: "bolloon://skill/<cid>" | "ipfs://<cid>" | "<cid>"}' });
|
|
2980
|
+
return;
|
|
2981
|
+
}
|
|
2982
|
+
const { getSkillsManager } = await import('../agents/skills-manager.js');
|
|
2983
|
+
const r = await getSkillsManager().import(ref, { force: !!req.body?.force });
|
|
2984
|
+
if (!r.ok) {
|
|
2985
|
+
res.status(409).json({ error: r.error });
|
|
2986
|
+
return;
|
|
2987
|
+
}
|
|
2988
|
+
res.json({ ok: true, name: r.name, version: r.version, skill: r.skill });
|
|
2989
|
+
}
|
|
2990
|
+
catch (err) {
|
|
2991
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
2992
|
+
}
|
|
2993
|
+
});
|
|
2994
|
+
// enable / disable / approve / validate / quarantine (Express 5 不支持内联正则, 显式写开)
|
|
2995
|
+
for (const action of ['enable', 'disable', 'approve', 'validate', 'quarantine']) {
|
|
2996
|
+
app.post(`/api/skills/:name/${action}`, async (req, res) => {
|
|
2997
|
+
try {
|
|
2998
|
+
const { getSkillsManager } = await import('../agents/skills-manager.js');
|
|
2999
|
+
const sm = getSkillsManager();
|
|
3000
|
+
const name = String(req.params.name);
|
|
3001
|
+
const r = action === 'approve' ? await sm.approve(name, 'web')
|
|
3002
|
+
: action === 'quarantine' ? await sm.quarantine(name, String(req.body?.reason || 'web 手动隔离'))
|
|
3003
|
+
: action === 'enable' ? await sm.enable(name)
|
|
3004
|
+
: action === 'disable' ? await sm.disable(name)
|
|
3005
|
+
: await sm.validate(name);
|
|
3006
|
+
if (!r.ok && !r.skill) {
|
|
3007
|
+
res.status(409).json({ error: r.reason || r.issues || '操作未完成' });
|
|
3008
|
+
return;
|
|
3009
|
+
}
|
|
3010
|
+
res.json({ ok: r.ok, name, action, skill: r.skill, issues: r.issues });
|
|
3011
|
+
}
|
|
3012
|
+
catch (err) {
|
|
3013
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3014
|
+
}
|
|
3015
|
+
});
|
|
3016
|
+
}
|
|
3017
|
+
// 2026-09-16 (M2-B): 长期执行层诊断 + 手动推进 —— 与 CLI /supervise 读同一份持久化记录
|
|
3018
|
+
app.get('/api/supervisor', async (_req, res) => {
|
|
3019
|
+
try {
|
|
3020
|
+
const { getSupervisor } = await import('../agents/execution-supervisor.js');
|
|
3021
|
+
const { readSupervisorState, supervisorStatePath } = await import('../agents/supervisor-host.js');
|
|
3022
|
+
const { wakeReport, listRunnableGoals } = await import('../agents/goal-store.js');
|
|
3023
|
+
const { runnable, skipped } = await listRunnableGoals({ now: Date.now() });
|
|
3024
|
+
res.json({
|
|
3025
|
+
supervisor: getSupervisor().status(),
|
|
3026
|
+
/** 宿主身份/心跳 (谁在跑、跑到哪、上次有没有好好停) */
|
|
3027
|
+
host: await readSupervisorState(),
|
|
3028
|
+
hostStatePath: supervisorStatePath(),
|
|
3029
|
+
wake: await wakeReport(),
|
|
3030
|
+
runnable: runnable.map((g) => ({ goalId: g.goalId, objective: g.objective, status: g.status })),
|
|
3031
|
+
skipped,
|
|
3032
|
+
});
|
|
3033
|
+
}
|
|
3034
|
+
catch (err) {
|
|
3035
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3036
|
+
}
|
|
3037
|
+
});
|
|
3038
|
+
// 手动踢一个调度周期 (调试/验收用; 不改变任何"事实来源")
|
|
3039
|
+
app.post('/api/supervisor/tick', async (_req, res) => {
|
|
3040
|
+
try {
|
|
3041
|
+
const { getSupervisor } = await import('../agents/execution-supervisor.js');
|
|
3042
|
+
const report = await getSupervisor().tickOnce();
|
|
3043
|
+
res.json({ ok: true, report });
|
|
3044
|
+
}
|
|
3045
|
+
catch (err) {
|
|
3046
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3047
|
+
}
|
|
3048
|
+
});
|
|
3049
|
+
// 2026-09-16 (2-E 第 4 类): 外部事件到达 → 唤醒在等它的 Goal (不重发已成功的请求)
|
|
3050
|
+
app.post('/api/goals/:goalId/wake', async (req, res) => {
|
|
3051
|
+
const goalId = String(req.params.goalId);
|
|
3052
|
+
try {
|
|
3053
|
+
const { getSupervisor } = await import('../agents/execution-supervisor.js');
|
|
3054
|
+
const { readGoal, setContinuation } = await import('../agents/goal-store.js');
|
|
3055
|
+
const g = await readGoal(goalId);
|
|
3056
|
+
if (!g) {
|
|
3057
|
+
res.status(404).json({ error: `goal 不存在: ${goalId}` });
|
|
3058
|
+
return;
|
|
3059
|
+
}
|
|
3060
|
+
const woke = await getSupervisor().notifyExternal(goalId);
|
|
3061
|
+
if (!woke) {
|
|
3062
|
+
// 没在等外部事件也要如实回答 (可能它其实在等人/在跑), 但允许显式加急
|
|
3063
|
+
if (req.body?.force) {
|
|
3064
|
+
await setContinuation(goalId, { wakeReason: 'active', autoContinue: true, wakeAt: undefined, needsExternal: undefined });
|
|
3065
|
+
res.json({ ok: true, goalId, woke: false, forced: true, note: `原状态 ${g.status} (不在等外部事件), 已按 force 加急` });
|
|
3066
|
+
return;
|
|
3067
|
+
}
|
|
3068
|
+
res.status(409).json({ error: `Goal 不在等外部事件 (status=${g.status}, wakeReason=${g.continuation?.wakeReason || '无'})`, force_hint: '加 body {"force":true} 可强制加急' });
|
|
3069
|
+
return;
|
|
3070
|
+
}
|
|
3071
|
+
res.json({ ok: true, goalId, woke: true, note: '已唤醒: 下一次 Supervisor tick 会推进它' });
|
|
3072
|
+
}
|
|
3073
|
+
catch (err) {
|
|
3074
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3075
|
+
}
|
|
3076
|
+
});
|
|
3077
|
+
// 2026-09-16 (M5): 运行列表 —— Web 与 CLI 读同一份 run 事实 (~/.bolloon/runs)
|
|
3078
|
+
app.get('/api/runs', async (req, res) => {
|
|
3079
|
+
try {
|
|
3080
|
+
const { listRuns, formatRunLine } = await import('../agents/run-store.js');
|
|
3081
|
+
const status = String(req.query?.status || '').trim();
|
|
3082
|
+
const limit = Number(req.query?.limit || 20);
|
|
3083
|
+
const runs = await listRuns({ status: status ? status : undefined, limit });
|
|
3084
|
+
res.json({
|
|
3085
|
+
count: runs.length,
|
|
3086
|
+
runs,
|
|
3087
|
+
lines: runs.map(formatRunLine),
|
|
3088
|
+
});
|
|
3089
|
+
}
|
|
3090
|
+
catch (err) {
|
|
3091
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3092
|
+
}
|
|
3093
|
+
});
|
|
3094
|
+
// 2026-09-16 (M5): 单个 run + 停止原因/checkpoint/最近步骤/recovery/harness 事件 (两端同一份事实)
|
|
3095
|
+
app.get('/api/runs/:runId', async (req, res) => {
|
|
3096
|
+
try {
|
|
3097
|
+
const { readRun } = await import('../agents/run-store.js');
|
|
3098
|
+
const run = await readRun(String(req.params.runId));
|
|
3099
|
+
if (!run) {
|
|
3100
|
+
res.status(404).json({ error: `run 不存在: ${req.params.runId}` });
|
|
3101
|
+
return;
|
|
3102
|
+
}
|
|
3103
|
+
const goal = run.goalId ? await (await import('../agents/goal-store.js')).readGoal(run.goalId) : null;
|
|
3104
|
+
res.json({ run, goal, checkpoint: run.checkpoint, steps: run.steps, recovery: run.recovery, harness: run.harness || [] });
|
|
3105
|
+
}
|
|
3106
|
+
catch (err) {
|
|
3107
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3108
|
+
}
|
|
3109
|
+
});
|
|
3110
|
+
// 2026-09-16 (M5): 继续 interrupted/stalled/paused/needs_human/awaiting_external 的运行 —— 真从 checkpoint 继续
|
|
3111
|
+
app.post('/api/runs/:runId/resume', async (req, res) => {
|
|
3112
|
+
const runId = String(req.params.runId);
|
|
3113
|
+
try {
|
|
3114
|
+
const { readRun, RESUMABLE_STATUSES } = await import('../agents/run-store.js');
|
|
3115
|
+
const run = await readRun(runId);
|
|
3116
|
+
if (!run) {
|
|
3117
|
+
res.status(404).json({ error: `run 不存在: ${runId}` });
|
|
3118
|
+
return;
|
|
3119
|
+
}
|
|
3120
|
+
// 先做只读校验: 不可恢复的状态明确拒绝 (不假装"已开始"), 避免前端以为在跑
|
|
3121
|
+
if (!RESUMABLE_STATUSES.includes(run.status)) {
|
|
3122
|
+
res.status(409).json({ error: `状态 ${run.status} 不可恢复 (可恢复: ${RESUMABLE_STATUSES.join('/')})` });
|
|
3123
|
+
return;
|
|
3124
|
+
}
|
|
3125
|
+
if (!run.channelId) {
|
|
3126
|
+
res.status(400).json({ error: '该 run 没有 channelId, 无法在 web 端恢复 (请在 CLI 用 /resume)' });
|
|
3127
|
+
return;
|
|
3128
|
+
}
|
|
3129
|
+
const agent = await getAgentForChannel(run.channelId);
|
|
3130
|
+
if (!agent?.resumeRun) {
|
|
3131
|
+
res.status(409).json({ error: '该 channel 的 agent 不支持 resumeRun' });
|
|
3132
|
+
return;
|
|
3133
|
+
}
|
|
3134
|
+
// 异步执行: resume 可能跑几分钟; 状态从 /api/runs/:runId 读 (recovering → running → done/failed)
|
|
3135
|
+
void agent.resumeRun(runId).then((r) => {
|
|
3136
|
+
if (!r?.ok)
|
|
3137
|
+
console.warn(`[runs] resume ${runId} 失败:`, r?.reason);
|
|
3138
|
+
}).catch((err) => console.warn(`[runs] resume ${runId} 异常:`, err?.message));
|
|
3139
|
+
res.json({ ok: true, accepted: true, runId, note: '已开始恢复; 状态请读 GET /api/runs/:runId' });
|
|
3140
|
+
}
|
|
3141
|
+
catch (err) {
|
|
3142
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3143
|
+
}
|
|
3144
|
+
});
|
|
3145
|
+
// 2026-09-16 (M5): 人工批准 —— needs_human 的运行经人确认后继续 (= 带批准的 resume)
|
|
3146
|
+
app.post('/api/runs/:runId/approve', async (req, res) => {
|
|
3147
|
+
const runId = String(req.params.runId);
|
|
3148
|
+
try {
|
|
3149
|
+
const { readRun, recordRecovery } = await import('../agents/run-store.js');
|
|
3150
|
+
const run = await readRun(runId);
|
|
3151
|
+
if (!run) {
|
|
3152
|
+
res.status(404).json({ error: `run 不存在: ${runId}` });
|
|
3153
|
+
return;
|
|
3154
|
+
}
|
|
3155
|
+
if (run.status !== 'needs_human') {
|
|
3156
|
+
res.status(409).json({ error: `只有 needs_human 的运行需要批准 (当前 ${run.status})` });
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
await recordRecovery(runId, { errorClass: run.errorClass || 'unknown', message: '人工批准后继续', action: 'resume' });
|
|
3160
|
+
if (!run.channelId) {
|
|
3161
|
+
res.status(400).json({ error: '该 run 没有 channelId, 请在 CLI 用 /approve' });
|
|
3162
|
+
return;
|
|
3163
|
+
}
|
|
3164
|
+
const agent = await getAgentForChannel(run.channelId, run.channelId, run.agentId, {});
|
|
3165
|
+
if (!agent?.resumeRun) {
|
|
3166
|
+
res.status(409).json({ error: '该 channel 的 agent 不支持 resumeRun' });
|
|
3167
|
+
return;
|
|
3168
|
+
}
|
|
3169
|
+
void agent.resumeRun(runId).catch((err) => console.warn(`[runs] approve ${runId} 异常:`, err?.message));
|
|
3170
|
+
res.json({ ok: true, accepted: true, runId, approved: true });
|
|
3171
|
+
}
|
|
3172
|
+
catch (err) {
|
|
3173
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3174
|
+
}
|
|
3175
|
+
});
|
|
3176
|
+
// 2026-09-16 (M5): 暂停 / 中止 —— 只改 run 状态; 运行中的 agent 会在下一次循环检查时如实停下
|
|
3177
|
+
// (Express 5 的 path-to-regexp 不再支持 `:action(a|b)` 内联正则, 所以两条写开)
|
|
3178
|
+
for (const [suffix, to] of [['pause', 'paused'], ['abort', 'aborted']]) {
|
|
3179
|
+
app.post(`/api/runs/:runId/${suffix}`, async (req, res) => {
|
|
3180
|
+
const runId = String(req.params.runId);
|
|
3181
|
+
try {
|
|
3182
|
+
const { setRunStatus } = await import('../agents/run-store.js');
|
|
3183
|
+
const r = await setRunStatus(runId, to, { error: `外部请求 (${suffix})` });
|
|
3184
|
+
if (!r.ok) {
|
|
3185
|
+
res.status(409).json({ error: r.reason || '状态迁移被拒绝' });
|
|
3186
|
+
return;
|
|
3187
|
+
}
|
|
3188
|
+
res.json({ ok: true, runId, status: to, note: '运行中的 agent 会在下一轮循环检查时停下 (轮内不打断)' });
|
|
3189
|
+
}
|
|
3190
|
+
catch (err) {
|
|
3191
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3192
|
+
}
|
|
3193
|
+
});
|
|
3194
|
+
}
|
|
3195
|
+
// 2026-09-16 (M2): 目标 (Goal) —— 目标事实来源; runId → goalId → objective/successCriteria 反查链
|
|
3196
|
+
app.get('/api/goals', async (req, res) => {
|
|
3197
|
+
try {
|
|
3198
|
+
const { listGoals, formatGoalLine } = await import('../agents/goal-store.js');
|
|
3199
|
+
const status = String(req.query?.status || '').trim();
|
|
3200
|
+
const goals = await listGoals({ status: status ? status : undefined, limit: Number(req.query?.limit || 30) });
|
|
3201
|
+
res.json({ count: goals.length, goals, lines: goals.map(formatGoalLine) });
|
|
3202
|
+
}
|
|
3203
|
+
catch (err) {
|
|
3204
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3205
|
+
}
|
|
3206
|
+
});
|
|
3207
|
+
app.get('/api/goals/:goalId', async (req, res) => {
|
|
3208
|
+
try {
|
|
3209
|
+
const { readGoal, evaluateGoalCompletion } = await import('../agents/goal-store.js');
|
|
3210
|
+
const goal = await readGoal(String(req.params.goalId));
|
|
3211
|
+
if (!goal) {
|
|
3212
|
+
res.status(404).json({ error: `goal 不存在: ${req.params.goalId}` });
|
|
3213
|
+
return;
|
|
3214
|
+
}
|
|
3215
|
+
res.json({ goal, completion: evaluateGoalCompletion(goal) });
|
|
3216
|
+
}
|
|
3217
|
+
catch (err) {
|
|
3218
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3219
|
+
}
|
|
3220
|
+
});
|
|
2835
3221
|
// 2026-08-13 (Phase E1): Agent 服务 Registry — 发现层 (OrbitDB 去中心化 + 本地 fallback)
|
|
2836
3222
|
app.get('/api/registry', async (_req, res) => {
|
|
2837
3223
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bolloon/bolloon-agent",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.25",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
|
|
6
6
|
"main": "dist/cli-entry.js",
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
"vitest": "^5.0.0"
|
|
131
131
|
},
|
|
132
132
|
"build": {
|
|
133
|
-
"appId": "com.bolloon
|
|
133
|
+
"appId": "com.hibs.bolloon",
|
|
134
134
|
"productName": "Bolloon Agent",
|
|
135
135
|
"directories": {
|
|
136
136
|
"output": "release",
|