@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.
- package/dist/agents/parse-tool-call.js +6 -1
- package/dist/agents/pi-sdk-session-factory.js +36 -3
- package/dist/agents/pi-sdk.js +349 -199
- package/dist/agents/run-context.js +20 -0
- package/dist/agents/runner-resolver.js +2 -1
- package/dist/electron-build/agents/parse-tool-call.js +6 -1
- package/dist/electron-build/agents/parse-tool-call.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-session-factory.js +36 -3
- package/dist/electron-build/agents/pi-sdk-session-factory.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +349 -199
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/run-context.js +24 -0
- package/dist/electron-build/agents/run-context.js.map +1 -0
- package/dist/electron-build/agents/runner-resolver.js +2 -1
- package/dist/electron-build/agents/runner-resolver.js.map +1 -1
- package/dist/electron-build/kernel/channel-actor.js +314 -0
- package/dist/electron-build/kernel/channel-actor.js.map +1 -0
- package/dist/index.js +24 -19
- package/dist/kernel/channel-actor.js +297 -0
- package/dist/kernel/control.js +95 -0
- package/dist/kernel/gate-scan.js +1036 -0
- package/dist/kernel/model-runtime.js +472 -0
- package/dist/kernel/plan-channel-actor.js +298 -0
- package/dist/kernel/plan-constraint.js +165 -0
- package/dist/kernel/plan-deletion.js +32 -0
- package/dist/kernel/plan-modelruntime.js +60 -0
- package/dist/kernel/plan-runcontext.js +163 -0
- package/dist/kernel/plan.js +153 -0
- package/dist/kernel/roster.js +242 -0
- package/dist/web/bolloon-web.json +3 -3
- package/dist/web/client.js +3 -2
- package/dist/web/routes-tasks.js +5 -1
- package/dist/web/server.js +58 -18
- package/dist/web/sw.js +1 -1
- package/dist/web/ui/message-renderer.js +473 -29
- package/dist/web/ui/step-timeline.js +13 -12
- package/package.json +1 -1
- package/scripts/build-web.ts +74 -1
- package/dist/constraint-runtime/_archive_helper.d.ts +0 -1
- package/dist/constraint-runtime/_archive_helper.js +0 -9
- package/dist/constraint-runtime/assistant/index.d.ts +0 -11
- package/dist/constraint-runtime/assistant/index.js +0 -12
- package/dist/constraint-runtime/bootstrap/index.d.ts +0 -11
- package/dist/constraint-runtime/bootstrap/index.js +0 -12
- package/dist/constraint-runtime/bridge/index.d.ts +0 -11
- package/dist/constraint-runtime/bridge/index.js +0 -12
- package/dist/constraint-runtime/buddy/index.d.ts +0 -11
- package/dist/constraint-runtime/buddy/index.js +0 -12
- package/dist/constraint-runtime/cli/index.d.ts +0 -11
- package/dist/constraint-runtime/cli/index.js +0 -12
- package/dist/constraint-runtime/components/index.d.ts +0 -11
- package/dist/constraint-runtime/components/index.js +0 -12
- package/dist/constraint-runtime/constants/index.d.ts +0 -11
- package/dist/constraint-runtime/constants/index.js +0 -12
- package/dist/constraint-runtime/coordinator/index.d.ts +0 -11
- package/dist/constraint-runtime/coordinator/index.js +0 -12
- package/dist/constraint-runtime/cost_hook.d.ts +0 -2
- package/dist/constraint-runtime/cost_hook.js +0 -4
- package/dist/constraint-runtime/entrypoints/index.d.ts +0 -11
- package/dist/constraint-runtime/entrypoints/index.js +0 -12
- package/dist/constraint-runtime/execution_registry.d.ts +0 -20
- package/dist/constraint-runtime/execution_registry.js +0 -44
- package/dist/constraint-runtime/hooks/index.d.ts +0 -11
- package/dist/constraint-runtime/hooks/index.js +0 -12
- package/dist/constraint-runtime/ink.d.ts +0 -1
- package/dist/constraint-runtime/ink.js +0 -4
- package/dist/constraint-runtime/keybindings/index.d.ts +0 -11
- package/dist/constraint-runtime/keybindings/index.js +0 -12
- package/dist/constraint-runtime/memdir/index.d.ts +0 -11
- package/dist/constraint-runtime/memdir/index.js +0 -12
- package/dist/constraint-runtime/migrations/index.d.ts +0 -11
- package/dist/constraint-runtime/migrations/index.js +0 -12
- package/dist/constraint-runtime/moreright/index.d.ts +0 -11
- package/dist/constraint-runtime/moreright/index.js +0 -12
- package/dist/constraint-runtime/native_ts/index.d.ts +0 -11
- package/dist/constraint-runtime/native_ts/index.js +0 -12
- package/dist/constraint-runtime/output_styles/index.d.ts +0 -11
- package/dist/constraint-runtime/output_styles/index.js +0 -12
- package/dist/constraint-runtime/plugins/index.d.ts +0 -11
- package/dist/constraint-runtime/plugins/index.js +0 -12
- package/dist/constraint-runtime/port_manifest.d.ts +0 -7
- package/dist/constraint-runtime/port_manifest.js +0 -11
- package/dist/constraint-runtime/query.d.ts +0 -6
- package/dist/constraint-runtime/query.js +0 -1
- package/dist/constraint-runtime/remote/index.d.ts +0 -11
- package/dist/constraint-runtime/remote/index.js +0 -12
- package/dist/constraint-runtime/schemas/index.d.ts +0 -11
- package/dist/constraint-runtime/schemas/index.js +0 -12
- package/dist/constraint-runtime/screens/index.d.ts +0 -11
- package/dist/constraint-runtime/screens/index.js +0 -12
- package/dist/constraint-runtime/server/index.d.ts +0 -11
- package/dist/constraint-runtime/server/index.js +0 -12
- package/dist/constraint-runtime/services/index.d.ts +0 -11
- package/dist/constraint-runtime/services/index.js +0 -12
- package/dist/constraint-runtime/state/index.d.ts +0 -11
- package/dist/constraint-runtime/state/index.js +0 -12
- package/dist/constraint-runtime/system_init.d.ts +0 -1
- package/dist/constraint-runtime/system_init.js +0 -20
- package/dist/constraint-runtime/types/index.d.ts +0 -11
- package/dist/constraint-runtime/types/index.js +0 -12
- package/dist/constraint-runtime/upstream_proxy/index.d.ts +0 -11
- package/dist/constraint-runtime/upstream_proxy/index.js +0 -12
- package/dist/constraint-runtime/utils/index.d.ts +0 -11
- package/dist/constraint-runtime/utils/index.js +0 -12
- package/dist/constraint-runtime/vim/index.d.ts +0 -11
- package/dist/constraint-runtime/vim/index.js +0 -12
- package/dist/constraint-runtime/voice/index.d.ts +0 -11
- package/dist/constraint-runtime/voice/index.js +0 -12
package/dist/agents/pi-sdk.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
*
|
|
382
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
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
|
-
* 期间通过
|
|
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.
|
|
436
|
-
this.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
1092
|
+
agentId: this.actor.state.agentId || 'default',
|
|
957
1093
|
input,
|
|
958
|
-
channelId: channelId || this.
|
|
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.
|
|
1001
|
-
this.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1030
|
-
agentId: this.
|
|
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.
|
|
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.
|
|
1041
|
-
agentId: this.
|
|
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.
|
|
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
|
-
|
|
1087
|
-
|
|
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.
|
|
1093
|
-
this.currentIntentHint = intentHint(this.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1276
|
+
this.runCtx.eventSink?.({ type: 'status', content: `🔎 ${line}`, tool: 'system' });
|
|
1140
1277
|
}
|
|
1141
1278
|
}
|
|
1142
1279
|
catch (e) {
|
|
1143
|
-
this.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 用
|
|
1290
|
-
|
|
1291
|
-
this.
|
|
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.
|
|
1297
|
-
this.currentIntentHint = intentHint(this.
|
|
1432
|
+
this.runCtx.intent = classifyIntent(userText);
|
|
1433
|
+
this.currentIntentHint = intentHint(this.runCtx.intent);
|
|
1298
1434
|
this.currentUserInput = userText;
|
|
1299
|
-
if (this.
|
|
1300
|
-
onStream({ type: 'phase', phase: 'intent_classified', detail: this.
|
|
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.
|
|
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.
|
|
1323
|
-
agentId: this.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1566
|
-
this.currentIntentHint = intentHint(this.
|
|
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: 把
|
|
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 的
|
|
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 的
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1866
|
+
await this.safeSetRunStatus(this.actor.state.activeRun, 'awaiting_external');
|
|
1732
1867
|
// 2026-09-16 (2-C.4): 把"在等什么"写成持久化事实 (来源/关联/过期), 否则真实回包到了也不知道该唤醒谁。
|
|
1733
1868
|
try {
|
|
1734
|
-
if (this.
|
|
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.
|
|
1738
|
-
requestId: this.pendingExternalRequestId || `${this.
|
|
1739
|
-
continuationId: newContinuationId(this.
|
|
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.
|
|
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.
|
|
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.
|
|
1897
|
+
await this.safeSetRunStatus(this.actor.state.activeRun, 'needs_human');
|
|
1763
1898
|
}
|
|
1764
1899
|
}
|
|
1765
1900
|
getRunId() {
|
|
1766
|
-
return this.
|
|
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.
|
|
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.
|
|
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.
|
|
1840
|
-
void recordHarnessEvent(this.
|
|
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.
|
|
1849
|
-
goalId: this.
|
|
1850
|
-
agentId: this.
|
|
1851
|
-
channelId: this.
|
|
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.
|
|
2053
|
+
this.actor.state.activeRun = this.resumeRunId;
|
|
1919
2054
|
this.lastRunId = this.resumeRunId;
|
|
1920
2055
|
try {
|
|
1921
|
-
await markRunRunning(this.
|
|
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.
|
|
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.
|
|
2069
|
+
let boundGoalId = this.actor.state.goalBinding;
|
|
1935
2070
|
if (!boundGoalId) {
|
|
1936
2071
|
try {
|
|
1937
|
-
const active = await findActiveGoal({ channelId: this.
|
|
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.
|
|
1953
|
-
agentId: this.
|
|
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.
|
|
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.
|
|
1970
|
-
agentId: this.
|
|
2104
|
+
channelId: this.actor.state.channelId || undefined,
|
|
2105
|
+
agentId: this.actor.state.agentId || undefined,
|
|
1971
2106
|
modelConfig: await this.runModelSnapshot(),
|
|
1972
2107
|
});
|
|
1973
|
-
this.
|
|
2108
|
+
this.actor.state.activeRun = rec.runId;
|
|
1974
2109
|
this.lastRunId = rec.runId;
|
|
1975
|
-
this.
|
|
1976
|
-
if (this.
|
|
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.
|
|
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.
|
|
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.
|
|
2145
|
+
if (this.actor.state.activeRun) {
|
|
2011
2146
|
try {
|
|
2012
|
-
const rec = await readRun(this.
|
|
2147
|
+
const rec = await readRun(this.actor.state.activeRun);
|
|
2013
2148
|
if (!rec)
|
|
2014
|
-
throw new Error(`运行记录读不到: ${this.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
2647
|
+
await this.safeSetRunStatus(this.actor.state.activeRun, 'running');
|
|
2513
2648
|
}
|
|
2514
2649
|
// 2026-09-16: 持久化 run harness — 每步工具调用立即落盘 (崩在这里也能看到做到哪步)
|
|
2515
|
-
if (this.
|
|
2650
|
+
if (this.actor.state.activeRun) {
|
|
2516
2651
|
try {
|
|
2517
|
-
await recordStep(this.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
2805
|
-
goalId: this.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
2925
|
-
runId: this.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
3101
|
+
this.actor.state.activeRun = '';
|
|
2967
3102
|
}
|
|
2968
|
-
else if (this.
|
|
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.
|
|
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
|
-
* 这里只负责接到 `
|
|
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
|
-
|
|
3173
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
3391
|
-
channelId: this.
|
|
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.
|
|
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.
|
|
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.
|
|
3954
|
+
const wrote = await applyPersonaToDocs(this.actor.state.agentId, persona);
|
|
3805
3955
|
if (wrote.length)
|
|
3806
|
-
console.warn(`[persona] 已把 ${this.
|
|
3956
|
+
console.warn(`[persona] 已把 ${this.actor.state.agentId} 的 persona 写进身份文档: ${wrote.join(' · ')}`);
|
|
3807
3957
|
}
|
|
3808
3958
|
catch { /* 非致命 */ }
|
|
3809
3959
|
}
|