@bolloon/bolloon-agent 0.6.0 → 0.6.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/execution-entry.js +10 -0
- package/dist/agents/goal-flywheel/wiring/seams.js +5 -1
- package/dist/agents/pi-sdk-tools.js +18 -8
- package/dist/agents/pi-sdk.js +472 -1263
- package/dist/agents/runner-resolver.js +2 -1
- package/dist/agents/tool-capability/index.js +51 -0
- package/dist/agents/workflow-pivot-loop.js +31 -10
- package/dist/agents/write-verify.js +60 -0
- package/dist/bollharness-integration/integration.js +8 -0
- package/dist/bollharness-integration/skill-adapter.js +32 -3
- package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +5 -1
- package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-tools.js +21 -11
- package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +473 -1262
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/runner-resolver.js +2 -1
- package/dist/electron-build/agents/runner-resolver.js.map +1 -1
- package/dist/electron-build/agents/tool-capability/index.js +90 -0
- package/dist/electron-build/agents/tool-capability/index.js.map +1 -0
- package/dist/electron-build/agents/workflow-pivot-loop.js +31 -10
- package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
- package/dist/electron-build/agents/write-verify.js +99 -0
- package/dist/electron-build/agents/write-verify.js.map +1 -0
- package/dist/electron-build/bollharness-integration/integration.js +8 -0
- package/dist/electron-build/bollharness-integration/integration.js.map +1 -1
- package/dist/electron-build/bollharness-integration/skill-adapter.js +32 -3
- package/dist/electron-build/bollharness-integration/skill-adapter.js.map +1 -1
- package/dist/electron-build/kernel/channel-actor.js +25 -0
- package/dist/electron-build/kernel/channel-actor.js.map +1 -1
- package/dist/electron-build/kernel/control.js +102 -0
- package/dist/electron-build/kernel/control.js.map +1 -0
- package/dist/electron-build/kernel/model-runtime.js +508 -0
- package/dist/electron-build/kernel/model-runtime.js.map +1 -0
- package/dist/electron-build/kernel/run-lifecycle.js +146 -0
- package/dist/electron-build/kernel/run-lifecycle.js.map +1 -0
- package/dist/electron-build/kernel/transport.js +119 -0
- package/dist/electron-build/kernel/transport.js.map +1 -0
- package/dist/electron-build/llm/native-adapter.js +122 -0
- package/dist/electron-build/llm/native-adapter.js.map +1 -0
- package/dist/index.js +19 -0
- package/dist/ios/bolloon-web.json +3 -3
- package/dist/ios/client.js +13 -7
- package/dist/ios/mobile-core.js +7762 -6289
- package/dist/ios/mobile.js +64 -0
- package/dist/ios/routes-mobile-tasks.js +2 -1
- package/dist/ios/routes-tasks.js +16 -1
- package/dist/ios/routes-x402-info.js +28 -1
- package/dist/ios/server-storage.js +26 -5
- package/dist/ios/server.js +840 -862
- package/dist/ios/sw.js +1 -1
- package/dist/ios/ui/message-renderer.js +473 -29
- package/dist/ios/ui/step-timeline.js +13 -12
- package/dist/kernel/channel-actor.js +24 -0
- package/dist/kernel/gate-scan.js +318 -4
- package/dist/kernel/model-runtime.js +22 -0
- package/dist/kernel/plan-channel-actor.js +12 -5
- package/dist/kernel/plan-communication.js +163 -0
- package/dist/kernel/plan-constraint.js +2 -15
- package/dist/kernel/plan-deletion.js +1 -1
- package/dist/kernel/plan-harness.js +78 -0
- package/dist/kernel/plan.js +1 -5
- package/dist/kernel/roster.js +13 -11
- package/dist/kernel/run-lifecycle.js +138 -0
- package/dist/kernel/transport.js +111 -0
- package/dist/llm/native-adapter.js +117 -0
- package/dist/web/bolloon-web.json +3 -3
- package/dist/web/client.js +9 -5
- package/dist/web/routes-tasks.js +12 -1
- package/dist/web/server.js +802 -866
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
|
@@ -126,12 +126,6 @@ export const A_UNUSED_DEBT_FROZEN_AT = 5;
|
|
|
126
126
|
export const CONSTRAINT_USES = [
|
|
127
127
|
{ file: 'agents/constraint-layer.ts', target: '__pkg_entry__', count: 1, kind: 'prod', cls: 'A' },
|
|
128
128
|
{ file: 'agents/pi-sdk-session-manager.ts', target: '__pkg_entry__', count: 1, kind: 'prod', cls: 'A' },
|
|
129
|
-
{ file: 'agents/pi-sdk-tools.ts', target: 'tools/PolymarketSDK/cancelOrder', count: 2, kind: 'prod', cls: 'B' },
|
|
130
|
-
{ file: 'agents/pi-sdk-tools.ts', target: 'tools/PolymarketSDK/createOrder', count: 2, kind: 'prod', cls: 'B' },
|
|
131
|
-
{ file: 'agents/pi-sdk-tools.ts', target: 'tools/PolymarketSDK/getMarket', count: 2, kind: 'prod', cls: 'B' },
|
|
132
|
-
{ file: 'agents/pi-sdk-tools.ts', target: 'tools/PolymarketSDK/getOrders', count: 2, kind: 'prod', cls: 'B' },
|
|
133
|
-
{ file: 'agents/pi-sdk-tools.ts', target: 'tools/PolymarketSDK/listMarkets', count: 2, kind: 'prod', cls: 'B' },
|
|
134
|
-
{ file: 'agents/pi-sdk-tools.ts', target: 'tools/SafeSDK/deploySafe', count: 2, kind: 'prod', cls: 'B' },
|
|
135
129
|
{ file: 'agents/pi-sdk.ts', target: '__pkg_entry__', count: 2, kind: 'prod', cls: 'A' },
|
|
136
130
|
{ file: 'agents/skill-loader.ts', target: '__pkg_entry__', count: 1, kind: 'prod', cls: 'A' },
|
|
137
131
|
{ file: 'bollharness-integration/gate-state-machine.ts', target: '__pkg_entry__', count: 1, kind: 'prod', cls: 'A' },
|
|
@@ -148,15 +142,8 @@ export const CONSTRAINT_USES = [
|
|
|
148
142
|
{ file: 'test/wallet-polymarket-verify.test.ts', target: 'tools/WalletTools/importWallet', count: 1, kind: 'test', cls: 'B' },
|
|
149
143
|
{ file: 'test/wallet-polymarket-verify.test.ts', target: 'tools/WalletTools/signMessage', count: 1, kind: 'test', cls: 'B' },
|
|
150
144
|
];
|
|
151
|
-
export const B_DIRECT_IMPORT_DEBT = [
|
|
152
|
-
|
|
153
|
-
{ file: 'agents/pi-sdk-tools.ts', target: 'tools/PolymarketSDK/createOrder', count: 2, payDownIn: 'K7' },
|
|
154
|
-
{ file: 'agents/pi-sdk-tools.ts', target: 'tools/PolymarketSDK/getMarket', count: 2, payDownIn: 'K7' },
|
|
155
|
-
{ file: 'agents/pi-sdk-tools.ts', target: 'tools/PolymarketSDK/getOrders', count: 2, payDownIn: 'K7' },
|
|
156
|
-
{ file: 'agents/pi-sdk-tools.ts', target: 'tools/PolymarketSDK/listMarkets', count: 2, payDownIn: 'K7' },
|
|
157
|
-
{ file: 'agents/pi-sdk-tools.ts', target: 'tools/SafeSDK/deploySafe', count: 2, payDownIn: 'K7' },
|
|
158
|
-
];
|
|
159
|
-
export const B_DIRECT_IMPORT_FROZEN_AT = 12;
|
|
145
|
+
export const B_DIRECT_IMPORT_DEBT = [];
|
|
146
|
+
export const B_DIRECT_IMPORT_FROZEN_AT = 0; // 12 → 0 (2026-10-02 K1 ④: 领域 SDK 直连改经 Tool Capability 层, 欠账还清)
|
|
160
147
|
/**
|
|
161
148
|
* 引用台账的扫描面**排除名单** (冻结, 只许减):
|
|
162
149
|
* 本门自己的测试文件里有"人造引用"探针串 (判别力自证用), 不排掉它就会被当成一条真引用
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
export const DELETION_VERDICTS = [
|
|
14
14
|
{ group: 'CR 源: migrations/', target: 'src/constraint-runtime/src/migrations/', verdict: 'done', blockers: [], reason: "15 行 stub; 仓内(含 constraint-runtime 自身与 dist) 0 引用; **不在 `CR/src/index.ts` 的导出面** ⇒ 可直删" },
|
|
15
15
|
{ group: 'CR 源: remote/', target: 'src/constraint-runtime/src/remote/', verdict: 'done', blockers: [], reason: "15 行 stub; 0 引用; 不在导出面 ⇒ 可直删" },
|
|
16
|
-
{ group: 'CR/dist 构建产物 (89 文件/1164 行)', target: 'src/constraint-runtime/dist/', verdict: 'blocked', blockers: [{ file: 'src/agents/
|
|
16
|
+
{ group: 'CR/dist 构建产物 (89 文件/1164 行)', target: 'src/constraint-runtime/dist/', verdict: 'blocked', blockers: [{ file: 'src/agents/tool-capability/index.ts', why: "2026-10-02 K1 ④ 收敛后, 领域 SDK 的动态 import 从 pi-sdk-tools.ts 的 6 处**搬到这里** (唯一入口, dist 优先 + src 回落); 因此 dist 的删除仍被阻断, 但阻断点换了位置 —— 台账必须跟着换, 否则指向已不存在的耦合" }, { file: 'Dockerfile', why: "第167行把 src/constraint-runtime/dist COPY 进 node_modules/@bolloon/constraint-runtime/dist" }, { file: 'src/constraint-runtime/package.json', why: "main/exports 指向 dist/index.js; files=['dist/**/*']" }], reason: "**dist 才是运行期目标** (源码树不是) ⇒ 删 dist 直接断掉 B 类工具与包入口。必须先做完 K7 (B 类改 Tool Capability) 才有资格谈" },
|
|
17
17
|
{ group: 'CR 源: reference_data/ (子系统快照数据)', target: 'src/constraint-runtime/src/reference_data/', verdict: 'blocked', blockers: [{ file: 'src/constraint-runtime/src/tools.ts', why: "同目录 tools_snapshot.json 是运行期派发台账 (PORTED_TOOLS + executeToolFromSnapshot 按 source_hint import) ⇒ 整个目录不能整删; subsystems/*.json 现在是**孤立数据** (读取者 _archive_helper 已删), 处置要单独决定" }], reason: "26 个存档壳删掉后 subsystems/*.json 只剩数据; 同目录混着运行期台账 ⇒ 需按文件分别判, 不是目录级删除" }, { group: 'CR 源: parity_audit.ts / remote_runtime.ts / native_ts/ / upstream_proxy/', target: 'src/constraint-runtime/src/parity_audit.ts', verdict: 'blocked', blockers: [{ file: 'src/constraint-runtime/src/index.ts', why: "第21-22行 re-export (runParityAudit / runRemoteMode / runSshMode / runTeleportMode)" }, { file: 'src/constraint-runtime/dist/index.js', why: "编译副本同样 re-export ⇒ 删源码还要重建 dist" }], reason: "可达包入口 ⇒ 删除要**同时改 index.ts + 重建 dist**, 属 K1-④ 的活, 不是纯删" },
|
|
18
18
|
{ group: 'src/bollharness/ (另一个项目的镜像)', target: 'src/bollharness/', verdict: 'blocked', blockers: [{ file: 'scripts/smoke-esm.mjs', why: "第38行引用 dist/bollharness/src/scripts/context_router.js" }, { file: 'scripts/gen-copyright-source.ts', why: "第25行明写: src/bollharness 是**第三方 vendored 框架** (版权属 bollharness contributors), 不进版权登记" }], reason: "**第三方 vendored 框架** (不是 Bolloon 代码) + 版权归属未处置 + smoke 脚本引用 ⇒ 处置要先定它的归属, 属第一批「审查」而非机械化删除" },
|
|
19
19
|
{ group: 'K1 第②步批二: 26 个移植存档壳', target: 'src/constraint-runtime/src/<26 个移植存档壳>/index.ts', verdict: 'done', blockers: [], reason: "包入口闭包外 + 主仓 0 引用 + 快照 0 点名; 自述为 Python placeholder package; 已删 (记录见 DELETION_LEDGER)" },
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* plan-harness.ts — K7 台账: **Harness 作为唯一系统调用门**
|
|
3
|
+
*
|
|
4
|
+
* K7 的目标形态 (设计页 §7 第 164 行原文):
|
|
5
|
+
* `discover → permission → policy → budget → idempotency → execute → verify → evidence → event`
|
|
6
|
+
* 覆盖 普通工具 / MCP / Skill / delegate / 子 Agent / 联系人 / 支付 / 文件写入 / 外部通信
|
|
7
|
+
* 判据: **任何绕过 Harness 的代码都视为架构缺陷**; 工具旁路全删。
|
|
8
|
+
*
|
|
9
|
+
* 关键判断 (开工前已定, 不是新建): **Harness 作系统调用门是「提升」不是「新建」** ——
|
|
10
|
+
* `deny → pre-tool-validator → react-harness` 这条顺序**已经存在**且被源码级断言锁着
|
|
11
|
+
* (`src/test/pi-harness.test.ts`: "那唯一一处对 pre-tool-validator 的引用在门面内部 (注入), 不在调用点")。
|
|
12
|
+
* K7 要补的是: ① 把 9 个阶段与 9 个覆盖面**清单化** ② 把"谁在执行"数出来并登记 ③ 收敛旁路。
|
|
13
|
+
*
|
|
14
|
+
* 本文件是**台账 (数据)**, 不含运行时逻辑 —— 与 K3 分档一致 (台账档 ≠ 代码档)。
|
|
15
|
+
*/
|
|
16
|
+
export const HARNESS_STAGES = [
|
|
17
|
+
{ stage: 'discover', gate: 'src/kernel/roster.ts 的模块名册 + src/agents/tool-registry.ts 的工具名册', why: '模型能看到的工具集合必须是冻结名册的子集 (K0 ⑤ import 白名单 + 工具名册双源)' },
|
|
18
|
+
{ stage: 'permission', gate: 'src/agents/deny-pipeline.ts', why: 'deny 最先: 被禁的动作不进后续任何阶段 (顺序不可换)' },
|
|
19
|
+
{ stage: 'policy', gate: 'src/agents/pre-tool-validator.ts (经 src/agents/pi-harness.ts 门面注入)', why: '参数级校验 + 策略拒绝; 唯一引用在门面内部 ⇒ 调用点无法绕过' },
|
|
20
|
+
{ stage: 'budget', gate: 'src/agents/goal-flywheel/run-closure.ts (HardLimits) + Run 预算', why: '预算穷尽要能在**执行前**拒 (花掉才发现就晚了)' },
|
|
21
|
+
{ stage: 'idempotency', gate: 'src/agents/x402/* + src/agents/contacts/* 的幂等键 + 写操作读回自证', why: '支付/联系人/写文件这三类是"重复执行有真实副作用"的, 幂等键必须在门内' },
|
|
22
|
+
{ stage: 'execute', gate: 'src/agents/pi-sdk.ts 主执行点 (经门链) + src/agents/tool-registry.ts 统一执行口', why: '执行必须只有一条咽喉; 台账里逐点登记谁在执行 (含旁路)' },
|
|
23
|
+
{ stage: 'verify', gate: 'src/agents/pi-sdk-tools.ts 写类工具读回核对', why: '"工具说成功 ≠ 任务成功" 要从规矩变成机制' },
|
|
24
|
+
{ stage: 'evidence', gate: 'src/agents/x402/direct-payment.ts 台账 + Run 落盘', why: '有副作用的动作要留可核验证据' },
|
|
25
|
+
{ stage: 'event', gate: 'runCtx.eventSink (K2 外置面) → Web/CLI 双面', why: '事件面只许有一个出口 (K2 外置面的 eventSink)' },
|
|
26
|
+
];
|
|
27
|
+
export const HARNESS_SURFACES = [
|
|
28
|
+
{ surface: 'tool', canonical: 'src/agents/tool-registry.ts', why: '普通工具的统一执行口' },
|
|
29
|
+
{ surface: 'mcp', canonical: 'src/agents/pi-sdk-tools.ts', why: 'MCP 工具经这里执行 (台账里 1 处)' },
|
|
30
|
+
{ surface: 'skill', canonical: 'src/bollharness-integration/skill-adapter.ts', why: 'Skill 执行; **注意现在有两条 skill 路径** (见旁路候选)' },
|
|
31
|
+
{ surface: 'delegate', canonical: 'src/web/agent-delegate-server.ts', why: '委派执行面' },
|
|
32
|
+
{ surface: 'subagent', canonical: 'src/agents/runner-resolver.ts', why: '子 Agent 经 runner-resolver 拿 session (K5 已接线)' },
|
|
33
|
+
{ surface: 'contact', canonical: 'src/agents/contacts/consent.ts', why: '联系人/同意门 (consent 单次 vs grant 长期)' },
|
|
34
|
+
{ surface: 'payment', canonical: 'src/agents/x402/direct-payment.ts', why: '支付/收款: 幂等 + 链上核验' },
|
|
35
|
+
{ surface: 'file-write', canonical: 'src/agents/tool-path-args.ts', why: '路径参数统一处理 + 敏感子目录护栏' },
|
|
36
|
+
{ surface: 'external-comm', canonical: 'src/agents/pi-sdk-tools.ts', why: '外部通信 (HTTP/SSRF 面) 与 MCP 同文件' },
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* 实测 (2026-10-02, 剥注释后共 **17** 处 —— K4-B 删老 `runReActLoop` 前是 18) —— 数的口径写在 countHarnessExecSites 里, 判据会重算比对。
|
|
40
|
+
*
|
|
41
|
+
* **规则 (2026-10-02 立): 台账按「符号」不按「行号」。** 行号会随任何一次编辑漂移, 而判据只核**计数**
|
|
42
|
+
* 不核行号 ⇒ 行号一旦写进台账, 它迟早会**指向错的位置而没人发现** (K7 第二步 b 当场发生:
|
|
43
|
+
* 在 pi-sdk 插入 ~35 行后, 台账里的 4144 实际已变 4176)。说清"是哪一处"用函数名/类名/调用形态。
|
|
44
|
+
*/
|
|
45
|
+
export const HARNESS_EXEC_SITES = [
|
|
46
|
+
{ file: 'src/agents/pi-sdk.ts', count: 5, kinds: ['main', 'skill', 'bypass', 'homonym', 'registry'], why: '**主执行点 1 = pivot 的 `guardedExecute` 端口内那次 `await tool.execute(args)`** (2026-10-02 K4-B: 老 `runReActLoop` 的执行点随 loop 删除, 端口成为唯一主执行点; 它先过 `beforeToolCall` 再执行、执行后过 `afterToolCall`) · skill 2 (`getSkillRegistry()` 暴露的 `skillRegistry.execute` + 内置 `sk.execute`; **2026-10-02 量到: 这两条在生产代码里零调用者** —— 但 `PiAgentSession.executeSkill`/`getSkillRegistry` 是**已发布 npm 包的公开方法** ⇒ 属"不擅动公开契约", 删/收口都要等用户口径) · 内置 tscTool 直调 1 (bypass, 已在 `runTurnEndTypecheck` 内先问门) · `loop.execute` 1 (homonym)' },
|
|
47
|
+
{ file: 'src/agents/workflow-pivot-loop.ts', count: 2, kinds: ['bypass', 'homonym'], why: '**pivot loop 直接执行工具** 1 (bypass —— 2026-10-02 已收敛: 走注入的 `guardedExecute` 门端口) · `loop.execute` 1 (homonym)' },
|
|
48
|
+
{ file: 'src/agents/tool-registry.ts', count: 1, kinds: ['registry'], why: '注册表统一执行口 (唯一咽喉候选)' },
|
|
49
|
+
{ file: 'src/agents/pi-sdk-tools.ts', count: 1, kinds: ['mcp'], why: 'MCP `executeTool` 1' },
|
|
50
|
+
{ file: 'src/bollharness-integration/skill-adapter.ts', count: 1, kinds: ['skill'], why: 'skill 第二条路径 (`SkillAdapter.executeSkill` → `registry.execute`)' },
|
|
51
|
+
{ file: 'src/agents/browser-cdp.ts', count: 1, kinds: ['homonym'], why: 'CDP `session.execute` —— 浏览器命令, 不是工具执行' },
|
|
52
|
+
{ file: 'src/agents/chain/chain-wallet.ts', count: 1, kinds: ['homonym'], why: '`params.execute(signer)` —— 钱包动作, 不是工具执行' },
|
|
53
|
+
{ file: 'src/web/agent-delegate-server.ts', count: 1, kinds: ['port-callback'], why: '`options.execute({...})` —— 2026-10-02 定性: 它是**注入的执行器端口** (`execute?: (req: DelegateExecutionRequest) => Promise<DelegateExecutionResult>`, 见该文件顶部接口定义), 委派服务器**自己不执行工具** ⇒ 不是旁路; 但"注入的那个执行器有没有走门"属 delegate 覆盖面的事, 不在这条普查里' },
|
|
54
|
+
{ file: 'src/bollharness-integration/index.ts', count: 1, kinds: ['import'], why: 'import 列表里的名字' },
|
|
55
|
+
{ file: 'src/pi-ecosystem/index.ts', count: 1, kinds: ['import'], why: 'import 列表里的名字' },
|
|
56
|
+
{ file: 'src/pi-ecosystem-mcp/index.ts', count: 1, kinds: ['decl'], why: '`export async function executeTool(` —— 函数定义' },
|
|
57
|
+
{ file: 'src/kernel/plan-deletion.ts', count: 1, kinds: ['ledger-string'], why: '台账字符串里的字面' },
|
|
58
|
+
];
|
|
59
|
+
export const K7_BYPASS_CANDIDATES = [
|
|
60
|
+
{ target: 'src/agents/workflow-pivot-loop.ts', symbol: 'guardedExecute', replacesWith: '走 Harness 门链 (与 pi-sdk 主执行点同一条咽喉)', why: 'pivot loop 是第二条工具执行路径 ⇒ 未经 deny/policy 就能执行工具 (架构缺陷定义的那一类)', status: 'converged', evidence: '2026-10-02: `PivotLoopConfig.guardedExecute` 端口 (`044cde2`) + pi-sdk 在 `promptWithPivotLoop` 注入同一 `piHarness().beforeToolCall` (`69397f6`); 用例: 未注入行为不变 / 注入后 `tool.execute` 计数 0 / 端口抛错不执行 / 机械接线断言' },
|
|
61
|
+
{ target: 'src/agents/pi-sdk.ts', symbol: 'tscTool.execute', status: 'converged', replacesWith: '经门链执行, 或在台账里登记为"内部诊断白名单"并写明理由', why: '内置诊断工具直调: 要么走门, 要么**显式登记**为白名单 —— 不许处于"没人知道它绕过"的状态。**2026-10-02 选"走门"并已接线**: 回合末尾 TS 自检先问 `beforeToolCall({tool: tsc_check, args: {}, ctx: harnessCtx(), permissionMode})`, 被拒或抛错 ⇒ **不执行**且**可见**报出 (拒绝不静默); 机械断言: 判定在 `tscTool.execute` 之前 + `if (!tscAllowed)` 分支存在 + 拒绝文案存在。**端到端未单独取证** (该路径由"本回合改过 TS"的收尾自检触发, 需要真 LLM 回合) ⇒ **2026-10-02 端到端取证完成** (`src/test/k7-tsc-tool-e2e.test.ts`, 真 LLM 回合 + 真门实例): ① 允许路 —— 门被问过 `tsc_check` 且恰执行一次 (门被问到 `read_file, edit_file, tsc_check`), 结论 `🔎 类型检查` 进对话流; ② 拒绝路 —— 门拒绝 ⇒ `tsc_check` **零执行** + 拒绝文案 (带门给出的理由) 进对话流。**两条变异都真做且判红**: 变异①把问门时的工具名改掉 ⇒ 允许路判红 (`expected [...] to include 「tsc_check」`); 变异②把拒绝分支改成 fail-open (`if (false && !tscAllowed)`) ⇒ 拒绝路判红 (`被门拒绝 ⇒ 一次都不许执行: expected 1 to be +0`)。**如实记边界**: 拒绝路里"拒绝"这个判定是测试注入的 (沿用仓里端口注入法), 产品侧被证的是"**先问门、被拒后不执行**"这一半; "门自己判出来的拒绝"由 pre-tool-validator/react-harness 既有用例覆盖。**踩坑两条**: (a) 少了 `initMinimax()` 装配 ⇒ session 走未初始化模型兜底路 (回显、不调工具), 门一次都不会被问 ⇒ 假红; (b) 少了 `resetAgentSession()` ⇒ `createAgentSession` 复用上一条用例的 session (带着"我改过这个文件了"的记忆) ⇒ 第二条用例的模型回"需求已完整满足"不调工具 ⇒ 前提假红。两条都写进用例头。',
|
|
62
|
+
evidence: '2026-10-02 端到端: `src/test/k7-tsc-tool-e2e.test.ts` (真 LLM 回合 + 真门实例包裹) —— 允许路: 门被问过 `tsc_check` 且恰执行一次, `🔎 类型检查` 进对话流; 拒绝路 (判定注入): `tsc_check` 零执行 + 拒绝文案带理由进对话流。变异①(问门工具名改掉)⇒允许路判红; 变异②(拒绝分支改 fail-open)⇒拒绝路判红。**2026-10-02 K4-B 侦查补正**: 上面两条用的是**默认配置** (老 `runReActLoop`), 而**生产里 web 走 pivot 路径** (`usePivotLoop: true` + 流式入口原先"提前 return") ⇒ 那条自检在生产**一次都没跑过** (早先证据只覆盖非生产形状)。已修: 抽成 `runTurnEndTypecheck()`, **三条路径全挂** (pivot prompt / 老 loop / 流式 pivot); 用例补第三条"生产形状" (注入 touched 状态 ⇒ 判**接线**), **变异 (撤掉流式路径那行) ⇒ 判红** (`expected [] to include tsc_check`)。' },
|
|
63
|
+
{ target: 'src/agents/pi-sdk.ts', symbol: 'getSkillRegistry', replacesWith: '返回**受门包装** (`GuardedSkillRegistry`): `execute` / `get().execute` / `list()[].execute` 三条都经 `executeSkill` ⇒ Harness', status: 'converged', evidence: '2026-10-02 按 leo 口径 **(b)** 落地 (原选 a 为纸面约定, leo 裁定走 b): ① 包装堵住**三条**隐藏路径 —— 只堵 `execute` 不够, `get()`/`list()` 返回的 `Skill` 自带裸 `execute`; ② 真跑 `k7-session-skill-gate.test.ts` ⑥: allow 时三条各恰一次 (计数 3), `denyTool` 后**三条全零执行** (计数仍 3); ③ 真变异两条 (`list` 裸透传 / `get` 裸透传) 均判红 (`expected FACADE_EXECUTED to match /^拒绝: /`), 还原回绿; ④ 机械断言: 方法体内无 `return this.skillRegistry;`、`list` 过 `guarded` 映射、**零** `this.skillRegistry.execute(` 直连。⚠️ 行为变更 (可能属破坏性, leo 已知并裁定): 旧 `.execute()` 被拒时**不再执行**, 改为返回 `拒绝: [...]`; 方法名/签名不变 ⇒ 源码级兼容。**管不到的一条 (如实记)**: 调用方自己 `register` 时传进来的那个对象仍有裸 `execute` —— 那是调用方的对象, 不属本出口能管的面', why: '**未过门的原始出口**: 拿到裸 registry 就有三条绕过 Harness 的路径 (`execute` / `get().execute` / `list()[].execute`) —— 公开兼容面不许无声删, 但也**不许不登记** (leo 的最终要求: 任何 skill 执行入口只能经 Harness)' },
|
|
64
|
+
{ target: 'src/bollharness-integration/skill-adapter.ts', symbol: 'registry.execute', replacesWith: '收敛成唯一 skill 执行入口 (且该入口经门)', status: 'converged', evidence: '2026-10-02 两条路都真跑取证+变异验证: ① adapter 路 `k7-skill-denylist-e2e.test.ts` (真 session→真 Harness→integration→adapter) ② 公开出口路 `k7-session-skill-gate.test.ts` (`executeSkill` 探针 skill 计数: 允许恰 1 次执行返回标记 · deny **零执行** · 重试仍拒 · 放开又能执行); 两处变异(拆保护/门永远放行)均判红 ⇒ 用例承重', why: 'skill 有两条执行路径 ⇒ 必然一条有门一条没有 ("同一个能力只许有一套实现" 的同型教训). **2026-10-02 实测**: 活路径 = `SkillAdapter.executeSkill` (`bollharness-integration/integration.ts` ← `index.ts` 的 harness 入口); `PiAgentSession.executeSkill` **生产零调用者** (但属公开契约 ⇒ 删除待用户口径). 活路径已接线: 端口 (`setGuardedExecute`, **判定**契约) + integration 透传 (`setSkillGuard`) + `index.ts` 活路径注入 (门建不起来 ⇒ deny-all); **真跑已通 (allow 路)**: `node dist/index.js --harness-skill arch get_gate` 真执行并返回真实 skill 输出 —— **注意读的地方**: 响应经"启动期日志闸门"写进 `~/.bolloon/logs/startup.log` (`[boot] 🎯 Skill <name> 执行结果:`), **stdout 里看不到** (我上一轮据此误判"没走到分发器", 是假阴性); 且无 `[K7] skill 门未能建立` 警告 ⇒ 门确实建起来了。**deny 路已取得 ★真跑**: `k7-skill-denylist-e2e.test.ts` 走**真链** (真 session → 真 Harness 的 deny-list checker → integration → adapter → registry): allow 基线不被拒 · 把 `skill:arch` 放进拒绝列表 ⇒ 结果 `拒绝: [deny-list]` 且拿不到 skill 真实输出 · 重试仍被拒 · 放开名单 ⇒ 又能执行; **变异** (拆掉 adapter 的门分支) ⇒ 该用例判红, 还原 ⇒ 回绿且无 diff ⇒ **用例承重**。**状态仍 open**: 本条目含两条路径, 公开兼容面 (`PiAgentSession.executeSkill`) 未收敛 (leo 第 4 步: 降为兼容转发 ⇒ 唯一 SkillExecutionPort)' },
|
|
65
|
+
];
|
|
66
|
+
/** K7 验收 (与设计页 §7 的判据对应) */
|
|
67
|
+
export const K7_ACCEPTANCE = [
|
|
68
|
+
'9 个阶段**顺序**即契约, 且每阶段的 gate 是真存在的文件/判据 (不是待建)',
|
|
69
|
+
'9 个覆盖面每个都能说出"现在谁管它", canonical 文件真存在',
|
|
70
|
+
'执行点普查与盘上**逐字一致** (增 = 新旁路未登记 · 减 = 改了盘没改账)',
|
|
71
|
+
'旁路候选逐条有替代路径; 收敛后**旁路数只许减**',
|
|
72
|
+
'收敛动作必须走五条件 (唯一替代路径 · 无有效引用 · 真跑覆盖 · 完整回归 + 故障恢复 · 可回滚提交点)',
|
|
73
|
+
];
|
|
74
|
+
export const K7_PROGRESS = {
|
|
75
|
+
stage: 'ledger-landed',
|
|
76
|
+
execSitesTotal: 17, // 16 (普查基线) + 1 (K7 第二步 b: pivot loop 端口内执行, 在门之后) —— 2026-10-02 K4-B 删老 loop, 它那处执行点随之消失: 18 → 17
|
|
77
|
+
bypasses: 0, // 3 → 2 (pivot loop) → 1 (getSkillRegistry 受门包装, leo 口径 (b)) → **0** (tscTool 端到端取证完成: 允许路/拒绝路真回合 + 两条变异, 2026-10-02)
|
|
78
|
+
};
|
package/dist/kernel/plan.js
CHANGED
|
@@ -78,20 +78,16 @@ export const ENTRY_GRAPH = [
|
|
|
78
78
|
{ file: 'agents/pi-sdk.ts', kind: 'adapter-internal', method: 'prompt', count: 2, path: 'direct', targetPhase: 'K4', note: "适配器内部 pivot 分派 + runExecution 的非流式派发 (都不是外部入口)" },
|
|
79
79
|
{ file: 'agents/pi-sdk.ts', kind: 'adapter-internal', method: 'promptStream', count: 1, path: 'direct', targetPhase: 'K4', note: "runExecution 的流式派发 (K5 步骤⑦ 新增)" },
|
|
80
80
|
{ file: 'agents/pi-sdk.ts', kind: 'adapter-internal', method: 'promptWithPivotLoop', count: 2, path: 'direct', targetPhase: 'K4', note: "适配器内部 pivot 分派 (不是外部入口)" },
|
|
81
|
-
{ file: 'agents/runner-resolver.ts', kind: 'agent-entry', method: 'prompt', count: 1, path: 'direct', targetPhase: 'K3', note: "独立宿主: 目前直调" },
|
|
82
81
|
{ file: 'cli/interface.ts', kind: 'readline-tui', method: 'prompt', count: 3, path: 'queued', targetPhase: 'K3', note: "readline 输入提示, 非 agent 入口" },
|
|
83
82
|
{ file: 'index.ts', kind: 'agent-entry', method: 'prompt', count: 7, path: 'direct', targetPhase: 'K3', note: "CLI 装配点" },
|
|
84
83
|
{ file: 'index.ts', kind: 'agent-entry', method: 'promptStream', count: 1, path: 'direct', targetPhase: 'K3', note: "CLI 装配点" },
|
|
85
|
-
{ file: 'web/routes-tasks.ts', kind: 'agent-entry', method: 'prompt', count: 1, path: 'direct', targetPhase: 'K3', note: "任务路由: 目前直调" },
|
|
86
|
-
{ file: 'web/server.ts', kind: 'agent-entry', method: 'prompt', count: 3, path: 'direct', targetPhase: 'K3', note: "Web 主入口 (非流式 3 处仍直调)" },
|
|
87
|
-
{ file: 'web/server.ts', kind: 'agent-entry', method: 'promptStream', count: 7, path: 'direct', targetPhase: 'K3', note: "Web 主入口: 1 处已改请求式 (K5 步骤⑦), 余 10 处仍直调" },
|
|
88
84
|
];
|
|
89
85
|
/**
|
|
90
86
|
* 直调 `prompt*()` 的调用点总数 (K3 只许把它压到 0; 棘轮只许减)。
|
|
91
87
|
* ⚠️ 24 → **25** (2026-10-02, K5 步骤⑦): `runExecution` 内部新增 `prompt`/`promptStream` 派发各 1 处 (+2),
|
|
92
88
|
* web/server.ts 用户消息路径改成请求式 (-1)。**这是"形态变化"不是"旁路复活"** —— 见 ENTRY_GRAPH 的两行 adapter-internal。
|
|
93
89
|
*/
|
|
94
|
-
export const ENTRY_DIRECT_CALLS_FROZEN_AT = 25;
|
|
90
|
+
export const ENTRY_DIRECT_CALLS_FROZEN_AT = 13; // 2026-10-02: 20 → 16 → 13 (K8 第四/五步: server.ts 7 处全迁; 只剩 CLI 侧 index.ts 的 8 处) // 2026-10-02: 25 → 20 (K8: 5 处改走 runExecution; 棘轮只许降)
|
|
95
91
|
export const DELETION_LEDGER = [
|
|
96
92
|
{
|
|
97
93
|
target: 'src/constraint-runtime/src/migrations/',
|
package/dist/kernel/roster.js
CHANGED
|
@@ -100,7 +100,7 @@ export const KERNEL_ALLOWED_IMPORT_PREFIXES = ['kernel/'];
|
|
|
100
100
|
* 判据是**双向相等**: 盘上多一个未登记文件 ⇒ 红; 名册有而盘上没有 ⇒ 红。
|
|
101
101
|
* (先例: SEAM_ROSTER 的「名册外无人越界」)
|
|
102
102
|
*/
|
|
103
|
-
export const KERNEL_FILES = ['kernel/channel-actor.ts', 'kernel/control.ts', 'kernel/gate-scan.ts', 'kernel/model-runtime.ts', 'kernel/plan-modelruntime.ts', 'kernel/plan-constraint.ts', 'kernel/plan-channel-actor.ts', 'kernel/plan-deletion.ts', 'kernel/plan-runcontext.ts', 'kernel/plan.ts', 'kernel/roster.ts'];
|
|
103
|
+
export const KERNEL_FILES = ['kernel/channel-actor.ts', 'kernel/control.ts', 'kernel/run-lifecycle.ts', 'kernel/transport.ts', 'kernel/gate-scan.ts', 'kernel/model-runtime.ts', 'kernel/plan-harness.ts', 'kernel/plan-modelruntime.ts', 'kernel/plan-constraint.ts', 'kernel/plan-channel-actor.ts', 'kernel/plan-deletion.ts', 'kernel/plan-runcontext.ts', 'kernel/plan-communication.ts', 'kernel/plan.ts', 'kernel/roster.ts'];
|
|
104
104
|
/** Goal 事实的写入口 (goal-store.ts 的导出中带副作用的那些) */
|
|
105
105
|
export const GOAL_WRITES = [
|
|
106
106
|
'createGoal',
|
|
@@ -202,12 +202,14 @@ export const STAGE_STATUS = {
|
|
|
202
202
|
K2: 'done',
|
|
203
203
|
K3: 'done',
|
|
204
204
|
K5: 'done',
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
205
|
+
// 2026-10-02 (leo 拍板): K4 = **Kernel Execution Core**, 拆两半, 不许两个含义共用一个编号
|
|
206
|
+
'K4-A': 'done', // 内核控制面 / Goal·Run 写权限收口 (AUTHORITY_DEBT 3 → 0, 经 kernel/control.ts)
|
|
207
|
+
'K4-B': 'done', // 2026-10-02 合并完成: 老 `runReActLoop` (1123 行) **已删除**, 所有入口只跑 pivot (`usePivotLoop` 默认 true); 同时补回只存在于老 loop 的门面阶段 (sessionStart/sessionEnd/beforeModelCall/afterModelCall/afterToolCall) + TS 记账上移到唯一执行点 + 自检三路径。**如实两条边界**: ① `reviewFinal` 已恢复调用与留痕, 但它的判定**不参与流程** (pivot 收尾仍由自己的质量判定决定) ② 老 loop 的 `IterationBudget` (净用量+批处理退还) 随它退休, pivot 用复杂度画像预算 (行为差量) // 合并两套 Agent Loop —— 已落: 默认切 pivot (所有入口一套) + 收尾自检抽成 runTurnEndTypecheck 挂**三条路径**(原先生产 pivot 路径从没跑过自检, 见 K7 evidence 补正); 待做: 删老 `runReActLoop` (~1100 行, 需先核清两条入口 finally/尾段差异)
|
|
208
|
+
K6: 'done', // 2026-10-02 修正: 能力 9/9 全 done (capabilities-done) —— 原写 not-started 是台账滞后
|
|
209
|
+
K7: 'done', // 2026-10-02 收尾: 旁路 **3 → 0** (pivot ✅ · skill ✅ 两路端到端 · tscTool ✅ 端到端取证含两条变异 · getSkillRegistry ✅ 受门包装) — 提交 5483e3a / 1af7e46
|
|
210
|
+
K8: 'partial', // 台账+门已落 · 直连 12 → **0** · 各通道状态 37/33 · didFixQueue 经邮箱 · **正刀已落地** (每条消息都进邮箱, 删掉重复的第二条路径, 提交 2fc197e, 真跑: 一条不丢/严格串行/FIFO) ⇒ 剩 `abortController` 语义定夺 (目标是 ExecutionRequest.signal), 显式留下不随队列顺手合并
|
|
211
|
+
K9: 'done', // 2026-10-02: **转 done (口径已重述并逐条取证)** —— 设计页那条"与 Pi 完全相同的五套验收"里, 4 套全过; 第 5 套("Durable Run/checkpoint 经适配器")经核实**按字面不可造**(startRun/recordStep 的真调用者只有 真钱 M1 任务 / x402 付费 / 回退路径, 与"适配器只在 LLM 可用时生效"互斥) ⇒ 重述为两半并各自取证: ① durable 写入经内核端口 = K10 ① (门 8/8 + 4/4, 两处变异判红); ② 适配器驱动真跑 = CLI + Web 真跑(会话落库)。 要合成一条"经适配器 + durable"的验收需先补**不含真钱**的 durable-run 路径 ⇒ 已登记为产品/验收设计待办, **不阻塞 K9 收口** (不假装跑过)。原始逐条记录见本条尾部。 // 2026-10-02: **不能算 done** —— 设计页的 K9 要求"最小 Native Adapter 必须通过与 Pi **完全相同**的五套验收"; 已过: model call (真端点 200+回复) · tool call 经 Harness 门恰一次/拒绝零执行 · CLI 非流式真跑 · 名字净化/回程还原 · 超时/退避/abort/4xx 不重试。**未过 → 2026-10-02 口径重述 (有证据)**: "Durable Run / checkpoint 经适配器"这条**按字面不可造** —— 核实三处: ① `startRun` 的真调用者只有 **`agents/task/task-runner.ts:358`(M1 任务, **走真钱/预算**) · `x402/goal-run-bridge.ts`(付费信息场景) · pi-sdk 回退路径(本机不可达)**; ② `recordStep`(每步写 checkpoint) 的调用者也全在这三条路径上; ③ checkpoint 字段实际由 `recordStep`(每步)/`prepareResume`(resume) 写, `saveCheckpoint` 本身**无人调**。 ⇒ 可选路径里**没有一条既不含真钱又能经适配器**(适配器只在 LLM 可用时生效, 而"回退路径"按定义要求 LLM 不可用 ⇒ 互斥)。**诚实处置**: 该条拆成两半分别验 —— ① "durable 写入经内核端口" = K10 ① (门 8/8 + 4/4 + 两处变异); ② "适配器驱动真跑" = CLI(`三面通过`)+Web(会话落库) 真跑。要合成一条"经适配器 + durable"的验收, 得先补一条**不含真钱**的 durable-run 路径 ⇒ 属产品/验收设计, 需拍板。 (适配器层的并发/取消/流式回调已由 ⑧⑨⑩ 覆盖: 3 条并发各自拿到自己的回复 · abort 中途打断且适配器之后仍可用 · 真 loop 的 onStream 收到 token/reply-preview)。以下是已落地部分: **第二个 (非 Pi) 推理适配器** `src/llm/native-adapter.ts` (fetch 直连 OpenAI 兼容端点, 源码不 import 任何 Pi 模块) + 唯一选择点 `BOLLOON_NATIVE_ADAPTER=1`; 门 `k9-native-adapter.test.ts` **8/8** (机械: 不许是 Pi 的壳 · 行为: 回复/tokens · native tool_calls 原样 · 429 退避重试 · abort 透传 · 4xx 不重试 · **出程名字净化+回程还原** · **过同一套门**: 适配器的 tool_call 经受门端口恰执行一次, 拒绝⇒零执行); **真跑**: 非流式 `prompt` 走该适配器打真端点 ⇒ 回 `适配器切换成功` (HTTP 200)。**顺带修掉一处隐式 Pi 依赖**: pivot 入口的 `!minimaxAvailable ⇒ fallback` 改成问**当前适配器**。**如实范围**: 适配器只实现接口所需的 chat 契约 + 超时/退避/abort/名字净化; Pi 侧的 KV 前缀/轻量分流等属 Pi 自身特性, 不在接口面内
|
|
212
|
+
K10: 'done', // 2026-10-02: **八步全落地** —— ① 4/4 (门 8/8 + 4/4, 两处变异) · ② 轻版落地 (内核运行时进生产路径, 门 6/6 + 变异 + 真跑) · ③ 实质完成 · ④ 全接线 (门 26/26 + 变异) · ⑤⑥ 早完成 (K7/K4-B) · ⑦ 已查清处置 (接上死机制, 其余登记保留) · ⑧ 按判据给出结论 (不宣称替换成功 · 不物理删)。**余项(不阻塞)**: 6 处辅助调用仍拿现成 handle; `checkMinimax` 仍 Pi 专有; 12.3% 运行/通道接线待薄化。 // 2026-10-02 起手 (先量后删): pi-sdk.ts **3353 行**; 8 步现状 —— **⑤ 旧工具 gate ✅ (K7 已做)** · **⑥ 旧 loop ✅ (K4-B 删 runReActLoop 1123 行)** · **① Pi 的 Goal/Run 写入 = 已接线 4/4** (端口 `kernel/run-lifecycle.ts` 落地 + 回退路径三处 `start-run/record-step/finish-run` 改经端口 + `assertLifecycleOk` 响失; 门 `k10-run-lifecycle-port.test.ts` 8/8 + 变异判红; **真跑待补**: 该分支需 `getMinimax()` 真抛, 本机模型总能构造 ⇒ 不可达; 第 4 处 = `safeSetRunStatus` 的状态迁移**已改经控制面** (`submitRunControl(kind:"set-run-status")` + `runControlPorts()` 注入; 口径保真: 被拒⇒false 且不记降级 · 未注入/抛错⇒false 并记降级), 门 `k10-run-control-wiring.test.ts` **4/4**(真调私有方法体三态 + 反回归) + **变异判红**(绕过控制面 ⇒ 4 条全红)): 写点 4 处 (1244 `startRun` / 1254 `recordStep` / 1255 `finishRun` = 回退路径落 run 事实; 1999 在 `setGoalId` 里 = Pi 的 Goal 写入), 内核**已有控制面** `kernel/control.ts` (`RunControlRequest{kind,origin,runId,goalId,payload}` → `RunControlPorts{recordRecovery,setRunStatus,setContinuation}`), 但它是**命令式控制面** (供 web/cli/supervisor 发 pause/resume/abort/recover), **不是 run 生命周期写口** ⇒ ① 的正确下一刀 = **照 control.ts 的形状新增 run-lifecycle 端口** (`startRun/recordStep/finishRun/saveCheckpoint`, 同样端口注入 + 未注入即拒), 再把 4 处改经它 (禁直接调 run-store); 不许把生命周期写入硬塞进控制面的 `kind` 里 (语义不同: 一个是"谁命令我停", 一个是"我这一轮的事实") · **② 模型配置解析 = 轻版落地** (回合的模型**先向内核运行时取租约** —— `ModelRuntime.acquire(snapshot)` 只读入手, 超时/取消/退避/熔断/回退/记账/连接池/能力检查由 K6 运行时负责; **传输仍是 Pi 客户端** (端口 `openPiConnection` 注入) ⇒ 行为不变; 取不到就**如实记**"内核租约不可用 ⇒ 回落 Pi 直连"(不假装); 新增只读投影 `snapshotFromSelection`(缺 provider/model ⇒ null, 不许编); 门 `k10-model-runtime-wiring.test.ts` **5/5** + 变异判红; **真跑**: `推理适配器 = kernel-model-runtime (只读 acquire 租约)` + 回复 `内核租约` ✓ ⇒ K6 那个"造了没人用"的运行时**进生产路径了**。**② 余下 (预留升级)**: 6 处辅助调用的 `getMinimax()` 仍拿现成 handle; `checkMinimax` 仍是 Pi 专有探针 —— 两条都属"下一刀", 已点名) (10 处: 6 处 `getMinimax()` 供辅助功能自取自用 [judgment hint 2625 · suggestRename 2915/2946 · summarizeDocument 2970 · improveDocument 3003 · 自动压缩注入 1392] + `checkMinimax` 1127 + 适配器回落 1735 + 注释 2; 内核已有 `class ModelRuntime`(517 行, `acquire(snapshot): ModelLease` 只读) 与能力 9/9 ⇒ **缺的不是运行时, 是取舍**: ① snapshot 由谁传给 Pi (Run 级 vs 会话级) ② 辅助调用拿 lease 还是拿现 handle ③ `checkMinimax` 这种 Pi 专有探针是否保留 —— 三条都影响公开行为, 须拍板后再动, 不许硬编) · **③ Channel 状态 = 已量清, 实质完成** (Pi **无自有**通道状态字段: `this.channelId/currentSessionId/sessionKey/channelName` 赋值与读点均 **0**; 剩余 8 处 `actor.state.channelId` 读的正是 K5 actor —— 归属已在内核) · **⑦ 已查清处置**: 查 6 个导出时挖出 **`verifyWriteOutcome` 是"写后读回"纪律的唯一实现却 0 调用点, 且函数体用 `require('node:fs')`(ESM 产物里必炸、被自己 catch 吞成 `[未核对] … require is not defined` ⇒ 它一次都没成功过)** ⇒ 按"无替代路径不许删"**改为接上**: 拆成 `src/agents/write-verify.ts`(静态 import) + `withWriteVerified` 接进 `write_file`/`edit_file` 成功返回; 门 `k10-write-verify-wiring.test.ts` **10/10**(含两条端到端真跑) + **变异判红**(拆接线 ⇒ 2 条红); 其余 4 导出各有测试与用途 ⇒ **登记保留**, 不删 · · **④ 通信入口 = 首刀落地** (新增内核端口 `kernel/transport.ts`: `send/broadcast/peers` 三动作, 端口注入 + 未注入即拒 + `{ok:false}` 归一化 + 审计, 与 control/run-lifecycle 同一套写法; pi-sdk 的 `sendMessage`/`broadcast` 改经端口 + **失败一律抛**(发送有副作用: 静默丢消息比抛错更糟); 门 `k10-transport-port.test.ts` **8/8**(含真调方法体两条) + **变异判红**(绕过端口 ⇒ 3 条红, 直连真传输那次的失效形态正是 `Node not initialized`); **④ 后半已落地 = ④ 全接线**: 加**同步**读入口 `transportPeersSync(ports)` + `peersSync` 端口 (不硬塞进 async 的 `submitTransport` —— 分开是因为**签名**不是语义: 上层有同步调用点, sync 签名不能凭空变 async); `getPeers()`/`listPeers()` 改经端口: `listPeers` 在端口未注入时**明说"读不到"**(不假装"没有对端"), `getPeers()` 未注入时回落直读并 `console.warn` 留痕; 门扩到 **11/11**(同步入口两种注入态 + 真调方法体三态) + **变异判红**(同步读绕过端口 ⇒ `listPeers` 谎报"无对端", 门抓到)) · **⑦ 兼容层 6 个导出面** · **⑧ 是否删 Pi Adapter 本身 = 已按判据给出结论 (不物理删)**。量占比 (**行数口径**, 方法: 按方法块归类, 脚本在 `docs/wiki/log.md`): `pi-sdk.ts` **3497 行** ⇒ 传输/供应商 **14.4%** · turn-glue **14.0%** · 运行/通道/通信接线 **12.3%** · 文档辅助 **9.2%** · 其余 50.1% 通用编排/装配。**职责口径**: K0–K10 已把「循环 (K4-B) · 工具门 (K7) · Run 事实 (①) · 通道状态 (③) · 通信 (④) · 模型选择 (②)」**全部搬出 Pi** ⇒ 岗位职责只剩"传输适配 + 供应商/Harness 特性 + 文档辅助 + 端口接线"。**结论**: 按判据「迁移后 Pi 职责仍超原职责 30% ⇒ 不许宣称替换成功」—— 行数口径下 Pi 仍是本仓最大单体 ⇒ **不宣称替换成功** (诚实口径); **不物理删** (五个删除条件不满足: 传输在 Pi 有专有实现、无替代路径; 且属公开契约)。**下一刀候选** (已点名): 继续薄化那 12.3% 的运行/通道**接线**与 turn-glue 里的生命周期部分。
|
|
211
213
|
};
|
|
212
214
|
/**
|
|
213
215
|
* **越权欠账台账 (棘轮)** —— 判据是「实际违规多重集 == 本表逐字相等」, 不是「⊆」:
|
|
@@ -228,15 +230,15 @@ export const AUTHORITY_DEBT_FROZEN_AT = 0; // 3 → 0 (K4 用内核控制面还
|
|
|
228
230
|
* 目的只有一个: **不许所有逻辑回流到 kernel.ts**。要加就得显式抬这个数字, 留下痕迹。
|
|
229
231
|
* 数值 = 当前 kernel 目录真实行数, 不留余量。
|
|
230
232
|
*/
|
|
231
|
-
export const KERNEL_LINE_BUDGET =
|
|
233
|
+
export const KERNEL_LINE_BUDGET = 3305; // 2026-10-02: K10 ② 内核新增 `snapshotFromSelection` 只读投影 (+20, 显式抬档); // 2026-10-02 **显式抬档 +22** (K8 收尾: channel-actor 加 `abortActorsOfChannel` + 台账/门同步) // 2026-10-02 **显式抬档 +16** (K10 ④ 后半: transport.ts 加同步读入口 `transportPeersSync` + `peersSync` 端口) // 2026-10-02 **显式抬档 +145** (K10 ④: 新增内核模块 kernel/transport.ts —— 通信入口端口) // 2026-10-02 **显式抬档 +184** (K10 ①: 新增内核模块 kernel/run-lifecycle.ts —— 运行生命周期写口, 与 control.ts 的控制面分开; 预算 == 真实值) // 2026-10-02: +1 (K4-B: gate-scan 复位阈值注释) // 2026-10-02: +5 (K8 正刀: gate-scan 加"收口字段数 3→1"+ queue 反回归) // 2026-10-02: +54 (K8: gate-scan 加 K8 判据) // 2026-10-02: +2 (STAGE_STATUS 拆 K4-A/K4-B + K6/K7 如实修正)
|
|
232
234
|
/** 预算冻结值 (棘轮: 只许减; 想抬预算必须同时改上面那个数字 ⇒ 一次显式动作, diff 里看得见) */
|
|
233
|
-
export const KERNEL_LINE_BUDGET_FROZEN_AT =
|
|
235
|
+
export const KERNEL_LINE_BUDGET_FROZEN_AT = 3305; // 同步至 2026-10-02 真实值 (K10 ④ 新增 transport.ts 后)
|
|
234
236
|
/**
|
|
235
237
|
* K3b —— **台账数据**单独一档预算 (`src/kernel/plan.ts`)。
|
|
236
238
|
*
|
|
237
239
|
* 为什么分开: K3 要防的是「逻辑回流到内核代码」; 台账是**数据** (owner 名册 / 入口图 / 删除候选),
|
|
238
240
|
* 把它算进代码预算会逼着人抬代码上限, 棘轮的信号就废了。两档各自冻结, 都只许减。
|
|
239
241
|
*/
|
|
240
|
-
export const KERNEL_PLAN_LINE_BUDGET =
|
|
242
|
+
export const KERNEL_PLAN_LINE_BUDGET = 1442; // 2026-10-02 **显式抬档 +3** (K8 收尾: plan-communication 把 abortController 条目改成删除说明) // 2026-10-02: **收紧** -2 (queue 字段条目删除: 8 字段 → 7) // 2026-10-02: +14 (K8 前置自纠: 把错的"check-then-set 非原子"换成已核实的"handoff 丢消息") // 2026-10-02: K7 tscTool 端到端取证 (evidence 字段 + 变异/踩坑记录) ⇒ 显式抬档 +1 // 2026-10-02: K8 通道状态台账+门落地 ⇒ 显式抬档 (棘轮只许减, 抬档要一次显式动作) // 真实值 (删 ENTRY_GRAPH 一行后); 变异用例要求预算 == 真实值 // 2026-10-02: +91 (K8 台账 plan-communication.ts 落地) // 2026-10-02: +12 (K7 第二步 b: 旁路 status/symbol/evidence 字段 + 卫生规则注释 + 防漂用例)
|
|
241
243
|
/** 台账预算冻结值 (棘轮: 只许减) */
|
|
242
|
-
export const KERNEL_PLAN_LINE_BUDGET_FROZEN_AT =
|
|
244
|
+
export const KERNEL_PLAN_LINE_BUDGET_FROZEN_AT = 1442; // 同步至 2026-10-02 真实值
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-lifecycle.ts — K10 ①: **运行生命周期写口** (与 `control.ts` 分开: 语义不同)
|
|
3
|
+
*
|
|
4
|
+
* `control.ts` = **命令式控制面**: 「谁命令我停 / 继续 / 恢复」—— web · cli · supervisor 发起, 带 origin 审计。
|
|
5
|
+
* 本文件 = **回合事实写口**: 「我这一轮发生了什么」—— start-run · record-step · finish-run · save-checkpoint。
|
|
6
|
+
*
|
|
7
|
+
* 为什么必须分开 (K10 ① 的下一刀定形时复核过):
|
|
8
|
+
* 混进同一个 `kind` 空间之后, K10 ②③④ 那些批次就无法区分「这条写入是外部命令还是回合事实」,
|
|
9
|
+
* 两者的**失败语义也不同** —— 控制面被拒要给 409 (调用方改主意), 而事实写不进是**响亮失败**
|
|
10
|
+
* (run-store 是 strict 的: 写不进 = 这次运行在事实层面不存在, 不许静默继续)。
|
|
11
|
+
*
|
|
12
|
+
* 形状照抄 `control.ts` (同一个仓库里只该有一种端口写法):
|
|
13
|
+
* 端口注入 · **未注入即拒** (不静默降级) · 一律**返回结果对象不抛** · 端口用 `{ok:false}` 表达拒绝要被归一化 · 审计流水可读。
|
|
14
|
+
*/
|
|
15
|
+
export const RUN_LIFECYCLE_OPS = [
|
|
16
|
+
'start-run',
|
|
17
|
+
'record-step',
|
|
18
|
+
'finish-run',
|
|
19
|
+
'save-checkpoint',
|
|
20
|
+
];
|
|
21
|
+
/** 哪些操作必须有 runId (start-run 例外: 它的 runId 是**结果**, 不是入参) */
|
|
22
|
+
export const RUN_LIFECYCLE_NEEDS_RUN_ID = {
|
|
23
|
+
'start-run': false,
|
|
24
|
+
'record-step': true,
|
|
25
|
+
'finish-run': true,
|
|
26
|
+
'save-checkpoint': true,
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* 端口**不抛异常但明确拒绝** (`{ ok: false, reason }`) 的归一化 —— 与 `control.ts` 同口径。
|
|
30
|
+
* 为什么必须有: store 类原语用返回值表达拒绝 (例如状态迁移不合法), 只看"有没有抛"会把**被拒当成成功**。
|
|
31
|
+
*/
|
|
32
|
+
function portRefusal(res) {
|
|
33
|
+
if (res && typeof res === 'object' && 'ok' in res) {
|
|
34
|
+
const r = res;
|
|
35
|
+
if (r.ok === false)
|
|
36
|
+
return `端口拒绝: ${String(r.reason ?? '(未给原因)')}`;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const audit = [];
|
|
41
|
+
const AUDIT_MAX = 200;
|
|
42
|
+
/** 审计流水 (最近 N 条; 测试与运维可读) */
|
|
43
|
+
export function runLifecycleAudit() {
|
|
44
|
+
return audit;
|
|
45
|
+
}
|
|
46
|
+
export function resetRunLifecycleAudit() {
|
|
47
|
+
audit.length = 0;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 提交一条生命周期写入。**唯一入口** —— 校验 (op 合法 / origin 非空 / 需要的定位字段齐) → 派发到 port → 记审计。
|
|
51
|
+
* 一律**返回结果对象**, 不抛 (调用方不该用 try/catch 表达"没落盘"); 要不要把 `ok=false` 变成异常由
|
|
52
|
+
* `assertLifecycleOk` 决定 —— 生命周期写入**一律**要 (见该函数注释)。
|
|
53
|
+
*/
|
|
54
|
+
export async function submitRunLifecycle(req, ports) {
|
|
55
|
+
const op = req?.op;
|
|
56
|
+
const finish = (ok, detail, port, result) => {
|
|
57
|
+
audit.push({
|
|
58
|
+
at: Date.now(),
|
|
59
|
+
op,
|
|
60
|
+
origin: req?.origin ?? '(anonymous)',
|
|
61
|
+
target: String(req?.runId ?? '(new)'),
|
|
62
|
+
ok,
|
|
63
|
+
detail,
|
|
64
|
+
});
|
|
65
|
+
if (audit.length > AUDIT_MAX)
|
|
66
|
+
audit.splice(0, audit.length - AUDIT_MAX);
|
|
67
|
+
return { ok, op, via: 'kernel-run-lifecycle', detail, port, result };
|
|
68
|
+
};
|
|
69
|
+
if (!RUN_LIFECYCLE_OPS.includes(op))
|
|
70
|
+
return finish(false, `未知写入类型: ${String(req?.op)}`);
|
|
71
|
+
if (!req.origin)
|
|
72
|
+
return finish(false, '缺少 origin (审计要求: 不许匿名写入)');
|
|
73
|
+
if (RUN_LIFECYCLE_NEEDS_RUN_ID[op] && !req.runId)
|
|
74
|
+
return finish(false, `${op} 缺少 runId`);
|
|
75
|
+
const payload = req.payload ?? {};
|
|
76
|
+
if (op === 'start-run') {
|
|
77
|
+
if (typeof ports.startRun !== 'function')
|
|
78
|
+
return finish(false, 'port 未注入: startRun');
|
|
79
|
+
try {
|
|
80
|
+
const res = await ports.startRun(payload);
|
|
81
|
+
const refusal = portRefusal(res);
|
|
82
|
+
return refusal ? finish(false, refusal, 'startRun', res) : finish(true, undefined, 'startRun', res);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
return finish(false, `startRun 失败: ${String(err?.message ?? err).slice(0, 160)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (op === 'record-step') {
|
|
89
|
+
if (typeof ports.recordStep !== 'function')
|
|
90
|
+
return finish(false, 'port 未注入: recordStep');
|
|
91
|
+
try {
|
|
92
|
+
const res = await ports.recordStep(req.runId, payload);
|
|
93
|
+
const refusal = portRefusal(res);
|
|
94
|
+
return refusal ? finish(false, refusal, 'recordStep', res) : finish(true, undefined, 'recordStep', res);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
return finish(false, `recordStep 失败: ${String(err?.message ?? err).slice(0, 160)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (op === 'finish-run') {
|
|
101
|
+
if (typeof ports.finishRun !== 'function')
|
|
102
|
+
return finish(false, 'port 未注入: finishRun');
|
|
103
|
+
try {
|
|
104
|
+
const res = await ports.finishRun(req.runId, payload);
|
|
105
|
+
const refusal = portRefusal(res);
|
|
106
|
+
return refusal ? finish(false, refusal, 'finishRun', res) : finish(true, undefined, 'finishRun', res);
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
return finish(false, `finishRun 失败: ${String(err?.message ?? err).slice(0, 160)}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (op === 'save-checkpoint') {
|
|
113
|
+
if (typeof ports.saveCheckpoint !== 'function')
|
|
114
|
+
return finish(false, 'port 未注入: saveCheckpoint');
|
|
115
|
+
try {
|
|
116
|
+
const res = await ports.saveCheckpoint(req.runId, payload);
|
|
117
|
+
const refusal = portRefusal(res);
|
|
118
|
+
return refusal ? finish(false, refusal, 'saveCheckpoint', res) : finish(true, undefined, 'saveCheckpoint', res);
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
return finish(false, `saveCheckpoint 失败: ${String(err?.message ?? err).slice(0, 160)}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return finish(false, `未处理的写入类型: ${String(op)}`);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 生命周期写入**必须**落盘 —— 拿不到 `ok` 就抛。
|
|
128
|
+
*
|
|
129
|
+
* 依据: `run-store` 的持久化是 **strict** (写不进 = 这次运行在事实层面不存在),
|
|
130
|
+
* 所以「写失败但继续跑」= 制造"agent 跑了但没记录"的老毛病。要降级只有一种合法姿势:
|
|
131
|
+
* 由调用方显式换持久化等级 (环境开关), 不许在这里静默吞。
|
|
132
|
+
*/
|
|
133
|
+
export function assertLifecycleOk(out) {
|
|
134
|
+
if (!out.ok) {
|
|
135
|
+
throw new Error(`[kernel-run-lifecycle] ${out.op} 未落盘: ${out.detail ?? '未知原因'}`
|
|
136
|
+
+ ' (run-store 是 strict: 写不进 = 这次运行在事实层面不存在, 不许静默继续)');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* transport.ts — K10 ④: **通信入口端口** (与 `control.ts` / `run-lifecycle.ts` 同一套端口写法)
|
|
3
|
+
*
|
|
4
|
+
* 目标形态 (设计页): `transport → router → channel mailbox` —— 也就是说:
|
|
5
|
+
* **上层只对"通信端口"说话, 不直接对某条传输 (hyperswarm / iroh / p2pNetwork) 说话**。
|
|
6
|
+
* Pi 现在自己持有 3 个薄包装 (`getPeers` / `sendMessage` / `broadcast` → `p2pNetwork`),
|
|
7
|
+
* 那正是"Pi 内部通信发送逻辑"还没迁完的形状 (K10 ④)。
|
|
8
|
+
*
|
|
9
|
+
* 为什么用端口而不是直接调传输:
|
|
10
|
+
* - 传输是可替换的 (hyperswarm / iroh / 以后的东西) ⇒ 上层不该 import 具体实现;
|
|
11
|
+
* - 发送是**有副作用**的动作 ⇒ 要能审计 (谁发的 / 发给谁 / 成没成), 且**未注入即拒** (不静默丢弃消息);
|
|
12
|
+
* - 与 `control.ts` / `run-lifecycle.ts` 保持同一种形状, 免得仓里出现第三种端口写法。
|
|
13
|
+
*
|
|
14
|
+
* 注: 本模块只做"入口收口", **不搬传输实现** (传输仍在 `src/network/**`); 判据见 `src/test/k10-transport-port.test.ts`。
|
|
15
|
+
*/
|
|
16
|
+
export const TRANSPORT_OPS = ['send', 'broadcast', 'peers'];
|
|
17
|
+
/** 哪些操作必须有目标 (send 要 peerId; broadcast / peers 不需要) */
|
|
18
|
+
export const TRANSPORT_NEEDS_TARGET = {
|
|
19
|
+
send: true,
|
|
20
|
+
broadcast: false,
|
|
21
|
+
peers: false,
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* 同步读对端 (K10 ④ 后半) —— 与 `submitTransport` 分开是因为**签名**而不是因为语义:
|
|
25
|
+
* 传输层是异步的, 但上层有同步调用点 (sync 签名不能凭空变 async)。
|
|
26
|
+
* 返回 `null` = 端口未注入 (调用方必须自己决定降级, 不许假装"没有对端" —— 那是两回事)。
|
|
27
|
+
*/
|
|
28
|
+
export function transportPeersSync(ports) {
|
|
29
|
+
if (typeof ports?.peersSync !== 'function')
|
|
30
|
+
return null;
|
|
31
|
+
return ports.peersSync();
|
|
32
|
+
}
|
|
33
|
+
/** 端口用 `{ok:false}` 表达拒绝的归一化 (与另外两个端口同口径) */
|
|
34
|
+
function portRefusal(res) {
|
|
35
|
+
if (res && typeof res === 'object' && 'ok' in res) {
|
|
36
|
+
const r = res;
|
|
37
|
+
if (r.ok === false)
|
|
38
|
+
return `端口拒绝: ${String(r.reason ?? '(未给原因)')}`;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const audit = [];
|
|
43
|
+
const AUDIT_MAX = 200;
|
|
44
|
+
/** 审计流水 (最近 N 条; 测试与运维可读) */
|
|
45
|
+
export function transportAudit() {
|
|
46
|
+
return audit;
|
|
47
|
+
}
|
|
48
|
+
export function resetTransportAudit() {
|
|
49
|
+
audit.length = 0;
|
|
50
|
+
}
|
|
51
|
+
/** 提交一次通信动作。**唯一入口** —— 校验 → 派发到 port → 记审计; 一律返回结果对象, 不抛。 */
|
|
52
|
+
export async function submitTransport(req, ports) {
|
|
53
|
+
const op = req?.op;
|
|
54
|
+
const finish = (ok, detail, port, result) => {
|
|
55
|
+
audit.push({
|
|
56
|
+
at: Date.now(),
|
|
57
|
+
op,
|
|
58
|
+
origin: req?.origin ?? '(anonymous)',
|
|
59
|
+
target: String(req?.peerId ?? '(all)'),
|
|
60
|
+
ok,
|
|
61
|
+
detail,
|
|
62
|
+
});
|
|
63
|
+
if (audit.length > AUDIT_MAX)
|
|
64
|
+
audit.splice(0, audit.length - AUDIT_MAX);
|
|
65
|
+
return { ok, op, via: 'kernel-transport', detail, port, result };
|
|
66
|
+
};
|
|
67
|
+
if (!TRANSPORT_OPS.includes(op))
|
|
68
|
+
return finish(false, `未知通信动作: ${String(req?.op)}`);
|
|
69
|
+
if (!req.origin)
|
|
70
|
+
return finish(false, '缺少 origin (审计要求: 不许匿名发送)');
|
|
71
|
+
if (TRANSPORT_NEEDS_TARGET[op] && !req.peerId)
|
|
72
|
+
return finish(false, `${op} 缺少 peerId`);
|
|
73
|
+
const kind = req.kind ?? 'message';
|
|
74
|
+
const payload = req.payload ?? '';
|
|
75
|
+
if (op === 'send') {
|
|
76
|
+
if (typeof ports.send !== 'function')
|
|
77
|
+
return finish(false, 'port 未注入: send');
|
|
78
|
+
try {
|
|
79
|
+
const res = await ports.send(req.peerId, kind, payload);
|
|
80
|
+
const refusal = portRefusal(res);
|
|
81
|
+
return refusal ? finish(false, refusal, 'send', res) : finish(true, undefined, 'send', res);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
return finish(false, `send 失败: ${String(err?.message ?? err).slice(0, 160)}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (op === 'broadcast') {
|
|
88
|
+
if (typeof ports.broadcast !== 'function')
|
|
89
|
+
return finish(false, 'port 未注入: broadcast');
|
|
90
|
+
try {
|
|
91
|
+
const res = await ports.broadcast(kind, payload);
|
|
92
|
+
const refusal = portRefusal(res);
|
|
93
|
+
return refusal ? finish(false, refusal, 'broadcast', res) : finish(true, undefined, 'broadcast', res);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
return finish(false, `broadcast 失败: ${String(err?.message ?? err).slice(0, 160)}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (op === 'peers') {
|
|
100
|
+
if (typeof ports.peers !== 'function')
|
|
101
|
+
return finish(false, 'port 未注入: peers');
|
|
102
|
+
try {
|
|
103
|
+
const res = await ports.peers();
|
|
104
|
+
return finish(true, undefined, 'peers', res);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
return finish(false, `peers 失败: ${String(err?.message ?? err).slice(0, 160)}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return finish(false, `未处理的通信动作: ${String(op)}`);
|
|
111
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { sanitizeToolsForApi } from './tool-name.js';
|
|
2
|
+
/** 可被 abort 打断的 sleep (退避用) */
|
|
3
|
+
function sleep(ms, signal) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const t = setTimeout(() => { signal?.removeEventListener?.('abort', onAbort); resolve(); }, ms);
|
|
6
|
+
const onAbort = () => { clearTimeout(t); reject(new Error('aborted during backoff')); };
|
|
7
|
+
if (signal?.aborted)
|
|
8
|
+
onAbort();
|
|
9
|
+
else
|
|
10
|
+
signal?.addEventListener?.('abort', onAbort, { once: true });
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export function createNativeAdapter(cfg) {
|
|
14
|
+
const timeoutMs = cfg.timeoutMs ?? 120_000;
|
|
15
|
+
const maxRetries = cfg.maxRetries ?? 3;
|
|
16
|
+
const url = `${String(cfg.baseUrl || '').replace(/\/+$/, '')}/chat/completions`;
|
|
17
|
+
let calls = 0; // 观测: 真发出去几次 (重试也算)
|
|
18
|
+
let retries = 0;
|
|
19
|
+
const adapter = {
|
|
20
|
+
providerId: cfg.providerId ?? 'native',
|
|
21
|
+
stats: () => ({ calls, retries }),
|
|
22
|
+
async chat(context, systemPrompt, signal, tools, purpose) {
|
|
23
|
+
const messages = [
|
|
24
|
+
{ role: 'system', content: String(systemPrompt ?? '') },
|
|
25
|
+
{ role: 'user', content: String(context ?? '') },
|
|
26
|
+
];
|
|
27
|
+
let attempt = 0;
|
|
28
|
+
let lastErr;
|
|
29
|
+
while (attempt <= maxRetries) {
|
|
30
|
+
const ac = new AbortController();
|
|
31
|
+
const onAbort = () => ac.abort();
|
|
32
|
+
if (signal?.aborted)
|
|
33
|
+
ac.abort();
|
|
34
|
+
else
|
|
35
|
+
signal?.addEventListener?.('abort', onAbort, { once: true });
|
|
36
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
37
|
+
try {
|
|
38
|
+
calls += 1;
|
|
39
|
+
// ⚠️ 出程必须**净化工具名** —— 端点只接受 `^[a-zA-Z0-9_-]{1,64}$` (实测: 原样转发 183 个工具
|
|
40
|
+
// 里第 124 个不合规 ⇒ HTTP 400, 整轮空回复)。净化会把映射登记进**全局路由表**,
|
|
41
|
+
// 回程由 pivot 的 `resolveApiToolName` 还原 ⇒ 上下层协议不变 (这正是"适配器边界"的一部分)。
|
|
42
|
+
const wireTools = Array.isArray(tools) && tools.length > 0 ? sanitizeToolsForApi(tools) : undefined;
|
|
43
|
+
const res = await fetch(url, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${cfg.apiKey}` },
|
|
46
|
+
body: JSON.stringify({
|
|
47
|
+
model: cfg.model,
|
|
48
|
+
messages,
|
|
49
|
+
...(wireTools ? { tools: wireTools } : {}),
|
|
50
|
+
stream: false,
|
|
51
|
+
}),
|
|
52
|
+
signal: ac.signal,
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
const text = await res.text().catch(() => '');
|
|
56
|
+
const retryable = res.status === 429 || res.status >= 500;
|
|
57
|
+
if (retryable && attempt < maxRetries) {
|
|
58
|
+
lastErr = new Error(`native-adapter HTTP ${res.status}: ${text.slice(0, 160)}`);
|
|
59
|
+
attempt += 1;
|
|
60
|
+
retries += 1;
|
|
61
|
+
await sleep(1000 * 2 ** (attempt - 1), signal);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// 非 429 的 4xx ⇒ **不重试** (重试无意义)。这里必须打标记: 否则下面的 catch
|
|
65
|
+
// 会把它当"网络错"再退避重试 —— 踩过 (用例⑤: 一个 400 被发了 4 次)。
|
|
66
|
+
const fatal = new Error(`native-adapter HTTP ${res.status}: ${text.slice(0, 200)}`);
|
|
67
|
+
fatal.noRetry = true;
|
|
68
|
+
throw fatal;
|
|
69
|
+
}
|
|
70
|
+
const data = await res.json();
|
|
71
|
+
const msg = data?.choices?.[0]?.message ?? {};
|
|
72
|
+
const toolCalls = Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0 ? msg.tool_calls : undefined;
|
|
73
|
+
return {
|
|
74
|
+
reply: String(msg.content ?? ''),
|
|
75
|
+
tokens: data?.usage?.total_tokens,
|
|
76
|
+
toolCalls,
|
|
77
|
+
messages: messages.concat([{ role: 'assistant', content: String(msg.content ?? '') }]),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
// 调用方主动 abort ⇒ 原样抛出 (别当网络错去重试)
|
|
82
|
+
if (signal?.aborted)
|
|
83
|
+
throw e;
|
|
84
|
+
if (e?.noRetry)
|
|
85
|
+
throw e; // 非 429 的 4xx 等: 不重试
|
|
86
|
+
lastErr = e;
|
|
87
|
+
if (attempt >= maxRetries)
|
|
88
|
+
throw e;
|
|
89
|
+
attempt += 1;
|
|
90
|
+
retries += 1;
|
|
91
|
+
await sleep(1000 * 2 ** (attempt - 1), signal);
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
signal?.removeEventListener?.('abort', onAbort);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
throw lastErr ?? new Error('native-adapter: 不可达');
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
return adapter;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* 从环境装配一个 native 适配器 (供 `BOLLOON_NATIVE_ADAPTER=1` 开关使用)。
|
|
105
|
+
* 缺 key ⇒ 返回 null (调用方回落到原适配器, 不假装可用)。
|
|
106
|
+
*/
|
|
107
|
+
export function nativeAdapterFromEnv(env = process.env) {
|
|
108
|
+
const apiKey = env.BOLLOON_NATIVE_API_KEY || env.DEEPSEEK_API_KEY || '';
|
|
109
|
+
if (!apiKey)
|
|
110
|
+
return null;
|
|
111
|
+
return createNativeAdapter({
|
|
112
|
+
baseUrl: env.BOLLOON_NATIVE_BASE_URL || 'https://api.deepseek.com/v1',
|
|
113
|
+
apiKey,
|
|
114
|
+
model: env.BOLLOON_NATIVE_MODEL || 'deepseek-chat',
|
|
115
|
+
providerId: 'native-openai-compatible',
|
|
116
|
+
});
|
|
117
|
+
}
|