@bolloon/bolloon-agent 0.5.5 → 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.
Files changed (158) hide show
  1. package/dist/agents/execution-entry.js +10 -0
  2. package/dist/agents/goal-flywheel/wiring/seams.js +5 -1
  3. package/dist/agents/parse-tool-call.js +6 -1
  4. package/dist/agents/pi-sdk-session-factory.js +36 -3
  5. package/dist/agents/pi-sdk-tools.js +18 -8
  6. package/dist/agents/pi-sdk.js +749 -1390
  7. package/dist/agents/run-context.js +20 -0
  8. package/dist/agents/runner-resolver.js +3 -1
  9. package/dist/agents/tool-capability/index.js +51 -0
  10. package/dist/agents/workflow-pivot-loop.js +31 -10
  11. package/dist/agents/write-verify.js +60 -0
  12. package/dist/bollharness-integration/integration.js +8 -0
  13. package/dist/bollharness-integration/skill-adapter.js +32 -3
  14. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +5 -1
  15. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -1
  16. package/dist/electron-build/agents/parse-tool-call.js +6 -1
  17. package/dist/electron-build/agents/parse-tool-call.js.map +1 -1
  18. package/dist/electron-build/agents/pi-sdk-session-factory.js +36 -3
  19. package/dist/electron-build/agents/pi-sdk-session-factory.js.map +1 -1
  20. package/dist/electron-build/agents/pi-sdk-tools.js +21 -11
  21. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  22. package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
  23. package/dist/electron-build/agents/pi-sdk.js +750 -1389
  24. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  25. package/dist/electron-build/agents/run-context.js +24 -0
  26. package/dist/electron-build/agents/run-context.js.map +1 -0
  27. package/dist/electron-build/agents/runner-resolver.js +3 -1
  28. package/dist/electron-build/agents/runner-resolver.js.map +1 -1
  29. package/dist/electron-build/agents/tool-capability/index.js +90 -0
  30. package/dist/electron-build/agents/tool-capability/index.js.map +1 -0
  31. package/dist/electron-build/agents/workflow-pivot-loop.js +31 -10
  32. package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
  33. package/dist/electron-build/agents/write-verify.js +99 -0
  34. package/dist/electron-build/agents/write-verify.js.map +1 -0
  35. package/dist/electron-build/bollharness-integration/integration.js +8 -0
  36. package/dist/electron-build/bollharness-integration/integration.js.map +1 -1
  37. package/dist/electron-build/bollharness-integration/skill-adapter.js +32 -3
  38. package/dist/electron-build/bollharness-integration/skill-adapter.js.map +1 -1
  39. package/dist/electron-build/kernel/channel-actor.js +339 -0
  40. package/dist/electron-build/kernel/channel-actor.js.map +1 -0
  41. package/dist/electron-build/kernel/control.js +102 -0
  42. package/dist/electron-build/kernel/control.js.map +1 -0
  43. package/dist/electron-build/kernel/model-runtime.js +508 -0
  44. package/dist/electron-build/kernel/model-runtime.js.map +1 -0
  45. package/dist/electron-build/kernel/run-lifecycle.js +146 -0
  46. package/dist/electron-build/kernel/run-lifecycle.js.map +1 -0
  47. package/dist/electron-build/kernel/transport.js +119 -0
  48. package/dist/electron-build/kernel/transport.js.map +1 -0
  49. package/dist/electron-build/llm/native-adapter.js +122 -0
  50. package/dist/electron-build/llm/native-adapter.js.map +1 -0
  51. package/dist/index.js +43 -19
  52. package/dist/ios/bolloon-web.json +3 -3
  53. package/dist/ios/client.js +13 -7
  54. package/dist/ios/mobile-core.js +7762 -6289
  55. package/dist/ios/mobile.js +64 -0
  56. package/dist/ios/routes-mobile-tasks.js +2 -1
  57. package/dist/ios/routes-tasks.js +16 -1
  58. package/dist/ios/routes-x402-info.js +28 -1
  59. package/dist/ios/server-storage.js +26 -5
  60. package/dist/ios/server.js +840 -862
  61. package/dist/ios/sw.js +1 -1
  62. package/dist/ios/ui/message-renderer.js +473 -29
  63. package/dist/ios/ui/step-timeline.js +13 -12
  64. package/dist/kernel/channel-actor.js +321 -0
  65. package/dist/kernel/control.js +95 -0
  66. package/dist/kernel/gate-scan.js +1350 -0
  67. package/dist/kernel/model-runtime.js +494 -0
  68. package/dist/kernel/plan-channel-actor.js +305 -0
  69. package/dist/kernel/plan-communication.js +163 -0
  70. package/dist/kernel/plan-constraint.js +152 -0
  71. package/dist/kernel/plan-deletion.js +32 -0
  72. package/dist/kernel/plan-harness.js +78 -0
  73. package/dist/kernel/plan-modelruntime.js +60 -0
  74. package/dist/kernel/plan-runcontext.js +163 -0
  75. package/dist/kernel/plan.js +149 -0
  76. package/dist/kernel/roster.js +244 -0
  77. package/dist/kernel/run-lifecycle.js +138 -0
  78. package/dist/kernel/transport.js +111 -0
  79. package/dist/llm/native-adapter.js +117 -0
  80. package/dist/web/bolloon-web.json +3 -3
  81. package/dist/web/client.js +12 -7
  82. package/dist/web/routes-tasks.js +16 -1
  83. package/dist/web/server.js +838 -862
  84. package/dist/web/sw.js +1 -1
  85. package/dist/web/ui/message-renderer.js +473 -29
  86. package/dist/web/ui/step-timeline.js +13 -12
  87. package/package.json +1 -1
  88. package/scripts/build-web.ts +74 -1
  89. package/dist/constraint-runtime/_archive_helper.d.ts +0 -1
  90. package/dist/constraint-runtime/_archive_helper.js +0 -9
  91. package/dist/constraint-runtime/assistant/index.d.ts +0 -11
  92. package/dist/constraint-runtime/assistant/index.js +0 -12
  93. package/dist/constraint-runtime/bootstrap/index.d.ts +0 -11
  94. package/dist/constraint-runtime/bootstrap/index.js +0 -12
  95. package/dist/constraint-runtime/bridge/index.d.ts +0 -11
  96. package/dist/constraint-runtime/bridge/index.js +0 -12
  97. package/dist/constraint-runtime/buddy/index.d.ts +0 -11
  98. package/dist/constraint-runtime/buddy/index.js +0 -12
  99. package/dist/constraint-runtime/cli/index.d.ts +0 -11
  100. package/dist/constraint-runtime/cli/index.js +0 -12
  101. package/dist/constraint-runtime/components/index.d.ts +0 -11
  102. package/dist/constraint-runtime/components/index.js +0 -12
  103. package/dist/constraint-runtime/constants/index.d.ts +0 -11
  104. package/dist/constraint-runtime/constants/index.js +0 -12
  105. package/dist/constraint-runtime/coordinator/index.d.ts +0 -11
  106. package/dist/constraint-runtime/coordinator/index.js +0 -12
  107. package/dist/constraint-runtime/cost_hook.d.ts +0 -2
  108. package/dist/constraint-runtime/cost_hook.js +0 -4
  109. package/dist/constraint-runtime/entrypoints/index.d.ts +0 -11
  110. package/dist/constraint-runtime/entrypoints/index.js +0 -12
  111. package/dist/constraint-runtime/execution_registry.d.ts +0 -20
  112. package/dist/constraint-runtime/execution_registry.js +0 -44
  113. package/dist/constraint-runtime/hooks/index.d.ts +0 -11
  114. package/dist/constraint-runtime/hooks/index.js +0 -12
  115. package/dist/constraint-runtime/ink.d.ts +0 -1
  116. package/dist/constraint-runtime/ink.js +0 -4
  117. package/dist/constraint-runtime/keybindings/index.d.ts +0 -11
  118. package/dist/constraint-runtime/keybindings/index.js +0 -12
  119. package/dist/constraint-runtime/memdir/index.d.ts +0 -11
  120. package/dist/constraint-runtime/memdir/index.js +0 -12
  121. package/dist/constraint-runtime/migrations/index.d.ts +0 -11
  122. package/dist/constraint-runtime/migrations/index.js +0 -12
  123. package/dist/constraint-runtime/moreright/index.d.ts +0 -11
  124. package/dist/constraint-runtime/moreright/index.js +0 -12
  125. package/dist/constraint-runtime/native_ts/index.d.ts +0 -11
  126. package/dist/constraint-runtime/native_ts/index.js +0 -12
  127. package/dist/constraint-runtime/output_styles/index.d.ts +0 -11
  128. package/dist/constraint-runtime/output_styles/index.js +0 -12
  129. package/dist/constraint-runtime/plugins/index.d.ts +0 -11
  130. package/dist/constraint-runtime/plugins/index.js +0 -12
  131. package/dist/constraint-runtime/port_manifest.d.ts +0 -7
  132. package/dist/constraint-runtime/port_manifest.js +0 -11
  133. package/dist/constraint-runtime/query.d.ts +0 -6
  134. package/dist/constraint-runtime/query.js +0 -1
  135. package/dist/constraint-runtime/remote/index.d.ts +0 -11
  136. package/dist/constraint-runtime/remote/index.js +0 -12
  137. package/dist/constraint-runtime/schemas/index.d.ts +0 -11
  138. package/dist/constraint-runtime/schemas/index.js +0 -12
  139. package/dist/constraint-runtime/screens/index.d.ts +0 -11
  140. package/dist/constraint-runtime/screens/index.js +0 -12
  141. package/dist/constraint-runtime/server/index.d.ts +0 -11
  142. package/dist/constraint-runtime/server/index.js +0 -12
  143. package/dist/constraint-runtime/services/index.d.ts +0 -11
  144. package/dist/constraint-runtime/services/index.js +0 -12
  145. package/dist/constraint-runtime/state/index.d.ts +0 -11
  146. package/dist/constraint-runtime/state/index.js +0 -12
  147. package/dist/constraint-runtime/system_init.d.ts +0 -1
  148. package/dist/constraint-runtime/system_init.js +0 -20
  149. package/dist/constraint-runtime/types/index.d.ts +0 -11
  150. package/dist/constraint-runtime/types/index.js +0 -12
  151. package/dist/constraint-runtime/upstream_proxy/index.d.ts +0 -11
  152. package/dist/constraint-runtime/upstream_proxy/index.js +0 -12
  153. package/dist/constraint-runtime/utils/index.d.ts +0 -11
  154. package/dist/constraint-runtime/utils/index.js +0 -12
  155. package/dist/constraint-runtime/vim/index.d.ts +0 -11
  156. package/dist/constraint-runtime/vim/index.js +0 -12
  157. package/dist/constraint-runtime/voice/index.d.ts +0 -11
  158. package/dist/constraint-runtime/voice/index.js +0 -12
@@ -44,24 +44,30 @@ var __importStar = (this && this.__importStar) || (function () {
44
44
  };
45
45
  })();
46
46
  Object.defineProperty(exports, "__esModule", { value: true });
47
- exports.PiAgentSession = exports.PROACTIVE_WORK_DISCIPLINE = exports.runSelfImproveLoop = exports.resetAgentSession = exports.getAgentSession = exports.createAgentSession = exports.PiSessionManager = exports.TOOL_DEFINITIONS = void 0;
47
+ exports.PiAgentSession = exports.PROACTIVE_WORK_DISCIPLINE = exports.WRITE_TOOLS = exports.withWriteVerified = exports.verifyWriteOutcome = exports.runSelfImproveLoop = exports.resetAgentSession = exports.getAgentSession = exports.createAgentSession = exports.PiSessionManager = exports.TOOL_DEFINITIONS = void 0;
48
48
  exports.decideUnreported = decideUnreported;
49
49
  exports.writeBackCurrentTurnInto = writeBackCurrentTurnInto;
50
- exports.verifyWriteOutcome = verifyWriteOutcome;
51
50
  const fs = __importStar(require("fs/promises"));
52
51
  const fsSync = __importStar(require("fs"));
53
52
  const os = __importStar(require("os"));
54
53
  const path = __importStar(require("path"));
55
- const tool_path_args_js_1 = require("./tool-path-args.js");
54
+ const run_context_js_1 = require("./run-context.js");
55
+ const channel_actor_js_1 = require("../kernel/channel-actor.js");
56
+ // K10 ①: 运行生命周期写入经内核端口 (与 kernel/control.ts 的控制面分开 —— 一个回答"谁命令我停",
57
+ // 一个回答"我这一轮发生了什么"; 失败语义也不同: 控制面被拒 → 409, 事实写不进 → 响亮失败)
58
+ const run_lifecycle_js_1 = require("../kernel/run-lifecycle.js");
59
+ // K10 ④: 通信发送入口经内核端口 (上层不再直接对某条传输说话; 换传输只改这一处注入)
60
+ const transport_js_1 = require("../kernel/transport.js");
61
+ // K10 ①: run 状态迁移经内核**控制面**端口 (它本来就管"谁命令这条 run 改状态": 带 origin 审计 + 拒绝归一化)
62
+ const control_js_1 = require("../kernel/control.js");
63
+ // K10 ②: 回合的模型经**内核运行时**只读取租约 (传输仍是 Pi 客户端 ⇒ 行为不变; 取不到如实回落)
64
+ const model_runtime_js_1 = require("../kernel/model-runtime.js");
56
65
  const background_notices_js_1 = require("./background-notices.js");
57
66
  const skill_ledger_js_1 = require("./skill-ledger.js");
58
67
  const node_crypto_1 = require("node:crypto");
59
68
  const experience_review_js_1 = require("./experience-review.js");
60
69
  const lesson_to_skill_js_1 = require("./lesson-to-skill.js");
61
70
  const managed_services_js_1 = require("./managed-services.js");
62
- const iteration_budget_js_1 = require("./iteration-budget.js");
63
- const tool_telemetry_js_1 = require("./tool-telemetry.js");
64
- const tool_result_gate_js_1 = require("./tool-result-gate.js");
65
71
  const tool_subset_js_1 = require("./tool-subset.js");
66
72
  const code_write_gate_js_1 = require("./code-write-gate.js");
67
73
  // 2026-10-01: 身份解析必须**静态导入** —— 之前在函数体里用 require(), 而打包后是 ESM ⇒
@@ -83,12 +89,11 @@ const constraint_layer_js_1 = require("./constraint-layer.js");
83
89
  const workflow_engine_js_1 = require("./workflow-engine.js");
84
90
  const constraint_runtime_1 = require("@bolloon/constraint-runtime");
85
91
  const workflow_pivot_loop_js_1 = require("./workflow-pivot-loop.js");
92
+ const native_adapter_js_1 = require("../llm/native-adapter.js"); // K9: 第二个 (非 Pi) 推理适配器
86
93
  const p2p_document_tools_js_1 = require("./p2p-document-tools.js");
87
94
  const run_store_js_1 = require("./run-store.js");
88
95
  const goal_store_js_1 = require("./goal-store.js");
89
96
  const model_selection_js_1 = require("../llm/model-selection.js");
90
- // 2026-09-26: 工具名出网净化 (pi-ai.ts 唯一边界) + 回程派发还原 (原名 ↔ API 名)
91
- const tool_name_js_1 = require("../llm/tool-name.js");
92
97
  // 2026-09-28 (前缀 KV 可命中): CURRENT TURN 注入标记 —— "当前轮是否已注入"的唯一判据
93
98
  const pi_ai_js_1 = require("../llm/pi-ai.js");
94
99
  const pi_harness_js_1 = require("./pi-harness.js");
@@ -123,18 +128,14 @@ const exhaust_scrubber_js_1 = require("../bootstrap/exhaust-scrubber.js");
123
128
  const monitor_gate_js_1 = require("../pi-ecosystem-judgment/monitor-gate.js");
124
129
  // Bootstrap 生命周期 hook (SessionStart / Stop / PreToolUse)
125
130
  const human_value_pipeline_js_1 = require("../pi-ecosystem-judgment/human-value-pipeline.js");
126
- const lifecycle_hooks_js_1 = require("../bootstrap/lifecycle-hooks.js");
127
131
  const index_js_2 = require("../context-compaction/index.js");
128
132
  // React Harness: 8-gate + 4-guard (防越权 / 防 prompt 注入)
129
133
  const react_harness_js_1 = require("../security/react-harness.js");
130
134
  const hooks_engine_js_1 = require("../hooks/hooks-engine.js");
131
135
  const deny_pipeline_js_1 = require("./deny-pipeline.js");
132
136
  const parse_tool_call_js_1 = require("./parse-tool-call.js");
133
- const error_classifier_js_1 = require("./error-classifier.js");
134
137
  const session_store_js_1 = require("./session-store.js");
135
138
  const tool_registry_js_1 = require("./tool-registry.js");
136
- const react_loop_js_1 = require("./react-loop.js");
137
- const loop_review_js_1 = require("./loop-review.js");
138
139
  // PiSessionManager 已抽到 ./pi-sdk-session-manager.ts (2026-07-06)
139
140
  // Tool / ToolResult / Message / StreamCallback / StreamEvent / HeartbeatConfig / AgentSession / TOOL_DEFINITIONS
140
141
  // 已抽到 ./pi-sdk-types.ts (2026-07-06)
@@ -192,32 +193,17 @@ function writeBackCurrentTurnInto(history, wire) {
192
193
  return 0; // 写回失败绝不影响对话本身
193
194
  }
194
195
  }
195
- const tool_loop_guard_js_1 = require("./tool-loop-guard.js");
196
196
  /**
197
- * 写操作"读回自证" (2026-10-01 优化 #4): 写类工具成功后**自动**核一次, 把事实拼进结果。
198
- * 为什么: 过程纪律写了"读回一次", 但**没人执行** ✗ ⇒ 现在由机制执行: "工具说成功 ≠ 任务成功"。
199
- * 只做**便宜**的核对(存在性/大小); 失败静默(核对不该把工具搞失败)。
197
+ * 写操作「读回自证」的实现已拆到 `./write-verify.js` (K10 ⑦):
198
+ * - 要接进 `pi-sdk-tools.ts` 的写类工具 ⇒ 留在这里会形成循环 import;
199
+ * - 原实现在函数体里用 `require('node:fs')`, 而产物是 ESM ⇒ 它此前**一次都没成功过**
200
+ * (只会被 catch 吞成 `[未核对] … require is not defined`)。
201
+ * 这里保留**同名转出**, 老的 import 点不受影响。
200
202
  */
201
- function verifyWriteOutcome(toolName, args, cwd) {
202
- try {
203
- const fsMod = require('node:fs');
204
- const pathMod = require('node:path');
205
- const WRITE = new Set(['write_file', 'edit_file', 'mkdir', 'move_file', 'copy_file']);
206
- if (!WRITE.has(String(toolName)))
207
- return null;
208
- const rel = String(args?.path ?? args?.to ?? args?.dir ?? '').trim();
209
- if (!rel)
210
- return null;
211
- const abs = pathMod.isAbsolute(rel) ? rel : pathMod.resolve(cwd, rel);
212
- const st = fsMod.statSync(abs);
213
- if (st.isDirectory())
214
- return `[已核对] 目录存在: ${rel}`;
215
- return `[已核对] 文件已落盘: ${rel} (${st.size} 字节, ${new Date(st.mtimeMs).toISOString()})`;
216
- }
217
- catch (e) {
218
- return `[未核对] 读回失败: ${String(e?.message || e).slice(0, 80)} —— 别急着说"已完成", 先确认路径/权限`;
219
- }
220
- }
203
+ var write_verify_js_1 = require("./write-verify.js");
204
+ Object.defineProperty(exports, "verifyWriteOutcome", { enumerable: true, get: function () { return write_verify_js_1.verifyWriteOutcome; } });
205
+ Object.defineProperty(exports, "withWriteVerified", { enumerable: true, get: function () { return write_verify_js_1.withWriteVerified; } });
206
+ Object.defineProperty(exports, "WRITE_TOOLS", { enumerable: true, get: function () { return write_verify_js_1.WRITE_TOOLS; } });
221
207
  /**
222
208
  * 过程纪律 (2026-10-01, 用户: 「智能体回复方式没有主动性 … 在过程里面更加主动考虑」)。
223
209
  *
@@ -260,7 +246,90 @@ class PiAgentSession {
260
246
  sessionManager;
261
247
  agentsManager;
262
248
  socialHeartbeat = null;
263
- messageHistory = [];
249
+ /**
250
+ * **K5 第 4 步 (所有权转移)**: history 的**本体**。
251
+ * · 未绑定 actor ⇒ 本体在这里 (绑定前的暂存);
252
+ * · 绑定 actor 后 ⇒ 本体是 `actor.state.messageHistory`, 本地那份被**收养**并清空。
253
+ * 绑定由 `attachActor()` 做, 且**只在会话身份已知时**发生 (见 session factory) —— 没有身份就不归属。
254
+ */
255
+ /**
256
+ * **K5 第 4 步 — history 写入的唯一漏斗 (push)**。
257
+ * 为什么必须同步: 调用点遍布 ReAct 循环 / 工具回灌 / 压缩, 而且**写完立刻读** (length / 索引 / slice),
258
+ * 改成 await 会改变同拍可见性。所以这里有意识地**不排队** —— 归属收敛 + 可数 (门能断言"零处直写"),
259
+ * 并发安全由入口投递进 mailbox 负责 (K5 第 5 步)。
260
+ */
261
+ pushHistory(...msgs) {
262
+ for (const m of msgs) {
263
+ // 步骤⑧ 起**只有一条路径**: 写 actor (实例侧暂存字段已删除) —— 也彻底消除了"自递归"的可能
264
+ this.actor.appendMessageSync(m);
265
+ }
266
+ }
267
+ /** **K5 第 4 步 — history 写入漏斗 (pop)** —— 同 pushHistory 的性质 */
268
+ popHistory() {
269
+ return this.actor.popMessageSync();
270
+ }
271
+ /** **K5 第 4 步 — history 写入漏斗 (整体替换)**: hydrate 回灌 / 压缩后的整块赋值都走这里 */
272
+ /**
273
+ * **整体替换**历史 —— 唯一漏斗之一, 步骤⑧ 起是**唯一**的"种历史"入口 (公开)。
274
+ * 公开的理由: 上层/测试要种一份历史时必须走这里; 直接给对象赋一个新数组会**换掉数组身份**
275
+ * (actor 里那份仍是空的) —— 那是静默丢数据 (实测: 删掉 setter 后 `session.messageHistory = [...]`
276
+ * 变成写在一个凭空出现的自有属性上, 测试于是看到 0 条)。
277
+ */
278
+ replaceHistory(next) {
279
+ this.actor.replaceHistory(next);
280
+ }
281
+ /**
282
+ * **K5 步骤⑦ — 把一次性请求里的绑定落到位** (入口不再散着设字段: 只交一个请求)。
283
+ * 写入都走访问器 ⇒ 本体落进 `actor.state` (步骤③ 起就是这样); 没给的不覆盖 (与环境变量"只覆盖显式给出的"同秩)。
284
+ */
285
+ applyExecutionRequest(req) {
286
+ if (req.channelId)
287
+ this.actor.state.channelId = req.channelId;
288
+ if (req.agentId)
289
+ this.actor.state.agentId = req.agentId;
290
+ if (req.goalId)
291
+ this.actor.state.goalBinding = req.goalId;
292
+ if (req.resumeRunId)
293
+ this.resumeRunId = req.resumeRunId;
294
+ }
295
+ /**
296
+ * **K5 步骤⑦ — Pi 的唯一执行入口**: 只接收一次性 `ExecutionRequest`。
297
+ * 有 `onStream` ⇒ 走 `promptStream` (流式); 否则走 `prompt`。绑定由 `applyExecutionRequest` 落位。
298
+ * 入口侧只需: `deliverThroughActor(session, () => session.runExecution(req))`。
299
+ */
300
+ async runExecution(req) {
301
+ this.applyExecutionRequest(req);
302
+ if (req.onStream) {
303
+ return this.promptStream(req.input, req.onStream, req.signal, req.channelId);
304
+ }
305
+ return this.prompt(req.input, { signal: req.signal, channelId: req.channelId });
306
+ }
307
+ /**
308
+ * **K5 第 4 步**: 绑定 actor 并**收养**绑定前已存在的本地历史。
309
+ * 收养规则: actor 侧为空 且 本地非空 ⇒ 搬过去 (任何绑定时序都不丢历史); 收养后清空本地。
310
+ * 约束 (全量回归打出来的): 调用方必须用**会话身份**当注册键; 没有身份就**不要调**这个方法。
311
+ */
312
+ attachActor(actor) {
313
+ const prev = this.actor;
314
+ this.actor = actor;
315
+ if (!prev || prev === actor)
316
+ return;
317
+ // **收养**: 把上一个 actor (通常是"出生时的私有 actor") 的状态搬进新家。
318
+ // 新家已有值时不覆盖 —— 同一会话身份以先到者为准 (禁止"后到的调用把先建的状态冲掉")。
319
+ const from = prev.state;
320
+ const to = actor.state;
321
+ if (from.messageHistory.length > 0 && to.messageHistory.length === 0) {
322
+ to.messageHistory.push(...from.messageHistory);
323
+ }
324
+ if (from.channelId && !to.channelId)
325
+ to.channelId = from.channelId;
326
+ if (from.agentId && !to.agentId)
327
+ to.agentId = from.agentId;
328
+ if (from.goalBinding && !to.goalBinding)
329
+ to.goalBinding = from.goalBinding;
330
+ if (from.activeRun && !to.activeRun)
331
+ to.activeRun = from.activeRun;
332
+ }
264
333
  tools = new Map();
265
334
  /** 2026-06-30: tool registry 模块 — 独立 alias resolve, 测试可消融. */
266
335
  _toolRegistry = new tool_registry_js_1.ToolRegistry();
