@bolloon/bolloon-agent 0.4.23 → 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 +95 -18
- package/dist/ios/manifest.json +1 -1
- package/dist/ios/mobile-agent.js +244 -5
- package/dist/ios/mobile-chain.js +481 -0
- package/dist/ios/mobile-core.js +25177 -24664
- package/dist/ios/mobile-helia.js +683 -0
- package/dist/ios/mobile-ipfs.js +680 -0
- package/dist/ios/mobile-orbit.js +268 -0
- package/dist/ios/mobile-p2p.js +130 -1
- package/dist/ios/mobile-social.js +624 -0
- package/dist/ios/mobile-sync.js +193 -0
- package/dist/ios/mobile-trade.js +492 -0
- package/dist/ios/mobile-wallet.js +11 -0
- package/dist/ios/mobile.css +39 -0
- package/dist/ios/mobile.html +95 -18
- package/dist/ios/mobile.js +1054 -55
- package/dist/ios/routes-x402-info.js +194 -0
- package/dist/ios/server.js +350 -9
- package/dist/ios/sw.js +26 -2
- package/dist/web/agent-delegate-server.js +58 -12
- 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 +197 -3
- package/dist/web/mobile-core.js +16417 -16090
- 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 +437 -4
- package/package.json +2 -2
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* supervisor-host.ts — Supervisor 的「宿主分离」层 (2026-09-16, 批次 2-C.1)
|
|
3
|
+
*
|
|
4
|
+
* 之前的问题: Supervisor 的**启动点**长在 web server 里, 执行器也直接引用 web 的 channel agent
|
|
5
|
+
* → 长期执行能力事实上依附于「web 进程还活着」。
|
|
6
|
+
*
|
|
7
|
+
* 这一层把两件事彻底分开:
|
|
8
|
+
* 执行器 (runner) ← 由 RunnerResolver 在**每次执行前**解析 (web agent / CLI agent / 独立 agent / fake)
|
|
9
|
+
* 启动宿主 (host) ← web server / CLI `/supervise` / 独立 `bolloon supervise` / 测试进程, 都只是 host
|
|
10
|
+
*
|
|
11
|
+
* 冻结的接口:
|
|
12
|
+
* Supervisor { scheduler · lease · reducer · wake } + runnerResolver
|
|
13
|
+
* resolver 解析不出来 → **只诊断不执行, 且不写任何 Goal 状态** (绝不假装跑过, 也绝不误判完成)
|
|
14
|
+
*
|
|
15
|
+
* 宿主身份与状态落盘 (~/.bolloon/supervisor.json): owner / workerId / pid / host / 启动时间 /
|
|
16
|
+
* 最近 tick / tick 次数 / runner 种类 / 优雅停止时间 —— 让"谁在跑、跑到哪、为什么停"可查。
|
|
17
|
+
*/
|
|
18
|
+
import * as os from 'os';
|
|
19
|
+
import * as fsp from 'fs/promises';
|
|
20
|
+
import * as path from 'path';
|
|
21
|
+
import * as crypto from 'crypto';
|
|
22
|
+
import { acquireTickLock } from '../cron/tick-lock.js';
|
|
23
|
+
export function supervisorDir(home = os.homedir()) {
|
|
24
|
+
return path.join(home, '.bolloon', 'supervisor');
|
|
25
|
+
}
|
|
26
|
+
export function supervisorStatePath(home = os.homedir()) {
|
|
27
|
+
return path.join(home, '.bolloon', 'supervisor.json');
|
|
28
|
+
}
|
|
29
|
+
/** Supervisor tick 的跨进程锁 (与 cron 的 tick 锁同语义, 但不共用文件) */
|
|
30
|
+
export function supervisorTickLockPath(home = os.homedir()) {
|
|
31
|
+
return path.join(supervisorDir(home), '.tick.lock');
|
|
32
|
+
}
|
|
33
|
+
export async function readSupervisorState(home = os.homedir()) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(await fsp.readFile(supervisorStatePath(home), 'utf8'));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function writeSupervisorState(state, home = os.homedir()) {
|
|
42
|
+
const p = supervisorStatePath(home);
|
|
43
|
+
await fsp.mkdir(path.dirname(p), { recursive: true });
|
|
44
|
+
const tmp = `${p}.tmp`;
|
|
45
|
+
await fsp.writeFile(tmp, JSON.stringify(state, null, 2), 'utf8');
|
|
46
|
+
await fsp.rename(tmp, p); // 原子替换, 避免读到半截状态
|
|
47
|
+
}
|
|
48
|
+
export function newWorkerId() {
|
|
49
|
+
return `${Date.now().toString(36)}-${crypto.randomBytes(3).toString('hex')}`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 启动一个 Supervisor 宿主。
|
|
53
|
+
* 语义要点:
|
|
54
|
+
* - **跨进程单 tick 互斥**: 拿不到 tick 锁的宿主本轮直接让路 (不阻塞不排队), 并如实记原因;
|
|
55
|
+
* - **优雅停止**: stop() 会等当前 tick 跑完再退出 (不半路丢状态), 并落 stoppedAt/stopReason;
|
|
56
|
+
* - 宿主身份与心跳落 `~/.bolloon/supervisor.json`, 谁在跑/跑到哪可查。
|
|
57
|
+
*/
|
|
58
|
+
export async function runSupervisorHost(opts) {
|
|
59
|
+
const home = opts.home ?? os.homedir();
|
|
60
|
+
const log = opts.log ?? (() => { });
|
|
61
|
+
const workerId = newWorkerId();
|
|
62
|
+
const state = {
|
|
63
|
+
owner: opts.supervisor.owner,
|
|
64
|
+
workerId,
|
|
65
|
+
pid: process.pid,
|
|
66
|
+
host: os.hostname(),
|
|
67
|
+
startedAt: new Date().toISOString(),
|
|
68
|
+
runnerKind: opts.runnerKind ?? 'injected',
|
|
69
|
+
dryRun: !!opts.dryRun,
|
|
70
|
+
tickIntervalMs: opts.tickIntervalMs ?? 30_000,
|
|
71
|
+
leaseTtlMs: opts.leaseTtlMs ?? 90_000,
|
|
72
|
+
ticks: 0,
|
|
73
|
+
v: 1,
|
|
74
|
+
};
|
|
75
|
+
await writeSupervisorState(state, home);
|
|
76
|
+
const persist = async () => {
|
|
77
|
+
if (opts.extraState)
|
|
78
|
+
Object.assign(state, opts.extraState());
|
|
79
|
+
await writeSupervisorState(state, home);
|
|
80
|
+
};
|
|
81
|
+
// 让调用方随时能把状态写下去 (解析阶段报告一确定就可查, 不必等 tick 结束)
|
|
82
|
+
opts.onStateReady?.(persist);
|
|
83
|
+
const acquire = opts.acquireLock ?? acquireTickLock;
|
|
84
|
+
const useLock = opts.crossProcessLock !== false;
|
|
85
|
+
let stopping = false;
|
|
86
|
+
let ticking = false;
|
|
87
|
+
let timer = null;
|
|
88
|
+
const tickWithLock = async () => {
|
|
89
|
+
if (stopping || ticking)
|
|
90
|
+
return; // 进程内互斥
|
|
91
|
+
ticking = true;
|
|
92
|
+
const tickNo = (state.ticks ?? 0) + 1;
|
|
93
|
+
const startedMs = Date.now();
|
|
94
|
+
// 卡住的 tick 必须可观测 (长期运行时"没日志"和"卡死"必须能区分)
|
|
95
|
+
const watchdog = setInterval(() => {
|
|
96
|
+
log(`[supervisor-host] ⚠ tick #${tickNo} 已运行 ${Math.round((Date.now() - startedMs) / 1000)}s 仍未结束 (可能卡在 runner/session 创建)`);
|
|
97
|
+
}, 30_000);
|
|
98
|
+
watchdog.unref?.();
|
|
99
|
+
let lockPath;
|
|
100
|
+
let release;
|
|
101
|
+
try {
|
|
102
|
+
if (useLock) {
|
|
103
|
+
const r = await acquire({ lockPath: supervisorTickLockPath(home), staleMs: 300_000, log: () => { } });
|
|
104
|
+
if (!r.acquired) {
|
|
105
|
+
// 别的宿主在 tick → 本轮让路 (不阻塞), 原因写进状态, 可观测
|
|
106
|
+
state.lastTickAt = new Date().toISOString();
|
|
107
|
+
state.lastSummary = `本轮让路: tick 锁被 ${r.holder?.pid ?? '?'}@${r.holder?.host ?? '?'} 持有`;
|
|
108
|
+
await persist();
|
|
109
|
+
log(`[supervisor-host] ${state.lastSummary}`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
release = r.release;
|
|
113
|
+
lockPath = supervisorTickLockPath(home);
|
|
114
|
+
}
|
|
115
|
+
const rep = await opts.supervisor.tickOnce();
|
|
116
|
+
state.ticks = (state.ticks ?? 0) + 1;
|
|
117
|
+
state.lastTickAt = rep.at;
|
|
118
|
+
state.lastSummary = `#${rep.tick} 认领 ${rep.claimed.length} · 执行 ${rep.executed.length} · 跳过 ${rep.skipped.length}${rep.errors.length ? ` · 错误 ${rep.errors.length}` : ''}`;
|
|
119
|
+
await persist();
|
|
120
|
+
log(`[supervisor-host] ${state.lastSummary}`);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
state.lastTickAt = new Date().toISOString();
|
|
124
|
+
state.lastSummary = `tick 异常: ${String(err?.message || err).slice(0, 160)}`;
|
|
125
|
+
await persist().catch(() => { });
|
|
126
|
+
log(`[supervisor-host] ${state.lastSummary}`);
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
clearInterval(watchdog);
|
|
130
|
+
try {
|
|
131
|
+
await release?.();
|
|
132
|
+
}
|
|
133
|
+
catch { /* 锁释放失败: 陈旧回收兜底 */ }
|
|
134
|
+
void lockPath;
|
|
135
|
+
ticking = false;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
if ((opts.mode ?? 'interval') === 'once') {
|
|
139
|
+
await tickWithLock();
|
|
140
|
+
state.stoppedAt = new Date().toISOString();
|
|
141
|
+
state.stopReason = 'once';
|
|
142
|
+
await writeSupervisorState(state, home);
|
|
143
|
+
return { stop: async () => { }, state };
|
|
144
|
+
}
|
|
145
|
+
// 宿主驱动 tick (不用 supervisor 自己的 interval, 避免两条 tick 循环重复推进同一个 Goal)
|
|
146
|
+
timer = setInterval(() => { void tickWithLock(); }, state.tickIntervalMs);
|
|
147
|
+
if (!opts.keepAlive)
|
|
148
|
+
timer.unref?.();
|
|
149
|
+
log(`[supervisor-host] 启动 owner=${state.owner} worker=${workerId} tick=${state.tickIntervalMs}ms`);
|
|
150
|
+
// 启动后**立刻**跑一轮: 重启/接管时不该白等一个 tick 间隔才开始推进 (ticking 标志同步置位, stop() 会等它收尾)
|
|
151
|
+
void tickWithLock();
|
|
152
|
+
const stop = async (reason = 'requested') => {
|
|
153
|
+
if (stopping)
|
|
154
|
+
return;
|
|
155
|
+
stopping = true;
|
|
156
|
+
if (timer) {
|
|
157
|
+
clearInterval(timer);
|
|
158
|
+
timer = null;
|
|
159
|
+
}
|
|
160
|
+
opts.supervisor.stop();
|
|
161
|
+
// 等当前 tick 收尾 (最多 30s), 保证不半路丢状态
|
|
162
|
+
for (let i = 0; i < 300 && ticking; i++)
|
|
163
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
164
|
+
state.stoppedAt = new Date().toISOString();
|
|
165
|
+
state.stopReason = reason;
|
|
166
|
+
await persist().catch(() => { });
|
|
167
|
+
log(`[supervisor-host] 已优雅停止 (${reason})`);
|
|
168
|
+
};
|
|
169
|
+
return { stop, state };
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* 给独立宿主用的解析器: 按 Goal 的 channelId 建/复用**专用 agent session** 执行。
|
|
173
|
+
* 解析不出来 (没有 channelId / agent 不可用 / 显式关闭) → { ok:false }, Goal 只被诊断不被执行。
|
|
174
|
+
*/
|
|
175
|
+
/**
|
|
176
|
+
* 给独立宿主用的解析器: 委托给**分阶段** resolver (`runner-resolver.ts`)。
|
|
177
|
+
* 阶段报告通过 `onResolution` 交给宿主落盘 (lastResolution), 让 wakeReport/API/CLI 能回答
|
|
178
|
+
* "这个 Goal 为什么没被执行、卡在哪一阶段、耗时多少"。
|
|
179
|
+
*/
|
|
180
|
+
export function createLocalAgentResolver(opts) {
|
|
181
|
+
const cache = new Map();
|
|
182
|
+
return async (req) => {
|
|
183
|
+
const { resolveGoalRunner } = await import('./runner-resolver.js');
|
|
184
|
+
const res = await resolveGoalRunner(req, opts);
|
|
185
|
+
opts.onResolution?.(req.goal.goalId, res);
|
|
186
|
+
if (res.ok && res.runner)
|
|
187
|
+
cache.set(req.goal.goalId, res);
|
|
188
|
+
return res;
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* 起一个**独立**的 Supervisor 宿主。
|
|
193
|
+
* - 执行器由 `createLocalAgentResolver` 按 Goal 的 channelId 解析专用 agent session (懒建 + 复用);
|
|
194
|
+
* - `BOLLOON_SUPERVISE_AGENT=0` 或 dryRun → 解析器恒 ok:false → 只诊断不执行 (Goal 状态不动);
|
|
195
|
+
* - 常驻模式下进程保持存活, 收到 SIGINT/SIGTERM 优雅停止并落 stoppedAt。
|
|
196
|
+
*/
|
|
197
|
+
export async function runStandaloneSupervisorHost(opts = {}) {
|
|
198
|
+
const log = opts.log ?? ((m) => console.log(m));
|
|
199
|
+
const { ExecutionSupervisor } = await import('./execution-supervisor.js');
|
|
200
|
+
let lastResolution = null;
|
|
201
|
+
let persistNow = async () => { };
|
|
202
|
+
const resolver = opts.dryRun
|
|
203
|
+
? async (req) => ({ ok: false, kind: 'none', reason: `dry-run: 不执行 (goal=${req.goal.goalId})` })
|
|
204
|
+
: createLocalAgentResolver({
|
|
205
|
+
allow: process.env.BOLLOON_SUPERVISE_AGENT !== '0',
|
|
206
|
+
log,
|
|
207
|
+
home: opts.home,
|
|
208
|
+
onResolution: (goalId, res) => {
|
|
209
|
+
lastResolution = {
|
|
210
|
+
goalId, at: new Date().toISOString(), ok: !!res.ok,
|
|
211
|
+
failedStage: res.failedStage, reason: res.reason,
|
|
212
|
+
stages: (res.stages || []).map((x) => `${x.stage}${x.ok ? '✓' : '✗'}${x.ms}ms`).join(' → '),
|
|
213
|
+
detail: (res.stages || []).map((x) => ({ stage: x.stage, ok: x.ok, ms: x.ms, note: x.note, error: x.error, errorClass: x.errorClass })),
|
|
214
|
+
};
|
|
215
|
+
log(`[supervisor-host] 解析 ${res.ok ? '✅ 可执行' : `⛔ 卡在 ${res.failedStage}`} — ${res.reason || ''}`);
|
|
216
|
+
void persistNow().catch(() => { }); // 阶段报告立刻落盘
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
const sup = new ExecutionSupervisor({
|
|
220
|
+
resolver: resolver,
|
|
221
|
+
maxPerTick: opts.maxPerTick ?? (Number(process.env.BOLLOON_SUPERVISOR_MAX_PER_TICK) || 1),
|
|
222
|
+
tickIntervalMs: opts.tickIntervalMs ?? (Number(process.env.BOLLOON_SUPERVISOR_TICK_MS) || 30_000),
|
|
223
|
+
leaseTtlMs: opts.leaseTtlMs ?? (Number(process.env.BOLLOON_SUPERVISOR_LEASE_MS) || 90_000),
|
|
224
|
+
log,
|
|
225
|
+
});
|
|
226
|
+
const host = await runSupervisorHost({
|
|
227
|
+
supervisor: sup,
|
|
228
|
+
mode: opts.once ? 'once' : 'interval',
|
|
229
|
+
keepAlive: !opts.once,
|
|
230
|
+
tickIntervalMs: sup.status().tickIntervalMs,
|
|
231
|
+
leaseTtlMs: sup.status().leaseTtlMs,
|
|
232
|
+
runnerKind: opts.dryRun ? 'none' : 'standalone',
|
|
233
|
+
dryRun: !!opts.dryRun,
|
|
234
|
+
home: opts.home,
|
|
235
|
+
log,
|
|
236
|
+
extraState: () => (lastResolution ? { lastResolution } : {}),
|
|
237
|
+
onStateReady: (write) => { persistNow = write; },
|
|
238
|
+
});
|
|
239
|
+
if (opts.once) {
|
|
240
|
+
log(`[supervisor-host] once 完成: ${host.state.lastSummary || '(空)'}`);
|
|
241
|
+
return { ticks: host.state.ticks ?? 0, state: host.state, lastReport: sup.status().lastReport };
|
|
242
|
+
}
|
|
243
|
+
const onSignal = (sig) => {
|
|
244
|
+
void host.stop(`signal:${sig}`).then(() => process.exit(0));
|
|
245
|
+
};
|
|
246
|
+
process.once('SIGINT', () => onSignal('SIGINT'));
|
|
247
|
+
process.once('SIGTERM', () => onSignal('SIGTERM'));
|
|
248
|
+
return { ticks: host.state.ticks ?? 0, state: host.state, lastReport: null };
|
|
249
|
+
}
|
package/dist/cron/tick-lock.js
CHANGED
|
@@ -125,7 +125,7 @@ async function reclaim(lockPath, log, holder, reason) {
|
|
|
125
125
|
*/
|
|
126
126
|
export async function acquireTickLock(opts = {}) {
|
|
127
127
|
const home = opts.home ?? os.homedir();
|
|
128
|
-
const lockPath = tickLockPath(home);
|
|
128
|
+
const lockPath = opts.lockPath ?? tickLockPath(home);
|
|
129
129
|
const staleMs = opts.staleMs ?? DEFAULT_STALE_MS;
|
|
130
130
|
const now = (opts.now ?? (() => new Date()))();
|
|
131
131
|
const log = opts.log ?? noopLogger;
|