@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
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* ④ 用 eventId 去重 ⑤ 写入事件事实 (Goal 证据) ⑥ 唤醒 Goal ⑦ 由 Supervisor 下一轮继续
|
|
11
11
|
* **事件处理器不直接启动 agent** —— 它只写事实 + 唤醒; 谁来执行由 Supervisor 决定。
|
|
12
12
|
*/
|
|
13
|
-
import { readGoal, setContinuation,
|
|
13
|
+
import { readGoal, setContinuation, listGoals, addEvidence, } from './goal-store.js';
|
|
14
|
+
// 2026-09-25 (M0 接线冻结, 规则 ②): Goal 状态变更只有一个漏斗
|
|
15
|
+
import { reduceGoalState } from './goal-state-reducer.js';
|
|
14
16
|
// ── 绑定等待 ────────────────────────────────────────────────────────────────
|
|
15
17
|
export function defaultWaitExpiry(now = Date.now(), ttlMs = 30 * 60_000) {
|
|
16
18
|
return new Date(now + ttlMs).toISOString();
|
|
@@ -114,6 +116,13 @@ export async function deliverExternalEvent(event, deps = {}) {
|
|
|
114
116
|
await addEvidence(goal.goalId, [
|
|
115
117
|
`外部事件 (${event.source}${event.fromDid ? `, ${String(event.fromDid).slice(0, 16)}…` : ''}): ${event.eventName || '结果'} eventId=${event.eventId} requestId=${event.requestId || '-'} payload=${JSON.stringify(event.payload ?? null).slice(0, 300)}`,
|
|
116
118
|
]).catch(() => { });
|
|
119
|
+
// ★ 等待类的 continuation.state 也必须一起拉回 `active`:
|
|
120
|
+
// 飞轮的"等外部"判定读的是 continuation.state (`state=waiting_external` → wait), 而界面
|
|
121
|
+
// (`toUserVisibleState`) 也只读 continuation —— 只改 wakeReason 不改 state 会留下
|
|
122
|
+
// "wakeReason=active 但 state=awaiting_external" 的自相矛盾 (M5-⑤ 真跑复现: Goal 显示已醒,
|
|
123
|
+
// 下一轮却仍被判"在等外部", 永远没人跑)。
|
|
124
|
+
const wasWaiting = ['awaiting_external', 'retry_wait', 'recovering'].includes(String(goal.status))
|
|
125
|
+
|| ['awaiting_external', 'retry_wait', 'recovering'].includes(String(goal.continuation?.state));
|
|
117
126
|
await setContinuation(goal.goalId, {
|
|
118
127
|
deliveredEventIds: [...seen, event.eventId].slice(-20),
|
|
119
128
|
external: undefined,
|
|
@@ -122,18 +131,33 @@ export async function deliverExternalEvent(event, deps = {}) {
|
|
|
122
131
|
needsExternal: undefined,
|
|
123
132
|
autoContinue: true,
|
|
124
133
|
wakeAt: undefined,
|
|
134
|
+
...(wasWaiting ? { state: 'active' } : {}),
|
|
125
135
|
});
|
|
126
136
|
// 状态拉回 active (只有还在"等外部"的状态才动它) —— Supervisor 下一轮才会真的执行
|
|
137
|
+
// 规则 ②: 状态变更经 Goal reducer (单一漏斗; 它自己会跳过终态 Goal)
|
|
127
138
|
try {
|
|
128
139
|
if (['awaiting_external', 'retry_wait', 'recovering'].includes(String(goal.status))) {
|
|
129
|
-
await
|
|
140
|
+
await reduceGoalState({
|
|
141
|
+
goalId: goal.goalId,
|
|
142
|
+
intent: 'external_event_arrived',
|
|
143
|
+
now: new Date(now).toISOString(),
|
|
144
|
+
by: 'external-events',
|
|
145
|
+
});
|
|
130
146
|
}
|
|
131
147
|
}
|
|
132
148
|
catch { /* 状态写失败 → 下一轮仍会跳过等待; 事件事实已写入, 不丢 */ }
|
|
133
149
|
// 唤醒 (由 Supervisor 下一轮真正执行)
|
|
150
|
+
// `woke` 的语义跟着**盘上事实**走, 不只看宿主回调的返回值: 上面已经把等待事实清掉、把
|
|
151
|
+
// wakeReason/state 拉回 active 了 —— 宿主注入的 wake (= `Supervisor.notifyExternal`) 的
|
|
152
|
+
// "还在等外部吗"前置条件此刻已不成立, 它会早退返回 false。若照抄它的返回值, 一次真唤醒会被
|
|
153
|
+
// 报成"没唤醒" (M5-⑤ 真跑就是这么读到的)。宿主回调仍然要调 (它有清等待/记次数的副作用)。
|
|
134
154
|
let woke = false;
|
|
135
155
|
if (deps.wake)
|
|
136
|
-
|
|
156
|
+
await deps.wake(goal.goalId).catch(() => false);
|
|
157
|
+
const after = await readGoal(goal.goalId);
|
|
158
|
+
const stillWaiting = !!after && (['awaiting_external', 'retry_wait', 'recovering'].includes(String(after.status))
|
|
159
|
+
|| after.continuation?.state === 'awaiting_external');
|
|
160
|
+
woke = !!after && !stillWaiting;
|
|
137
161
|
return { ok: true, reason: 'delivered', goalId: goal.goalId, woke };
|
|
138
162
|
}
|
|
139
163
|
/**
|
|
@@ -154,9 +178,14 @@ export async function expireExternalWaits(opts = {}) {
|
|
|
154
178
|
continue;
|
|
155
179
|
const reason = `外部事件超时 (${w.expectedSource}${w.expectedEvent ? `:${w.expectedEvent}` : ''}, requestId=${w.requestId}, 过期于 ${w.expiresAt}) → 转人工`;
|
|
156
180
|
out.push({ goalId: g.goalId, wait: w, reason });
|
|
157
|
-
|
|
158
|
-
await
|
|
159
|
-
|
|
181
|
+
// 规则 ②: 撤等待 + 转人工 + 证据, 一次走 Goal reducer (旧写法是 setContinuation + updateGoal + addEvidence 三连)
|
|
182
|
+
await reduceGoalState({
|
|
183
|
+
goalId: g.goalId,
|
|
184
|
+
intent: 'external_wait_expired',
|
|
185
|
+
now: new Date(now).toISOString(),
|
|
186
|
+
by: 'external-events',
|
|
187
|
+
reason,
|
|
188
|
+
}).catch(() => { });
|
|
160
189
|
}
|
|
161
190
|
return out;
|
|
162
191
|
}
|
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 符合用户习惯: 群组 = 微信式群聊 — 加入链接即进群, 发消息全网同步.
|
|
8
8
|
* 持久化: ~/.bolloon/gateway-groups.json (重启后仍是群成员, 自动重开 store).
|
|
9
|
+
*
|
|
10
|
+
* 2026-09-24 跨进程可重开:
|
|
11
|
+
* · 群消息 (OrbitDB events store 的 oplog 条目 + manifest) 现在真落盘 ——
|
|
12
|
+
* `createBolloonIpfs` 用 FsBlockstore/FsDatastore 写 ~/.bolloon/orbitdb/ipfs/{blocks,datastore},
|
|
13
|
+
* 所以"建群的那个进程退出后, 新进程用群链接重开还能读到既有消息"。
|
|
14
|
+
* (修复前: 区块只在内存, 新进程一开就报 "No block brokers capable of retrieving blocks")
|
|
15
|
+
* · 读不到必须报读不到: `groupMessages` 在 store 打不开时**抛 GroupStoreUnreachableError**,
|
|
16
|
+
* 不再返回 `[]`。把"读不到"显示成"群里没有消息"是骗人, 本条不可回退。
|
|
9
17
|
*/
|
|
10
18
|
import * as os from 'os';
|
|
11
19
|
import * as path from 'path';
|
|
@@ -19,6 +27,33 @@ function getDb() { return _dbOverride ?? getCIDDatabase(); }
|
|
|
19
27
|
export function resetGroupState() {
|
|
20
28
|
storeCache.clear();
|
|
21
29
|
onChangeCallbacks.clear();
|
|
30
|
+
openFailures.clear();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 群 store 打不开 (区块不在本机 / 地址不可解析)。
|
|
34
|
+
*
|
|
35
|
+
* 2026-09-24: 以前打不开 → `groupMessages` 返回 `[]`, 于是 CLI 把"整个群读不到"报成
|
|
36
|
+
* "群里本期没有过程痕迹 (群里真没有, 不是读失败)" —— 把"读不到"说成了"没有"。
|
|
37
|
+
* 现在读不到就抛: 上游 (task trail / API) 必须如实报 TRANSPORT_FAILED。
|
|
38
|
+
*/
|
|
39
|
+
export class GroupStoreUnreachableError extends Error {
|
|
40
|
+
groupId;
|
|
41
|
+
address;
|
|
42
|
+
cause;
|
|
43
|
+
code = 'STORE_UNREACHABLE';
|
|
44
|
+
constructor(groupId, address, cause) {
|
|
45
|
+
const why = String(cause?.message ?? cause ?? '未知原因').slice(0, 160);
|
|
46
|
+
super(`群组 store 不可达 (${groupId}): ${why}`);
|
|
47
|
+
this.groupId = groupId;
|
|
48
|
+
this.address = address;
|
|
49
|
+
this.cause = cause;
|
|
50
|
+
this.name = 'GroupStoreUnreachableError';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** 打开失败的一句话原因 (给人类输出用; 没有失败记录时返回通用话术) */
|
|
54
|
+
function unreachableReason(err) {
|
|
55
|
+
const m = String(err?.message ?? err ?? '').trim();
|
|
56
|
+
return m ? m.slice(0, 160) : '区块不在本机且没有可用的 block broker (跨机同步仍是另一回事, 需 peers)';
|
|
22
57
|
}
|
|
23
58
|
// ============ 持久化 ============
|
|
24
59
|
const groupsFile = () => path.join(os.homedir() || '/tmp', '.bolloon', 'gateway-groups.json');
|
|
@@ -71,23 +106,42 @@ export function detectGroupLink(text) {
|
|
|
71
106
|
const storeCache = new Map();
|
|
72
107
|
/** 订阅回调注册 (server 层挂 SSE 广播) */
|
|
73
108
|
const onChangeCallbacks = new Map();
|
|
109
|
+
/** store 打开失败的原始原因 (按 groupId) —— 用来把"打不开"和"没有消息"分开 */
|
|
110
|
+
const openFailures = new Map();
|
|
74
111
|
function groupIdOf(address) {
|
|
75
112
|
// /orbitdb/zdpu... → zdpu... (地址后 12 位做短 id)
|
|
76
113
|
const m = /\/orbitdb\/(.{8,})/.exec(address);
|
|
77
114
|
return m ? m[1] : address;
|
|
78
115
|
}
|
|
79
|
-
/**
|
|
116
|
+
/**
|
|
117
|
+
* 打开群组 store (缓存) — 可写 (replica=false, write:'*')。
|
|
118
|
+
* 打不开 → 记下原始原因 + 返回 null (不抛: 调用方各自决定用什么话术报)。
|
|
119
|
+
* 2026-09-24: 必须 try/catch —— openStoreByAddress 现在抛 OrbitDBStoreUnreachableError。
|
|
120
|
+
*/
|
|
80
121
|
async function openGroupStore(address) {
|
|
81
122
|
const id = groupIdOf(address);
|
|
82
123
|
if (storeCache.has(id))
|
|
83
124
|
return storeCache.get(id);
|
|
84
125
|
const db = getDb();
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
126
|
+
let store = null;
|
|
127
|
+
try {
|
|
128
|
+
// 2026-09-24: 不再传 `replica` —— `@orbitdb/core` 4.0.0 的 open() 没有这个参数
|
|
129
|
+
// (src/orbitdb.js:118), 传了被丢; 之前那行是"不存在的语义"。
|
|
130
|
+
// 可写与否由 manifest 里的 ACL 决定, 且**打开既有地址时 ACL 从 manifest 取回、
|
|
131
|
+
// 入参 accessController 被覆盖** (src/orbitdb.js:129-131) —— 所以这里传什么都不改变权限。
|
|
132
|
+
store = await db.openStoreByAddress(address, 'events', {
|
|
133
|
+
accessController: { write: ['*'] },
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
openFailures.set(id, e);
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
if (!store) {
|
|
141
|
+
openFailures.set(id, new Error('openStoreByAddress 返回 null (没拿到 store, 也没给出原因)'));
|
|
90
142
|
return null;
|
|
143
|
+
}
|
|
144
|
+
openFailures.delete(id);
|
|
91
145
|
storeCache.set(id, store);
|
|
92
146
|
// 订阅: 新消息 → 通知回调 (server SSE)
|
|
93
147
|
store.onChange(() => {
|
|
@@ -161,7 +215,11 @@ export async function joinGroup(link) {
|
|
|
161
215
|
}
|
|
162
216
|
const store = await openGroupStore(parsed.address);
|
|
163
217
|
if (!store) {
|
|
164
|
-
return {
|
|
218
|
+
return {
|
|
219
|
+
ok: false,
|
|
220
|
+
code: 'STORE_UNREACHABLE',
|
|
221
|
+
error: `群组 store 不可达: ${unreachableReason(openFailures.get(id))}`,
|
|
222
|
+
};
|
|
165
223
|
}
|
|
166
224
|
const name = parsed.name || id.slice(0, 12);
|
|
167
225
|
const info = {
|
|
@@ -177,7 +235,36 @@ export async function joinGroup(link) {
|
|
|
177
235
|
export async function listGroups() {
|
|
178
236
|
return loadGroups();
|
|
179
237
|
}
|
|
180
|
-
/**
|
|
238
|
+
/**
|
|
239
|
+
* 退群 (2026-09-24): 从本机群列表里摘掉 (撤销"我是成员"这条本地事实)。
|
|
240
|
+
*
|
|
241
|
+
* 只改**本机**记录: 群 store 本身是公共 append-only 的, 别人那边的成员表/消息
|
|
242
|
+
* 不会因为本机退出而改 (本模块没有"踢人/解散"的权限, 也不假装有)。
|
|
243
|
+
* 返回 `removed` = 被摘掉的 groupId (按 id 或群名匹配)。
|
|
244
|
+
*
|
|
245
|
+
* 注意: 底层 OrbitDB store 已在进程内打开的**不会**在这里关 (适配层没暴露 close);
|
|
246
|
+
* 对 `bolloon task group leave` 这种一次性进程无影响, 长驻进程里它活到进程结束。
|
|
247
|
+
*/
|
|
248
|
+
export async function leaveGroup(idOrName) {
|
|
249
|
+
const raw = String(idOrName || '').trim();
|
|
250
|
+
if (!raw)
|
|
251
|
+
return { ok: false, error: '缺少 groupId (或群名)' };
|
|
252
|
+
const groups = await loadGroups();
|
|
253
|
+
const hit = groups.find((g) => g.id === raw) || groups.find((g) => g.name === raw) || null;
|
|
254
|
+
if (!hit)
|
|
255
|
+
return { ok: false, error: `本机没有这个群: ${raw} (既不是已加入群的 groupId, 也不是群名)` };
|
|
256
|
+
storeCache.delete(hit.id);
|
|
257
|
+
openFailures.delete(hit.id);
|
|
258
|
+
await saveGroups(groups.filter((g) => g.id !== hit.id));
|
|
259
|
+
return { ok: true, removed: hit.id };
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* 获取群组 store 的最新消息 (ts 升序, 取最后 N 条)。
|
|
263
|
+
*
|
|
264
|
+
* 两个"空"必须分开 (2026-09-24):
|
|
265
|
+
* · 本机没这个群 (不在群列表里) → 返回 `[]` (上游 resolveGroupRef 已单独拦成 NOT_FOUND)
|
|
266
|
+
* · 群在列表里但 store 打不开 → **抛 GroupStoreUnreachableError** (读不到 ≠ 没有消息)
|
|
267
|
+
*/
|
|
181
268
|
export async function groupMessages(groupId, limit = 50) {
|
|
182
269
|
let store = storeCache.get(groupId) ?? null;
|
|
183
270
|
if (!store) {
|
|
@@ -187,9 +274,9 @@ export async function groupMessages(groupId, limit = 50) {
|
|
|
187
274
|
return [];
|
|
188
275
|
store = await openGroupStore(g.address);
|
|
189
276
|
if (!store)
|
|
190
|
-
|
|
277
|
+
throw new GroupStoreUnreachableError(groupId, g.address, openFailures.get(groupId));
|
|
191
278
|
}
|
|
192
|
-
const all = await store.all()
|
|
279
|
+
const all = await store.all();
|
|
193
280
|
const msgs = [];
|
|
194
281
|
for (const entry of all) {
|
|
195
282
|
const v = entry.value;
|
|
@@ -217,8 +304,9 @@ export async function groupSend(groupId, text, from) {
|
|
|
217
304
|
if (!g)
|
|
218
305
|
return { ok: false, error: '群组不存在 (先 joinGroup)' };
|
|
219
306
|
store = await openGroupStore(g.address);
|
|
220
|
-
if (!store)
|
|
221
|
-
return { ok: false, error:
|
|
307
|
+
if (!store) {
|
|
308
|
+
return { ok: false, error: `群组 store 不可达: ${unreachableReason(openFailures.get(groupId))}` };
|
|
309
|
+
}
|
|
222
310
|
}
|
|
223
311
|
try {
|
|
224
312
|
await store.add({ from: String(from || 'anonymous'), text: msg, ts: Date.now() });
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* · 证据跨 Run 汇总 (Run 证据 → Goal 证据, 去重), unresolvedItems 跨 Run 保留。
|
|
10
10
|
*/
|
|
11
11
|
import { readGoal, setCriteria, addEvidence, updateGoal, } from './goal-store.js';
|
|
12
|
+
// 2026-09-25 (M0 接线冻结, 规则 ②): Goal 状态变更只有一个漏斗
|
|
13
|
+
import { reduceGoalState } from './goal-state-reducer.js';
|
|
12
14
|
import { readRun } from './run-store.js';
|
|
13
15
|
/** 太模糊的目标: 判据无从谈起 (短且没有可核验的动作词) */
|
|
14
16
|
export function looksVague(objective) {
|
|
@@ -54,9 +56,14 @@ export async function proposeForGoal(goalId) {
|
|
|
54
56
|
}
|
|
55
57
|
const p = proposeCriteria(goal.objective);
|
|
56
58
|
if (!p.ok) {
|
|
57
|
-
// 太模糊 → 明确交人, 不写假判据
|
|
58
|
-
await
|
|
59
|
-
|
|
59
|
+
// 太模糊 → 明确交人, 不写假判据 (规则 ②: 状态经 Goal reducer 写; 它同时留一条可追溯证据)
|
|
60
|
+
await reduceGoalState({
|
|
61
|
+
goalId,
|
|
62
|
+
intent: 'criteria_needs_human',
|
|
63
|
+
now: new Date().toISOString(),
|
|
64
|
+
by: 'goal-criteria',
|
|
65
|
+
reason: p.reason,
|
|
66
|
+
}).catch(() => null);
|
|
60
67
|
return p;
|
|
61
68
|
}
|
|
62
69
|
await setCriteria(goalId, { criteria: p.criteria, source: 'agent_proposed', confirm: false });
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* block-executor.ts — P3b: 把 `planBlockHandling` 算出的**处置动作真执行** (2026-09-26)
|
|
3
|
+
*
|
|
4
|
+
* 存在的理由 (P5 验收 §2 缺口 4, 原话): 「`applyBlockHandling` 只对 `needs_human` / `escalate_parent` /
|
|
5
|
+
* `change_plan` 真的改 Goal 状态 (→ needs_human) 或清 `pendingReports` (takeover); 而
|
|
6
|
+
* `send_adjustment` / `request_report` / **`replace_child`** 只被塞进 `requests` 字符串列表 ——
|
|
7
|
+
* 没有真的下发调整指令、没有真的换人、没有重派新合同」。
|
|
8
|
+
* 本模块补上"**真执行**"这一半: 动作 → 具体一次调用 (带正确的对象与参数), 并把
|
|
9
|
+
* 「做不了」的原因**显式**交人, 而不是静默塞进一个列表。
|
|
10
|
+
*
|
|
11
|
+
* ## 纪律 (与 `work-monitor.ts` 同一套, 不许松)
|
|
12
|
+
*
|
|
13
|
+
* - **纯逻辑**: 零 I/O —— 不 import `fs` / 不 import 其它阶段的实现, 只与 `types.ts` +
|
|
14
|
+
* `work-monitor.ts` 的**纯函数** (`planBlockHandling`) 耦合。所有副作用走**注入的端口**。
|
|
15
|
+
* - **时间注入**: `now` 由调用方给, 本模块不读真实钟。
|
|
16
|
+
* - **不越权**: ① 执行权不在手上 (`authority.leaseHeld !== true`) → `takeover` **拒绝执行,
|
|
17
|
+
* 端口一次都不调**; ② 调用者是子 Agent (`caller === 'child'`) → 一切改别人状态的动作全拒;
|
|
18
|
+
* ③ 工具/权限/预算被阻 → 即使记录里写着 `takeover`/`replace_child` 也**硬拒** (纵深防御,
|
|
19
|
+
* 不靠上游自觉)。
|
|
20
|
+
* - **不静默**: 每一项"本该做却做不了"的事 (端口缺失 / 端口抛错 / 越权被拒 / 没有动作可做)
|
|
21
|
+
* 都必须出现在 `needsHuman[]` 里, 并带人可读原因。`silentRisk` 是这份保证的自检位。
|
|
22
|
+
* - **只给结论不动手**: 本模块自己**不写** Goal / Run / 磁盘 —— 那是注入端口 (`record`) 与
|
|
23
|
+
* 接线层的事; 因此它不会绕过「只有 Goal reducer 能改 Goal 状态」这条冻结规则 (规则 ②)。
|
|
24
|
+
*
|
|
25
|
+
* ## 接入点 (接线 = 别人的文件, 本模块只写清"差哪一行")
|
|
26
|
+
*
|
|
27
|
+
* 真实 tick 里的接入点是 `src/agents/execution-supervisor.ts` 的 `tick()` 内、
|
|
28
|
+
* 阻塞巡检那一段 (**当前 L423–L439**「2.5 阻塞巡检」, `applyBlockHandling` 在 **L429**): 现在那里只调
|
|
29
|
+
* `collectWorkBlocks` + `applyBlockHandling`, 拿到 `handling.actions` 后仅写进 `report.blocks` 与 log
|
|
30
|
+
* (`handling.takeovers` 也只打了一行"父接管子工作"的日志)。**差的就是一行**:
|
|
31
|
+
*
|
|
32
|
+
* ```ts
|
|
33
|
+
* // execution-supervisor.ts, applyBlockHandling(...) 之后 (同一段 try 内)
|
|
34
|
+
* const execution = await executeBlockHandling({
|
|
35
|
+
* goalId: g.goalId,
|
|
36
|
+
* blocks,
|
|
37
|
+
* now: nowIso,
|
|
38
|
+
* // 执行权: 本 tick 内该 Goal 的 lease **尚未**被本 worker 持有 (认领发生在第 3 步) →
|
|
39
|
+
* // 这里如实传 false; 真正的父侧接管应由"已持有 lease 的那条路径"调 (见下)
|
|
40
|
+
* authority: { leaseHeld: false, caller: 'supervisor', leaseOwner: null },
|
|
41
|
+
* ports: blockExecutorPorts({ home }), // ← 这个 helper **还没写**: 属接线层的活 (本模块是纯逻辑)
|
|
42
|
+
* });
|
|
43
|
+
* for (const a of execution.needsHuman) {
|
|
44
|
+
* this.emit({ kind: 'needs_human', goalId: g.goalId, message: `${a.blockId}: ${a.reason}` });
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* 端口实现落在**接线层** (`goal-flywheel-wiring.ts`, 本模块不 import 它):
|
|
49
|
+
* `claimLease` → `goal-store.claimGoal` (抢到再 `heartbeatGoal` 续) ·
|
|
50
|
+
* `replaceChild` → `subagent-manager` 停旧 + `dispatchChildWork` 重派新合同 ·
|
|
51
|
+
* `sendAdjustment` / `requestReport` → `sendChildInstruction` 一类的下发面 ·
|
|
52
|
+
* `changePlan` → 父改计划 (写 Goal 的计划字段, 经 reducer) ·
|
|
53
|
+
* `escalate` → Goal reducer 的 `block_escalated_human` (to='parent' 在 Goal 层同样收敛到人) ·
|
|
54
|
+
* `record` → `addEvidence` + `recordWorkHeartbeat` 一类的事实记录。
|
|
55
|
+
* **本模块刻意不做这些事** —— 端口没注入时它只会如实报 `needs_human` (端口缺失), 不会假装做过。
|
|
56
|
+
*
|
|
57
|
+
* 阴性对照 (怎么知道这道门不是装饰; 见 `src/test/goal-flywheel-p6-block-executor.test.ts`):
|
|
58
|
+
* ① 把手上的 `leaseHeld` 传成 false → `takeover` 必须变成 `refused` 且 `claimLease` **零调用**;
|
|
59
|
+
* ② 把 `tool_blocked` 记录的 `suggestedAction` 改成 `replace_child` → 必须被硬拒 (不自动绕过 Harness);
|
|
60
|
+
* ③ 把所有端口拿掉 → 每一项都必须是 `needsHuman` (不是空数组、不是静默跳过);
|
|
61
|
+
* ④ 把 `escalate` 端口改成抛错 → 那一项必须是 `failed` + 原因里含错误原文 (不吞)。
|
|
62
|
+
*/
|
|
63
|
+
import { planBlockHandling } from './work-monitor.js';
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// 硬规则常量 (导出: 测试与接线层都不许自己编)
|
|
66
|
+
// ============================================================================
|
|
67
|
+
/**
|
|
68
|
+
* **永不**自动绕过 Harness 的阻塞类型。
|
|
69
|
+
*
|
|
70
|
+
* `planBlockHandling` 本身就不会给这些类型 `takeover`/`replace_child`; 本表是**纵深防御** ——
|
|
71
|
+
* 磁盘/JSON 造出来的记录 (`suggestedAction` 可以被外部写成任意值) 也挡得住。
|
|
72
|
+
*/
|
|
73
|
+
export const NO_AUTO_BYPASS_KINDS = [
|
|
74
|
+
'tool_blocked',
|
|
75
|
+
'permission_blocked',
|
|
76
|
+
'budget_blocked',
|
|
77
|
+
];
|
|
78
|
+
/** 这些动作要动别人的执行状态, 子 Agent 侧一律不许做 (只允许父/调度器/人) */
|
|
79
|
+
export const PARENT_ONLY_ACTIONS = [
|
|
80
|
+
'takeover',
|
|
81
|
+
'replace_child',
|
|
82
|
+
'send_adjustment',
|
|
83
|
+
'request_report',
|
|
84
|
+
'change_plan',
|
|
85
|
+
'escalate_parent',
|
|
86
|
+
];
|
|
87
|
+
/** 没有任何副作用、只表示"继续等"的动作 (执行后记 `deferred`, 不算失败) */
|
|
88
|
+
export const DEFERRED_ACTIONS = ['wait_dependency'];
|
|
89
|
+
/** 动作 → 它需要的端口名 (缺端口 = 做不了 = 必须如实报人) */
|
|
90
|
+
export const PORT_FOR_ACTION = {
|
|
91
|
+
send_adjustment: 'sendAdjustment',
|
|
92
|
+
replace_child: 'replaceChild',
|
|
93
|
+
takeover: 'claimLease',
|
|
94
|
+
request_report: 'requestReport',
|
|
95
|
+
escalate_parent: 'escalate',
|
|
96
|
+
change_plan: 'changePlan',
|
|
97
|
+
wait_dependency: null, // 无副作用: 不需要端口
|
|
98
|
+
needs_human: 'escalate', // 有 escalate 就真的上报; 没有也必须在 needsHuman 里留下
|
|
99
|
+
};
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// 纯判定 (导出给接线层/测试复用: "这个动作在我这个处境下能不能做")
|
|
102
|
+
// ============================================================================
|
|
103
|
+
/** 该动作在给定处境下能不能做 (不能做时给出**人可读**的原因; 能做时 reason=null) */
|
|
104
|
+
export function mayExecuteAction(action, kind, authority) {
|
|
105
|
+
if (NO_AUTO_BYPASS_KINDS.includes(kind) && (action === 'takeover' || action === 'replace_child')) {
|
|
106
|
+
return {
|
|
107
|
+
allowed: false,
|
|
108
|
+
reason: `不自动绕过 Harness: 阻塞类型「${kind}」只允许 改计划/等依赖/转人工, 不得 ${action}`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (authority.caller === 'child' && PARENT_ONLY_ACTIONS.includes(action)) {
|
|
112
|
+
return { allowed: false, reason: `子 Agent 不得执行「${action}」—— 处置权只在父/调度器/人手上` };
|
|
113
|
+
}
|
|
114
|
+
if (action === 'takeover' && authority.leaseHeld !== true) {
|
|
115
|
+
return {
|
|
116
|
+
allowed: false,
|
|
117
|
+
reason: `执行权不在手上 (拿不到互斥执行权${authority.leaseOwner ? `, 现持有者=${authority.leaseOwner}` : ''}) → 拒绝接管, 交人`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return { allowed: true, reason: null };
|
|
121
|
+
}
|
|
122
|
+
/** 调整指令原文 (纯函数; 不含时间戳, 同一阻塞得到同一句话 —— 便于幂等比对) */
|
|
123
|
+
export function buildAdjustmentDirective(b) {
|
|
124
|
+
return `调整指令 (${b.kind}): ${b.note} —— 这是第一档处置 (先给一次调整, 不是立刻换人); `
|
|
125
|
+
+ `请就当前目标给出一次可核验的进展或明确的阻塞回报, 报告须带逐条证据。`;
|
|
126
|
+
}
|
|
127
|
+
/** 催报原文 (纯函数) */
|
|
128
|
+
export function buildReportRequestWhat(b) {
|
|
129
|
+
return `请补齐报告 (${b.kind}): ${b.note}${b.dependency ? ` [依赖=${b.dependency}]` : ''} —— `
|
|
130
|
+
+ `漂亮但没有逐条证据的回报不接受为完成。`;
|
|
131
|
+
}
|
|
132
|
+
// ============================================================================
|
|
133
|
+
// 执行 (唯一入口)
|
|
134
|
+
// ============================================================================
|
|
135
|
+
function uniq(items) {
|
|
136
|
+
return [...new Set(items)];
|
|
137
|
+
}
|
|
138
|
+
function errText(e) {
|
|
139
|
+
return String(e?.message || e).slice(0, 200);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* 执行一次阻塞处置。
|
|
143
|
+
*
|
|
144
|
+
* 顺序**固定** (便于复现): 逐条 (按输入顺序, 同 blockId 只做一次) →
|
|
145
|
+
* `planBlockHandling` 得动作 → `mayExecuteAction` 判权 → 端口存在性 → 真调用。
|
|
146
|
+
*
|
|
147
|
+
* 每一步失败都**显式**落进结果 (`refused` / `failed` + `needsHuman`), 绝不静默 ——
|
|
148
|
+
* 这是本模块存在的全部理由 (P5 缺口 4)。
|
|
149
|
+
*/
|
|
150
|
+
export async function executeBlockHandling(input) {
|
|
151
|
+
const out = {
|
|
152
|
+
executed: [], refused: [], deferred: [], failed: [], needsHuman: [], silentRisk: false, attempts: [],
|
|
153
|
+
};
|
|
154
|
+
if (!Array.isArray(input.blocks)) {
|
|
155
|
+
// 调用方 bug 不许静默: 说清"阻塞清单不是数组", 直接交人
|
|
156
|
+
out.needsHuman.push({
|
|
157
|
+
blockId: '(input)', workId: null, action: 'needs_human',
|
|
158
|
+
reason: '阻塞清单不是数组 (调用方 bug) —— 无法巡检, 不许当成"没有阻塞"',
|
|
159
|
+
});
|
|
160
|
+
out.silentRisk = true;
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
const unresolved = input.blocks.filter((b) => b && b.resolvedAt === null);
|
|
164
|
+
const seenBlocks = new Set();
|
|
165
|
+
const now = input.now;
|
|
166
|
+
const authority = input.authority;
|
|
167
|
+
for (const b of unresolved) {
|
|
168
|
+
if (seenBlocks.has(b.blockId))
|
|
169
|
+
continue;
|
|
170
|
+
seenBlocks.add(b.blockId);
|
|
171
|
+
if (input.onlyActions && !input.onlyActions.includes(planBlockHandling(b)))
|
|
172
|
+
continue;
|
|
173
|
+
const action = planBlockHandling(b);
|
|
174
|
+
const attempt = {
|
|
175
|
+
blockId: b.blockId, workId: b.workId ?? null, childAgentId: b.childAgentId ?? null,
|
|
176
|
+
kind: b.kind, action, outcome: 'refused', called: [], detail: '', recorded: false,
|
|
177
|
+
};
|
|
178
|
+
const base = {
|
|
179
|
+
goalId: input.goalId, workId: b.workId ?? null, childAgentId: b.childAgentId ?? null,
|
|
180
|
+
blockId: b.blockId, kind: b.kind, now, reason: b.note,
|
|
181
|
+
};
|
|
182
|
+
// 1. 权限 (越权一律不调端口)
|
|
183
|
+
const verdict = mayExecuteAction(action, b.kind, authority);
|
|
184
|
+
if (!verdict.allowed) {
|
|
185
|
+
attempt.outcome = 'refused';
|
|
186
|
+
attempt.detail = `拒绝执行 (未调任何执行器): ${verdict.reason}`;
|
|
187
|
+
out.refused.push(attempt);
|
|
188
|
+
out.attempts.push(attempt);
|
|
189
|
+
out.needsHuman.push({ blockId: b.blockId, workId: b.workId ?? null, action, reason: attempt.detail });
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
// 2. 端口存在性 (缺端口 = 做不了 = 交人, 不许静默跳过)
|
|
193
|
+
const portName = PORT_FOR_ACTION[action];
|
|
194
|
+
const port = portName ? input.ports[portName] : null;
|
|
195
|
+
if (portName && typeof port !== 'function') {
|
|
196
|
+
attempt.outcome = 'refused';
|
|
197
|
+
attempt.detail = `没有注入执行「${action}」的执行器 (端口 ${portName} 缺失) → 处置无法落地, 交人`;
|
|
198
|
+
out.refused.push(attempt);
|
|
199
|
+
out.attempts.push(attempt);
|
|
200
|
+
out.needsHuman.push({ blockId: b.blockId, workId: b.workId ?? null, action, reason: attempt.detail });
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
// 3. 真调用 (端口抛错 = failed + 原因原文, 不吞)
|
|
204
|
+
try {
|
|
205
|
+
await dispatchAction(action, attempt, b, base, input.ports, authority);
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
attempt.outcome = 'failed';
|
|
209
|
+
attempt.detail = `执行器抛错: ${errText(e)}`;
|
|
210
|
+
out.failed.push(attempt);
|
|
211
|
+
out.attempts.push(attempt);
|
|
212
|
+
out.needsHuman.push({ blockId: b.blockId, workId: b.workId ?? null, action, reason: attempt.detail });
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (attempt.outcome === 'executed')
|
|
216
|
+
out.executed.push(attempt);
|
|
217
|
+
else if (attempt.outcome === 'deferred')
|
|
218
|
+
out.deferred.push(attempt);
|
|
219
|
+
else
|
|
220
|
+
out.refused.push(attempt);
|
|
221
|
+
out.attempts.push(attempt);
|
|
222
|
+
// 4. 显式交人: 动作本身就是转人工, 或端口如实回"没做成"
|
|
223
|
+
if (action === 'needs_human') {
|
|
224
|
+
out.needsHuman.push({
|
|
225
|
+
blockId: b.blockId, workId: b.workId ?? null, action,
|
|
226
|
+
reason: `${b.note} → 需要人决定 (加预算 / 改权限 / 换目标都属于人)`,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
else if (attempt.outcome === 'refused') {
|
|
230
|
+
out.needsHuman.push({ blockId: b.blockId, workId: b.workId ?? null, action, reason: attempt.detail });
|
|
231
|
+
}
|
|
232
|
+
// 5. 记事实 (可选端口; 不注入会在 attempt.recorded 里如实标 false)
|
|
233
|
+
if (input.ports.record) {
|
|
234
|
+
try {
|
|
235
|
+
await input.ports.record({
|
|
236
|
+
goalId: input.goalId, blockId: b.blockId, workId: b.workId ?? null,
|
|
237
|
+
action, outcome: attempt.outcome, now, detail: attempt.detail,
|
|
238
|
+
});
|
|
239
|
+
attempt.recorded = true;
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
attempt.recorded = false;
|
|
243
|
+
attempt.detail = `${attempt.detail} [记事实失败: ${errText(e)}]`;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// 自检: 有未解决阻塞却什么都没落地、也没交人 ⇒ 静默 (维护者必须知道)。
|
|
248
|
+
// 「继续等」(`deferred`) 算**落地过** —— 它是规则表里合法的无副作用处置, 不该被误报成需要人。
|
|
249
|
+
if (unresolved.length > 0 && out.executed.length === 0 && out.deferred.length === 0 && out.needsHuman.length === 0) {
|
|
250
|
+
out.silentRisk = true;
|
|
251
|
+
out.needsHuman.push({
|
|
252
|
+
blockId: '(self-check)', workId: null, action: 'needs_human',
|
|
253
|
+
reason: `有 ${unresolved.length} 条未解决阻塞, 但一条处置都没落地、也没有交人理由 —— 静默, 交人`,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
/** 按动作把调用派到对应端口, 并把端口返回如实翻成 attempt */
|
|
259
|
+
async function dispatchAction(action, attempt, b, base, ports, authority) {
|
|
260
|
+
switch (action) {
|
|
261
|
+
case 'takeover': {
|
|
262
|
+
const lease = await ports.claimLease(base);
|
|
263
|
+
attempt.called.push('claimLease');
|
|
264
|
+
if (lease === true) {
|
|
265
|
+
attempt.outcome = 'executed';
|
|
266
|
+
attempt.detail = `真抢到执行权 (此前 leaseHeld=${authority.leaseHeld}) → 接管 ${base.workId ?? '-'}`;
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
attempt.outcome = 'refused';
|
|
270
|
+
attempt.detail = `抢执行权没成功 (claimLease=false) → 不接管, 交人`;
|
|
271
|
+
}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
case 'replace_child': {
|
|
275
|
+
const res = await ports.replaceChild(base);
|
|
276
|
+
attempt.called.push('replaceChild');
|
|
277
|
+
if (res && res.replaced) {
|
|
278
|
+
attempt.outcome = 'executed';
|
|
279
|
+
attempt.detail = `真换人: 停掉 ${base.workId ?? '-'}, 新工作=${res.newWorkId ?? '(未重派合同)'}`;
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
attempt.outcome = 'refused';
|
|
283
|
+
attempt.detail = `换人没做成 (replaceChild.replaced=false) → 交人`;
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
case 'send_adjustment': {
|
|
288
|
+
const ok = await ports.sendAdjustment({ ...base, directive: buildAdjustmentDirective(b) });
|
|
289
|
+
attempt.called.push('sendAdjustment');
|
|
290
|
+
attempt.outcome = ok === true ? 'executed' : 'refused';
|
|
291
|
+
attempt.detail = ok === true ? `真下发调整指令给 ${base.childAgentId ?? '(子 Agent)'}` : '调整指令下发失败 → 交人';
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
case 'request_report': {
|
|
295
|
+
const ok = await ports.requestReport({ ...base, what: buildReportRequestWhat(b) });
|
|
296
|
+
attempt.called.push('requestReport');
|
|
297
|
+
attempt.outcome = ok === true ? 'executed' : 'refused';
|
|
298
|
+
attempt.detail = ok === true ? `真要求补齐报告 (${base.workId ?? '-'})` : '催报下发失败 → 交人';
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
case 'change_plan': {
|
|
302
|
+
const ok = await ports.changePlan({ ...base, why: base.reason });
|
|
303
|
+
attempt.called.push('changePlan');
|
|
304
|
+
attempt.outcome = ok === true ? 'executed' : 'refused';
|
|
305
|
+
attempt.detail = ok === true ? '真改了计划 (父侧)' : '改计划没做成 → 交人';
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
case 'escalate_parent':
|
|
309
|
+
case 'needs_human': {
|
|
310
|
+
const to = action === 'needs_human' ? 'human' : 'parent';
|
|
311
|
+
const ok = await ports.escalate({ ...base, to, detail: base.reason });
|
|
312
|
+
attempt.called.push('escalate');
|
|
313
|
+
attempt.outcome = ok === true ? 'executed' : 'refused';
|
|
314
|
+
attempt.detail = ok === true
|
|
315
|
+
? `真上报给${to === 'human' ? '人' : '父'} (${base.kind})`
|
|
316
|
+
: `上报给${to === 'human' ? '人' : '父'}没做成 → 仍在 needsHuman 里留痕`;
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
case 'wait_dependency': {
|
|
320
|
+
attempt.outcome = 'deferred';
|
|
321
|
+
attempt.detail = `继续等 (无副作用): ${base.reason}`;
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
default: {
|
|
325
|
+
// 动作集是闭集; 走到这里说明 types.ts 加了新动作而这里没跟上 —— 不静默
|
|
326
|
+
attempt.outcome = 'refused';
|
|
327
|
+
attempt.detail = `未知动作「${String(action)}」: 执行器没跟上动作集, 交人`;
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/** 把结果压成一行摘要 (接线层 log / 界面用; 不含内部词) */
|
|
333
|
+
export function summarizeBlockExecution(out) {
|
|
334
|
+
return [
|
|
335
|
+
`已落地 ${out.executed.length}`,
|
|
336
|
+
`被拒 ${out.refused.length}`,
|
|
337
|
+
`等依赖 ${out.deferred.length}`,
|
|
338
|
+
`失败 ${out.failed.length}`,
|
|
339
|
+
`需人 ${out.needsHuman.length}`,
|
|
340
|
+
`去重后阻塞 ${out.attempts.length}`,
|
|
341
|
+
].join(' · ');
|
|
342
|
+
}
|
|
343
|
+
/** 动作集是否被本执行器完全覆盖 (types.ts 加了动作而这里没跟上 → false, 门来钉) */
|
|
344
|
+
export function coveredActions() {
|
|
345
|
+
return uniq(Object.keys(PORT_FOR_ACTION));
|
|
346
|
+
}
|