@bolloon/bolloon-agent 0.4.32 → 0.5.0
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/chain/explorer.js +52 -0
- package/dist/agents/chain/index.js +2 -0
- package/dist/agents/chain/onchain-trade.js +15 -1
- package/dist/agents/contacts/chain.js +27 -7
- package/dist/agents/execution-supervisor.js +493 -35
- 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/network-pulse.js +525 -27
- package/dist/agents/pi-sdk.js +75 -20
- package/dist/agents/run-store.js +18 -5
- package/dist/agents/skill-readiness.js +52 -10
- package/dist/agents/subagent-manager.js +209 -3
- package/dist/agents/task/task-onchain-runner.js +19 -1
- package/dist/agents/task/task-runner.js +36 -6
- package/dist/agents/task-board.js +702 -0
- package/dist/agents/task-group.js +524 -0
- package/dist/agents/task-public-text.js +89 -0
- package/dist/cli/commands/index.js +3 -0
- package/dist/cli/commands/tasks.js +977 -8
- package/dist/cli/identity-command.js +177 -0
- package/dist/cli/protocol-envelope.js +4 -0
- package/dist/cli/setup-wizard.js +71 -0
- package/dist/cli/update-commands.js +150 -24
- package/dist/cli-entry.js +12 -2
- package/dist/electron-build/agents/chain/explorer.js +59 -0
- package/dist/electron-build/agents/chain/explorer.js.map +1 -0
- 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 +492 -34
- 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/local-signer.js +101 -0
- package/dist/electron-build/agents/local-signer.js.map +1 -0
- package/dist/electron-build/agents/network-pulse.js +531 -28
- package/dist/electron-build/agents/network-pulse.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +74 -19
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/run-store.js +20 -5
- 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 +35 -5
- package/dist/electron-build/agents/task/task-runner.js.map +1 -1
- package/dist/electron-build/agents/task-board.js +761 -0
- package/dist/electron-build/agents/task-board.js.map +1 -0
- package/dist/electron-build/agents/task-contract.js +382 -0
- package/dist/electron-build/agents/task-contract.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.js +471 -0
- package/dist/electron-build/utils/dual-source.js.map +1 -0
- package/dist/electron-build/utils/update-manager.js +331 -57
- package/dist/electron-build/utils/update-manager.js.map +1 -1
- package/dist/electron-build/utils/update-state.js +16 -1
- package/dist/electron-build/utils/update-state.js.map +1 -1
- package/dist/electron-build/utils/version-info.js +82 -2
- package/dist/electron-build/utils/version-info.js.map +1 -1
- package/dist/index.js +103 -12
- 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 +329 -0
- package/dist/ios/mobile-core.js +1379 -226
- package/dist/ios/mobile-privacy.js +185 -0
- package/dist/ios/mobile.css +19 -0
- package/dist/ios/mobile.html +96 -0
- package/dist/ios/mobile.js +706 -7
- package/dist/ios/routes-chain-index.js +108 -0
- package/dist/ios/routes-contacts.js +420 -0
- package/dist/ios/routes-onchain-trade.js +99 -0
- package/dist/ios/routes-x402-info.js +1 -0
- package/dist/ios/server.js +762 -0
- package/dist/ios/task-frame-server.js +211 -0
- package/dist/orbitdb/cid-database.js +81 -16
- package/dist/orbitdb/ipfs-node.js +31 -2
- package/dist/utils/dual-source.js +426 -0
- package/dist/utils/update-cli.js +16 -21
- package/dist/utils/update-health.js +35 -5
- package/dist/utils/update-manager.js +329 -57
- package/dist/utils/update-state.js +16 -1
- package/dist/utils/version-info.js +75 -1
- package/dist/web/mobile-contacts.js +44 -11
- package/dist/web/mobile-core.js +1197 -513
- package/dist/web/mobile-tasks.js +339 -0
- package/dist/web/mobile.css +19 -0
- package/dist/web/mobile.html +64 -0
- package/dist/web/mobile.js +531 -1
- package/dist/web/routes-mobile-tasks.js +353 -0
- package/dist/web/server.js +163 -4
- package/package.json +3 -1
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wiring/contract.ts — 接缝 ③「这一步该派给谁, 回报算不算完成?」 (**M3 独占**)
|
|
3
|
+
*
|
|
4
|
+
* 归属: `seams.ts` 的 `SEAM_ROSTER` 里 `id: 'contract'`, `stage: 'M3'`。
|
|
5
|
+
* M3 只准改本文件 + `wiring/monitor.ts` + 声明的 `wiringPoints`
|
|
6
|
+
* (`goal-flywheel/work-contract.ts` · `subagent-manager.ts`)。
|
|
7
|
+
*
|
|
8
|
+
* ## 这条接缝钉住的冻结规则
|
|
9
|
+
*
|
|
10
|
+
* **规则 ⑤ 子 Agent 不能直接改 Goal**。
|
|
11
|
+
* 钉法: 派活与收回报**都只经过父侧** (`issueWork` / `acceptReport` / `dispatchWorker`), 且接缝自己做三件事:
|
|
12
|
+
* ① 拒绝 `caller='child_agent'` 的派活 (子不许派生无限子任务);
|
|
13
|
+
* ② 回报一律先过 `validateChildReport` + `acceptsAsComplete` —— **漂亮但没有逐条证据的
|
|
14
|
+
* 回报不算完成** (P5 强负例 7); 子 Agent 侧因此没有任何写 Goal 的路径;
|
|
15
|
+
* ③ **派前必须有工作合同** (M3 新增): `dispatchWorker` 是"签合同 + 交给派遣器"的**唯一**入口 ——
|
|
16
|
+
* 合同签不出来 (缺判据 / 缺目标 / deadline 早于 now / 金额没币种 / 没注入签发面) 就**不派**,
|
|
17
|
+
* 而且派遣端口**一次都不调**。今天 `SubAgentManager` 的旧形状是"签合同失败 → 记个 error 照样派出去",
|
|
18
|
+
* 那正是"派前必须有合同"被绕过的地方 (本文件 + `subagent-manager.ts` 一起堵)。
|
|
19
|
+
*
|
|
20
|
+
* ## 为什么接缝自己带 P2 的核验口径 (`realReportVerdict` / `createContractSeamFromLogic`)
|
|
21
|
+
*
|
|
22
|
+
* 接缝只依赖三样东西: 本目录 `seams.ts` 的接口 · `../types.js` 的冻结类型 · **自己那个阶段**的实现模块。
|
|
23
|
+
* P2 的实现模块就是 `../work-contract.js` (纯函数, 零 I/O)。把它接进来意味着:
|
|
24
|
+
* · 判定口径**全仓一份** —— 接缝不重新实现"算不算完成", 而是直接调真 P2 的那两个函数;
|
|
25
|
+
* · 接缝可以**独立于 M0 组合层**使用 (测试与其它宿主只需注入落盘/读盘这两个端口),
|
|
26
|
+
* 而不是"必须有 `goal-flywheel-wiring.ts` 才能用"。
|
|
27
|
+
* 落盘/读盘仍按参数注入 (接缝不做 I/O, 与 `../work-contract.js` 的边界一致)。
|
|
28
|
+
*/
|
|
29
|
+
import { acceptsAsComplete, issueWorkContract, validateChildReport, } from '../work-contract.js';
|
|
30
|
+
import { refuse } from './seams.js';
|
|
31
|
+
export const CONTRACT_SEAM_ID = 'contract';
|
|
32
|
+
/**
|
|
33
|
+
* 派活的拒绝码 (接缝自己的闭集)。
|
|
34
|
+
*
|
|
35
|
+
* 刻意**不**复用 `FrozenRule`: 六条冻结规则说的是"责任人是谁", 而这里是"这一步做不成的原因"。
|
|
36
|
+
* 把 `contract_invalid` 硬说成某条冻结规则 = 编一个规则号, 那会让日志与门都开始说谎。
|
|
37
|
+
* 只有真的越界 (`caller_not_allowed`) 才附 `refusal` (它确实违反规则 ⑤)。
|
|
38
|
+
*/
|
|
39
|
+
export const CONTRACT_REFUSAL_CODES = [
|
|
40
|
+
'caller_not_allowed',
|
|
41
|
+
'no_criteria',
|
|
42
|
+
'contract_invalid',
|
|
43
|
+
'no_dispatch_port',
|
|
44
|
+
'dispatch_failed',
|
|
45
|
+
];
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// §0. 与真 P2 模块的唯一耦合点 (判定口径全仓一份)
|
|
48
|
+
// ============================================================================
|
|
49
|
+
/** 用真 `issueWorkContract` 签发 —— 合同不合法时抛带 `issues` 的 `WorkContractError` (接缝抓它转成结构化拒绝) */
|
|
50
|
+
export function realIssueContract(input) {
|
|
51
|
+
return issueWorkContract({
|
|
52
|
+
goalId: input.goalId,
|
|
53
|
+
parentRunId: input.parentRunId,
|
|
54
|
+
childAgentId: input.childAgentId,
|
|
55
|
+
capability: input.capability,
|
|
56
|
+
objective: input.objective,
|
|
57
|
+
inputs: input.inputs ?? {},
|
|
58
|
+
allowedTools: input.allowedTools ?? [],
|
|
59
|
+
budget: input.budget,
|
|
60
|
+
deadline: input.deadline ?? null,
|
|
61
|
+
successCriteria: input.successCriteria,
|
|
62
|
+
now: input.now,
|
|
63
|
+
issuedBy: input.issuedBy ?? 'supervisor',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/** 用真 `validateChildReport` + `acceptsAsComplete` 核验 —— 接缝不另写一套"算不算完成" */
|
|
67
|
+
export function realReportVerdict(contract, report) {
|
|
68
|
+
const validation = validateChildReport(contract, report);
|
|
69
|
+
const verdict = acceptsAsComplete(contract, report);
|
|
70
|
+
return {
|
|
71
|
+
outcome: verdict.accepted ? 'accepted' : 'incomplete',
|
|
72
|
+
accepted: verdict.accepted,
|
|
73
|
+
reason: verdict.reason,
|
|
74
|
+
missingFields: validation.missingFields,
|
|
75
|
+
missingEvidence: validation.missingEvidence,
|
|
76
|
+
violation: validation.violation,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 从**真 P2 逻辑** + 两个存储端口拼一条接缝 (M3 的自带适配器)。
|
|
81
|
+
*
|
|
82
|
+
* 为什么需要它: `createContractSeam` 要求宿主自己注入 `issue`/`accept` (M0 组合层注入的是
|
|
83
|
+
* `dispatchChildWork` / `handleChildReport`)。这条工厂让**其它宿主** (测试 / CLI / Web)
|
|
84
|
+
* 也能用同一条接缝, 而判定口径仍是 `../work-contract.js` 那一份 —— 不会出现"第二份判定"。
|
|
85
|
+
*
|
|
86
|
+
* 注意 `persistContract`: 合同**必须先落盘再派出** —— 子拿到合同的依据是盘上那份,
|
|
87
|
+
* 而不是父内存里那句口头约定 (否则子回报时核心验的是"合同存不存在"就永远对不上)。
|
|
88
|
+
*/
|
|
89
|
+
export function createContractSeamFromLogic(ports) {
|
|
90
|
+
return createContractSeam({
|
|
91
|
+
issue: async (input) => {
|
|
92
|
+
const contract = realIssueContract(input);
|
|
93
|
+
await ports.persistContract(contract);
|
|
94
|
+
return contract;
|
|
95
|
+
},
|
|
96
|
+
accept: async (input) => {
|
|
97
|
+
const contract = await ports.loadContract({ goalId: input.goalId, workId: input.workId });
|
|
98
|
+
if (!contract) {
|
|
99
|
+
return {
|
|
100
|
+
outcome: 'no_contract', accepted: false,
|
|
101
|
+
reason: `没有找到合同 ${input.workId} → 没有合同的回报不核验也不接受`,
|
|
102
|
+
missingFields: [], missingEvidence: [], violation: null,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return realReportVerdict(contract, input.report);
|
|
106
|
+
},
|
|
107
|
+
dispatch: ports.dispatch,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
// ============================================================================
|
|
111
|
+
// §1. 合同与派遣的边界判定 (纯函数 —— 接线层 / SubAgentManager 直接复用, 不重写一套)
|
|
112
|
+
// ============================================================================
|
|
113
|
+
/**
|
|
114
|
+
* 换人必须重签合同 (M3)。
|
|
115
|
+
*
|
|
116
|
+
* 合同把 `childAgentId` 钉死了 (`validateChildReport` 要求 `report.childAgentId === contract.childAgentId`),
|
|
117
|
+
* 所以"把同一份合同指给另一个子 Agent"不是改个字段: 新子回报必然对不上合同 → 旧的合规判定直接失效
|
|
118
|
+
* (要么假红, 要么被人放宽判据 —— 两条都是两套事实)。
|
|
119
|
+
* 结论: **换人 = 换合同**。本函数是那条规则的唯一判据。
|
|
120
|
+
*/
|
|
121
|
+
export function childMatchesContract(contract, childAgentId) {
|
|
122
|
+
const next = String(childAgentId ?? '').trim();
|
|
123
|
+
if (!next)
|
|
124
|
+
return { ok: false, reason: '新执行者没有 id —— 合同无法绑定到具体执行者 (换人必须重签)' };
|
|
125
|
+
if (next === contract.childAgentId) {
|
|
126
|
+
return { ok: true, reason: `执行者未变 (${next}), 合同 ${contract.workId} 仍然有效` };
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
reason: `合同 ${contract.workId} 绑定的执行者是 ${contract.childAgentId}, 不是 ${next} `
|
|
131
|
+
+ '—— 换人必须重签合同 (否则新子 Agent 的回报与合同对不上, 合规判定失效)',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
// ============================================================================
|
|
135
|
+
// §2. 接缝本体
|
|
136
|
+
// ============================================================================
|
|
137
|
+
function errText(e) {
|
|
138
|
+
return String(e?.message || e).slice(0, 400);
|
|
139
|
+
}
|
|
140
|
+
/** 抓签发失败的结构化原因 (WorkContractError 带 issues; 其它错误只带 message) */
|
|
141
|
+
function issueFailure(input, e) {
|
|
142
|
+
const issues = e?.issues;
|
|
143
|
+
if (Array.isArray(issues) && issues.length > 0) {
|
|
144
|
+
return {
|
|
145
|
+
code: 'contract_invalid',
|
|
146
|
+
reason: `合同不合法, 拒签也拒派 (${input.capability} → ${input.childAgentId}): ${issues.join('; ')}`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return { code: 'contract_invalid', reason: `合同签发失败, 拒派: ${errText(e)}` };
|
|
150
|
+
}
|
|
151
|
+
export function createContractSeam(deps) {
|
|
152
|
+
return {
|
|
153
|
+
id: CONTRACT_SEAM_ID,
|
|
154
|
+
stage: 'M3',
|
|
155
|
+
async issueWork(input) {
|
|
156
|
+
if (input.caller === 'child_agent') {
|
|
157
|
+
return refuse('child_cannot_mutate_goal', `拒绝: 子 Agent (${input.childAgentId}) 想给「${input.capability}」派活 —— `
|
|
158
|
+
+ '子 Agent 不许派生无限子任务, 也不许改父 Goal 的派遣面 (CHILD_PROHIBITIONS.spawn_unbounded_subtasks)');
|
|
159
|
+
}
|
|
160
|
+
if (!input.successCriteria?.length) {
|
|
161
|
+
return refuse('child_cannot_mutate_goal', '拒绝: 合同没有成功判据 —— 子 Agent 会自己定义"算完成"');
|
|
162
|
+
}
|
|
163
|
+
return deps.issue(input);
|
|
164
|
+
},
|
|
165
|
+
async acceptReport(input) {
|
|
166
|
+
if (!input.workId) {
|
|
167
|
+
return refuse('child_cannot_mutate_goal', '拒绝: 没有 workId 的回报无法对合同核验 → 不接受为完成');
|
|
168
|
+
}
|
|
169
|
+
const verdict = await deps.accept({ goalId: input.goalId, workId: input.workId, report: input.report, now: input.now });
|
|
170
|
+
// 无证据的漂亮回报: 显式拒绝, 并说清缺什么 (不许静默降级成"完成")
|
|
171
|
+
if (verdict.outcome === 'incomplete' && !verdict.reason) {
|
|
172
|
+
return { ...verdict, reason: '报告不完整 → 不接受为完成 (缺字段/缺证据)' };
|
|
173
|
+
}
|
|
174
|
+
return verdict;
|
|
175
|
+
},
|
|
176
|
+
/**
|
|
177
|
+
* 派前必须有工作合同 (M3 唯一派遣入口)。
|
|
178
|
+
*
|
|
179
|
+
* 顺序固定 (便于复现与断言):
|
|
180
|
+
* ① 权限: 子 Agent 不许派活 → 拒 (附冻结规则 ⑤, 端口零调用);
|
|
181
|
+
* ② 判据: 没有成功判据 → 拒 (没有"完成"的定义, 派出去只能扯皮);
|
|
182
|
+
* ③ 派遣面: 没注入 `dispatch` → 拒 (**不假装派过**);
|
|
183
|
+
* ④ 签合同: 真签 (`deps.issue`)。签不出 → 拒, 派遣端口**零调用** (这是"派前必须有合同"的硬点);
|
|
184
|
+
* ⑤ 真派: 调 `dispatch`。端口抛错 / 回 `dispatched:false` → 如实拒绝 + 把人叫上 (不静默降级成"派出去了")。
|
|
185
|
+
*
|
|
186
|
+
* 合同**先落盘再派出**由 `deps.issue` 的宿主保证 (M0: `dispatchChildWork` 先写 goal-works/ 再写 pendingReports)。
|
|
187
|
+
*/
|
|
188
|
+
async dispatchWorker(input) {
|
|
189
|
+
if (input.caller === 'child_agent') {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
code: 'caller_not_allowed',
|
|
193
|
+
reason: `拒绝派遣: 调用者是子 Agent (${input.childAgentId} → 「${input.capability}」) —— `
|
|
194
|
+
+ '子 Agent 不许派生无限子任务 (CHILD_PROHIBITIONS.spawn_unbounded_subtasks)',
|
|
195
|
+
refusal: refuse('child_cannot_mutate_goal', '子 Agent 不许派生无限子任务 / 不许改父 Goal 的派遣面'),
|
|
196
|
+
detail: 'no_dispatch: 未签发合同, 派遣端口零调用',
|
|
197
|
+
contract: null,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (!input.successCriteria?.length) {
|
|
201
|
+
return {
|
|
202
|
+
ok: false,
|
|
203
|
+
code: 'no_criteria',
|
|
204
|
+
reason: `拒绝派遣「${input.capability}」: 合同没有成功判据 —— 没有"完成"的定义就不许派 `
|
|
205
|
+
+ '(子 Agent 会自己定义"算完成")',
|
|
206
|
+
refusal: null,
|
|
207
|
+
detail: 'no_dispatch: 未签发合同, 派遣端口零调用',
|
|
208
|
+
contract: null,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const dispatch = deps.dispatch;
|
|
212
|
+
if (typeof dispatch !== 'function') {
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
code: 'no_dispatch_port',
|
|
216
|
+
reason: `拒绝派遣「${input.capability}」: 宿主没有注入派遣面 (端口缺失) —— 拿不到"真派出去了"的事实, `
|
|
217
|
+
+ '不许把签发当成已派遣',
|
|
218
|
+
refusal: null,
|
|
219
|
+
detail: 'no_dispatch: 未签发合同, 派遣端口零调用',
|
|
220
|
+
contract: null,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
let contract;
|
|
224
|
+
try {
|
|
225
|
+
contract = await deps.issue(input);
|
|
226
|
+
}
|
|
227
|
+
catch (e) {
|
|
228
|
+
const fail = issueFailure(input, e);
|
|
229
|
+
return {
|
|
230
|
+
ok: false,
|
|
231
|
+
code: fail.code,
|
|
232
|
+
reason: fail.reason,
|
|
233
|
+
refusal: null,
|
|
234
|
+
detail: 'no_dispatch: 合同没签成, 派遣端口零调用',
|
|
235
|
+
contract: null,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const receipt = await dispatch({ contract, caller: input.caller, now: input.now });
|
|
240
|
+
if (receipt?.dispatched !== true) {
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
code: 'dispatch_failed',
|
|
244
|
+
reason: `合同 ${contract.workId} 已签发, 但派遣端口回"没派出去": ${receipt?.detail ?? '(端口没给原因)'}`,
|
|
245
|
+
refusal: null,
|
|
246
|
+
detail: `dispatched=false (workId=${contract.workId})`,
|
|
247
|
+
contract,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
ok: true,
|
|
252
|
+
contract,
|
|
253
|
+
dispatched: true,
|
|
254
|
+
detail: receipt.detail,
|
|
255
|
+
receipt: receipt.receipt ?? null,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
catch (e) {
|
|
259
|
+
return {
|
|
260
|
+
ok: false,
|
|
261
|
+
code: 'dispatch_failed',
|
|
262
|
+
reason: `合同 ${contract.workId} 已签发, 但派遣端口抛错: ${errText(e)}`,
|
|
263
|
+
refusal: null,
|
|
264
|
+
detail: `dispatch 抛错: ${errText(e)}`,
|
|
265
|
+
contract,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* goal-flywheel/wiring/index.ts — 接线接缝的对外入口 (M0)
|
|
3
|
+
*
|
|
4
|
+
* 只做转发: 接缝名册在 `seams.ts` (M0 冻结), 五个接缝实现各归各的阶段文件。
|
|
5
|
+
* 这里**不定义任何函数** —— 与 `../index.ts` 同一纪律 (转发行, 不夹带实现)。
|
|
6
|
+
*
|
|
7
|
+
* M1–M4 各自只 import 自己那个接缝文件; 想拿到接缝实例请走 M0 接线层
|
|
8
|
+
* (`src/agents/goal-flywheel-wiring.ts` 的 `flywheelSeams()`), 它负责注入真依赖。
|
|
9
|
+
*/
|
|
10
|
+
export { DEFAULT_ALLOW, FROZEN_RULES, FROZEN_RULE_TEXT, FUNNEL_ALIASES, FUNNEL_CALL_RE, FUNNEL_ENTRY, SCANNED_SOURCES, SEAM_IDS, SEAM_MODULES, SEAM_ROSTER, SKILL_CHANNEL_EVIDENCE, TERMINAL_PATHS, WIRING_CALLERS, WIRING_STAGES, canRunStagesInParallel, filesOfStage, isRefusal, refuse, seamModuleOf, seamOf, seamRosterViolations, scanChildCannotMutateGoal, scanFunnelAliases, scanOnlyCloseRunCloses, scanOnlyGoalReducerWritesState, scanOnlySupervisorDecides, scanSkillChannel, scanTerminalPaths, stageOf, } from './seams.js';
|
|
11
|
+
export { CONTINUATION_SEAM_ID, RHYTHM_BASIS_TEXT, SAFETY_CAPS, SAFETY_CAP_TEXT, SEAM_CAP_DEFAULTS, SEAM_RHYTHM_REQUIRED, SEAM_ROUNDS_AS_RHYTHM, bindingCaps, createContinuationSeam, judgeInjectedOutcome, judgeLegacyStop, planRhythm, rhythmicDecision, safetyCapsFrom, scanContinuationSeamRhythm, } from './continuation.js';
|
|
12
|
+
export { CLOSURE_IDEMPOTENCY_NOTE, CLOSURE_SEAM_ID, CLOSURE_TERMINAL_KINDS, auditClosureOutcome, auditTerminalCoverage, closureTerminalKindFor, createClosureSeam, probeClosureArtifacts, terminalKindAccepts, terminalRegistryCoverage, } from './closure.js';
|
|
13
|
+
export { CONTRACT_SEAM_ID, CONTRACT_REFUSAL_CODES, childMatchesContract, createContractSeam, createContractSeamFromLogic, realIssueContract, realReportVerdict, } from './contract.js';
|
|
14
|
+
export { INTERNAL_VISIBLE_WORDS, MONITOR_SEAM_ID, MONITOR_SWEEP_DEFAULT_LIMIT, MONITOR_SWEEP_MAX_LIMIT, USER_FACING_STATES, createMonitorSeam, createVisibleStateProbe, describeVisibleState, isUserVisibleState, scanInternalLeak, userFacingState, } from './monitor.js';
|
|
15
|
+
export { CHANGE_SEAM_ID, USER_ONLY_CHANGE_KINDS, changeVisibleState, createChangeSeam, } from './change.js';
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// 跨阶段的**纯函数面** (2026-09-25 串行收口): 接缝与宿主都用的判定/归一化在这里一并转发 ——
|
|
18
|
+
// 宿主与界面要的是"同一份口径", 不该各自深入阶段目录再 import 一遍 (M3 报的"index 补导"的完整形态)。
|
|
19
|
+
//
|
|
20
|
+
// 转发的是**同一个函数对象**, 不是同名的壳: 见 `goal-flywheel-m0-serial-hooks.test.ts` ⑥ 的恒等断言。
|
|
21
|
+
// 尤其 `toUserVisibleState` / `normalizeContinuationRecord` —— 工作监控与 web 路由直传**部分**
|
|
22
|
+
// continuation 时必须读同一份归一化 (2026-09-25 跨阶段真问题 ①)。
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
export { normalizeContinuationRecord, toUserVisibleState, } from '../work-monitor.js';
|
|
25
|
+
export { planChangeInjection } from '../goal-change.js';
|
|
26
|
+
export { USER_VISIBLE_STATES } from '../types.js';
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wiring/monitor.ts — 接缝 ④「有谁卡住了, 该谁动手?」 (**M3 独占**)
|
|
3
|
+
*
|
|
4
|
+
* 归属: `seams.ts` 的 `SEAM_ROSTER` 里 `id: 'monitor'`, `stage: 'M3'`。
|
|
5
|
+
* M3 只准改本文件 + `wiring/contract.ts` + 声明的 `wiringPoints` (`goal-flywheel/work-monitor.ts`)。
|
|
6
|
+
*
|
|
7
|
+
* ## 这条接缝钉住的冻结规则
|
|
8
|
+
*
|
|
9
|
+
* **规则 ④ 的"子 Agent 被阻塞"那一类终止路径必须经过收尾漏斗**。
|
|
10
|
+
* Watchdog 只看进程存活; 这条接缝看的是"任务卡住了没有"。它只做三件事:
|
|
11
|
+
* ① **统一巡检** (`sweepAll` / `sweep`): 一次把**所有**待回报 Goal 的阻塞查清楚 (有界, 不静默);
|
|
12
|
+
* ② **按计划处理** (`handle`): 处置动作交给宿主执行 —— 接缝自己**永不自动绕过 Harness**;
|
|
13
|
+
* ③ **用户可见态** (`visibleState` / `createVisibleStateProbe`): 只暴露冻结层的闭集, 内部词不外泄。
|
|
14
|
+
*
|
|
15
|
+
* 阻塞升级到人 → Goal 的状态变更**不在这里直接写**, 而是交给 Goal reducer
|
|
16
|
+
* (规则 ②), 由接线层在 `handle` 之后调用。本文件因此没有任何 Goal 写入面。
|
|
17
|
+
*
|
|
18
|
+
* ## M3 的两处深化 (为什么不是"又一个透传壳")
|
|
19
|
+
*
|
|
20
|
+
* 1. **统一监控**: 真实 tick 里"逐个 Goal 巡检"是散在各处的循环 (`execution-supervisor.ts` 那段
|
|
21
|
+
* `listGoalsWithPendingWork` → `collectWorkBlocks` → `applyBlockHandling`)。`sweepAll` 把这条循环
|
|
22
|
+
* 收成**一次调用**: 有界扫描 + 每个 Goal 一份结论 + **失败必须留痕** (`errors[]`),
|
|
23
|
+
* 不许"某个 Goal 巡检抛错 → 这一轮当它不存在"。
|
|
24
|
+
* 2. **用户只看到闭集**: `createVisibleStateProbe` 以前是**裸透传** (`return deps.visible(input)`),
|
|
25
|
+
* 端口给什么就往外说什么 —— 于是"用户视野只有这几类"只是端口自觉。现在它**逐值校验闭集**
|
|
26
|
+
* (`USER_VISIBLE_STATES`), 闭集外的值 / 端口抛错一律**拒绝**并说清原因, 而不是把内部词
|
|
27
|
+
* 顺手暴露给用户。
|
|
28
|
+
*
|
|
29
|
+
* ### 五类还是六类 (如实说明, 不改冻结层)
|
|
30
|
+
*
|
|
31
|
+
* 设计稿 §8 写的是"用户可见只暴露**五类**": 正在执行 / 等待外部回复 / 子 Agent 被阻塞 /
|
|
32
|
+
* 暂时没有进展 / 需要你决定。冻结层 (`types.ts`) 在 2026-09-25 接线时**追加了第 6 类** `ended`
|
|
33
|
+
* (理由: 前五类里没有"已结束", 已结束的目标只能借用 `executing` 或 `needs_your_decision`,
|
|
34
|
+
* 那正是 P3/P4 要防的假象)。改冻结层要单独一个提交 + 更新类型计数门, **不归 M3**。
|
|
35
|
+
* 所以本接缝按**冻结闭集** (五类 + `ended`) 钉住: 断言"返回值只能是闭集里的值"是硬的,
|
|
36
|
+
* 断言"闭集有几个值"留给冻结层 (`goal-flywheel-types.test.ts` 的计数门)。
|
|
37
|
+
*/
|
|
38
|
+
import { USER_VISIBLE_STATES, USER_VISIBLE_STATE_LABELS, } from '../types.js';
|
|
39
|
+
// 自己那个阶段的实现模块 (P3, 纯函数): "用户该看到哪一类"的判定只有一份
|
|
40
|
+
import { normalizeContinuationRecord, toUserVisibleState } from '../work-monitor.js';
|
|
41
|
+
import { refuse } from './seams.js';
|
|
42
|
+
export const MONITOR_SEAM_ID = 'monitor';
|
|
43
|
+
/** 统一巡检的默认/上限 (有界: 不许在 tick 里无界扫盘) */
|
|
44
|
+
export const MONITOR_SWEEP_DEFAULT_LIMIT = 20;
|
|
45
|
+
export const MONITOR_SWEEP_MAX_LIMIT = 200;
|
|
46
|
+
function errText(e) {
|
|
47
|
+
return String(e?.message || e).slice(0, 300);
|
|
48
|
+
}
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// §1. 用户可见面: 闭集校验 + 内部词扫描 (纯函数, 可被门/变异验证复用)
|
|
51
|
+
// ============================================================================
|
|
52
|
+
/**
|
|
53
|
+
* 一个 continuation 决策是否长得像 `ContinuationDecision` (只做形状检查, 不猜内容)。
|
|
54
|
+
* 用它把 `decision?: unknown` 安全地喂给真 P3 判定: 不像就传 null (宁可少一层精化, 不许瞎 cast)。
|
|
55
|
+
*/
|
|
56
|
+
function isDecisionLike(x) {
|
|
57
|
+
if (!x || typeof x !== 'object')
|
|
58
|
+
return false;
|
|
59
|
+
const d = x;
|
|
60
|
+
return typeof d.decision === 'string' || typeof d.state === 'string';
|
|
61
|
+
}
|
|
62
|
+
/** 用户可见态的**闭集** (冻结层唯一事实来源; 本文件不另立一份) */
|
|
63
|
+
export const USER_FACING_STATES = USER_VISIBLE_STATES;
|
|
64
|
+
export function isUserVisibleState(x) {
|
|
65
|
+
return typeof x === 'string' && USER_FACING_STATES.includes(x);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* **不该出现在用户视野里**的内部词 (设计 §8 的名单 + 同义写法)。
|
|
69
|
+
*
|
|
70
|
+
* 为什么连 `retry_count` / `leaseOwner` 这种代码标识也扫: 界面拼字符串时最常干的事就是
|
|
71
|
+
* 把内部字段名顺手带出去 (`lease owner=worker-3`), 只扫中文词会漏掉这一类。
|
|
72
|
+
*/
|
|
73
|
+
export const INTERNAL_VISIBLE_WORDS = [
|
|
74
|
+
'lease',
|
|
75
|
+
'leaseOwner',
|
|
76
|
+
'lease_until',
|
|
77
|
+
'reducer',
|
|
78
|
+
'internal status',
|
|
79
|
+
'retry counter',
|
|
80
|
+
'retryCounter',
|
|
81
|
+
'retry_count',
|
|
82
|
+
'worker owner',
|
|
83
|
+
'workerOwner',
|
|
84
|
+
'attempts',
|
|
85
|
+
'抢租约',
|
|
86
|
+
'租约',
|
|
87
|
+
'内部状态',
|
|
88
|
+
'重试计数',
|
|
89
|
+
];
|
|
90
|
+
/** 扫出文本里出现的内部词 (空数组 = 干净)。大小写不敏感; 只报词, 不改文本。 */
|
|
91
|
+
export function scanInternalLeak(text) {
|
|
92
|
+
const s = String(text ?? '');
|
|
93
|
+
if (!s)
|
|
94
|
+
return [];
|
|
95
|
+
const lower = s.toLowerCase();
|
|
96
|
+
const hits = [];
|
|
97
|
+
for (const w of INTERNAL_VISIBLE_WORDS) {
|
|
98
|
+
if (lower.includes(w.toLowerCase()))
|
|
99
|
+
hits.push(w);
|
|
100
|
+
}
|
|
101
|
+
return hits;
|
|
102
|
+
}
|
|
103
|
+
/** 闭集里的一个状态 → 用户看到的**全部**内容 (只有这三样, 没有内部字段) */
|
|
104
|
+
export function userFacingState(state) {
|
|
105
|
+
if (!isUserVisibleState(state)) {
|
|
106
|
+
return refuse('only_supervisor_decides_continuation', `拒绝: 用户可见态 ${JSON.stringify(state) ?? String(state)} 不在冻结闭集里 `
|
|
107
|
+
+ `(${USER_FACING_STATES.join(' / ')}) —— 用户可见态是 continuation 的投影, 不许第三方编一个状态出来`);
|
|
108
|
+
}
|
|
109
|
+
const labels = USER_VISIBLE_STATE_LABELS[state];
|
|
110
|
+
return { state, zh: labels.zh, en: labels.en };
|
|
111
|
+
}
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// §2. 接缝本体
|
|
114
|
+
// ============================================================================
|
|
115
|
+
export function createMonitorSeam(deps) {
|
|
116
|
+
// 可见面来源: 宿主注入的 `visible` 优先; 否则用**真 P3** 的判定 (注入的 continuation 是它的输入)。
|
|
117
|
+
// 两条路都不通 ⇒ 探针会如实拒绝 (不编一个"正在执行")。
|
|
118
|
+
const visiblePort = deps.visible ?? (deps.continuationOf
|
|
119
|
+
? async (input) => {
|
|
120
|
+
const raw = await deps.continuationOf(input.goalId);
|
|
121
|
+
// 盘上的 continuation 允许缺字段 (goal-store 的 `pendingReports?` 是 optional; `setContinuation`
|
|
122
|
+
// 又是部分覆盖) —— 归一化只有**一份**实现 (`work-monitor.normalizeContinuationRecord`),
|
|
123
|
+
// 判定层(`toUserVisibleState`)自己也调它; 这里用同一个函数是为了让"接缝这一路也没传半份记录"可读。
|
|
124
|
+
const continuation = normalizeContinuationRecord(raw);
|
|
125
|
+
const decision = isDecisionLike(input.decision) ? input.decision : null;
|
|
126
|
+
return toUserVisibleState(continuation, input.blocks ?? [], decision);
|
|
127
|
+
}
|
|
128
|
+
: undefined);
|
|
129
|
+
const visibleOrRefuse = createVisibleStateProbe({ visible: visiblePort });
|
|
130
|
+
return {
|
|
131
|
+
id: MONITOR_SEAM_ID,
|
|
132
|
+
stage: 'M3',
|
|
133
|
+
async sweep(input) {
|
|
134
|
+
if (!input.goalId) {
|
|
135
|
+
return refuse('all_terminal_paths_pass_close_run', '拒绝: 阻塞巡检没有 goalId → 拿到的是"谁都不卡"的假结论');
|
|
136
|
+
}
|
|
137
|
+
return deps.collect({ goalId: input.goalId, now: input.now, runnerAvailable: input.runnerAvailable });
|
|
138
|
+
},
|
|
139
|
+
async handle(input) {
|
|
140
|
+
if (!input.blocks?.length)
|
|
141
|
+
return { actions: [], escalated: [], takeovers: [], requests: [] };
|
|
142
|
+
return deps.handle({ goalId: input.goalId, blocks: input.blocks, now: input.now });
|
|
143
|
+
},
|
|
144
|
+
pendingGoals(limit) {
|
|
145
|
+
return deps.goalsWithPendingWork(limit);
|
|
146
|
+
},
|
|
147
|
+
/**
|
|
148
|
+
* 统一监控 (M3): 一次调用 = 所有待回报 Goal 的阻塞结论。
|
|
149
|
+
*
|
|
150
|
+
* 纪律:
|
|
151
|
+
* · **子 Agent 不许巡检别人的阻塞**: `caller='child_agent'` → 拒 (规则 ⑤, 一条都不查);
|
|
152
|
+
* · **有界**: `limit` 夹在 [1, 200] (默认 20) —— tick 里扫盘不许无界;
|
|
153
|
+
* · **不静默**: 单个 Goal 巡检/处置抛错 → 该 Goal 的 `error` 与顶层 `errors[]` 都留痕,
|
|
154
|
+
* 而不是"这一轮它不存在"。全失败时 `silentRisk=true` 并要求交人;
|
|
155
|
+
* · 没有阻塞的 Goal **也占一格** (它有待回报工作 → 用户看到的是"等待外部回复", 不是"没人管")。
|
|
156
|
+
*/
|
|
157
|
+
async sweepAll(input) {
|
|
158
|
+
if (input.caller === 'child_agent') {
|
|
159
|
+
return refuse('child_cannot_mutate_goal', '拒绝: 子 Agent 不许巡检/处置其它执行者的阻塞 —— 巡检与处置是父/调度器的责任面');
|
|
160
|
+
}
|
|
161
|
+
const limit = Math.min(Math.max(Math.floor(input.limit ?? MONITOR_SWEEP_DEFAULT_LIMIT) || 1, 1), MONITOR_SWEEP_MAX_LIMIT);
|
|
162
|
+
const view = {
|
|
163
|
+
goals: [], blockCount: 0, escalated: [], takeovers: [], requests: [],
|
|
164
|
+
visibleCounts: {}, errors: [], silentRisk: false,
|
|
165
|
+
};
|
|
166
|
+
let pending = [];
|
|
167
|
+
try {
|
|
168
|
+
const raw = await deps.goalsWithPendingWork(limit);
|
|
169
|
+
pending = Array.isArray(raw) ? raw.filter((g) => !!g?.goalId).slice(0, limit) : [];
|
|
170
|
+
if (!Array.isArray(raw))
|
|
171
|
+
view.errors.push('待回报 Goal 清单不是数组 (宿主 bug) —— 无法巡检, 不许当成"没有阻塞"');
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
view.errors.push(`取待回报 Goal 清单失败: ${errText(e)}`);
|
|
175
|
+
view.silentRisk = true;
|
|
176
|
+
return view; // 拿不到事实就如实报 (不是"巡检过了, 没问题")
|
|
177
|
+
}
|
|
178
|
+
for (const g of pending) {
|
|
179
|
+
const cell = { goalId: g.goalId, blocks: [], handling: null, visibleState: null, error: null };
|
|
180
|
+
try {
|
|
181
|
+
const blocks = await deps.collect({ goalId: g.goalId, now: input.now, runnerAvailable: input.runnerAvailable });
|
|
182
|
+
cell.blocks = Array.isArray(blocks) ? blocks : [];
|
|
183
|
+
if (cell.blocks.length > 0) {
|
|
184
|
+
cell.handling = await deps.handle({ goalId: g.goalId, blocks: cell.blocks, now: input.now });
|
|
185
|
+
view.escalated.push(...(cell.handling?.escalated ?? []));
|
|
186
|
+
view.takeovers.push(...(cell.handling?.takeovers ?? []));
|
|
187
|
+
view.requests.push(...(cell.handling?.requests ?? []));
|
|
188
|
+
}
|
|
189
|
+
const vis = await visibleOrRefuse({ goalId: g.goalId, blocks: cell.blocks, now: input.now });
|
|
190
|
+
if (vis && typeof vis === 'object' && vis.ok === false) {
|
|
191
|
+
// 可见面拒绝 → 这一格如实留痕 (不是静默给个"正在执行")
|
|
192
|
+
cell.error = vis.reason;
|
|
193
|
+
view.errors.push(`goal=${g.goalId} 可见态: ${vis.reason}`);
|
|
194
|
+
}
|
|
195
|
+
else if (vis) {
|
|
196
|
+
cell.visibleState = vis;
|
|
197
|
+
view.visibleCounts[cell.visibleState] = (view.visibleCounts[cell.visibleState] ?? 0) + 1;
|
|
198
|
+
}
|
|
199
|
+
view.blockCount += cell.blocks.length;
|
|
200
|
+
}
|
|
201
|
+
catch (e) {
|
|
202
|
+
cell.error = errText(e);
|
|
203
|
+
view.errors.push(`goal=${g.goalId} 巡检失败: ${cell.error}`);
|
|
204
|
+
}
|
|
205
|
+
view.goals.push(cell);
|
|
206
|
+
}
|
|
207
|
+
const inspected = view.goals.filter((c) => c.error === null).length;
|
|
208
|
+
if (pending.length > 0 && inspected === 0) {
|
|
209
|
+
view.silentRisk = true;
|
|
210
|
+
view.escalated.push(...pending.map((g) => `巡检未完成: ${g.goalId}`));
|
|
211
|
+
}
|
|
212
|
+
return view;
|
|
213
|
+
},
|
|
214
|
+
async visibleState(input) {
|
|
215
|
+
return visibleOrRefuse({ goalId: input.goalId, blocks: input.blocks, decision: input.decision, now: input.now });
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* 用户可见态探针: 只回答"用户该看到哪一类", 且**只**能是冻结闭集里的值。
|
|
221
|
+
*
|
|
222
|
+
* 硬化的三件事 (旧版是 `return deps.visible(input)` 的裸透传):
|
|
223
|
+
* ① **闭集校验**: 端口给出闭集外的值 (或 undefined / null / 对象) → 拒绝 + 说清收到了什么 ——
|
|
224
|
+
* 用户视野的假象比"什么都不显示"更贵;
|
|
225
|
+
* ② **端口抛错不吞**: 原文进拒绝理由 (不静默降级成 `executing`);
|
|
226
|
+
* ③ **缺端口 = 拒**: 没有可见面来源就不编一个状态 (旧版会直接 `TypeError`)。
|
|
227
|
+
*
|
|
228
|
+
* 返回值用冻结类型 + `userFacingState` 的文案; 调用方拿到的就是用户看到的东西。
|
|
229
|
+
*/
|
|
230
|
+
export function createVisibleStateProbe(deps) {
|
|
231
|
+
return async (input) => {
|
|
232
|
+
if (typeof deps.visible !== 'function') {
|
|
233
|
+
return refuse('only_supervisor_decides_continuation', '拒绝: 没有注入用户可见面 (拿不到权威 continuation 的投影) —— 不许编一个状态给用户');
|
|
234
|
+
}
|
|
235
|
+
let raw;
|
|
236
|
+
try {
|
|
237
|
+
raw = await deps.visible(input);
|
|
238
|
+
}
|
|
239
|
+
catch (e) {
|
|
240
|
+
return refuse('only_supervisor_decides_continuation', `拒绝: 用户可见面抛错 (${errText(e)}) —— 不静默降级成"正在执行"`);
|
|
241
|
+
}
|
|
242
|
+
const view = userFacingState(raw);
|
|
243
|
+
if (view.ok === false)
|
|
244
|
+
return view;
|
|
245
|
+
return view.state;
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* 给界面用的**唯一**文案出口: 状态 → `{state, zh, en}` (闭集外的值直接拒绝)。
|
|
250
|
+
* 界面不许自己拼状态名, 也不许把内部字段带出去 (见 `scanInternalLeak`)。
|
|
251
|
+
*/
|
|
252
|
+
export function describeVisibleState(state) {
|
|
253
|
+
return userFacingState(state);
|
|
254
|
+
}
|