@bolloon/bolloon-agent 0.5.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/agents/parse-tool-call.js +6 -1
  2. package/dist/agents/pi-sdk-session-factory.js +36 -3
  3. package/dist/agents/pi-sdk.js +349 -199
  4. package/dist/agents/run-context.js +20 -0
  5. package/dist/agents/runner-resolver.js +2 -1
  6. package/dist/electron-build/agents/parse-tool-call.js +6 -1
  7. package/dist/electron-build/agents/parse-tool-call.js.map +1 -1
  8. package/dist/electron-build/agents/pi-sdk-session-factory.js +36 -3
  9. package/dist/electron-build/agents/pi-sdk-session-factory.js.map +1 -1
  10. package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
  11. package/dist/electron-build/agents/pi-sdk.js +349 -199
  12. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  13. package/dist/electron-build/agents/run-context.js +24 -0
  14. package/dist/electron-build/agents/run-context.js.map +1 -0
  15. package/dist/electron-build/agents/runner-resolver.js +2 -1
  16. package/dist/electron-build/agents/runner-resolver.js.map +1 -1
  17. package/dist/electron-build/kernel/channel-actor.js +314 -0
  18. package/dist/electron-build/kernel/channel-actor.js.map +1 -0
  19. package/dist/index.js +24 -19
  20. package/dist/kernel/channel-actor.js +297 -0
  21. package/dist/kernel/control.js +95 -0
  22. package/dist/kernel/gate-scan.js +1036 -0
  23. package/dist/kernel/model-runtime.js +472 -0
  24. package/dist/kernel/plan-channel-actor.js +298 -0
  25. package/dist/kernel/plan-constraint.js +165 -0
  26. package/dist/kernel/plan-deletion.js +32 -0
  27. package/dist/kernel/plan-modelruntime.js +60 -0
  28. package/dist/kernel/plan-runcontext.js +163 -0
  29. package/dist/kernel/plan.js +153 -0
  30. package/dist/kernel/roster.js +242 -0
  31. package/dist/web/bolloon-web.json +3 -3
  32. package/dist/web/client.js +3 -2
  33. package/dist/web/routes-tasks.js +5 -1
  34. package/dist/web/server.js +58 -18
  35. package/dist/web/sw.js +1 -1
  36. package/dist/web/ui/message-renderer.js +473 -29
  37. package/dist/web/ui/step-timeline.js +13 -12
  38. package/package.json +1 -1
  39. package/scripts/build-web.ts +74 -1
  40. package/dist/constraint-runtime/_archive_helper.d.ts +0 -1
  41. package/dist/constraint-runtime/_archive_helper.js +0 -9
  42. package/dist/constraint-runtime/assistant/index.d.ts +0 -11
  43. package/dist/constraint-runtime/assistant/index.js +0 -12
  44. package/dist/constraint-runtime/bootstrap/index.d.ts +0 -11
  45. package/dist/constraint-runtime/bootstrap/index.js +0 -12
  46. package/dist/constraint-runtime/bridge/index.d.ts +0 -11
  47. package/dist/constraint-runtime/bridge/index.js +0 -12
  48. package/dist/constraint-runtime/buddy/index.d.ts +0 -11
  49. package/dist/constraint-runtime/buddy/index.js +0 -12
  50. package/dist/constraint-runtime/cli/index.d.ts +0 -11
  51. package/dist/constraint-runtime/cli/index.js +0 -12
  52. package/dist/constraint-runtime/components/index.d.ts +0 -11
  53. package/dist/constraint-runtime/components/index.js +0 -12
  54. package/dist/constraint-runtime/constants/index.d.ts +0 -11
  55. package/dist/constraint-runtime/constants/index.js +0 -12
  56. package/dist/constraint-runtime/coordinator/index.d.ts +0 -11
  57. package/dist/constraint-runtime/coordinator/index.js +0 -12
  58. package/dist/constraint-runtime/cost_hook.d.ts +0 -2
  59. package/dist/constraint-runtime/cost_hook.js +0 -4
  60. package/dist/constraint-runtime/entrypoints/index.d.ts +0 -11
  61. package/dist/constraint-runtime/entrypoints/index.js +0 -12
  62. package/dist/constraint-runtime/execution_registry.d.ts +0 -20
  63. package/dist/constraint-runtime/execution_registry.js +0 -44
  64. package/dist/constraint-runtime/hooks/index.d.ts +0 -11
  65. package/dist/constraint-runtime/hooks/index.js +0 -12
  66. package/dist/constraint-runtime/ink.d.ts +0 -1
  67. package/dist/constraint-runtime/ink.js +0 -4
  68. package/dist/constraint-runtime/keybindings/index.d.ts +0 -11
  69. package/dist/constraint-runtime/keybindings/index.js +0 -12
  70. package/dist/constraint-runtime/memdir/index.d.ts +0 -11
  71. package/dist/constraint-runtime/memdir/index.js +0 -12
  72. package/dist/constraint-runtime/migrations/index.d.ts +0 -11
  73. package/dist/constraint-runtime/migrations/index.js +0 -12
  74. package/dist/constraint-runtime/moreright/index.d.ts +0 -11
  75. package/dist/constraint-runtime/moreright/index.js +0 -12
  76. package/dist/constraint-runtime/native_ts/index.d.ts +0 -11
  77. package/dist/constraint-runtime/native_ts/index.js +0 -12
  78. package/dist/constraint-runtime/output_styles/index.d.ts +0 -11
  79. package/dist/constraint-runtime/output_styles/index.js +0 -12
  80. package/dist/constraint-runtime/plugins/index.d.ts +0 -11
  81. package/dist/constraint-runtime/plugins/index.js +0 -12
  82. package/dist/constraint-runtime/port_manifest.d.ts +0 -7
  83. package/dist/constraint-runtime/port_manifest.js +0 -11
  84. package/dist/constraint-runtime/query.d.ts +0 -6
  85. package/dist/constraint-runtime/query.js +0 -1
  86. package/dist/constraint-runtime/remote/index.d.ts +0 -11
  87. package/dist/constraint-runtime/remote/index.js +0 -12
  88. package/dist/constraint-runtime/schemas/index.d.ts +0 -11
  89. package/dist/constraint-runtime/schemas/index.js +0 -12
  90. package/dist/constraint-runtime/screens/index.d.ts +0 -11
  91. package/dist/constraint-runtime/screens/index.js +0 -12
  92. package/dist/constraint-runtime/server/index.d.ts +0 -11
  93. package/dist/constraint-runtime/server/index.js +0 -12
  94. package/dist/constraint-runtime/services/index.d.ts +0 -11
  95. package/dist/constraint-runtime/services/index.js +0 -12
  96. package/dist/constraint-runtime/state/index.d.ts +0 -11
  97. package/dist/constraint-runtime/state/index.js +0 -12
  98. package/dist/constraint-runtime/system_init.d.ts +0 -1
  99. package/dist/constraint-runtime/system_init.js +0 -20
  100. package/dist/constraint-runtime/types/index.d.ts +0 -11
  101. package/dist/constraint-runtime/types/index.js +0 -12
  102. package/dist/constraint-runtime/upstream_proxy/index.d.ts +0 -11
  103. package/dist/constraint-runtime/upstream_proxy/index.js +0 -12
  104. package/dist/constraint-runtime/utils/index.d.ts +0 -11
  105. package/dist/constraint-runtime/utils/index.js +0 -12
  106. package/dist/constraint-runtime/vim/index.d.ts +0 -11
  107. package/dist/constraint-runtime/vim/index.js +0 -12
  108. package/dist/constraint-runtime/voice/index.d.ts +0 -11
  109. package/dist/constraint-runtime/voice/index.js +0 -12
@@ -13,6 +13,8 @@ import * as fs from 'fs/promises';
13
13
  import * as fsSync from 'fs';
14
14
  import * as os from 'os';
15
15
  import * as path from 'path';
16
+ import { createRunContext } from './run-context.js';
17
+ import { createPrivateActor } from '../kernel/channel-actor.js';
16
18
  import { expandHomeArgs } from './tool-path-args.js';
17
19
  import { renderDelegateNotices, pushNotice, renderNoticeBlock } from './background-notices.js';
18
20
  import { runWithWriteOrigin } from './skill-ledger.js';
