@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,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExecutionSupervisor — 长期执行层 (2026-09-16, M2-B / 计划 §13 2-A+2-B)
|
|
3
|
+
*
|
|
4
|
+
* 职责边界 (刻意与 Harness 分开):
|
|
5
|
+
* Harness 只管「这一段能不能安全执行」
|
|
6
|
+
* Supervisor 才管「这个目标还要不要继续执行」
|
|
7
|
+
*
|
|
8
|
+
* 它是一个常驻 worker: 扫描可执行的 Goal → 抢 lease → 开新 Run 或恢复旧 Run → 执行 →
|
|
9
|
+
* 按结果决策 Goal 状态 → 写下一次唤醒信息 → 释放 lease → 换下一个 Goal。
|
|
10
|
+
*
|
|
11
|
+
* 它不塞进 Web request, 也不依赖浏览器是否打开。触发器可以复用 cron/heartbeat 的 tick,
|
|
12
|
+
* 但**事实来源永远是持久化记录** (GoalStore / RunStore / lease 文件), 不是内存状态。
|
|
13
|
+
*/
|
|
14
|
+
import * as os from 'os';
|
|
15
|
+
import { listRunnableGoals, claimGoal, heartbeatGoal, releaseGoal, readGoal, setContinuation, bumpContinuationAttempts, evaluateGoalCompletion, completeGoalIfEligible, addEvidence, } from './goal-store.js';
|
|
16
|
+
import { readRun, reconcileOrphans, superviseRuns, buildContinuationPlan, RESUMABLE_STATUSES, } from './run-store.js';
|
|
17
|
+
/** 自动继续的退避 (attempts → 等待毫秒)。0 次 = 立刻。 */
|
|
18
|
+
export function continuationBackoffMs(attempts) {
|
|
19
|
+
const steps = [0, 15_000, 60_000, 5 * 60_000, 15 * 60_000];
|
|
20
|
+
return steps[Math.min(Math.max(attempts, 0), steps.length - 1)];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Run 结果 → Goal 下一步。**Run done ≠ Goal completed** 是这里的核心不变式:
|
|
24
|
+
* Run 结束从来不自动等于目标完成, 必须过 evaluateGoalCompletion。
|
|
25
|
+
*/
|
|
26
|
+
export function decideGoalOutcome(goal, run, opts = {}) {
|
|
27
|
+
const now = opts.now ?? Date.now();
|
|
28
|
+
// maxAttempts = 允许的自动继续次数 (默认 2) → 第 3 次失败进 needs_human
|
|
29
|
+
const maxAttempts = opts.maxAttempts ?? 2;
|
|
30
|
+
const attempts = (goal.continuation?.attempts || 0);
|
|
31
|
+
const nextAction = run?.checkpoint?.nextAction;
|
|
32
|
+
// 这一轮没有失败/没有等待 → 自动继续计数清零 (连续失败才累加)
|
|
33
|
+
const base = { autoContinue: true, lastRunId: run?.runId, nextAction };
|
|
34
|
+
if (!run) {
|
|
35
|
+
return { goalStatus: 'active', continuation: { ...base, wakeReason: 'new_goal' }, reason: '还没有 Run' };
|
|
36
|
+
}
|
|
37
|
+
// 人定的状态优先, 不覆盖 (外部 pause/abort 是人的决定)
|
|
38
|
+
if (run.status === 'paused') {
|
|
39
|
+
return { goalStatus: 'paused', continuation: { ...base, autoContinue: false, wakeReason: 'paused' }, reason: '运行被人工暂停: 等 resume' };
|
|
40
|
+
}
|
|
41
|
+
switch (run.status) {
|
|
42
|
+
case 'interrupted':
|
|
43
|
+
return {
|
|
44
|
+
goalStatus: 'recovering',
|
|
45
|
+
continuation: { ...base, wakeReason: 'recovering' },
|
|
46
|
+
reason: '进程中断 (crash): 从 checkpoint 恢复, 不重头开始',
|
|
47
|
+
};
|
|
48
|
+
case 'stalled':
|
|
49
|
+
return {
|
|
50
|
+
goalStatus: 'stalled',
|
|
51
|
+
continuation: { ...base, wakeReason: 'stalled' },
|
|
52
|
+
reason: '运行失速 (心跳过期): 交 Supervisor 决策恢复或转人工',
|
|
53
|
+
};
|
|
54
|
+
case 'awaiting_external':
|
|
55
|
+
return {
|
|
56
|
+
goalStatus: 'awaiting_external',
|
|
57
|
+
continuation: {
|
|
58
|
+
...base,
|
|
59
|
+
wakeReason: 'awaiting_external',
|
|
60
|
+
needsExternal: String(run.error || '外部节点回复'),
|
|
61
|
+
},
|
|
62
|
+
reason: '在等外部事件: 不重发请求, 由事件唤醒',
|
|
63
|
+
};
|
|
64
|
+
case 'aborted':
|
|
65
|
+
return {
|
|
66
|
+
goalStatus: 'active',
|
|
67
|
+
continuation: { ...base, wakeReason: 'active', wakeAt: undefined },
|
|
68
|
+
reason: `运行被中止 (${run.errorClass || run.error || '预算/人工'}): 目标仍 active, 交给下一个 Run 继续`,
|
|
69
|
+
};
|
|
70
|
+
case 'done': {
|
|
71
|
+
const verdict = evaluateGoalCompletion(goal);
|
|
72
|
+
if (verdict.complete) {
|
|
73
|
+
return {
|
|
74
|
+
goalStatus: 'completed',
|
|
75
|
+
continuation: { ...base, autoContinue: false, wakeReason: 'completed' },
|
|
76
|
+
reason: `判据全部满足: ${verdict.reason}`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
// Run 说完成, 但 Goal 的判据/证据不足 → 不许装作完成 (这一轮没失败, 自动继续计数清零)
|
|
80
|
+
return {
|
|
81
|
+
goalStatus: 'active',
|
|
82
|
+
continuation: { ...base, wakeReason: 'active', wakeAt: undefined, attempts: 0 },
|
|
83
|
+
reason: `Run 已 done 但目标未达成 (${verdict.reason}) → 继续下一个 Run`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
case 'failed':
|
|
87
|
+
case 'needs_human': {
|
|
88
|
+
const cls = run.errorClass || 'unknown';
|
|
89
|
+
const needsHuman = ['auth', 'persist_failed', 'corrupt_state', 'policy_denied', 'repeat_failure', 'bad_args', 'no_such_tool'].includes(cls)
|
|
90
|
+
|| attempts >= maxAttempts;
|
|
91
|
+
if (needsHuman) {
|
|
92
|
+
return {
|
|
93
|
+
goalStatus: 'needs_human',
|
|
94
|
+
continuation: { ...base, autoContinue: false, wakeReason: 'needs_human', attempts },
|
|
95
|
+
reason: `${cls} 需要人工介入${attempts >= maxAttempts ? ` (自动继续已试 ${attempts} 次)` : ''}`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const wait = continuationBackoffMs(attempts);
|
|
99
|
+
return {
|
|
100
|
+
goalStatus: 'retry_wait',
|
|
101
|
+
continuation: {
|
|
102
|
+
...base,
|
|
103
|
+
wakeReason: 'retry_wait',
|
|
104
|
+
wakeAt: new Date(now + wait).toISOString(),
|
|
105
|
+
attempts: attempts + 1,
|
|
106
|
+
},
|
|
107
|
+
reason: `可恢复错误 ${cls}: 第 ${attempts + 1} 次自动继续, ${Math.round(wait / 1000)}s 后唤醒`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
case 'recovering':
|
|
111
|
+
return { goalStatus: 'recovering', continuation: { ...base, wakeReason: 'recovering' }, reason: '正在恢复' };
|
|
112
|
+
default: { // queued / running
|
|
113
|
+
return { goalStatus: 'active', continuation: { ...base, wakeReason: 'active' }, reason: `运行中 (${run.status})` };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
118
|
+
/** 调度上下文的时间戳 (不含 lease 镜像: 认领本身会写 lease, 不应把快照判成过期) */
|
|
119
|
+
function stampOf(g) {
|
|
120
|
+
return [g.status, g.currentRunId || '', String(g.runs.length), g.continuation?.updatedAt || '', g.goalId].join('|');
|
|
121
|
+
}
|
|
122
|
+
export class ExecutionSupervisor {
|
|
123
|
+
owner;
|
|
124
|
+
tickIntervalMs;
|
|
125
|
+
leaseTtlMs;
|
|
126
|
+
maxPerTick;
|
|
127
|
+
runner;
|
|
128
|
+
resolver;
|
|
129
|
+
now;
|
|
130
|
+
maxRetries;
|
|
131
|
+
onEvent;
|
|
132
|
+
logFn;
|
|
133
|
+
timer = null;
|
|
134
|
+
ticking = false;
|
|
135
|
+
tickCount = 0;
|
|
136
|
+
reconciledOnce = false;
|
|
137
|
+
lastReport = null;
|
|
138
|
+
constructor(opts = {}) {
|
|
139
|
+
this.owner = opts.owner || `${os.hostname()}:${process.pid}`;
|
|
140
|
+
this.tickIntervalMs = opts.tickIntervalMs ?? 30_000;
|
|
141
|
+
this.leaseTtlMs = opts.leaseTtlMs ?? 90_000;
|
|
142
|
+
this.maxPerTick = opts.maxPerTick ?? 1;
|
|
143
|
+
this.runner = opts.runner;
|
|
144
|
+
this.resolver = opts.resolver;
|
|
145
|
+
this.now = opts.now ?? (() => Date.now());
|
|
146
|
+
this.maxRetries = opts.maxRetries ?? (Number(process.env.BOLLOON_GOAL_MAX_RETRIES) >= 0 ? Number(process.env.BOLLOON_GOAL_MAX_RETRIES) : 2);
|
|
147
|
+
this.onEvent = opts.onEvent;
|
|
148
|
+
this.logFn = opts.log;
|
|
149
|
+
}
|
|
150
|
+
/** 有固定执行器或解析器 → 可以真执行; 都没有 → 只诊断 (dry-run) */
|
|
151
|
+
get canExecute() { return !!(this.runner || this.resolver); }
|
|
152
|
+
log(msg) {
|
|
153
|
+
this.logFn?.(msg);
|
|
154
|
+
}
|
|
155
|
+
emit(e) {
|
|
156
|
+
try {
|
|
157
|
+
this.onEvent?.(e);
|
|
158
|
+
}
|
|
159
|
+
catch { /* 观测失败不影响调度 */ }
|
|
160
|
+
}
|
|
161
|
+
get running() { return this.timer !== null; }
|
|
162
|
+
status() {
|
|
163
|
+
return {
|
|
164
|
+
owner: this.owner,
|
|
165
|
+
running: this.running,
|
|
166
|
+
tickIntervalMs: this.tickIntervalMs,
|
|
167
|
+
leaseTtlMs: this.leaseTtlMs,
|
|
168
|
+
maxPerTick: this.maxPerTick,
|
|
169
|
+
ticks: this.tickCount,
|
|
170
|
+
dryRun: !this.canExecute,
|
|
171
|
+
hasResolver: !!this.resolver,
|
|
172
|
+
lastReport: this.lastReport,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
start() {
|
|
176
|
+
if (this.timer)
|
|
177
|
+
return;
|
|
178
|
+
if (!this.canExecute)
|
|
179
|
+
this.log('[supervisor] 未注入 runner/resolver → 只诊断不执行 (dry-run)');
|
|
180
|
+
this.timer = setInterval(() => { void this.tickOnce().catch((err) => this.log(`[supervisor] tick 失败: ${err?.message}`)); }, this.tickIntervalMs);
|
|
181
|
+
this.timer.unref?.();
|
|
182
|
+
this.log(`[supervisor] 启动 owner=${this.owner} tick=${this.tickIntervalMs}ms leaseTtl=${this.leaseTtlMs}ms`);
|
|
183
|
+
void this.tickOnce().catch(() => { });
|
|
184
|
+
}
|
|
185
|
+
stop() {
|
|
186
|
+
if (this.timer) {
|
|
187
|
+
clearInterval(this.timer);
|
|
188
|
+
this.timer = null;
|
|
189
|
+
this.log('[supervisor] 停止');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** 一个调度周期 (可单测/可手动触发)。所有决策基于持久化记录。 */
|
|
193
|
+
async tickOnce() {
|
|
194
|
+
if (this.ticking)
|
|
195
|
+
return this.lastReport || this.emptyReport();
|
|
196
|
+
this.ticking = true;
|
|
197
|
+
this.tickCount++;
|
|
198
|
+
const report = { ...this.emptyReport(), tick: this.tickCount };
|
|
199
|
+
try {
|
|
200
|
+
// 1. 对账孤儿 (进程死了的 run) —— 只在启动后第一次做, 之后靠 tick 的常规巡检
|
|
201
|
+
if (!this.reconciledOnce) {
|
|
202
|
+
report.reconciled = await reconcileOrphans();
|
|
203
|
+
this.reconciledOnce = true;
|
|
204
|
+
if (report.reconciled.interrupted.length)
|
|
205
|
+
this.log(`[supervisor] 对账: ${report.reconciled.interrupted.length} 条僵尸 run → interrupted`);
|
|
206
|
+
}
|
|
207
|
+
report.supervised = await superviseRuns();
|
|
208
|
+
// 2. 扫描可执行 Goal
|
|
209
|
+
const { runnable, skipped } = await listRunnableGoals({ now: this.now(), owner: this.owner });
|
|
210
|
+
report.skipped = skipped;
|
|
211
|
+
// 3. 逐个推进 (每个 Goal: 抢 lease → 执行 → 决策 → 释放 lease)
|
|
212
|
+
for (const goal of runnable.slice(0, this.maxPerTick)) {
|
|
213
|
+
const claimed = await claimGoal(goal.goalId, { owner: this.owner, ttlMs: this.leaseTtlMs });
|
|
214
|
+
if (!claimed.ok) {
|
|
215
|
+
report.skipped.push({ goalId: goal.goalId, reason: claimed.reason || 'claim 失败' });
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
report.claimed.push(goal.goalId);
|
|
219
|
+
const leaseId = claimed.lease.leaseId;
|
|
220
|
+
try {
|
|
221
|
+
const res = await this.runGoal(goal, leaseId, report);
|
|
222
|
+
report.executed.push(res);
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
report.errors.push(`${goal.goalId}: ${err?.message || err}`);
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
const rel = await releaseGoal(goal.goalId, leaseId);
|
|
229
|
+
if (!rel.ok)
|
|
230
|
+
report.skipped.push({ goalId: goal.goalId, reason: rel.reason || 'release 失败' });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
report.errors.push(`tick: ${err?.message || err}`);
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
this.ticking = false;
|
|
239
|
+
this.lastReport = report;
|
|
240
|
+
}
|
|
241
|
+
return report;
|
|
242
|
+
}
|
|
243
|
+
emptyReport() {
|
|
244
|
+
return {
|
|
245
|
+
at: new Date().toISOString(), owner: this.owner, tick: this.tickCount,
|
|
246
|
+
reconciled: { interrupted: [], stillRunning: [], failed: [] },
|
|
247
|
+
supervised: { stalled: [], failed: [] },
|
|
248
|
+
claimed: [], executed: [], skipped: [], errors: [], dryRun: !this.runner,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
/** 认领后执行一个 Goal: 决定 resume 还是开新 Run → 跑 → 决策 Goal 状态 */
|
|
252
|
+
async runGoal(goal, leaseId, report) {
|
|
253
|
+
// 乐观并发检查: 认领后重新读一次 —— 若这个 Goal 在我扫描之后已被别的 worker 推进
|
|
254
|
+
// (状态/当前 run/run 列表/continuation 变了), 就让路。否则同一个状态版本会被两个 worker 各跑一次。
|
|
255
|
+
const freshGoal = await readGoal(goal.goalId);
|
|
256
|
+
if (!freshGoal || stampOf(freshGoal) !== stampOf(goal)) {
|
|
257
|
+
this.log(`[supervisor] goal=${goal.goalId} 扫描后状态已变 (别的 worker 推进过) → 让路`);
|
|
258
|
+
report.skipped.push({ goalId: goal.goalId, reason: '状态在扫描后被其它 worker 推进 (乐观并发检查) → 本周期不重复执行' });
|
|
259
|
+
return { goalId: goal.goalId, status: 'stale_skip' };
|
|
260
|
+
}
|
|
261
|
+
// 到点唤醒 (2-C.3): 这条 Goal 之前是 retry_wait —— 现在唤醒它, 旧的 wakeAt/wakeReason 必须清掉,
|
|
262
|
+
// 否则下一轮 tick 还会把它当"等时间"重复跳过 (或留下过期的等待事实)。
|
|
263
|
+
if (goal.continuation?.wakeReason === 'retry_wait' || goal.status === 'retry_wait') {
|
|
264
|
+
await setContinuation(goal.goalId, { wakeReason: 'active', wakeAt: undefined });
|
|
265
|
+
report.skipped.push({ goalId: goal.goalId, reason: `到点唤醒: 已清 wakeAt (第 ${(goal.continuation?.attempts || 0) + 1} 次自动继续)` });
|
|
266
|
+
this.emit({ kind: 'retry_woke', goalId: goal.goalId, message: `到点唤醒, 第 ${(goal.continuation?.attempts || 0) + 1} 次自动继续` });
|
|
267
|
+
this.log(`[supervisor] goal=${goal.goalId} retry_wait 到点 → 唤醒并清 wakeAt`);
|
|
268
|
+
}
|
|
269
|
+
const prevRunId = goal.currentRunId;
|
|
270
|
+
const prevRun = prevRunId ? await readRun(prevRunId) : null;
|
|
271
|
+
const plan = prevRunId ? await buildContinuationPlan(prevRunId).catch(() => null) : null;
|
|
272
|
+
const guards = plan?.replayGuards || [];
|
|
273
|
+
const instruction = plan
|
|
274
|
+
? `继续这个目标 (不要重头开始):\n目标: ${plan.objective || goal.objective}\n已完成 ${plan.completedSteps.length} 步; 下一步: ${plan.nextAction}`
|
|
275
|
+
: `开始执行这个目标:\n目标: ${goal.objective}${goal.successCriteria.length ? `\n完成判据: ${goal.successCriteria.join('; ')}` : ''}`;
|
|
276
|
+
const kind = !prevRun ? 'first_run'
|
|
277
|
+
: RESUMABLE_STATUSES.includes(prevRun.status) ? 'resume'
|
|
278
|
+
: 'continue_new_run';
|
|
279
|
+
if (!this.canExecute) {
|
|
280
|
+
this.log(`[supervisor] (dry-run) 会执行 goal=${goal.goalId} kind=${kind} prevRun=${prevRunId || '-'}`);
|
|
281
|
+
return { goalId: goal.goalId, runId: prevRunId, status: 'dry_run' };
|
|
282
|
+
}
|
|
283
|
+
// 执行器解析 (2-C.1): 解析不出来 → **只诊断, 不执行, 不写任何 Goal 状态**
|
|
284
|
+
let runner = this.runner;
|
|
285
|
+
if (!runner && this.resolver) {
|
|
286
|
+
let res;
|
|
287
|
+
try {
|
|
288
|
+
res = await this.resolver({ goal, kind, prevRunId, instruction, guards });
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
res = { ok: false, kind: 'none', reason: `resolver 抛错: ${String(err?.message || err).slice(0, 140)}` };
|
|
292
|
+
}
|
|
293
|
+
if (!res.ok || !res.runner) {
|
|
294
|
+
const why = res.reason || '解析不到执行器';
|
|
295
|
+
report.skipped.push({ goalId: goal.goalId, reason: `无执行器: ${why} (Goal 状态未改动)` });
|
|
296
|
+
this.emit({ kind: 'no_runner', goalId: goal.goalId, message: why });
|
|
297
|
+
this.log(`[supervisor] goal=${goal.goalId} 无执行器 → 只诊断, 不执行也不改状态: ${why}`);
|
|
298
|
+
return { goalId: goal.goalId, runId: prevRunId, status: 'unresolved', error: why };
|
|
299
|
+
}
|
|
300
|
+
runner = res.runner;
|
|
301
|
+
}
|
|
302
|
+
if (!runner) {
|
|
303
|
+
report.skipped.push({ goalId: goal.goalId, reason: '无执行器 (Goal 状态未改动)' });
|
|
304
|
+
return { goalId: goal.goalId, status: 'unresolved' };
|
|
305
|
+
}
|
|
306
|
+
// 执行期间持续续租: 续租失败 = 已被别人接管 → 记录 (不掩盖)
|
|
307
|
+
const hb = setInterval(() => {
|
|
308
|
+
void heartbeatGoal(goal.goalId, leaseId, this.leaseTtlMs).then((r) => {
|
|
309
|
+
if (!r.ok)
|
|
310
|
+
this.emit({ kind: 'lease_lost', goalId: goal.goalId, message: r.reason || '续租失败' });
|
|
311
|
+
});
|
|
312
|
+
}, Math.max(5_000, Math.floor(this.leaseTtlMs / 3)));
|
|
313
|
+
hb.unref?.();
|
|
314
|
+
let result;
|
|
315
|
+
const t0 = Date.now();
|
|
316
|
+
try {
|
|
317
|
+
result = await runner({ goal, kind, prevRunId, instruction, guards });
|
|
318
|
+
}
|
|
319
|
+
catch (err) {
|
|
320
|
+
result = { error: err?.message || String(err) };
|
|
321
|
+
}
|
|
322
|
+
finally {
|
|
323
|
+
clearInterval(hb);
|
|
324
|
+
}
|
|
325
|
+
// Run 结束 → Goal 决策 (确定性 reducer, 落盘)
|
|
326
|
+
const finalRunId = result.runId || prevRunId;
|
|
327
|
+
const finalRun = finalRunId ? await readRun(finalRunId) : null;
|
|
328
|
+
if (kind !== 'resume' && finalRun && finalRun.goalId !== goal.goalId) {
|
|
329
|
+
// 新 Run 必须挂在同一 Goal 下; 没挂上就是执行器没接住 goalId → 如实记, 不掩盖
|
|
330
|
+
report.errors.push(`${goal.goalId}: 新 Run ${finalRunId} 未绑定本 Goal (goalId=${finalRun.goalId || '空'})`);
|
|
331
|
+
}
|
|
332
|
+
const decision = decideGoalOutcome(goal, finalRun, { now: this.now(), maxAttempts: this.maxRetries });
|
|
333
|
+
await this.applyDecision(goal, decision, finalRun);
|
|
334
|
+
this.emit({ kind: 'goal_decision', goalId: goal.goalId, runId: finalRunId, message: `${finalRun?.status || result.status || '?'} → ${decision.goalStatus}: ${decision.reason}` });
|
|
335
|
+
this.log(`[supervisor] goal=${goal.goalId} run=${finalRunId || '-'} ${finalRun?.status || result.status || '?'} → goal=${decision.goalStatus} (${decision.reason}) ${Date.now() - t0}ms`);
|
|
336
|
+
return { goalId: goal.goalId, runId: finalRunId, status: finalRun?.status || result.status, error: result.error };
|
|
337
|
+
}
|
|
338
|
+
/** 把决策写进 Goal (+ 证据同步 + 完成出口), 并写下一次唤醒信息 */
|
|
339
|
+
async applyDecision(goal, decision, run) {
|
|
340
|
+
const { updateGoal } = await import('./goal-store.js');
|
|
341
|
+
// 证据同步: Run 的成功步骤 → Goal 证据 (长期执行的判据要有据可依)
|
|
342
|
+
if (run) {
|
|
343
|
+
const ev = run.steps.filter((s) => s.ok).slice(-5).map((s) => `${run.runId}/${s.tool}: ${String(s.summary || '(完成)').slice(0, 120)}`);
|
|
344
|
+
if (ev.length)
|
|
345
|
+
await addEvidence(goal.goalId, ev).catch(() => null);
|
|
346
|
+
}
|
|
347
|
+
// 唯一完成出口: 只有经 completeGoalIfEligible 才能把 Goal 判成 completed
|
|
348
|
+
if (decision.goalStatus === 'completed') {
|
|
349
|
+
const r = await completeGoalIfEligible(goal.goalId);
|
|
350
|
+
if (!r.ok) {
|
|
351
|
+
// 完成门拒绝 → 如实退回 active, 并保留原因 (不许装作完成)
|
|
352
|
+
await setContinuation(goal.goalId, { ...decision.continuation, wakeReason: 'active', autoContinue: true });
|
|
353
|
+
await updateGoal(goal.goalId, { status: 'active' });
|
|
354
|
+
this.log(`[supervisor] goal=${goal.goalId} 完成门拒绝: ${r.reason}`);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (decision.continuation.wakeReason !== 'completed')
|
|
358
|
+
await setContinuation(goal.goalId, decision.continuation);
|
|
359
|
+
this.emit({ kind: 'goal_completed', goalId: goal.goalId, runId: run?.runId, message: decision.reason });
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (decision.goalStatus !== goal.status)
|
|
363
|
+
await updateGoal(goal.goalId, { status: decision.goalStatus });
|
|
364
|
+
await setContinuation(goal.goalId, decision.continuation);
|
|
365
|
+
if (decision.continuation.wakeReason === 'needs_human') {
|
|
366
|
+
this.emit({ kind: 'needs_human', goalId: goal.goalId, message: decision.reason });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/** 外部事件到达 → 给对应 Goal 清除等待并加速唤醒 (2-E 第 4 类的入口) */
|
|
370
|
+
async notifyExternal(goalId) {
|
|
371
|
+
const g = await readGoal(goalId);
|
|
372
|
+
if (!g)
|
|
373
|
+
return false;
|
|
374
|
+
if (g.continuation?.wakeReason !== 'awaiting_external' && !g.continuation?.needsExternal)
|
|
375
|
+
return false;
|
|
376
|
+
await setContinuation(goalId, { wakeReason: 'active', needsExternal: undefined, autoContinue: true, wakeAt: undefined });
|
|
377
|
+
await bumpContinuationAttempts(goalId); // 记一次唤醒 (可观测)
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/** 单例 (Web/CLI 共享同一个进程内调度器; 跨进程靠 lease 排他) */
|
|
382
|
+
let singleton = null;
|
|
383
|
+
export function getSupervisor(opts) {
|
|
384
|
+
if (!singleton)
|
|
385
|
+
singleton = new ExecutionSupervisor(opts);
|
|
386
|
+
return singleton;
|
|
387
|
+
}
|
|
388
|
+
export function resetSupervisorForTest() {
|
|
389
|
+
singleton?.stop();
|
|
390
|
+
singleton = null;
|
|
391
|
+
}
|