@bolloon/bolloon-agent 0.4.33 → 0.5.1
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/contacts/chain.js +27 -7
- package/dist/agents/execution-supervisor.js +580 -40
- package/dist/agents/external-events.js +35 -6
- package/dist/agents/gateway-group.js +100 -12
- package/dist/agents/goal-criteria.js +10 -3
- package/dist/agents/goal-flywheel/block-executor.js +346 -0
- package/dist/agents/goal-flywheel/continuation-decision.js +592 -0
- package/dist/agents/goal-flywheel/goal-change.js +754 -0
- package/dist/agents/goal-flywheel/index.js +26 -0
- package/dist/agents/goal-flywheel/memory-layers.js +227 -0
- package/dist/agents/goal-flywheel/run-closure.js +809 -0
- package/dist/agents/goal-flywheel/skill-candidate.js +628 -0
- package/dist/agents/goal-flywheel/types.js +329 -0
- package/dist/agents/goal-flywheel/wiring/change.js +288 -0
- package/dist/agents/goal-flywheel/wiring/closure.js +697 -0
- package/dist/agents/goal-flywheel/wiring/continuation.js +432 -0
- package/dist/agents/goal-flywheel/wiring/contract.js +270 -0
- package/dist/agents/goal-flywheel/wiring/index.js +26 -0
- package/dist/agents/goal-flywheel/wiring/monitor.js +254 -0
- package/dist/agents/goal-flywheel/wiring/seams.js +592 -0
- package/dist/agents/goal-flywheel/work-contract.js +478 -0
- package/dist/agents/goal-flywheel/work-monitor.js +488 -0
- package/dist/agents/goal-flywheel-wiring.js +1870 -0
- package/dist/agents/goal-state-reducer.js +406 -0
- package/dist/agents/goal-store.js +35 -1
- package/dist/agents/mobile-flywheel-view.js +144 -0
- package/dist/agents/mobile-task-actions-verify.js +74 -0
- package/dist/agents/mobile-task-actions.js +178 -0
- package/dist/agents/mobile-task-views.js +199 -0
- package/dist/agents/model-policy.js +651 -0
- package/dist/agents/network-pulse.js +304 -18
- package/dist/agents/pi-sdk-tools.js +84 -33
- package/dist/agents/pi-sdk.js +157 -42
- package/dist/agents/run-store.js +71 -5
- package/dist/agents/skill-readiness.js +52 -10
- package/dist/agents/subagent-manager.js +209 -3
- package/dist/agents/task/task-runner.js +17 -4
- package/dist/agents/task-group.js +8 -51
- package/dist/agents/task-public-text.js +89 -0
- package/dist/agents/workflow-pivot-loop.js +19 -12
- package/dist/cli/clipboard.js +65 -0
- package/dist/cli/commands/index.js +1 -0
- package/dist/cli/commands/tasks.js +350 -2
- package/dist/cli/identity-command.js +177 -0
- package/dist/cli/ink-app.js +145 -17
- package/dist/cli/log-gate.js +386 -0
- package/dist/cli/mention-data.js +2 -0
- package/dist/cli/model-selector.js +538 -0
- package/dist/cli/reply-hygiene.js +54 -0
- package/dist/cli/setup-wizard.js +757 -69
- package/dist/cli/startup-notice.js +160 -0
- package/dist/cli/stores.js +22 -1
- package/dist/cli/theme.js +49 -0
- package/dist/cli/tui-select.js +970 -0
- package/dist/cli/update-commands.js +150 -24
- package/dist/cli-entry.js +137 -20
- package/dist/electron-build/agents/contacts/chain.js +26 -6
- package/dist/electron-build/agents/contacts/chain.js.map +1 -1
- package/dist/electron-build/agents/execution-supervisor.js +578 -38
- package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
- package/dist/electron-build/agents/external-events.js +34 -5
- package/dist/electron-build/agents/external-events.js.map +1 -1
- package/dist/electron-build/agents/goal-criteria.js +10 -3
- package/dist/electron-build/agents/goal-criteria.js.map +1 -1
- package/dist/electron-build/agents/goal-flywheel/continuation-decision.js +598 -0
- package/dist/electron-build/agents/goal-flywheel/continuation-decision.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/goal-change.js +773 -0
- package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/memory-layers.js +269 -0
- package/dist/electron-build/agents/goal-flywheel/memory-layers.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/run-closure.js +818 -0
- package/dist/electron-build/agents/goal-flywheel/run-closure.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/skill-candidate.js +642 -0
- package/dist/electron-build/agents/goal-flywheel/skill-candidate.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/types.js +333 -0
- package/dist/electron-build/agents/goal-flywheel/types.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/change.js +294 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/change.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/closure.js +742 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/closure.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js +444 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/contract.js +279 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/contract.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/index.js +111 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/index.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js +264 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +612 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/work-contract.js +488 -0
- package/dist/electron-build/agents/goal-flywheel/work-contract.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/work-monitor.js +501 -0
- package/dist/electron-build/agents/goal-flywheel/work-monitor.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel-wiring.js +1962 -0
- package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -0
- package/dist/electron-build/agents/goal-state-reducer.js +414 -0
- package/dist/electron-build/agents/goal-state-reducer.js.map +1 -0
- package/dist/electron-build/agents/goal-store.js +35 -1
- package/dist/electron-build/agents/goal-store.js.map +1 -1
- package/dist/electron-build/agents/model-policy.js +706 -0
- package/dist/electron-build/agents/model-policy.js.map +1 -0
- package/dist/electron-build/agents/network-pulse.js +307 -19
- package/dist/electron-build/agents/network-pulse.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-tools.js +84 -33
- package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +156 -41
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/run-store.js +75 -6
- package/dist/electron-build/agents/run-store.js.map +1 -1
- package/dist/electron-build/agents/skill-readiness.js +52 -9
- package/dist/electron-build/agents/skill-readiness.js.map +1 -1
- package/dist/electron-build/agents/task/task-runner.js +16 -3
- package/dist/electron-build/agents/task/task-runner.js.map +1 -1
- package/dist/electron-build/agents/workflow-pivot-loop.js +19 -12
- package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
- package/dist/electron-build/llm/config-store.js +146 -6
- package/dist/electron-build/llm/config-store.js.map +1 -1
- package/dist/electron-build/llm/connection-probe.js +764 -0
- package/dist/electron-build/llm/connection-probe.js.map +1 -0
- package/dist/electron-build/llm/custom-provider-store.js +272 -0
- package/dist/electron-build/llm/custom-provider-store.js.map +1 -0
- package/dist/electron-build/llm/data/provider-catalog-baked.js +23 -0
- package/dist/electron-build/llm/data/provider-catalog-baked.js.map +1 -0
- package/dist/electron-build/llm/model-catalog.js +778 -0
- package/dist/electron-build/llm/model-catalog.js.map +1 -0
- package/dist/electron-build/llm/model-discovery.js +1202 -0
- package/dist/electron-build/llm/model-discovery.js.map +1 -0
- package/dist/electron-build/llm/model-selection.js +1315 -0
- package/dist/electron-build/llm/model-selection.js.map +1 -0
- package/dist/electron-build/llm/pi-ai.js +70 -11
- package/dist/electron-build/llm/pi-ai.js.map +1 -1
- package/dist/electron-build/llm/provider-catalog.js +969 -0
- package/dist/electron-build/llm/provider-catalog.js.map +1 -0
- package/dist/electron-build/llm/provider-registry.js +950 -0
- package/dist/electron-build/llm/provider-registry.js.map +1 -0
- package/dist/electron-build/llm/tool-name.js +266 -0
- package/dist/electron-build/llm/tool-name.js.map +1 -0
- package/dist/electron-build/orbitdb/cid-database.js +83 -17
- package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
- package/dist/electron-build/orbitdb/ipfs-node.js +31 -2
- package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
- package/dist/electron-build/utils/dual-source-facts.js +236 -0
- package/dist/electron-build/utils/dual-source-facts.js.map +1 -0
- package/dist/electron-build/utils/dual-source.js +347 -0
- package/dist/electron-build/utils/dual-source.js.map +1 -0
- package/dist/electron-build/utils/update-manager.js +328 -104
- package/dist/electron-build/utils/update-manager.js.map +1 -1
- package/dist/electron-build/utils/update-state.js +9 -11
- package/dist/electron-build/utils/update-state.js.map +1 -1
- package/dist/electron-build/utils/version-identity.js +193 -0
- package/dist/electron-build/utils/version-identity.js.map +1 -0
- package/dist/electron-build/utils/version-info.js +57 -31
- package/dist/electron-build/utils/version-info.js.map +1 -1
- package/dist/index.js +385 -108
- package/dist/ios/bolloon-web.json +10 -0
- package/dist/ios/chain-explorer.js +617 -0
- package/dist/ios/explorer.html +163 -0
- package/dist/ios/index.html +96 -0
- package/dist/ios/mobile-agent.js +2 -2
- package/dist/ios/mobile-contacts.js +362 -0
- package/dist/ios/mobile-core.js +3102 -310
- package/dist/ios/mobile-privacy.js +185 -0
- package/dist/ios/mobile-tasks.js +339 -0
- package/dist/ios/mobile-update.js +1442 -0
- package/dist/ios/mobile.css +19 -0
- package/dist/ios/mobile.html +96 -0
- package/dist/ios/mobile.js +1013 -41
- package/dist/ios/routes-chain-index.js +108 -0
- package/dist/ios/routes-contacts.js +420 -0
- package/dist/ios/routes-llm-config.js +344 -52
- package/dist/ios/routes-mobile-tasks.js +353 -0
- package/dist/ios/routes-onchain-trade.js +99 -0
- package/dist/ios/routes-x402-info.js +1 -0
- package/dist/ios/server.js +921 -0
- package/dist/ios/sw.js +1 -1
- package/dist/ios/task-frame-server.js +211 -0
- package/dist/llm/config-store.js +146 -6
- package/dist/llm/connection-probe.js +744 -0
- package/dist/llm/custom-provider-store.js +224 -0
- package/dist/llm/data/provider-catalog-baked.js +19 -0
- package/dist/llm/model-catalog.js +717 -0
- package/dist/llm/model-discovery.js +1137 -0
- package/dist/llm/model-selection.js +1241 -0
- package/dist/llm/pi-ai.js +70 -11
- package/dist/llm/provider-catalog.js +898 -0
- package/dist/llm/provider-registry.js +917 -0
- package/dist/llm/tool-name.js +220 -0
- package/dist/orbitdb/cid-database.js +81 -16
- package/dist/orbitdb/ipfs-node.js +31 -2
- package/dist/setup/onboard.js +34 -8
- package/dist/utils/dual-source-facts.js +223 -0
- package/dist/utils/dual-source.js +297 -0
- package/dist/utils/update-cli.js +16 -21
- package/dist/utils/update-health.js +35 -5
- package/dist/utils/update-manager.js +325 -96
- package/dist/utils/update-state.js +13 -10
- package/dist/utils/version-identity.js +176 -0
- package/dist/utils/version-info.js +27 -22
- package/dist/web/bolloon-web.json +10 -0
- package/dist/web/mobile-contacts.js +44 -11
- package/dist/web/mobile-core.js +2901 -580
- package/dist/web/mobile-tasks.js +339 -0
- package/dist/web/mobile-update.js +1442 -0
- package/dist/web/mobile.css +19 -0
- package/dist/web/mobile.html +64 -0
- package/dist/web/mobile.js +838 -35
- package/dist/web/routes-llm-config.js +344 -52
- package/dist/web/routes-mobile-tasks.js +353 -0
- package/dist/web/server.js +163 -4
- package/dist/web/sw.js +1 -1
- package/package.json +3 -1
- package/scripts/build-web.ts +36 -0
|
@@ -12,8 +12,21 @@
|
|
|
12
12
|
* 但**事实来源永远是持久化记录** (GoalStore / RunStore / lease 文件), 不是内存状态。
|
|
13
13
|
*/
|
|
14
14
|
import * as os from 'os';
|
|
15
|
-
import { listRunnableGoals, claimGoal, heartbeatGoal, releaseGoal, readGoal, setContinuation, bumpContinuationAttempts, evaluateGoalCompletion,
|
|
16
|
-
|
|
15
|
+
import { listRunnableGoals, claimGoal, heartbeatGoal, releaseGoal, readGoal, setContinuation, bumpContinuationAttempts, evaluateGoalCompletion, addEvidence, } from './goal-store.js';
|
|
16
|
+
// 2026-09-26 (P7 接线): 长期任务的模型策略 —— 「下一个 Run 用哪一份模型」的唯一决定函数,
|
|
17
|
+
// 以及「这次失败换模型能解决吗」的唯一判定函数 (Supervisor 不再自己看失败类别猜)。
|
|
18
|
+
import { resolveNextRunModel, supervisorMaySwitchModel, parseGoalModelPolicy, formatModelDecision, isTerminalRunStatus, } from './model-policy.js';
|
|
19
|
+
import { readRun, reconcileOrphans, superviseRuns, buildContinuationPlan, recordModelSwitch, RESUMABLE_STATUSES, } from './run-store.js';
|
|
20
|
+
// 2026-09-25 (飞轮接线): P0–P4 的七个模块经**唯一**适配层接进真实执行路径
|
|
21
|
+
// (节奏由进展决定 · Run 收尾必过 closeRun · 子 Agent 走工作合同 · 阻塞监控 · 变更注入 · 用户可见态)
|
|
22
|
+
// 2026-09-25 (串行收口): 四条线各自报"只有主线能加"的钩子在这里补齐 ——
|
|
23
|
+
// ① 节奏判定走 **M1 接缝的事实路径** (`preflightGoalStep`, 不再直连 `decideGoalStep`);
|
|
24
|
+
// ② Skill 试用在**候选产出处**开 (`closeGoalRun` 内部) / 在**下一条 Run 成功点**结算;
|
|
25
|
+
// ③ 阻塞巡检走 **M3 的统一巡检** (`flywheelSeams().monitor.sweepAll`, 不再自己拼一条循环);
|
|
26
|
+
// ④ 撤销走 **M4 的 change 接缝** (`ingestRequirement`, 非 web 宿主也能真停住在跑的 Run)。
|
|
27
|
+
import { claimedTerminalKindForRun, closeRunOnce, dispatchChildWork, flywheelSeams, flywheelTickNote, ingestRequirementViaSeam, installRunTerminalHook, isRefusal, markChangesConsumed, mergeGoalOutcome, nextRunChangeDirective, pendingContractDigest, preflightGoalStep, settleSkillTrialsForRun, } from './goal-flywheel-wiring.js';
|
|
28
|
+
// 2026-09-25 (M0 接线冻结, 规则 ②): Goal 状态只有一个写入出口
|
|
29
|
+
import { reduceGoalState } from './goal-state-reducer.js';
|
|
17
30
|
/** 自动继续的退避 (attempts → 等待毫秒)。0 次 = 立刻。 */
|
|
18
31
|
export function continuationBackoffMs(attempts) {
|
|
19
32
|
const steps = [0, 15_000, 60_000, 5 * 60_000, 15 * 60_000];
|
|
@@ -86,13 +99,19 @@ export function decideGoalOutcome(goal, run, opts = {}) {
|
|
|
86
99
|
case 'failed':
|
|
87
100
|
case 'needs_human': {
|
|
88
101
|
const cls = run.errorClass || 'unknown';
|
|
102
|
+
// ★ 2026-09-26 (P7 接线): 这次失败"换一个模型能不能解决"由**唯一判定函数**回答, 不再由
|
|
103
|
+
// Supervisor 这边看类别自己猜。结论照实写进 reason (含策略模式与理由), 但**不改写**任何
|
|
104
|
+
// 在跑的 Run —— 真正换模型发生在下一个 Run 的决定里 (`resolveNextRunModel`, 见 runGoal)。
|
|
105
|
+
const policyMode = parseGoalModelPolicy(goal.modelPolicy).policy.mode;
|
|
106
|
+
const modelVerdict = supervisorMaySwitchModel({ mode: policyMode, errorClass: run.errorClass });
|
|
107
|
+
const modelHint = `; 模型: 策略=${policyMode} · ${modelVerdict.reason}`;
|
|
89
108
|
const needsHuman = ['auth', 'persist_failed', 'corrupt_state', 'policy_denied', 'repeat_failure', 'bad_args', 'no_such_tool'].includes(cls)
|
|
90
109
|
|| attempts >= maxAttempts;
|
|
91
110
|
if (needsHuman) {
|
|
92
111
|
return {
|
|
93
112
|
goalStatus: 'needs_human',
|
|
94
113
|
continuation: { ...base, autoContinue: false, wakeReason: 'needs_human', attempts },
|
|
95
|
-
reason: `${cls} 需要人工介入${attempts >= maxAttempts ? ` (自动继续已试 ${attempts} 次)` : ''}`,
|
|
114
|
+
reason: `${cls} 需要人工介入${attempts >= maxAttempts ? ` (自动继续已试 ${attempts} 次)` : ''}${modelHint}`,
|
|
96
115
|
};
|
|
97
116
|
}
|
|
98
117
|
const wait = continuationBackoffMs(attempts);
|
|
@@ -104,7 +123,7 @@ export function decideGoalOutcome(goal, run, opts = {}) {
|
|
|
104
123
|
wakeAt: new Date(now + wait).toISOString(),
|
|
105
124
|
attempts: attempts + 1,
|
|
106
125
|
},
|
|
107
|
-
reason: `可恢复错误 ${cls}: 第 ${attempts + 1} 次自动继续, ${Math.round(wait / 1000)}s
|
|
126
|
+
reason: `可恢复错误 ${cls}: 第 ${attempts + 1} 次自动继续, ${Math.round(wait / 1000)}s 后唤醒${modelHint}`,
|
|
108
127
|
};
|
|
109
128
|
}
|
|
110
129
|
case 'recovering':
|
|
@@ -197,6 +216,11 @@ export class ExecutionSupervisor {
|
|
|
197
216
|
this.tickCount++;
|
|
198
217
|
const report = { ...this.emptyReport(), tick: this.tickCount };
|
|
199
218
|
try {
|
|
219
|
+
// 0. (2026-09-25, M0 接线冻结) 把"Run 终止 → 唯一责任链"的钩子装上:
|
|
220
|
+
// 崩溃恢复 / 失速这两条终止路径由 run-store 判出, 由接线层收尾 (规则 ④)。
|
|
221
|
+
await installRunTerminalHook().catch((err) => {
|
|
222
|
+
report.errors.push(`Run 终止钩子安装失败: ${err?.message || err}`);
|
|
223
|
+
});
|
|
200
224
|
// 1. 对账孤儿 (进程死了的 run) —— 只在启动后第一次做, 之后靠 tick 的常规巡检
|
|
201
225
|
if (!this.reconciledOnce) {
|
|
202
226
|
report.reconciled = await reconcileOrphans();
|
|
@@ -249,8 +273,102 @@ export class ExecutionSupervisor {
|
|
|
249
273
|
// 2. 扫描可执行 Goal
|
|
250
274
|
const { runnable, skipped } = await listRunnableGoals({ now: this.now(), owner: this.owner });
|
|
251
275
|
report.skipped = skipped;
|
|
252
|
-
|
|
276
|
+
const nowIso = new Date(this.now()).toISOString();
|
|
277
|
+
// 2.5 (2026-09-25, 飞轮接线 P3 + 串行收口): 阻塞巡检 —— **不看进程存活, 看任务是否卡住**
|
|
278
|
+
// 走 M3 的**统一巡检** (`flywheelSeams().monitor.sweepAll`): 一次调用 = 所有待回报 Goal 的
|
|
279
|
+
// 结论 (有界 + 失败留痕 + 每格带用户可见态)。之前 supervisor 自己拼了一条
|
|
280
|
+
// `listGoalsWithPendingWork → collectWorkBlocks → applyBlockHandling` 的循环 —— 那是
|
|
281
|
+
// 第二套巡检实现 (`sweepAll` 才是 M3 的门面), 于是 M3 的"统一巡检"在真路径上从来没有调用方。
|
|
282
|
+
let monitorView = null;
|
|
283
|
+
try {
|
|
284
|
+
const view = await flywheelSeams().monitor.sweepAll({
|
|
285
|
+
caller: 'supervisor',
|
|
286
|
+
now: nowIso,
|
|
287
|
+
runnerAvailable: this.canExecute,
|
|
288
|
+
});
|
|
289
|
+
if (isRefusal(view)) {
|
|
290
|
+
// 拒绝不是"没有阻塞": 如实记进 errors, 不许静默当成巡检通过
|
|
291
|
+
report.errors.push(`阻塞巡检被拒: ${view.reason}`);
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
monitorView = view;
|
|
295
|
+
report.monitor = view;
|
|
296
|
+
for (const cell of view.goals) {
|
|
297
|
+
for (const a of cell.handling?.actions ?? []) {
|
|
298
|
+
report.blocks.push({ goalId: cell.goalId, workId: a.workId, kind: a.kind, action: a.action, note: a.note });
|
|
299
|
+
}
|
|
300
|
+
if (cell.error)
|
|
301
|
+
report.errors.push(`阻塞巡检 goal=${cell.goalId}: ${cell.error}`);
|
|
302
|
+
}
|
|
303
|
+
const acted = view.goals.filter((c) => (c.handling?.actions.length ?? 0) > 0);
|
|
304
|
+
for (const c of acted) {
|
|
305
|
+
this.log(`[supervisor] goal=${c.goalId} 阻塞巡检: ${(c.handling?.actions ?? []).map((a) => `${a.kind}→${a.action}`).join(', ')}`);
|
|
306
|
+
}
|
|
307
|
+
const escalated = [...new Set(view.escalated)];
|
|
308
|
+
if (escalated.length) {
|
|
309
|
+
// 巡检未完成 (silentRisk) 与"交人"是两回事: 前者是巡检自身没查成, 后者是任务真的卡住
|
|
310
|
+
const why = view.silentRisk
|
|
311
|
+
? `巡检未完成 (silentRisk=true, 一个 Goal 都没查成): ${escalated.join(', ')}`
|
|
312
|
+
: `子 Agent 工作升级到人: ${escalated.join(', ')}`;
|
|
313
|
+
this.emit({ kind: 'needs_human', goalId: view.goals[0]?.goalId, message: why });
|
|
314
|
+
this.log(`[supervisor] 阻塞巡检 → 交人: ${why}`);
|
|
315
|
+
}
|
|
316
|
+
if (view.takeovers.length)
|
|
317
|
+
this.log(`[supervisor] 父接管子工作 (无心跳 + 执行权空闲 + 合同允许): ${view.takeovers.join(', ')}`);
|
|
318
|
+
if (view.requests.length)
|
|
319
|
+
this.log(`[supervisor] 阻塞巡检要求补齐: ${view.requests.join(', ')}`);
|
|
320
|
+
if (view.errors.length && !escalated.length)
|
|
321
|
+
report.errors.push(`阻塞巡检 (接缝自检): ${view.errors.join(' | ')}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
report.errors.push(`阻塞巡检: ${err?.message || err}`);
|
|
326
|
+
}
|
|
327
|
+
// 3. 逐个推进 (每个 Goal: 节奏判定 → 抢 lease → 执行 → 收尾 → 决策 → 释放 lease)
|
|
253
328
|
for (const goal of runnable.slice(0, this.maxPerTick)) {
|
|
329
|
+
// 3.1 (P0 + M1 串行收口) 节奏由进展决定: 走 **M1 接缝的事实路径**
|
|
330
|
+
// (`readFacts` 读 Goal/Run/进展/无进展连击 → 接缝用 P0 自己判 + 上限归因)。
|
|
331
|
+
// 事实拿不到时接缝内部才回落到旧注入口径, 并在 `step.source` 上如实标出来。
|
|
332
|
+
let step = null;
|
|
333
|
+
try {
|
|
334
|
+
const pre = await preflightGoalStep({
|
|
335
|
+
goalId: goal.goalId, now: nowIso, maxRetries: this.maxRetries, writeRecord: true,
|
|
336
|
+
});
|
|
337
|
+
if (pre && isRefusal(pre)) {
|
|
338
|
+
// 接缝真拒 (caller 不是 supervisor / 没有 goalId) → 如实记, 不静默跳过
|
|
339
|
+
report.errors.push(`${goal.goalId}: 节奏判定被拒: ${pre.reason}`);
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
step = pre;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
report.errors.push(`${goal.goalId}: 节奏判定失败 ${err?.message || err}`);
|
|
347
|
+
}
|
|
348
|
+
if (step) {
|
|
349
|
+
// 阻塞结论复用**同一 tick 的统一巡检**结果 (没有那一格才自己读): 一条 tick 里不查第二遍
|
|
350
|
+
const swept = monitorView?.goals.find((c) => c.goalId === goal.goalId)?.blocks ?? null;
|
|
351
|
+
report.flywheel.push(await flywheelTickNote({ goalId: goal.goalId, step, now: nowIso, blocks: swept }).catch(() => ({
|
|
352
|
+
goalId: goal.goalId,
|
|
353
|
+
decision: step.decision?.decision ?? 'first_run',
|
|
354
|
+
state: step.decision?.state ?? 'progressing',
|
|
355
|
+
runnable: step.runnable,
|
|
356
|
+
reason: step.reason,
|
|
357
|
+
noProgressStreak: step.noProgressStreak,
|
|
358
|
+
visibleState: 'executing',
|
|
359
|
+
nextAction: step.decision?.nextAction ?? '(首个 Run)',
|
|
360
|
+
source: step.source,
|
|
361
|
+
basis: step.basis,
|
|
362
|
+
caps: step.caps,
|
|
363
|
+
})));
|
|
364
|
+
if (!step.runnable) {
|
|
365
|
+
report.skipped.push({ goalId: goal.goalId, reason: `飞轮: ${step.reason}` });
|
|
366
|
+
await this.applyFlywheelStop(goal.goalId, step, nowIso).catch((err) => {
|
|
367
|
+
report.errors.push(`${goal.goalId}: 飞轮停写失败 ${err?.message || err}`);
|
|
368
|
+
});
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
254
372
|
const claimed = await claimGoal(goal.goalId, { owner: this.owner, ttlMs: this.leaseTtlMs });
|
|
255
373
|
if (!claimed.ok) {
|
|
256
374
|
report.skipped.push({ goalId: goal.goalId, reason: claimed.reason || 'claim 失败' });
|
|
@@ -259,7 +377,7 @@ export class ExecutionSupervisor {
|
|
|
259
377
|
report.claimed.push(goal.goalId);
|
|
260
378
|
const leaseId = claimed.lease.leaseId;
|
|
261
379
|
try {
|
|
262
|
-
const res = await this.runGoal(goal, leaseId, report);
|
|
380
|
+
const res = await this.runGoal(goal, leaseId, report, step);
|
|
263
381
|
report.executed.push(res);
|
|
264
382
|
}
|
|
265
383
|
catch (err) {
|
|
@@ -288,10 +406,58 @@ export class ExecutionSupervisor {
|
|
|
288
406
|
payments: { scanned: 0, reconciled: [], awaitingPayment: [], mustNotRepay: [], closed: [], goalsWoken: [], goalsFlagged: [], errors: [] },
|
|
289
407
|
supervised: { stalled: [], failed: [] },
|
|
290
408
|
claimed: [], executed: [], skipped: [], errors: [], dryRun: !this.runner,
|
|
409
|
+
flywheel: [], blocks: [], monitor: null, skillTrials: [], skillTrialOpenings: [], closures: [], workContracts: [],
|
|
291
410
|
};
|
|
292
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* 飞轮说"不该跑"时, 该不该把"停"写进 Goal?
|
|
414
|
+
*
|
|
415
|
+
* 只有**明确要求停**的决策才写 (fail / pause / 无进展熔断 / 硬底线 / 阻塞):
|
|
416
|
+
* 这些是"停就是停"的判断, 不写下去的话下一个 tick 还会来问一遍 (且用户看不到原因)。
|
|
417
|
+
* 其余 `ask_human` (例如"本轮没有新证据, 未到熔断阈值 → 需要人定夺") 只**跳过本轮**、
|
|
418
|
+
* 不动状态: 那属于"没有继续的资格", 由运输层(退避/等外部)与人决定 ——
|
|
419
|
+
* 这样既不会用轮次冒充节奏, 也不会把一次没证据的 Run 直接判死。
|
|
420
|
+
*/
|
|
421
|
+
async applyFlywheelStop(goalId, step, now) {
|
|
422
|
+
const d = step.decision;
|
|
423
|
+
if (!d)
|
|
424
|
+
return;
|
|
425
|
+
const hardLine = /硬底线/.test(step.reason);
|
|
426
|
+
const stop = d.decision === 'fail'
|
|
427
|
+
|| d.decision === 'pause'
|
|
428
|
+
|| (d.decision === 'ask_human' && (d.state === 'no_progress' || d.state === 'blocked'))
|
|
429
|
+
|| hardLine;
|
|
430
|
+
if (!stop)
|
|
431
|
+
return;
|
|
432
|
+
const status = d.decision === 'fail' ? 'failed' : d.decision === 'pause' ? 'paused' : 'needs_human';
|
|
433
|
+
// 规则 ②: 状态与 continuation 都经 Goal reducer 写 (终态保护 / 完成门也在这里, 不全仓各写一遍)
|
|
434
|
+
const applied = await reduceGoalState({
|
|
435
|
+
goalId,
|
|
436
|
+
intent: 'flywheel_stop',
|
|
437
|
+
now,
|
|
438
|
+
by: this.owner,
|
|
439
|
+
stopStatus: status,
|
|
440
|
+
reason: d.reason,
|
|
441
|
+
decisionId: d.decisionId,
|
|
442
|
+
nextAction: d.nextAction,
|
|
443
|
+
unresolvedItems: d.unresolvedItems,
|
|
444
|
+
});
|
|
445
|
+
if (!applied.ok) {
|
|
446
|
+
this.log(`[supervisor] goal=${goalId} 飞轮判停写不进 Goal (不掩盖): ${applied.reason}`);
|
|
447
|
+
}
|
|
448
|
+
this.emit({ kind: status === 'needs_human' ? 'needs_human' : status, goalId, message: d.reason });
|
|
449
|
+
this.log(`[supervisor] goal=${goalId} 飞轮判停 → ${status}: ${d.reason}`
|
|
450
|
+
+ `${applied.applied.length ? ` [${applied.applied.join(', ')}]` : ''}`);
|
|
451
|
+
}
|
|
293
452
|
/** 认领后执行一个 Goal: 决定 resume 还是开新 Run → 跑 → 决策 Goal 状态 */
|
|
294
|
-
async runGoal(goal, leaseId, report
|
|
453
|
+
async runGoal(goal, leaseId, report,
|
|
454
|
+
/**
|
|
455
|
+
* ★ 2026-09-25 (串行收口): 主循环**这一刻**已经落定的节奏判定 (3.1 的结果)。
|
|
456
|
+
* 传下来的理由: 「同一条 tick 里事实只读一次」。以前门禁自己又 `preflightGoalStep()` 了一遍,
|
|
457
|
+
* 于是同一次事实读留痕翻倍、而且门禁看到的是**另一个时刻**的事实 —— 与
|
|
458
|
+
* `flywheelTickNote` 复用统一巡检结论 (`blocks`) 是同一条纪律。
|
|
459
|
+
*/
|
|
460
|
+
tickStep = null) {
|
|
295
461
|
// 乐观并发检查: 认领后重新读一次 —— 若这个 Goal 在我扫描之后已被别的 worker 推进
|
|
296
462
|
// (状态/当前 run/run 列表/continuation 变了), 就让路。否则同一个状态版本会被两个 worker 各跑一次。
|
|
297
463
|
const freshGoal = await readGoal(goal.goalId);
|
|
@@ -302,11 +468,21 @@ export class ExecutionSupervisor {
|
|
|
302
468
|
}
|
|
303
469
|
// 到点唤醒 (2-C.3): 这条 Goal 之前是 retry_wait —— 现在唤醒它, 旧的 wakeAt/wakeReason 必须清掉,
|
|
304
470
|
// 否则下一轮 tick 还会把它当"等时间"重复跳过 (或留下过期的等待事实)。
|
|
471
|
+
// ★ M5-⑦ (2026-09-25 真跑): **状态也要一起拉回 `active`** —— 旧写法只清 continuation, `goal.status`
|
|
472
|
+
// 仍停在 `retry_wait`; 这条 Run 跑完后的收尾判定读到 stale 的 `retry_wait` → 判 `wait`, 合并规则
|
|
473
|
+
// 再把"等"落成 `awaiting_external` (wakeAt 已清空 ⇒ 只能靠事件唤醒) ⇒ **刚有进展的目标被挂起来,
|
|
474
|
+
// 没人会再跑它** (P6-③ 时钟用例阴性对照判红 + 临时探针复现)。走唯一漏斗 (规则 ②)。
|
|
305
475
|
if (goal.continuation?.wakeReason === 'retry_wait' || goal.status === 'retry_wait') {
|
|
306
|
-
await
|
|
476
|
+
await reduceGoalState({
|
|
477
|
+
goalId: goal.goalId,
|
|
478
|
+
intent: 'scheduled_wake',
|
|
479
|
+
now: new Date(this.now()).toISOString(),
|
|
480
|
+
by: this.owner,
|
|
481
|
+
reason: `到点唤醒 (wakeAt=${goal.continuation?.wakeAt ?? '-'} 已到) → 回到可继续, 清 wakeAt 并拉回 active`,
|
|
482
|
+
});
|
|
307
483
|
report.skipped.push({ goalId: goal.goalId, reason: `到点唤醒: 已清 wakeAt (第 ${(goal.continuation?.attempts || 0) + 1} 次自动继续)` });
|
|
308
484
|
this.emit({ kind: 'retry_woke', goalId: goal.goalId, message: `到点唤醒, 第 ${(goal.continuation?.attempts || 0) + 1} 次自动继续` });
|
|
309
|
-
this.log(`[supervisor] goal=${goal.goalId} retry_wait 到点 →
|
|
485
|
+
this.log(`[supervisor] goal=${goal.goalId} retry_wait 到点 → 唤醒 (状态拉回 active) 并清 wakeAt`);
|
|
310
486
|
}
|
|
311
487
|
// ★ 2026-09-18 (M2 收口): `bolloon task` 建的目标走**同一个恢复决策函数**
|
|
312
488
|
// (`decideTaskRecovery`) —— CLI `task --resume` 与 Supervisor 不再各判一次。
|
|
@@ -333,9 +509,68 @@ export class ExecutionSupervisor {
|
|
|
333
509
|
const prevRun = prevRunId ? await readRun(prevRunId) : null;
|
|
334
510
|
const plan = prevRunId ? await buildContinuationPlan(prevRunId).catch(() => null) : null;
|
|
335
511
|
const guards = plan?.replayGuards || [];
|
|
336
|
-
|
|
512
|
+
// ★ 2026-09-26 (P7 接线): 下一个 Run 该用**哪一份**模型配置 —— 唯一决定函数 `resolveNextRunModel`。
|
|
513
|
+
// 它自己走完四步: ① 读 Goal 策略 (记录上的 modelPolicy 优先, 没有才读 sidecar);
|
|
514
|
+
// ② 定稿配置 (auto → 最新 Global 默认; pinned → 固定三元组; session → 会话绑定);
|
|
515
|
+
// ③ 按**失败类别**判定能不能挑备用模型 (内部就是 `supervisorMaySwitchModel`);
|
|
516
|
+
// ④ 跨 Run 非幂等守卫照带 (模型换了也不重做副作用), 并把这次决定写成 Run 事件。
|
|
517
|
+
// 它算的是**下一条** Run: 在跑的那条 Run 的快照一个字都不动 (旧 Run 可追溯)。
|
|
518
|
+
//
|
|
519
|
+
// ⚠ **事件落点** (接线时真跑逼出来的, 不是设计洁癖): P7 自己文档里的事实边界是
|
|
520
|
+
// 「本层算出来的只用于①下一个 Run 的启动快照 ②切换事件 —— **它不回头改老 Run**」,
|
|
521
|
+
// 飞轮规则 ⑦ (`goal-flywheel-wiring.test.ts`) 更是**逐字节**钉住「已发生的 Run 记录不被改写」。
|
|
522
|
+
// 而决定函数的事件缺省落点是 `prevRunId` —— 上一条 Run **已经收尾**时, 那条写入等于
|
|
523
|
+
// 往历史记录上追加一格 (真跑实测: `updatedAt`/`modelSwitches`/`harness` 全变 ⇒ 判红)。
|
|
524
|
+
// 所以分两种, 都说清: 上一条 Run **还活着** → 事件就写它 (它是当事 Run, 也是缺省);
|
|
525
|
+
// **已收尾** → 它是历史, 决定"只算不写", 等新 Run 起来后把同一条决定记到**新 Run 自己**的
|
|
526
|
+
// 账本上 (`RunModelSwitchEvent.to` 的语义本来就是"这个 Run **或下一个 Run** 该用的那一份")。
|
|
527
|
+
const prevIsHistory = prevRun ? isTerminalRunStatus(prevRun.status) : true;
|
|
528
|
+
let modelResolution = null;
|
|
529
|
+
try {
|
|
530
|
+
// 备用候选: 只有上一个 Run 是**模型相关失败**时才值得去凑 (凑法在切换入口里, 只挑现在真能用的)
|
|
531
|
+
const fallbackCandidates = prevRun?.errorClass
|
|
532
|
+
? await (async () => {
|
|
533
|
+
const ms = await import('../llm/model-selection.js');
|
|
534
|
+
return await ms.usableFallbackCandidates(prevRun.modelConfig ?? null);
|
|
535
|
+
})().catch(() => [])
|
|
536
|
+
: [];
|
|
537
|
+
modelResolution = await resolveNextRunModel({
|
|
538
|
+
goalId: goal.goalId,
|
|
539
|
+
// 上一条 Run 已收尾 = 历史 → 不给事件落点 (P7 契约里"只算不写"的那一档), 免得往历史追加
|
|
540
|
+
...(prevIsHistory ? {} : { prevRunId: prevRunId || undefined }),
|
|
541
|
+
policy: goal.modelPolicy,
|
|
542
|
+
errorClass: prevRun?.errorClass,
|
|
543
|
+
...(fallbackCandidates.length ? { fallbackCandidates } : {}),
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
catch (err) {
|
|
547
|
+
// 算不出来**不猜**: 下一句日志说清, 执行器按自己的默认启动 (Run 记录里因此没有模型快照)
|
|
548
|
+
this.log(`[supervisor] goal=${goal.goalId} 下一个 Run 的模型决定算不出来 (按无快照启动): ${String(err?.message || err).slice(0, 160)}`);
|
|
549
|
+
}
|
|
550
|
+
const nextModelConfig = modelResolution?.startRunModelConfig;
|
|
551
|
+
if (modelResolution) {
|
|
552
|
+
this.log(`[supervisor] goal=${goal.goalId} 下一个 Run 模型: ${formatModelDecision(modelResolution.decision)}`
|
|
553
|
+
+ `; 策略来源=${modelResolution.policyOrigin}`
|
|
554
|
+
+ `${modelResolution.startRunModelConfig ? `; 启动快照 hash=${modelResolution.startRunModelConfig.configHash}` : '; 没有可信快照 → 不写快照'}`
|
|
555
|
+
+ `; 事件=${modelResolution.event.ok
|
|
556
|
+
? `已写(Run ${modelResolution.event.onRunId || '-'})`
|
|
557
|
+
: prevIsHistory && modelResolution.startRunModelConfig
|
|
558
|
+
? '等新 Run 起来后写在它自己的账本上 (上一条 Run 已收尾 = 历史, 不许改写)'
|
|
559
|
+
: `未写(${modelResolution.event.reason || '原因未明'})`}`);
|
|
560
|
+
}
|
|
561
|
+
// 2026-09-25 (飞轮接线 P4 / P2): 下一个 Run 必须真的读到**同一份**事实 ——
|
|
562
|
+
// ① 权威 continuation 的 nextAction (飞轮收尾写下的"下一步是什么");
|
|
563
|
+
// ② 已生效的新要求 (变更注入: 只影响后续 Run, 不改写历史);
|
|
564
|
+
// ③ 还在等回报的子 Agent 工作合同 (规则 5: 子必须拿到同一份合同)。
|
|
565
|
+
const continuationHint = goal.continuation?.nextAction ? `\n飞轮下一步 (权威 continuation): ${goal.continuation.nextAction}` : '';
|
|
566
|
+
const changeHint = await nextRunChangeDirective(goal.goalId).catch(() => null);
|
|
567
|
+
const contractHint = await pendingContractDigest(goal.goalId).catch(() => null);
|
|
568
|
+
const instruction = (plan
|
|
337
569
|
? `继续这个目标 (不要重头开始):\n目标: ${plan.objective || goal.objective}\n已完成 ${plan.completedSteps.length} 步; 下一步: ${plan.nextAction}`
|
|
338
|
-
: `开始执行这个目标:\n目标: ${goal.objective}${goal.successCriteria.length ? `\n完成判据: ${goal.successCriteria.join('; ')}` : ''}
|
|
570
|
+
: `开始执行这个目标:\n目标: ${goal.objective}${goal.successCriteria.length ? `\n完成判据: ${goal.successCriteria.join('; ')}` : ''}`)
|
|
571
|
+
+ continuationHint
|
|
572
|
+
+ (changeHint ? `\n${changeHint}` : '')
|
|
573
|
+
+ (contractHint ? `\n${contractHint}` : '');
|
|
339
574
|
const kind = !prevRun ? 'first_run'
|
|
340
575
|
: RESUMABLE_STATUSES.includes(prevRun.status) ? 'resume'
|
|
341
576
|
: 'continue_new_run';
|
|
@@ -348,7 +583,7 @@ export class ExecutionSupervisor {
|
|
|
348
583
|
if (!runner && this.resolver) {
|
|
349
584
|
let res;
|
|
350
585
|
try {
|
|
351
|
-
res = await this.resolver({ goal, kind, prevRunId, instruction, guards });
|
|
586
|
+
res = await this.resolver({ goal, kind, prevRunId, instruction, guards, ...(nextModelConfig ? { modelConfig: nextModelConfig } : {}) });
|
|
352
587
|
}
|
|
353
588
|
catch (err) {
|
|
354
589
|
res = { ok: false, kind: 'none', reason: `resolver 抛错: ${String(err?.message || err).slice(0, 140)}` };
|
|
@@ -367,12 +602,37 @@ export class ExecutionSupervisor {
|
|
|
367
602
|
return { goalId: goal.goalId, status: 'unresolved' };
|
|
368
603
|
}
|
|
369
604
|
// 2026-09-16 (2-G.2): 执行前技能就绪门禁 —— 缺/未启用/损坏/漂移 → 不启动 Run, Goal → needs_human
|
|
605
|
+
//
|
|
606
|
+
// ★ 2026-09-25 (P5 验收修复): 飞轮这一步的裁决是 **delegate** (缺能力 → 派给子 Agent 并签工作合同)
|
|
607
|
+
// 时, **本地技能门禁不适用** —— 这一步本来就不由本节点执行, 能力由子 Agent 凭合同交付。
|
|
608
|
+
// 原来一律拦成 `blocked_by_skills`, 于是"缺能力 → 派活"这条链在真路径上永远走不到,
|
|
609
|
+
// `dispatchIfDelegate` 成了死代码 (P5 ★①②③ 实测: 合同一份都没签发)。
|
|
610
|
+
// 其余裁决 (continue/first_run/...) 照旧: 技能不就绪照样不启动 Run (门禁没有放松)。
|
|
611
|
+
let flywheelDelegates = false;
|
|
612
|
+
let flywheelCapability = '';
|
|
613
|
+
// 与 3.1 **同一份**判定 (而且是**同一次**事实读): 主循环算好的 `tickStep` 直接沿用 ——
|
|
614
|
+
// 旧写法在这里又 `preflightGoalStep()` 了一遍 (判定口径同, 但事实读了两遍、时刻也不同)。
|
|
615
|
+
// `tickStep === null` (主循环没判成 / 门禁被单独调用) → 按"没有裁决"处理 (fail-closed, 照旧拦)。
|
|
616
|
+
{
|
|
617
|
+
flywheelDelegates = tickStep?.decision?.decision === 'delegate';
|
|
618
|
+
flywheelCapability = tickStep?.decision?.requiredCapability || '';
|
|
619
|
+
}
|
|
370
620
|
try {
|
|
371
|
-
const { ensureGoalSkillsReady, blockGoalOnSkills } = await import('./skill-readiness.js');
|
|
621
|
+
const { ensureGoalSkillsReady, blockGoalOnSkills, recordSkillReadiness } = await import('./skill-readiness.js');
|
|
372
622
|
const ready = await ensureGoalSkillsReady(goal);
|
|
373
623
|
for (const d of ready.degradations)
|
|
374
624
|
this.log(`[supervisor] goal=${goal.goalId} 技能降级: ${d}`);
|
|
375
|
-
if (!ready.ok) {
|
|
625
|
+
if (!ready.ok && flywheelDelegates) {
|
|
626
|
+
// 本地缺的能力由子 Agent 提供: 记清楚"放过门禁的原因", 不静默。
|
|
627
|
+
// 这里**不进** report.skipped —— 门禁放过 = 本轮**真跑**了 (Run + 之后的合同签发),
|
|
628
|
+
// 记成 skipped 会与事实相反。
|
|
629
|
+
await recordSkillReadiness(goal.goalId, ready).catch(() => { });
|
|
630
|
+
const why = `飞轮裁决=delegate (缺能力「${flywheelCapability || '-'}」) → 本地技能门禁不适用, `
|
|
631
|
+
+ `本轮照跑(不拦)并改为签发工作合同交给子 Agent; 本地缺: ${ready.reason || ''}`;
|
|
632
|
+
this.log(`[supervisor] goal=${goal.goalId} 技能门禁对 delegate 放行: ${why}`);
|
|
633
|
+
this.emit({ kind: 'skill_gate_waived_for_delegate', goalId: goal.goalId, message: why });
|
|
634
|
+
}
|
|
635
|
+
else if (!ready.ok) {
|
|
376
636
|
await blockGoalOnSkills(goal.goalId, ready);
|
|
377
637
|
report.skipped.push({ goalId: goal.goalId, reason: `技能未就绪: ${ready.reason}` });
|
|
378
638
|
this.emit({ kind: 'needs_human', goalId: goal.goalId, message: ready.reason || '技能未就绪' });
|
|
@@ -398,7 +658,7 @@ export class ExecutionSupervisor {
|
|
|
398
658
|
let result;
|
|
399
659
|
const t0 = Date.now();
|
|
400
660
|
try {
|
|
401
|
-
result = await runner({ goal, kind, prevRunId, instruction, guards });
|
|
661
|
+
result = await runner({ goal, kind, prevRunId, instruction, guards, ...(nextModelConfig ? { modelConfig: nextModelConfig } : {}) });
|
|
402
662
|
}
|
|
403
663
|
catch (err) {
|
|
404
664
|
result = { error: err?.message || String(err) };
|
|
@@ -406,6 +666,30 @@ export class ExecutionSupervisor {
|
|
|
406
666
|
finally {
|
|
407
667
|
clearInterval(hb);
|
|
408
668
|
}
|
|
669
|
+
// ★ 2026-09-26 (P7 接线): 决定"只算不写"的那一档, 把同一条决定**补写进它真正生效的那条 Run**。
|
|
670
|
+
// 为什么不写在算决定的那一刻: 那时新 Run 还没有 id, 而唯一的现成落点是已经收尾的上一条 Run
|
|
671
|
+
// —— 那是历史 (`prevIsHistory` 分支就是为了不碰它)。落点等执行器报回 runId 再定, 事实不丢。
|
|
672
|
+
const runIdOfThisRun = result.runId && result.runId !== prevRunId ? result.runId : null;
|
|
673
|
+
if (modelResolution && nextModelConfig && runIdOfThisRun && !modelResolution.event.ok
|
|
674
|
+
&& prevIsHistory && (await readRun(runIdOfThisRun).catch(() => null))) {
|
|
675
|
+
const d = modelResolution.decision;
|
|
676
|
+
const from = prevRun?.modelConfig ?? null;
|
|
677
|
+
// 同口径 = 同一份 `configHash` 就是同一份 (P7 的 configHash 契约), 别自己另立一套
|
|
678
|
+
const outcome = (d.conflict && d.conflict.length) ? 'conflict'
|
|
679
|
+
: (from && from.configHash === nextModelConfig.configHash) ? 'frozen' : 'switched';
|
|
680
|
+
const w = await recordModelSwitch(runIdOfThisRun, {
|
|
681
|
+
to: nextModelConfig,
|
|
682
|
+
from,
|
|
683
|
+
outcome,
|
|
684
|
+
mode: (d.mode === 'auto' || d.mode === 'pinned' || d.mode === 'session' ? d.mode : 'unknown'),
|
|
685
|
+
source: d.source,
|
|
686
|
+
reason: `${d.reason}; 事件落在新 Run (上一条 Run 已收尾 = 历史, 不许改写)`.slice(0, 400),
|
|
687
|
+
guardsCarried: guards.length,
|
|
688
|
+
goalId: goal.goalId,
|
|
689
|
+
});
|
|
690
|
+
this.log(`[supervisor] goal=${goal.goalId} 模型决定补写在新 Run ${runIdOfThisRun} 的账本上: `
|
|
691
|
+
+ `${w.ok ? `outcome=${outcome}` : `写不进去 (${w.reason || '原因未明'})`}`);
|
|
692
|
+
}
|
|
409
693
|
// Run 结束 → Goal 决策 (确定性 reducer, 落盘)
|
|
410
694
|
const finalRunId = result.runId || prevRunId;
|
|
411
695
|
const finalRun = finalRunId ? await readRun(finalRunId) : null;
|
|
@@ -434,55 +718,311 @@ export class ExecutionSupervisor {
|
|
|
434
718
|
}
|
|
435
719
|
// 重新读一次 Goal: Run 期间判据可能已被满足 (否则会拿旧快照判决)
|
|
436
720
|
const goalForDecision = (await readGoal(goal.goalId)) || goal;
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
721
|
+
const legacy = decideGoalOutcome(goalForDecision, finalRun, { now: this.now(), maxAttempts: this.maxRetries, lastRunStatus: finalRun?.status });
|
|
722
|
+
// ★ 2026-09-25 (飞轮接线 P1): Run 结束**必过收尾飞轮** —— 正常 / 失败 / 中断恢复都走同一条
|
|
723
|
+
// 9 步流水线 (写 Memory · 生成 Skill 候选 · 更新 continuation · 生成用户汇报)。
|
|
724
|
+
// 飞轮决策是权威 (是否继续), 既有 reducer 退化为运输层 (退避/wakeAt/attempts) —— 见 mergeGoalOutcome。
|
|
725
|
+
let closureDecision = null;
|
|
726
|
+
let merged = {
|
|
727
|
+
goalStatus: legacy.goalStatus,
|
|
728
|
+
continuation: legacy.continuation,
|
|
729
|
+
reason: legacy.reason,
|
|
730
|
+
flywheelStop: false,
|
|
731
|
+
roundsOverridden: false,
|
|
732
|
+
};
|
|
733
|
+
/** 本轮收尾要派遣的能力 (定状态之后才签合同 —— 顺序见下方注释) */
|
|
734
|
+
let capabilityWanted = null;
|
|
735
|
+
if (finalRun) {
|
|
736
|
+
try {
|
|
737
|
+
const closureRes = await closeRunOnce({
|
|
738
|
+
goalId: goal.goalId,
|
|
739
|
+
runId: finalRun.runId,
|
|
740
|
+
caller: 'supervisor',
|
|
741
|
+
now: new Date(this.now()).toISOString(),
|
|
742
|
+
finalReview: this.finalReviewText(finalRun, result),
|
|
743
|
+
maxRetries: this.maxRetries,
|
|
744
|
+
// ★ 串行收口: 把**申明**的终止原因透传下去 (超时 / 权限·支付·工具失败 / 子阻塞 / 人工停)
|
|
745
|
+
// —— 只由 Run 事实推导的话回执里只剩笼统的 failure。这里给的就是从该 Run 事实推出来的那一个
|
|
746
|
+
// (`claimedTerminalKindForRun` 只在事实足够具体时给更细的类别, 且必须与该状态相容)。
|
|
747
|
+
terminalKind: claimedTerminalKindForRun(finalRun),
|
|
748
|
+
});
|
|
749
|
+
if (isRefusal(closureRes)) {
|
|
750
|
+
// 说不清是哪条 Run 的收尾 → 如实记, 不当收过 (也不编一份产物)
|
|
751
|
+
report.errors.push(`${goal.goalId}: 收尾被拒: ${closureRes.reason}`);
|
|
752
|
+
this.emit({ kind: 'run_closure_refused', goalId: goal.goalId, runId: finalRun.runId, message: closureRes.reason });
|
|
753
|
+
}
|
|
754
|
+
else if (!closureRes.outcome) {
|
|
755
|
+
report.errors.push(`${goal.goalId}: ${closureRes.reason}`);
|
|
756
|
+
this.emit({ kind: 'run_closure_facts_missing', goalId: goal.goalId, runId: finalRun.runId, message: closureRes.reason });
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
const view = closureRes.outcome;
|
|
760
|
+
closureDecision = view.decision;
|
|
761
|
+
merged = mergeGoalOutcome({
|
|
762
|
+
legacy,
|
|
763
|
+
flywheel: { decision: view.decision, continuation: view.continuation },
|
|
764
|
+
run: finalRun,
|
|
765
|
+
pendingReports: goalForDecision.continuation?.pendingReports ?? [],
|
|
766
|
+
now: new Date(this.now()).toISOString(),
|
|
767
|
+
});
|
|
768
|
+
report.closures.push({
|
|
769
|
+
goalId: goal.goalId,
|
|
770
|
+
runId: finalRun.runId,
|
|
771
|
+
steps: view.steps,
|
|
772
|
+
decision: view.decision.decision,
|
|
773
|
+
memories: view.memories,
|
|
774
|
+
candidates: view.candidates,
|
|
775
|
+
// ★ 串行收口 ②(入口): 这次收尾**开出**的 Skill 试用逐条登记 (结构化面, 与
|
|
776
|
+
// `skillTrialOpenings` 同一份事实) —— 候选写出来了但没开试用, 这里就短一截, 一眼可见
|
|
777
|
+
trials: [...(view.trials ?? [])].map((t) => ({ ...t, goalId: goal.goalId, runId: finalRun.runId })),
|
|
778
|
+
reportPath: view.reportPath,
|
|
779
|
+
decisionRecordPath: view.decisionRecordPath,
|
|
780
|
+
});
|
|
781
|
+
// ★ 串行收口 ②(入口): 候选产出处开出的试用 —— 逐个登记 (候选写了但没开试用 = 数组短了, 一眼可见)
|
|
782
|
+
for (const t of view.trials ?? []) {
|
|
783
|
+
report.skillTrialOpenings.push({ ...t, goalId: goal.goalId, runId: finalRun.runId });
|
|
784
|
+
if (t.ok)
|
|
785
|
+
this.log(`[supervisor] goal=${goal.goalId} Skill 试用开出: ${t.skillName} (候选 ${t.candidateId})`);
|
|
786
|
+
else
|
|
787
|
+
this.log(`[supervisor] goal=${goal.goalId} Skill 试用未开 (卡在 ${t.refusal?.stage ?? '?'}): ${t.skillName} — ${t.refusal?.reason ?? ''}`);
|
|
788
|
+
}
|
|
789
|
+
this.emit({
|
|
790
|
+
kind: 'run_closure',
|
|
791
|
+
goalId: goal.goalId,
|
|
792
|
+
runId: finalRun.runId,
|
|
793
|
+
message: `收尾 ${view.steps} 步 → ${view.decision.decision}`
|
|
794
|
+
+ `${closureRes.alreadyClosed ? ' [幂等: 这次收尾由 Runner 先做, 事实读回]' : ''}`
|
|
795
|
+
+ ` (memory ${view.memories} · skill 候选 ${view.candidates} · 用户汇报 ${view.userReport.visibleState})`,
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
catch (err) {
|
|
800
|
+
// 收尾失败**不掩盖**: 如实记进 errors, 但仍按运输层决策收口 (不许因为收尾炸了就不写状态)
|
|
801
|
+
report.errors.push(`${goal.goalId}: Run 收尾失败: ${err?.message || err}`);
|
|
802
|
+
this.emit({ kind: 'run_closure_failed', goalId: goal.goalId, runId: finalRun.runId, message: String(err?.message || err) });
|
|
803
|
+
}
|
|
804
|
+
// ★ 串行收口 ②: **下一条 Run 的成功点**结算 Skill 试用 (通道 ⑥ 的真调用方)。
|
|
805
|
+
// 顺序在这里的理由: 试用的兑现证据 = "另一条 Run 成功 + 它的证据面点名了这个 Skill",
|
|
806
|
+
// 所以必须在这条 Run 的结局**已知**之后 (收尾写下的证据面正好是它的输入)。
|
|
807
|
+
// 不成功 → 回退 (rolled_back) 且**不提升**; 成功但证据面没点名 → 留在试用位 (无证据不算复用)。
|
|
808
|
+
// 只写记录 (校验+快照+可回退), 永不写 skills/ —— 见 settleSkillTrialsForRun 的纪律。
|
|
809
|
+
try {
|
|
810
|
+
const settled = await settleSkillTrialsForRun({
|
|
811
|
+
goalId: goal.goalId,
|
|
812
|
+
runId: finalRun.runId,
|
|
813
|
+
runStatus: String(finalRun.status ?? ''),
|
|
814
|
+
now: new Date(this.now()).toISOString(),
|
|
815
|
+
approvedBy: 'supervisor',
|
|
816
|
+
});
|
|
817
|
+
for (const s of settled) {
|
|
818
|
+
report.skillTrials.push({ ...s, runId: finalRun.runId });
|
|
819
|
+
if (s.promoted) {
|
|
820
|
+
this.log(`[supervisor] goal=${goal.goalId} Skill 试用提升: ${s.skillName} → ${s.toVersion} (候选 ${s.candidateId})`);
|
|
821
|
+
this.emit({ kind: 'skill_trial_promoted', goalId: goal.goalId, runId: finalRun.runId, message: s.reason });
|
|
822
|
+
}
|
|
823
|
+
else if (s.status !== 'trialing') {
|
|
824
|
+
this.log(`[supervisor] goal=${goal.goalId} Skill 试用回退 (不提升): ${s.skillName} — ${s.reason}`);
|
|
825
|
+
this.emit({ kind: 'skill_trial_rolled_back', goalId: goal.goalId, runId: finalRun.runId, message: s.reason });
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
catch (err) {
|
|
830
|
+
// 结算失败不掩盖 (也不影响 Run 的收口): 如实记进 errors
|
|
831
|
+
report.errors.push(`${goal.goalId}: Skill 试用结算失败: ${err?.message || err}`);
|
|
832
|
+
}
|
|
833
|
+
// P2: 飞轮说"这一步该派活" → **必发工作合同** (不是发一句话)
|
|
834
|
+
// 能力来源 (权威顺序): ① 这次收尾决策点名的能力 ② continuation 上记的 requiredAgent
|
|
835
|
+
// ③ **本轮跑之前**飞轮裁决=delegate 时点名的能力 (且本地**仍然**缺它) ——
|
|
836
|
+
// 收尾决策只拿得到 Run 的结果, 拿不到"目标声明的能力本地有没有"这条输入,
|
|
837
|
+
// 所以"缺能力 → 派活"必须能回落到 preflight 的结论; 否则它永远只在判定层成立、
|
|
838
|
+
// 在动作层消失 (P5 ★① 实测: 技能门禁放行之后, 合同还是一份都签不出来)。
|
|
839
|
+
const stillMissing = (await readGoal(goal.goalId))?.continuation?.skillReadiness;
|
|
840
|
+
const stillMissingCap = !!flywheelCapability && stillMissing?.ok === false
|
|
841
|
+
&& (stillMissing.missing ?? []).includes(flywheelCapability);
|
|
842
|
+
capabilityWanted = closureDecision?.requiredCapability
|
|
843
|
+
|| merged.continuation.requiredAgent
|
|
844
|
+
|| (stillMissingCap ? flywheelCapability : null);
|
|
845
|
+
// 变更注入的"版本已下发"记账 (新要求只影响后续 Run; 记下它被第几个 Run 读到)
|
|
846
|
+
await markChangesConsumed(goal.goalId, goalForDecision.runs.length).catch(() => null);
|
|
847
|
+
}
|
|
848
|
+
await this.applyDecision(goal, merged, finalRun);
|
|
849
|
+
// ★ 合同必须在**状态写完之后**签发 (P5 ★① 实测的顺序缺陷):
|
|
850
|
+
// `applyDecision` 是整份 continuation 的覆盖写 (`setContinuation(decision.continuation)`),
|
|
851
|
+
// 而 `dispatchChildWork` 会把"等这份回报"写进 `continuation.pendingReports` ——
|
|
852
|
+
// 先签发再写状态 ⇒ 刚登记的 pendingReports 立刻被覆盖成空, 父 Goal 上不留痕,
|
|
853
|
+
// 阻塞巡检再次没有输入 (与修复前一样"空转")。顺序: 定状态 → 签合同。
|
|
854
|
+
if (finalRun && capabilityWanted && merged.goalStatus !== 'completed' && !merged.flywheelStop) {
|
|
855
|
+
await this.dispatchIfDelegate(goal.goalId, finalRun, report, capabilityWanted).catch((err) => {
|
|
856
|
+
report.errors.push(`${goal.goalId}: 派遣合同失败: ${err?.message || err}`);
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
this.emit({ kind: 'goal_decision', goalId: goal.goalId, runId: finalRunId, message: `${finalRun?.status || result.status || '?'} → ${merged.goalStatus}: ${merged.reason}` });
|
|
860
|
+
this.log(`[supervisor] goal=${goal.goalId} run=${finalRunId || '-'} ${finalRun?.status || result.status || '?'} → goal=${merged.goalStatus} (${merged.reason})`
|
|
861
|
+
+ `${merged.roundsOverridden ? ' [飞轮覆盖轮次上限]' : ''} ${Date.now() - t0}ms`);
|
|
441
862
|
return { goalId: goal.goalId, runId: finalRunId, status: finalRun?.status || result.status, error: result.error };
|
|
442
863
|
}
|
|
864
|
+
/**
|
|
865
|
+
* Run 收尾用的 Final Review 文本 (P1 的输入契约, 见 run-closure.ts 文件头):
|
|
866
|
+
* ① 宿主/验收可用 `BOLLOON_RUN_FINAL_REVIEW` 注入结构化评审 (评审人 + 事实 + 教训 + 候选);
|
|
867
|
+
* ② 否则用执行器这次给的回执文本 —— 散文**也能收尾** (评论非结构化只影响"是否产出教训/候选",
|
|
868
|
+
* 不会让收尾整条跳过)。
|
|
869
|
+
*/
|
|
870
|
+
finalReviewText(run, result) {
|
|
871
|
+
const injected = process.env.BOLLOON_RUN_FINAL_REVIEW;
|
|
872
|
+
if (injected && injected.trim())
|
|
873
|
+
return injected;
|
|
874
|
+
const parts = [
|
|
875
|
+
result.reply,
|
|
876
|
+
run.summary,
|
|
877
|
+
run.error,
|
|
878
|
+
].filter((s) => typeof s === 'string' && s.trim().length > 0);
|
|
879
|
+
return parts.join('\n');
|
|
880
|
+
}
|
|
881
|
+
/** 飞轮要求派遣 (decision=delegate) → 签发工作合同并把"等回报"写进 continuation */
|
|
882
|
+
async dispatchIfDelegate(goalId, run, report, capability = '') {
|
|
883
|
+
if (!capability)
|
|
884
|
+
return;
|
|
885
|
+
const goal = await readGoal(goalId);
|
|
886
|
+
if (!goal)
|
|
887
|
+
return;
|
|
888
|
+
const nextAction = goal.continuation?.nextAction || `把需要「${capability}」的子目标派出去`;
|
|
889
|
+
const contract = await dispatchChildWork({
|
|
890
|
+
goalId,
|
|
891
|
+
parentRunId: run.runId,
|
|
892
|
+
// 合同只锁"能力", 具体派给谁由执行器解析 (这里如实写能力名, 不假装知道某个 agent 的 id)
|
|
893
|
+
childAgentId: capability,
|
|
894
|
+
capability,
|
|
895
|
+
objective: nextAction,
|
|
896
|
+
inputs: { goalObjective: goal.objective, successCriteria: goal.successCriteria },
|
|
897
|
+
allowedTools: [],
|
|
898
|
+
budget: { maxSteps: null, maxDurationMs: run.budget?.deadlineMs ?? null, maxAmount: null, currency: null },
|
|
899
|
+
deadline: null,
|
|
900
|
+
successCriteria: goal.successCriteria.length ? goal.successCriteria : [nextAction],
|
|
901
|
+
issuedBy: this.owner,
|
|
902
|
+
});
|
|
903
|
+
this.log(`[supervisor] goal=${goalId} 已签发工作合同 workId=${contract.workId} (能力「${capability}」, ${contract.successCriteria.length} 条判据, 必带证据 ${contract.requiredEvidence.length} 条)`);
|
|
904
|
+
this.emit({ kind: 'work_contract_issued', goalId, runId: run.runId, message: `workId=${contract.workId} 能力=${capability}` });
|
|
905
|
+
report.workContracts.push({ goalId, workId: contract.workId, capability });
|
|
906
|
+
}
|
|
443
907
|
/** 把决策写进 Goal (+ 证据同步 + 完成出口), 并写下一次唤醒信息 */
|
|
444
908
|
async applyDecision(goal, decision, run) {
|
|
445
|
-
const { updateGoal } = await import('./goal-store.js');
|
|
446
909
|
// 证据同步: Run 的成功步骤 → Goal 证据 (长期执行的判据要有据可依)
|
|
447
910
|
if (run) {
|
|
448
911
|
const ev = run.steps.filter((s) => s.ok).slice(-5).map((s) => `${run.runId}/${s.tool}: ${String(s.summary || '(完成)').slice(0, 120)}`);
|
|
449
912
|
if (ev.length)
|
|
450
913
|
await addEvidence(goal.goalId, ev).catch(() => null);
|
|
451
914
|
}
|
|
452
|
-
//
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
915
|
+
// 规则 ②: 唯一落盘出口。完成门 (completed 必须过 completeGoalIfEligible) 也在 reducer 里,
|
|
916
|
+
// 门拒绝 → 退回 active 并保留原因 (不许装作完成)。
|
|
917
|
+
const applied = await reduceGoalState({
|
|
918
|
+
goalId: goal.goalId,
|
|
919
|
+
intent: 'closure_outcome',
|
|
920
|
+
now: new Date(this.now()).toISOString(),
|
|
921
|
+
by: this.owner,
|
|
922
|
+
outcome: {
|
|
923
|
+
goalStatus: decision.goalStatus,
|
|
924
|
+
continuation: decision.continuation,
|
|
925
|
+
reason: decision.reason,
|
|
926
|
+
},
|
|
927
|
+
runId: run?.runId ?? null,
|
|
928
|
+
});
|
|
929
|
+
if (applied.gateRejected) {
|
|
930
|
+
this.log(`[supervisor] goal=${goal.goalId} 完成门拒绝: ${applied.gateRejected}`);
|
|
931
|
+
}
|
|
932
|
+
if (!applied.ok) {
|
|
933
|
+
this.log(`[supervisor] goal=${goal.goalId} 决策写不进 Goal (不掩盖): ${applied.reason}`);
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
if (applied.status === 'completed') {
|
|
937
|
+
this.emit({ kind: 'goal_completed', goalId: goal.goalId, runId: run?.runId, message: applied.reason });
|
|
466
938
|
return;
|
|
467
939
|
}
|
|
468
|
-
if (decision.
|
|
469
|
-
await updateGoal(goal.goalId, { status: decision.goalStatus });
|
|
470
|
-
await setContinuation(goal.goalId, decision.continuation);
|
|
471
|
-
if (decision.continuation.wakeReason === 'needs_human') {
|
|
940
|
+
if (decision.continuation.wakeReason === 'needs_human' || applied.status === 'needs_human') {
|
|
472
941
|
this.emit({ kind: 'needs_human', goalId: goal.goalId, message: decision.reason });
|
|
473
942
|
}
|
|
474
943
|
}
|
|
475
|
-
/**
|
|
944
|
+
/**
|
|
945
|
+
* 外部事件到达 → 给对应 Goal 清除等待并加速唤醒 (2-E 第 4 类的入口)
|
|
946
|
+
* 手动唤醒 (CLI `/wake` / `POST /api/goals/:id/wake`): 人明确说"外部条件我已经满足了"。
|
|
947
|
+
*
|
|
948
|
+
* ★ 2026-09-25 (P5 验收修复): 原来只清等待事实、**不改 goal.status** —— `listRunnableGoals`
|
|
949
|
+
* 又按 `status === 'awaiting_external'` 直接跳过, 于是"唤醒"成了空操作:
|
|
950
|
+
* 接口回话 `woke:true`, 目标却仍然醒不过来 (真 tick executed=0)。
|
|
951
|
+
* 真事件路径 (`deliverExternalEvent`) 本来就把状态拉回 `active` —— 两条唤醒路径必须一致。
|
|
952
|
+
*
|
|
953
|
+
* 只拉**等待类**状态: 终态与人已判定的状态 (completed/failed/abandoned/paused/needs_human)
|
|
954
|
+
* 不动 —— 唤醒是"外部条件到了", 不是绕过人的决定。
|
|
955
|
+
*/
|
|
476
956
|
async notifyExternal(goalId) {
|
|
477
957
|
const g = await readGoal(goalId);
|
|
478
958
|
if (!g)
|
|
479
959
|
return false;
|
|
480
960
|
if (g.continuation?.wakeReason !== 'awaiting_external' && !g.continuation?.needsExternal)
|
|
481
961
|
return false;
|
|
482
|
-
|
|
962
|
+
// 等待类的 continuation.state 也要拉回 `active` (与 goal.status 同步): 界面只读 continuation
|
|
963
|
+
// (`toUserVisibleState`), 留着 `awaiting_external` 的话页面仍显示"等待外部回复" ——
|
|
964
|
+
// 与"已唤醒、下一次 tick 会推进它"自相矛盾 (真 DOM 就是这么读到的)。
|
|
965
|
+
const wasWaiting = ['awaiting_external', 'retry_wait'].includes(String(g.continuation?.state));
|
|
966
|
+
await setContinuation(goalId, {
|
|
967
|
+
wakeReason: 'active', needsExternal: undefined, autoContinue: true, wakeAt: undefined,
|
|
968
|
+
...(wasWaiting ? { state: 'active' } : {}),
|
|
969
|
+
});
|
|
970
|
+
if (g.status === 'awaiting_external') {
|
|
971
|
+
// 规则 ②: 唤醒也是一种 Goal 状态变更 → 走 Goal reducer, 不在调用方手写 updateGoal
|
|
972
|
+
await reduceGoalState({
|
|
973
|
+
goalId,
|
|
974
|
+
intent: 'manual_wake',
|
|
975
|
+
now: new Date().toISOString(),
|
|
976
|
+
by: 'human',
|
|
977
|
+
reason: '人工唤醒 (外部条件已满足)',
|
|
978
|
+
}).catch(() => null);
|
|
979
|
+
}
|
|
483
980
|
await bumpContinuationAttempts(goalId); // 记一次唤醒 (可观测)
|
|
484
981
|
return true;
|
|
485
982
|
}
|
|
983
|
+
/**
|
|
984
|
+
* ★ 2026-09-25 (串行收口 ④): **非 web 路径**提一条新要求/撤销 —— CLI 与宿主进程都走它。
|
|
985
|
+
*
|
|
986
|
+
* 为什么要挂在 Supervisor 上: 撤销 (用户说"别继续做了") 的语义是"停住在跑的 Run + 只影响后续 Run",
|
|
987
|
+
* 而这件事**只有一个正确做法** (M4 的 change 接缝: 判定 + 事实注入 + 停止原语)。以前只有
|
|
988
|
+
* `/api/goals/:id/requirement` 那条 HTTP 路由会走到它 —— 于是 CLI / 宿主直接调 `ingestGoalChange`
|
|
989
|
+
* 时, 在跑的 Run **停不下来** (接缝拿到的是 `factsMissing=true`, 只能如实说"不知道")。
|
|
990
|
+
*
|
|
991
|
+
* 这里就是那个入口: 读事实 (`runningRun` / 在跑的子 Agent) → 接缝判定 → 真停 Run (`setRunStatus`)。
|
|
992
|
+
* 返回值原样透出接缝视图 (含 `runBoundary.stopped` 与逐 workId 的下发内容), 拒绝也原样透出。
|
|
993
|
+
*/
|
|
994
|
+
async ingestRequirement(goalId, instruction, by = 'human') {
|
|
995
|
+
const now = new Date(this.now()).toISOString();
|
|
996
|
+
const view = await ingestRequirementViaSeam({
|
|
997
|
+
goalId,
|
|
998
|
+
instruction,
|
|
999
|
+
source: 'user',
|
|
1000
|
+
recordedBy: `cli:${by}`,
|
|
1001
|
+
caller: 'human', // 人提的变更: 只有人能提撤销/缩范围
|
|
1002
|
+
now,
|
|
1003
|
+
});
|
|
1004
|
+
if (view === null)
|
|
1005
|
+
return { ok: false, reason: `变更无法入档 (goal=${goalId})` };
|
|
1006
|
+
if (isRefusal(view))
|
|
1007
|
+
return { ok: false, reason: view.reason };
|
|
1008
|
+
const b = view.runBoundary;
|
|
1009
|
+
const note = b.action === 'stop_running_run'
|
|
1010
|
+
? (b.stopped
|
|
1011
|
+
? `在跑的 Run ${b.runId} 已写 ${b.runStatus} (执行器下一轮自行停下)`
|
|
1012
|
+
: `判定要停 Run ${b.runId}, 但停止动作没落到记录上: ${b.reason}`)
|
|
1013
|
+
: b.reason;
|
|
1014
|
+
this.emit({ kind: 'requirement_ingested', goalId, message: `${view.request.kind} (${view.plan.outcome}) — ${note}` });
|
|
1015
|
+
this.log(`[supervisor] goal=${goalId} 变更入档: ${view.request.kind} → ${view.plan.outcome}; ${note}`);
|
|
1016
|
+
return {
|
|
1017
|
+
ok: true,
|
|
1018
|
+
kind: view.request.kind,
|
|
1019
|
+
outcome: view.plan.outcome,
|
|
1020
|
+
stopped: b.stopped,
|
|
1021
|
+
runId: b.runId,
|
|
1022
|
+
visibleState: view.visibleState,
|
|
1023
|
+
note,
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
486
1026
|
}
|
|
487
1027
|
/** 单例 (Web/CLI 共享同一个进程内调度器; 跨进程靠 lease 排他) */
|
|
488
1028
|
let singleton = null;
|