@@ -355,7 +424,7 @@ class PiAgentSession {
355
424
  harnessEnabled = false;
356
425
  /** 8-gate + 4-guard 集中调度 (防越权 / 防 prompt 注入) */
357
426
  reactHarness = new react_harness_js_1.ReactHarness();
358
- usePivotLoop = false;
427
+ usePivotLoop = true; // K4-B (2026-10-02): 默认切到 pivot —— 所有入口只跑**一套** loop (老 ReAct loop 待删)
359
428
  pivotLoopConfig;
360
429
  /** P2: 当前会话的 permission mode (每次 promptStream 入口解析) */
361
430
  currentPermissionMode = 'default';
@@ -423,11 +492,29 @@ class PiAgentSession {
423
492
  // 拼到 systemPrompt 末尾, 别再混进 user message
424
493
  contextHintAddition = '';
425
494
  /**
426
- * 当前 onStream 引用 + abort signal (computeJudgmentGate 需要 onStream 广播 phase)
427
- * 每次 prompt / promptStream / promptWithPivotLoop 入口设置, 用完即清
495
+ * **K2 收尾 (leo 2026-10-02 口径)**: **唯一一处**把"已有的恢复身份"播种进 RunContext。
496
+ *
497
+ * · `currentRunId` 属 **run-boundary**, 不是循环内部的可变 RunContext 状态 ⇒ 只允许这一次显式播种读取;
498
+ * · 冻结 `CURRENT_RUN_ID_SEED_READS = 1` (见 src/kernel/plan-runcontext.ts), 由门强制;
499
+ * · 只发生在**进入一次 Run 时**; 不得在循环中新增读取点; 不得成为新的写入口;
500
+ * · K5 Channel Actor 完成后改为 `createRunContext({ runId: request.resumeRunId })`, 届时删除 `currentRunId` 字段本体。
501
+ */
502
+ seedRunContext(extra = {}) {
503
+ return (0, run_context_js_1.createRunContext)({ runId: this.actor.state.activeRun, ...extra });
504
+ }
505
+ /**
506
+ * **K5 迁移中**: 本会话所属 channel 的 Actor (由 session factory 在创建时绑定)。
507
+ * 现在只做**归属** —— 会话状态 (messageHistory / channelId / agentId / goalId) **仍然**在实例字段上,
508
+ * 逐项迁入 Actor 见 `src/kernel/plan-channel-actor.ts` 的 8 步。
428
509
  */
429
- currentOnStream = null;
430
- currentSignal = null;
510
+ actor;
511
+ /**
512
+ * **K2 迁移中**: 一次 Run 的显式状态载体 (见 src/agents/run-context.ts)。
513
+ * 入口 (prompt / promptStream / promptWithPivotLoop) 用 createRunContext() 快照一次, 用完即清。
514
+ * **已外置**: `eventSink` (原 currentOnStream) · `abortSignal` (原 currentSignal)。
515
+ * 其余 6 个字段仍以本类实例字段为准, 逐格迁移。
516
+ */
517
+ runCtx = (0, run_context_js_1.createRunContext)();
431
518
  /** Bootstrap SessionStart 拼的 system prompt 片段 (用完即清) */
432
519
  bootstrapAddition = '';
433
520
  /** 2026-08-12 (Task3 认知卸载): 工具选择与认知卸载指南 — 注入 system prompt, 降低"模型不触发 write/edit/read"率. */
@@ -441,12 +528,17 @@ class PiAgentSession {
441
528
  - 每步只选一个最合适的工具, 先读后写, 写完验证 (跑 tsc/vitest 或读回文件).`;
442
529
  /** 当前 prompt 开始时间 (供 Stop hook 计算 durationMs) */
443
530
  promptStartTime = 0;
444
- /** 当前 channel id (由 getAgentForChannel / prompt 4 参注入, 供 hook / log 使用) */
445
- currentChannelId = '';
446
- /** 2026-07-04: 当前 agentId (server.ts 通过 createAgentSession 选项注入), 供 onSessionStart 加载 persona docs */
447
- currentAgentId = '';
448
- /** M2.2 (2026-06-17): 当前轮的用户请求 intent, runReActLoop 拼 systemPrompt 时会读这个 */
449
- currentIntent = 'chitchat';
531
+ /**
532
+ * **K5 步骤③ (会话绑定迁入 Actor)**: 三处绑定 (channelId / agentId / goalBinding) 的本体住进
533
+ * `actor.state`, 实例上只留**绑定前的暂存** (`_channelId` 等), 由 `attachActor()` 收养后清空。
534
+ * 用访问器而不是改调用点: 写入点 (prompt 入口 / Goal 绑定 / snapshot) 分散且语义各异, 逐个改风险大;
535
+ * 访问器让所有读写自动落到 actor (同一个值对象), 调用点零改动。
536
+ * 注意: 这三个字段的**访问数不变** (台账按 "self.currentX" 形态计数) —— 迁的是**所有权**, 不是删访问。
537
+ * 本注释刻意不写出那种带 `this.` 前缀的字面形态: 计数口径只剥 `//` 行注释, 块注释里的同形串会被算进去
538
+ * (曾因此把 currentChannelId 的计数虚增 1, 被门照出)。
539
+ */
540
+ // **K5 步骤⑧ 批次4**: channelId 的访问器已删 —— 本体 `actor.state.channelId`, 直接影响本体。
541
+ // M2.2 intent 已外置到 runCtx.intent (K2); 拼 systemPrompt 时读 this.runCtx.intent
450
542
  /** 2026-08-10: 本轮用户原始输入 (loop-review 任务动词兜底检测用) */
451
543
  currentUserInput = '';
452
544
  /**
@@ -454,7 +546,16 @@ class PiAgentSession {
454
546
  * 之前只有"跑完才写"的轨迹 (trajectory-store, fire-and-forget, 崩了就没有), 没有跨重载可见的
455
547
  * 运行事实, 也没有预算闸门/失速巡检; 这里补的就是那一层。
456
548
  */
457
- currentRunId = '';
549
+ /**
550
+ * **K5 步骤⑥ (Run 身份归属 Actor)**: 本体是 `actor.state.activeRun`。
551
+ * · 未绑定 actor ⇒ 住在这里 (`_runId`, 绑定前的暂存);
552
+ * · 绑定后 ⇒ 读写全落 actor (同一个值), 由 `attachActor()` 收养绑定前的值。
553
+ * 语义: "本会话当前活跃的 Run" —— 一个会话身份同时只有一个活跃 Run ✓ 与 Actor 的粒度一致。
554
+ * 注意: 访问数**不变** —— 迁的是**所有权**不是删访问 (与步骤③同理)。
555
+ * 本注释刻意不写出"带 self. 前缀 + 该字段名"的那种字面形态: K2 的计数只剥 `//` 行注释,
556
+ * 块注释里的同形串会被算进去 (同类踩过三次, 每次都被门照出)。
557
+ */
558
+ // **K5 步骤⑧ 批次5 (收尾)**: currentRunId 访问器已删 —— Run 身份的本体是 `actor.state.activeRun`, 直接读写本体。
458
559
  /** 上一次运行的 runId (收尾不清空; 见 getLastRunId) */
459
560
  lastRunId = '';
460
561
  runSurface = 'cli';
@@ -471,14 +572,14 @@ class PiAgentSession {
471
572
  currentIntentHint = '';
472
573
  /**
473
574
  * 算 judgment 注入门: 失败静默, 不阻塞主对话
474
- * 期间通过 currentOnStream 广播 phase 事件, 前端可显示 "正在检索判断力..." 状态
575
+ * 期间通过 runCtx.eventSink 广播 phase 事件, 前端可显示 "正在检索判断力..." 状态
475
576
  * 调用方负责用完即清 (judgmentGateAddition='')
476
577
  */
477
578
  async computeJudgmentGate(input) {
478
579
  const safePhase = (phase, extra = {}) => {
479
580
  try {
480
- if (this.currentOnStream) {
481
- this.currentOnStream({ type: 'phase', phase, ...extra, content: '' });
581
+ if (this.runCtx.eventSink) {
582
+ this.runCtx.eventSink({ type: 'phase', phase, ...extra, content: '' });
482
583
  }
483
584
  }
484
585
  catch { /* 静默 */ }
@@ -520,19 +621,23 @@ class PiAgentSession {
520
621
  this.judgmentGateNegativeUsedIds = [];
521
622
  }
522
623
  constructor(config) {
624
+ // **K5 步骤⑧**: actor 从出生就在 —— 工厂通常在**构造前**就把身份 actor 注进来 (`config.actor`);
625
+ // 没有身份时用一份**私有 actor** (不注册 ⇒ 谁也拿不到, 隔离性优先)。
626
+ // ⇒ 实例侧不再需要"绑定前暂存"字段 (那些字段本步删除)。
627
+ this.actor = config.actor ?? (0, channel_actor_js_1.createPrivateActor)();
523
628
  this.cwd = config.cwd;
524
629
  this.peerId = config.peerId || 'local';
525
630
  this.identity = config.identityDoc || this.createDefaultIdentity();
526
631
  this.minimaxAvailable = this.checkMinimax();
527
632
  // 2026-07-04: 透传 agentId (server.ts 通过 createAgentSession 选项注入)
528
- this.currentAgentId = config.agentId || '';
633
+ this.actor.state.agentId = config.agentId || '';
529
634
  // 2026-10-01 **身份自愈** (用户实测: get_identity 返回 "DID: " 空 + 各 channel 名字串台):
530
635
  // 工具上下文里的身份来自 this.identity, 而它可能是 config.identityDoc 与 session 实例身份的
531
636
  // 合并结果 —— **空 did 会盖掉真 did** ✗; 名字也可能缺失或被别处覆盖。
532
637
  // 规矩: 缺 did / 名字时, 按 **currentAgentId** 从真身份生成器补齐 (did:key + 落盘密钥),
533
638
  // 名字优先取该 agent 自己的 persona.json。绝不回落到全局/用户身份名。
534
639
  try {
535
- if (this.currentAgentId) {
640
+ if (this.actor.state.agentId) {
536
641
  const cur = this.identity || {};
537
642
  const curDid = String(cur.did || '');
538
643
  const badDid = !curDid || /^did:(local|pi):/i.test(curDid);
@@ -540,17 +645,17 @@ class PiAgentSession {
540
645
  // 2026-10-01 加固: **did 与本人密钥不符** 也算坏 —— 用户实测 get_identity 返回一个
541
646
  // 数据里根本不存在的 did (按 peerId 当 scope 新造的), 而它非空、非假值 ⇒ 上一版判据漏过 ✗。
542
647
  // 规矩: 只要 currentAgentId 有值, 身份的 did 就必须等于该 agent 自己的密钥 did。
543
- const mine = (0, agent_identity_js_1.loadOrCreateAgentIdentity)(this.currentAgentId);
648
+ const mine = (0, agent_identity_js_1.loadOrCreateAgentIdentity)(this.actor.state.agentId);
544
649
  const mismatch = !!mine?.did && !!curDid && curDid !== mine.did;
545
650
  if (badDid || missingName || mismatch) {
546
651
  if (badDid || mismatch) {
547
- console.warn(`[identity] 身份纠正: ${this.currentAgentId} 的 did ${curDid ? curDid.slice(0, 26) : '(空)'} ⇒ ${mine.did.slice(0, 26)} (以该 agent 自己的密钥为准)`);
652
+ console.warn(`[identity] 身份纠正: ${this.actor.state.agentId} 的 did ${curDid ? curDid.slice(0, 26) : '(空)'} ⇒ ${mine.did.slice(0, 26)} (以该 agent 自己的密钥为准)`);
548
653
  }
549
654
  // 2026-10-01: 同样必须**原地改** —— 这段在 registerTools() **之后**跑, 工具上下文已经按引用
550
655
  // 捕获了 this.identity; 换对象 ⇒ 工具看不到纠正结果 ✗ (同类 bug, 一并修)
551
656
  Object.assign(this.identity, {
552
657
  ...(mine?.did ? { did: mine.did, publicKey: mine.publicKey || cur.publicKey } : {}),
553
- ...(missingName ? { name: (0, channel_identity_js_1.agentPersonaName)(this.currentAgentId) || this.currentAgentId } : {}),
658
+ ...(missingName ? { name: (0, channel_identity_js_1.agentPersonaName)(this.actor.state.agentId) || this.actor.state.agentId } : {}),
554
659
  });
555
660
  }
556
661
  }
@@ -560,9 +665,9 @@ class PiAgentSession {
560
665
  this._sessionStore = config.sessionStore ?? session_store_js_1.sessionStore;
561
666
  this.constraintLayer = new constraint_layer_js_1.ConstraintLayer();
562
667
  this.workflowEngine = new workflow_engine_js_1.WorkflowEngine(this.constraintLayer);
563
- this.sessionManager = new pi_sdk_session_manager_js_2.PiSessionManager(this.identity.did, this.cwd, this.currentAgentId);
668
+ this.sessionManager = new pi_sdk_session_manager_js_2.PiSessionManager(this.identity.did, this.cwd, this.actor.state.agentId);
564
669
  this.agentsManager = new heartbeat_js_1.DiscoveredAgentsManager();
565
- this.usePivotLoop = config.usePivotLoop ?? false;
670
+ this.usePivotLoop = config.usePivotLoop ?? true; // K4-B: 默认 true (K4-B 之前是 false ⇒ CLI 走老 loop, 与 web 分叉)
566
671
  this.pivotLoopConfig = config.pivotLoopConfig;
567
672
  this.initSession();
568
673
  (0, p2p_document_tools_js_1.initDocumentReceiver)();
@@ -646,6 +751,18 @@ class PiAgentSession {
646
751
  */
647
752
  async hydrateMessageHistory(sessionKey, maxMessages) {
648
753
  try {
754
+ // **K5 第 4 步**: 实现体 (load → filter → 截断 → 替换) 已搬进 Actor —— 走邮箱 ⇒ 与 append/persist 串行。
755
+ // 业务侧只交两个**纯回调** (内核因此不必 import 业务模块); 未绑定 actor 的会话走下面原路径 (行为不变)。
756
+ if (this.actor) {
757
+ const n = await this.actor.hydrateHistory({
758
+ load: () => this._sessionStore.loadMessages(sessionKey),
759
+ filter: (loaded) => this._filterToMessage(loaded),
760
+ maxMessages,
761
+ });
762
+ if (n > 0)
763
+ console.log(`[PiAgent] 从 ${sessionKey} 回灌 ${n} 条历史 (经 Channel Actor)`);
764
+ return;
765
+ }
649
766
  const loaded = await this._sessionStore.loadMessages(sessionKey);
650
767
  if (!loaded) {
651
768
  console.log(`[PiAgent] hydrate: 没有 ${sessionKey} 的历史`);
@@ -653,7 +770,7 @@ class PiAgentSession {
653
770
  }
654
771
  const hydrated = this._filterToMessage(loaded).slice(-maxMessages);
655
772
  if (hydrated.length > 0) {
656
- this.messageHistory = hydrated;
773
+ this.replaceHistory(hydrated);
657
774
  console.log(`[PiAgent] 从 ${sessionKey} 回灌 ${hydrated.length} 条历史`);
658
775
  }
659
776
  }
@@ -673,7 +790,10 @@ class PiAgentSession {
673
790
  * 即可获得"重启 / 跨进程接续"的语义.
674
791
  */
675
792
  async saveCurrentSession(key) {
676
- const persisted = this.messageHistory.map((m) => ({
793
+ // **K5 第 4 步**: 取数拍走 Actor 的快照 (走邮箱 ⇒ 与 append 串行, 不会抓到"边写边读"的半截状态);
794
+ // 未绑定 actor 的会话走原路径 (行为不变)。
795
+ const source = this.actor ? await this.actor.historySnapshot() : this.actor.state.messageHistory;
796
+ const persisted = source.map((m) => ({
677
797
  role: m.role,
678
798
  content: m.content,
679
799
  toolCall: m.toolCall,
@@ -690,9 +810,9 @@ class PiAgentSession {
690
810
  * 与 loadSessionKey (构造时读) 不同: 这个是 session 已建好后再读.
691
811
  */
692
812
  async resumeSession(key, maxMessages = 30) {
693
- const before = this.messageHistory.length;
813
+ const before = this.actor.state.messageHistory.length;
694
814
  await this.hydrateMessageHistory(key, maxMessages);
695
- return this.messageHistory.length - before;
815
+ return this.actor.state.messageHistory.length - before;
696
816
  }
697
817
  /**
698
818
  * 2026-06-30: 读历史不修改 messageHistory.
@@ -864,7 +984,7 @@ class PiAgentSession {
864
984
  const { CHANNELS_PATH } = await Promise.resolve().then(() => __importStar(require('../web/server-types.js')));
865
985
  const { loadChannels } = await Promise.resolve().then(() => __importStar(require('../web/server-storage.js')));
866
986
  const channels = await loadChannels();
867
- const ch = channels.find((c) => c.id === this.currentChannelId);
987
+ const ch = channels.find((c) => c.id === this.actor.state.channelId);
868
988
  if (ch && ch.encryptedPrivateKey && ch.encryptedPrivateKeyIv && ch.walletAddress) {
869
989
  return {
870
990
  encryptedPrivateKey: ch.encryptedPrivateKey,
@@ -939,7 +1059,7 @@ class PiAgentSession {
939
1059
  // 2026-10-01: 原先自造 `did:pi:<peerId>` —— **不是有效 DID** (仓里 server.ts 自己都把 did:pi:
940
1060
  // 当"待升级"占位; 用户实测 get_identity 报 did:pi:ch_1785668060213)。
941
1061
  // 改用仓里既有的真身份生成器 (agent-identity.loadOrCreateAgentIdentity, 同步, 产 did:key + 落盘密钥)。
942
- const scope = this.currentAgentId || this.peerId || 'default';
1062
+ const scope = this.actor.state.agentId || this.peerId || 'default';
943
1063
  try {
944
1064
  const real = (0, agent_identity_js_1.loadOrCreateAgentIdentity)(scope);
945
1065
  return {
@@ -998,9 +1118,9 @@ class PiAgentSession {
998
1118
  const did = await resolveUserDid();
999
1119
  const model = this.llmConfig?.model || this.model || '';
1000
1120
  return new TrajectoryRecorder({
1001
- agentId: this.currentAgentId || 'default',
1121
+ agentId: this.actor.state.agentId || 'default',
1002
1122
  input,
1003
- channelId: channelId || this.currentChannelId,
1123
+ channelId: channelId || this.actor.state.channelId,
1004
1124
  did: did || undefined,
1005
1125
  model: typeof model === 'string' && model ? model : undefined,
1006
1126
  });
@@ -1042,8 +1162,8 @@ class PiAgentSession {
1042
1162
  if (gate !== 'ready') {
1043
1163
  const why = `初始化未就绪 (${gate}, 当前阶段 ${state.stage})${state.lastError ? ` — ${state.lastError.message}` : ''}`;
1044
1164
  const hint = (state.actions && state.actions[0]) || '运行 `bolloon setup` 完成初始化';
1045
- this.messageHistory.push({ role: 'user', content: input });
1046
- this.messageHistory.push({ role: 'assistant', content: `[初始化未就绪] ${why}\n${hint}` });
1165
+ this.pushHistory({ role: 'user', content: input });
1166
+ this.pushHistory({ role: 'assistant', content: `[初始化未就绪] ${why}\n${hint}` });
1047
1167
  console.warn(`[PiAgent] 拒绝执行: ${why}`);
1048
1168
  return `[初始化未就绪] ${why}\n${hint}`;
1049
1169
  }
@@ -1051,48 +1171,64 @@ class PiAgentSession {
1051
1171
  catch { /* 门禁自身异常 → 按"不能判定"处理会阻塞一切; 这里只在能读到状态时才拦 */ }
1052
1172
  }
1053
1173
  this.minimaxAvailable = this.checkMinimax();
1054
- this.currentChannelId = options?.channelId ?? this.currentChannelId;
1174
+ this.actor.state.channelId = options?.channelId ?? this.actor.state.channelId;
1055
1175
  // 2026-08-08: 运行轨迹采集 (落盘 + OrbitDB, 失败静默) — 包裹 onStream 收集步骤事件
1056
1176
  const trajRec = await this.createTrajectoryRecorder(input, options?.channelId);
1057
1177
  if (trajRec && options?.onStream) {
1058
1178
  options = { ...options, onStream: this.wrapTrajectoryStream(options.onStream, trajRec) };
1059
1179
  }
1060
- this.messageHistory.push({
1180
+ this.pushHistory({
1061
1181
  role: 'user',
1062
1182
  content: input
1063
1183
  });
1064
- if (!this.minimaxAvailable) {
1184
+ // K9 (2026-10-02): 同 `promptWithPivotLoop` —— **回合入口的可用性判据必须问当前适配器**。
1185
+ // 这类 `!minimaxAvailable` 闸一共 6 处: 1223(prompt)/1466(promptStream)/1714(pivot, 已修)
1186
+ // 是**回合入口** ⇒ 全部改用 `inferenceAvailable()`; 其余三处是 Pi 自己的文档类功能
1187
+ // (`suggestRename`/`summarizeDocument`/`improveDocument`), 不在 `LLMInterface` 面内 ⇒ 如实保留。
1188
+ if (!this.inferenceAvailable()) {
1065
1189
  // 2026-09-16 (2-C.2): fallback **也必须留下 Run 事实**。
1066
1190
  // 旧行为: 直接返回兜底文案, 连 Run 都不建 —— 上层 (独立宿主/Supervisor) 只看到"执行完成但没有 Run",
1067
1191
  // 于是把"什么都没跑"当成一次正常执行 (这正是"agent 跑了但没记录"的老毛病在 fallback 路径的翻版)。
1068
1192
  const response = await this.handleFallback(input);
1069
1193
  try {
1070
- let boundGoalId = this.currentGoalId;
1194
+ let boundGoalId = this.actor.state.goalBinding;
1071
1195
  if (!boundGoalId) {
1072
1196
  const g = await (0, goal_store_js_1.createGoal)({
1073
1197
  objective: this.currentUserInput || input.slice(0, 200),
1074
- channelId: this.currentChannelId || undefined,
1075
- agentId: this.currentAgentId || undefined,
1198
+ channelId: this.actor.state.channelId || undefined,
1199
+ agentId: this.actor.state.agentId || undefined,
1076
1200
  createdBy: this.runSurface,
1077
1201
  });
1078
1202
  boundGoalId = g.goalId;
1079
1203
  }
1080
- this.currentGoalId = boundGoalId;
1081
- const rec = await (0, run_store_js_1.startRun)({
1082
- surface: this.runSurface,
1083
- goal: this.currentUserInput || input.slice(0, 200),
1084
- goalId: boundGoalId,
1085
- channelId: this.currentChannelId || undefined,
1086
- agentId: this.currentAgentId || undefined,
1087
- modelConfig: await this.runModelSnapshot(),
1088
- });
1204
+ this.actor.state.goalBinding = boundGoalId;
1205
+ // K10 ①: 三处写入均经内核端口; 写不进就 assertLifecycleOk 抛 (run-store strict: 写不进 = 这次运行在事实层面不存在)
1206
+ const startOut = await (0, run_lifecycle_js_1.submitRunLifecycle)({
1207
+ op: 'start-run',
1208
+ origin: 'pi-session',
1209
+ payload: {
1210
+ surface: this.runSurface,
1211
+ goal: this.currentUserInput || input.slice(0, 200),
1212
+ goalId: boundGoalId,
1213
+ channelId: this.actor.state.channelId || undefined,
1214
+ agentId: this.actor.state.agentId || undefined,
1215
+ modelConfig: await this.runModelSnapshot(),
1216
+ },
1217
+ }, { startRun: run_store_js_1.startRun });
1218
+ (0, run_lifecycle_js_1.assertLifecycleOk)(startOut);
1219
+ const rec = startOut.result;
1089
1220
  this.lastRunId = rec.runId;
1090
1221
  await (0, goal_store_js_1.attachRun)(boundGoalId, rec.runId).catch(() => null);
1091
- await (0, run_store_js_1.recordStep)(rec.runId, { tool: 'llm', ok: false, error: 'LLM 不可用 (provider 未初始化/无 apiKey) → 走了 fallback' });
1092
- await (0, run_store_js_1.finishRun)(rec.runId, {
1093
- status: 'needs_human',
1094
- error: 'LLM 不可用: provider 未初始化或无 apiKey (fallback 不是执行结果, 需要配置或人工处理)',
1095
- });
1222
+ const stepOut = await (0, run_lifecycle_js_1.submitRunLifecycle)({
1223
+ op: 'record-step',
1224
+ origin: 'pi-session',
1225
+ runId: rec.runId,
1226
+ payload: { tool: 'llm', ok: false, error: 'LLM 不可用 (provider 未初始化/无 apiKey) → 走了 fallback' },
1227
+ }, { recordStep: run_store_js_1.recordStep });
1228
+ (0, run_lifecycle_js_1.assertLifecycleOk)(stepOut);
1229
+ // 收尾漏斗门的登记项按**单行**匹配 (seams.ts:564 `ls.find(...)`) ⇒ 这次调用刻意写成一行, 好让登记项能钉住它
1230
+ const finOut = await (0, run_lifecycle_js_1.submitRunLifecycle)({ op: 'finish-run', origin: 'pi-session', runId: rec.runId, payload: { status: 'needs_human', error: 'LLM 不可用: provider 未初始化或无 apiKey (fallback 不是执行结果, 需要配置或人工处理)' } }, { finishRun: run_store_js_1.finishRun });
1231
+ (0, run_lifecycle_js_1.assertLifecycleOk)(finOut);
1096
1232
  // ★ M0: "工具/权限不可用"也是一条终止路径 → 同样进收尾漏斗 (规则 ④)。
1097
1233
  const wiring = await Promise.resolve().then(() => __importStar(require('./goal-flywheel-wiring.js')));
1098
1234
  const closed = await wiring.closeRunOnce({
@@ -1122,25 +1258,27 @@ class PiAgentSession {
1122
1258
  catch (err) {
1123
1259
  await (0, run_store_js_1.recordDegradation)({ kind: 'core', op: 'pi-sdk.fallbackRun', runId: this.lastRunId || '', message: String(err?.message || err).slice(0, 160) }).catch(() => { });
1124
1260
  }
1125
- this.messageHistory.push({ role: 'assistant', content: response });
1261
+ this.pushHistory({ role: 'assistant', content: response });
1126
1262
  this.reportUsageToContextManager();
1127
1263
  this.finishTrajectory(trajRec, response);
1128
1264
  return response;
1129
1265
  }
1130
1266
  // P0 注入门
1131
- this.currentSignal = options?.signal ?? null;
1132
- this.currentOnStream = options?.onStream ?? null;
1267
+ // K2: eventSink + abortSignal 已外置, 入口一次建好本轮 Context。
1268
+ // 其余字段**不在这里抄一份** —— 抄写会新增对旧实例字段的读, 违反方向判据
1269
+ // 「新层出现后旧写口调用点数只许不变或减少」。逐字段迁移时再各自搬进来。
1270
+ this.runCtx = this.seedRunContext({ eventSink: options?.onStream ?? null, abortSignal: options?.signal ?? null });
1133
1271
  await this.computeJudgmentGate(input);
1134
1272
  // M2.2 (2026-06-17): intent 分类 — prompt() 路径也跑 (跟 promptStream 对齐)
1135
1273
  try {
1136
1274
  const { classifyIntent, intentHint } = await Promise.resolve().then(() => __importStar(require('./intent-classifier.js')));
1137
- this.currentIntent = classifyIntent(input);
1138
- this.currentIntentHint = intentHint(this.currentIntent);
1275
+ this.runCtx.intent = classifyIntent(input);
1276
+ this.currentIntentHint = intentHint(this.runCtx.intent);
1139
1277
  this.currentUserInput = input;
1140
1278
  }
1141
1279
  catch (err) {
1142
1280
  console.warn('[PiAgent] classifyIntent in prompt() failed:', err);
1143
- this.currentIntent = 'chitchat';
1281
+ this.runCtx.intent = 'chitchat';
1144
1282
  this.currentIntentHint = '';
1145
1283
  }
1146
1284
  // P2: 解析当前 permission mode
@@ -1152,43 +1290,22 @@ class PiAgentSession {
1152
1290
  console.warn('[PiAgent] resolvePermissionMode failed (non-fatal):', err);
1153
1291
  this.currentPermissionMode = 'default';
1154
1292
  }
1155
- // M3.1 (2026-06-17): 跟 promptStream 一样, usePivotLoop 时走 pivotLoop 路径
1156
- // 之前 prompt() 永远跑老 runReActLoop, CLI/web 行为不一致
1157
- if (this.usePivotLoop) {
1158
- try {
1159
- const lr = await this.promptWithPivotLoop(input, undefined, options?.channelId);
1160
- this.finishTrajectory(trajRec, lr.response || '');
1161
- return lr.response || '';
1162
- }
1163
- finally {
1164
- if (this.judgmentGateUsedIds.length > 0) {
1165
- (0, injection_gate_js_1.recordJudgmentUsage)(this.judgmentGateUsedIds, { userInput: input }).catch((err) => console.warn('[PiAgent] recordJudgmentUsage failed:', err));
1166
- }
1167
- this.clearJudgmentGate();
1168
- this.currentSignal = null;
1169
- this.currentOnStream = null;
1170
- this.reportUsageToContextManager();
1171
- }
1172
- }
1293
+ // K4-B (2026-10-02): **这条路已经只有一条** —— 原先这里有个 `if (this.usePivotLoop) { ... return ... }`
1294
+ // 的提前返回分支, 它的 finally 只是**子集** (只记正极性用法), 而且**少跑了**:
1295
+ // 回合后复盘 `runExperienceReview` · `monitorAfterReply`/`onStop` · `finishTrajectory(reply, ok|error)`
1296
+ // · 负极性用法记账 · `bootstrapAddition`/`promptStartTime` 清理。现在统一走下面那条完整收尾。
1173
1297
  try {
1174
- // 2026-06-16: runReActLoop 现在返回 { reply, aiFailed, aiFailureReason } — 这里只需 reply 字符串
1175
- const loopResult = await this.runReActLoop(this.currentOnStream ?? undefined, options?.signal);
1176
- // 2026-10-01: **本回合改过 TS ⇒ 收尾自动类型检查** (一轮一次) —— 复用已有的 tsc_check 工具
1177
- if ((0, code_write_gate_js_1.decideTypecheck)(this.tsTouchedThisTurn, this.typecheckRanThisTurn)) {
1178
- this.typecheckRanThisTurn = true;
1179
- try {
1180
- const tscTool = this.tools.get('tsc_check');
1181
- if (tscTool?.execute) {
1182
- const r = await tscTool.execute({});
1183
- const line = (0, code_write_gate_js_1.formatTypecheckResult)(r?.success !== false && !/error TS\d+/.test(String(r?.output || '')), String(r?.output || ''));
1184
- this.currentOnStream?.({ type: 'status', content: `🔎 ${line}`, tool: 'system' });
1185
- }
1186
- }
1187
- catch (e) {
1188
- this.currentOnStream?.({ type: 'status', content: `🔎 类型检查没能跑起来: ${String(e?.message || e).slice(0, 100)} (改动已落盘, 记得自己跑一次)`, tool: 'system' });
1189
- }
1190
- this.tsTouchedThisTurn = [];
1191
- }
1298
+ // K4-B (2026-10-02): **合并两套 loop ⇒ 只剩 pivot 这一套** (老 `runReActLoop` 已删除)。
1299
+ // 这里保留一个与原签名同形的 `loopResult`, 让下面那段**完整收尾**(自检 → 复盘 → 轨迹 → 记账)
1300
+ // 原样继续工作, 不搬家。
1301
+ const lr = await this.promptWithPivotLoop(input, undefined, options?.channelId);
1302
+ const loopResult = {
1303
+ reply: lr.response || '',
1304
+ aiFailed: !lr.success,
1305
+ aiFailureReason: String(lr.exitReason || ''),
1306
+ };
1307
+ // K4-B (2026-10-02): 收尾自检抽成方法, 两条 loop 路径都调 (原先只在老 loop 之后 ⇒ 生产 pivot 路径从没跑过)
1308
+ await this.runTurnEndTypecheck();
1192
1309
  // 2026-10-01: **回合后自审 (纯旁路)** —— 沉淀可复用经验, 对应"结束后沉淀"那一段。
1193
1310
  // 纪律: fire-and-forget (不阻塞主回合) · 只写 ~/.bolloon/experience/ (**不碰主对话/prompt 缓存**)
1194
1311
  // · 节流 (默认 10 分钟/agent) · 失败只记一行日志, 绝不影响本回合结果。
@@ -1217,7 +1334,7 @@ class PiAgentSession {
1217
1334
  }
1218
1335
  catch { /* 落日志失败不影响主流程 */ }
1219
1336
  try {
1220
- this.currentOnStream?.({ type: 'status', content: `📚 复盘: ${m}`, tool: 'system' });
1337
+ this.runCtx.eventSink?.({ type: 'status', content: `📚 复盘: ${m}`, tool: 'system' });
1221
1338
  }
1222
1339
  catch { /* 上屏失败不打断 */ }
1223
1340
  };
@@ -1284,16 +1401,15 @@ class PiAgentSession {
1284
1401
  (0, injection_gate_js_1.recordJudgmentUsage)(this.judgmentGateNegativeUsedIds, { userInput: input, polarity: 'negative' }).catch((err) => console.warn('[PiAgent] recordJudgmentUsage (negative) failed:', err));
1285
1402
  }
1286
1403
  this.clearJudgmentGate();
1287
- this.currentSignal = null;
1288
- this.currentOnStream = null;
1404
+ this.runCtx = (0, run_context_js_1.createRunContext)(); // K2: 用完即清 (换新 Context, 不继承残留);
1289
1405
  this.reportUsageToContextManager();
1290
1406
  }
1291
1407
  }
1292
1408
  async promptStream(input, onStream, signal, channelId) {
1293
1409
  console.log(`[PiAgent.promptStream] ENTRY, channelId=${channelId}, input chars=${input.length}`);
1294
1410
  this.minimaxAvailable = this.checkMinimax();
1295
- console.log(`[PiAgent.promptStream] minimaxAvailable=${this.minimaxAvailable}`);
1296
- this.currentChannelId = channelId ?? this.currentChannelId;
1411
+ console.log(`[PiAgent.promptStream] minimaxAvailable=${this.minimaxAvailable} nativeAdapter=${process.env.BOLLOON_NATIVE_ADAPTER === '1'} usable=${this.inferenceAvailable()}`);
1412
+ this.actor.state.channelId = channelId ?? this.actor.state.channelId;
1297
1413
  // 2026-08-08: 运行轨迹采集 (落盘 + OrbitDB, 失败静默) — 包裹 onStream 收集步骤事件
1298
1414
  const trajRec = await this.createTrajectoryRecorder(input, channelId);
1299
1415
  if (trajRec)
@@ -1306,7 +1422,7 @@ class PiAgentSession {
1306
1422
  const userText = markerMatch ? markerMatch[1].trim() : input;
1307
1423
  const contextHint = markerMatch ? input.replace(markerMatch[0], '').trim() : '';
1308
1424
  console.log(`[PiAgent.promptStream] marker matched=${!!markerMatch}, userText chars=${userText.length}, contextHint chars=${contextHint.length}`);
1309
- this.messageHistory.push({
1425
+ this.pushHistory({
1310
1426
  role: 'user',
1311
1427
  content: userText
1312
1428
  });
@@ -1316,38 +1432,40 @@ class PiAgentSession {
1316
1432
  // 让 agent 能"回忆起"之前 session 的记忆 (自动获取之前 session), 而非只靠启动时批量压缩.
1317
1433
  try {
1318
1434
  const { recallMemory } = await Promise.resolve().then(() => __importStar(require('./memory-recall.js')));
1319
- const recalled = await recallMemory({ query: userText, agentId: this.currentAgentId || this.peerId || '' });
1435
+ const recalled = await recallMemory({ query: userText, agentId: this.actor.state.agentId || this.peerId || '' });
1320
1436
  if (recalled) {
1321
1437
  this.contextHintAddition = [this.contextHintAddition, recalled].filter(Boolean).join('\n\n');
1322
1438
  }
1323
1439
  }
1324
1440
  catch { /* 记忆召回失败静默 (增强层) */ }
1325
1441
  onStream({ type: 'thinking', content: '🤔 开始思考...' });
1326
- if (!this.minimaxAvailable) {
1442
+ // K9: 流式入口同样问**当前适配器** (web/手机端就走这条路 —— 只修 pivot 那道闸时,
1443
+ // CLI 通了、web 仍然静默空回复, 现象是"适配器被选中但这条路一次没跑")。
1444
+ if (!this.inferenceAvailable()) {
1327
1445
  const response = await this.handleFallback(userText);
1328
- this.messageHistory.push({ role: 'assistant', content: response });
1446
+ this.pushHistory({ role: 'assistant', content: response });
1329
1447
  onStream({ type: 'done', content: '' });
1330
1448
  this.reportUsageToContextManager();
1331
1449
  this.finishTrajectory(trajRec, response);
1332
1450
  return response;
1333
1451
  }
1334
- // P0 注入门: 缓存 onStream + signal, computeJudgmentGate 用 currentOnStream 广播 phase
1335
- this.currentOnStream = onStream;
1336
- this.currentSignal = signal ?? null;
1452
+ // P0 注入门: 缓存 onStream + signal, computeJudgmentGate 用 runCtx.eventSink 广播 phase
1453
+ // K2: eventSink + abortSignal 一起建进本轮 Context (只搬已外置的字段, 不抄未迁移的)
1454
+ this.runCtx = this.seedRunContext({ eventSink: onStream, abortSignal: signal ?? null });
1337
1455
  await this.computeJudgmentGate(userText);
1338
1456
  // M2.2 (2026-06-17): intent 分类 — 0 LLM 成本, 5 行 keyword 匹配
1339
1457
  try {
1340
1458
  const { classifyIntent, intentHint } = await Promise.resolve().then(() => __importStar(require('./intent-classifier.js')));
1341
- this.currentIntent = classifyIntent(userText);
1342
- this.currentIntentHint = intentHint(this.currentIntent);
1459
+ this.runCtx.intent = classifyIntent(userText);
1460
+ this.currentIntentHint = intentHint(this.runCtx.intent);
1343
1461
  this.currentUserInput = userText;
1344
- if (this.currentIntent !== 'chitchat') {
1345
- onStream({ type: 'phase', phase: 'intent_classified', detail: this.currentIntent, content: '' });
1462
+ if (this.runCtx.intent !== 'chitchat') {
1463
+ onStream({ type: 'phase', phase: 'intent_classified', detail: this.runCtx.intent, content: '' });
1346
1464
  }
1347
1465
  }
1348
1466
  catch (err) {
1349
1467
  console.warn('[PiAgent] classifyIntent failed (non-fatal):', err);
1350
- this.currentIntent = 'chitchat';
1468
+ this.runCtx.intent = 'chitchat';
1351
1469
  this.currentIntentHint = '';
1352
1470
  }
1353
1471
  // P1.1: 异步跑 Auto-Compact (LLM 摘要, 仅在 budget 超限时触发, 失败静默)
@@ -1364,8 +1482,8 @@ class PiAgentSession {
1364
1482
  let bootstrapAddition = '';
1365
1483
  try {
1366
1484
  const ss = await (0, human_value_pipeline_js_1.onSessionStart)({
1367
- channelId: this.currentChannelId || undefined,
1368
- agentId: this.currentAgentId || undefined,
1485
+ channelId: this.actor.state.channelId || undefined,
1486
+ agentId: this.actor.state.agentId || undefined,
1369
1487
  });
1370
1488
  bootstrapAddition = ss.systemAddition || '';
1371
1489
  }
@@ -1374,10 +1492,10 @@ class PiAgentSession {
1374
1492
  }
1375
1493
  // 2026-07-07 P1-B: 注入最近 5 条项目事件日志 (L2) — 让 LLM 知道项目状态/feature 变化
1376
1494
  // 失败静默, append 到 bootstrapAddition 末尾 (超 800 字截断)
1377
- if (this.currentChannelId) {
1495
+ if (this.actor.state.channelId) {
1378
1496
  try {
1379
1497
  const { getRecentEvents } = await Promise.resolve().then(() => __importStar(require('../bootstrap/event-log.js')));
1380
- const events = await getRecentEvents(this.currentChannelId, 5);
1498
+ const events = await getRecentEvents(this.actor.state.channelId, 5);
1381
1499
  if (events.length > 0) {
1382
1500
  const eventBlock = [
1383
1501
  '## 最近项目事件 (最近 5 条, 倒序)',
@@ -1392,7 +1510,7 @@ class PiAgentSession {
1392
1510
  // 2026-07-07 P2-C: 注入项目当前状态 (L3) — 目标/约束/待办/已完成
1393
1511
  try {
1394
1512
  const { readState, formatStateForPrompt } = await Promise.resolve().then(() => __importStar(require('../bootstrap/project-state.js')));
1395
- const state = await readState({ channelId: this.currentChannelId });
1513
+ const state = await readState({ channelId: this.actor.state.channelId });
1396
1514
  const stateText = formatStateForPrompt(state);
1397
1515
  if (stateText) {
1398
1516
  bootstrapAddition = (bootstrapAddition + '\n\n' + stateText).slice(-2500);
@@ -1404,7 +1522,7 @@ class PiAgentSession {
1404
1522
  // 2026-07-07 P2-C: 向量检索 top-3 (L4) — 按当前 channelId + userText 找历史相关片段
1405
1523
  try {
1406
1524
  const { searchIndex } = await Promise.resolve().then(() => __importStar(require('../bootstrap/vector-index.js')));
1407
- const indexName = `channel-${this.currentChannelId}`;
1525
+ const indexName = `channel-${this.actor.state.channelId}`;
1408
1526
  const results = await searchIndex({
1409
1527
  indexName,
1410
1528
  query: userText,
@@ -1433,53 +1551,11 @@ class PiAgentSession {
1433
1551
  this.currentPermissionMode = 'default';
1434
1552
  }
1435
1553
  this.promptStartTime = Date.now();
1436
- // M3.1 (2026-06-17): 走 WorkflowPivotLoop (usePivotLoop: true)
1437
- // pivot loop 自带 quality scoring / 30 iter cap / complexity analysis — 比老 runReActLoop 鲁棒
1438
- if (this.usePivotLoop) {
1439
- let pivotResult = '';
1440
- try {
1441
- const lr = await this.promptWithPivotLoop(userText, undefined, channelId);
1442
- pivotResult = lr.response || '';
1443
- onStream({ type: 'done', content: '' });
1444
- }
1445
- catch (err) {
1446
- if (signal?.aborted || err?.name === 'AbortError') {
1447
- console.log(`[chat] pivot aborted channel=${channelId}`);
1448
- }
1449
- else {
1450
- console.error(`[chat] pivot 失败 channel=${channelId}:`, err);
1451
- pivotResult = `[错误: pivot loop 失败] ${String(err?.message || err).slice(0, 300)}`;
1452
- try {
1453
- onStream({ type: 'error', content: pivotResult, tool: 'system' });
1454
- }
1455
- catch { }
1456
- }
1457
- }
1458
- finally {
1459
- if (this.judgmentGateUsedIds.length > 0) {
1460
- try {
1461
- onStream({ type: 'used_judgments', usedIds: this.judgmentGateUsedIds, content: '' });
1462
- }
1463
- catch { }
1464
- }
1465
- (0, monitor_gate_js_1.monitorAfterReply)(userText, pivotResult);
1466
- const stopStartTime = this.promptStartTime || Date.now();
1467
- (0, human_value_pipeline_js_1.onStop)({
1468
- channelId: this.currentChannelId || 'unknown',
1469
- durationMs: Date.now() - stopStartTime,
1470
- usedJudgmentIds: [...this.judgmentGateUsedIds],
1471
- }).catch((err) => console.warn('[PiAgent] onStop failed:', err));
1472
- this.clearJudgmentGate();
1473
- this.currentOnStream = null;
1474
- this.currentSignal = null;
1475
- this.bootstrapAddition = '';
1476
- this.contextHintAddition = '';
1477
- this.promptStartTime = 0;
1478
- this.reportUsageToContextManager();
1479
- }
1480
- this.finishTrajectory(trajRec, pivotResult);
1481
- return pivotResult;
1482
- }
1554
+ // K4-B (2026-10-02): **流式路径也只剩一条** —— 原先这里有个 pivot 提前 return 分支,
1555
+ // 它自己的 finally 是**子集** (且少了 `contextHintAddition` 清理)。现在统一走下面的
1556
+ // "外层重试 + 完整收尾", 而重试体里跑的是 pivot (老 `runReActLoop` 已删除)。
1557
+ // 重试语义**刻意保留**: 临时网络抖动/配额瞬时超限可自愈, 且状态文案被
1558
+ // `reply-hygiene.test.ts` 与 `web-loop-status-bar.spec.ts` 两个用例锁着。
1483
1559
  // 2026-06-16: loop 自动重试 — runReActLoop 内部遇到 [AI 服务调用失败] sentinel 时,
1484
1560
  // 会设 aiFailed=true 并提前 break. 这里在外层重跑整个 loop (不是单次 LLM 调用),
1485
1561
  // 临时网络抖动 / 配额瞬时超限可自愈. 最多 3 次, 指数退避 1s/2s/4s.
@@ -1490,19 +1566,20 @@ class PiAgentSession {
1490
1566
  let lastAiFailureReason = '';
1491
1567
  while (attempt <= MAX_LOOP_RETRIES) {
1492
1568
  try {
1493
- const loopResult = await this.runReActLoop(onStream, signal);
1494
- result = loopResult.reply;
1569
+ const lr = await this.promptWithPivotLoop(userText, undefined, channelId);
1570
+ result = lr.response || '';
1571
+ // K4-B: 收尾自检在**这条**路径上也要跑 (原先只有 pivot 分支跑, 而 pivot 分支已被合并掉)
1572
+ await this.runTurnEndTypecheck();
1495
1573
  // 持久化失败不重试 (写不进去就是写不进去): 直接按本次结果收尾
1496
1574
  if (this.runPersistenceBlocked)
1497
1575
  break;
1498
- if (!loopResult.aiFailed)
1576
+ if (lr.success)
1499
1577
  break; // 正常完成, 退出 retry 循环
1500
- lastAiFailureReason = loopResult.aiFailureReason || 'AI 调用失败';
1578
+ lastAiFailureReason = String(lr.exitReason || 'AI 调用失败');
1501
1579
  }
1502
1580
  catch (err) {
1503
1581
  // abort 失败: 视作"已中断", 抛错让上层用 partial 兜底
1504
- this.currentOnStream = null;
1505
- this.currentSignal = null;
1582
+ this.runCtx = (0, run_context_js_1.createRunContext)(); // K2: 用完即清 (换新 Context, 不继承残留);
1506
1583
  throw err;
1507
1584
  }
1508
1585
  attempt++;
@@ -1540,8 +1617,8 @@ class PiAgentSession {
1540
1617
  // 因为 messageHistory.push({role:'user'}) 在 promptStream 顶部已经做过, 重跑 runReActLoop 不会重复 push,
1541
1618
  // 但 assistant 失败那条也别留 (留了会污染下一轮 LLM context).
1542
1619
  // 简化: 重试前 pop 一次 assistant (如果最后一条是 assistant)
1543
- if (this.messageHistory.length > 0 && this.messageHistory[this.messageHistory.length - 1].role === 'assistant') {
1544
- this.messageHistory.pop();
1620
+ if (this.actor.state.messageHistory.length > 0 && this.actor.state.messageHistory[this.actor.state.messageHistory.length - 1].role === 'assistant') {
1621
+ this.popHistory();
1545
1622
  }
1546
1623
  }
1547
1624
  onStream({ type: 'done', content: '' });
@@ -1559,24 +1636,130 @@ class PiAgentSession {
1559
1636
  // Bootstrap Stop hook: fire-and-forget 写本次 session 摘要
1560
1637
  const stopStartTime = this.promptStartTime || Date.now();
1561
1638
  (0, human_value_pipeline_js_1.onStop)({
1562
- channelId: this.currentChannelId || 'unknown',
1639
+ channelId: this.actor.state.channelId || 'unknown',
1563
1640
  durationMs: Date.now() - stopStartTime,
1564
1641
  usedJudgmentIds: [...this.judgmentGateUsedIds],
1565
1642
  }).catch((err) => console.warn('[PiAgent] onStop failed:', err));
1566
1643
  // 用完即清, 避免污染下一轮
1567
1644
  this.clearJudgmentGate();
1568
- this.currentOnStream = null;
1645
+ this.runCtx = (0, run_context_js_1.createRunContext)(); // K2: 用完即清 (换新 Context, 不继承残留);
1569
1646
  this.reportUsageToContextManager();
1570
- this.currentSignal = null;
1571
1647
  this.bootstrapAddition = '';
1648
+ this.contextHintAddition = ''; // K4-B: 与被合并掉的那条分支对齐 (它清了这条, 老尾段漏了)
1572
1649
  this.promptStartTime = 0;
1573
1650
  // 2026-08-08: 轨迹收尾 — 落盘 + OrbitDB (失败静默, 不阻塞回复)
1574
1651
  this.finishTrajectory(trajRec, result);
1575
1652
  return result;
1576
1653
  }
1654
+ /**
1655
+ * K9 (2026-10-02): 推理适配器的**唯一选择点** —— "用哪个适配器"只在这里决定一次。
1656
+ *
1657
+ * `BOLLOON_NATIVE_ADAPTER=1` ⇒ 用**非 Pi** 的 native 适配器 (`src/llm/native-adapter.ts`,
1658
+ * 源码里不 import 任何 Pi 模块, 只用 fetch 直连 OpenAI 兼容端点);
1659
+ * 否则 ⇒ Pi 侧模型 (原行为, 默认不变)。
1660
+ *
1661
+ * 关键: 上层 (pivot loop / Harness 门 / Run / Channel) **一行都不用改** ——
1662
+ * 这正是 K10 撤换判据里「**出现第二个推理适配器能过同一套门**」要证的东西。
1663
+ */
1664
+ /** K9: 当前适配器是否可用 (native 开关打开且拿到 key ⇒ 用它自己的判据, 不再借 Pi 的) */
1665
+ inferenceAvailable() {
1666
+ if (process.env.BOLLOON_NATIVE_ADAPTER === '1' && (0, native_adapter_js_1.nativeAdapterFromEnv)())
1667
+ return true;
1668
+ return this.minimaxAvailable;
1669
+ }
1670
+ // ── K10 ②: 内核模型运行时的接缝 ────────────────────────────────────────────
1671
+ /** 进程内的内核运行时 (懒建)。传输由 `openPiConnection` 注入 ⇒ 真传输仍是 Pi 的客户端 */
1672
+ runtimeSingleton;
1673
+ kernelModelRuntime() {
1674
+ if (!this.runtimeSingleton) {
1675
+ this.runtimeSingleton = new model_runtime_js_1.ModelRuntime({ openConnection: async (snapshot) => this.openPiConnection(snapshot) }, 30_000, 4);
1676
+ }
1677
+ return this.runtimeSingleton;
1678
+ }
1679
+ /** 端口实现: "开一条连接" = 拿 Pi 的 LLM 客户端 (进程级单例, 由 installRuntime 装配) */
1680
+ async openPiConnection(snapshot) {
1681
+ const client = (0, index_js_1.getMinimax)();
1682
+ return {
1683
+ id: `${snapshot.provider}:${snapshot.model}`,
1684
+ call: async (req) => {
1685
+ try {
1686
+ const raw = await client.chat(req.context, req.systemPrompt, req.signal, req.tools, req.purpose, req.source);
1687
+ return { ok: true, provider: snapshot.provider, raw };
1688
+ }
1689
+ catch (err) {
1690
+ return { ok: false, provider: snapshot.provider, error: String(err?.message ?? err).slice(0, 300) };
1691
+ }
1692
+ },
1693
+ // Pi 客户端是**进程级单例** ⇒ 不随连接关闭 (连接池负责复用/计数)
1694
+ close: async () => { },
1695
+ };
1696
+ }
1697
+ /**
1698
+ * K10 ②: 用内核运行时取一个**租约**并适配成 `LLMInterface`。
1699
+ * 拿不到有效快照 / acquire 失败 ⇒ 返回 null ⇒ 调用方**如实记**并回落 Pi 直连 (不假装走了内核)。
1700
+ */
1701
+ async kernelLeaseAdapter() {
1702
+ let snapshot = null;
1703
+ try {
1704
+ const eff = await (0, model_selection_js_1.captureRunModelConfig)();
1705
+ snapshot = (0, model_runtime_js_1.snapshotFromSelection)({ provider: eff.provider, model: eff.model, baseUrl: eff.baseUrl });
1706
+ }
1707
+ catch {
1708
+ return null;
1709
+ }
1710
+ if (!snapshot)
1711
+ return null;
1712
+ // ⚠️ 踩过的坑 (2026-10-02): 一开始在**适配器创建时**取一个租约、每次 chat 后 `lease.release()`
1713
+ // ⇒ 第二次 chat 撞上「租约已归还」(model-runtime.ts:278 的守卫) ⇒ 适配器抛错 ⇒ 回合在第 2 轮就死
1714
+ // (实测: k7-tsc-tool-e2e 允许路/拒绝路判红, 而强制 Pi 直连全绿)。
1715
+ // 正确姿势: **每次调用现取租约** (连接由池复用, 取租约几乎零成本), 用完立刻归还。
1716
+ const runtime = this.kernelModelRuntime();
1717
+ return {
1718
+ chat: async (context, systemPrompt, signal, tools, purpose, source) => {
1719
+ let lease;
1720
+ try {
1721
+ lease = await runtime.acquire(snapshot);
1722
+ }
1723
+ catch (err) {
1724
+ throw new Error(`[kernel-model-runtime] acquire 失败: ${String(err?.message ?? err).slice(0, 160)}`);
1725
+ }
1726
+ try {
1727
+ const res = await lease.call({ context, systemPrompt, signal, tools, purpose, source }, { signal });
1728
+ if (!res.ok)
1729
+ throw new Error(res.error || '内核模型调用失败');
1730
+ return res.raw;
1731
+ }
1732
+ finally {
1733
+ lease.release(); // 归还租约 (连接由池管理, 不关)
1734
+ }
1735
+ },
1736
+ };
1737
+ }
1738
+ async inferenceAdapter() {
1739
+ if (process.env.BOLLOON_NATIVE_ADAPTER === '1') {
1740
+ const a = (0, native_adapter_js_1.nativeAdapterFromEnv)();
1741
+ if (a) {
1742
+ console.log(`[PiAgent] 推理适配器 = ${a.providerId} (非 Pi: fetch 直连, 不 import Pi)`);
1743
+ return a;
1744
+ }
1745
+ console.warn('[PiAgent] BOLLOON_NATIVE_ADAPTER=1 但没拿到 key ⇒ 回落 Pi 适配器 (不假装切换成功)');
1746
+ }
1747
+ // K10 ②: 先向**内核运行时**取租约 (只读 `acquire(snapshot)`) —— 超时/取消/退避/熔断/回退/记账由内核负责;
1748
+ // 传输仍是 Pi 客户端 (端口注入) ⇒ 行为不变。取不到就**如实记**并回落 Pi 直连 (不假装走了内核)。
1749
+ const leased = await this.kernelLeaseAdapter();
1750
+ if (leased) {
1751
+ console.log('[PiAgent] 推理适配器 = kernel-model-runtime (只读 acquire 租约)');
1752
+ return leased;
1753
+ }
1754
+ console.warn('[PiAgent] 内核租约不可用 ⇒ 回落 Pi 直连 (如实记, 不假装走了内核)');
1755
+ return (0, index_js_1.getMinimax)();
1756
+ }
1577
1757
  async promptWithPivotLoop(input, config, channelId) {
1578
- this.currentChannelId = channelId ?? this.currentChannelId;
1579
- if (!this.minimaxAvailable) {
1758
+ this.actor.state.channelId = channelId ?? this.actor.state.channelId;
1759
+ // K9 (2026-10-02): **可用性判据必须问"当前适配器"**, 不能无条件问 Pi ——
1760
+ // 原先这里是 `if (!this.minimaxAvailable)`, 于是"换成第二个(非 Pi)适配器"仍然被 **Pi 的可用性**卡死
1761
+ // (实跑现象: 适配器被选中、但 loop 一次都没跑、回复为空)。这类"隐式仍依赖 Pi"的点正是 K10 要清的。
1762
+ if (!this.inferenceAvailable()) {
1580
1763
  const response = await this.handleFallback(input);
1581
1764
  return {
1582
1765
  success: false,
@@ -1596,8 +1779,100 @@ class PiAgentSession {
1596
1779
  }
1597
1780
  };
1598
1781
  }
1599
- const llm = (0, index_js_1.getMinimax)();
1600
- const loopConfig = config || this.pivotLoopConfig || (0, workflow_pivot_loop_js_1.createDefaultPivotConfig)();
1782
+ const llm = await this.inferenceAdapter(); // K10 ②: 可能要向内核运行时取租约 (异步)
1783
+ // K4-B (2026-10-02): **每次模型调用也要过门面** —— `beforeModelCall`/`afterModelCall` 原先**只**由老
1784
+ // `runReActLoop` 调用; 老 loop 删除后全仓零调用者, 而 pivot (生产: web/CLI 现在都走它) **从来没调过**
1785
+ // ⇒ 模型调用的前后留痕一直是断的。这里把门面包在 `llm` **外面** ⇒ pivot 内部一行不用改。
1786
+ // ⚠️ 踩过的坑 (2026-10-02, 当场复现): 一开始把 `llm` 换成**纯函数**包一层 ⇒ 第 1 轮就空回复。
1787
+ // 原因: pivot 调的是 **`llm.chat(...)`** (对象方法), 不是 `llm(...)` ⇒ 换掉对象等于把 `.chat` 抹了。
1788
+ // 正确做法: 以**原型继承**造一个代理对象, 只覆写 `chat`, 其余成员照旧从原型上取。
1789
+ const llmWithHarness = Object.create(llm);
1790
+ llmWithHarness.chat = async (...args) => {
1791
+ try {
1792
+ this.piHarness().beforeModelCall(this.harnessCtx());
1793
+ }
1794
+ catch { /* 门面异常不改变模型调用本身 */ }
1795
+ const __modelT0 = Date.now();
1796
+ try {
1797
+ return await llm.chat(...args);
1798
+ }
1799
+ finally {
1800
+ try {
1801
+ this.piHarness().afterModelCall(this.harnessCtx(), { ms: Date.now() - __modelT0 });
1802
+ }
1803
+ catch { /* 同上 */ }
1804
+ }
1805
+ };
1806
+ const baseConfig = config || this.pivotLoopConfig || (0, workflow_pivot_loop_js_1.createDefaultPivotConfig)();
1807
+ // 2026-10-02 (K7 第二步 b): pivot loop 的工具执行**必须与主路径同一个门**。
1808
+ // 接线前: pivot loop 在 workflow-pivot-loop.ts:613 直调 tool.execute —— 那条路径上
1809
+ // 没有任何 harness 调用 (K7 台账里的**真旁路** ①); 主路径则在 runReActLoop 内先过
1810
+ // `beforeToolCall` (顺序: deny → pre-tool-validator(4 步链) → react-harness(8-gate))。
1811
+ // 这里把同一个判定包成端口注入 ⇒ 两处执行共用一扇门。
1812
+ // **fail-closed**: 门面抛错 ⇒ 拒绝执行 (绝不回落成直调; 旧实现有过两条 fail-open 路径)。
1813
+ const loopConfig = {
1814
+ ...baseConfig,
1815
+ guardedExecute: async (tool, args) => {
1816
+ let decision;
1817
+ try {
1818
+ decision = await this.piHarness().beforeToolCall({
1819
+ tool: tool.name,
1820
+ args,
1821
+ ctx: this.harnessCtx(),
1822
+ permissionMode: this.currentPermissionMode,
1823
+ });
1824
+ }
1825
+ catch (err) {
1826
+ return {
1827
+ success: false,
1828
+ error: `拒绝: [harness-error] Harness 门面异常: ${String(err?.message || err).slice(0, 150)}`,
1829
+ };
1830
+ }
1831
+ if (!decision.allow) {
1832
+ return {
1833
+ success: false,
1834
+ error: `拒绝: [${decision.rejectedBy || decision.source || 'unknown'}] ${decision.reason}`,
1835
+ };
1836
+ }
1837
+ // K4-B (2026-10-02): **改过 TS 就记账** —— 这笔账原先只记在**老 loop 的工具分发**里,
1838
+ // pivot 路径没有 ⇒ 默认切 pivot 后 pivot 里编辑了 TS 却不记账 ⇒ 收尾自检永远不触发
1839
+ // (与"自检只挂在老 loop"同族的缺陷: 同一件事只在一套 loop 里实现)。
1840
+ // 补在 pivot 的**唯一执行点**上 ⇒ 两条 loop 口径一致。
1841
+ {
1842
+ const tsFile = (0, code_write_gate_js_1.codeWriteTarget)(tool.name, args);
1843
+ if (tsFile && !this.tsTouchedThisTurn.includes(tsFile))
1844
+ this.tsTouchedThisTurn.push(tsFile);
1845
+ }
1846
+ const execResult = await tool.execute(args);
1847
+ // K4-B (2026-10-02): **工具执行后的门面阶段必须跟上** —— `afterToolCall` (输出门:
1848
+ // 敏感信息拦截 / router hint) 原先**只**在老 `runReActLoop` 的工具分发里调用;
1849
+ // 老 loop 删除后生产里**一个调用者都没有** ⇒ 输出门形同停用 (安全回归)。补在 pivot 的唯一执行点上。
1850
+ // 门面内部对"gate 自己失败"的策略是**放行但留痕** (输出已产生, 拦不住源头) ⇒ 这里同样不改写结论。
1851
+ try {
1852
+ const after = await this.piHarness().afterToolCall({
1853
+ tool: tool.name,
1854
+ output: String(execResult?.output ?? ''),
1855
+ ctx: this.harnessCtx(),
1856
+ ok: execResult?.success !== false,
1857
+ });
1858
+ if (after?.outputBlocked) {
1859
+ // API 契约: "输出被 gate 拦下 ⇒ 调用方要把 result.output 换掉"
1860
+ const blocked = `[输出已被门拦下: ${after.outputBlocked.reason}] (原始输出未交付)`;
1861
+ if (execResult && typeof execResult === 'object')
1862
+ execResult.output = blocked;
1863
+ }
1864
+ if (after?.routeHint?.systemAddition) {
1865
+ // 差量如实记: 老 loop 把它拼进**下一轮的 system prompt**; pivot 没有对应的注入端口,
1866
+ // 这里改拼进**工具结果文本** —— 模型同样能在下一步看到 (送达渠道不同, 效果等价)。
1867
+ const hint = `\n[路由提示] ${after.routeHint.systemAddition}`;
1868
+ if (execResult && typeof execResult === 'object')
1869
+ execResult.output = String(execResult.output ?? '') + hint;
1870
+ }
1871
+ }
1872
+ catch { /* 后置阶段失败不改变已做出的执行 (门面内部已按策略留痕) */ }
1873
+ return execResult;
1874
+ },
1875
+ };
1601
1876
  const loop = new workflow_pivot_loop_js_1.WorkflowPivotLoop(loopConfig);
1602
1877
  for (const tool of this.tools.values()) {
1603
1878
  loop.registerTool(tool);
@@ -1607,8 +1882,8 @@ class PiAgentSession {
1607
1882
  // M2.2 (2026-06-17): intent 分类 — pivot loop 也要拿到 hint
1608
1883
  try {
1609
1884
  const { classifyIntent, intentHint } = await Promise.resolve().then(() => __importStar(require('./intent-classifier.js')));
1610
- this.currentIntent = classifyIntent(input);
1611
- this.currentIntentHint = intentHint(this.currentIntent);
1885
+ this.runCtx.intent = classifyIntent(input);
1886
+ this.currentIntentHint = intentHint(this.runCtx.intent);
1612
1887
  this.currentUserInput = input;
1613
1888
  }
1614
1889
  catch (err) {
@@ -1655,21 +1930,21 @@ ${await this.renderActivePlansSection()}
1655
1930
  - 工具调用前可以简短思考 (1-2 句话), 但**不要写长篇 thinking** (会撞 max_tokens)
1656
1931
  - 工具调用后必须等结果, 不要在同一个回复里继续输出
1657
1932
  - <final gen> 只在**真完成所有任务**时输出, 不要在工具调用前/中输出${this.judgmentGateAddition}${this.contextHintAddition}`;
1658
- // 2026-06-15: 把 currentOnStream 传给 loop, 让 step-timeline 在 pivot 循环里也能 emit step_start/done
1933
+ // 2026-06-15: 把 runCtx.eventSink 传给 loop, 让 step-timeline 在 pivot 循环里也能 emit step_start/done
1659
1934
  // 之前 loop.execute() 不接 streamCallback, 导致 step-timeline 只能看到老 runReActLoop 路径
1660
1935
  // promptWithPivotLoop 路径 0 step events — UI 显示 timeline 但永远是空
1661
1936
  // 2026-06-17: 透传 signal 让 abort 工作 — loop.execute() 当前不接 signal 参数,
1662
1937
  // 所以 abort 行为通过 this.currentSignal 共享给 loop 内部读 (后续 M3.2 接 task plan 时一起加)
1663
1938
  // 2026-07-06: pivot 内 token 累计到 ~70% 时回调 (workflow-pivot-loop.ts line 270+).
1664
- // pivot 的 messageHistory 是 process-local, 不和 pi-sdk 的 this.messageHistory 同步.
1939
+ // pivot 的 messageHistory 是 process-local, 不和 pi-sdk 的 actor 的 messageHistory 同步.
1665
1940
  // 真正折叠需要把 pi-sdk 历史灌回 pivot 的 history 数组 — 侵入较大.
1666
1941
  // 当前 priority: 临时传空实现, 让 budget 公式放够 (workflow-pivot-loop.ts line 220)
1667
1942
  // 不再撞预算. 这条路径留作技术债.
1668
1943
  // 2026-07-17 Bug 1 修: 注入 messageHistory (hydrateMessageHistory 从 session JSON 回灌的) 到 system prompt
1669
- // pivot loop execute() 内部自己维护 messageHistory, 跟 pi-sdk 的 this.messageHistory 隔离,
1944
+ // pivot loop execute() 内部自己维护 messageHistory, 跟 pi-sdk 的 actor 的 messageHistory 隔离,
1670
1945
  // 不注入的话 LLM 看不到历史对话, 每次都是新对话.
1671
1946
  const historyLines = [];
1672
- const historyToInject = this.messageHistory.slice(-20, -1);
1947
+ const historyToInject = this.actor.state.messageHistory.slice(-20, -1);
1673
1948
  for (const m of historyToInject) {
1674
1949
  const roleLabel = m.role === 'user' ? '用户' : m.role === 'assistant' ? '你' : m.role === 'tool' ? '工具结果' : m.role;
1675
1950
  const text = (m.content || '').slice(0, 2000);
@@ -1682,9 +1957,37 @@ ${await this.renderActivePlansSection()}
1682
1957
  const onCompact = async () => {
1683
1958
  // no-op (best-effort hook for future pi-sdk/pivot history sync)
1684
1959
  };
1685
- const result = await loop.execute(input, llm, systemPrompt + historyBlock, this.currentOnStream ?? undefined, this.currentSignal ?? undefined, onCompact);
1960
+ // K4-B (2026-10-02): **run 生命周期也要过门面** —— `sessionStart`/`sessionEnd` 原先只由老
1961
+ // `runReActLoop` 调用 (删除后全仓零调用者), 而 pivot (生产) 从来没调过 ⇒ 这里恢复。
1962
+ await this.piHarness().sessionStart(this.harnessCtx());
1963
+ let result;
1964
+ try {
1965
+ result = await loop.execute(input, llmWithHarness, systemPrompt + historyBlock, this.runCtx.eventSink ?? undefined, this.runCtx.abortSignal ?? undefined, onCompact);
1966
+ }
1967
+ finally {
1968
+ try {
1969
+ await this.piHarness().sessionEnd(this.harnessCtx());
1970
+ }
1971
+ catch { /* 收尾留痕失败不改变本次结论 */ }
1972
+ }
1973
+ // K4-B (2026-10-02): `reviewFinal` (收尾目标对齐审查) 原先同样只在老 loop 里调 ⇒ 恢复**调用与留痕**。
1974
+ // ⚠️ 如实记边界: pivot 的收尾仍由它自己的质量判定决定, 这里**不**按 review 的返回改流程
1975
+ // (老 loop 会据它自动续跑一轮); 且这里给的是 pivot 手上**真有的**数据 (工具调用计数),
1976
+ // `completedTools`/`actionLog` 的逐条清单 pivot 侧目前没有 ⇒ 传空并留 TODO, **不编数据**。
1977
+ try {
1978
+ const reviewDecision = this.piHarness().reviewFinal({
1979
+ reviewsDone: 0,
1980
+ userIntent: this.currentUserInput,
1981
+ completedTools: [],
1982
+ actionLog: [],
1983
+ runId: this.actor.state.activeRun || undefined,
1984
+ goalId: this.actor.state.goalBinding || undefined,
1985
+ });
1986
+ console.log(`[PiAgent] reviewFinal (K4-B 恢复调用, 判定不参与流程): ${JSON.stringify(reviewDecision).slice(0, 160)}`);
1987
+ }
1988
+ catch { /* 审查异常不改变本次结论 (门面内部对"审查失效"自有留痕策略) */ }
1686
1989
  if (result.response) {
1687
- this.messageHistory.push({ role: 'assistant', content: result.response });
1990
+ this.pushHistory({ role: 'assistant', content: result.response });
1688
1991
  }
1689
1992
  // 回溯 + 清场
1690
1993
  if (this.judgmentGateUsedIds.length > 0) {
@@ -1700,7 +2003,9 @@ ${await this.renderActivePlansSection()}
1700
2003
  */
1701
2004
  _harness = null;
1702
2005
  /** M2 绑定 GoalStore 后填真值; 在此之前为空 (事件里 goalId 字段已就位) */
1703
- currentGoalId = '';
2006
+ // **K5 步骤③/⑧**: 会话的 Goal 默认绑定本体在 `actor.state.goalBinding` (访问器已删, 直接读本体)。
2007
+ // leo 口径不变: 这只是默认值; 每次执行开始必须把最终绑定写进 RunContext/Run 记录,
2008
+ // 运行中重绑必须走显式 Goal Binding 操作, 不许靠裸字段隐式生效。
1704
2009
  /** 2026-09-16 (M2): 本次执行是"从 checkpoint 恢复"的 runId (非空 = 恢复模式, 不再新建 run) */
1705
2010
  resumeRunId = '';
1706
2011
  resumePlan = null;
@@ -1723,32 +2028,47 @@ ${await this.renderActivePlansSection()}
1723
2028
  }
1724
2029
  /** CLI / Web 注入"当前目标" (有 goalId 就在该 Goal 下执行) */
1725
2030
  setGoalId(goalId) {
1726
- this.currentGoalId = String(goalId || '');
2031
+ this.actor.state.goalBinding = String(goalId || '');
1727
2032
  }
1728
- /** 核心状态迁移的兜底: 失败 → 记降级 + 交给循环顶部的持久化硬闸 (不吞错) */
2033
+ /**
2034
+ * K10 ①: 控制面的**注入面** —— 写原语来自 run-store, 但调用一律经 `submitRunControl`
2035
+ * (与 `transportPorts()` 同一个模式: 换实现只改这一处, 上层不直接对 store 说话)。
2036
+ */
2037
+ runControlPorts() {
2038
+ return {
2039
+ setRunStatus: run_store_js_1.setRunStatus,
2040
+ recordRecovery: run_store_js_1.recordRecovery,
2041
+ };
2042
+ }
2043
+ /**
2044
+ * 核心状态迁移的兜底: 失败 → 记降级 + 交给循环顶部的持久化硬闸 (不吞错)。
2045
+ * K10 ①: 迁移动作经内核控制面; **口径不变** ——
2046
+ * `{ok:false}` 且原因是"端口拒绝"(状态迁移不合法) ⇒ 只返回 false (那不是持久化故障);
2047
+ * 未注入 / 抛错 ⇒ 才算持久化失败, 记降级 (与旧实现同款)。
2048
+ */
1729
2049
  async safeSetRunStatus(runId, to) {
1730
- try {
1731
- const r = await (0, run_store_js_1.setRunStatus)(runId, to);
1732
- return !!r.ok;
1733
- }
1734
- catch (err) {
1735
- this.persistenceFailure = `状态迁移失败 (${to}): ${String(err?.message || err).slice(0, 180)}`;
2050
+ const out = await (0, control_js_1.submitRunControl)({ kind: 'set-run-status', origin: 'pi-session', runId, payload: { status: to } }, this.runControlPorts());
2051
+ if (out.ok)
2052
+ return true;
2053
+ const detail = String(out.detail || '未知原因');
2054
+ if (!detail.startsWith('端口拒绝')) {
2055
+ this.persistenceFailure = `状态迁移失败 (${to}): ${detail.slice(0, 180)}`;
1736
2056
  await (0, run_store_js_1.recordDegradation)({ kind: 'core', op: 'pi-sdk.setRunStatus', runId, message: this.persistenceFailure }).catch(() => { });
1737
- return false;
1738
2057
  }
2058
+ return false;
1739
2059
  }
1740
2060
  /**
1741
2061
  * 2026-09-16 (M3): 工具失败接线 —— 分类 → recovery 留痕 → 熔断 / 外部等待。
1742
2062
  * 这是"错误恢复闭环"的运行时接线点 (此前 recordRecovery / repeatedFailureCount 只是数据结构)。
1743
2063
  */
1744
2064
  async wireToolFailure(tool, errorText, args) {
1745
- if (!this.currentRunId)
2065
+ if (!this.actor.state.activeRun)
1746
2066
  return;
1747
2067
  const cls = (0, run_store_js_1.classifyError)(errorText);
1748
2068
  const digest = (0, run_store_js_1.argsDigestOf)(args);
1749
2069
  let repeats = 1;
1750
2070
  try {
1751
- const rec = await (0, run_store_js_1.readRun)(this.currentRunId);
2071
+ const rec = await (0, run_store_js_1.readRun)(this.actor.state.activeRun);
1752
2072
  // recordStep 已经把**这次**失败记进去了, 所以"连续失败次数"直接就是当前值 (不再 +1)
1753
2073
  const seen = rec ? (0, run_store_js_1.repeatedFailureCount)(rec, tool, digest) : 0;
1754
2074
  repeats = Math.max(seen, 1);
@@ -1759,7 +2079,7 @@ ${await this.renderActivePlansSection()}
1759
2079
  : cls === 'external_no_reply' ? 'pause'
1760
2080
  : 'retry';
1761
2081
  try {
1762
- await (0, run_store_js_1.recordRecovery)(this.currentRunId, {
2082
+ await (0, run_store_js_1.recordRecovery)(this.actor.state.activeRun, {
1763
2083
  errorClass: cls,
1764
2084
  message: `${tool}: ${errorText}`.slice(0, 200),
1765
2085
  action,
@@ -1768,20 +2088,20 @@ ${await this.renderActivePlansSection()}
1768
2088
  });
1769
2089
  }
1770
2090
  catch (err) {
1771
- await (0, run_store_js_1.recordDegradation)({ kind: 'core', op: 'pi-sdk.recordRecovery', runId: this.currentRunId, message: String(err?.message || err) }).catch(() => { });
2091
+ await (0, run_store_js_1.recordDegradation)({ kind: 'core', op: 'pi-sdk.recordRecovery', runId: this.actor.state.activeRun, message: String(err?.message || err) }).catch(() => { });
1772
2092
  }
1773
2093
  // 外部无响应 → awaiting_external (不算失败; 等回话后回到 running)
1774
2094
  if (cls === 'external_no_reply') {
1775
2095
  this.awaitingExternal = true;
1776
- await this.safeSetRunStatus(this.currentRunId, 'awaiting_external');
2096
+ await this.safeSetRunStatus(this.actor.state.activeRun, 'awaiting_external');
1777
2097
  // 2026-09-16 (2-C.4): 把"在等什么"写成持久化事实 (来源/关联/过期), 否则真实回包到了也不知道该唤醒谁。
1778
2098
  try {
1779
- if (this.currentGoalId) {
2099
+ if (this.actor.state.goalBinding) {
1780
2100
  const { bindExternalWait, newContinuationId, defaultWaitExpiry } = await Promise.resolve().then(() => __importStar(require('./external-events.js')));
1781
2101
  const isDelegate = /delegate/i.test(String(tool || ''));
1782
- await bindExternalWait(this.currentGoalId, {
1783
- requestId: this.pendingExternalRequestId || `${this.currentRunId}:${Date.now().toString(36)}`,
1784
- continuationId: newContinuationId(this.currentGoalId),
2102
+ await bindExternalWait(this.actor.state.goalBinding, {
2103
+ requestId: this.pendingExternalRequestId || `${this.actor.state.activeRun}:${Date.now().toString(36)}`,
2104
+ continuationId: newContinuationId(this.actor.state.goalBinding),
1785
2105
  expectedSource: isDelegate ? 'delegate' : 'p2p',
1786
2106
  expectedEvent: 'result',
1787
2107
  createdAt: new Date().toISOString(),
@@ -1791,31 +2111,31 @@ ${await this.renderActivePlansSection()}
1791
2111
  }
1792
2112
  }
1793
2113
  catch (e) {
1794
- await (0, run_store_js_1.recordDegradation)({ kind: 'core', op: 'pi-sdk.bindExternalWait', runId: this.currentRunId, message: String(e?.message || e) }).catch(() => { });
2114
+ await (0, run_store_js_1.recordDegradation)({ kind: 'core', op: 'pi-sdk.bindExternalWait', runId: this.actor.state.activeRun, message: String(e?.message || e) }).catch(() => { });
1795
2115
  }
1796
2116
  return;
1797
2117
  }
1798
2118
  // 鉴权类: 不重试, 直接交人
1799
2119
  if (cls === 'auth') {
1800
2120
  this.breakerReason = `鉴权类错误不重试 (${tool}): ${errorText.slice(0, 120)}`;
1801
- await this.safeSetRunStatus(this.currentRunId, 'needs_human');
2121
+ await this.safeSetRunStatus(this.actor.state.activeRun, 'needs_human');
1802
2122
  return;
1803
2123
  }
1804
2124
  // 重复失败熔断: 同一工具 + 同一组参数连续失败达 3 次
1805
2125
  if (repeats >= MAX_SAME_TOOL_FAILURES) {
1806
2126
  this.breakerReason = `同一工具 ${tool} 连续失败 ${repeats} 次 (${cls}) → 熔断, 不再重试`;
1807
- await this.safeSetRunStatus(this.currentRunId, 'needs_human');
2127
+ await this.safeSetRunStatus(this.actor.state.activeRun, 'needs_human');
1808
2128
  }
1809
2129
  }
1810
2130
  getRunId() {
1811
- return this.currentRunId;
2131
+ return this.actor.state.activeRun;
1812
2132
  }
1813
2133
  /**
1814
2134
  * 2026-09-16 (M2-B): **上一次**运行的 runId —— prompt 收尾时 `currentRunId` 会被清空,
1815
2135
  * 而 Supervisor / 控制面要在运行结束后才知道"刚才跑的是哪条 run" (否则会拿旧 run 做决策)。
1816
2136
  */
1817
2137
  getLastRunId() {
1818
- return this.lastRunId || this.currentRunId;
2138
+ return this.lastRunId || this.actor.state.activeRun;
1819
2139
  }
1820
2140
  /**
1821
2141
  * 2026-09-16 (M2): 从 checkpoint 恢复一次运行 —— **不是**重发原 prompt。
@@ -1862,7 +2182,7 @@ ${await this.renderActivePlansSection()}
1862
2182
  }
1863
2183
  this.resumeRunId = runId;
1864
2184
  this.resumePlan = prep.plan;
1865
- this.currentGoalId = prep.plan.goalId || this.currentGoalId;
2185
+ this.actor.state.goalBinding = prep.plan.goalId || this.actor.state.goalBinding;
1866
2186
  try {
1867
2187
  const reply = await this.prompt((0, run_store_js_1.buildResumeInstruction)(prep.plan), {});
1868
2188
  return { ok: true, reply, ...(modelDrift ? { modelDrift } : {}), ...(modelApplied ? { modelApplied } : {}) };
@@ -1872,6 +2192,27 @@ ${await this.renderActivePlansSection()}
1872
2192
  this.resumePlan = null;
1873
2193
  }
1874
2194
  }
2195
+ /**
2196
+ * 2026-10-02 (K7): 把**本会话的 Harness 判定**包成 skill 门, 供 `BollharnessIntegration.setSkillGuard` 注入。
2197
+ * - 只返回**判定**, 不执行 skill ⇒ "允许后执行"留在 SkillAdapter 的**唯一**执行点 (恰一次)。
2198
+ * - 身份 (runId/goalId/agentId/channelId/surface) 由 `harnessCtx()` 带齐。
2199
+ * - 不在这里 catch: 门面抛错由 adapter 侧 **fail-closed** 处理 (拒执行, 绝不回落 registry)。
2200
+ */
2201
+ createSkillGuard() {
2202
+ return async (name, params) => {
2203
+ const decision = await this.piHarness().beforeToolCall({
2204
+ tool: `skill:${name}`,
2205
+ args: params,
2206
+ ctx: this.harnessCtx(),
2207
+ permissionMode: this.currentPermissionMode,
2208
+ });
2209
+ return {
2210
+ allow: decision.allow,
2211
+ reason: decision.reason,
2212
+ rejectedBy: decision.rejectedBy || decision.source,
2213
+ };
2214
+ };
2215
+ }
1875
2216
  piHarness() {
1876
2217
  if (!this._harness) {
1877
2218
  this._harness = new pi_harness_js_1.PiAgentHarness({
@@ -1881,8 +2222,8 @@ ${await this.renderActivePlansSection()}
1881
2222
  // pre-tool-validator 4 步链 (modeGate/blacklist/shell-guard/schema), 经 human-value-pipeline 包装
1882
2223
  preToolUse: async (o) => (0, human_value_pipeline_js_1.onPreToolUse)({ tool: o.tool, args: o.args, permissionMode: o.permissionMode }),
1883
2224
  // 事件写 Run: 观测级 (记账失败不改变已做出的决策)
1884
- events: (e) => { if (this.currentRunId)
1885
- void (0, run_store_js_1.recordHarnessEvent)(this.currentRunId, e); },
2225
+ events: (e) => { if (this.actor.state.activeRun)
2226
+ void (0, run_store_js_1.recordHarnessEvent)(this.actor.state.activeRun, e); },
1886
2227
  });
1887
2228
  }
1888
2229
  return this._harness;
@@ -1890,1134 +2231,69 @@ ${await this.renderActivePlansSection()}
1890
2231
  /** 每个生命周期事件都带上的运行身份 (runId / goalId / agentId / channelId / surface) */
1891
2232
  harnessCtx() {
1892
2233
  return {
1893
- runId: this.currentRunId || undefined,
1894
- goalId: this.currentGoalId || undefined,
1895
- agentId: this.currentAgentId || undefined,
1896
- channelId: this.currentChannelId || undefined,
2234
+ runId: this.actor.state.activeRun || undefined,
2235
+ goalId: this.actor.state.goalBinding || undefined,
2236
+ agentId: this.actor.state.agentId || undefined,
2237
+ channelId: this.actor.state.channelId || undefined,
1897
2238
  surface: this.runSurface,
1898
2239
  };
1899
2240
  }
1900
- async runReActLoop(onStream, signal) {
1901
- const llm = (0, index_js_1.getMinimax)();
1902
- let iteration = 0;
1903
- let finalResponse = '';
1904
- let lastQualityScore = 0;
1905
- let refineAttempts = 0;
1906
- let consecutiveErrors = 0;
1907
- // 2026-06-16 新增: 累计错误数 (跨工具, 兜底防 LLM 轮换工具名死循环)
1908
- let totalErrors = 0;
1909
- let lastFailedTool = ''; // 跟踪最近一次失败的 tool name
1910
- let lastFailedToolCount = 0; // 最近失败工具的连续失败次数
1911
- // 2026-06-16: AI sentinel 标志 — runReActLoop 返回 aiFailed=true,
1912
- // promptStream 据此自动重跑整个 loop 最多 N 次 (不是单次 LLM 重试)
1913
- let aiFailed = false;
1914
- let aiFailureReason = '';
1915
- const MAX_CONSECUTIVE_ERRORS = 3;
1916
- // 同一工具连续失败 3 次, 强制让 LLM 给出最终答案 (模块级常量 MAX_SAME_TOOL_FAILURES 也用它做熔断)
1917
- // 2026-07-29: Hermes 风格硬限制 — 防死循环 (不再靠 soft hint)
1918
- const MAX_IDEMPOTENT_TOOL = 5; // 同工具成功调 5 次 → 注入 hint 强制 final gen
1919
- const MAX_TOOL_CALLS_PER_LOOP = 25; // 单轮循环总工具调用上限 → 注入 hint
1920
- let totalToolCallsThisLoop = 0;
1921
- const lastNTools = []; // 最近 MAX_IDEMPOTENT_TOOL 次工具名, 检测重复
1922
- // 2026-10-01: 工具停滞观测状态 (分类 + 温和引导 + 重复结果引用) —— 见 agents/tool-loop-guard.ts
1923
- // 与上面的"同工具 5 次就提示"不同: 它按 (工具名 + 参数 + 结果) 签名判定, 并抓 A→B→A→B 的循环
1924
- const stallState = new tool_loop_guard_js_1.LoopStallState();
1925
- // 2026-08-10: unreported 循环逃生门 — LLM 反复不把工具结果写进回复时, 3 次后强制 final (不死板)
1926
- const MAX_UNREPORTED_RETRIES = 3;
1927
- let unreportedRetries = 0;
1928
- // 2026-08-10: 工具失败时的终端逃生引导 (shell_exec 白名单命令可诊断环境/推进任务)
1929
- const SHELL_ESCAPE_HINT = ' [逃生] 若工具无法响应/报错, 可用 shell_exec 跑终端命令诊断 (白名单: ls/cat/head/tail/pwd/git status/npm run test 等), 或调整参数换一种方式完成; 不要重复调用同一失败工具.';
1930
- // 2026-08-08: final 前 review 续跑 — 目标对齐 + 需求深挖 (见 loop-review.ts)
1931
- // 不潦草收尾: LLM 想 <final gen> 时先跑 1-2 次 review, 达成用户需求才放行.
1932
- // 上限=2 次 (用户要求"运行一两次"), 结束后按用户需求为准.
1933
- let loopReviewCount = 0;
1934
- const loopReviewCompletedTools = new Set();
1935
- // 2026-08-09: 本轮行动日志 — 每轮工具执行都记录 (args + 结果摘要),
1936
- // final 前 review 用逐条核查目标; 也注入 system prompt 让 LLM 看到连续进度
1937
- // (防"每轮都像重启" — 之前 LLM 看不到自己已完成什么, 容易重复 react)
1938
- const loopActionLog = [];
1939
- // 发送循环开始的事件
1940
- if (onStream) {
1941
- onStream({ type: 'status', internal: true, content: '🔄 开始 ReAct 循环...', tool: 'system' });
1942
- }
1943
- // React Harness: 循环开始 (重置 turn 计数 + 触发 harness sessionStart)
1944
- // 失败静默 (fail-open), 不阻塞主循环
1945
- // 2026-09-16 (Milestone 1-B): 会话开启走唯一门面 (react-harness 8-gate 复位 + hooks onLoopStart)
1946
- await this.piHarness().sessionStart(this.harnessCtx());
1947
- // 2026-09-16: 持久化 run harness — 本次运行立即落盘 (~/.bolloon/runs/<id>.json)。
1948
- // 之后每步工具调用都追加一条, 所以刷新页面/进程重载/崩溃都能看到"做到哪一步"。
1949
- let runStopReason = '';
1950
- // 2026-09-16 (Milestone 1): 持久化硬约束 —— 核心 run 状态写不进去时, 运行必须停。
1951
- // 理由: "agent 实际跑了但没记录" 比 "agent 没跑" 更危险 (UI 显示旧状态/重启后无从知晓/结果可能被错标 done)。
1952
- let runPersistenceFailure = '';
1953
- /** 2026-09-16 (M3): 熔断/需要人处置 → 收尾落 needs_human (不是 done, 也不是普通 failed) */
1954
- let runNeedsHuman = '';
1955
- /** 2026-09-16 (M5): 外部 (CLI/Web) 把 run 改成 paused/aborted → 如实停, 不再覆盖它的状态 */
1956
- let runExternallyPaused = false;
1957
- let runExternallyAborted = false;
1958
- this.runPersistenceBlocked = false;
1959
- this.breakerReason = '';
1960
- this.awaitingExternal = false;
1961
- if (this.resumeRunId) {
1962
- // ── 恢复模式: 复用原来的 runId, 不新建 run (历史保留) ──
1963
- this.currentRunId = this.resumeRunId;
1964
- this.lastRunId = this.resumeRunId;
2241
+ /**
2242
+ * 回合收尾的**受门类型自检** (K7 → K4-B 抽成方法)。
2243
+ *
2244
+ * 为什么必须抽出来: 它原先**只写在老 `runReActLoop` 之后** —— 而生产里 web 走的是 pivot 路径
2245
+ * (`usePivotLoop: true`, `promptWithPivotLoop` 提前 return) ⇒ 这条"系统自检过门"在**生产根本没触发过**。
2246
+ * 抽成方法后两条 loop 路径都调它 (一次一轮, 由 `typecheckRanThisTurn` 保证)。
2247
+ */
2248
+ async runTurnEndTypecheck() {
2249
+ // 2026-10-01: **本回合改过 TS ⇒ 收尾自动类型检查** (一轮一次) —— 复用已有的 tsc_check 工具
2250
+ if ((0, code_write_gate_js_1.decideTypecheck)(this.tsTouchedThisTurn, this.typecheckRanThisTurn)) {
2251
+ this.typecheckRanThisTurn = true;
1965
2252
  try {
1966
- await (0, run_store_js_1.markRunRunning)(this.currentRunId);
1967
- }
1968
- catch (err) {
1969
- runPersistenceFailure = `恢复时状态迁移失败 (recovering → running): ${String(err?.message || err).slice(0, 180)}`;
1970
- }
1971
- const doneN = this.resumePlan?.completedSteps.length ?? 0;
1972
- const guards = this.resumePlan?.replayGuards.length ?? 0;
1973
- onStream?.({ type: 'status', internal: true, content: `♻️ 从 checkpoint 恢复运行 ${this.currentRunId} (已完成 ${doneN} 步, 非幂等重放守卫 ${guards} 条)`, tool: 'harness' });
1974
- }
1975
- else {
1976
- // 2026-09-16 (M2): 目标绑定 —— 有 goalId 就在该 Goal 下执行; 没有就建 Goal 再建 Run。
1977
- // 延续规则 (确定性, 不靠猜): 该 channel/agent 上已有 open/active Goal, 且它的上一次执行**没收尾**
1978
- // (interrupted/stalled/needs_human/paused/awaiting_external/recovering) → 继续该 Goal; 否则新建。
1979
- let boundGoalId = this.currentGoalId;
1980
- if (!boundGoalId) {
1981
- try {
1982
- const active = await (0, goal_store_js_1.findActiveGoal)({ channelId: this.currentChannelId || undefined, agentId: this.currentAgentId || undefined });
1983
- if (active?.currentRunId) {
1984
- const prev = await (0, run_store_js_1.readRun)(active.currentRunId);
1985
- const unfinished = prev && ['interrupted', 'stalled', 'needs_human', 'paused', 'awaiting_external', 'recovering'].includes(prev.status);
1986
- if (unfinished)
1987
- boundGoalId = active.goalId;
1988
- }
1989
- }
1990
- catch (err) {
1991
- console.warn('[PiAgent] 查找进行中 Goal 失败 (按新建处理):', err?.message);
1992
- }
1993
- if (!boundGoalId) {
2253
+ const tscTool = this.tools.get('tsc_check');
2254
+ if (tscTool?.execute) {
2255
+ // 2026-10-02 (K7): **系统自检也过门** —— 统一成"任何工具执行都走同一扇 Harness 门"。
2256
+ // 这里不是模型发起的工具调用 (args 恒空, 是本回合改过 TS 后的收尾自检), 所以**更**不能例外:
2257
+ // 例外一旦靠"没人知道它绕过"活着, 门就不再是唯一的执行咽喉。
2258
+ // 语义: 被拒或门抛错 ⇒ **不执行** (fail-closed), 且**可见**地报出来 (拒绝不许静默)。
2259
+ let tscAllowed = true;
2260
+ let tscWhy = '';
1994
2261
  try {
1995
- const g = await (0, goal_store_js_1.createGoal)({
1996
- objective: this.currentUserInput || '(未记录目标)',
1997
- channelId: this.currentChannelId || undefined,
1998
- agentId: this.currentAgentId || undefined,
1999
- createdBy: this.runSurface,
2000
- });
2001
- boundGoalId = g.goalId;
2002
- }
2003
- catch (err) {
2004
- console.warn('[PiAgent] 创建 Goal 失败 (无目标也要有运行记录):', err?.message);
2005
- }
2006
- }
2007
- this.currentGoalId = boundGoalId;
2008
- }
2009
- try {
2010
- const rec = await (0, run_store_js_1.startRun)({
2011
- surface: this.runSurface,
2012
- goal: this.currentUserInput || '(未记录目标)',
2013
- goalId: boundGoalId || undefined,
2014
- channelId: this.currentChannelId || undefined,
2015
- agentId: this.currentAgentId || undefined,
2016
- modelConfig: await this.runModelSnapshot(),
2017
- });
2018
- this.currentRunId = rec.runId;
2019
- this.lastRunId = rec.runId;
2020
- this.currentGoalId = rec.goalId || this.currentGoalId;
2021
- if (this.currentGoalId) {
2022
- // Run → Goal 反查链: runId → goalId → objective / success criteria
2023
- await (0, goal_store_js_1.attachRun)(this.currentGoalId, rec.runId).catch((err) => console.warn('[PiAgent] attachRun 失败:', err?.message));
2024
- }
2025
- onStream?.({ type: 'status', internal: true, content: `🧷 运行已登记 (run=${rec.runId}${this.currentGoalId ? `, goal=${this.currentGoalId}` : ''}, 预算 ${rec.budget.maxSteps} 步 / ${Math.round(rec.budget.deadlineMs / 60000)} 分钟)`, tool: 'harness' });
2026
- }
2027
- catch (err) {
2028
- // 核心写失败: 不再 warn 后继续 —— 没有运行记录就不执行 (strict 模式默认如此)
2029
- runPersistenceFailure = `无法创建运行记录: ${String(err?.message || err).slice(0, 200)}`;
2030
- this.runPersistenceBlocked = true;
2031
- console.error('[PiAgent] run-store startRun 失败 (核心持久化) → 拒绝无记录执行:', runPersistenceFailure);
2032
- onStream?.({ type: 'error', content: `⛔ ${runPersistenceFailure} — 已停止 (不在没有记录的情况下执行)`, tool: 'harness' });
2033
- }
2034
- }
2035
- while (iteration < this.MAX_REACT_ITERATIONS) {
2036
- iteration++;
2037
- // 2026-09-16 (Milestone 1): 持久化失败硬闸 —— 到这一层说明记录已经不可信, 继续跑就是"无约束执行"
2038
- if (runPersistenceFailure || this.persistenceFailure) {
2039
- runPersistenceFailure = runPersistenceFailure || this.persistenceFailure;
2040
- this.runPersistenceBlocked = true;
2041
- aiFailed = true;
2042
- aiFailureReason = aiFailureReason || runPersistenceFailure;
2043
- finalResponse = finalResponse || `❌ 运行已停止: ${runPersistenceFailure}\n\n(运行记录无法写入/校验, 按协议停在 needs_human, 不假装完成)`;
2044
- break;
2045
- }
2046
- // 2026-09-16 (M3): 熔断硬闸 —— 同一工具连续失败达上限后, 不许再"自动重试成功"式地把运行放活
2047
- if (this.breakerReason) {
2048
- runNeedsHuman = this.breakerReason;
2049
- aiFailed = true;
2050
- aiFailureReason = aiFailureReason || this.breakerReason;
2051
- finalResponse = finalResponse || `❌ 已熔断: ${this.breakerReason}\n\n(重复失败不再重试, 按协议停在 needs_human 交人处置)`;
2052
- break;
2053
- }
2054
- // 2026-09-16: 预算闸门 (持久化 harness 的约束面) —— 到点必须**如实**终止, 不许静默算完成
2055
- if (this.currentRunId) {
2056
- try {
2057
- const rec = await (0, run_store_js_1.readRun)(this.currentRunId);
2058
- if (!rec)
2059
- throw new Error(`运行记录读不到: ${this.currentRunId}`);
2060
- // 2026-09-16 (M5): 外部控制面 (CLI /pause /abort, Web API) 改过状态 → 如实停在那儿。
2061
- // 不覆盖成 done/failed: 人按下暂停就是暂停, 人按下中止就是中止。
2062
- if (rec.status === 'paused' || rec.status === 'aborted') {
2063
- runExternallyPaused = rec.status === 'paused';
2064
- runExternallyAborted = rec.status === 'aborted';
2065
- runStopReason = `外部请求: ${rec.status}`;
2066
- onStream?.({ type: 'error', content: `⏹️ 运行被外部${rec.status === 'paused' ? '暂停' : '中止'} (run=${this.currentRunId})`, tool: 'harness' });
2067
- finalResponse = finalResponse || `(运行已${rec.status === 'paused' ? '暂停' : '中止'})`;
2068
- break;
2069
- }
2070
- const verdict = (0, run_store_js_1.budgetVerdict)(rec);
2071
- if (verdict.exceeded) {
2072
- runStopReason = verdict.reason || '运行预算用尽';
2073
- onStream?.({ type: 'error', content: `⛔ 运行预算用尽: ${runStopReason} (已如实终止, 不假装完成)`, tool: 'harness' });
2074
- finalResponse = finalResponse || `(运行预算用尽: ${runStopReason})`;
2075
- break;
2076
- }
2077
- }
2078
- catch (err) {
2079
- // 预算闸门读不到状态 = 约束失效, 不能"当作没超预算"继续跑
2080
- runPersistenceFailure = `预算闸门无法校验运行状态: ${String(err?.message || err).slice(0, 200)}`;
2081
- this.runPersistenceBlocked = true;
2082
- console.error('[PiAgent] run-store 预算检查失败 (核心持久化):', runPersistenceFailure);
2083
- onStream?.({ type: 'error', content: `⛔ ${runPersistenceFailure} — 已停止`, tool: 'harness' });
2084
- break;
2085
- }
2086
- }
2087
- // 停止条件 1: max turns (fail-safe 10000, 正常任务永远跑不到)
2088
- // 2026-07-01 (v0.2.4 子任务 1): 委托给 react-loop.decideMaxIterations 纯函数
2089
- // 2026-10-01 (落实②: 可退还的迭代预算): 惩罚零碎调用, **奖励批处理** ——
2090
- // 程序化工具(execute_code, 一次能顶多次)调用后归还一次迭代 ⇒ 有效寿命被延长。
2091
- // 退出条件仍由 decideMaxIterations 决定(读**净**用量), 不做硬刹车。
2092
- if (iteration === 0 || !this.iterBudget)
2093
- this.iterBudget = new iteration_budget_js_1.IterationBudget(this.MAX_REACT_ITERATIONS);
2094
- const iterBudget = this.iterBudget;
2095
- iterBudget.consume();
2096
- if (iterBudget.warn(0.8) && !this.iterBudgetWarned) {
2097
- this.iterBudgetWarned = true;
2098
- onStream?.({ type: 'status', internal: true, content: `⏳ 迭代预算已用 ${iterBudget.describe()} (批量工具会退还)` });
2099
- }
2100
- const maxIterDecision = (0, react_loop_js_1.decideMaxIterations)(iterBudget.used, iterBudget.maxTotal);
2101
- if (maxIterDecision.shouldExit) {
2102
- console.warn(`[PiAgent] 达到最大循环数 ${this.MAX_REACT_ITERATIONS}, 强制终止 (fail-safe)`);
2103
- onStream?.({ type: 'error', content: `⏹️ 达到最大循环数 (${this.MAX_REACT_ITERATIONS}, fail-safe)`, tool: 'loop' });
2104
- finalResponse = finalResponse || maxIterDecision.finalAnswer;
2105
- break;
2106
- }
2107
- // 停止条件 2: signal.aborted (显式 abort / 用户中断)
2108
- if (signal?.aborted) {
2109
- console.warn('[PiAgent] runReActLoop aborted by signal');
2110
- onStream?.({ type: 'error', content: '⏹️ 用户中断', tool: 'loop' });
2111
- finalResponse = finalResponse || '(用户中断)';
2112
- break;
2113
- }
2114
- // 2026-07-29: Hermes 风格硬限制 (idempotent tool / total call cap)
2115
- if (totalToolCallsThisLoop >= MAX_TOOL_CALLS_PER_LOOP) {
2116
- console.warn(`[PiAgent] 单轮工具调用已达 ${MAX_TOOL_CALLS_PER_LOOP}, 注入 hint 让 LLM 总结`);
2117
- onStream?.({ type: 'error', content: `⏹️ 工具调用已达上限 (${MAX_TOOL_CALLS_PER_LOOP}), 请基于已有结果回答`, tool: 'loop' });
2118
- this.messageHistory.push({ role: 'system', content: `[注意] 你已连续调用 ${MAX_TOOL_CALLS_PER_LOOP} 次工具。请基于已有结果直接回答用户, 不要再次调用任何工具。在回答末尾加 <final gen> 标记结束。` });
2119
- totalToolCallsThisLoop = 0; // 重置计数器, 只防连续死循环
2120
- }
2121
- if (lastNTools.length >= MAX_IDEMPOTENT_TOOL && new Set(lastNTools).size === 1) {
2122
- const repeatedTool = lastNTools[0];
2123
- console.warn(`[PiAgent] 同工具 ${repeatedTool} 连续成功调 ${MAX_IDEMPOTENT_TOOL} 次, 注入 hint 让 LLM 总结`);
2124
- onStream?.({ type: 'error', content: `⏹️ 工具 ${repeatedTool} 重复调用 ${MAX_IDEMPOTENT_TOOL} 次, 请基于已有结果回答`, tool: 'loop' });
2125
- this.messageHistory.push({ role: 'system', content: `[注意] 你已连续 ${MAX_IDEMPOTENT_TOOL} 次调用 ${repeatedTool}。请基于已有结果直接回答用户, 不要再次调用任何工具。在回答末尾加 <final gen> 标记结束。` });
2126
- lastNTools.length = 0; // 重置计数器
2127
- // 不 break — 让 LLM 在下一轮用已有信息回答
2128
- }
2129
- // 2026-06-16 新增: 累计错误兜底 — 跨工具, 防 LLM 轮换工具名绕过 MAX_SAME_TOOL_FAILURES
2130
- if (totalErrors >= this.MAX_TOTAL_ERRORS) {
2131
- console.warn(`[PiAgent] 累计错误 ${totalErrors} >= ${this.MAX_TOTAL_ERRORS}, 强制终止 (防死循环)`);
2132
- onStream?.({ type: 'error', content: `⛔ 累计 ${totalErrors} 次错误, 强制终止 (防止 LLM 死循环)`, tool: 'loop' });
2133
- // 2026-06-19: 即使 LLM 一直失败, 也汇总之前成功执行的 tool result 给用户
2134
- if (this.successfulToolResults.length > 0) {
2135
- finalResponse = `✅ 之前步骤成功执行了 ${this.successfulToolResults.length} 个工具 (但 LLM 后续 ${totalErrors} 次调用失败):\n` +
2136
- this.successfulToolResults.map((r, i) => ` ${i + 1}. ${r.tool}: ${r.outputPreview}`).join('\n') +
2137
- `\n\n⚠️ (LLM 连续失败, 可能是上游限流/网络问题, 工具已成功执行但 LLM 没能继续总结)`;
2138
- }
2139
- else {
2140
- finalResponse = finalResponse || `(本轮 ReAct 循环累计 ${totalErrors} 次错误, 强制结束。请换个思路或简化任务重试。)`;
2141
- }
2142
- break;
2143
- }
2144
- // 2026-06-16 新增: loop 内自动压缩 — token 超 80% 阈值时跑一次
2145
- // compact 失败走 C 路径: 不强行 break, 让现有 60K 阈值兜底 (后面有检查)
2146
- // 2026-07-01 (v0.2.4 子任务 1): 触发判定走 shouldCompactBeforeIteration 纯函数
2147
- const compactThreshold = this.maxContextTokens() * this.LOOP_COMPACT_RATIO;
2148
- const estimatedTokensBefore = this.estimateHistoryTokens();
2149
- // 2026-08-06: 每轮上报 usage 到 ContextManager (CLI/Web 状态栏数据源, warning 事件触发点)
2150
- (0, context_manager_js_1.getContextManager)().updateUsage(estimatedTokensBefore);
2151
- if ((0, react_loop_js_1.shouldCompactBeforeIteration)(estimatedTokensBefore, compactThreshold)) {
2152
- const tokensBeforeCompact = estimatedTokensBefore;
2153
- console.log(`[PiAgent] loop 入口 token ${tokensBeforeCompact} > ${compactThreshold}, 触发自动压缩`);
2154
- onStream?.({ type: 'status', internal: true, content: `🗜️ loop 自动压缩 (token ${tokensBeforeCompact} > ${compactThreshold})`, tool: 'compactor' });
2155
- try {
2156
- await this.maybeAutoCompact(onStream, signal);
2157
- }
2158
- catch (compactErr) {
2159
- // C 路径: compact 失败不 break, 让 token 阈值检查兜底
2160
- console.warn(`[PiAgent] loop 内 maybeAutoCompact 失败 (non-fatal, 继续走 token 阈值):`, compactErr);
2161
- }
2162
- }
2163
- // 停止条件 3: context overflow (compact 后还超, 强制终止)
2164
- // 2026-07-01 (v0.2.4 子任务 1): 委托给 react-loop.decideContextOverflow 纯函数
2165
- const estimatedTokens = this.estimateHistoryTokens();
2166
- const overflowDecision = (0, react_loop_js_1.decideContextOverflow)(estimatedTokens, this.maxContextTokens());
2167
- if (overflowDecision.shouldExit) {
2168
- console.warn(`[PiAgent] context overflow (${estimatedTokens} tokens > ${this.maxContextTokens()})`);
2169
- onStream?.({ type: 'error', content: `⏹️ 上下文溢出 (${estimatedTokens} tokens, 阈值 ${this.maxContextTokens()})`, tool: 'loop' });
2170
- finalResponse = finalResponse || overflowDecision.finalAnswer;
2171
- break;
2172
- }
2173
- // 调试日志:显示每次循环开始
2174
- console.log(`[PiAgent] 循环 ${iteration}/${this.MAX_REACT_ITERATIONS} 开始`);
2175
- if (onStream) {
2176
- onStream({ type: 'status', internal: true, content: `🔄 循环 ${iteration}/${this.MAX_REACT_ITERATIONS}`, tool: 'loop' });
2177
- }
2178
- const context = this.buildContext();
2179
- // M3.5 (2026-06-17): 也构造 messages 数组版本, 让 LLM 看到结构化 tool 角色
2180
- // buildContext() 把 history 序列化成字符串 — LLM 看不到 tool 调用的真实结果
2181
- // 新版用 messages 数组直接喂给 LLM, 保留 role 语义 (user/assistant/tool/system)
2182
- const messages = this.buildMessages();
2183
- const toolDefs = this.getToolDefinitions();
2184
- // 动态构建 refine 上下文
2185
- let refineContext = '';
2186
- if (refineAttempts > 0 && lastQualityScore < this.QUALITY_THRESHOLD) {
2187
- refineContext = `\n【改进提示】上轮结果质量分 ${(lastQualityScore * 10).toFixed(1)}/10,请改进回答。`;
2188
- }
2189
- // 连续错误时的额外提示
2190
- if (consecutiveErrors > 0) {
2191
- refineContext += `\n【错误提示】上轮发生 ${consecutiveErrors} 次错误,请重新分析问题或换一种方式处理。`;
2192
- }
2193
- // M2.4: persona section 缓存 — persona 在 loadPersona() 时一次设定, 此后不变
2194
- if (!this.cachedPersonaSection && this.persona) {
2195
- this.cachedPersonaSection = `
2196
- 角色描述: ${this.persona.description || '无'}
2197
- 性格特点: ${this.persona.personality || '无'}
2198
- 问候语: ${this.persona.greeting || '无'}
2199
- `;
2200
- }
2201
- const personaSection = this.cachedPersonaSection;
2202
- // 2026-08-09: 循环进度段 — 让 LLM 看到本轮已完成的动作 (连续进度, 不重启)
2203
- // Hermes 式 Agent Runtime: 循环是状态机, LLM 每次看到的是"第 N 步 + 已完成 X"
2204
- // (之前每轮都是全新上下文, LLM 不知道做过什么 → 重复 react / 衔接差)
2205
- let loopProgressSection = '';
2206
- if (loopActionLog.length > 0) {
2207
- const actionLines = loopActionLog
2208
- .map((a, i) => {
2209
- const args = a.argsPreview ? `(${a.argsPreview.slice(0, 60)})` : '';
2210
- const res = a.success ? '✓' : '✗';
2211
- return ` ${i + 1}. ${res} ${a.tool}${args}`;
2212
- })
2213
- .join('\n');
2214
- loopProgressSection = `\n【本轮循环进度】你已完成以下 ${loopActionLog.length} 个动作, 这是连续执行的同一轮任务:\n${actionLines}\n请基于已有结果继续推进, 不要重复执行上面已成功的动作. 全部完成后用 <final gen> 结束.\n`;
2215
- }
2216
- // 2026-09-28 (前缀 KV 可命中): `refineContext` / `loopProgressSection` 是**每轮都在变**的段
2217
- // (质量分 / 连续错误数 / 本轮已完成动作). 留在 system 里 = 每轮把 IMMUTABLE PREFIX 打碎 =
2218
- // 服务端前缀 KV 永远 miss. 移到 CURRENT TURN 区 (注入到最后一条 user 前部) —— 文本一字未改, 只换位置.
2219
- const currentTurnContext = `${refineContext}${loopProgressSection}`;
2220
- const systemPrompt = `${this.bootstrapAddition}你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${personaSection}
2221
- 当前工作目录: ${this.cwd}
2222
- 当前身份: ${this.identity.name} (${this.identity.did})
2223
- ${this.currentIntentHint}
2224
-
2225
- ${toolDefs}
2226
-
2227
- ${PiAgentSession.TOOL_SELECTION_GUIDE}
2228
-
2229
- ${exports.PROACTIVE_WORK_DISCIPLINE}
2230
-
2231
- ${await this.renderActivePlansSection()}
2232
-
2233
- 工作模式:
2234
- 1. 理解用户自然语言请求
2235
- 2. 分析需要哪些工具来完成
2236
- 3. 按顺序调用工具并观察结果
2237
- 4. 根据观察结果决定下一步
2238
- 5. 最终给出完整回答
2239
-
2240
- 重要:
2241
- - 每次只调用一个工具
2242
- - 仔细分析工具返回结果
2243
- - 当任务完成时,必须在回答末尾添加 <final gen> 标记表示结束
2244
- - 如果需要更多信息,继续调用工具${this.judgmentGateAddition}${this.contextHintAddition}`;
2245
- // 3 个恢复机制 (Claude Code 论文 9-step pipeline 内部):
2246
- // 1. max output token 升级 (最多 3 次, 每次 maxOutputTokens 翻倍)
2247
- // 2. reactive compaction (prompt 估算超阈值, 跑压缩)
2248
- // 3. prompt-too-long (LLM 报错 4xxx token 错误, 跑 reactive compaction 再试 1 次)
2249
- // 失败静默: 全部重试失败 → 空 reply (上层用 no tool_use 终止)
2250
- // Bug 5: pass tool IDs for native OpenAI tool calling — 2026-07-29: 过滤拒绝工具
2251
- const toolIds = Array.from(this.tools.keys()).filter(n => !this._deniedToolNames.has(n));
2252
- // 2026-07-29: 从 this.tools Map 生成 OpenAI 原生 tools 格式 (含参数 schema)
2253
- const openaiFormattedTools = [];
2254
- for (const [name, tool] of this.tools) {
2255
- if (this._deniedToolNames.has(name))
2256
- continue;
2257
- const params = tool.parameters || {};
2258
- const properties = {};
2259
- const required = [];
2260
- for (const [pName, pDesc] of Object.entries(params)) {
2261
- properties[pName] = { type: 'string', description: String(pDesc) };
2262
- if (String(pDesc).includes('必填'))
2263
- required.push(pName);
2264
- }
2265
- openaiFormattedTools.push({
2266
- type: 'function',
2267
- function: {
2268
- name,
2269
- description: tool.description || name,
2270
- parameters: { type: 'object', properties, required },
2271
- },
2272
- });
2273
- }
2274
- // 2026-09-16 (Milestone 1-B): 模型调用前后走唯一门面 (扩展点 + 计数留痕; 默认不加新 hook 事件, 避免改变现有触发次数)
2275
- this.piHarness().beforeModelCall(this.harnessCtx());
2276
- const _modelCallT0 = Date.now();
2277
- const response = await this.callLlmWithRecovery(llm, messages, systemPrompt, signal, onStream, openaiFormattedTools, currentTurnContext);
2278
- // 2026-09-28 (前缀 KV 可命中): 把注入了 CURRENT TURN 的当前轮**原样写回自己的 messageHistory** ——
2279
- // buildMessages() 每轮重建全新对象, 不写回的话下一轮那条 user 就退回注入前, 前缀从那里分叉.
2280
- // 用 chat() 回带的 wire (真正发出去的那一份), 而不是把 messages 再拼一遍.
2281
- this.writeBackCurrentTurn(response.messages);
2282
- this.piHarness().afterModelCall(this.harnessCtx(), { ms: Date.now() - _modelCallT0 });
2283
- const reply = (response.reply || '').trim();
2284
- // 2026-06-30: OpenAI 协议 native tool_calls (LLM 真产了 tool_call 时, minimax/M3 会返回 id)
2285
- const nativeToolCalls = response.toolCalls;
2286
- // 2026-06-19 架构 fix: 不再因 [AI 服务调用失败] break
2287
- // 旧逻辑: sentinel → aiFailed=true → break → 外层 retry 整个 loop (重置 history)
2288
- // 新逻辑: 把错误当 tool_result push 进 history → 下一轮 LLM 看到错误能反思重试
2289
- // 这是 dive-into 文档的"fail-open error recovery" — 错误进入 context, 不让 LLM 重复犯同样错
2290
- // 2026-07-06: 对不可恢复的 API 错误直接终止, 不再无限重试
2291
- if (reply.startsWith('[AI 服务调用失败]')) {
2292
- console.log(`[PiAgent] 收到 AI 错误 sentinel`);
2293
- console.log(`[sentinel DEBUG] 完整 reply: ${reply}`);
2294
- console.log(`[sentinel DEBUG] 上一轮 messages 数量: ${Array.isArray(messages) ? messages.length : 'N/A'}, systemPrompt 长度: ${systemPrompt.length}`);
2295
- aiFailureReason = reply.length > 200 ? reply.substring(0, 200) : reply;
2296
- totalErrors++;
2297
- consecutiveErrors++;
2298
- // 2026-07-06: 检测不可恢复的 API 错误 — 这些错误 LLM 无法通过反思修复, 重试无意义
2299
- const isFatalApiError = reply.includes('chat content is empty') ||
2300
- reply.includes('invalid params') ||
2301
- reply.includes('401') ||
2302
- reply.includes('403') ||
2303
- reply.includes('quota') ||
2304
- reply.includes('rate limit') ||
2305
- reply.includes('API key') ||
2306
- reply.includes('authentication') ||
2307
- reply.includes('unauthorized');
2308
- if (isFatalApiError) {
2309
- console.log(`[PiAgent] 检测到不可恢复的 API 错误, 终止 loop: ${aiFailureReason}`);
2310
- if (onStream) {
2311
- onStream({ type: 'error', content: `⛔ API 错误无法恢复: ${aiFailureReason}`, tool: 'system' });
2312
- }
2313
- finalResponse = `❌ AI 服务调用失败: ${aiFailureReason}\n\n这是一个底层 API 错误, 不是任务本身的问题。请检查 API 配置或稍后重试。`;
2314
- aiFailed = true;
2315
- break;
2316
- }
2317
- // 连续错误过多也终止, 防止 LLM 陷入死循环
2318
- if (consecutiveErrors >= 3) {
2319
- console.log(`[PiAgent] 连续 ${consecutiveErrors} 次 AI 错误, 终止 loop`);
2320
- if (onStream) {
2321
- onStream({ type: 'error', content: `⛔ 连续 ${consecutiveErrors} 次 AI 错误, 终止循环`, tool: 'system' });
2322
- }
2323
- finalResponse = `❌ AI 连续调用失败 ${consecutiveErrors} 次, 已终止。\n\n失败原因: ${aiFailureReason}\n\n请检查 API 配置或简化任务后重试。`;
2324
- aiFailed = true;
2325
- break;
2326
- }
2327
- // 把错误当成 tool 结果 push 进 history, 这样下一轮 LLM 看到错误能调整
2328
- this.messageHistory.push({
2329
- role: 'system',
2330
- content: `[Loop 错误恢复 ${totalErrors}/${this.MAX_TOTAL_ERRORS}] ${aiFailureReason}\n\n请基于上轮工具结果继续完成任务, 不要重复调用同一失败操作. 如果工具已成功执行, 请基于 result.output 给用户总结; 如果工具失败, 请换其他方式或重试.`
2331
- });
2332
- if (onStream) {
2333
- onStream({ type: 'status', content: `⚠️ AI 调用失败 ${totalErrors}/${this.MAX_TOTAL_ERRORS}, 已 push 错误到 history 让 LLM 反思`, tool: 'system' });
2334
- }
2335
- // 退避 2s 后继续 — 临时上游限流避开, 不让 loop 终止
2336
- await new Promise(resolve => setTimeout(resolve, 2000));
2337
- // 关键: 不设 aiFailed=true, 让外层不重试整个 loop (重置 history), 继续内层循环
2338
- continue;
2339
- }
2340
- console.log(`[PiAgent] LLM 回复长度: ${reply.length}, 内容预览: "${reply.substring(0, 80)}..."`);
2341
- console.log(`[PiAgent] LLM 完整回复:\n${reply}`);
2342
- // 通知前端:收到 LLM 回复 (2026-08-09: 不再截断 100 字符 — 前端流式渲染完整内容,
2343
- // 配合 Hermes 式回复框: 加载中显示完整文本, 完成后封闭底框)
2344
- if (onStream) {
2345
- onStream({ type: 'token', content: reply });
2346
- }
2347
- // 2026-06-19 架构 fix: parseToolCall 优先于 isFinalResponse
2348
- // 之前: 思考块里的 "<final gen>" 触发 isFinalResponse 提前 break, 工具从未真正执行
2349
- // 现在: 先尝试解析 tool_call, 有就执行; 没有才检查是不是真正的 final gen
2350
- // Bug 5 (2026-07-17): 优先用 LLM 的 native tool_calls (response.toolCalls), 再回退到文本解析
2351
- // deepseek-v4-flash 用 OpenAI 协议 tools 时, 会真返回结构化 tool_calls 数组
2352
- // 之前 nativeToolCalls 被读了不用, 只查 reply 文本, 导致 LLM 明明选了工具但代码找不到
2353
- // 2026-07-28: 修复多工具调用 — 收集 ALL tool calls, 顺序执行后一次性返回
2354
- let toolCalls = [];
2355
- // 路径 A: native OpenAI 协议 tool_calls (可能多个)
2356
- if (nativeToolCalls && nativeToolCalls.length > 0) {
2357
- for (const nc of nativeToolCalls) {
2358
- try {
2359
- const args = typeof nc.function?.arguments === 'string'
2360
- ? JSON.parse(nc.function.arguments)
2361
- : (nc.function?.arguments || {});
2362
- toolCalls.push({
2363
- name: nc.function?.name,
2364
- args,
2365
- id: nc.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
2366
- });
2367
- }
2368
- catch (err) {
2369
- console.warn(`[PiAgent] 解析 native tool_call 失败: ${err.message?.slice(0, 100)}`);
2370
- }
2371
- }
2372
- }
2373
- // 路径 B: 文本解析 (parseAllToolCalls 收集全部)
2374
- if (toolCalls.length === 0) {
2375
- // 2026-09-26: 已知名集合 = 注册表原名 + 它们的 API 名 (净化后的).
2376
- // 出网时工具面给的是 API 名 (见 pi-ai.ts 的唯一净化边界), LLM 在文本里
2377
- // 回吐的也可能是 API 名 —— 只用原名集合过滤会把这类调用整个丢掉.
2378
- const knownTools = (0, tool_name_js_1.expandKnownToolNames)(this.tools.keys());
2379
- toolCalls = (0, parse_tool_call_js_1.parseAllToolCalls)(reply, { tools: knownTools });
2380
- }
2381
- // 回退路径 C: 原生 parseToolCall (单个)
2382
- if (toolCalls.length === 0) {
2383
- const single = this.parseToolCall(reply);
2384
- if (single)
2385
- toolCalls.push(single);
2386
- }
2387
- // 2026-09-26: **回程派发的唯一还原点** —— LLM 回吐的是 API 名 (净化过的), 这里
2388
- // 还原成注册表真名再交给 this.tools.get(); 原名 (LLM 照 system prompt 抄的) 原样穿透.
2389
- for (const tc of toolCalls) {
2390
- if (typeof tc.name === 'string' && tc.name)
2391
- tc.name = (0, tool_name_js_1.resolveApiToolName)(tc.name);
2392
- }
2393
- // 给每个 toolCall 分配稳定 id
2394
- for (const tc of toolCalls) {
2395
- if (!tc.id) {
2396
- tc.id = `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
2397
- }
2398
- }
2399
- if (toolCalls.length > 0) {
2400
- // 把原始 LLM 回复 push 进 history (仅一次)
2401
- this.messageHistory.push({
2402
- role: 'assistant',
2403
- content: reply,
2404
- toolCalls: toolCalls.length > 1 ? toolCalls : [toolCalls[0]],
2405
- // 2026-09-15: 思考模式思维链原样存回 (下一轮带 tools 的请求必须回带, 否则 deepseek 400)
2406
- reasoningContent: response?.reasoningContent,
2407
- });
2408
- // 2026-08-09: 并发执行本轮所有工具 (Hermes 式 Agent Runtime: 一轮内多工具并行,
2409
- // 一轮没跑完之前不中断 — 工具执行不检查 abort, 全部完成才 continue 下一轮)
2410
- // 旧实现顺序 for 循环, 一个工具等一个, 慢; 且多工具时 LLM 要等全部串完才能看到结果.
2411
- await Promise.all(toolCalls.map(async (toolCall, ti) => {
2412
- const isMulti = toolCalls.length > 1;
2413
- // 通知前端
2414
- if (onStream) {
2415
- onStream({ type: 'tool', content: `🔧 调用工具 (${ti + 1}/${toolCalls.length}): ${toolCall.name}`, tool: toolCall.name });
2416
- if (toolCall.args && Object.keys(toolCall.args).length > 0) {
2417
- onStream({ type: 'status', internal: true, content: `📋 参数: ${JSON.stringify(toolCall.args)}`, tool: toolCall.name });
2418
- }
2419
- onStream({
2420
- type: 'step_start',
2421
- content: `调用 ${toolCall.name}${isMulti ? ` (${ti + 1}/${toolCalls.length})` : ''}`,
2422
- tool: toolCall.name,
2423
- args: toolCall.args || {},
2424
- });
2425
- }
2426
- const tool = this.tools.get(toolCall.name);
2427
- if (!tool) {
2428
- consecutiveErrors++;
2429
- totalErrors++;
2430
- const errorResult = { success: false, error: `未知工具: ${toolCall.name}` };
2431
- this.messageHistory.push({ role: 'tool', content: JSON.stringify(errorResult), toolResult: errorResult });
2432
- this.logToHarness(toolCall.name, toolCall.args, errorResult);
2433
- // 2026-07-28: 注入 Reflection 帮助 LLM 理解错误
2434
- const obs = (0, error_classifier_js_1.buildObservation)(toolCall.name, toolCall.args, errorResult);
2435
- const ref = (0, error_classifier_js_1.buildReflection)(toolCall.name, errorResult.error, totalErrors, lastFailedToolCount);
2436
- this.messageHistory.push({ role: 'system', content: (0, error_classifier_js_1.formatObservationWithReflection)(obs, ref) });
2437
- if (onStream)
2438
- onStream({ type: 'status', content: `💡 Reflection: ${obs.summary}`, tool: 'system' });
2439
- console.warn(`[PiAgent] 未知工具: ${toolCall.name} (累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}),跳过并继续`);
2440
- return;
2441
- }
2442
- // 2026-09-16 (Milestone 1-B): 工具调用前的**唯一**约束入口。
2443
- // 顺序由 PiAgentHarness 决定: deny-pipeline → pre-tool-validator(4 步链) → react-harness(8-gate)。
2444
- // 旧实现是这三处在不同位置各自调用 (且有两条路径 fail-open); 现在 pi-sdk 不再散调任何 gate。
2445
- let toolDecision;
2446
- try {
2447
- toolDecision = await this.piHarness().beforeToolCall({
2448
- tool: toolCall.name,
2449
- args: toolCall.args || {},
2262
+ const d = await this.piHarness().beforeToolCall({
2263
+ tool: 'tsc_check',
2264
+ args: {},
2450
2265
  ctx: this.harnessCtx(),
2451
2266
  permissionMode: this.currentPermissionMode,
2452
2267
  });
2268
+ tscAllowed = d.allow;
2269
+ tscWhy = d.reason || d.rejectedBy || '';
2453
2270
  }
2454
- catch (err) {
2455
- // 门面自身抛错 = 核心约束失效 → fail-closed (不执行工具, 也不静默放行)
2456
- toolDecision = {
2457
- allow: false,
2458
- source: 'harness-error',
2459
- kind: 'core_constraint',
2460
- reason: `Harness 门面异常: ${String(err?.message || err).slice(0, 150)}`,
2461
- };
2462
- }
2463
- if (!toolDecision.allow) {
2464
- const src = toolDecision.source || 'unknown';
2465
- if (src === 'deny-pipeline') {
2466
- // 旧 deny-pipeline 分支: 不计连续失败计数, 文案 "拒绝: [source] reason"
2467
- consecutiveErrors++;
2468
- totalErrors++;
2469
- const denyResultMsg = { success: false, error: `拒绝: [${toolDecision.rejectedBy || 'deny-pipeline'}] ${toolDecision.reason}` };
2470
- this.messageHistory.push({ role: 'tool', content: JSON.stringify(denyResultMsg), toolResult: denyResultMsg });
2471
- this.logToHarness(toolCall.name, toolCall.args, denyResultMsg);
2472
- return;
2473
- }
2474
- const isGateDeny = src === 'react-harness';
2475
- const isHarnessError = src === 'harness-error';
2476
- const deniedResult = {
2477
- success: false,
2478
- error: isGateDeny
2479
- ? `Harness gate 拒绝 (${toolDecision.rejectedBy}): ${toolDecision.reason || '未通过安全校验'}`
2480
- : `PreToolUse 拒绝: ${toolDecision.reason || '未通过安全校验'}`,
2481
- };
2482
- this.messageHistory.push({ role: 'tool', content: JSON.stringify(deniedResult), toolResult: deniedResult });
2483
- this.logToHarness(toolCall.name, toolCall.args, deniedResult);
2484
- if (onStream) {
2485
- const gateName = isGateDeny ? `Harness ${toolDecision.rejectedBy}` : (isHarnessError ? '核心约束层' : 'PreToolUse');
2486
- onStream({ type: 'error', content: `🛡️ ${gateName} 拒绝 ${toolCall.name}: ${toolDecision.reason || '安全校验失败'}`, tool: toolCall.name });
2487
- onStream({ type: 'step_error', content: `${gateName} 拒绝 ${toolCall.name}`, tool: toolCall.name, error: toolDecision.reason || '安全校验失败' });
2488
- }
2489
- console.warn(`[PiAgent] 工具被拒 ${toolCall.name} (${src}${toolDecision.rejectedBy ? ':' + toolDecision.rejectedBy : ''}): ${toolDecision.reason}`);
2490
- consecutiveErrors++;
2491
- totalErrors++;
2492
- if (toolCall.name === lastFailedTool) {
2493
- lastFailedToolCount++;
2494
- }
2495
- else {
2496
- lastFailedTool = toolCall.name;
2497
- lastFailedToolCount = 1;
2498
- }
2499
- // 达到同一工具连续失败上限 / 连续错误上限时的引导语 (按拒绝来源保持原有文案)
2500
- const systemMaxMsg = isGateDeny
2501
- ? `[注意] 工具 ${toolCall.name} 被 Harness 拒绝 (连续 ${MAX_SAME_TOOL_FAILURES} 次). 请不要再次尝试, 末尾加 <final gen>.`
2502
- : isHarnessError
2503
- ? `[注意] 工具 ${toolCall.name} 的约束校验层失效, 已按 fail-closed 阻止 (连续 ${MAX_SAME_TOOL_FAILURES} 次). 请换其他工具或直接回答用户, 末尾加 <final gen>.`
2504
- : `[注意] 工具 ${toolCall.name} 被系统拒绝 (连续 ${MAX_SAME_TOOL_FAILURES} 次). 请不要再次尝试, 直接用已有信息回答用户, 末尾加 <final gen>.`;
2505
- const systemConsecMsg = isGateDeny
2506
- ? `[注意] 连续 ${consecutiveErrors} 次工具调用被 Harness 拒绝. 请换其他工具或直接回答.`
2507
- : isHarnessError
2508
- ? `[注意] 连续 ${consecutiveErrors} 次工具调用因约束层失效被阻止. 请换其他工具或直接回答用户, 末尾加 <final gen>.`
2509
- : `[注意] 连续 ${consecutiveErrors} 次工具调用被系统拒绝. 请换其他工具或直接回答用户, 末尾加 <final gen>.`;
2510
- if (lastFailedToolCount >= MAX_SAME_TOOL_FAILURES) {
2511
- this.messageHistory.push({ role: 'system', content: systemMaxMsg });
2512
- lastFailedTool = '';
2513
- lastFailedToolCount = 0;
2514
- consecutiveErrors = 0;
2515
- }
2516
- else if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
2517
- this.messageHistory.push({ role: 'system', content: systemConsecMsg });
2518
- consecutiveErrors = 0;
2519
- }
2520
- return;
2521
- }
2522
- if (toolDecision.systemAddition) {
2523
- this.contextHintAddition += '\n' + toolDecision.systemAddition;
2524
- }
2525
- try {
2526
- const toolStart = Date.now();
2527
- // 2026-09-16 (M2): 恢复重放守卫 —— 中断前**已成功执行过的非幂等动作**恢复后不再重做,
2528
- // 直接复用当时的结果 (避免重复副作用: 重复写文件/重复提交/重复付款)。
2529
- // (M2-B: 守卫也覆盖"上一个 Run 已做过的非幂等动作" —— 跨 Run 续跑同样不许重做)
2530
- let replaySkip = null;
2531
- const guards = [
2532
- ...(this.resumePlan?.replayGuards || []),
2533
- ...(this.continuationGuards || []),
2534
- ];
2535
- if (guards.length) {
2536
- const d = (0, run_store_js_1.argsDigestOf)(toolCall.args);
2537
- const hit = guards.find((g) => g.tool === toolCall.name && (!g.argsDigest || !d || g.argsDigest === d));
2538
- if (hit)
2539
- replaySkip = hit.summary;
2540
- }
2541
- let result = replaySkip
2542
- ? { success: true, output: `[恢复保护] ${toolCall.name} 在中断前已成功执行过, 本次不重复执行 (避免重复副作用)。当时结果: ${replaySkip}`, _replaySkipped: true }
2543
- : await (toolCall.__t0 = Date.now(), tool.execute((0, tool_path_args_js_1.expandHomeArgs)(toolCall.args)));
2544
- const toolDurationMs = Date.now() - toolStart;
2545
- if (replaySkip) {
2546
- console.log(`[PiAgent] 恢复重放守卫: 跳过已完成的非幂等工具 ${toolCall.name}`);
2547
- onStream?.({ type: 'status', internal: true, content: `🛡️ 恢复保护: ${toolCall.name} 此前已成功执行, 本次不重复执行`, tool: toolCall.name });
2548
- }
2549
- console.log(`[PiAgent] 工具 ${toolCall.name} 执行完成: success=${result.success} (${toolDurationMs}ms)`);
2550
- // 2026-09-16 (M3): 失败接线 —— 分类 + recovery 留痕 + 熔断 / 外部等待
2551
- if (!result.success && this.currentRunId && !replaySkip) {
2552
- await this.wireToolFailure(toolCall.name, String(result.error || ''), toolCall.args);
2553
- }
2554
- else if (result.success && this.currentRunId && this.awaitingExternal) {
2555
- // 外部回话了: awaiting_external → running (不是"恢复完成", 只是等待结束)
2556
- this.awaitingExternal = false;
2557
- await this.safeSetRunStatus(this.currentRunId, 'running');
2558
- }
2559
- // 2026-09-16: 持久化 run harness — 每步工具调用立即落盘 (崩在这里也能看到做到哪步)
2560
- if (this.currentRunId) {
2561
- try {
2562
- await (0, run_store_js_1.recordStep)(this.currentRunId, {
2563
- tool: toolCall.name,
2564
- ok: !!result.success,
2565
- ms: toolDurationMs,
2566
- args: toolCall.args,
2567
- summary: String(result.output || '').slice(0, 200),
2568
- error: result.error ? String(result.error) : undefined,
2569
- });
2570
- }
2571
- catch (err) {
2572
- // 核心写失败 → 本轮工具批跑完即停 (循环顶部硬闸), 不再 warn 后继续
2573
- runPersistenceFailure = `工具步骤写盘失败 (${toolCall.name}): ${String(err?.message || err).slice(0, 200)}`;
2574
- this.runPersistenceBlocked = true;
2575
- console.error('[PiAgent] run-store recordStep 失败 (核心持久化):', runPersistenceFailure);
2576
- onStream?.({ type: 'error', content: `⛔ ${runPersistenceFailure} — 本轮结束后停止`, tool: 'harness' });
2577
- }
2578
- }
2579
- try {
2580
- await (0, lifecycle_hooks_js_1.onPostToolUse)({ tool: toolCall.name, args: toolCall.args || {}, result: { success: result.success, output: result.output?.substring(0, 500), error: result.error }, durationMs: toolDurationMs });
2581
- }
2582
- catch (postErr) {
2583
- console.warn('[PiAgent] onPostToolUse failed (non-fatal):', postErr);
2584
- }
2585
- // 2026-09-16 (Milestone 1-B): 工具调用后的唯一入口 (router hint + 输出 gate 一起判定)
2586
- const after = await this.piHarness().afterToolCall({
2587
- tool: toolCall.name,
2588
- output: String(result.output || ''),
2589
- ctx: this.harnessCtx(),
2590
- ok: !!result.success,
2591
- });
2592
- if (after.routeHint?.systemAddition) {
2593
- this.messageHistory.push({ role: 'system', content: `[Harness Router Hint: ${after.routeHint.reason}]\n${after.routeHint.systemAddition}` });
2594
- }
2595
- if (after.outputBlocked) {
2596
- if (onStream) {
2597
- onStream({ type: 'error', content: `🛡️ Harness output 拒绝 ${toolCall.name}: ${after.outputBlocked.reason}`, tool: toolCall.name });
2598
- }
2599
- console.warn(`[PiAgent] Harness output denied ${toolCall.name}: ${after.outputBlocked.reason}`);
2600
- result = { ...result, output: `[harness output gate: 输出含敏感内容, 已屏蔽. 原因: ${after.outputBlocked.reason}]`, _harnessDenied: true };
2601
- }
2602
- // 2026-10-01: 落库前做一次停滞观测 —— 命中的是"引导/引用", 不是"拒绝执行":
2603
- // 完全相同的返回从第 2 次起折叠成引用 (省上下文); 连续 3 次同参数同结果或检测到循环 ⇒
2604
- // 在结果尾部追加一条系统提示 (保留模型的选择权, 不硬停 —— 硬停那条已被用户否决)。
2605
- try {
2606
- const obs = (0, tool_loop_guard_js_1.observeToolCall)(stallState, {
2607
- toolName: toolCall.name,
2608
- sameSignatureBefore: this.lastToolSig === (0, tool_telemetry_js_1.argsFingerprint)(toolCall.args),
2609
- args: toolCall.args,
2610
- resultText: String(result.output || (result.success ? '' : String(result.error || ''))),
2611
- ok: !!result.success,
2612
- seenResultBefore: stallState.hasSeenResult(String(result.output || '')),
2613
- });
2614
- if (obs.stub) {
2615
- result = { ...result, output: obs.stub, _stubbed: true };
2616
- }
2617
- else if (obs.notice) {
2618
- result = { ...result, output: `${String(result.output || '')}\n\n${obs.notice}` };
2619
- }
2620
- if (obs.action === 'warn') {
2621
- console.warn(`[PiAgent] 工具停滞引导 (${obs.code}, 第 ${obs.count} 次): ${toolCall.name}`);
2622
- onStream?.({ type: 'status', internal: true, content: `🩺 检测到重复调用 ${toolCall.name} (${obs.code}), 已提示模型换法`, tool: 'loop' });
2623
- }
2624
- }
2625
- catch { /* 观测失败绝不影响主路径 */ }
2626
- this.messageHistory.push({ role: 'tool', content: JSON.stringify(result), toolResult: result, toolCallId: toolCall.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` });
2627
- // 2026-10-01 (用户: 「为什么这么慢」): 量到的事实 —— 每次 LLM 往返平均 3338ms,
2628
- // 而"一轮发多个工具"(toolCalls>1) 只占 12/63 ✗ ⇒ 回合时长 ≈ 往返次数 × 3.3 秒 ✓。
2629
- // 本回合**第一次**且**只发一个**工具时, 附一句带代价的提醒(不是口号 ✓), 只提一次免得刷屏。
2630
- if (toolCalls.length === 1 && this.successfulToolResults.length <= 1) { // 本回合头一次(免得每轮刷屏)
2631
- const __batchHint = (0, tool_loop_guard_js_1.batchHint)(toolCall.name, 1);
2632
- if (__batchHint)
2633
- this.messageHistory.push({ role: 'system', content: __batchHint.trim() });
2634
- }
2635
- this.logToHarness(toolCall.name, toolCall.args, result);
2636
- // 2026-08-09: 记录到本轮行动日志 (循环进度 + final 前目标核查用)
2637
- // 去重: 同一工具同 args 连续成功只记一次 (防 LLM 重复 react 刷屏)
2638
- const argsPreview = JSON.stringify(toolCall.args || {}).slice(0, 120);
2639
- const isDup = loopActionLog.some((a) => a.tool === toolCall.name && a.argsPreview === argsPreview && a.success === !!result.success);
2640
- if (!isDup) {
2641
- loopActionLog.push({
2642
- tool: toolCall.name,
2643
- argsPreview,
2644
- resultPreview: result.success
2645
- ? String(result.output || '(无输出)').slice(0, 200)
2646
- : String(result.error || 'failed').slice(0, 200),
2647
- success: !!result.success,
2648
- });
2649
- }
2650
- if (onStream) {
2651
- if (result.success) {
2652
- onStream({ type: 'status', internal: true, content: `✅ ${toolCall.name} 执行成功`, tool: toolCall.name });
2653
- if (result.output) {
2654
- onStream({ type: 'tool', content: `📤 结果: ${result.output.substring(0, 200)}${result.output.length > 200 ? '...' : ''}`, tool: toolCall.name });
2655
- }
2656
- onStream({ type: 'step_done', content: `${toolCall.name} 执行成功`, tool: toolCall.name, success: true, output: result.output });
2657
- }
2658
- else {
2659
- onStream({ type: 'error', content: `❌ ${toolCall.name} 执行失败: ${result.error}`, tool: toolCall.name });
2660
- onStream({ type: 'step_error', content: `${toolCall.name} 执行失败`, tool: toolCall.name, error: result.error });
2661
- }
2662
- }
2663
- // 2026-10-01 (优化 #1/#3/#4): 工具结果的**进上下文闸** + **遥测** + **写操作读回自证**
2664
- {
2665
- const __ms = Date.now() - Number(toolCall.__t0 || Date.now());
2666
- try {
2667
- // #1 结果闸: 超上限 ⇒ 头尾 + 完整结果落文件(给路径) ⇒ 上下文不再被一条大输出长期占住
2668
- const capped = (0, tool_result_gate_js_1.capToolResult)(String(result.output ?? ''), { tool: toolCall.name });
2669
- if (capped.capped) {
2670
- result.output = capped.text;
2671
- console.warn(`[PiAgent] ${toolCall.name} 结果过长(${capped.originalChars})已截断` + (capped.spilledTo ? `, 完整内容: ${capped.spilledTo}` : ''));
2672
- }
2673
- // #4 写操作"读回自证": 写类工具成功后自动核一次(存在? 大小?) ⇒ "工具说成功≠任务成功"从规矩变成机制
2674
- // 2026-10-01: 改了 TS 源码就记账 ⇒ 回合收尾自动跑类型检查 (把"靠自觉"变成机制)
2675
- {
2676
- const tsFile = (0, code_write_gate_js_1.codeWriteTarget)(toolCall.name, toolCall.args);
2677
- if (tsFile && !this.tsTouchedThisTurn.includes(tsFile))
2678
- this.tsTouchedThisTurn.push(tsFile);
2679
- }
2680
- const verified = verifyWriteOutcome(toolCall.name, toolCall.args, this.cwd);
2681
- if (verified)
2682
- result.output = `${String(result.output ?? '')}\n${verified}`;
2683
- // #3 遥测: 记一行(工具/指纹/耗时/成败/结果大小/是否与上一次同签名) ⇒ 重复率可算
2684
- (0, tool_telemetry_js_1.recordToolCall)({
2685
- tool: toolCall.name,
2686
- sig: (0, tool_telemetry_js_1.argsFingerprint)(toolCall.args),
2687
- ms: __ms,
2688
- ok: true,
2689
- resultChars: String(result.output ?? '').length,
2690
- prevSig: this.lastToolSig ?? null,
2691
- });
2692
- this.lastToolSig = (0, tool_telemetry_js_1.argsFingerprint)(toolCall.args);
2693
- }
2694
- catch { /* 任何优化项失败都不影响工具结果本身 */ }
2695
- }
2696
- if (result.success) {
2697
- consecutiveErrors = 0;
2698
- // 2026-07-29: Hermes 风格硬限制计数
2699
- totalToolCallsThisLoop++;
2700
- // 2026-10-01: 批处理工具(一次顶多次) ⇒ **退还**这次迭代 (奖励批处理, 压零碎调用)
2701
- if (this.iterBudget && (0, iteration_budget_js_1.isRefundableTool)(toolCall.name)) {
2702
- this.iterBudget.refund();
2703
- console.warn(`[PiAgent] ${toolCall.name} 是批量工具 ⇒ 退还 1 次迭代 (现 ${this.iterBudget.describe()})`);
2704
- }
2705
- lastNTools.push(toolCall.name);
2706
- if (lastNTools.length > MAX_IDEMPOTENT_TOOL)
2707
- lastNTools.shift();
2708
- if (result.output) {
2709
- this.successfulToolResults.push({ tool: toolCall.name, outputPreview: result.output.substring(0, 200) + (result.output.length > 200 ? '...' : '') });
2710
- }
2711
- else {
2712
- this.successfulToolResults.push({ tool: toolCall.name, outputPreview: '(无输出)' });
2713
- }
2714
- loopReviewCompletedTools.add(toolCall.name);
2715
- lastQualityScore = this.estimateToolResultQuality(result);
2716
- if (lastQualityScore < this.QUALITY_THRESHOLD && refineAttempts < this.MAX_REFINE_ATTEMPTS) {
2717
- refineAttempts++;
2718
- }
2719
- if (onStream) {
2720
- onStream({ type: 'status', internal: true, content: `🔄 工具执行完成,继续循环...`, tool: 'loop' });
2721
- }
2722
- }
2723
- else {
2724
- consecutiveErrors++;
2725
- // 2026-10-01 (优化 #3): 失败也记一行(带错误类别) ⇒ "错工具率"从这类错误里看得出来
2726
- try {
2727
- const __cls = (0, error_classifier_js_1.classifyError)(String(result.error || '')).label || '未分类';
2728
- (0, tool_telemetry_js_1.recordToolCall)({ tool: toolCall.name, sig: (0, tool_telemetry_js_1.argsFingerprint)(toolCall.args), ms: 0,
2729
- ok: false, resultChars: 0, prevSig: this.lastToolSig ?? null, errorClass: __cls });
2730
- this.lastToolSig = (0, tool_telemetry_js_1.argsFingerprint)(toolCall.args);
2731
- }
2732
- catch { /* 遥测失败不影响 */ }
2733
- totalErrors++;
2734
- if (toolCall.name === lastFailedTool) {
2735
- lastFailedToolCount++;
2736
- }
2737
- else {
2738
- lastFailedTool = toolCall.name;
2739
- lastFailedToolCount = 1;
2740
- }
2741
- console.warn(`[PiAgent] 工具 ${toolCall.name} 执行失败 (${lastFailedToolCount}/${MAX_SAME_TOOL_FAILURES}, 累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}): ${result.error}`);
2742
- // 2026-07-28: 注入 Observation + Reflection 替代旧 hardcode 提示
2743
- const obs = (0, error_classifier_js_1.buildObservation)(toolCall.name, toolCall.args, { success: false, error: result.error });
2744
- const ref = (0, error_classifier_js_1.buildReflection)(toolCall.name, result.error, totalErrors, lastFailedToolCount);
2745
- this.messageHistory.push({ role: 'system', content: (0, error_classifier_js_1.formatObservationWithReflection)(obs, ref) + SHELL_ESCAPE_HINT });
2746
- if (onStream)
2747
- onStream({ type: 'status', content: `💡 Reflection: ${obs.summary} → ${ref[0]?.action || '放弃'}`, tool: 'system' });
2748
- if (lastFailedToolCount >= MAX_SAME_TOOL_FAILURES) {
2749
- this.messageHistory.push({ role: 'system', content: `[注意] 工具 ${toolCall.name} 在这个上下文中不可用 (连续 ${MAX_SAME_TOOL_FAILURES} 次失败: ${result.error}). 请不要再次调用它, 直接用你已知的信息回答用户, 并在回答开头标记 <final gen>.` });
2750
- lastFailedTool = '';
2751
- lastFailedToolCount = 0;
2752
- consecutiveErrors = 0;
2753
- return;
2754
- }
2755
- if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
2756
- this.messageHistory.push({ role: 'system', content: `[注意] 前面的工具调用连续失败。请尝试其他工具或换一种方式完成用户请求, 或用 <final gen> 给出最终回答.` });
2757
- consecutiveErrors = 0;
2758
- }
2759
- }
2760
- }
2761
- catch (execError) {
2762
- consecutiveErrors++;
2763
- totalErrors++;
2764
- const errorResult = { success: false, error: String(execError) };
2765
- this.messageHistory.push({ role: 'tool', content: JSON.stringify(errorResult), toolResult: errorResult });
2766
- this.logToHarness(toolCall.name, toolCall.args, errorResult);
2767
- const obs = (0, error_classifier_js_1.buildObservation)(toolCall.name, toolCall.args, errorResult);
2768
- const ref = (0, error_classifier_js_1.buildReflection)(toolCall.name, errorResult.error, totalErrors, lastFailedToolCount);
2769
- this.messageHistory.push({ role: 'system', content: (0, error_classifier_js_1.formatObservationWithReflection)(obs, ref) + SHELL_ESCAPE_HINT });
2770
- if (onStream)
2771
- onStream({ type: 'status', content: `💡 Reflection: ${obs.summary}`, tool: 'system' });
2772
- console.error(`[PiAgent] 工具执行异常 (累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}): ${execError}`);
2773
- }
2774
- })); // end Promise.all(toolCalls.map(async ...))
2775
- // 所有工具执行完毕后, continue while 循环, 让 LLM 看到结果
2776
- continue;
2777
- }
2778
- else {
2779
- // LLM 返回的不是 tool call 格式
2780
- this.messageHistory.push({
2781
- role: 'assistant',
2782
- content: reply,
2783
- reasoningContent: response?.reasoningContent,
2784
- });
2785
- // 通知前端收到非工具调用回复 (2026-08-09: 完整内容, 不再截断 150)
2786
- if (onStream) {
2787
- onStream({ type: 'token', content: reply });
2788
- }
2789
- // 2026-06-19 架构 fix: 只有 strip <think> 后才检查 isFinalResponse
2790
- // (parseToolCall 已先尝试, 既然没解析出 tool_call, 现在检查 final gen 是否真的在最终回答区)
2791
- if (this.isFinalResponse(reply)) {
2792
- // 2026-06-19 dive-into 风格修复: 如果还有 successful tool results 没汇报,
2793
- // LLM 不能提前 final_gen — harness 自动注入"请汇报剩余工具结果" hint 再 continue
2794
- // 这是 dive-into 文档"step 9 stop condition check" 的具体化:
2795
- // stop condition = (有工具结果未汇报) ? continue : break
2796
- // 检查回复是否包含工具结果内容(避免无限循环)
2797
- const hasToolResultContent = this.successfulToolResults.some(r => reply.includes(r.tool) || reply.includes(r.outputPreview.substring(0, 50)));
2798
- // 如果回复包含工具结果内容,清除 successfulToolResults
2799
- if (hasToolResultContent) {
2800
- console.log(`[PiAgent] 回复包含工具结果内容, 清除 successfulToolResults (${this.successfulToolResults.length} 个)`);
2801
- this.successfulToolResults = [];
2802
- }
2803
- // 2026-08-10: 逃生门 — decideUnreported: 未达上限 → 再提示一次; 超限 → 清空积压强制 final (防死循环)
2804
- const unreportedDecision = decideUnreported(this.successfulToolResults.length, unreportedRetries, MAX_UNREPORTED_RETRIES);
2805
- if (unreportedDecision === 'retry' && iteration < this.MAX_REACT_ITERATIONS) {
2806
- unreportedRetries++;
2807
- const unreported = this.successfulToolResults.length;
2808
- console.log(`[PiAgent] LLM 想 final_gen 但还有 ${unreported} 个工具结果未汇报 (${unreportedRetries}/${MAX_UNREPORTED_RETRIES}), push hint 让其继续`);
2809
- this.messageHistory.push({
2810
- role: 'system',
2811
- content: `[dive-into stop condition] 你之前已成功执行了 ${unreported} 个工具, 但当前回复里没把它们的结果告诉用户. 请基于已有的工具结果 (在 history 里) 写一个完整总结回复给用户, 用 <final gen> 结尾. 不要再调工具.`
2812
- });
2813
- if (onStream) {
2814
- onStream({ type: 'status', internal: true, content: `🔄 还有 ${unreported} 个工具结果未汇报, 让 LLM 继续总结 (${unreportedRetries}/${MAX_UNREPORTED_RETRIES})`, tool: 'system' });
2815
- }
2816
- continue;
2817
- }
2818
- else if (unreportedDecision === 'force-final') {
2819
- // 反复提示仍未汇报超过上限 → 清空积压强制 final, 不再死循环
2820
- console.log(`[PiAgent] unreported 循环超限 (${unreportedRetries} 次), 清空积压强制 final`);
2821
- this.successfulToolResults = [];
2822
- this.messageHistory.push({
2823
- role: 'system',
2824
- content: `[dive-into stop condition] 已多次提示汇报工具结果仍未完成 (超过 ${MAX_UNREPORTED_RETRIES} 次). 现在直接基于你已知的信息写最终回复给用户, 用 <final gen> 结尾, 不要再调任何工具.`
2825
- });
2826
- if (onStream) {
2827
- onStream({ type: 'status', internal: true, content: `🔄 工具结果汇报超限, 强制收尾`, tool: 'system' });
2828
- }
2271
+ catch (gateErr) {
2272
+ tscAllowed = false;
2273
+ tscWhy = `harness-error: ${String(gateErr?.message || gateErr)}`;
2829
2274
  }
2830
- lastQualityScore = this.estimateResponseQuality(reply);
2831
- // 2026-07-29: 质量门 — 即使 LLM 声称完成, 质量太低也继续
2832
- if (lastQualityScore < this.QUALITY_THRESHOLD && refineAttempts < this.MAX_REFINE_ATTEMPTS) {
2833
- console.log(`[PiAgent] final gen 质量 ${lastQualityScore.toFixed(2)} < ${this.QUALITY_THRESHOLD}, 注入 refine hint`);
2834
- this.messageHistory.push({ role: 'system', content: `[质量检查] 你的回答质量评分为 ${(lastQualityScore * 10).toFixed(1)}/10, 低于 ${(this.QUALITY_THRESHOLD * 10).toFixed(1)}/10 阈值。请提供更完整、详细的回答, 包含工具调用获取到的具体信息, 末尾加 <final gen>。` });
2835
- refineAttempts++;
2836
- continue;
2837
- }
2838
- // 2026-08-08: final 前目标对齐 review — 不潦草收尾 (见 loop-review.ts)
2839
- // LLM 想 <final gen> 时, 先跑 1-2 次「目标对齐 + 需求深挖」review;
2840
- // 达成用户需求才放行真正结束. 达上限或无需深挖则以用户需求为准结束.
2841
- // 2026-09-16 (Milestone 1-B): 目标审查走唯一门面 (loop-review 是 Harness 的一个环节)
2842
- const reviewDecision = this.piHarness().reviewFinal({
2843
- reviewsDone: loopReviewCount,
2844
- // 2026-08-10: 传用户原始输入 (不是派生 intentHint) — LLM 对照原文自查完成度,
2845
- // 未完成 → 自动继续调工具 (自动触发后续步骤)
2846
- userIntent: this.currentUserInput,
2847
- completedTools: Array.from(loopReviewCompletedTools),
2848
- actionLog: loopActionLog,
2849
- runId: this.currentRunId || undefined,
2850
- goalId: this.currentGoalId || undefined,
2851
- }, loop_review_js_1.DEFAULT_MAX_REVIEWS);
2852
- if (reviewDecision.kind === 'continue-review') {
2853
- loopReviewCount++;
2854
- console.log(`[PiAgent] review ${loopReviewCount}/${loop_review_js_1.DEFAULT_MAX_REVIEWS}: LLM 想 final 但先对齐需求深挖一次`);
2855
- this.messageHistory.push({ role: 'system', content: reviewDecision.hint });
2856
- if (onStream) {
2857
- onStream({ type: 'status', internal: true, content: `🔄 目标对齐 review ${loopReviewCount}/${loop_review_js_1.DEFAULT_MAX_REVIEWS}: 深挖续跑`, tool: 'system' });
2858
- }
2859
- continue; // 让 LLM 看到 hint, 深挖或确认完成后再次 final
2860
- }
2861
- finalResponse = this.extractFinalAnswer(reply);
2862
- break;
2863
- }
2864
- // 检查是否需要继续循环处理
2865
- // 更严格的判断:只有当回复明确表示需要更多信息时才继续
2866
- const containsToolCallIntent = reply.includes('调用工具') || reply.includes('tool(') ||
2867
- reply.includes('使用工具') || reply.includes('需要获取') || reply.includes('需要查看') ||
2868
- // 兼容 LLM 用对象字面量输出 tool call (上轮没解析成功时, 至少要继续)
2869
- reply.includes('tool =>') || reply.includes('[TOOL_CALL]') ||
2870
- // 2026-06-15 修: 兼容 LLM 用 XML 标签输出 tool call (<shell_exec>...</shell_exec>)
2871
- // 这时 parseToolCall 失败, 至少要让 loop 继续
2872
- /<\w+>[\s\S]*?<\/\w+>/.test(reply);
2873
- const hasError = ['不存在', '找不到', '无法找到', 'not found', 'does not exist',
2874
- '错误', 'error', '失败', 'failed'].some(k => reply.includes(k));
2875
- const isTooShort = reply.length < 50 && reply.length > 0;
2876
- const hasQuestion = reply.includes('?') && (reply.includes('怎么') || reply.includes('如何') || reply.includes('什么'));
2877
- const needsMoreWork = hasError || containsToolCallIntent || isTooShort || hasQuestion;
2878
- if (needsMoreWork && iteration < this.MAX_REACT_ITERATIONS) {
2879
- console.log(`[PiAgent] 继续循环处理 (${iteration}/${this.MAX_REACT_ITERATIONS}): needsMoreWork=${needsMoreWork}, hasError=${hasError}, containsToolCallIntent=${containsToolCallIntent}`);
2880
- if (onStream) {
2881
- onStream({ type: 'status', internal: true, content: `🔄 继续处理,循环 ${iteration}...`, tool: 'loop' });
2882
- }
2883
- continue;
2884
- }
2885
- // 否则把这个当作可能的最终回答
2886
- finalResponse = reply;
2887
- if (onStream) {
2888
- onStream({ type: 'status', internal: true, content: `📝 提取最终回答,长度 ${reply.length}`, tool: 'system' });
2889
- }
2890
- break;
2891
- }
2892
- }
2893
- if (!finalResponse) {
2894
- // 走到这里通常是 LLM 一直在调同一个不存在的工具, 没输出 <final gen>
2895
- // 把已知的失败信息也带回去, 让用户知道发生了什么
2896
- const reason = lastFailedTool
2897
- ? `(工具 ${lastFailedTool} 连续 ${MAX_SAME_TOOL_FAILURES} 次失败, 已放弃)`
2898
- : `(共 ${iteration - 1} 轮无最终输出)`;
2899
- finalResponse = `抱歉,任务未能完成 ${reason}。请换个方式提问,或明确告诉 agent 不要调用工具。`;
2900
- if (onStream) {
2901
- onStream({ type: 'error', content: `⚠️ 任务未完成: ${reason}`, tool: 'system' });
2902
- }
2903
- }
2904
- // 通知前端循环完成
2905
- if (onStream) {
2906
- onStream({ type: 'status', internal: true, content: `✅ 处理完成,共 ${iteration - 1} 次循环`, tool: 'system' });
2907
- }
2908
- this.messageHistory.push({ role: 'assistant', content: finalResponse });
2909
- // React Harness: 循环结束
2910
- // 2026-09-16 (Milestone 1-B): 会话收尾走唯一门面
2911
- await this.piHarness().sessionEnd(this.harnessCtx());
2912
- // 2026-09-16: 收尾落盘 — done / failed / aborted / needs_human 如实写回 (不留幽灵 running)
2913
- if (this.currentRunId && !runExternallyPaused && !runExternallyAborted) {
2914
- try {
2915
- // 2026-09-16 (M4): 完成门 —— "模型说完成"不等于"系统确认完成"。
2916
- // 证据 = 成功步骤的事实摘要; 末尾还有失败步骤没被后续成功覆盖 → 不许 done。
2917
- const recBefore = await (0, run_store_js_1.readRun)(this.currentRunId).catch(() => null);
2918
- const steps = recBefore?.steps || [];
2919
- const evidence = steps.filter((s) => s.ok).map((s) => `${s.tool}: ${(s.summary || '').slice(0, 120)}`).slice(-10);
2920
- const lastStep = steps[steps.length - 1];
2921
- const trailingFailure = !!lastStep && !lastStep.ok;
2922
- /** 有工具步骤却一条成功证据都没有 → 不算完成 (证据门槛, 不是文案) */
2923
- const noEvidence = steps.length > 0 && evidence.length === 0;
2924
- const errText = runPersistenceFailure || this.breakerReason || runNeedsHuman || runStopReason || aiFailureReason || '';
2925
- const cls = errText ? (0, run_store_js_1.classifyError)(errText) : 'unknown';
2926
- // 持久化失败 / 熔断 / 鉴权 → needs_human; 预算/中止 → aborted; 末尾仍失败或无证据 → failed (不许 done)
2927
- const status = runPersistenceFailure
2928
- ? 'needs_human'
2929
- : (this.breakerReason || runNeedsHuman)
2930
- ? 'needs_human'
2931
- : runStopReason
2932
- ? 'aborted'
2933
- : aiFailed
2934
- ? (cls === 'auth' ? 'needs_human' : 'failed')
2935
- : (trailingFailure || noEvidence)
2936
- ? 'failed'
2937
- : 'done';
2938
- await (0, run_store_js_1.finishRun)(this.currentRunId, {
2939
- status,
2940
- summary: finalResponse ? String(finalResponse).slice(0, 400) : undefined,
2941
- error: errText
2942
- || (trailingFailure ? `末尾步骤失败 (${lastStep.tool}): ${(lastStep.error || '').slice(0, 150)}` : undefined)
2943
- || (noEvidence ? '有工具步骤但没有任何成功证据: 不许判 done' : undefined),
2944
- evidence,
2945
- });
2946
- }
2947
- catch (err) {
2948
- // 终态也写不下去: 这是最坏情况 —— 除了留痕, 没有别的自愈手段, 所以必须显眼
2949
- const message = `收尾落盘失败 (状态无法写回, 记录会停在 running): ${String(err?.message || err).slice(0, 200)}`;
2950
- console.error('[PiAgent] run-store finishRun 失败 (核心持久化):', message);
2951
- await (0, run_store_js_1.recordDegradation)({ kind: 'core', op: 'pi-sdk.finishRun', runId: this.currentRunId, message }).catch(() => { });
2952
- onStream?.({ type: 'error', content: `⚠️ ${message}`, tool: 'harness' });
2953
- }
2954
- // ★ 2026-09-25 (M0 接线冻结, 规则 ③ + ④): Run 结束**必过收尾漏斗**。
2955
- //
2956
- // 旧写法在这里另起了一套 Goal 侧收尾 (读证据 → `evaluateGoalCompletion` →
2957
- // `completeGoalIfEligible` / `setUnresolved`): 它**不收尾** —— 不写 Memory、不生成 Skill 候选、
2958
- // 不写权威 continuation、不留决策记录。于是同一条 Run 在 Supervisor 那里还会再收一次 (或反过来
2959
- // 这条路径根本不收), 全仓就有了两套"这一轮跑完意味着什么"。那一套已删除。
2960
- //
2961
- // 现在: `closeRunOnce` (幂等) → 9 步收尾 → 产物落盘; 再由 Goal reducer 把"下一步是什么"
2962
- // 落进 Goal (没有 Supervisor 宿主时 Runner 也要留下 continuation, 否则下一次还是从零开始)。
2963
- if (this.currentGoalId) {
2964
- try {
2965
- const wiring = await Promise.resolve().then(() => __importStar(require('./goal-flywheel-wiring.js')));
2966
- const { reduceGoalState } = await Promise.resolve().then(() => __importStar(require('./goal-state-reducer.js')));
2967
- const nowIso = new Date().toISOString();
2968
- const closed = await wiring.closeRunOnce({
2969
- goalId: this.currentGoalId,
2970
- runId: this.currentRunId,
2971
- caller: 'runner',
2972
- now: nowIso,
2973
- finalReview: finalResponse ? String(finalResponse).slice(0, 2000) : '',
2974
- });
2975
- if (wiring.isRefusal(closed)) {
2976
- onStream?.({ type: 'status', content: `⚠️ 收尾被拒 (不当作收过): ${closed.reason}`, tool: 'harness' });
2977
- }
2978
- else if (closed.outcome) {
2979
- const view = closed.outcome;
2980
- const applied = await reduceGoalState({
2981
- goalId: this.currentGoalId,
2982
- intent: 'closure_outcome',
2983
- now: nowIso,
2984
- by: 'runner',
2985
- outcome: {
2986
- goalStatus: wiring.goalStatusFromDecision(view.decision),
2987
- continuation: wiring.toGoalStoreContinuation(view.continuation),
2988
- reason: view.decision.reason,
2989
- },
2990
- runId: view.runId,
2991
- });
2992
- const line = view.decision.decision === 'complete'
2993
- ? `🎯 目标已达成 (仍要过完成门): ${view.decision.reason}`
2994
- : `🎯 目标仍在进行 (未判完成): ${view.decision.reason}`;
2995
- onStream?.({
2996
- type: 'status', internal: true,
2997
- content: `${line}${applied.gateRejected ? ` [完成门拒绝: ${applied.gateRejected}]` : ''} `
2998
- + `(收尾 ${view.steps} 步 · memory ${view.memories} · skill 候选 ${view.candidates} · goal=${this.currentGoalId})`,
2999
- tool: 'harness',
3000
- });
2275
+ if (!tscAllowed) {
2276
+ this.runCtx.eventSink?.({ type: 'status', content: `🔎 类型检查被门拒绝, 未执行: ${tscWhy.slice(0, 120)}`, tool: 'system' });
3001
2277
  }
3002
2278
  else {
3003
- // 幂等: 别处 (或上一次) 已经收过尾 —— 事实读不回来就如实说, 不重收也不假装
3004
- onStream?.({ type: 'status', content: `🎯 ${closed.reason}`, tool: 'harness' });
2279
+ const r = await tscTool.execute({});
2280
+ const line = (0, code_write_gate_js_1.formatTypecheckResult)(r?.success !== false && !/error TS\d+/.test(String(r?.output || '')), String(r?.output || ''));
2281
+ this.runCtx.eventSink?.({ type: 'status', content: `🔎 ${line}`, tool: 'system' });
3005
2282
  }
3006
2283
  }
3007
- catch (err) {
3008
- await (0, run_store_js_1.recordDegradation)({ kind: 'observational', op: 'pi-sdk.runClosure', runId: this.currentRunId, message: String(err?.message || err).slice(0, 160) }).catch(() => { });
3009
- }
3010
2284
  }
3011
- this.currentRunId = '';
3012
- }
3013
- else if (this.currentRunId) {
3014
- // 外部暂停/中止: 状态是人定的, 不覆盖 (paused 等 /resume; aborted 是终态)
3015
- onStream?.({ type: 'status', content: `⏹️ 运行状态保持为 ${runExternallyPaused ? 'paused' : 'aborted'} (由外部控制面决定)`, tool: 'harness' });
3016
- this.currentRunId = '';
2285
+ catch (e) {
2286
+ this.runCtx.eventSink?.({ type: 'status', content: `🔎 类型检查没能跑起来: ${String(e?.message || e).slice(0, 100)} (改动已落盘, 记得自己跑一次)`, tool: 'system' });
2287
+ }
2288
+ this.tsTouchedThisTurn = [];
3017
2289
  }
3018
- // 2026-06-16: 暴露 aiFailed 标志 — promptStream 据此决定是否自动重试整个 loop
3019
- return { reply: finalResponse, aiFailed, aiFailureReason: aiFailureReason || undefined };
3020
2290
  }
2291
+ // 2026-10-02 K4-B: 老 `runReActLoop` **已删除** (原 1123 行) —— 它曾经是"第二条 loop"
2292
+ // 并行存在的根源 (与 `WorkflowPivotLoop` 各跑一套, 行为按入口分叉: web=pivot / CLI=老 loop)。
2293
+ // 删除依据 (五条件): ① 唯一替代路径 = pivot (默认已切, 三处入口全走它) ② 全仓无引用 (只剩注释/测试名)
2294
+ // ③ 真跑覆盖 = CLI 非流式带工具改文件 ✓ · Web 流式改 TS + 自检 ✓ · 同通道并发 3 条 ✓ ④ 全量回归 ✓
2295
+ // ⑤ 可回滚提交点 = 本提交。
2296
+ // 它独有的记账 (TS 触达) 已上移到 pivot 的唯一执行点 `guardedExecute`, 口径归一。
3021
2297
  async deepThink(prompt) {
3022
2298
  const result = await this.thinkingEngine.think(prompt);
3023
2299
  let response = `深度思考完成(${result.depth}层):\n\n`;
@@ -3067,7 +2343,7 @@ ${await this.renderActivePlansSection()}
3067
2343
  // 失败静默: 任何 stage 抛错 → 走老 slice(-10) 逻辑
3068
2344
  //
3069
2345
  // P1.2: 如果 maybeAutoCompact 算过 Context Collapse 投影, 用 this.projectedHistory (读时投影, 非破坏)
3070
- const source = this.projectedHistory ?? this.messageHistory;
2346
+ const source = this.projectedHistory ?? this.actor.state.messageHistory;
3071
2347
  const recentMessages = this.compressHistorySync(source).slice(-10);
3072
2348
  return recentMessages.map(m => {
3073
2349
  if (m.role === 'user')
@@ -3094,7 +2370,7 @@ ${await this.renderActivePlansSection()}
3094
2370
  // 2026-08-06: 来源优先用 projectedHistory (Context Collapse 投影, 非破坏) —
3095
2371
  // 与 buildContext 一致; 之前只让字符串路径用投影, messages 数组路径被跳过,
3096
2372
  // 导致 LLM 实际看到的还是未压缩的历史.
3097
- const source = this.projectedHistory ?? this.messageHistory;
2373
+ const source = this.projectedHistory ?? this.actor.state.messageHistory;
3098
2374
  const WINDOW = 15;
3099
2375
  const out = [];
3100
2376
  // 2026-09-28 (前缀 KV 可命中): 带 CURRENT TURN 注入的条目要**原样回带**.
@@ -3173,11 +2449,11 @@ ${await this.renderActivePlansSection()}
3173
2449
  /**
3174
2450
  * 2026-09-28 (前缀 KV 可命中): 把 chat() 回带的当前轮写回自己的 messageHistory.
3175
2451
  * 逻辑在模块级 `writeBackCurrentTurnInto` (纯函数, 门可以直接驱它断言);
3176
- * 这里只负责接到 `this.messageHistory` 上, 并保证任何异常都静默 (写回失败不影响对话).
2452
+ * 这里只负责接到 `actor 的 messageHistory` 上, 并保证任何异常都静默 (写回失败不影响对话).
3177
2453
  */
3178
2454
  writeBackCurrentTurn(wire) {
3179
2455
  try {
3180
- return writeBackCurrentTurnInto(this.messageHistory, wire);
2456
+ return writeBackCurrentTurnInto(this.actor.state.messageHistory, wire);
3181
2457
  }
3182
2458
  catch (err) {
3183
2459
  console.warn('[PiAgent] writeBackCurrentTurn failed (silent):', err);
@@ -3191,7 +2467,7 @@ ${await this.renderActivePlansSection()}
3191
2467
  estimateHistoryTokens() {
3192
2468
  try {
3193
2469
  const { estimateTokens } = _piRequire('../context-compaction/index.js');
3194
- return estimateTokens(this.messageHistory);
2470
+ return estimateTokens(this.actor.state.messageHistory);
3195
2471
  }
3196
2472
  catch {
3197
2473
  return 0;
@@ -3214,8 +2490,10 @@ ${await this.renderActivePlansSection()}
3214
2490
  console.warn(`[PiAgent] reactive compaction pre-check (${estimated} tokens > 80% threshold)`);
3215
2491
  onStream?.({ type: 'status', internal: true, content: '⚠️ reactive compaction 预检触发', tool: 'recovery' });
3216
2492
  try {
3217
- const compacted = this.compressHistorySync(this.messageHistory);
3218
- this.messageHistory = compacted;
2493
+ // 同步压缩: 取快照与替换在**同一拍相邻两行** (中间没有 await) ⇒ 不存在"变换期间被追加"的窗口。
2494
+ // 若哪天这里插入 await, 必须改成 `actor.rebaseHistory(…, snapshotLen)` (见 channel-actor.ts)。
2495
+ const compacted = this.compressHistorySync(this.actor.state.messageHistory);
2496
+ this.replaceHistory(compacted);
3219
2497
  if (this.estimateHistoryTokens() > this.maxContextTokens() * 0.8) {
3220
2498
  await this.maybeAutoCompact(onStream, signal);
3221
2499
  }
@@ -3271,7 +2549,7 @@ ${await this.renderActivePlansSection()}
3271
2549
  try {
3272
2550
  const rc = String(response?.reasoningContent || '').trim();
3273
2551
  if (rc)
3274
- this.currentOnStream?.({ type: 'reasoning', content: rc });
2552
+ this.runCtx.eventSink?.({ type: 'reasoning', content: rc });
3275
2553
  }
3276
2554
  catch { /* 送不出去不影响主流程 */ } // 2026-06-30: 透传 toolCalls (OpenAI 协议 native) 给上层, 让 assistant message 能 emit 真 id
3277
2555
  return { reply: response.reply || '', toolCalls: response.toolCalls, messages: response.messages };
@@ -3311,8 +2589,8 @@ ${await this.renderActivePlansSection()}
3311
2589
  if (Array.isArray(contextOrMessages)) {
3312
2590
  contextOrMessages = this.buildContext();
3313
2591
  // 也清除最近一轮的 toolCalls, 防止再触发
3314
- if (this.messageHistory.length > 1) {
3315
- const last = this.messageHistory[this.messageHistory.length - 1];
2592
+ if (this.actor.state.messageHistory.length > 1) {
2593
+ const last = this.actor.state.messageHistory[this.actor.state.messageHistory.length - 1];
3316
2594
  if (last.role === 'assistant' && last.toolCalls) {
3317
2595
  delete last.toolCalls;
3318
2596
  }
@@ -3375,7 +2653,7 @@ ${await this.renderActivePlansSection()}
3375
2653
  * onStream 广播: 跟 computeJudgmentGate 风格一致 (phase 事件供 UI timeline 显示)
3376
2654
  */
3377
2655
  async maybeAutoCompact(onStream, signal) {
3378
- if (this.messageHistory.length < 10)
2656
+ if (this.actor.state.messageHistory.length < 10)
3379
2657
  return; // 历史太短, 不值得压
3380
2658
  onStream?.({ type: 'status', internal: true, content: '🗜️ 评估是否需要压缩上下文...', tool: 'compactor' });
3381
2659
  // 注入 LLM (用 getMinimax().chat, 与 judgment 注入门 / ReAct 循环同一来源)
@@ -3394,14 +2672,17 @@ ${await this.renderActivePlansSection()}
3394
2672
  const maxTokens = Math.max(4000, Math.round(cfg.maxTokens * cfg.compressionThreshold));
3395
2673
  const beforeTokens = this.estimateHistoryTokens();
3396
2674
  const { compactPipeline, isContextCollapseEnabled } = await Promise.resolve().then(() => __importStar(require('../context-compaction/index.js')));
3397
- const result = await compactPipeline(this.messageHistory, {
2675
+ // **K5 第 4 步**: 记下取快照时的长度 —— 压缩是 async, 从这一拍到 "落地" 之间是 await 窗口;
2676
+ // 期间 append 进来的消息必须由 `actor.rebaseHistory(…, snapshotLen)` 接回去, 否则会被整块替换丢掉。
2677
+ const snapshotLen = this.actor.state.messageHistory.length;
2678
+ const result = await compactPipeline(this.actor.state.messageHistory, {
3398
2679
  maxTokens,
3399
2680
  llmChat,
3400
2681
  collapseLlmChat: llmChat, // P1.2: Context Collapse 投影也用同一 LLM
3401
- cacheScope: this.currentChannelId || 'default',
2682
+ cacheScope: this.actor.state.channelId || 'default',
3402
2683
  });
3403
- if (result.compacted && result.history.length < this.messageHistory.length) {
3404
- const saved = this.messageHistory.length - result.history.length;
2684
+ if (result.compacted && result.history.length < this.actor.state.messageHistory.length) {
2685
+ const saved = this.actor.state.messageHistory.length - result.history.length;
3405
2686
  const stagesApplied = result.stages.filter((s) => s.applied).map((s) => s.stage).join(' → ');
3406
2687
  const afterTokens = this.estimateHistoryTokens();
3407
2688
  const savedTokens = Math.max(0, beforeTokens - afterTokens);
@@ -3419,7 +2700,17 @@ ${await this.renderActivePlansSection()}
3419
2700
  // messageHistory 不变 (非破坏)
3420
2701
  }
3421
2702
  else {
3422
- this.messageHistory = result.history; // 真破坏性更新
2703
+ // 真破坏性更新 —— **K5 第 4 步**: 绑定了 actor 就交它落地 (rebase: 保住变换期间的追加);
2704
+ // 未绑定的会话走原路径 (行为不变)。
2705
+ if (this.actor) {
2706
+ const { keptTail } = await this.actor.rebaseHistory(result.history, snapshotLen);
2707
+ if (keptTail > 0) {
2708
+ console.warn(`[PiAgent] 压缩落地: 保住变换期间新追加的 ${keptTail} 条消息 (快照后到达)`);
2709
+ }
2710
+ }
2711
+ else {
2712
+ this.replaceHistory(result.history);
2713
+ }
3423
2714
  this.projectedHistory = null;
3424
2715
  }
3425
2716
  // 2026-08-06: snapshot 记录 before/after + 摘要 (供恢复/调试/UI), 事件广播
@@ -3430,10 +2721,10 @@ ${await this.renderActivePlansSection()}
3430
2721
  afterTokens,
3431
2722
  summary: `压缩管道: ${summaryLine}; 节省 ${savedTokens} tokens / ${saved} 条消息`,
3432
2723
  preservedMemory: [
3433
- ...this.messageHistory.filter(m => m.role === 'user').slice(-3).map(m => (m.content || '').slice(0, 80)),
2724
+ ...this.actor.state.messageHistory.filter(m => m.role === 'user').slice(-3).map(m => (m.content || '').slice(0, 80)),
3434
2725
  ],
3435
- agentId: this.currentAgentId,
3436
- channelId: this.currentChannelId,
2726
+ agentId: this.actor.state.agentId,
2727
+ channelId: this.actor.state.channelId,
3437
2728
  });
3438
2729
  cm.markCompressComplete(snap);
3439
2730
  }
@@ -3799,21 +3090,46 @@ ${this.extractOperationsFromRef(operationsRef)}
3799
3090
  getOperationLogs() {
3800
3091
  return this.constraintLayer.getLogs();
3801
3092
  }
3093
+ /**
3094
+ * K10 ④: 通信端口的**注入面** —— 传输实现仍在本仓网络层 (`src/network/**`), 内核只认形状。
3095
+ * 换传输 (hyperswarm → iroh → …) 时只改这一处; 上层一律走 `submitTransport`。
3096
+ */
3097
+ transportPorts() {
3098
+ return {
3099
+ send: (peerId, kind, payload) => p2p_js_1.p2pNetwork.sendMessage(peerId, kind, payload),
3100
+ broadcast: (kind, payload) => p2p_js_1.p2pNetwork.broadcast(kind, payload),
3101
+ peers: () => p2p_js_1.p2pNetwork.getPeers(),
3102
+ peersSync: () => p2p_js_1.p2pNetwork.getPeers(),
3103
+ };
3104
+ }
3802
3105
  listPeers() {
3803
- const peers = p2p_js_1.p2pNetwork.getPeers();
3106
+ // K10 ④ 后半: 同步读也经端口 (`transportPeersSync`); 端口未注入 ⇒ 明确说"读不到", 不假装"没有对端"
3107
+ const peers = (0, transport_js_1.transportPeersSync)(this.transportPorts());
3108
+ if (peers === null)
3109
+ return '(通信端口未注入: 读不到对端列表 —— 不是"没有对端", 是没接上)';
3804
3110
  if (peers.length === 0) {
3805
3111
  return '当前无连接的对等节点';
3806
3112
  }
3807
3113
  return `已连接节点 (${peers.length}):\n${peers.map(p => ` - ${p}`).join('\n')}`;
3808
3114
  }
3809
3115
  getPeers() {
3116
+ const peers = (0, transport_js_1.transportPeersSync)(this.transportPorts());
3117
+ if (peers !== null)
3118
+ return peers;
3119
+ // 端口未注入时**回落到传输直读**并留痕: 同步读路径没有"拒绝"的余地, 静默返回空列表会被误读成"没有对端"
3120
+ console.warn('[kernel-transport] peersSync 端口未注入 ⇒ 回落直读传输 (K10 ④ 未接线的调用点)');
3810
3121
  return p2p_js_1.p2pNetwork.getPeers();
3811
3122
  }
3812
3123
  async sendMessage(peerId, message) {
3813
- await p2p_js_1.p2pNetwork.sendMessage(peerId, 'message', message);
3124
+ // K10 ④: 经内核通信端口。发送有副作用 ⇒ 未注入/被拒**一律响亮** (静默丢消息比报错更糟)
3125
+ const out = await (0, transport_js_1.submitTransport)({ op: 'send', origin: 'pi-session', peerId, kind: 'message', payload: message }, this.transportPorts());
3126
+ if (!out.ok)
3127
+ throw new Error(`[kernel-transport] send 未发出: ${out.detail ?? '未知原因'}`);
3814
3128
  }
3815
3129
  async broadcast(message) {
3816
- await p2p_js_1.p2pNetwork.broadcast('message', message);
3130
+ const out = await (0, transport_js_1.submitTransport)({ op: 'broadcast', origin: 'pi-session', kind: 'message', payload: message }, this.transportPorts());
3131
+ if (!out.ok)
3132
+ throw new Error(`[kernel-transport] broadcast 未发出: ${out.detail ?? '未知原因'}`);
3817
3133
  }
3818
3134
  getIdentity() {
3819
3135
  return { ...this.identity };
@@ -3828,7 +3144,7 @@ ${this.extractOperationsFromRef(operationsRef)}
3828
3144
  Object.assign(this.identity, updates);
3829
3145
  }
3830
3146
  setCurrentChannelId(channelId) {
3831
- this.currentChannelId = channelId;
3147
+ this.actor.state.channelId = channelId;
3832
3148
  }
3833
3149
  getSessionState() {
3834
3150
  return this.sessionManager.getState();
@@ -3843,12 +3159,12 @@ ${this.extractOperationsFromRef(operationsRef)}
3843
3159
  await this.sessionManager.savePersona(persona);
3844
3160
  // 2026-10-01: **写透** —— 有身份文档的 agent 不套 persona.json, 只写 JSON 等于没改
3845
3161
  // (用户报"每次让智能体改都是同一个") ⇒ 同步落进它自己的身份文档 (标记区内)
3846
- if (this.currentAgentId) {
3162
+ if (this.actor.state.agentId) {
3847
3163
  try {
3848
3164
  const { applyPersonaToDocs } = await Promise.resolve().then(() => __importStar(require('../bootstrap/persona-init.js')));
3849
- const wrote = await applyPersonaToDocs(this.currentAgentId, persona);
3165
+ const wrote = await applyPersonaToDocs(this.actor.state.agentId, persona);
3850
3166
  if (wrote.length)
3851
- console.warn(`[persona] 已把 ${this.currentAgentId} 的 persona 写进身份文档: ${wrote.join(' · ')}`);
3167
+ console.warn(`[persona] 已把 ${this.actor.state.agentId} 的 persona 写进身份文档: ${wrote.join(' · ')}`);
3852
3168
  }
3853
3169
  catch { /* 非致命 */ }
3854
3170
  }
@@ -3910,13 +3226,58 @@ ${this.extractOperationsFromRef(operationsRef)}
3910
3226
  this.socialHeartbeat = null;
3911
3227
  }
3912
3228
  }
3229
+ /**
3230
+ * **受门包装**的 skill 面 (2026-10-02 · leo 口径 (b): 原裸出口已收口)。
3231
+ *
3232
+ * 为什么不能返回裸 registry: 拿到裸 registry 就有**三条**绕过 Harness 的路径 ——
3233
+ * ① `registry.execute(n, p)` ② `registry.get(n).execute(p)` ③ `registry.list()[i].execute(p)`
3234
+ * (后两条藏在返回的 `Skill` 对象里, 只看方法名看不出来)。
3235
+ * 现在三条**都**经本会话唯一受门口: `executeSkill` ⇒ `createSkillGuard()` ⇒ `piHarness().beforeToolCall()`,
3236
+ * 执行落在 registry 的唯一调用点 (**恰一次**), 被拒 ⇒ **零执行** 且返回 `拒绝: [rejectedBy] reason`。
3237
+ *
3238
+ * ⚠️ **行为变更 (可能属破坏性)**: 旧用法 `.execute('x', p)` 在**被拒时不再执行**, 改为返回拒绝串
3239
+ * (与 `executeSkill` 同形)。方法名/签名不变 ⇒ **源码级兼容**; 但"被拒即不执行"是新约定。
3240
+ *
3241
+ * **管不到的一条 (如实记)**: 调用方**自己**在 `register(skill)` 时传入的那个 skill 对象仍有裸 `execute`
3242
+ * —— 那是调用方的对象, 不属本出口能管的面。收口管的是**本出口给出的任何引用**。
3243
+ */
3913
3244
  getSkillRegistry() {
3914
- return this.skillRegistry;
3245
+ // 任何被交出去的 Skill 引用, 其 execute 都换成受门版本 (堵 ②③ 两条隐藏路径)
3246
+ const guarded = (sk) => ({
3247
+ name: sk.name,
3248
+ description: sk.description,
3249
+ execute: (params) => this.executeSkill(sk.name, params),
3250
+ });
3251
+ return {
3252
+ register: (sk) => this.skillRegistry.register(sk),
3253
+ unregister: (name) => this.skillRegistry.unregister(name),
3254
+ has: (name) => this.skillRegistry.has(name),
3255
+ get: (name) => { const sk = this.skillRegistry.get(name); return sk ? guarded(sk) : undefined; },
3256
+ list: () => this.skillRegistry.list().map(guarded),
3257
+ execute: (name, params) => this.executeSkill(name, params),
3258
+ };
3915
3259
  }
3916
3260
  registerSkill(skill) {
3917
3261
  this.skillRegistry.register(skill);
3918
3262
  }
3263
+ /**
3264
+ * 2026-10-02 (K7) **唯一 skill 执行口**:
3265
+ * 判定一律经 Harness (`createSkillGuard()` ⇒ `piHarness().beforeToolCall`, 身份带 runId/goalId/agentId/channelId);
3266
+ * 执行落在本会话 registry 的**唯一**调用点 (**恰一次**)。
3267
+ * **fail-closed**: 门抛错 ⇒ 返回 `拒绝: [harness-error] …`, **绝不回落**直调 registry。
3268
+ * 公开签名不变 (仍返回 string) ⇒ 不破坏已发布契约 (leo 的方案 a: 保留 API, 内部降为经门转发)。
3269
+ */
3919
3270
  async executeSkill(name, params) {
3271
+ let decision;
3272
+ try {
3273
+ decision = await this.createSkillGuard()(name, params);
3274
+ }
3275
+ catch (guardErr) {
3276
+ return `拒绝: [harness-error] ${String(guardErr?.message ?? guardErr)}`;
3277
+ }
3278
+ if (!decision.allow) {
3279
+ return `拒绝: [${decision.rejectedBy || 'harness'}] ${decision.reason || '未说明理由'}`;
3280
+ }
3920
3281
  return this.skillRegistry.execute(name, params);
3921
3282
  }
3922
3283
  async addUserAction(content, importance) {