@@ -215,7 +217,90 @@ export class PiAgentSession {
215
217
  sessionManager;
216
218
  agentsManager;
217
219
  socialHeartbeat = null;
218
- messageHistory = [];
220
+ /**
221
+ * **K5 第 4 步 (所有权转移)**: history 的**本体**。
222
+ * · 未绑定 actor ⇒ 本体在这里 (绑定前的暂存);
223
+ * · 绑定 actor 后 ⇒ 本体是 `actor.state.messageHistory`, 本地那份被**收养**并清空。
224
+ * 绑定由 `attachActor()` 做, 且**只在会话身份已知时**发生 (见 session factory) —— 没有身份就不归属。
225
+ */
226
+ /**
227
+ * **K5 第 4 步 — history 写入的唯一漏斗 (push)**。
228
+ * 为什么必须同步: 调用点遍布 ReAct 循环 / 工具回灌 / 压缩, 而且**写完立刻读** (length / 索引 / slice),
229
+ * 改成 await 会改变同拍可见性。所以这里有意识地**不排队** —— 归属收敛 + 可数 (门能断言"零处直写"),
230
+ * 并发安全由入口投递进 mailbox 负责 (K5 第 5 步)。
231
+ */
232
+ pushHistory(...msgs) {
233
+ for (const m of msgs) {
234
+ // 步骤⑧ 起**只有一条路径**: 写 actor (实例侧暂存字段已删除) —— 也彻底消除了"自递归"的可能
235
+ this.actor.appendMessageSync(m);
236
+ }
237
+ }
238
+ /** **K5 第 4 步 — history 写入漏斗 (pop)** —— 同 pushHistory 的性质 */
239
+ popHistory() {
240
+ return this.actor.popMessageSync();
241
+ }
242
+ /** **K5 第 4 步 — history 写入漏斗 (整体替换)**: hydrate 回灌 / 压缩后的整块赋值都走这里 */
243
+ /**
244
+ * **整体替换**历史 —— 唯一漏斗之一, 步骤⑧ 起是**唯一**的"种历史"入口 (公开)。
245
+ * 公开的理由: 上层/测试要种一份历史时必须走这里; 直接给对象赋一个新数组会**换掉数组身份**
246
+ * (actor 里那份仍是空的) —— 那是静默丢数据 (实测: 删掉 setter 后 `session.messageHistory = [...]`
247
+ * 变成写在一个凭空出现的自有属性上, 测试于是看到 0 条)。
248
+ */
249
+ replaceHistory(next) {
250
+ this.actor.replaceHistory(next);
251
+ }
252
+ /**
253
+ * **K5 步骤⑦ — 把一次性请求里的绑定落到位** (入口不再散着设字段: 只交一个请求)。
254
+ * 写入都走访问器 ⇒ 本体落进 `actor.state` (步骤③ 起就是这样); 没给的不覆盖 (与环境变量"只覆盖显式给出的"同秩)。
255
+ */
256
+ applyExecutionRequest(req) {
257
+ if (req.channelId)
258
+ this.actor.state.channelId = req.channelId;
259
+ if (req.agentId)
260
+ this.actor.state.agentId = req.agentId;
261
+ if (req.goalId)
262
+ this.actor.state.goalBinding = req.goalId;
263
+ if (req.resumeRunId)
264
+ this.resumeRunId = req.resumeRunId;
265
+ }
266
+ /**
267
+ * **K5 步骤⑦ — Pi 的唯一执行入口**: 只接收一次性 `ExecutionRequest`。
268
+ * 有 `onStream` ⇒ 走 `promptStream` (流式); 否则走 `prompt`。绑定由 `applyExecutionRequest` 落位。
269
+ * 入口侧只需: `deliverThroughActor(session, () => session.runExecution(req))`。
270
+ */
271
+ async runExecution(req) {
272
+ this.applyExecutionRequest(req);
273
+ if (req.onStream) {
274
+ return this.promptStream(req.input, req.onStream, req.signal, req.channelId);
275
+ }
276
+ return this.prompt(req.input, { signal: req.signal, channelId: req.channelId });
277
+ }
278
+ /**
279
+ * **K5 第 4 步**: 绑定 actor 并**收养**绑定前已存在的本地历史。
280
+ * 收养规则: actor 侧为空 且 本地非空 ⇒ 搬过去 (任何绑定时序都不丢历史); 收养后清空本地。
281
+ * 约束 (全量回归打出来的): 调用方必须用**会话身份**当注册键; 没有身份就**不要调**这个方法。
282
+ */
283
+ attachActor(actor) {
284
+ const prev = this.actor;
285
+ this.actor = actor;
286
+ if (!prev || prev === actor)
287
+ return;
288
+ // **收养**: 把上一个 actor (通常是"出生时的私有 actor") 的状态搬进新家。
289
+ // 新家已有值时不覆盖 —— 同一会话身份以先到者为准 (禁止"后到的调用把先建的状态冲掉")。
290
+ const from = prev.state;
291
+ const to = actor.state;
292
+ if (from.messageHistory.length > 0 && to.messageHistory.length === 0) {
293
+ to.messageHistory.push(...from.messageHistory);
294
+ }
295
+ if (from.channelId && !to.channelId)
296
+ to.channelId = from.channelId;
297
+ if (from.agentId && !to.agentId)
298
+ to.agentId = from.agentId;
299
+ if (from.goalBinding && !to.goalBinding)
300
+ to.goalBinding = from.goalBinding;
301
+ if (from.activeRun && !to.activeRun)
302
+ to.activeRun = from.activeRun;
303
+ }
219
304
  tools = new Map();
220
305
  /** 2026-06-30: tool registry 模块 — 独立 alias resolve, 测试可消融. */
221
306
  _toolRegistry = new ToolRegistry();
@@ -378,11 +463,29 @@ export class PiAgentSession {
378
463
  // 拼到 systemPrompt 末尾, 别再混进 user message
379
464
  contextHintAddition = '';
380
465
  /**
381
- * 当前 onStream 引用 + abort signal (computeJudgmentGate 需要 onStream 广播 phase)
382
- * 每次 prompt / promptStream / promptWithPivotLoop 入口设置, 用完即清
466
+ * **K2 收尾 (leo 2026-10-02 口径)**: **唯一一处**把"已有的恢复身份"播种进 RunContext。
467
+ *
468
+ * · `currentRunId` 属 **run-boundary**, 不是循环内部的可变 RunContext 状态 ⇒ 只允许这一次显式播种读取;
469
+ * · 冻结 `CURRENT_RUN_ID_SEED_READS = 1` (见 src/kernel/plan-runcontext.ts), 由门强制;
470
+ * · 只发生在**进入一次 Run 时**; 不得在循环中新增读取点; 不得成为新的写入口;
471
+ * · K5 Channel Actor 完成后改为 `createRunContext({ runId: request.resumeRunId })`, 届时删除 `currentRunId` 字段本体。
472
+ */
473
+ seedRunContext(extra = {}) {
474
+ return createRunContext({ runId: this.actor.state.activeRun, ...extra });
475
+ }
476
+ /**
477
+ * **K5 迁移中**: 本会话所属 channel 的 Actor (由 session factory 在创建时绑定)。
478
+ * 现在只做**归属** —— 会话状态 (messageHistory / channelId / agentId / goalId) **仍然**在实例字段上,
479
+ * 逐项迁入 Actor 见 `src/kernel/plan-channel-actor.ts` 的 8 步。
480
+ */
481
+ actor;
482
+ /**
483
+ * **K2 迁移中**: 一次 Run 的显式状态载体 (见 src/agents/run-context.ts)。
484
+ * 入口 (prompt / promptStream / promptWithPivotLoop) 用 createRunContext() 快照一次, 用完即清。
485
+ * **已外置**: `eventSink` (原 currentOnStream) · `abortSignal` (原 currentSignal)。
486
+ * 其余 6 个字段仍以本类实例字段为准, 逐格迁移。
383
487
  */
384
- currentOnStream = null;
385
- currentSignal = null;
488
+ runCtx = createRunContext();
386
489
  /** Bootstrap SessionStart 拼的 system prompt 片段 (用完即清) */
387
490
  bootstrapAddition = '';
388
491
  /** 2026-08-12 (Task3 认知卸载): 工具选择与认知卸载指南 — 注入 system prompt, 降低"模型不触发 write/edit/read"率. */
@@ -396,12 +499,17 @@ export class PiAgentSession {
396
499
  - 每步只选一个最合适的工具, 先读后写, 写完验证 (跑 tsc/vitest 或读回文件).`;
397
500
  /** 当前 prompt 开始时间 (供 Stop hook 计算 durationMs) */
398
501
  promptStartTime = 0;
399
- /** 当前 channel id (由 getAgentForChannel / prompt 4 参注入, 供 hook / log 使用) */
400
- currentChannelId = '';
401
- /** 2026-07-04: 当前 agentId (server.ts 通过 createAgentSession 选项注入), 供 onSessionStart 加载 persona docs */
402
- currentAgentId = '';
403
- /** M2.2 (2026-06-17): 当前轮的用户请求 intent, runReActLoop 拼 systemPrompt 时会读这个 */
404
- currentIntent = 'chitchat';
502
+ /**
503
+ * **K5 步骤③ (会话绑定迁入 Actor)**: 三处绑定 (channelId / agentId / goalBinding) 的本体住进
504
+ * `actor.state`, 实例上只留**绑定前的暂存** (`_channelId` 等), 由 `attachActor()` 收养后清空。
505
+ * 用访问器而不是改调用点: 写入点 (prompt 入口 / Goal 绑定 / snapshot) 分散且语义各异, 逐个改风险大;
506
+ * 访问器让所有读写自动落到 actor (同一个值对象), 调用点零改动。
507
+ * 注意: 这三个字段的**访问数不变** (台账按 "self.currentX" 形态计数) —— 迁的是**所有权**, 不是删访问。
508
+ * 本注释刻意不写出那种带 `this.` 前缀的字面形态: 计数口径只剥 `//` 行注释, 块注释里的同形串会被算进去
509
+ * (曾因此把 currentChannelId 的计数虚增 1, 被门照出)。
510
+ */
511
+ // **K5 步骤⑧ 批次4**: channelId 的访问器已删 —— 本体 `actor.state.channelId`, 直接影响本体。
512
+ // M2.2 intent 已外置到 runCtx.intent (K2); 拼 systemPrompt 时读 this.runCtx.intent
405
513
  /** 2026-08-10: 本轮用户原始输入 (loop-review 任务动词兜底检测用) */
406
514
  currentUserInput = '';
407
515
  /**
@@ -409,7 +517,16 @@ export class PiAgentSession {
409
517
  * 之前只有"跑完才写"的轨迹 (trajectory-store, fire-and-forget, 崩了就没有), 没有跨重载可见的
410
518
  * 运行事实, 也没有预算闸门/失速巡检; 这里补的就是那一层。
411
519
  */
412
- currentRunId = '';
520
+ /**
521
+ * **K5 步骤⑥ (Run 身份归属 Actor)**: 本体是 `actor.state.activeRun`。
522
+ * · 未绑定 actor ⇒ 住在这里 (`_runId`, 绑定前的暂存);
523
+ * · 绑定后 ⇒ 读写全落 actor (同一个值), 由 `attachActor()` 收养绑定前的值。
524
+ * 语义: "本会话当前活跃的 Run" —— 一个会话身份同时只有一个活跃 Run ✓ 与 Actor 的粒度一致。
525
+ * 注意: 访问数**不变** —— 迁的是**所有权**不是删访问 (与步骤③同理)。
526
+ * 本注释刻意不写出"带 self. 前缀 + 该字段名"的那种字面形态: K2 的计数只剥 `//` 行注释,
527
+ * 块注释里的同形串会被算进去 (同类踩过三次, 每次都被门照出)。
528
+ */
529
+ // **K5 步骤⑧ 批次5 (收尾)**: currentRunId 访问器已删 —— Run 身份的本体是 `actor.state.activeRun`, 直接读写本体。
413
530
  /** 上一次运行的 runId (收尾不清空; 见 getLastRunId) */
414
531
  lastRunId = '';
415
532
  runSurface = 'cli';
@@ -426,14 +543,14 @@ export class PiAgentSession {
426
543
  currentIntentHint = '';
427
544
  /**
428
545
  * 算 judgment 注入门: 失败静默, 不阻塞主对话
429
- * 期间通过 currentOnStream 广播 phase 事件, 前端可显示 "正在检索判断力..." 状态
546
+ * 期间通过 runCtx.eventSink 广播 phase 事件, 前端可显示 "正在检索判断力..." 状态
430
547
  * 调用方负责用完即清 (judgmentGateAddition='')
431
548
  */
432
549
  async computeJudgmentGate(input) {
433
550
  const safePhase = (phase, extra = {}) => {
434
551
  try {
435
- if (this.currentOnStream) {
436
- this.currentOnStream({ type: 'phase', phase, ...extra, content: '' });
552
+ if (this.runCtx.eventSink) {
553
+ this.runCtx.eventSink({ type: 'phase', phase, ...extra, content: '' });
437
554
  }
438
555
  }
439
556
  catch { /* 静默 */ }
@@ -475,19 +592,23 @@ export class PiAgentSession {
475
592
  this.judgmentGateNegativeUsedIds = [];
476
593
  }
477
594
  constructor(config) {
595
+ // **K5 步骤⑧**: actor 从出生就在 —— 工厂通常在**构造前**就把身份 actor 注进来 (`config.actor`);
596
+ // 没有身份时用一份**私有 actor** (不注册 ⇒ 谁也拿不到, 隔离性优先)。
597
+ // ⇒ 实例侧不再需要"绑定前暂存"字段 (那些字段本步删除)。
598
+ this.actor = config.actor ?? createPrivateActor();
478
599
  this.cwd = config.cwd;
479
600
  this.peerId = config.peerId || 'local';
480
601
  this.identity = config.identityDoc || this.createDefaultIdentity();
481
602
  this.minimaxAvailable = this.checkMinimax();
482
603
  // 2026-07-04: 透传 agentId (server.ts 通过 createAgentSession 选项注入)
483
- this.currentAgentId = config.agentId || '';
604
+ this.actor.state.agentId = config.agentId || '';
484
605
  // 2026-10-01 **身份自愈** (用户实测: get_identity 返回 "DID: " 空 + 各 channel 名字串台):
485
606
  // 工具上下文里的身份来自 this.identity, 而它可能是 config.identityDoc 与 session 实例身份的
486
607
  // 合并结果 —— **空 did 会盖掉真 did** ✗; 名字也可能缺失或被别处覆盖。
487
608
  // 规矩: 缺 did / 名字时, 按 **currentAgentId** 从真身份生成器补齐 (did:key + 落盘密钥),
488
609
  // 名字优先取该 agent 自己的 persona.json。绝不回落到全局/用户身份名。
489
610
  try {
490
- if (this.currentAgentId) {
611
+ if (this.actor.state.agentId) {
491
612
  const cur = this.identity || {};
492
613
  const curDid = String(cur.did || '');
493
614
  const badDid = !curDid || /^did:(local|pi):/i.test(curDid);
@@ -495,17 +616,17 @@ export class PiAgentSession {
495
616
  // 2026-10-01 加固: **did 与本人密钥不符** 也算坏 —— 用户实测 get_identity 返回一个
496
617
  // 数据里根本不存在的 did (按 peerId 当 scope 新造的), 而它非空、非假值 ⇒ 上一版判据漏过 ✗。
497
618
  // 规矩: 只要 currentAgentId 有值, 身份的 did 就必须等于该 agent 自己的密钥 did。
498
- const mine = loadOrCreateAgentIdentity(this.currentAgentId);
619
+ const mine = loadOrCreateAgentIdentity(this.actor.state.agentId);
499
620
  const mismatch = !!mine?.did && !!curDid && curDid !== mine.did;
500
621
  if (badDid || missingName || mismatch) {
501
622
  if (badDid || mismatch) {
502
- console.warn(`[identity] 身份纠正: ${this.currentAgentId} 的 did ${curDid ? curDid.slice(0, 26) : '(空)'} ⇒ ${mine.did.slice(0, 26)} (以该 agent 自己的密钥为准)`);
623
+ console.warn(`[identity] 身份纠正: ${this.actor.state.agentId} 的 did ${curDid ? curDid.slice(0, 26) : '(空)'} ⇒ ${mine.did.slice(0, 26)} (以该 agent 自己的密钥为准)`);
503
624
  }
504
625
  // 2026-10-01: 同样必须**原地改** —— 这段在 registerTools() **之后**跑, 工具上下文已经按引用
505
626
  // 捕获了 this.identity; 换对象 ⇒ 工具看不到纠正结果 ✗ (同类 bug, 一并修)
506
627
  Object.assign(this.identity, {
507
628
  ...(mine?.did ? { did: mine.did, publicKey: mine.publicKey || cur.publicKey } : {}),
508
- ...(missingName ? { name: agentPersonaName(this.currentAgentId) || this.currentAgentId } : {}),
629
+ ...(missingName ? { name: agentPersonaName(this.actor.state.agentId) || this.actor.state.agentId } : {}),
509
630
  });
510
631
  }
511
632
  }
@@ -515,7 +636,7 @@ export class PiAgentSession {
515
636
  this._sessionStore = config.sessionStore ?? defaultSessionStore;
516
637
  this.constraintLayer = new ConstraintLayer();
517
638
  this.workflowEngine = new WorkflowEngine(this.constraintLayer);
518
- this.sessionManager = new PiSessionManager(this.identity.did, this.cwd, this.currentAgentId);
639
+ this.sessionManager = new PiSessionManager(this.identity.did, this.cwd, this.actor.state.agentId);
519
640
  this.agentsManager = new DiscoveredAgentsManager();
520
641
  this.usePivotLoop = config.usePivotLoop ?? false;
521
642
  this.pivotLoopConfig = config.pivotLoopConfig;
@@ -601,6 +722,18 @@ export class PiAgentSession {
601
722
  */
602
723
  async hydrateMessageHistory(sessionKey, maxMessages) {
603
724
  try {
725
+ // **K5 第 4 步**: 实现体 (load → filter → 截断 → 替换) 已搬进 Actor —— 走邮箱 ⇒ 与 append/persist 串行。
726
+ // 业务侧只交两个**纯回调** (内核因此不必 import 业务模块); 未绑定 actor 的会话走下面原路径 (行为不变)。
727
+ if (this.actor) {
728
+ const n = await this.actor.hydrateHistory({
729
+ load: () => this._sessionStore.loadMessages(sessionKey),
730
+ filter: (loaded) => this._filterToMessage(loaded),
731
+ maxMessages,
732
+ });
733
+ if (n > 0)
734
+ console.log(`[PiAgent] 从 ${sessionKey} 回灌 ${n} 条历史 (经 Channel Actor)`);
735
+ return;
736
+ }
604
737
  const loaded = await this._sessionStore.loadMessages(sessionKey);
605
738
  if (!loaded) {
606
739
  console.log(`[PiAgent] hydrate: 没有 ${sessionKey} 的历史`);
@@ -608,7 +741,7 @@ export class PiAgentSession {
608
741
  }
609
742
  const hydrated = this._filterToMessage(loaded).slice(-maxMessages);
610
743
  if (hydrated.length > 0) {
611
- this.messageHistory = hydrated;
744
+ this.replaceHistory(hydrated);
612
745
  console.log(`[PiAgent] 从 ${sessionKey} 回灌 ${hydrated.length} 条历史`);
613
746
  }
614
747
  }
@@ -628,7 +761,10 @@ export class PiAgentSession {
628
761
  * 即可获得"重启 / 跨进程接续"的语义.
629
762
  */
630
763
  async saveCurrentSession(key) {
631
- const persisted = this.messageHistory.map((m) => ({
764
+ // **K5 第 4 步**: 取数拍走 Actor 的快照 (走邮箱 ⇒ 与 append 串行, 不会抓到"边写边读"的半截状态);
765
+ // 未绑定 actor 的会话走原路径 (行为不变)。
766
+ const source = this.actor ? await this.actor.historySnapshot() : this.actor.state.messageHistory;
767
+ const persisted = source.map((m) => ({
632
768
  role: m.role,
633
769
  content: m.content,
634
770
  toolCall: m.toolCall,
@@ -645,9 +781,9 @@ export class PiAgentSession {
645
781
  * 与 loadSessionKey (构造时读) 不同: 这个是 session 已建好后再读.
646
782
  */
647
783
  async resumeSession(key, maxMessages = 30) {
648
- const before = this.messageHistory.length;
784
+ const before = this.actor.state.messageHistory.length;
649
785
  await this.hydrateMessageHistory(key, maxMessages);
650
- return this.messageHistory.length - before;
786
+ return this.actor.state.messageHistory.length - before;
651
787
  }
652
788
  /**
653
789
  * 2026-06-30: 读历史不修改 messageHistory.
@@ -819,7 +955,7 @@ export class PiAgentSession {
819
955
  const { CHANNELS_PATH } = await import('../web/server-types.js');
820
956
  const { loadChannels } = await import('../web/server-storage.js');
821
957
  const channels = await loadChannels();
822
- const ch = channels.find((c) => c.id === this.currentChannelId);
958
+ const ch = channels.find((c) => c.id === this.actor.state.channelId);
823
959
  if (ch && ch.encryptedPrivateKey && ch.encryptedPrivateKeyIv && ch.walletAddress) {
824
960
  return {
825
961
  encryptedPrivateKey: ch.encryptedPrivateKey,
@@ -894,7 +1030,7 @@ export class PiAgentSession {
894
1030
  // 2026-10-01: 原先自造 `did:pi:<peerId>` —— **不是有效 DID** (仓里 server.ts 自己都把 did:pi:
895
1031
  // 当"待升级"占位; 用户实测 get_identity 报 did:pi:ch_1785668060213)。
896
1032
  // 改用仓里既有的真身份生成器 (agent-identity.loadOrCreateAgentIdentity, 同步, 产 did:key + 落盘密钥)。
897
- const scope = this.currentAgentId || this.peerId || 'default';
1033
+ const scope = this.actor.state.agentId || this.peerId || 'default';
898
1034
  try {
899
1035
  const real = loadOrCreateAgentIdentity(scope);
900
1036
  return {
@@ -953,9 +1089,9 @@ export class PiAgentSession {
953
1089
  const did = await resolveUserDid();
954
1090
  const model = this.llmConfig?.model || this.model || '';
955
1091
  return new TrajectoryRecorder({
956
- agentId: this.currentAgentId || 'default',
1092
+ agentId: this.actor.state.agentId || 'default',
957
1093
  input,
958
- channelId: channelId || this.currentChannelId,
1094
+ channelId: channelId || this.actor.state.channelId,
959
1095
  did: did || undefined,
960
1096
  model: typeof model === 'string' && model ? model : undefined,
961
1097
  });
@@ -997,8 +1133,8 @@ export class PiAgentSession {
997
1133
  if (gate !== 'ready') {
998
1134
  const why = `初始化未就绪 (${gate}, 当前阶段 ${state.stage})${state.lastError ? ` — ${state.lastError.message}` : ''}`;
999
1135
  const hint = (state.actions && state.actions[0]) || '运行 `bolloon setup` 完成初始化';
1000
- this.messageHistory.push({ role: 'user', content: input });
1001
- this.messageHistory.push({ role: 'assistant', content: `[初始化未就绪] ${why}\n${hint}` });
1136
+ this.pushHistory({ role: 'user', content: input });
1137
+ this.pushHistory({ role: 'assistant', content: `[初始化未就绪] ${why}\n${hint}` });
1002
1138
  console.warn(`[PiAgent] 拒绝执行: ${why}`);
1003
1139
  return `[初始化未就绪] ${why}\n${hint}`;
1004
1140
  }
@@ -1006,13 +1142,13 @@ export class PiAgentSession {
1006
1142
  catch { /* 门禁自身异常 → 按"不能判定"处理会阻塞一切; 这里只在能读到状态时才拦 */ }
1007
1143
  }
1008
1144
  this.minimaxAvailable = this.checkMinimax();
1009
- this.currentChannelId = options?.channelId ?? this.currentChannelId;
1145
+ this.actor.state.channelId = options?.channelId ?? this.actor.state.channelId;
1010
1146
  // 2026-08-08: 运行轨迹采集 (落盘 + OrbitDB, 失败静默) — 包裹 onStream 收集步骤事件
1011
1147
  const trajRec = await this.createTrajectoryRecorder(input, options?.channelId);
1012
1148
  if (trajRec && options?.onStream) {
1013
1149
  options = { ...options, onStream: this.wrapTrajectoryStream(options.onStream, trajRec) };
1014
1150
  }
1015
- this.messageHistory.push({
1151
+ this.pushHistory({
1016
1152
  role: 'user',
1017
1153
  content: input
1018
1154
  });
@@ -1022,23 +1158,23 @@ export class PiAgentSession {
1022
1158
  // 于是把"什么都没跑"当成一次正常执行 (这正是"agent 跑了但没记录"的老毛病在 fallback 路径的翻版)。
1023
1159
  const response = await this.handleFallback(input);
1024
1160
  try {
1025
- let boundGoalId = this.currentGoalId;
1161
+ let boundGoalId = this.actor.state.goalBinding;
1026
1162
  if (!boundGoalId) {
1027
1163
  const g = await createGoal({
1028
1164
  objective: this.currentUserInput || input.slice(0, 200),
1029
- channelId: this.currentChannelId || undefined,
1030
- agentId: this.currentAgentId || undefined,
1165
+ channelId: this.actor.state.channelId || undefined,
1166
+ agentId: this.actor.state.agentId || undefined,
1031
1167
  createdBy: this.runSurface,
1032
1168
  });
1033
1169
  boundGoalId = g.goalId;
1034
1170
  }
1035
- this.currentGoalId = boundGoalId;
1171
+ this.actor.state.goalBinding = boundGoalId;
1036
1172
  const rec = await startRun({
1037
1173
  surface: this.runSurface,
1038
1174
  goal: this.currentUserInput || input.slice(0, 200),
1039
1175
  goalId: boundGoalId,
1040
- channelId: this.currentChannelId || undefined,
1041
- agentId: this.currentAgentId || undefined,
1176
+ channelId: this.actor.state.channelId || undefined,
1177
+ agentId: this.actor.state.agentId || undefined,
1042
1178
  modelConfig: await this.runModelSnapshot(),
1043
1179
  });
1044
1180
  this.lastRunId = rec.runId;
@@ -1077,25 +1213,27 @@ export class PiAgentSession {
1077
1213
  catch (err) {
1078
1214
  await recordDegradation({ kind: 'core', op: 'pi-sdk.fallbackRun', runId: this.lastRunId || '', message: String(err?.message || err).slice(0, 160) }).catch(() => { });
1079
1215
  }
1080
- this.messageHistory.push({ role: 'assistant', content: response });
1216
+ this.pushHistory({ role: 'assistant', content: response });
1081
1217
  this.reportUsageToContextManager();
1082
1218
  this.finishTrajectory(trajRec, response);
1083
1219
  return response;
1084
1220
  }
1085
1221
  // P0 注入门
1086
- this.currentSignal = options?.signal ?? null;
1087
- this.currentOnStream = options?.onStream ?? null;
1222
+ // K2: eventSink + abortSignal 已外置, 入口一次建好本轮 Context。
1223
+ // 其余字段**不在这里抄一份** —— 抄写会新增对旧实例字段的读, 违反方向判据
1224
+ // 「新层出现后旧写口调用点数只许不变或减少」。逐字段迁移时再各自搬进来。
1225
+ this.runCtx = this.seedRunContext({ eventSink: options?.onStream ?? null, abortSignal: options?.signal ?? null });
1088
1226
  await this.computeJudgmentGate(input);
1089
1227
  // M2.2 (2026-06-17): intent 分类 — prompt() 路径也跑 (跟 promptStream 对齐)
1090
1228
  try {
1091
1229
  const { classifyIntent, intentHint } = await import('./intent-classifier.js');
1092
- this.currentIntent = classifyIntent(input);
1093
- this.currentIntentHint = intentHint(this.currentIntent);
1230
+ this.runCtx.intent = classifyIntent(input);
1231
+ this.currentIntentHint = intentHint(this.runCtx.intent);
1094
1232
  this.currentUserInput = input;
1095
1233
  }
1096
1234
  catch (err) {
1097
1235
  console.warn('[PiAgent] classifyIntent in prompt() failed:', err);
1098
- this.currentIntent = 'chitchat';
1236
+ this.runCtx.intent = 'chitchat';
1099
1237
  this.currentIntentHint = '';
1100
1238
  }
1101
1239
  // P2: 解析当前 permission mode
@@ -1120,14 +1258,13 @@ export class PiAgentSession {
1120
1258
  recordJudgmentUsage(this.judgmentGateUsedIds, { userInput: input }).catch((err) => console.warn('[PiAgent] recordJudgmentUsage failed:', err));
1121
1259
  }
1122
1260
  this.clearJudgmentGate();
1123
- this.currentSignal = null;
1124
- this.currentOnStream = null;
1261
+ this.runCtx = createRunContext(); // K2: 用完即清 (换新 Context, 不继承残留);
1125
1262
  this.reportUsageToContextManager();
1126
1263
  }
1127
1264
  }
1128
1265
  try {
1129
1266
  // 2026-06-16: runReActLoop 现在返回 { reply, aiFailed, aiFailureReason } — 这里只需 reply 字符串
1130
- const loopResult = await this.runReActLoop(this.currentOnStream ?? undefined, options?.signal);
1267
+ const loopResult = await this.runReActLoop(this.runCtx.eventSink ?? undefined, options?.signal);
1131
1268
  // 2026-10-01: **本回合改过 TS ⇒ 收尾自动类型检查** (一轮一次) —— 复用已有的 tsc_check 工具
1132
1269
  if (decideTypecheck(this.tsTouchedThisTurn, this.typecheckRanThisTurn)) {
1133
1270
  this.typecheckRanThisTurn = true;
@@ -1136,11 +1273,11 @@ export class PiAgentSession {
1136
1273
  if (tscTool?.execute) {
1137
1274
  const r = await tscTool.execute({});
1138
1275
  const line = formatTypecheckResult(r?.success !== false && !/error TS\d+/.test(String(r?.output || '')), String(r?.output || ''));
1139
- this.currentOnStream?.({ type: 'status', content: `🔎 ${line}`, tool: 'system' });
1276
+ this.runCtx.eventSink?.({ type: 'status', content: `🔎 ${line}`, tool: 'system' });
1140
1277
  }
1141
1278
  }
1142
1279
  catch (e) {
1143
- this.currentOnStream?.({ type: 'status', content: `🔎 类型检查没能跑起来: ${String(e?.message || e).slice(0, 100)} (改动已落盘, 记得自己跑一次)`, tool: 'system' });
1280
+ this.runCtx.eventSink?.({ type: 'status', content: `🔎 类型检查没能跑起来: ${String(e?.message || e).slice(0, 100)} (改动已落盘, 记得自己跑一次)`, tool: 'system' });
1144
1281
  }
1145
1282
  this.tsTouchedThisTurn = [];
1146
1283
  }
@@ -1172,7 +1309,7 @@ export class PiAgentSession {
1172
1309
  }
1173
1310
  catch { /* 落日志失败不影响主流程 */ }
1174
1311
  try {
1175
- this.currentOnStream?.({ type: 'status', content: `📚 复盘: ${m}`, tool: 'system' });
1312
+ this.runCtx.eventSink?.({ type: 'status', content: `📚 复盘: ${m}`, tool: 'system' });
1176
1313
  }
1177
1314
  catch { /* 上屏失败不打断 */ }
1178
1315
  };
@@ -1239,8 +1376,7 @@ export class PiAgentSession {
1239
1376
  recordJudgmentUsage(this.judgmentGateNegativeUsedIds, { userInput: input, polarity: 'negative' }).catch((err) => console.warn('[PiAgent] recordJudgmentUsage (negative) failed:', err));
1240
1377
  }
1241
1378
  this.clearJudgmentGate();
1242
- this.currentSignal = null;
1243
- this.currentOnStream = null;
1379
+ this.runCtx = createRunContext(); // K2: 用完即清 (换新 Context, 不继承残留);
1244
1380
  this.reportUsageToContextManager();
1245
1381
  }
1246
1382
  }
@@ -1248,7 +1384,7 @@ export class PiAgentSession {
1248
1384
  console.log(`[PiAgent.promptStream] ENTRY, channelId=${channelId}, input chars=${input.length}`);
1249
1385
  this.minimaxAvailable = this.checkMinimax();
1250
1386
  console.log(`[PiAgent.promptStream] minimaxAvailable=${this.minimaxAvailable}`);
1251
- this.currentChannelId = channelId ?? this.currentChannelId;
1387
+ this.actor.state.channelId = channelId ?? this.actor.state.channelId;
1252
1388
  // 2026-08-08: 运行轨迹采集 (落盘 + OrbitDB, 失败静默) — 包裹 onStream 收集步骤事件
1253
1389
  const trajRec = await this.createTrajectoryRecorder(input, channelId);
1254
1390
  if (trajRec)
@@ -1261,7 +1397,7 @@ export class PiAgentSession {
1261
1397
  const userText = markerMatch ? markerMatch[1].trim() : input;
1262
1398
  const contextHint = markerMatch ? input.replace(markerMatch[0], '').trim() : '';
1263
1399
  console.log(`[PiAgent.promptStream] marker matched=${!!markerMatch}, userText chars=${userText.length}, contextHint chars=${contextHint.length}`);
1264
- this.messageHistory.push({
1400
+ this.pushHistory({
1265
1401
  role: 'user',
1266
1402
  content: userText
1267
1403
  });
@@ -1271,7 +1407,7 @@ export class PiAgentSession {
1271
1407
  // 让 agent 能"回忆起"之前 session 的记忆 (自动获取之前 session), 而非只靠启动时批量压缩.
1272
1408
  try {
1273
1409
  const { recallMemory } = await import('./memory-recall.js');
1274
- const recalled = await recallMemory({ query: userText, agentId: this.currentAgentId || this.peerId || '' });
1410
+ const recalled = await recallMemory({ query: userText, agentId: this.actor.state.agentId || this.peerId || '' });
1275
1411
  if (recalled) {
1276
1412
  this.contextHintAddition = [this.contextHintAddition, recalled].filter(Boolean).join('\n\n');
1277
1413
  }
@@ -1280,29 +1416,29 @@ export class PiAgentSession {
1280
1416
  onStream({ type: 'thinking', content: '🤔 开始思考...' });
1281
1417
  if (!this.minimaxAvailable) {
1282
1418
  const response = await this.handleFallback(userText);
1283
- this.messageHistory.push({ role: 'assistant', content: response });
1419
+ this.pushHistory({ role: 'assistant', content: response });
1284
1420
  onStream({ type: 'done', content: '' });
1285
1421
  this.reportUsageToContextManager();
1286
1422
  this.finishTrajectory(trajRec, response);
1287
1423
  return response;
1288
1424
  }
1289
- // P0 注入门: 缓存 onStream + signal, computeJudgmentGate 用 currentOnStream 广播 phase
1290
- this.currentOnStream = onStream;
1291
- this.currentSignal = signal ?? null;
1425
+ // P0 注入门: 缓存 onStream + signal, computeJudgmentGate 用 runCtx.eventSink 广播 phase
1426
+ // K2: eventSink + abortSignal 一起建进本轮 Context (只搬已外置的字段, 不抄未迁移的)
1427
+ this.runCtx = this.seedRunContext({ eventSink: onStream, abortSignal: signal ?? null });
1292
1428
  await this.computeJudgmentGate(userText);
1293
1429
  // M2.2 (2026-06-17): intent 分类 — 0 LLM 成本, 5 行 keyword 匹配
1294
1430
  try {
1295
1431
  const { classifyIntent, intentHint } = await import('./intent-classifier.js');
1296
- this.currentIntent = classifyIntent(userText);
1297
- this.currentIntentHint = intentHint(this.currentIntent);
1432
+ this.runCtx.intent = classifyIntent(userText);
1433
+ this.currentIntentHint = intentHint(this.runCtx.intent);
1298
1434
  this.currentUserInput = userText;
1299
- if (this.currentIntent !== 'chitchat') {
1300
- onStream({ type: 'phase', phase: 'intent_classified', detail: this.currentIntent, content: '' });
1435
+ if (this.runCtx.intent !== 'chitchat') {
1436
+ onStream({ type: 'phase', phase: 'intent_classified', detail: this.runCtx.intent, content: '' });
1301
1437
  }
1302
1438
  }
1303
1439
  catch (err) {
1304
1440
  console.warn('[PiAgent] classifyIntent failed (non-fatal):', err);
1305
- this.currentIntent = 'chitchat';
1441
+ this.runCtx.intent = 'chitchat';
1306
1442
  this.currentIntentHint = '';
1307
1443
  }
1308
1444
  // P1.1: 异步跑 Auto-Compact (LLM 摘要, 仅在 budget 超限时触发, 失败静默)
@@ -1319,8 +1455,8 @@ export class PiAgentSession {
1319
1455
  let bootstrapAddition = '';
1320
1456
  try {
1321
1457
  const ss = await onSessionStart({
1322
- channelId: this.currentChannelId || undefined,
1323
- agentId: this.currentAgentId || undefined,
1458
+ channelId: this.actor.state.channelId || undefined,
1459
+ agentId: this.actor.state.agentId || undefined,
1324
1460
  });
1325
1461
  bootstrapAddition = ss.systemAddition || '';
1326
1462
  }
@@ -1329,10 +1465,10 @@ export class PiAgentSession {
1329
1465
  }
1330
1466
  // 2026-07-07 P1-B: 注入最近 5 条项目事件日志 (L2) — 让 LLM 知道项目状态/feature 变化
1331
1467
  // 失败静默, append 到 bootstrapAddition 末尾 (超 800 字截断)
1332
- if (this.currentChannelId) {
1468
+ if (this.actor.state.channelId) {
1333
1469
  try {
1334
1470
  const { getRecentEvents } = await import('../bootstrap/event-log.js');
1335
- const events = await getRecentEvents(this.currentChannelId, 5);
1471
+ const events = await getRecentEvents(this.actor.state.channelId, 5);
1336
1472
  if (events.length > 0) {
1337
1473
  const eventBlock = [
1338
1474
  '## 最近项目事件 (最近 5 条, 倒序)',
@@ -1347,7 +1483,7 @@ export class PiAgentSession {
1347
1483
  // 2026-07-07 P2-C: 注入项目当前状态 (L3) — 目标/约束/待办/已完成
1348
1484
  try {
1349
1485
  const { readState, formatStateForPrompt } = await import('../bootstrap/project-state.js');
1350
- const state = await readState({ channelId: this.currentChannelId });
1486
+ const state = await readState({ channelId: this.actor.state.channelId });
1351
1487
  const stateText = formatStateForPrompt(state);
1352
1488
  if (stateText) {
1353
1489
  bootstrapAddition = (bootstrapAddition + '\n\n' + stateText).slice(-2500);
@@ -1359,7 +1495,7 @@ export class PiAgentSession {
1359
1495
  // 2026-07-07 P2-C: 向量检索 top-3 (L4) — 按当前 channelId + userText 找历史相关片段
1360
1496
  try {
1361
1497
  const { searchIndex } = await import('../bootstrap/vector-index.js');
1362
- const indexName = `channel-${this.currentChannelId}`;
1498
+ const indexName = `channel-${this.actor.state.channelId}`;
1363
1499
  const results = await searchIndex({
1364
1500
  indexName,
1365
1501
  query: userText,
@@ -1420,13 +1556,12 @@ export class PiAgentSession {
1420
1556
  monitorAfterReply(userText, pivotResult);
1421
1557
  const stopStartTime = this.promptStartTime || Date.now();
1422
1558
  onStop({
1423
- channelId: this.currentChannelId || 'unknown',
1559
+ channelId: this.actor.state.channelId || 'unknown',
1424
1560
  durationMs: Date.now() - stopStartTime,
1425
1561
  usedJudgmentIds: [...this.judgmentGateUsedIds],
1426
1562
  }).catch((err) => console.warn('[PiAgent] onStop failed:', err));
1427
1563
  this.clearJudgmentGate();
1428
- this.currentOnStream = null;
1429
- this.currentSignal = null;
1564
+ this.runCtx = createRunContext(); // K2: 用完即清 (换新 Context, 不继承残留);
1430
1565
  this.bootstrapAddition = '';
1431
1566
  this.contextHintAddition = '';
1432
1567
  this.promptStartTime = 0;
@@ -1456,8 +1591,7 @@ export class PiAgentSession {
1456
1591
  }
1457
1592
  catch (err) {
1458
1593
  // abort 失败: 视作"已中断", 抛错让上层用 partial 兜底
1459
- this.currentOnStream = null;
1460
- this.currentSignal = null;
1594
+ this.runCtx = createRunContext(); // K2: 用完即清 (换新 Context, 不继承残留);
1461
1595
  throw err;
1462
1596
  }
1463
1597
  attempt++;
@@ -1495,8 +1629,8 @@ export class PiAgentSession {
1495
1629
  // 因为 messageHistory.push({role:'user'}) 在 promptStream 顶部已经做过, 重跑 runReActLoop 不会重复 push,
1496
1630
  // 但 assistant 失败那条也别留 (留了会污染下一轮 LLM context).
1497
1631
  // 简化: 重试前 pop 一次 assistant (如果最后一条是 assistant)
1498
- if (this.messageHistory.length > 0 && this.messageHistory[this.messageHistory.length - 1].role === 'assistant') {
1499
- this.messageHistory.pop();
1632
+ if (this.actor.state.messageHistory.length > 0 && this.actor.state.messageHistory[this.actor.state.messageHistory.length - 1].role === 'assistant') {
1633
+ this.popHistory();
1500
1634
  }
1501
1635
  }
1502
1636
  onStream({ type: 'done', content: '' });
@@ -1514,15 +1648,14 @@ export class PiAgentSession {
1514
1648
  // Bootstrap Stop hook: fire-and-forget 写本次 session 摘要
1515
1649
  const stopStartTime = this.promptStartTime || Date.now();
1516
1650
  onStop({
1517
- channelId: this.currentChannelId || 'unknown',
1651
+ channelId: this.actor.state.channelId || 'unknown',
1518
1652
  durationMs: Date.now() - stopStartTime,
1519
1653
  usedJudgmentIds: [...this.judgmentGateUsedIds],
1520
1654
  }).catch((err) => console.warn('[PiAgent] onStop failed:', err));
1521
1655
  // 用完即清, 避免污染下一轮
1522
1656
  this.clearJudgmentGate();
1523
- this.currentOnStream = null;
1657
+ this.runCtx = createRunContext(); // K2: 用完即清 (换新 Context, 不继承残留);
1524
1658
  this.reportUsageToContextManager();
1525
- this.currentSignal = null;
1526
1659
  this.bootstrapAddition = '';
1527
1660
  this.promptStartTime = 0;
1528
1661
  // 2026-08-08: 轨迹收尾 — 落盘 + OrbitDB (失败静默, 不阻塞回复)
@@ -1530,7 +1663,7 @@ export class PiAgentSession {
1530
1663
  return result;
1531
1664
  }
1532
1665
  async promptWithPivotLoop(input, config, channelId) {
1533
- this.currentChannelId = channelId ?? this.currentChannelId;
1666
+ this.actor.state.channelId = channelId ?? this.actor.state.channelId;
1534
1667
  if (!this.minimaxAvailable) {
1535
1668
  const response = await this.handleFallback(input);
1536
1669
  return {
@@ -1562,8 +1695,8 @@ export class PiAgentSession {
1562
1695
  // M2.2 (2026-06-17): intent 分类 — pivot loop 也要拿到 hint
1563
1696
  try {
1564
1697
  const { classifyIntent, intentHint } = await import('./intent-classifier.js');
1565
- this.currentIntent = classifyIntent(input);
1566
- this.currentIntentHint = intentHint(this.currentIntent);
1698
+ this.runCtx.intent = classifyIntent(input);
1699
+ this.currentIntentHint = intentHint(this.runCtx.intent);
1567
1700
  this.currentUserInput = input;
1568
1701
  }
1569
1702
  catch (err) {
@@ -1610,21 +1743,21 @@ ${await this.renderActivePlansSection()}
1610
1743
  - 工具调用前可以简短思考 (1-2 句话), 但**不要写长篇 thinking** (会撞 max_tokens)
1611
1744
  - 工具调用后必须等结果, 不要在同一个回复里继续输出
1612
1745
  - <final gen> 只在**真完成所有任务**时输出, 不要在工具调用前/中输出${this.judgmentGateAddition}${this.contextHintAddition}`;
1613
- // 2026-06-15: 把 currentOnStream 传给 loop, 让 step-timeline 在 pivot 循环里也能 emit step_start/done
1746
+ // 2026-06-15: 把 runCtx.eventSink 传给 loop, 让 step-timeline 在 pivot 循环里也能 emit step_start/done
1614
1747
  // 之前 loop.execute() 不接 streamCallback, 导致 step-timeline 只能看到老 runReActLoop 路径
1615
1748
  // promptWithPivotLoop 路径 0 step events — UI 显示 timeline 但永远是空
1616
1749
  // 2026-06-17: 透传 signal 让 abort 工作 — loop.execute() 当前不接 signal 参数,
1617
1750
  // 所以 abort 行为通过 this.currentSignal 共享给 loop 内部读 (后续 M3.2 接 task plan 时一起加)
1618
1751
  // 2026-07-06: pivot 内 token 累计到 ~70% 时回调 (workflow-pivot-loop.ts line 270+).
1619
- // pivot 的 messageHistory 是 process-local, 不和 pi-sdk 的 this.messageHistory 同步.
1752
+ // pivot 的 messageHistory 是 process-local, 不和 pi-sdk 的 actor 的 messageHistory 同步.
1620
1753
  // 真正折叠需要把 pi-sdk 历史灌回 pivot 的 history 数组 — 侵入较大.
1621
1754
  // 当前 priority: 临时传空实现, 让 budget 公式放够 (workflow-pivot-loop.ts line 220)
1622
1755
  // 不再撞预算. 这条路径留作技术债.
1623
1756
  // 2026-07-17 Bug 1 修: 注入 messageHistory (hydrateMessageHistory 从 session JSON 回灌的) 到 system prompt
1624
- // pivot loop execute() 内部自己维护 messageHistory, 跟 pi-sdk 的 this.messageHistory 隔离,
1757
+ // pivot loop execute() 内部自己维护 messageHistory, 跟 pi-sdk 的 actor 的 messageHistory 隔离,
1625
1758
  // 不注入的话 LLM 看不到历史对话, 每次都是新对话.
1626
1759
  const historyLines = [];
1627
- const historyToInject = this.messageHistory.slice(-20, -1);
1760
+ const historyToInject = this.actor.state.messageHistory.slice(-20, -1);
1628
1761
  for (const m of historyToInject) {
1629
1762
  const roleLabel = m.role === 'user' ? '用户' : m.role === 'assistant' ? '你' : m.role === 'tool' ? '工具结果' : m.role;
1630
1763
  const text = (m.content || '').slice(0, 2000);
@@ -1637,9 +1770,9 @@ ${await this.renderActivePlansSection()}
1637
1770
  const onCompact = async () => {
1638
1771
  // no-op (best-effort hook for future pi-sdk/pivot history sync)
1639
1772
  };
1640
- const result = await loop.execute(input, llm, systemPrompt + historyBlock, this.currentOnStream ?? undefined, this.currentSignal ?? undefined, onCompact);
1773
+ const result = await loop.execute(input, llm, systemPrompt + historyBlock, this.runCtx.eventSink ?? undefined, this.runCtx.abortSignal ?? undefined, onCompact);
1641
1774
  if (result.response) {
1642
- this.messageHistory.push({ role: 'assistant', content: result.response });
1775
+ this.pushHistory({ role: 'assistant', content: result.response });
1643
1776
  }
1644
1777
  // 回溯 + 清场
1645
1778
  if (this.judgmentGateUsedIds.length > 0) {
@@ -1655,7 +1788,9 @@ ${await this.renderActivePlansSection()}
1655
1788
  */
1656
1789
  _harness = null;
1657
1790
  /** M2 绑定 GoalStore 后填真值; 在此之前为空 (事件里 goalId 字段已就位) */
1658
- currentGoalId = '';
1791
+ // **K5 步骤③/⑧**: 会话的 Goal 默认绑定本体在 `actor.state.goalBinding` (访问器已删, 直接读本体)。
1792
+ // leo 口径不变: 这只是默认值; 每次执行开始必须把最终绑定写进 RunContext/Run 记录,
1793
+ // 运行中重绑必须走显式 Goal Binding 操作, 不许靠裸字段隐式生效。
1659
1794
  /** 2026-09-16 (M2): 本次执行是"从 checkpoint 恢复"的 runId (非空 = 恢复模式, 不再新建 run) */
1660
1795
  resumeRunId = '';
1661
1796
  resumePlan = null;
@@ -1678,7 +1813,7 @@ ${await this.renderActivePlansSection()}
1678
1813
  }
1679
1814
  /** CLI / Web 注入"当前目标" (有 goalId 就在该 Goal 下执行) */
1680
1815
  setGoalId(goalId) {
1681
- this.currentGoalId = String(goalId || '');
1816
+ this.actor.state.goalBinding = String(goalId || '');
1682
1817
  }
1683
1818
  /** 核心状态迁移的兜底: 失败 → 记降级 + 交给循环顶部的持久化硬闸 (不吞错) */
1684
1819
  async safeSetRunStatus(runId, to) {
@@ -1697,13 +1832,13 @@ ${await this.renderActivePlansSection()}
1697
1832
  * 这是"错误恢复闭环"的运行时接线点 (此前 recordRecovery / repeatedFailureCount 只是数据结构)。
1698
1833
  */
1699
1834
  async wireToolFailure(tool, errorText, args) {
1700
- if (!this.currentRunId)
1835
+ if (!this.actor.state.activeRun)
1701
1836
  return;
1702
1837
  const cls = classifyRunError(errorText);
1703
1838
  const digest = argsDigestOf(args);
1704
1839
  let repeats = 1;
1705
1840
  try {
1706
- const rec = await readRun(this.currentRunId);
1841
+ const rec = await readRun(this.actor.state.activeRun);
1707
1842
  // recordStep 已经把**这次**失败记进去了, 所以"连续失败次数"直接就是当前值 (不再 +1)
1708
1843
  const seen = rec ? repeatedFailureCount(rec, tool, digest) : 0;
1709
1844
  repeats = Math.max(seen, 1);
@@ -1714,7 +1849,7 @@ ${await this.renderActivePlansSection()}
1714
1849
  : cls === 'external_no_reply' ? 'pause'
1715
1850
  : 'retry';
1716
1851
  try {
1717
- await recordRecovery(this.currentRunId, {
1852
+ await recordRecovery(this.actor.state.activeRun, {
1718
1853
  errorClass: cls,
1719
1854
  message: `${tool}: ${errorText}`.slice(0, 200),
1720
1855
  action,
@@ -1723,20 +1858,20 @@ ${await this.renderActivePlansSection()}
1723
1858
  });
1724
1859
  }
1725
1860
  catch (err) {
1726
- await recordDegradation({ kind: 'core', op: 'pi-sdk.recordRecovery', runId: this.currentRunId, message: String(err?.message || err) }).catch(() => { });
1861
+ await recordDegradation({ kind: 'core', op: 'pi-sdk.recordRecovery', runId: this.actor.state.activeRun, message: String(err?.message || err) }).catch(() => { });
1727
1862
  }
1728
1863
  // 外部无响应 → awaiting_external (不算失败; 等回话后回到 running)
1729
1864
  if (cls === 'external_no_reply') {
1730
1865
  this.awaitingExternal = true;
1731
- await this.safeSetRunStatus(this.currentRunId, 'awaiting_external');
1866
+ await this.safeSetRunStatus(this.actor.state.activeRun, 'awaiting_external');
1732
1867
  // 2026-09-16 (2-C.4): 把"在等什么"写成持久化事实 (来源/关联/过期), 否则真实回包到了也不知道该唤醒谁。
1733
1868
  try {
1734
- if (this.currentGoalId) {
1869
+ if (this.actor.state.goalBinding) {
1735
1870
  const { bindExternalWait, newContinuationId, defaultWaitExpiry } = await import('./external-events.js');
1736
1871
  const isDelegate = /delegate/i.test(String(tool || ''));
1737
- await bindExternalWait(this.currentGoalId, {
1738
- requestId: this.pendingExternalRequestId || `${this.currentRunId}:${Date.now().toString(36)}`,
1739
- continuationId: newContinuationId(this.currentGoalId),
1872
+ await bindExternalWait(this.actor.state.goalBinding, {
1873
+ requestId: this.pendingExternalRequestId || `${this.actor.state.activeRun}:${Date.now().toString(36)}`,
1874
+ continuationId: newContinuationId(this.actor.state.goalBinding),
1740
1875
  expectedSource: isDelegate ? 'delegate' : 'p2p',
1741
1876
  expectedEvent: 'result',
1742
1877
  createdAt: new Date().toISOString(),
@@ -1746,31 +1881,31 @@ ${await this.renderActivePlansSection()}
1746
1881
  }
1747
1882
  }
1748
1883
  catch (e) {
1749
- await recordDegradation({ kind: 'core', op: 'pi-sdk.bindExternalWait', runId: this.currentRunId, message: String(e?.message || e) }).catch(() => { });
1884
+ await recordDegradation({ kind: 'core', op: 'pi-sdk.bindExternalWait', runId: this.actor.state.activeRun, message: String(e?.message || e) }).catch(() => { });
1750
1885
  }
1751
1886
  return;
1752
1887
  }
1753
1888
  // 鉴权类: 不重试, 直接交人
1754
1889
  if (cls === 'auth') {
1755
1890
  this.breakerReason = `鉴权类错误不重试 (${tool}): ${errorText.slice(0, 120)}`;
1756
- await this.safeSetRunStatus(this.currentRunId, 'needs_human');
1891
+ await this.safeSetRunStatus(this.actor.state.activeRun, 'needs_human');
1757
1892
  return;
1758
1893
  }
1759
1894
  // 重复失败熔断: 同一工具 + 同一组参数连续失败达 3 次
1760
1895
  if (repeats >= MAX_SAME_TOOL_FAILURES) {
1761
1896
  this.breakerReason = `同一工具 ${tool} 连续失败 ${repeats} 次 (${cls}) → 熔断, 不再重试`;
1762
- await this.safeSetRunStatus(this.currentRunId, 'needs_human');
1897
+ await this.safeSetRunStatus(this.actor.state.activeRun, 'needs_human');
1763
1898
  }
1764
1899
  }
1765
1900
  getRunId() {
1766
- return this.currentRunId;
1901
+ return this.actor.state.activeRun;
1767
1902
  }
1768
1903
  /**
1769
1904
  * 2026-09-16 (M2-B): **上一次**运行的 runId —— prompt 收尾时 `currentRunId` 会被清空,
1770
1905
  * 而 Supervisor / 控制面要在运行结束后才知道"刚才跑的是哪条 run" (否则会拿旧 run 做决策)。
1771
1906
  */
1772
1907
  getLastRunId() {
1773
- return this.lastRunId || this.currentRunId;
1908
+ return this.lastRunId || this.actor.state.activeRun;
1774
1909
  }
1775
1910
  /**
1776
1911
  * 2026-09-16 (M2): 从 checkpoint 恢复一次运行 —— **不是**重发原 prompt。
@@ -1817,7 +1952,7 @@ ${await this.renderActivePlansSection()}
1817
1952
  }
1818
1953
  this.resumeRunId = runId;
1819
1954
  this.resumePlan = prep.plan;
1820
- this.currentGoalId = prep.plan.goalId || this.currentGoalId;
1955
+ this.actor.state.goalBinding = prep.plan.goalId || this.actor.state.goalBinding;
1821
1956
  try {
1822
1957
  const reply = await this.prompt(buildResumeInstruction(prep.plan), {});
1823
1958
  return { ok: true, reply, ...(modelDrift ? { modelDrift } : {}), ...(modelApplied ? { modelApplied } : {}) };
@@ -1836,8 +1971,8 @@ ${await this.renderActivePlansSection()}
1836
1971
  // pre-tool-validator 4 步链 (modeGate/blacklist/shell-guard/schema), 经 human-value-pipeline 包装
1837
1972
  preToolUse: async (o) => onPreToolUse({ tool: o.tool, args: o.args, permissionMode: o.permissionMode }),
1838
1973
  // 事件写 Run: 观测级 (记账失败不改变已做出的决策)
1839
- events: (e) => { if (this.currentRunId)
1840
- void recordHarnessEvent(this.currentRunId, e); },
1974
+ events: (e) => { if (this.actor.state.activeRun)
1975
+ void recordHarnessEvent(this.actor.state.activeRun, e); },
1841
1976
  });
1842
1977
  }
1843
1978
  return this._harness;
@@ -1845,10 +1980,10 @@ ${await this.renderActivePlansSection()}
1845
1980
  /** 每个生命周期事件都带上的运行身份 (runId / goalId / agentId / channelId / surface) */
1846
1981
  harnessCtx() {
1847
1982
  return {
1848
- runId: this.currentRunId || undefined,
1849
- goalId: this.currentGoalId || undefined,
1850
- agentId: this.currentAgentId || undefined,
1851
- channelId: this.currentChannelId || undefined,
1983
+ runId: this.actor.state.activeRun || undefined,
1984
+ goalId: this.actor.state.goalBinding || undefined,
1985
+ agentId: this.actor.state.agentId || undefined,
1986
+ channelId: this.actor.state.channelId || undefined,
1852
1987
  surface: this.runSurface,
1853
1988
  };
1854
1989
  }
@@ -1915,26 +2050,26 @@ ${await this.renderActivePlansSection()}
1915
2050
  this.awaitingExternal = false;
1916
2051
  if (this.resumeRunId) {
1917
2052
  // ── 恢复模式: 复用原来的 runId, 不新建 run (历史保留) ──
1918
- this.currentRunId = this.resumeRunId;
2053
+ this.actor.state.activeRun = this.resumeRunId;
1919
2054
  this.lastRunId = this.resumeRunId;
1920
2055
  try {
1921
- await markRunRunning(this.currentRunId);
2056
+ await markRunRunning(this.actor.state.activeRun);
1922
2057
  }
1923
2058
  catch (err) {
1924
2059
  runPersistenceFailure = `恢复时状态迁移失败 (recovering → running): ${String(err?.message || err).slice(0, 180)}`;
1925
2060
  }
1926
2061
  const doneN = this.resumePlan?.completedSteps.length ?? 0;
1927
2062
  const guards = this.resumePlan?.replayGuards.length ?? 0;
1928
- onStream?.({ type: 'status', internal: true, content: `♻️ 从 checkpoint 恢复运行 ${this.currentRunId} (已完成 ${doneN} 步, 非幂等重放守卫 ${guards} 条)`, tool: 'harness' });
2063
+ onStream?.({ type: 'status', internal: true, content: `♻️ 从 checkpoint 恢复运行 ${this.actor.state.activeRun} (已完成 ${doneN} 步, 非幂等重放守卫 ${guards} 条)`, tool: 'harness' });
1929
2064
  }
1930
2065
  else {
1931
2066
  // 2026-09-16 (M2): 目标绑定 —— 有 goalId 就在该 Goal 下执行; 没有就建 Goal 再建 Run。
1932
2067
  // 延续规则 (确定性, 不靠猜): 该 channel/agent 上已有 open/active Goal, 且它的上一次执行**没收尾**
1933
2068
  // (interrupted/stalled/needs_human/paused/awaiting_external/recovering) → 继续该 Goal; 否则新建。
1934
- let boundGoalId = this.currentGoalId;
2069
+ let boundGoalId = this.actor.state.goalBinding;
1935
2070
  if (!boundGoalId) {
1936
2071
  try {
1937
- const active = await findActiveGoal({ channelId: this.currentChannelId || undefined, agentId: this.currentAgentId || undefined });
2072
+ const active = await findActiveGoal({ channelId: this.actor.state.channelId || undefined, agentId: this.actor.state.agentId || undefined });
1938
2073
  if (active?.currentRunId) {
1939
2074
  const prev = await readRun(active.currentRunId);
1940
2075
  const unfinished = prev && ['interrupted', 'stalled', 'needs_human', 'paused', 'awaiting_external', 'recovering'].includes(prev.status);
@@ -1949,8 +2084,8 @@ ${await this.renderActivePlansSection()}
1949
2084
  try {
1950
2085
  const g = await createGoal({
1951
2086
  objective: this.currentUserInput || '(未记录目标)',
1952
- channelId: this.currentChannelId || undefined,
1953
- agentId: this.currentAgentId || undefined,
2087
+ channelId: this.actor.state.channelId || undefined,
2088
+ agentId: this.actor.state.agentId || undefined,
1954
2089
  createdBy: this.runSurface,
1955
2090
  });
1956
2091
  boundGoalId = g.goalId;
@@ -1959,25 +2094,25 @@ ${await this.renderActivePlansSection()}
1959
2094
  console.warn('[PiAgent] 创建 Goal 失败 (无目标也要有运行记录):', err?.message);
1960
2095
  }
1961
2096
  }
1962
- this.currentGoalId = boundGoalId;
2097
+ this.actor.state.goalBinding = boundGoalId;
1963
2098
  }
1964
2099
  try {
1965
2100
  const rec = await startRun({
1966
2101
  surface: this.runSurface,
1967
2102
  goal: this.currentUserInput || '(未记录目标)',
1968
2103
  goalId: boundGoalId || undefined,
1969
- channelId: this.currentChannelId || undefined,
1970
- agentId: this.currentAgentId || undefined,
2104
+ channelId: this.actor.state.channelId || undefined,
2105
+ agentId: this.actor.state.agentId || undefined,
1971
2106
  modelConfig: await this.runModelSnapshot(),
1972
2107
  });
1973
- this.currentRunId = rec.runId;
2108
+ this.actor.state.activeRun = rec.runId;
1974
2109
  this.lastRunId = rec.runId;
1975
- this.currentGoalId = rec.goalId || this.currentGoalId;
1976
- if (this.currentGoalId) {
2110
+ this.actor.state.goalBinding = rec.goalId || this.actor.state.goalBinding;
2111
+ if (this.actor.state.goalBinding) {
1977
2112
  // Run → Goal 反查链: runId → goalId → objective / success criteria
1978
- await attachRun(this.currentGoalId, rec.runId).catch((err) => console.warn('[PiAgent] attachRun 失败:', err?.message));
2113
+ await attachRun(this.actor.state.goalBinding, rec.runId).catch((err) => console.warn('[PiAgent] attachRun 失败:', err?.message));
1979
2114
  }
1980
- 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' });
2115
+ onStream?.({ type: 'status', internal: true, content: `🧷 运行已登记 (run=${rec.runId}${this.actor.state.goalBinding ? `, goal=${this.actor.state.goalBinding}` : ''}, 预算 ${rec.budget.maxSteps} 步 / ${Math.round(rec.budget.deadlineMs / 60000)} 分钟)`, tool: 'harness' });
1981
2116
  }
1982
2117
  catch (err) {
1983
2118
  // 核心写失败: 不再 warn 后继续 —— 没有运行记录就不执行 (strict 模式默认如此)
@@ -2007,18 +2142,18 @@ ${await this.renderActivePlansSection()}
2007
2142
  break;
2008
2143
  }
2009
2144
  // 2026-09-16: 预算闸门 (持久化 harness 的约束面) —— 到点必须**如实**终止, 不许静默算完成
2010
- if (this.currentRunId) {
2145
+ if (this.actor.state.activeRun) {
2011
2146
  try {
2012
- const rec = await readRun(this.currentRunId);
2147
+ const rec = await readRun(this.actor.state.activeRun);
2013
2148
  if (!rec)
2014
- throw new Error(`运行记录读不到: ${this.currentRunId}`);
2149
+ throw new Error(`运行记录读不到: ${this.actor.state.activeRun}`);
2015
2150
  // 2026-09-16 (M5): 外部控制面 (CLI /pause /abort, Web API) 改过状态 → 如实停在那儿。
2016
2151
  // 不覆盖成 done/failed: 人按下暂停就是暂停, 人按下中止就是中止。
2017
2152
  if (rec.status === 'paused' || rec.status === 'aborted') {
2018
2153
  runExternallyPaused = rec.status === 'paused';
2019
2154
  runExternallyAborted = rec.status === 'aborted';
2020
2155
  runStopReason = `外部请求: ${rec.status}`;
2021
- onStream?.({ type: 'error', content: `⏹️ 运行被外部${rec.status === 'paused' ? '暂停' : '中止'} (run=${this.currentRunId})`, tool: 'harness' });
2156
+ onStream?.({ type: 'error', content: `⏹️ 运行被外部${rec.status === 'paused' ? '暂停' : '中止'} (run=${this.actor.state.activeRun})`, tool: 'harness' });
2022
2157
  finalResponse = finalResponse || `(运行已${rec.status === 'paused' ? '暂停' : '中止'})`;
2023
2158
  break;
2024
2159
  }
@@ -2070,14 +2205,14 @@ ${await this.renderActivePlansSection()}
2070
2205
  if (totalToolCallsThisLoop >= MAX_TOOL_CALLS_PER_LOOP) {
2071
2206
  console.warn(`[PiAgent] 单轮工具调用已达 ${MAX_TOOL_CALLS_PER_LOOP}, 注入 hint 让 LLM 总结`);
2072
2207
  onStream?.({ type: 'error', content: `⏹️ 工具调用已达上限 (${MAX_TOOL_CALLS_PER_LOOP}), 请基于已有结果回答`, tool: 'loop' });
2073
- this.messageHistory.push({ role: 'system', content: `[注意] 你已连续调用 ${MAX_TOOL_CALLS_PER_LOOP} 次工具。请基于已有结果直接回答用户, 不要再次调用任何工具。在回答末尾加 <final gen> 标记结束。` });
2208
+ this.pushHistory({ role: 'system', content: `[注意] 你已连续调用 ${MAX_TOOL_CALLS_PER_LOOP} 次工具。请基于已有结果直接回答用户, 不要再次调用任何工具。在回答末尾加 <final gen> 标记结束。` });
2074
2209
  totalToolCallsThisLoop = 0; // 重置计数器, 只防连续死循环
2075
2210
  }
2076
2211
  if (lastNTools.length >= MAX_IDEMPOTENT_TOOL && new Set(lastNTools).size === 1) {
2077
2212
  const repeatedTool = lastNTools[0];
2078
2213
  console.warn(`[PiAgent] 同工具 ${repeatedTool} 连续成功调 ${MAX_IDEMPOTENT_TOOL} 次, 注入 hint 让 LLM 总结`);
2079
2214
  onStream?.({ type: 'error', content: `⏹️ 工具 ${repeatedTool} 重复调用 ${MAX_IDEMPOTENT_TOOL} 次, 请基于已有结果回答`, tool: 'loop' });
2080
- this.messageHistory.push({ role: 'system', content: `[注意] 你已连续 ${MAX_IDEMPOTENT_TOOL} 次调用 ${repeatedTool}。请基于已有结果直接回答用户, 不要再次调用任何工具。在回答末尾加 <final gen> 标记结束。` });
2215
+ this.pushHistory({ role: 'system', content: `[注意] 你已连续 ${MAX_IDEMPOTENT_TOOL} 次调用 ${repeatedTool}。请基于已有结果直接回答用户, 不要再次调用任何工具。在回答末尾加 <final gen> 标记结束。` });
2081
2216
  lastNTools.length = 0; // 重置计数器
2082
2217
  // 不 break — 让 LLM 在下一轮用已有信息回答
2083
2218
  }
@@ -2280,7 +2415,7 @@ ${await this.renderActivePlansSection()}
2280
2415
  break;
2281
2416
  }
2282
2417
  // 把错误当成 tool 结果 push 进 history, 这样下一轮 LLM 看到错误能调整
2283
- this.messageHistory.push({
2418
+ this.pushHistory({
2284
2419
  role: 'system',
2285
2420
  content: `[Loop 错误恢复 ${totalErrors}/${this.MAX_TOTAL_ERRORS}] ${aiFailureReason}\n\n请基于上轮工具结果继续完成任务, 不要重复调用同一失败操作. 如果工具已成功执行, 请基于 result.output 给用户总结; 如果工具失败, 请换其他方式或重试.`
2286
2421
  });
@@ -2353,7 +2488,7 @@ ${await this.renderActivePlansSection()}
2353
2488
  }
2354
2489
  if (toolCalls.length > 0) {
2355
2490
  // 把原始 LLM 回复 push 进 history (仅一次)
2356
- this.messageHistory.push({
2491
+ this.pushHistory({
2357
2492
  role: 'assistant',
2358
2493
  content: reply,
2359
2494
  toolCalls: toolCalls.length > 1 ? toolCalls : [toolCalls[0]],
@@ -2383,12 +2518,12 @@ ${await this.renderActivePlansSection()}
2383
2518
  consecutiveErrors++;
2384
2519
  totalErrors++;
2385
2520
  const errorResult = { success: false, error: `未知工具: ${toolCall.name}` };
2386
- this.messageHistory.push({ role: 'tool', content: JSON.stringify(errorResult), toolResult: errorResult });
2521
+ this.pushHistory({ role: 'tool', content: JSON.stringify(errorResult), toolResult: errorResult });
2387
2522
  this.logToHarness(toolCall.name, toolCall.args, errorResult);
2388
2523
  // 2026-07-28: 注入 Reflection 帮助 LLM 理解错误
2389
2524
  const obs = buildObservation(toolCall.name, toolCall.args, errorResult);
2390
2525
  const ref = buildReflection(toolCall.name, errorResult.error, totalErrors, lastFailedToolCount);
2391
- this.messageHistory.push({ role: 'system', content: formatObservationWithReflection(obs, ref) });
2526
+ this.pushHistory({ role: 'system', content: formatObservationWithReflection(obs, ref) });
2392
2527
  if (onStream)
2393
2528
  onStream({ type: 'status', content: `💡 Reflection: ${obs.summary}`, tool: 'system' });
2394
2529
  console.warn(`[PiAgent] 未知工具: ${toolCall.name} (累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}),跳过并继续`);
@@ -2422,7 +2557,7 @@ ${await this.renderActivePlansSection()}
2422
2557
  consecutiveErrors++;
2423
2558
  totalErrors++;
2424
2559
  const denyResultMsg = { success: false, error: `拒绝: [${toolDecision.rejectedBy || 'deny-pipeline'}] ${toolDecision.reason}` };
2425
- this.messageHistory.push({ role: 'tool', content: JSON.stringify(denyResultMsg), toolResult: denyResultMsg });
2560
+ this.pushHistory({ role: 'tool', content: JSON.stringify(denyResultMsg), toolResult: denyResultMsg });
2426
2561
  this.logToHarness(toolCall.name, toolCall.args, denyResultMsg);
2427
2562
  return;
2428
2563
  }
@@ -2434,7 +2569,7 @@ ${await this.renderActivePlansSection()}
2434
2569
  ? `Harness gate 拒绝 (${toolDecision.rejectedBy}): ${toolDecision.reason || '未通过安全校验'}`
2435
2570
  : `PreToolUse 拒绝: ${toolDecision.reason || '未通过安全校验'}`,
2436
2571
  };
2437
- this.messageHistory.push({ role: 'tool', content: JSON.stringify(deniedResult), toolResult: deniedResult });
2572
+ this.pushHistory({ role: 'tool', content: JSON.stringify(deniedResult), toolResult: deniedResult });
2438
2573
  this.logToHarness(toolCall.name, toolCall.args, deniedResult);
2439
2574
  if (onStream) {
2440
2575
  const gateName = isGateDeny ? `Harness ${toolDecision.rejectedBy}` : (isHarnessError ? '核心约束层' : 'PreToolUse');
@@ -2463,13 +2598,13 @@ ${await this.renderActivePlansSection()}
2463
2598
  ? `[注意] 连续 ${consecutiveErrors} 次工具调用因约束层失效被阻止. 请换其他工具或直接回答用户, 末尾加 <final gen>.`
2464
2599
  : `[注意] 连续 ${consecutiveErrors} 次工具调用被系统拒绝. 请换其他工具或直接回答用户, 末尾加 <final gen>.`;
2465
2600
  if (lastFailedToolCount >= MAX_SAME_TOOL_FAILURES) {
2466
- this.messageHistory.push({ role: 'system', content: systemMaxMsg });
2601
+ this.pushHistory({ role: 'system', content: systemMaxMsg });
2467
2602
  lastFailedTool = '';
2468
2603
  lastFailedToolCount = 0;
2469
2604
  consecutiveErrors = 0;
2470
2605
  }
2471
2606
  else if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
2472
- this.messageHistory.push({ role: 'system', content: systemConsecMsg });
2607
+ this.pushHistory({ role: 'system', content: systemConsecMsg });
2473
2608
  consecutiveErrors = 0;
2474
2609
  }
2475
2610
  return;
@@ -2503,18 +2638,18 @@ ${await this.renderActivePlansSection()}
2503
2638
  }
2504
2639
  console.log(`[PiAgent] 工具 ${toolCall.name} 执行完成: success=${result.success} (${toolDurationMs}ms)`);
2505
2640
  // 2026-09-16 (M3): 失败接线 —— 分类 + recovery 留痕 + 熔断 / 外部等待
2506
- if (!result.success && this.currentRunId && !replaySkip) {
2641
+ if (!result.success && this.actor.state.activeRun && !replaySkip) {
2507
2642
  await this.wireToolFailure(toolCall.name, String(result.error || ''), toolCall.args);
2508
2643
  }
2509
- else if (result.success && this.currentRunId && this.awaitingExternal) {
2644
+ else if (result.success && this.actor.state.activeRun && this.awaitingExternal) {
2510
2645
  // 外部回话了: awaiting_external → running (不是"恢复完成", 只是等待结束)
2511
2646
  this.awaitingExternal = false;
2512
- await this.safeSetRunStatus(this.currentRunId, 'running');
2647
+ await this.safeSetRunStatus(this.actor.state.activeRun, 'running');
2513
2648
  }
2514
2649
  // 2026-09-16: 持久化 run harness — 每步工具调用立即落盘 (崩在这里也能看到做到哪步)
2515
- if (this.currentRunId) {
2650
+ if (this.actor.state.activeRun) {
2516
2651
  try {
2517
- await recordStep(this.currentRunId, {
2652
+ await recordStep(this.actor.state.activeRun, {
2518
2653
  tool: toolCall.name,
2519
2654
  ok: !!result.success,
2520
2655
  ms: toolDurationMs,
@@ -2545,7 +2680,7 @@ ${await this.renderActivePlansSection()}
2545
2680
  ok: !!result.success,
2546
2681
  });
2547
2682
  if (after.routeHint?.systemAddition) {
2548
- this.messageHistory.push({ role: 'system', content: `[Harness Router Hint: ${after.routeHint.reason}]\n${after.routeHint.systemAddition}` });
2683
+ this.pushHistory({ role: 'system', content: `[Harness Router Hint: ${after.routeHint.reason}]\n${after.routeHint.systemAddition}` });
2549
2684
  }
2550
2685
  if (after.outputBlocked) {
2551
2686
  if (onStream) {
@@ -2578,14 +2713,14 @@ ${await this.renderActivePlansSection()}
2578
2713
  }
2579
2714
  }
2580
2715
  catch { /* 观测失败绝不影响主路径 */ }
2581
- this.messageHistory.push({ role: 'tool', content: JSON.stringify(result), toolResult: result, toolCallId: toolCall.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` });
2716
+ this.pushHistory({ role: 'tool', content: JSON.stringify(result), toolResult: result, toolCallId: toolCall.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` });
2582
2717
  // 2026-10-01 (用户: 「为什么这么慢」): 量到的事实 —— 每次 LLM 往返平均 3338ms,
2583
2718
  // 而"一轮发多个工具"(toolCalls>1) 只占 12/63 ✗ ⇒ 回合时长 ≈ 往返次数 × 3.3 秒 ✓。
2584
2719
  // 本回合**第一次**且**只发一个**工具时, 附一句带代价的提醒(不是口号 ✓), 只提一次免得刷屏。
2585
2720
  if (toolCalls.length === 1 && this.successfulToolResults.length <= 1) { // 本回合头一次(免得每轮刷屏)
2586
2721
  const __batchHint = batchHint(toolCall.name, 1);
2587
2722
  if (__batchHint)
2588
- this.messageHistory.push({ role: 'system', content: __batchHint.trim() });
2723
+ this.pushHistory({ role: 'system', content: __batchHint.trim() });
2589
2724
  }
2590
2725
  this.logToHarness(toolCall.name, toolCall.args, result);
2591
2726
  // 2026-08-09: 记录到本轮行动日志 (循环进度 + final 前目标核查用)
@@ -2697,18 +2832,18 @@ ${await this.renderActivePlansSection()}
2697
2832
  // 2026-07-28: 注入 Observation + Reflection 替代旧 hardcode 提示
2698
2833
  const obs = buildObservation(toolCall.name, toolCall.args, { success: false, error: result.error });
2699
2834
  const ref = buildReflection(toolCall.name, result.error, totalErrors, lastFailedToolCount);
2700
- this.messageHistory.push({ role: 'system', content: formatObservationWithReflection(obs, ref) + SHELL_ESCAPE_HINT });
2835
+ this.pushHistory({ role: 'system', content: formatObservationWithReflection(obs, ref) + SHELL_ESCAPE_HINT });
2701
2836
  if (onStream)
2702
2837
  onStream({ type: 'status', content: `💡 Reflection: ${obs.summary} → ${ref[0]?.action || '放弃'}`, tool: 'system' });
2703
2838
  if (lastFailedToolCount >= MAX_SAME_TOOL_FAILURES) {
2704
- this.messageHistory.push({ role: 'system', content: `[注意] 工具 ${toolCall.name} 在这个上下文中不可用 (连续 ${MAX_SAME_TOOL_FAILURES} 次失败: ${result.error}). 请不要再次调用它, 直接用你已知的信息回答用户, 并在回答开头标记 <final gen>.` });
2839
+ this.pushHistory({ role: 'system', content: `[注意] 工具 ${toolCall.name} 在这个上下文中不可用 (连续 ${MAX_SAME_TOOL_FAILURES} 次失败: ${result.error}). 请不要再次调用它, 直接用你已知的信息回答用户, 并在回答开头标记 <final gen>.` });
2705
2840
  lastFailedTool = '';
2706
2841
  lastFailedToolCount = 0;
2707
2842
  consecutiveErrors = 0;
2708
2843
  return;
2709
2844
  }
2710
2845
  if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
2711
- this.messageHistory.push({ role: 'system', content: `[注意] 前面的工具调用连续失败。请尝试其他工具或换一种方式完成用户请求, 或用 <final gen> 给出最终回答.` });
2846
+ this.pushHistory({ role: 'system', content: `[注意] 前面的工具调用连续失败。请尝试其他工具或换一种方式完成用户请求, 或用 <final gen> 给出最终回答.` });
2712
2847
  consecutiveErrors = 0;
2713
2848
  }
2714
2849
  }
@@ -2717,11 +2852,11 @@ ${await this.renderActivePlansSection()}
2717
2852
  consecutiveErrors++;
2718
2853
  totalErrors++;
2719
2854
  const errorResult = { success: false, error: String(execError) };
2720
- this.messageHistory.push({ role: 'tool', content: JSON.stringify(errorResult), toolResult: errorResult });
2855
+ this.pushHistory({ role: 'tool', content: JSON.stringify(errorResult), toolResult: errorResult });
2721
2856
  this.logToHarness(toolCall.name, toolCall.args, errorResult);
2722
2857
  const obs = buildObservation(toolCall.name, toolCall.args, errorResult);
2723
2858
  const ref = buildReflection(toolCall.name, errorResult.error, totalErrors, lastFailedToolCount);
2724
- this.messageHistory.push({ role: 'system', content: formatObservationWithReflection(obs, ref) + SHELL_ESCAPE_HINT });
2859
+ this.pushHistory({ role: 'system', content: formatObservationWithReflection(obs, ref) + SHELL_ESCAPE_HINT });
2725
2860
  if (onStream)
2726
2861
  onStream({ type: 'status', content: `💡 Reflection: ${obs.summary}`, tool: 'system' });
2727
2862
  console.error(`[PiAgent] 工具执行异常 (累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}): ${execError}`);
@@ -2732,7 +2867,7 @@ ${await this.renderActivePlansSection()}
2732
2867
  }
2733
2868
  else {
2734
2869
  // LLM 返回的不是 tool call 格式
2735
- this.messageHistory.push({
2870
+ this.pushHistory({
2736
2871
  role: 'assistant',
2737
2872
  content: reply,
2738
2873
  reasoningContent: response?.reasoningContent,
@@ -2761,7 +2896,7 @@ ${await this.renderActivePlansSection()}
2761
2896
  unreportedRetries++;
2762
2897
  const unreported = this.successfulToolResults.length;
2763
2898
  console.log(`[PiAgent] LLM 想 final_gen 但还有 ${unreported} 个工具结果未汇报 (${unreportedRetries}/${MAX_UNREPORTED_RETRIES}), push hint 让其继续`);
2764
- this.messageHistory.push({
2899
+ this.pushHistory({
2765
2900
  role: 'system',
2766
2901
  content: `[dive-into stop condition] 你之前已成功执行了 ${unreported} 个工具, 但当前回复里没把它们的结果告诉用户. 请基于已有的工具结果 (在 history 里) 写一个完整总结回复给用户, 用 <final gen> 结尾. 不要再调工具.`
2767
2902
  });
@@ -2774,7 +2909,7 @@ ${await this.renderActivePlansSection()}
2774
2909
  // 反复提示仍未汇报超过上限 → 清空积压强制 final, 不再死循环
2775
2910
  console.log(`[PiAgent] unreported 循环超限 (${unreportedRetries} 次), 清空积压强制 final`);
2776
2911
  this.successfulToolResults = [];
2777
- this.messageHistory.push({
2912
+ this.pushHistory({
2778
2913
  role: 'system',
2779
2914
  content: `[dive-into stop condition] 已多次提示汇报工具结果仍未完成 (超过 ${MAX_UNREPORTED_RETRIES} 次). 现在直接基于你已知的信息写最终回复给用户, 用 <final gen> 结尾, 不要再调任何工具.`
2780
2915
  });
@@ -2786,7 +2921,7 @@ ${await this.renderActivePlansSection()}
2786
2921
  // 2026-07-29: 质量门 — 即使 LLM 声称完成, 质量太低也继续
2787
2922
  if (lastQualityScore < this.QUALITY_THRESHOLD && refineAttempts < this.MAX_REFINE_ATTEMPTS) {
2788
2923
  console.log(`[PiAgent] final gen 质量 ${lastQualityScore.toFixed(2)} < ${this.QUALITY_THRESHOLD}, 注入 refine hint`);
2789
- this.messageHistory.push({ role: 'system', content: `[质量检查] 你的回答质量评分为 ${(lastQualityScore * 10).toFixed(1)}/10, 低于 ${(this.QUALITY_THRESHOLD * 10).toFixed(1)}/10 阈值。请提供更完整、详细的回答, 包含工具调用获取到的具体信息, 末尾加 <final gen>。` });
2924
+ this.pushHistory({ role: 'system', content: `[质量检查] 你的回答质量评分为 ${(lastQualityScore * 10).toFixed(1)}/10, 低于 ${(this.QUALITY_THRESHOLD * 10).toFixed(1)}/10 阈值。请提供更完整、详细的回答, 包含工具调用获取到的具体信息, 末尾加 <final gen>。` });
2790
2925
  refineAttempts++;
2791
2926
  continue;
2792
2927
  }
@@ -2801,13 +2936,13 @@ ${await this.renderActivePlansSection()}
2801
2936
  userIntent: this.currentUserInput,
2802
2937
  completedTools: Array.from(loopReviewCompletedTools),
2803
2938
  actionLog: loopActionLog,
2804
- runId: this.currentRunId || undefined,
2805
- goalId: this.currentGoalId || undefined,
2939
+ runId: this.actor.state.activeRun || undefined,
2940
+ goalId: this.actor.state.goalBinding || undefined,
2806
2941
  }, DEFAULT_MAX_REVIEWS);
2807
2942
  if (reviewDecision.kind === 'continue-review') {
2808
2943
  loopReviewCount++;
2809
2944
  console.log(`[PiAgent] review ${loopReviewCount}/${DEFAULT_MAX_REVIEWS}: LLM 想 final 但先对齐需求深挖一次`);
2810
- this.messageHistory.push({ role: 'system', content: reviewDecision.hint });
2945
+ this.pushHistory({ role: 'system', content: reviewDecision.hint });
2811
2946
  if (onStream) {
2812
2947
  onStream({ type: 'status', internal: true, content: `🔄 目标对齐 review ${loopReviewCount}/${DEFAULT_MAX_REVIEWS}: 深挖续跑`, tool: 'system' });
2813
2948
  }
@@ -2860,16 +2995,16 @@ ${await this.renderActivePlansSection()}
2860
2995
  if (onStream) {
2861
2996
  onStream({ type: 'status', internal: true, content: `✅ 处理完成,共 ${iteration - 1} 次循环`, tool: 'system' });
2862
2997
  }
2863
- this.messageHistory.push({ role: 'assistant', content: finalResponse });
2998
+ this.pushHistory({ role: 'assistant', content: finalResponse });
2864
2999
  // React Harness: 循环结束
2865
3000
  // 2026-09-16 (Milestone 1-B): 会话收尾走唯一门面
2866
3001
  await this.piHarness().sessionEnd(this.harnessCtx());
2867
3002
  // 2026-09-16: 收尾落盘 — done / failed / aborted / needs_human 如实写回 (不留幽灵 running)
2868
- if (this.currentRunId && !runExternallyPaused && !runExternallyAborted) {
3003
+ if (this.actor.state.activeRun && !runExternallyPaused && !runExternallyAborted) {
2869
3004
  try {
2870
3005
  // 2026-09-16 (M4): 完成门 —— "模型说完成"不等于"系统确认完成"。
2871
3006
  // 证据 = 成功步骤的事实摘要; 末尾还有失败步骤没被后续成功覆盖 → 不许 done。
2872
- const recBefore = await readRun(this.currentRunId).catch(() => null);
3007
+ const recBefore = await readRun(this.actor.state.activeRun).catch(() => null);
2873
3008
  const steps = recBefore?.steps || [];
2874
3009
  const evidence = steps.filter((s) => s.ok).map((s) => `${s.tool}: ${(s.summary || '').slice(0, 120)}`).slice(-10);
2875
3010
  const lastStep = steps[steps.length - 1];
@@ -2890,7 +3025,7 @@ ${await this.renderActivePlansSection()}
2890
3025
  : (trailingFailure || noEvidence)
2891
3026
  ? 'failed'
2892
3027
  : 'done';
2893
- await finishRun(this.currentRunId, {
3028
+ await finishRun(this.actor.state.activeRun, {
2894
3029
  status,
2895
3030
  summary: finalResponse ? String(finalResponse).slice(0, 400) : undefined,
2896
3031
  error: errText
@@ -2903,7 +3038,7 @@ ${await this.renderActivePlansSection()}
2903
3038
  // 终态也写不下去: 这是最坏情况 —— 除了留痕, 没有别的自愈手段, 所以必须显眼
2904
3039
  const message = `收尾落盘失败 (状态无法写回, 记录会停在 running): ${String(err?.message || err).slice(0, 200)}`;
2905
3040
  console.error('[PiAgent] run-store finishRun 失败 (核心持久化):', message);
2906
- await recordDegradation({ kind: 'core', op: 'pi-sdk.finishRun', runId: this.currentRunId, message }).catch(() => { });
3041
+ await recordDegradation({ kind: 'core', op: 'pi-sdk.finishRun', runId: this.actor.state.activeRun, message }).catch(() => { });
2907
3042
  onStream?.({ type: 'error', content: `⚠️ ${message}`, tool: 'harness' });
2908
3043
  }
2909
3044
  // ★ 2026-09-25 (M0 接线冻结, 规则 ③ + ④): Run 结束**必过收尾漏斗**。
@@ -2915,14 +3050,14 @@ ${await this.renderActivePlansSection()}
2915
3050
  //
2916
3051
  // 现在: `closeRunOnce` (幂等) → 9 步收尾 → 产物落盘; 再由 Goal reducer 把"下一步是什么"
2917
3052
  // 落进 Goal (没有 Supervisor 宿主时 Runner 也要留下 continuation, 否则下一次还是从零开始)。
2918
- if (this.currentGoalId) {
3053
+ if (this.actor.state.goalBinding) {
2919
3054
  try {
2920
3055
  const wiring = await import('./goal-flywheel-wiring.js');
2921
3056
  const { reduceGoalState } = await import('./goal-state-reducer.js');
2922
3057
  const nowIso = new Date().toISOString();
2923
3058
  const closed = await wiring.closeRunOnce({
2924
- goalId: this.currentGoalId,
2925
- runId: this.currentRunId,
3059
+ goalId: this.actor.state.goalBinding,
3060
+ runId: this.actor.state.activeRun,
2926
3061
  caller: 'runner',
2927
3062
  now: nowIso,
2928
3063
  finalReview: finalResponse ? String(finalResponse).slice(0, 2000) : '',
@@ -2933,7 +3068,7 @@ ${await this.renderActivePlansSection()}
2933
3068
  else if (closed.outcome) {
2934
3069
  const view = closed.outcome;
2935
3070
  const applied = await reduceGoalState({
2936
- goalId: this.currentGoalId,
3071
+ goalId: this.actor.state.goalBinding,
2937
3072
  intent: 'closure_outcome',
2938
3073
  now: nowIso,
2939
3074
  by: 'runner',
@@ -2950,7 +3085,7 @@ ${await this.renderActivePlansSection()}
2950
3085
  onStream?.({
2951
3086
  type: 'status', internal: true,
2952
3087
  content: `${line}${applied.gateRejected ? ` [完成门拒绝: ${applied.gateRejected}]` : ''} `
2953
- + `(收尾 ${view.steps} 步 · memory ${view.memories} · skill 候选 ${view.candidates} · goal=${this.currentGoalId})`,
3088
+ + `(收尾 ${view.steps} 步 · memory ${view.memories} · skill 候选 ${view.candidates} · goal=${this.actor.state.goalBinding})`,
2954
3089
  tool: 'harness',
2955
3090
  });
2956
3091
  }
@@ -2960,15 +3095,15 @@ ${await this.renderActivePlansSection()}
2960
3095
  }
2961
3096
  }
2962
3097
  catch (err) {
2963
- await recordDegradation({ kind: 'observational', op: 'pi-sdk.runClosure', runId: this.currentRunId, message: String(err?.message || err).slice(0, 160) }).catch(() => { });
3098
+ await recordDegradation({ kind: 'observational', op: 'pi-sdk.runClosure', runId: this.actor.state.activeRun, message: String(err?.message || err).slice(0, 160) }).catch(() => { });
2964
3099
  }
2965
3100
  }
2966
- this.currentRunId = '';
3101
+ this.actor.state.activeRun = '';
2967
3102
  }
2968
- else if (this.currentRunId) {
3103
+ else if (this.actor.state.activeRun) {
2969
3104
  // 外部暂停/中止: 状态是人定的, 不覆盖 (paused 等 /resume; aborted 是终态)
2970
3105
  onStream?.({ type: 'status', content: `⏹️ 运行状态保持为 ${runExternallyPaused ? 'paused' : 'aborted'} (由外部控制面决定)`, tool: 'harness' });
2971
- this.currentRunId = '';
3106
+ this.actor.state.activeRun = '';
2972
3107
  }
2973
3108
  // 2026-06-16: 暴露 aiFailed 标志 — promptStream 据此决定是否自动重试整个 loop
2974
3109
  return { reply: finalResponse, aiFailed, aiFailureReason: aiFailureReason || undefined };
@@ -3022,7 +3157,7 @@ ${await this.renderActivePlansSection()}
3022
3157
  // 失败静默: 任何 stage 抛错 → 走老 slice(-10) 逻辑
3023
3158
  //
3024
3159
  // P1.2: 如果 maybeAutoCompact 算过 Context Collapse 投影, 用 this.projectedHistory (读时投影, 非破坏)
3025
- const source = this.projectedHistory ?? this.messageHistory;
3160
+ const source = this.projectedHistory ?? this.actor.state.messageHistory;
3026
3161
  const recentMessages = this.compressHistorySync(source).slice(-10);
3027
3162
  return recentMessages.map(m => {
3028
3163
  if (m.role === 'user')
@@ -3049,7 +3184,7 @@ ${await this.renderActivePlansSection()}
3049
3184
  // 2026-08-06: 来源优先用 projectedHistory (Context Collapse 投影, 非破坏) —
3050
3185
  // 与 buildContext 一致; 之前只让字符串路径用投影, messages 数组路径被跳过,
3051
3186
  // 导致 LLM 实际看到的还是未压缩的历史.
3052
- const source = this.projectedHistory ?? this.messageHistory;
3187
+ const source = this.projectedHistory ?? this.actor.state.messageHistory;
3053
3188
  const WINDOW = 15;
3054
3189
  const out = [];
3055
3190
  // 2026-09-28 (前缀 KV 可命中): 带 CURRENT TURN 注入的条目要**原样回带**.
@@ -3128,11 +3263,11 @@ ${await this.renderActivePlansSection()}
3128
3263
  /**
3129
3264
  * 2026-09-28 (前缀 KV 可命中): 把 chat() 回带的当前轮写回自己的 messageHistory.
3130
3265
  * 逻辑在模块级 `writeBackCurrentTurnInto` (纯函数, 门可以直接驱它断言);
3131
- * 这里只负责接到 `this.messageHistory` 上, 并保证任何异常都静默 (写回失败不影响对话).
3266
+ * 这里只负责接到 `actor 的 messageHistory` 上, 并保证任何异常都静默 (写回失败不影响对话).
3132
3267
  */
3133
3268
  writeBackCurrentTurn(wire) {
3134
3269
  try {
3135
- return writeBackCurrentTurnInto(this.messageHistory, wire);
3270
+ return writeBackCurrentTurnInto(this.actor.state.messageHistory, wire);
3136
3271
  }
3137
3272
  catch (err) {
3138
3273
  console.warn('[PiAgent] writeBackCurrentTurn failed (silent):', err);
@@ -3146,7 +3281,7 @@ ${await this.renderActivePlansSection()}
3146
3281
  estimateHistoryTokens() {
3147
3282
  try {
3148
3283
  const { estimateTokens } = _piRequire('../context-compaction/index.js');
3149
- return estimateTokens(this.messageHistory);
3284
+ return estimateTokens(this.actor.state.messageHistory);
3150
3285
  }
3151
3286
  catch {
3152
3287
  return 0;
@@ -3169,8 +3304,10 @@ ${await this.renderActivePlansSection()}
3169
3304
  console.warn(`[PiAgent] reactive compaction pre-check (${estimated} tokens > 80% threshold)`);
3170
3305
  onStream?.({ type: 'status', internal: true, content: '⚠️ reactive compaction 预检触发', tool: 'recovery' });
3171
3306
  try {
3172
- const compacted = this.compressHistorySync(this.messageHistory);
3173
- this.messageHistory = compacted;
3307
+ // 同步压缩: 取快照与替换在**同一拍相邻两行** (中间没有 await) ⇒ 不存在"变换期间被追加"的窗口。
3308
+ // 若哪天这里插入 await, 必须改成 `actor.rebaseHistory(…, snapshotLen)` (见 channel-actor.ts)。
3309
+ const compacted = this.compressHistorySync(this.actor.state.messageHistory);
3310
+ this.replaceHistory(compacted);
3174
3311
  if (this.estimateHistoryTokens() > this.maxContextTokens() * 0.8) {
3175
3312
  await this.maybeAutoCompact(onStream, signal);
3176
3313
  }
@@ -3226,7 +3363,7 @@ ${await this.renderActivePlansSection()}
3226
3363
  try {
3227
3364
  const rc = String(response?.reasoningContent || '').trim();
3228
3365
  if (rc)
3229
- this.currentOnStream?.({ type: 'reasoning', content: rc });
3366
+ this.runCtx.eventSink?.({ type: 'reasoning', content: rc });
3230
3367
  }
3231
3368
  catch { /* 送不出去不影响主流程 */ } // 2026-06-30: 透传 toolCalls (OpenAI 协议 native) 给上层, 让 assistant message 能 emit 真 id
3232
3369
  return { reply: response.reply || '', toolCalls: response.toolCalls, messages: response.messages };
@@ -3266,8 +3403,8 @@ ${await this.renderActivePlansSection()}
3266
3403
  if (Array.isArray(contextOrMessages)) {
3267
3404
  contextOrMessages = this.buildContext();
3268
3405
  // 也清除最近一轮的 toolCalls, 防止再触发
3269
- if (this.messageHistory.length > 1) {
3270
- const last = this.messageHistory[this.messageHistory.length - 1];
3406
+ if (this.actor.state.messageHistory.length > 1) {
3407
+ const last = this.actor.state.messageHistory[this.actor.state.messageHistory.length - 1];
3271
3408
  if (last.role === 'assistant' && last.toolCalls) {
3272
3409
  delete last.toolCalls;
3273
3410
  }
@@ -3330,7 +3467,7 @@ ${await this.renderActivePlansSection()}
3330
3467
  * onStream 广播: 跟 computeJudgmentGate 风格一致 (phase 事件供 UI timeline 显示)
3331
3468
  */
3332
3469
  async maybeAutoCompact(onStream, signal) {
3333
- if (this.messageHistory.length < 10)
3470
+ if (this.actor.state.messageHistory.length < 10)
3334
3471
  return; // 历史太短, 不值得压
3335
3472
  onStream?.({ type: 'status', internal: true, content: '🗜️ 评估是否需要压缩上下文...', tool: 'compactor' });
3336
3473
  // 注入 LLM (用 getMinimax().chat, 与 judgment 注入门 / ReAct 循环同一来源)
@@ -3349,14 +3486,17 @@ ${await this.renderActivePlansSection()}
3349
3486
  const maxTokens = Math.max(4000, Math.round(cfg.maxTokens * cfg.compressionThreshold));
3350
3487
  const beforeTokens = this.estimateHistoryTokens();
3351
3488
  const { compactPipeline, isContextCollapseEnabled } = await import('../context-compaction/index.js');
3352
- const result = await compactPipeline(this.messageHistory, {
3489
+ // **K5 第 4 步**: 记下取快照时的长度 —— 压缩是 async, 从这一拍到 "落地" 之间是 await 窗口;
3490
+ // 期间 append 进来的消息必须由 `actor.rebaseHistory(…, snapshotLen)` 接回去, 否则会被整块替换丢掉。
3491
+ const snapshotLen = this.actor.state.messageHistory.length;
3492
+ const result = await compactPipeline(this.actor.state.messageHistory, {
3353
3493
  maxTokens,
3354
3494
  llmChat,
3355
3495
  collapseLlmChat: llmChat, // P1.2: Context Collapse 投影也用同一 LLM
3356
- cacheScope: this.currentChannelId || 'default',
3496
+ cacheScope: this.actor.state.channelId || 'default',
3357
3497
  });
3358
- if (result.compacted && result.history.length < this.messageHistory.length) {
3359
- const saved = this.messageHistory.length - result.history.length;
3498
+ if (result.compacted && result.history.length < this.actor.state.messageHistory.length) {
3499
+ const saved = this.actor.state.messageHistory.length - result.history.length;
3360
3500
  const stagesApplied = result.stages.filter((s) => s.applied).map((s) => s.stage).join(' → ');
3361
3501
  const afterTokens = this.estimateHistoryTokens();
3362
3502
  const savedTokens = Math.max(0, beforeTokens - afterTokens);
@@ -3374,7 +3514,17 @@ ${await this.renderActivePlansSection()}
3374
3514
  // messageHistory 不变 (非破坏)
3375
3515
  }
3376
3516
  else {
3377
- this.messageHistory = result.history; // 真破坏性更新
3517
+ // 真破坏性更新 —— **K5 第 4 步**: 绑定了 actor 就交它落地 (rebase: 保住变换期间的追加);
3518
+ // 未绑定的会话走原路径 (行为不变)。
3519
+ if (this.actor) {
3520
+ const { keptTail } = await this.actor.rebaseHistory(result.history, snapshotLen);
3521
+ if (keptTail > 0) {
3522
+ console.warn(`[PiAgent] 压缩落地: 保住变换期间新追加的 ${keptTail} 条消息 (快照后到达)`);
3523
+ }
3524
+ }
3525
+ else {
3526
+ this.replaceHistory(result.history);
3527
+ }
3378
3528
  this.projectedHistory = null;
3379
3529
  }
3380
3530
  // 2026-08-06: snapshot 记录 before/after + 摘要 (供恢复/调试/UI), 事件广播
@@ -3385,10 +3535,10 @@ ${await this.renderActivePlansSection()}
3385
3535
  afterTokens,
3386
3536
  summary: `压缩管道: ${summaryLine}; 节省 ${savedTokens} tokens / ${saved} 条消息`,
3387
3537
  preservedMemory: [
3388
- ...this.messageHistory.filter(m => m.role === 'user').slice(-3).map(m => (m.content || '').slice(0, 80)),
3538
+ ...this.actor.state.messageHistory.filter(m => m.role === 'user').slice(-3).map(m => (m.content || '').slice(0, 80)),
3389
3539
  ],
3390
- agentId: this.currentAgentId,
3391
- channelId: this.currentChannelId,
3540
+ agentId: this.actor.state.agentId,
3541
+ channelId: this.actor.state.channelId,
3392
3542
  });
3393
3543
  cm.markCompressComplete(snap);
3394
3544
  }
@@ -3783,7 +3933,7 @@ ${this.extractOperationsFromRef(operationsRef)}
3783
3933
  Object.assign(this.identity, updates);
3784
3934
  }
3785
3935
  setCurrentChannelId(channelId) {
3786
- this.currentChannelId = channelId;
3936
+ this.actor.state.channelId = channelId;
3787
3937
  }
3788
3938
  getSessionState() {
3789
3939
  return this.sessionManager.getState();
@@ -3798,12 +3948,12 @@ ${this.extractOperationsFromRef(operationsRef)}
3798
3948
  await this.sessionManager.savePersona(persona);
3799
3949
  // 2026-10-01: **写透** —— 有身份文档的 agent 不套 persona.json, 只写 JSON 等于没改
3800
3950
  // (用户报"每次让智能体改都是同一个") ⇒ 同步落进它自己的身份文档 (标记区内)
3801
- if (this.currentAgentId) {
3951
+ if (this.actor.state.agentId) {
3802
3952
  try {
3803
3953
  const { applyPersonaToDocs } = await import('../bootstrap/persona-init.js');
3804
- const wrote = await applyPersonaToDocs(this.currentAgentId, persona);
3954
+ const wrote = await applyPersonaToDocs(this.actor.state.agentId, persona);
3805
3955
  if (wrote.length)
3806
- console.warn(`[persona] 已把 ${this.currentAgentId} 的 persona 写进身份文档: ${wrote.join(' · ')}`);
3956
+ console.warn(`[persona] 已把 ${this.actor.state.agentId} 的 persona 写进身份文档: ${wrote.join(' · ')}`);
3807
3957
  }
3808
3958
  catch { /* 非致命 */ }
3809
3959
  }