@bolloon/bolloon-agent 0.5.4 → 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/agent-identity-store.js +41 -5
- package/dist/agents/background-notices.js +124 -0
- package/dist/agents/channel-identity.js +54 -0
- package/dist/agents/code-write-gate.js +47 -0
- package/dist/agents/deny-pipeline.js +11 -2
- package/dist/agents/egress-sanitize.js +69 -0
- package/dist/agents/error-classifier.js +67 -5
- package/dist/agents/experience-review.js +226 -0
- package/dist/agents/gateway-group.js +153 -18
- package/dist/agents/iteration-budget.js +64 -0
- package/dist/agents/lesson-to-skill.js +211 -0
- package/dist/agents/managed-services.js +64 -0
- package/dist/agents/parse-tool-call.js +43 -32
- package/dist/agents/pi-sdk-session-factory.js +36 -3
- package/dist/agents/pi-sdk-session-manager.js +72 -3
- package/dist/agents/pi-sdk-tools.js +201 -32
- package/dist/agents/pi-sdk.js +798 -206
- package/dist/agents/plan-store.js +36 -0
- package/dist/agents/process-runner.js +64 -2
- package/dist/agents/run-context.js +20 -0
- package/dist/agents/runner-resolver.js +2 -1
- package/dist/agents/shell-guard.js +109 -13
- package/dist/agents/skill-health.js +90 -0
- package/dist/agents/skill-ledger.js +129 -0
- package/dist/agents/tool-loop-guard.js +303 -0
- package/dist/agents/tool-path-args.js +53 -0
- package/dist/agents/tool-result-gate.js +50 -0
- package/dist/agents/tool-subset.js +109 -0
- package/dist/agents/tool-telemetry.js +97 -0
- package/dist/bootstrap/context-collector.js +37 -7
- package/dist/bootstrap/lifecycle-hooks.js +12 -3
- package/dist/bootstrap/persona-init.js +174 -0
- package/dist/bootstrap/persona-loader.js +5 -1
- package/dist/cli/ink-app.js +153 -6
- package/dist/cli/input-paste.js +218 -0
- package/dist/cli/loading-tui.js +3 -1
- package/dist/cli/log-gate.js +7 -0
- package/dist/cli/mention-data.js +30 -0
- package/dist/cli/reasoning-view.js +103 -0
- package/dist/cli/startup-notice.js +25 -0
- package/dist/cli/status-segments.js +87 -2
- package/dist/cli/trace-line.js +54 -0
- package/dist/context-compaction/auto-compact.js +37 -2
- package/dist/electron-build/agents/agent-identity-store.js +43 -5
- package/dist/electron-build/agents/agent-identity-store.js.map +1 -1
- package/dist/electron-build/agents/background-notices.js +138 -0
- package/dist/electron-build/agents/background-notices.js.map +1 -0
- package/dist/electron-build/agents/channel-identity.js +64 -0
- package/dist/electron-build/agents/channel-identity.js.map +1 -0
- package/dist/electron-build/agents/code-write-gate.js +55 -0
- package/dist/electron-build/agents/code-write-gate.js.map +1 -0
- package/dist/electron-build/agents/deny-pipeline.js +11 -2
- package/dist/electron-build/agents/deny-pipeline.js.map +1 -1
- package/dist/electron-build/agents/egress-sanitize.js +76 -0
- package/dist/electron-build/agents/egress-sanitize.js.map +1 -0
- package/dist/electron-build/agents/error-classifier.js +68 -5
- package/dist/electron-build/agents/error-classifier.js.map +1 -1
- package/dist/electron-build/agents/experience-review.js +243 -0
- package/dist/electron-build/agents/experience-review.js.map +1 -0
- package/dist/electron-build/agents/iteration-budget.js +73 -0
- package/dist/electron-build/agents/iteration-budget.js.map +1 -0
- package/dist/electron-build/agents/lesson-to-skill.js +228 -0
- package/dist/electron-build/agents/lesson-to-skill.js.map +1 -0
- package/dist/electron-build/agents/managed-services.js +69 -0
- package/dist/electron-build/agents/managed-services.js.map +1 -0
- package/dist/electron-build/agents/parse-tool-call.js +43 -32
- 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-session-manager.js +75 -3
- package/dist/electron-build/agents/pi-sdk-session-manager.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-tools.js +200 -31
- package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +799 -206
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/plan-store.js +37 -0
- package/dist/electron-build/agents/plan-store.js.map +1 -1
- package/dist/electron-build/agents/process-runner.js +71 -2
- package/dist/electron-build/agents/process-runner.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/agents/shell-guard.js +110 -14
- package/dist/electron-build/agents/shell-guard.js.map +1 -1
- package/dist/electron-build/agents/skill-health.js +103 -0
- package/dist/electron-build/agents/skill-health.js.map +1 -0
- package/dist/electron-build/agents/skill-ledger.js +146 -0
- package/dist/electron-build/agents/skill-ledger.js.map +1 -0
- package/dist/electron-build/agents/tool-loop-guard.js +319 -0
- package/dist/electron-build/agents/tool-loop-guard.js.map +1 -0
- package/dist/electron-build/agents/tool-path-args.js +63 -0
- package/dist/electron-build/agents/tool-path-args.js.map +1 -0
- package/dist/electron-build/agents/tool-result-gate.js +59 -0
- package/dist/electron-build/agents/tool-result-gate.js.map +1 -0
- package/dist/electron-build/agents/tool-subset.js +116 -0
- package/dist/electron-build/agents/tool-subset.js.map +1 -0
- package/dist/electron-build/agents/tool-telemetry.js +108 -0
- package/dist/electron-build/agents/tool-telemetry.js.map +1 -0
- package/dist/electron-build/bootstrap/context-collector.js +38 -7
- package/dist/electron-build/bootstrap/context-collector.js.map +1 -1
- package/dist/electron-build/bootstrap/lifecycle-hooks.js +12 -3
- package/dist/electron-build/bootstrap/lifecycle-hooks.js.map +1 -1
- package/dist/electron-build/bootstrap/persona-init.js +216 -0
- package/dist/electron-build/bootstrap/persona-init.js.map +1 -0
- package/dist/electron-build/bootstrap/persona-loader.js +5 -1
- package/dist/electron-build/bootstrap/persona-loader.js.map +1 -1
- package/dist/electron-build/context-compaction/auto-compact.js +38 -2
- package/dist/electron-build/context-compaction/auto-compact.js.map +1 -1
- package/dist/electron-build/external-engines/delegate.js +22 -0
- package/dist/electron-build/external-engines/delegate.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/electron-build/orbitdb/cid-database.js +55 -0
- package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
- package/dist/electron-build/orbitdb/ipfs-node.js +36 -11
- package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
- package/dist/electron-build/web/server-storage.js +26 -5
- package/dist/electron-build/web/server-storage.js.map +1 -1
- package/dist/external-engines/delegate.js +21 -0
- package/dist/index.js +526 -95
- 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/orbitdb/cid-database.js +55 -0
- package/dist/orbitdb/event-index.js +370 -0
- package/dist/orbitdb/event-query.js +131 -0
- package/dist/orbitdb/event-shell.js +510 -0
- package/dist/orbitdb/group-access.js +381 -0
- package/dist/orbitdb/group-shards.js +140 -0
- package/dist/orbitdb/ipfs-node.js +36 -11
- package/dist/web/bolloon-web.json +3 -3
- package/dist/web/client.js +4 -2
- package/dist/web/mobile-core.js +7762 -6289
- package/dist/web/routes-mobile-tasks.js +2 -1
- package/dist/web/routes-tasks.js +5 -1
- package/dist/web/server-storage.js +26 -5
- package/dist/web/server.js +60 -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 +2 -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
|
@@ -9,9 +9,28 @@
|
|
|
9
9
|
* - factory → ./pi-sdk-session-factory.ts (createAgentSession / getAgentSession / resetAgentSession / runSelfImproveLoop)
|
|
10
10
|
* - 本文件 (PiAgentSession 类: LLM 循环 / 系统提示 / 工具调用分发 / 压缩 / persistence)
|
|
11
11
|
*/
|
|
12
|
+
import * as fs from 'fs/promises';
|
|
12
13
|
import * as fsSync from 'fs';
|
|
13
14
|
import * as os from 'os';
|
|
14
15
|
import * as path from 'path';
|
|
16
|
+
import { createRunContext } from './run-context.js';
|
|
17
|
+
import { createPrivateActor } from '../kernel/channel-actor.js';
|
|
18
|
+
import { expandHomeArgs } from './tool-path-args.js';
|
|
19
|
+
import { renderDelegateNotices, pushNotice, renderNoticeBlock } from './background-notices.js';
|
|
20
|
+
import { runWithWriteOrigin } from './skill-ledger.js';
|
|
21
|
+
import { createHash } from 'node:crypto';
|
|
22
|
+
import { shouldReviewTask, runExperienceReview } from './experience-review.js';
|
|
23
|
+
import { decideLessonSink, skillsFromDirs, logLessonSuggestions, routeLessonToSkill } from './lesson-to-skill.js';
|
|
24
|
+
import { managedServices } from './managed-services.js';
|
|
25
|
+
import { IterationBudget, isRefundableTool } from './iteration-budget.js';
|
|
26
|
+
import { recordToolCall, argsFingerprint } from './tool-telemetry.js';
|
|
27
|
+
import { capToolResult } from './tool-result-gate.js';
|
|
28
|
+
import { renderToolListWithParams } from './tool-subset.js';
|
|
29
|
+
import { codeWriteTarget, decideTypecheck, formatTypecheckResult } from './code-write-gate.js';
|
|
30
|
+
// 2026-10-01: 身份解析必须**静态导入** —— 之前在函数体里用 require(), 而打包后是 ESM ⇒
|
|
31
|
+
// require 是 undefined ⇒ 抛错被 catch 静默吞掉 ⇒ "自愈"根本没跑, DID 一直是空的 ✗。
|
|
32
|
+
import { loadOrCreateAgentIdentity } from './agent-identity.js';
|
|
33
|
+
import { agentPersonaName } from './channel-identity.js';
|
|
15
34
|
import { getContextManager } from '../bootstrap/context-manager.js';
|
|
16
35
|
import { createRequire } from 'module';
|
|
17
36
|
import { currentPackageRoot } from '../utils/version-info.js';
|
|
@@ -68,7 +87,7 @@ import { ReactHarness } from '../security/react-harness.js';
|
|
|
68
87
|
import { HooksEngine } from '../hooks/hooks-engine.js';
|
|
69
88
|
import { DenyPipeline } from './deny-pipeline.js';
|
|
70
89
|
import { parseToolCall as parseToolCallImpl, parseAllToolCalls, isFinalResponse as isFinalResponseImpl, extractFinalAnswer as extractFinalAnswerImpl } from './parse-tool-call.js';
|
|
71
|
-
import { buildObservation, buildReflection, formatObservationWithReflection } from './error-classifier.js';
|
|
90
|
+
import { buildObservation, buildReflection, formatObservationWithReflection, classifyError } from './error-classifier.js';
|
|
72
91
|
import { sessionStore as defaultSessionStore } from './session-store.js';
|
|
73
92
|
import { ToolRegistry } from './tool-registry.js';
|
|
74
93
|
import { decideMaxIterations, decideContextOverflow, shouldCompactBeforeIteration } from './react-loop.js';
|
|
@@ -130,6 +149,62 @@ export function writeBackCurrentTurnInto(history, wire) {
|
|
|
130
149
|
return 0; // 写回失败绝不影响对话本身
|
|
131
150
|
}
|
|
132
151
|
}
|
|
152
|
+
import { LoopStallState, observeToolCall, batchHint } from './tool-loop-guard.js';
|
|
153
|
+
/**
|
|
154
|
+
* 写操作"读回自证" (2026-10-01 优化 #4): 写类工具成功后**自动**核一次, 把事实拼进结果。
|
|
155
|
+
* 为什么: 过程纪律写了"读回一次", 但**没人执行** ✗ ⇒ 现在由机制执行: "工具说成功 ≠ 任务成功"。
|
|
156
|
+
* 只做**便宜**的核对(存在性/大小); 失败静默(核对不该把工具搞失败)。
|
|
157
|
+
*/
|
|
158
|
+
export function verifyWriteOutcome(toolName, args, cwd) {
|
|
159
|
+
try {
|
|
160
|
+
const fsMod = require('node:fs');
|
|
161
|
+
const pathMod = require('node:path');
|
|
162
|
+
const WRITE = new Set(['write_file', 'edit_file', 'mkdir', 'move_file', 'copy_file']);
|
|
163
|
+
if (!WRITE.has(String(toolName)))
|
|
164
|
+
return null;
|
|
165
|
+
const rel = String(args?.path ?? args?.to ?? args?.dir ?? '').trim();
|
|
166
|
+
if (!rel)
|
|
167
|
+
return null;
|
|
168
|
+
const abs = pathMod.isAbsolute(rel) ? rel : pathMod.resolve(cwd, rel);
|
|
169
|
+
const st = fsMod.statSync(abs);
|
|
170
|
+
if (st.isDirectory())
|
|
171
|
+
return `[已核对] 目录存在: ${rel}`;
|
|
172
|
+
return `[已核对] 文件已落盘: ${rel} (${st.size} 字节, ${new Date(st.mtimeMs).toISOString()})`;
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
return `[未核对] 读回失败: ${String(e?.message || e).slice(0, 80)} —— 别急着说"已完成", 先确认路径/权限`;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* 过程纪律 (2026-10-01, 用户: 「智能体回复方式没有主动性 … 在过程里面更加主动考虑」)。
|
|
180
|
+
*
|
|
181
|
+
* 诊断: 系统提示里只有"理解→分析→调用→观察"这种**反应式**循环描述 ⇒ 模型容易"问一句答一句、
|
|
182
|
+
* 试一次就收尾、把方案当交付"。主动不是靠一句"请主动"能给的, 要落成**可判定的规矩**。
|
|
183
|
+
* 落地方式 (7 条, 都是"做/不做"能验收的): 先动手别先问 · 做到底别停在半成品 · 一个工具不够就换法继续 ·
|
|
184
|
+
* 工具说成功 ≠ 任务成功(读回验证) · 被阻塞如实说绝不编 · 每轮要么推进要么交付 · 顺手想影响面并给下一步建议。
|
|
185
|
+
*/
|
|
186
|
+
export const PROACTIVE_WORK_DISCIPLINE = `
|
|
187
|
+
过程纪律 (比"答得漂亮"更重要, 逐条可判定):
|
|
188
|
+
0. **先动手, 别先问**: 请求有显而易见的默认解释时, 按它做, 不要为了确认而停下 —— 只有"不同解释会让我做不同的事"时才问。
|
|
189
|
+
0.5 **别把"主动"用在闲聊上**: 寒暄 / 感受 / 纯闲聊这类**没有任务**的消息 ⇒ 直接回答, **不要**为此调工具
|
|
190
|
+
(用户实测: 一句「你好」去跑了 git_status 花 4.1s ✗ —— 那是浪费)。"主动"= 接到任务后**想在你前面**,
|
|
191
|
+
不是"每句话都先把环境查一遍" ✗。
|
|
192
|
+
1. **做到底**: 交付的是**能跑的东西 + 真实的工具输出**, 不是"我打算怎么做"。只写方案、只搭一半、只报告打算做什么, 都算没做完。
|
|
193
|
+
2. **一个工具不够就换法继续**: 同一件事试到确实不通为止 —— 换参数/换命令/换路径/换工具接着试, 不要试一次就收尾。
|
|
194
|
+
3. **工具说成功 ≠ 任务成功**: 写入/生成/发布这类动作, **读回一次**再声称完成(内容/存在/大小/哈希都可)。
|
|
195
|
+
4. **被阻塞就如实说**: 讲清卡在哪一步、为什么、还缺什么; **绝不**用编造的结果顶替(编一个"看起来对"的输出比说"没做成"更糟)。
|
|
196
|
+
5. **每一轮要么用工具推进, 要么给出结论**: 不要把"下一步我打算…"当成回答。
|
|
197
|
+
6. **收尾时说三句**: 改了什么 · 验过什么(拿什么读到的) · 还剩什么。不重述过程。
|
|
198
|
+
7.4 **技能是"要用时先读再照做"**: 库里有一批已装载技能(名字+说明), 用 list_skills <关键词> 找,
|
|
199
|
+
用 read_skill <名字> 读全文 —— **相关时先读它再动手**, 别凭印象做(技能里往往写着踩过的坑与判据 ✓)。
|
|
200
|
+
7.5 **一轮里可以同时调多个独立工具** (并行执行已支持): 互不依赖的读取/查询**一次发出去**, 别一个一个来回 —
|
|
201
|
+
零碎调用既慢又费预算(批量工具还会退还额度 ✓)。
|
|
202
|
+
7.6 **收尾标记的语义**(别猜, 就按这条): 写完那个结束标记 = "**我已收尾, 且自己验过**(改了什么/拿什么验证的), 请复核"。
|
|
203
|
+
只在这两种情况下写: ① 任务真的做完了(不是"我打算做") · ② 纯对话/问答也**不用**写(直接答即可)。
|
|
204
|
+
没做完就**别写** —— 写了会被当成"已完成"进入复核, 反而浪费一轮 ✓。
|
|
205
|
+
7. **过程中主动考虑**: 动手前先想这个改动的**影响面**(同类调用点 · 相邻功能 · 已有数据/契约), 发现关联问题就说出来,
|
|
206
|
+
并给出你建议的下一步 —— 主动是指"想在你前面", 不是"多问几句"。
|
|
207
|
+
`;
|
|
133
208
|
export class PiAgentSession {
|
|
134
209
|
cwd;
|
|
135
210
|
peerId;
|
|
@@ -142,7 +217,90 @@ export class PiAgentSession {
|
|
|
142
217
|
sessionManager;
|
|
143
218
|
agentsManager;
|
|
144
219
|
socialHeartbeat = null;
|
|
145
|
-
|
|
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
|
+
}
|
|
146
304
|
tools = new Map();
|
|
147
305
|
/** 2026-06-30: tool registry 模块 — 独立 alias resolve, 测试可消融. */
|
|
148
306
|
_toolRegistry = new ToolRegistry();
|
|
@@ -150,7 +308,51 @@ export class PiAgentSession {
|
|
|
150
308
|
/** M2.4: 缓存 tool 列表, registerTools() 之后不变, runReActLoop 多次循环复用 */
|
|
151
309
|
cachedToolDefinitions = '';
|
|
152
310
|
/** M2.4: 缓存 persona section */
|
|
311
|
+
/**
|
|
312
|
+
* 当前活跃计划 (待办) 的**重注入段** (2026-10-01 落实④)。
|
|
313
|
+
* 每轮都带 —— 因为上下文压缩会把早先注入的计划文本丢掉, 一次性注入等于"压完就忘"。
|
|
314
|
+
* 有界 (条数/字符都封顶); 读不到就返回空串, 绝不影响主流程。
|
|
315
|
+
*/
|
|
316
|
+
async renderActivePlansSection() {
|
|
317
|
+
// 2026-10-01: 后台委派的结果**自动回灌** —— 完成后的第一轮在这里给一行提示(只提一次)
|
|
318
|
+
let bg = '';
|
|
319
|
+
try {
|
|
320
|
+
bg = renderDelegateNotices();
|
|
321
|
+
}
|
|
322
|
+
catch { /* 提示失败不打断主流程 */ }
|
|
323
|
+
try {
|
|
324
|
+
const block = renderNoticeBlock(); // 通用回流(复盘产出/后台任务) —— 空闲排空 ⇒ 新一轮 ✓
|
|
325
|
+
if (block)
|
|
326
|
+
bg = bg ? `${bg}\n${block}` : block;
|
|
327
|
+
}
|
|
328
|
+
catch { /* 回流失败不打断 */ }
|
|
329
|
+
let plans = '';
|
|
330
|
+
try {
|
|
331
|
+
const { listActivePlans, formatPlansForPrompt } = await import('./plan-store.js');
|
|
332
|
+
const list = await listActivePlans();
|
|
333
|
+
plans = formatPlansForPrompt(list, { maxChars: 1200, maxItems: 3 });
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
return bg;
|
|
337
|
+
}
|
|
338
|
+
return bg ? `${bg}\n${plans}` : plans;
|
|
339
|
+
}
|
|
153
340
|
cachedPersonaSection = '';
|
|
341
|
+
/** 本轮迭代预算 (每轮重置; 批处理工具会退还 —— 见 iteration-budget.ts) */
|
|
342
|
+
/** 上一个工具调用的参数指纹 (遥测判"是否重复调用") */
|
|
343
|
+
lastToolSig = null;
|
|
344
|
+
/** 最近一条用户消息 (按需子集档用来判 intent) */
|
|
345
|
+
lastUserIntent = '';
|
|
346
|
+
/** 本回合改过的 TS 文件 (回合收尾自动类型检查用) */
|
|
347
|
+
tsTouchedThisTurn = [];
|
|
348
|
+
/** 本回合是否已跑过类型检查 (一轮只跑一次) */
|
|
349
|
+
typecheckRanThisTurn = false;
|
|
350
|
+
iterBudget = null;
|
|
351
|
+
iterBudgetWarned = false;
|
|
352
|
+
/** 上一次已复盘的**任务签名** (签名变了 ⇒ 立刻复盘, 对应"每次任务都总结" ✓) */
|
|
353
|
+
lastReviewedTaskSig = undefined;
|
|
354
|
+
/** 上一次回合后自审的时间 (节流; 0 = 从未) */
|
|
355
|
+
lastExperienceReviewAt = 0;
|
|
154
356
|
/** 2026-06-30: 持久化层 — 默认走 ~/.bolloon/sessions/cache/, 测试可注入临时目录. */
|
|
155
357
|
_sessionStore;
|
|
156
358
|
/** 构造期间 fire-and-forget 任务的 promise — whenReady() 等它 */
|
|
@@ -261,11 +463,29 @@ export class PiAgentSession {
|
|
|
261
463
|
// 拼到 systemPrompt 末尾, 别再混进 user message
|
|
262
464
|
contextHintAddition = '';
|
|
263
465
|
/**
|
|
264
|
-
*
|
|
265
|
-
*
|
|
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 步。
|
|
266
480
|
*/
|
|
267
|
-
|
|
268
|
-
|
|
481
|
+
actor;
|
|
482
|
+
/**
|
|
483
|
+
* **K2 迁移中**: 一次 Run 的显式状态载体 (见 src/agents/run-context.ts)。
|
|
484
|
+
* 入口 (prompt / promptStream / promptWithPivotLoop) 用 createRunContext() 快照一次, 用完即清。
|
|
485
|
+
* **已外置**: `eventSink` (原 currentOnStream) · `abortSignal` (原 currentSignal)。
|
|
486
|
+
* 其余 6 个字段仍以本类实例字段为准, 逐格迁移。
|
|
487
|
+
*/
|
|
488
|
+
runCtx = createRunContext();
|
|
269
489
|
/** Bootstrap SessionStart 拼的 system prompt 片段 (用完即清) */
|
|
270
490
|
bootstrapAddition = '';
|
|
271
491
|
/** 2026-08-12 (Task3 认知卸载): 工具选择与认知卸载指南 — 注入 system prompt, 降低"模型不触发 write/edit/read"率. */
|
|
@@ -279,12 +499,17 @@ export class PiAgentSession {
|
|
|
279
499
|
- 每步只选一个最合适的工具, 先读后写, 写完验证 (跑 tsc/vitest 或读回文件).`;
|
|
280
500
|
/** 当前 prompt 开始时间 (供 Stop hook 计算 durationMs) */
|
|
281
501
|
promptStartTime = 0;
|
|
282
|
-
/**
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
|
288
513
|
/** 2026-08-10: 本轮用户原始输入 (loop-review 任务动词兜底检测用) */
|
|
289
514
|
currentUserInput = '';
|
|
290
515
|
/**
|
|
@@ -292,7 +517,16 @@ export class PiAgentSession {
|
|
|
292
517
|
* 之前只有"跑完才写"的轨迹 (trajectory-store, fire-and-forget, 崩了就没有), 没有跨重载可见的
|
|
293
518
|
* 运行事实, 也没有预算闸门/失速巡检; 这里补的就是那一层。
|
|
294
519
|
*/
|
|
295
|
-
|
|
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`, 直接读写本体。
|
|
296
530
|
/** 上一次运行的 runId (收尾不清空; 见 getLastRunId) */
|
|
297
531
|
lastRunId = '';
|
|
298
532
|
runSurface = 'cli';
|
|
@@ -309,14 +543,14 @@ export class PiAgentSession {
|
|
|
309
543
|
currentIntentHint = '';
|
|
310
544
|
/**
|
|
311
545
|
* 算 judgment 注入门: 失败静默, 不阻塞主对话
|
|
312
|
-
* 期间通过
|
|
546
|
+
* 期间通过 runCtx.eventSink 广播 phase 事件, 前端可显示 "正在检索判断力..." 状态
|
|
313
547
|
* 调用方负责用完即清 (judgmentGateAddition='')
|
|
314
548
|
*/
|
|
315
549
|
async computeJudgmentGate(input) {
|
|
316
550
|
const safePhase = (phase, extra = {}) => {
|
|
317
551
|
try {
|
|
318
|
-
if (this.
|
|
319
|
-
this.
|
|
552
|
+
if (this.runCtx.eventSink) {
|
|
553
|
+
this.runCtx.eventSink({ type: 'phase', phase, ...extra, content: '' });
|
|
320
554
|
}
|
|
321
555
|
}
|
|
322
556
|
catch { /* 静默 */ }
|
|
@@ -358,17 +592,51 @@ export class PiAgentSession {
|
|
|
358
592
|
this.judgmentGateNegativeUsedIds = [];
|
|
359
593
|
}
|
|
360
594
|
constructor(config) {
|
|
595
|
+
// **K5 步骤⑧**: actor 从出生就在 —— 工厂通常在**构造前**就把身份 actor 注进来 (`config.actor`);
|
|
596
|
+
// 没有身份时用一份**私有 actor** (不注册 ⇒ 谁也拿不到, 隔离性优先)。
|
|
597
|
+
// ⇒ 实例侧不再需要"绑定前暂存"字段 (那些字段本步删除)。
|
|
598
|
+
this.actor = config.actor ?? createPrivateActor();
|
|
361
599
|
this.cwd = config.cwd;
|
|
362
600
|
this.peerId = config.peerId || 'local';
|
|
363
601
|
this.identity = config.identityDoc || this.createDefaultIdentity();
|
|
364
602
|
this.minimaxAvailable = this.checkMinimax();
|
|
365
603
|
// 2026-07-04: 透传 agentId (server.ts 通过 createAgentSession 选项注入)
|
|
366
|
-
this.
|
|
604
|
+
this.actor.state.agentId = config.agentId || '';
|
|
605
|
+
// 2026-10-01 **身份自愈** (用户实测: get_identity 返回 "DID: " 空 + 各 channel 名字串台):
|
|
606
|
+
// 工具上下文里的身份来自 this.identity, 而它可能是 config.identityDoc 与 session 实例身份的
|
|
607
|
+
// 合并结果 —— **空 did 会盖掉真 did** ✗; 名字也可能缺失或被别处覆盖。
|
|
608
|
+
// 规矩: 缺 did / 名字时, 按 **currentAgentId** 从真身份生成器补齐 (did:key + 落盘密钥),
|
|
609
|
+
// 名字优先取该 agent 自己的 persona.json。绝不回落到全局/用户身份名。
|
|
610
|
+
try {
|
|
611
|
+
if (this.actor.state.agentId) {
|
|
612
|
+
const cur = this.identity || {};
|
|
613
|
+
const curDid = String(cur.did || '');
|
|
614
|
+
const badDid = !curDid || /^did:(local|pi):/i.test(curDid);
|
|
615
|
+
const missingName = !String(cur.name || '').trim();
|
|
616
|
+
// 2026-10-01 加固: **did 与本人密钥不符** 也算坏 —— 用户实测 get_identity 返回一个
|
|
617
|
+
// 数据里根本不存在的 did (按 peerId 当 scope 新造的), 而它非空、非假值 ⇒ 上一版判据漏过 ✗。
|
|
618
|
+
// 规矩: 只要 currentAgentId 有值, 身份的 did 就必须等于该 agent 自己的密钥 did。
|
|
619
|
+
const mine = loadOrCreateAgentIdentity(this.actor.state.agentId);
|
|
620
|
+
const mismatch = !!mine?.did && !!curDid && curDid !== mine.did;
|
|
621
|
+
if (badDid || missingName || mismatch) {
|
|
622
|
+
if (badDid || mismatch) {
|
|
623
|
+
console.warn(`[identity] 身份纠正: ${this.actor.state.agentId} 的 did ${curDid ? curDid.slice(0, 26) : '(空)'} ⇒ ${mine.did.slice(0, 26)} (以该 agent 自己的密钥为准)`);
|
|
624
|
+
}
|
|
625
|
+
// 2026-10-01: 同样必须**原地改** —— 这段在 registerTools() **之后**跑, 工具上下文已经按引用
|
|
626
|
+
// 捕获了 this.identity; 换对象 ⇒ 工具看不到纠正结果 ✗ (同类 bug, 一并修)
|
|
627
|
+
Object.assign(this.identity, {
|
|
628
|
+
...(mine?.did ? { did: mine.did, publicKey: mine.publicKey || cur.publicKey } : {}),
|
|
629
|
+
...(missingName ? { name: agentPersonaName(this.actor.state.agentId) || this.actor.state.agentId } : {}),
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
catch { /* 自愈失败不致命 */ }
|
|
367
635
|
// 2026-06-30: 持久化层可注入 — 测试传 tmpDir, 业务用默认 ~/.bolloon/sessions/cache/
|
|
368
636
|
this._sessionStore = config.sessionStore ?? defaultSessionStore;
|
|
369
637
|
this.constraintLayer = new ConstraintLayer();
|
|
370
638
|
this.workflowEngine = new WorkflowEngine(this.constraintLayer);
|
|
371
|
-
this.sessionManager = new PiSessionManager(this.identity.did, this.cwd);
|
|
639
|
+
this.sessionManager = new PiSessionManager(this.identity.did, this.cwd, this.actor.state.agentId);
|
|
372
640
|
this.agentsManager = new DiscoveredAgentsManager();
|
|
373
641
|
this.usePivotLoop = config.usePivotLoop ?? false;
|
|
374
642
|
this.pivotLoopConfig = config.pivotLoopConfig;
|
|
@@ -454,6 +722,18 @@ export class PiAgentSession {
|
|
|
454
722
|
*/
|
|
455
723
|
async hydrateMessageHistory(sessionKey, maxMessages) {
|
|
456
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
|
+
}
|
|
457
737
|
const loaded = await this._sessionStore.loadMessages(sessionKey);
|
|
458
738
|
if (!loaded) {
|
|
459
739
|
console.log(`[PiAgent] hydrate: 没有 ${sessionKey} 的历史`);
|
|
@@ -461,7 +741,7 @@ export class PiAgentSession {
|
|
|
461
741
|
}
|
|
462
742
|
const hydrated = this._filterToMessage(loaded).slice(-maxMessages);
|
|
463
743
|
if (hydrated.length > 0) {
|
|
464
|
-
this.
|
|
744
|
+
this.replaceHistory(hydrated);
|
|
465
745
|
console.log(`[PiAgent] 从 ${sessionKey} 回灌 ${hydrated.length} 条历史`);
|
|
466
746
|
}
|
|
467
747
|
}
|
|
@@ -481,7 +761,10 @@ export class PiAgentSession {
|
|
|
481
761
|
* 即可获得"重启 / 跨进程接续"的语义.
|
|
482
762
|
*/
|
|
483
763
|
async saveCurrentSession(key) {
|
|
484
|
-
|
|
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) => ({
|
|
485
768
|
role: m.role,
|
|
486
769
|
content: m.content,
|
|
487
770
|
toolCall: m.toolCall,
|
|
@@ -498,9 +781,9 @@ export class PiAgentSession {
|
|
|
498
781
|
* 与 loadSessionKey (构造时读) 不同: 这个是 session 已建好后再读.
|
|
499
782
|
*/
|
|
500
783
|
async resumeSession(key, maxMessages = 30) {
|
|
501
|
-
const before = this.messageHistory.length;
|
|
784
|
+
const before = this.actor.state.messageHistory.length;
|
|
502
785
|
await this.hydrateMessageHistory(key, maxMessages);
|
|
503
|
-
return this.messageHistory.length - before;
|
|
786
|
+
return this.actor.state.messageHistory.length - before;
|
|
504
787
|
}
|
|
505
788
|
/**
|
|
506
789
|
* 2026-06-30: 读历史不修改 messageHistory.
|
|
@@ -606,6 +889,59 @@ export class PiAgentSession {
|
|
|
606
889
|
this._inboxMessages = [];
|
|
607
890
|
const toolCtx = {
|
|
608
891
|
tools: this.tools,
|
|
892
|
+
// 2026-10-01 (用户: 「process 也要可以管理群聊和去中心化交流进程」):
|
|
893
|
+
// 把**常驻的交流类服务**也挂进 process —— 能启停的给启停(社交心跳 ✓), 只能看的如实标"仅状态" ✓(不假装能停 ✗)
|
|
894
|
+
managedServices: (() => {
|
|
895
|
+
const ms = managedServices;
|
|
896
|
+
ms.register({
|
|
897
|
+
name: 'social-heartbeat',
|
|
898
|
+
description: '社交心跳: 主动发现节点/发起对话/组织群聊的常驻循环(去中心化交流用)',
|
|
899
|
+
status: () => (this.socialHeartbeat ? '运行中' : '已停止'),
|
|
900
|
+
start: () => this.startSocialHeartbeat(),
|
|
901
|
+
stop: () => this.stopSocialHeartbeat(),
|
|
902
|
+
});
|
|
903
|
+
ms.register({
|
|
904
|
+
name: 'p2p-network',
|
|
905
|
+
description: 'P2P 网络: 与其他 bolloon 节点的连接(群聊/消息都走它)',
|
|
906
|
+
status: () => { try {
|
|
907
|
+
const peers = this.getPeers?.() ?? [];
|
|
908
|
+
return `已连接 ${peers.length} 个节点`;
|
|
909
|
+
}
|
|
910
|
+
catch {
|
|
911
|
+
return '状态不可用';
|
|
912
|
+
} },
|
|
913
|
+
});
|
|
914
|
+
ms.register({
|
|
915
|
+
name: 'orbitdb-groups',
|
|
916
|
+
description: 'OrbitDB 群聊存储: 本地群列表与消息持久化',
|
|
917
|
+
status: () => { try {
|
|
918
|
+
const g = this.listGroupsShallow?.();
|
|
919
|
+
return g ? `本地群 ${g}` : '已就绪(用 /group 看列表)';
|
|
920
|
+
}
|
|
921
|
+
catch {
|
|
922
|
+
return '状态不可用';
|
|
923
|
+
} },
|
|
924
|
+
});
|
|
925
|
+
ms.register({
|
|
926
|
+
name: 'document-receiver',
|
|
927
|
+
description: '文档接收器: 监听并接收其他节点发来的文档分片',
|
|
928
|
+
status: () => '监听中(随进程启动)',
|
|
929
|
+
});
|
|
930
|
+
return ms;
|
|
931
|
+
})(),
|
|
932
|
+
// 2026-10-01: 技能发现(list_skills / read_skill)的实现 —— 技能原先只注册不露面 ✗, 现在模型能自己找
|
|
933
|
+
listSkills: () => this.skillRegistry.list().map((sk) => ({ name: sk.name, description: String(sk.description || '') })),
|
|
934
|
+
getSkillBody: async (name) => {
|
|
935
|
+
const sk = this.skillRegistry.get(name);
|
|
936
|
+
if (!sk)
|
|
937
|
+
return null;
|
|
938
|
+
try {
|
|
939
|
+
return await sk.execute({});
|
|
940
|
+
}
|
|
941
|
+
catch {
|
|
942
|
+
return null;
|
|
943
|
+
}
|
|
944
|
+
},
|
|
609
945
|
cwd: this.cwd,
|
|
610
946
|
identity: this.identity,
|
|
611
947
|
persona: this.persona,
|
|
@@ -619,7 +955,7 @@ export class PiAgentSession {
|
|
|
619
955
|
const { CHANNELS_PATH } = await import('../web/server-types.js');
|
|
620
956
|
const { loadChannels } = await import('../web/server-storage.js');
|
|
621
957
|
const channels = await loadChannels();
|
|
622
|
-
const ch = channels.find((c) => c.id === this.
|
|
958
|
+
const ch = channels.find((c) => c.id === this.actor.state.channelId);
|
|
623
959
|
if (ch && ch.encryptedPrivateKey && ch.encryptedPrivateKeyIv && ch.walletAddress) {
|
|
624
960
|
return {
|
|
625
961
|
encryptedPrivateKey: ch.encryptedPrivateKey,
|
|
@@ -668,16 +1004,18 @@ export class PiAgentSession {
|
|
|
668
1004
|
// 2026-07-29: 拒绝列表变化时清空缓存, 重新生成
|
|
669
1005
|
if (this.cachedToolDefinitions)
|
|
670
1006
|
return this.cachedToolDefinitions;
|
|
671
|
-
const defs = ['可用工具 (name(params) - 简介):'];
|
|
672
1007
|
// 2026-07-29: 使用 allowedTools() 过滤掉拒绝列表中的工具
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
1008
|
+
const allowed = Array.from(this.allowedTools());
|
|
1009
|
+
const header = '可用工具 (按类分组; name(params) - 简介):';
|
|
1010
|
+
// 2026-06-19: 压缩 tool 定义 — 只显示参数名 (不显示描述, 减少 60% 长度)
|
|
1011
|
+
// 完整 description 在 history 第一轮注入 (getToolDefinitionsFull 调用), 后续轮只看简短
|
|
1012
|
+
// 避免 system prompt 太大导致 minimax 撞 max_tokens 输出空
|
|
1013
|
+
// 2026-10-01 (优化 #2 v2): 按类**分组**列 + 说明"未列出的也能按名字调" ⇒ 治选择过载, 且不丢能力。
|
|
1014
|
+
// BOLLOON_TOOL_SUBSET=on 时改走"按 intent 只展开相关桶"(仍带 list_tools + 能力提示 ✓)。
|
|
1015
|
+
const subsetOn = String(process.env.BOLLOON_TOOL_SUBSET || '').toLowerCase() === 'on';
|
|
1016
|
+
const intentText = subsetOn ? this.lastUserIntent : '';
|
|
1017
|
+
const rendered = renderToolListWithParams(allowed, intentText, { subset: subsetOn, perBucket: 14 });
|
|
1018
|
+
this.cachedToolDefinitions = `${header}\n${rendered.text}`;
|
|
681
1019
|
return this.cachedToolDefinitions;
|
|
682
1020
|
}
|
|
683
1021
|
async initSession() {
|
|
@@ -689,12 +1027,28 @@ export class PiAgentSession {
|
|
|
689
1027
|
}
|
|
690
1028
|
}
|
|
691
1029
|
createDefaultIdentity() {
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
1030
|
+
// 2026-10-01: 原先自造 `did:pi:<peerId>` —— **不是有效 DID** (仓里 server.ts 自己都把 did:pi:
|
|
1031
|
+
// 当"待升级"占位; 用户实测 get_identity 报 did:pi:ch_1785668060213)。
|
|
1032
|
+
// 改用仓里既有的真身份生成器 (agent-identity.loadOrCreateAgentIdentity, 同步, 产 did:key + 落盘密钥)。
|
|
1033
|
+
const scope = this.actor.state.agentId || this.peerId || 'default';
|
|
1034
|
+
try {
|
|
1035
|
+
const real = loadOrCreateAgentIdentity(scope);
|
|
1036
|
+
return {
|
|
1037
|
+
did: real.did,
|
|
1038
|
+
name: `Agent-${this.peerId.substring(0, 8)}`,
|
|
1039
|
+
publicKey: real.publicKey || this.peerId,
|
|
1040
|
+
createdAt: Date.now(),
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
catch {
|
|
1044
|
+
// 真身份生成失败时**不编假 DID**: 如实留空, 由 /did 与 get_identity 报"未绑定"
|
|
1045
|
+
return {
|
|
1046
|
+
did: '',
|
|
1047
|
+
name: `Agent-${this.peerId.substring(0, 8)}`,
|
|
1048
|
+
publicKey: this.peerId,
|
|
1049
|
+
createdAt: Date.now(),
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
698
1052
|
}
|
|
699
1053
|
checkMinimax() {
|
|
700
1054
|
try {
|
|
@@ -735,9 +1089,9 @@ export class PiAgentSession {
|
|
|
735
1089
|
const did = await resolveUserDid();
|
|
736
1090
|
const model = this.llmConfig?.model || this.model || '';
|
|
737
1091
|
return new TrajectoryRecorder({
|
|
738
|
-
agentId: this.
|
|
1092
|
+
agentId: this.actor.state.agentId || 'default',
|
|
739
1093
|
input,
|
|
740
|
-
channelId: channelId || this.
|
|
1094
|
+
channelId: channelId || this.actor.state.channelId,
|
|
741
1095
|
did: did || undefined,
|
|
742
1096
|
model: typeof model === 'string' && model ? model : undefined,
|
|
743
1097
|
});
|
|
@@ -779,8 +1133,8 @@ export class PiAgentSession {
|
|
|
779
1133
|
if (gate !== 'ready') {
|
|
780
1134
|
const why = `初始化未就绪 (${gate}, 当前阶段 ${state.stage})${state.lastError ? ` — ${state.lastError.message}` : ''}`;
|
|
781
1135
|
const hint = (state.actions && state.actions[0]) || '运行 `bolloon setup` 完成初始化';
|
|
782
|
-
this.
|
|
783
|
-
this.
|
|
1136
|
+
this.pushHistory({ role: 'user', content: input });
|
|
1137
|
+
this.pushHistory({ role: 'assistant', content: `[初始化未就绪] ${why}\n${hint}` });
|
|
784
1138
|
console.warn(`[PiAgent] 拒绝执行: ${why}`);
|
|
785
1139
|
return `[初始化未就绪] ${why}\n${hint}`;
|
|
786
1140
|
}
|
|
@@ -788,13 +1142,13 @@ export class PiAgentSession {
|
|
|
788
1142
|
catch { /* 门禁自身异常 → 按"不能判定"处理会阻塞一切; 这里只在能读到状态时才拦 */ }
|
|
789
1143
|
}
|
|
790
1144
|
this.minimaxAvailable = this.checkMinimax();
|
|
791
|
-
this.
|
|
1145
|
+
this.actor.state.channelId = options?.channelId ?? this.actor.state.channelId;
|
|
792
1146
|
// 2026-08-08: 运行轨迹采集 (落盘 + OrbitDB, 失败静默) — 包裹 onStream 收集步骤事件
|
|
793
1147
|
const trajRec = await this.createTrajectoryRecorder(input, options?.channelId);
|
|
794
1148
|
if (trajRec && options?.onStream) {
|
|
795
1149
|
options = { ...options, onStream: this.wrapTrajectoryStream(options.onStream, trajRec) };
|
|
796
1150
|
}
|
|
797
|
-
this.
|
|
1151
|
+
this.pushHistory({
|
|
798
1152
|
role: 'user',
|
|
799
1153
|
content: input
|
|
800
1154
|
});
|
|
@@ -804,23 +1158,23 @@ export class PiAgentSession {
|
|
|
804
1158
|
// 于是把"什么都没跑"当成一次正常执行 (这正是"agent 跑了但没记录"的老毛病在 fallback 路径的翻版)。
|
|
805
1159
|
const response = await this.handleFallback(input);
|
|
806
1160
|
try {
|
|
807
|
-
let boundGoalId = this.
|
|
1161
|
+
let boundGoalId = this.actor.state.goalBinding;
|
|
808
1162
|
if (!boundGoalId) {
|
|
809
1163
|
const g = await createGoal({
|
|
810
1164
|
objective: this.currentUserInput || input.slice(0, 200),
|
|
811
|
-
channelId: this.
|
|
812
|
-
agentId: this.
|
|
1165
|
+
channelId: this.actor.state.channelId || undefined,
|
|
1166
|
+
agentId: this.actor.state.agentId || undefined,
|
|
813
1167
|
createdBy: this.runSurface,
|
|
814
1168
|
});
|
|
815
1169
|
boundGoalId = g.goalId;
|
|
816
1170
|
}
|
|
817
|
-
this.
|
|
1171
|
+
this.actor.state.goalBinding = boundGoalId;
|
|
818
1172
|
const rec = await startRun({
|
|
819
1173
|
surface: this.runSurface,
|
|
820
1174
|
goal: this.currentUserInput || input.slice(0, 200),
|
|
821
1175
|
goalId: boundGoalId,
|
|
822
|
-
channelId: this.
|
|
823
|
-
agentId: this.
|
|
1176
|
+
channelId: this.actor.state.channelId || undefined,
|
|
1177
|
+
agentId: this.actor.state.agentId || undefined,
|
|
824
1178
|
modelConfig: await this.runModelSnapshot(),
|
|
825
1179
|
});
|
|
826
1180
|
this.lastRunId = rec.runId;
|
|
@@ -859,25 +1213,27 @@ export class PiAgentSession {
|
|
|
859
1213
|
catch (err) {
|
|
860
1214
|
await recordDegradation({ kind: 'core', op: 'pi-sdk.fallbackRun', runId: this.lastRunId || '', message: String(err?.message || err).slice(0, 160) }).catch(() => { });
|
|
861
1215
|
}
|
|
862
|
-
this.
|
|
1216
|
+
this.pushHistory({ role: 'assistant', content: response });
|
|
863
1217
|
this.reportUsageToContextManager();
|
|
864
1218
|
this.finishTrajectory(trajRec, response);
|
|
865
1219
|
return response;
|
|
866
1220
|
}
|
|
867
1221
|
// P0 注入门
|
|
868
|
-
|
|
869
|
-
|
|
1222
|
+
// K2: eventSink + abortSignal 已外置, 入口一次建好本轮 Context。
|
|
1223
|
+
// 其余字段**不在这里抄一份** —— 抄写会新增对旧实例字段的读, 违反方向判据
|
|
1224
|
+
// 「新层出现后旧写口调用点数只许不变或减少」。逐字段迁移时再各自搬进来。
|
|
1225
|
+
this.runCtx = this.seedRunContext({ eventSink: options?.onStream ?? null, abortSignal: options?.signal ?? null });
|
|
870
1226
|
await this.computeJudgmentGate(input);
|
|
871
1227
|
// M2.2 (2026-06-17): intent 分类 — prompt() 路径也跑 (跟 promptStream 对齐)
|
|
872
1228
|
try {
|
|
873
1229
|
const { classifyIntent, intentHint } = await import('./intent-classifier.js');
|
|
874
|
-
this.
|
|
875
|
-
this.currentIntentHint = intentHint(this.
|
|
1230
|
+
this.runCtx.intent = classifyIntent(input);
|
|
1231
|
+
this.currentIntentHint = intentHint(this.runCtx.intent);
|
|
876
1232
|
this.currentUserInput = input;
|
|
877
1233
|
}
|
|
878
1234
|
catch (err) {
|
|
879
1235
|
console.warn('[PiAgent] classifyIntent in prompt() failed:', err);
|
|
880
|
-
this.
|
|
1236
|
+
this.runCtx.intent = 'chitchat';
|
|
881
1237
|
this.currentIntentHint = '';
|
|
882
1238
|
}
|
|
883
1239
|
// P2: 解析当前 permission mode
|
|
@@ -902,14 +1258,113 @@ export class PiAgentSession {
|
|
|
902
1258
|
recordJudgmentUsage(this.judgmentGateUsedIds, { userInput: input }).catch((err) => console.warn('[PiAgent] recordJudgmentUsage failed:', err));
|
|
903
1259
|
}
|
|
904
1260
|
this.clearJudgmentGate();
|
|
905
|
-
this.
|
|
906
|
-
this.currentOnStream = null;
|
|
1261
|
+
this.runCtx = createRunContext(); // K2: 用完即清 (换新 Context, 不继承残留);
|
|
907
1262
|
this.reportUsageToContextManager();
|
|
908
1263
|
}
|
|
909
1264
|
}
|
|
910
1265
|
try {
|
|
911
1266
|
// 2026-06-16: runReActLoop 现在返回 { reply, aiFailed, aiFailureReason } — 这里只需 reply 字符串
|
|
912
|
-
const loopResult = await this.runReActLoop(this.
|
|
1267
|
+
const loopResult = await this.runReActLoop(this.runCtx.eventSink ?? undefined, options?.signal);
|
|
1268
|
+
// 2026-10-01: **本回合改过 TS ⇒ 收尾自动类型检查** (一轮一次) —— 复用已有的 tsc_check 工具
|
|
1269
|
+
if (decideTypecheck(this.tsTouchedThisTurn, this.typecheckRanThisTurn)) {
|
|
1270
|
+
this.typecheckRanThisTurn = true;
|
|
1271
|
+
try {
|
|
1272
|
+
const tscTool = this.tools.get('tsc_check');
|
|
1273
|
+
if (tscTool?.execute) {
|
|
1274
|
+
const r = await tscTool.execute({});
|
|
1275
|
+
const line = formatTypecheckResult(r?.success !== false && !/error TS\d+/.test(String(r?.output || '')), String(r?.output || ''));
|
|
1276
|
+
this.runCtx.eventSink?.({ type: 'status', content: `🔎 ${line}`, tool: 'system' });
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
catch (e) {
|
|
1280
|
+
this.runCtx.eventSink?.({ type: 'status', content: `🔎 类型检查没能跑起来: ${String(e?.message || e).slice(0, 100)} (改动已落盘, 记得自己跑一次)`, tool: 'system' });
|
|
1281
|
+
}
|
|
1282
|
+
this.tsTouchedThisTurn = [];
|
|
1283
|
+
}
|
|
1284
|
+
// 2026-10-01: **回合后自审 (纯旁路)** —— 沉淀可复用经验, 对应"结束后沉淀"那一段。
|
|
1285
|
+
// 纪律: fire-and-forget (不阻塞主回合) · 只写 ~/.bolloon/experience/ (**不碰主对话/prompt 缓存**)
|
|
1286
|
+
// · 节流 (默认 10 分钟/agent) · 失败只记一行日志, 绝不影响本回合结果。
|
|
1287
|
+
try {
|
|
1288
|
+
const reviewNow = Date.now();
|
|
1289
|
+
const turnSummary = `用户: ${String(this.currentUserInput || '').slice(0, 2000)}\n助手: ${String(loopResult?.reply || '').slice(0, 4000)}`;
|
|
1290
|
+
// 任务签名: 用户这次要的东西 + 用了哪些工具 ⇒ 换任务立刻复盘(同一任务才节流) ✓
|
|
1291
|
+
const taskSig = createHash('sha256')
|
|
1292
|
+
.update(`${String(this.currentUserInput || '').slice(0, 300)}|${this.totalToolCallsThisTurn || ''}`)
|
|
1293
|
+
.digest('hex').slice(0, 12);
|
|
1294
|
+
if (shouldReviewTask(reviewNow, this.lastExperienceReviewAt, this.lastReviewedTaskSig, taskSig)) {
|
|
1295
|
+
this.lastExperienceReviewAt = reviewNow; // 先记账, 免得多路并发各起一次
|
|
1296
|
+
this.lastReviewedTaskSig = taskSig;
|
|
1297
|
+
// 2026-10-01 (用户: 「我没有看到执行完命令后 bolloon 知道选中更新 skills,是不是没有触发」):
|
|
1298
|
+
// 实测**确实没触发** ✓ —— 两个原因: ① 被调方又自己判了一次节流 ⇒ 换任务也被 throttled 掉 ✗;
|
|
1299
|
+
// ② 反馈走 `console.warn`(TUI 里被吞 ✗) + `void … .catch(() => {})`(失败无声 ✗)
|
|
1300
|
+
// ⇒ 你什么都看不到 ✓。现在: 传 force 尊重调用方的决定 ✓ · 复盘过程/结果/失败**都进对话流** ✓ ·
|
|
1301
|
+
// 同时落一份 `~/.bolloon/logs/experience-review.log` 供事后核 ✓。
|
|
1302
|
+
const reviewLog = (m) => {
|
|
1303
|
+
const line = `[${new Date().toISOString()}] ${m}`;
|
|
1304
|
+
try {
|
|
1305
|
+
const p = path.join(os.homedir(), '.bolloon', 'logs', 'experience-review.log');
|
|
1306
|
+
void fs.mkdir(path.dirname(p), { recursive: true })
|
|
1307
|
+
.then(() => fs.appendFile(p, line + '\n'))
|
|
1308
|
+
.catch(() => { });
|
|
1309
|
+
}
|
|
1310
|
+
catch { /* 落日志失败不影响主流程 */ }
|
|
1311
|
+
try {
|
|
1312
|
+
this.runCtx.eventSink?.({ type: 'status', content: `📚 复盘: ${m}`, tool: 'system' });
|
|
1313
|
+
}
|
|
1314
|
+
catch { /* 上屏失败不打断 */ }
|
|
1315
|
+
};
|
|
1316
|
+
reviewLog('开始(换了任务 ⇒ 立刻复盘)');
|
|
1317
|
+
void runExperienceReview({
|
|
1318
|
+
turnSummary,
|
|
1319
|
+
force: true,
|
|
1320
|
+
chat: async (prompt) => {
|
|
1321
|
+
const r = await getMinimax().chat(prompt);
|
|
1322
|
+
return typeof r === 'string' ? r : String(r?.content ?? r?.text ?? '');
|
|
1323
|
+
},
|
|
1324
|
+
log: reviewLog,
|
|
1325
|
+
// 2026-10-01: 写了经验之后再找"能沉淀进哪个已有技能"的候选 —— **只记候选, 不自动改技能** ✓
|
|
1326
|
+
// (实测: 拿真实教训撞真实 1300 个技能, 最高分只有 2 且 top 命中是瞎的 ✗ ⇒ 不替人决定 ✓)
|
|
1327
|
+
onLesson: (lesson) => {
|
|
1328
|
+
try {
|
|
1329
|
+
// ⓐ 管理: 找"能沉淀进哪个已有技能"的候选(只记, 不自动改 ✗)
|
|
1330
|
+
const dirs = defaultSkillPaths(os.homedir(), process.cwd());
|
|
1331
|
+
const decision = decideLessonSink(lesson, skillsFromDirs(dirs));
|
|
1332
|
+
logLessonSuggestions(lesson, decision);
|
|
1333
|
+
// ⓐ' 用户要求「**都进去**」⇒ 每条教训都落进技能库: 强命中写那个技能 ✓, 否则写沉淀技能 `lessons-learned` ✓
|
|
1334
|
+
// 2026-10-01 **写来源隔离**: 复盘/自审产生的写, 打上 review 标记 ✓ ⇒
|
|
1335
|
+
// 它只许治理**自己造出来**的技能 ✓(用户在 skills 里点名要的归用户 ✓)
|
|
1336
|
+
runWithWriteOrigin('review', () => routeLessonToSkill(lesson, skillsFromDirs(dirs)));
|
|
1337
|
+
// 2026-10-01 (用户: 「复盘任务能否触发 loop?」): 教训**回流成下一轮的任务源** ✓ ——
|
|
1338
|
+
// 只"记下来"等于闭环断在这 ✗; 投进共享队列 ⇒ 空闲时排空 ⇒ 以**新一轮**浮现 ✓(绝不中途插队 ✓)。
|
|
1339
|
+
try {
|
|
1340
|
+
pushNotice('review', `${lesson.title} —— ${String(lesson.body || '').replace(/\s+/g, ' ').slice(0, 120)}`);
|
|
1341
|
+
}
|
|
1342
|
+
catch { /* 回流失败不打断 */ }
|
|
1343
|
+
// ⓑ 接入**判断力系统**: 同一条教训也进 HumanJudgment(带 source/confidence/revisable ⇒ 可被后续演化取代 ✓)
|
|
1344
|
+
// 这样经验库与判断力库**同一份来源**, 判断力注入(gate)时就能用上 ✓
|
|
1345
|
+
import('../pi-ecosystem-judgment/human-value-store.js').then((m) => m.storeHumanJudgment({
|
|
1346
|
+
decision: lesson.title,
|
|
1347
|
+
decision_type: 'modify',
|
|
1348
|
+
reasons: [lesson.body],
|
|
1349
|
+
values_derived: [],
|
|
1350
|
+
context: { domain: lesson.klass || 'general', complexity: 'simple', stakes: 'medium', time_pressure: 'low' },
|
|
1351
|
+
outcome: { approved: true },
|
|
1352
|
+
metadata: { source: 'trajectory', confidence: 0.7, revisable: true },
|
|
1353
|
+
})).catch(() => { });
|
|
1354
|
+
}
|
|
1355
|
+
catch { /* 这一段整体是锦上添花, 绝不外泄错误 */ }
|
|
1356
|
+
},
|
|
1357
|
+
}).then((r) => {
|
|
1358
|
+
// 结果**如实上屏** ✓: 审没审 / 写没写 / 为什么没写 —— 不再无声 ✓
|
|
1359
|
+
reviewLog(r?.reviewed
|
|
1360
|
+
? (r?.applied ? `已沉淀经验 ✓${r?.file ? ` → ${path.basename(String(r.file))}` : ''}` : `审了但没写: ${r?.reason || '无'}`)
|
|
1361
|
+
: `没审: ${r?.reason || '无'}`);
|
|
1362
|
+
}).catch((e) => {
|
|
1363
|
+
reviewLog(`失败(已如实记录, 不外泄错误): ${String(e?.message || e).slice(0, 160)}`);
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
catch { /* 挂点自身失败也不影响主流程 */ }
|
|
913
1368
|
this.finishTrajectory(trajRec, loopResult.reply, loopResult.aiFailed ? 'error' : 'ok');
|
|
914
1369
|
return loopResult.reply;
|
|
915
1370
|
}
|
|
@@ -921,8 +1376,7 @@ export class PiAgentSession {
|
|
|
921
1376
|
recordJudgmentUsage(this.judgmentGateNegativeUsedIds, { userInput: input, polarity: 'negative' }).catch((err) => console.warn('[PiAgent] recordJudgmentUsage (negative) failed:', err));
|
|
922
1377
|
}
|
|
923
1378
|
this.clearJudgmentGate();
|
|
924
|
-
this.
|
|
925
|
-
this.currentOnStream = null;
|
|
1379
|
+
this.runCtx = createRunContext(); // K2: 用完即清 (换新 Context, 不继承残留);
|
|
926
1380
|
this.reportUsageToContextManager();
|
|
927
1381
|
}
|
|
928
1382
|
}
|
|
@@ -930,7 +1384,7 @@ export class PiAgentSession {
|
|
|
930
1384
|
console.log(`[PiAgent.promptStream] ENTRY, channelId=${channelId}, input chars=${input.length}`);
|
|
931
1385
|
this.minimaxAvailable = this.checkMinimax();
|
|
932
1386
|
console.log(`[PiAgent.promptStream] minimaxAvailable=${this.minimaxAvailable}`);
|
|
933
|
-
this.
|
|
1387
|
+
this.actor.state.channelId = channelId ?? this.actor.state.channelId;
|
|
934
1388
|
// 2026-08-08: 运行轨迹采集 (落盘 + OrbitDB, 失败静默) — 包裹 onStream 收集步骤事件
|
|
935
1389
|
const trajRec = await this.createTrajectoryRecorder(input, channelId);
|
|
936
1390
|
if (trajRec)
|
|
@@ -943,7 +1397,7 @@ export class PiAgentSession {
|
|
|
943
1397
|
const userText = markerMatch ? markerMatch[1].trim() : input;
|
|
944
1398
|
const contextHint = markerMatch ? input.replace(markerMatch[0], '').trim() : '';
|
|
945
1399
|
console.log(`[PiAgent.promptStream] marker matched=${!!markerMatch}, userText chars=${userText.length}, contextHint chars=${contextHint.length}`);
|
|
946
|
-
this.
|
|
1400
|
+
this.pushHistory({
|
|
947
1401
|
role: 'user',
|
|
948
1402
|
content: userText
|
|
949
1403
|
});
|
|
@@ -953,7 +1407,7 @@ export class PiAgentSession {
|
|
|
953
1407
|
// 让 agent 能"回忆起"之前 session 的记忆 (自动获取之前 session), 而非只靠启动时批量压缩.
|
|
954
1408
|
try {
|
|
955
1409
|
const { recallMemory } = await import('./memory-recall.js');
|
|
956
|
-
const recalled = await recallMemory({ query: userText, agentId: this.
|
|
1410
|
+
const recalled = await recallMemory({ query: userText, agentId: this.actor.state.agentId || this.peerId || '' });
|
|
957
1411
|
if (recalled) {
|
|
958
1412
|
this.contextHintAddition = [this.contextHintAddition, recalled].filter(Boolean).join('\n\n');
|
|
959
1413
|
}
|
|
@@ -962,29 +1416,29 @@ export class PiAgentSession {
|
|
|
962
1416
|
onStream({ type: 'thinking', content: '🤔 开始思考...' });
|
|
963
1417
|
if (!this.minimaxAvailable) {
|
|
964
1418
|
const response = await this.handleFallback(userText);
|
|
965
|
-
this.
|
|
1419
|
+
this.pushHistory({ role: 'assistant', content: response });
|
|
966
1420
|
onStream({ type: 'done', content: '' });
|
|
967
1421
|
this.reportUsageToContextManager();
|
|
968
1422
|
this.finishTrajectory(trajRec, response);
|
|
969
1423
|
return response;
|
|
970
1424
|
}
|
|
971
|
-
// P0 注入门: 缓存 onStream + signal, computeJudgmentGate 用
|
|
972
|
-
|
|
973
|
-
this.
|
|
1425
|
+
// P0 注入门: 缓存 onStream + signal, computeJudgmentGate 用 runCtx.eventSink 广播 phase
|
|
1426
|
+
// K2: eventSink + abortSignal 一起建进本轮 Context (只搬已外置的字段, 不抄未迁移的)
|
|
1427
|
+
this.runCtx = this.seedRunContext({ eventSink: onStream, abortSignal: signal ?? null });
|
|
974
1428
|
await this.computeJudgmentGate(userText);
|
|
975
1429
|
// M2.2 (2026-06-17): intent 分类 — 0 LLM 成本, 5 行 keyword 匹配
|
|
976
1430
|
try {
|
|
977
1431
|
const { classifyIntent, intentHint } = await import('./intent-classifier.js');
|
|
978
|
-
this.
|
|
979
|
-
this.currentIntentHint = intentHint(this.
|
|
1432
|
+
this.runCtx.intent = classifyIntent(userText);
|
|
1433
|
+
this.currentIntentHint = intentHint(this.runCtx.intent);
|
|
980
1434
|
this.currentUserInput = userText;
|
|
981
|
-
if (this.
|
|
982
|
-
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: '' });
|
|
983
1437
|
}
|
|
984
1438
|
}
|
|
985
1439
|
catch (err) {
|
|
986
1440
|
console.warn('[PiAgent] classifyIntent failed (non-fatal):', err);
|
|
987
|
-
this.
|
|
1441
|
+
this.runCtx.intent = 'chitchat';
|
|
988
1442
|
this.currentIntentHint = '';
|
|
989
1443
|
}
|
|
990
1444
|
// P1.1: 异步跑 Auto-Compact (LLM 摘要, 仅在 budget 超限时触发, 失败静默)
|
|
@@ -1001,8 +1455,8 @@ export class PiAgentSession {
|
|
|
1001
1455
|
let bootstrapAddition = '';
|
|
1002
1456
|
try {
|
|
1003
1457
|
const ss = await onSessionStart({
|
|
1004
|
-
channelId: this.
|
|
1005
|
-
agentId: this.
|
|
1458
|
+
channelId: this.actor.state.channelId || undefined,
|
|
1459
|
+
agentId: this.actor.state.agentId || undefined,
|
|
1006
1460
|
});
|
|
1007
1461
|
bootstrapAddition = ss.systemAddition || '';
|
|
1008
1462
|
}
|
|
@@ -1011,10 +1465,10 @@ export class PiAgentSession {
|
|
|
1011
1465
|
}
|
|
1012
1466
|
// 2026-07-07 P1-B: 注入最近 5 条项目事件日志 (L2) — 让 LLM 知道项目状态/feature 变化
|
|
1013
1467
|
// 失败静默, append 到 bootstrapAddition 末尾 (超 800 字截断)
|
|
1014
|
-
if (this.
|
|
1468
|
+
if (this.actor.state.channelId) {
|
|
1015
1469
|
try {
|
|
1016
1470
|
const { getRecentEvents } = await import('../bootstrap/event-log.js');
|
|
1017
|
-
const events = await getRecentEvents(this.
|
|
1471
|
+
const events = await getRecentEvents(this.actor.state.channelId, 5);
|
|
1018
1472
|
if (events.length > 0) {
|
|
1019
1473
|
const eventBlock = [
|
|
1020
1474
|
'## 最近项目事件 (最近 5 条, 倒序)',
|
|
@@ -1029,7 +1483,7 @@ export class PiAgentSession {
|
|
|
1029
1483
|
// 2026-07-07 P2-C: 注入项目当前状态 (L3) — 目标/约束/待办/已完成
|
|
1030
1484
|
try {
|
|
1031
1485
|
const { readState, formatStateForPrompt } = await import('../bootstrap/project-state.js');
|
|
1032
|
-
const state = await readState({ channelId: this.
|
|
1486
|
+
const state = await readState({ channelId: this.actor.state.channelId });
|
|
1033
1487
|
const stateText = formatStateForPrompt(state);
|
|
1034
1488
|
if (stateText) {
|
|
1035
1489
|
bootstrapAddition = (bootstrapAddition + '\n\n' + stateText).slice(-2500);
|
|
@@ -1041,7 +1495,7 @@ export class PiAgentSession {
|
|
|
1041
1495
|
// 2026-07-07 P2-C: 向量检索 top-3 (L4) — 按当前 channelId + userText 找历史相关片段
|
|
1042
1496
|
try {
|
|
1043
1497
|
const { searchIndex } = await import('../bootstrap/vector-index.js');
|
|
1044
|
-
const indexName = `channel-${this.
|
|
1498
|
+
const indexName = `channel-${this.actor.state.channelId}`;
|
|
1045
1499
|
const results = await searchIndex({
|
|
1046
1500
|
indexName,
|
|
1047
1501
|
query: userText,
|
|
@@ -1102,13 +1556,12 @@ export class PiAgentSession {
|
|
|
1102
1556
|
monitorAfterReply(userText, pivotResult);
|
|
1103
1557
|
const stopStartTime = this.promptStartTime || Date.now();
|
|
1104
1558
|
onStop({
|
|
1105
|
-
channelId: this.
|
|
1559
|
+
channelId: this.actor.state.channelId || 'unknown',
|
|
1106
1560
|
durationMs: Date.now() - stopStartTime,
|
|
1107
1561
|
usedJudgmentIds: [...this.judgmentGateUsedIds],
|
|
1108
1562
|
}).catch((err) => console.warn('[PiAgent] onStop failed:', err));
|
|
1109
1563
|
this.clearJudgmentGate();
|
|
1110
|
-
this.
|
|
1111
|
-
this.currentSignal = null;
|
|
1564
|
+
this.runCtx = createRunContext(); // K2: 用完即清 (换新 Context, 不继承残留);
|
|
1112
1565
|
this.bootstrapAddition = '';
|
|
1113
1566
|
this.contextHintAddition = '';
|
|
1114
1567
|
this.promptStartTime = 0;
|
|
@@ -1138,8 +1591,7 @@ export class PiAgentSession {
|
|
|
1138
1591
|
}
|
|
1139
1592
|
catch (err) {
|
|
1140
1593
|
// abort 失败: 视作"已中断", 抛错让上层用 partial 兜底
|
|
1141
|
-
this.
|
|
1142
|
-
this.currentSignal = null;
|
|
1594
|
+
this.runCtx = createRunContext(); // K2: 用完即清 (换新 Context, 不继承残留);
|
|
1143
1595
|
throw err;
|
|
1144
1596
|
}
|
|
1145
1597
|
attempt++;
|
|
@@ -1177,8 +1629,8 @@ export class PiAgentSession {
|
|
|
1177
1629
|
// 因为 messageHistory.push({role:'user'}) 在 promptStream 顶部已经做过, 重跑 runReActLoop 不会重复 push,
|
|
1178
1630
|
// 但 assistant 失败那条也别留 (留了会污染下一轮 LLM context).
|
|
1179
1631
|
// 简化: 重试前 pop 一次 assistant (如果最后一条是 assistant)
|
|
1180
|
-
if (this.messageHistory.length > 0 && this.messageHistory[this.messageHistory.length - 1].role === 'assistant') {
|
|
1181
|
-
this.
|
|
1632
|
+
if (this.actor.state.messageHistory.length > 0 && this.actor.state.messageHistory[this.actor.state.messageHistory.length - 1].role === 'assistant') {
|
|
1633
|
+
this.popHistory();
|
|
1182
1634
|
}
|
|
1183
1635
|
}
|
|
1184
1636
|
onStream({ type: 'done', content: '' });
|
|
@@ -1196,15 +1648,14 @@ export class PiAgentSession {
|
|
|
1196
1648
|
// Bootstrap Stop hook: fire-and-forget 写本次 session 摘要
|
|
1197
1649
|
const stopStartTime = this.promptStartTime || Date.now();
|
|
1198
1650
|
onStop({
|
|
1199
|
-
channelId: this.
|
|
1651
|
+
channelId: this.actor.state.channelId || 'unknown',
|
|
1200
1652
|
durationMs: Date.now() - stopStartTime,
|
|
1201
1653
|
usedJudgmentIds: [...this.judgmentGateUsedIds],
|
|
1202
1654
|
}).catch((err) => console.warn('[PiAgent] onStop failed:', err));
|
|
1203
1655
|
// 用完即清, 避免污染下一轮
|
|
1204
1656
|
this.clearJudgmentGate();
|
|
1205
|
-
this.
|
|
1657
|
+
this.runCtx = createRunContext(); // K2: 用完即清 (换新 Context, 不继承残留);
|
|
1206
1658
|
this.reportUsageToContextManager();
|
|
1207
|
-
this.currentSignal = null;
|
|
1208
1659
|
this.bootstrapAddition = '';
|
|
1209
1660
|
this.promptStartTime = 0;
|
|
1210
1661
|
// 2026-08-08: 轨迹收尾 — 落盘 + OrbitDB (失败静默, 不阻塞回复)
|
|
@@ -1212,7 +1663,7 @@ export class PiAgentSession {
|
|
|
1212
1663
|
return result;
|
|
1213
1664
|
}
|
|
1214
1665
|
async promptWithPivotLoop(input, config, channelId) {
|
|
1215
|
-
this.
|
|
1666
|
+
this.actor.state.channelId = channelId ?? this.actor.state.channelId;
|
|
1216
1667
|
if (!this.minimaxAvailable) {
|
|
1217
1668
|
const response = await this.handleFallback(input);
|
|
1218
1669
|
return {
|
|
@@ -1244,8 +1695,8 @@ export class PiAgentSession {
|
|
|
1244
1695
|
// M2.2 (2026-06-17): intent 分类 — pivot loop 也要拿到 hint
|
|
1245
1696
|
try {
|
|
1246
1697
|
const { classifyIntent, intentHint } = await import('./intent-classifier.js');
|
|
1247
|
-
this.
|
|
1248
|
-
this.currentIntentHint = intentHint(this.
|
|
1698
|
+
this.runCtx.intent = classifyIntent(input);
|
|
1699
|
+
this.currentIntentHint = intentHint(this.runCtx.intent);
|
|
1249
1700
|
this.currentUserInput = input;
|
|
1250
1701
|
}
|
|
1251
1702
|
catch (err) {
|
|
@@ -1268,6 +1719,10 @@ ${this.getToolDefinitions()}
|
|
|
1268
1719
|
|
|
1269
1720
|
${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
1270
1721
|
|
|
1722
|
+
${PROACTIVE_WORK_DISCIPLINE}
|
|
1723
|
+
|
|
1724
|
+
${await this.renderActivePlansSection()}
|
|
1725
|
+
|
|
1271
1726
|
工作模式:
|
|
1272
1727
|
1. 理解用户自然语言请求
|
|
1273
1728
|
2. 分析需要哪些工具来完成
|
|
@@ -1288,21 +1743,21 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1288
1743
|
- 工具调用前可以简短思考 (1-2 句话), 但**不要写长篇 thinking** (会撞 max_tokens)
|
|
1289
1744
|
- 工具调用后必须等结果, 不要在同一个回复里继续输出
|
|
1290
1745
|
- <final gen> 只在**真完成所有任务**时输出, 不要在工具调用前/中输出${this.judgmentGateAddition}${this.contextHintAddition}`;
|
|
1291
|
-
// 2026-06-15: 把
|
|
1746
|
+
// 2026-06-15: 把 runCtx.eventSink 传给 loop, 让 step-timeline 在 pivot 循环里也能 emit step_start/done
|
|
1292
1747
|
// 之前 loop.execute() 不接 streamCallback, 导致 step-timeline 只能看到老 runReActLoop 路径
|
|
1293
1748
|
// promptWithPivotLoop 路径 0 step events — UI 显示 timeline 但永远是空
|
|
1294
1749
|
// 2026-06-17: 透传 signal 让 abort 工作 — loop.execute() 当前不接 signal 参数,
|
|
1295
1750
|
// 所以 abort 行为通过 this.currentSignal 共享给 loop 内部读 (后续 M3.2 接 task plan 时一起加)
|
|
1296
1751
|
// 2026-07-06: pivot 内 token 累计到 ~70% 时回调 (workflow-pivot-loop.ts line 270+).
|
|
1297
|
-
// pivot 的 messageHistory 是 process-local, 不和 pi-sdk 的
|
|
1752
|
+
// pivot 的 messageHistory 是 process-local, 不和 pi-sdk 的 actor 的 messageHistory 同步.
|
|
1298
1753
|
// 真正折叠需要把 pi-sdk 历史灌回 pivot 的 history 数组 — 侵入较大.
|
|
1299
1754
|
// 当前 priority: 临时传空实现, 让 budget 公式放够 (workflow-pivot-loop.ts line 220)
|
|
1300
1755
|
// 不再撞预算. 这条路径留作技术债.
|
|
1301
1756
|
// 2026-07-17 Bug 1 修: 注入 messageHistory (hydrateMessageHistory 从 session JSON 回灌的) 到 system prompt
|
|
1302
|
-
// pivot loop execute() 内部自己维护 messageHistory, 跟 pi-sdk 的
|
|
1757
|
+
// pivot loop execute() 内部自己维护 messageHistory, 跟 pi-sdk 的 actor 的 messageHistory 隔离,
|
|
1303
1758
|
// 不注入的话 LLM 看不到历史对话, 每次都是新对话.
|
|
1304
1759
|
const historyLines = [];
|
|
1305
|
-
const historyToInject = this.messageHistory.slice(-20, -1);
|
|
1760
|
+
const historyToInject = this.actor.state.messageHistory.slice(-20, -1);
|
|
1306
1761
|
for (const m of historyToInject) {
|
|
1307
1762
|
const roleLabel = m.role === 'user' ? '用户' : m.role === 'assistant' ? '你' : m.role === 'tool' ? '工具结果' : m.role;
|
|
1308
1763
|
const text = (m.content || '').slice(0, 2000);
|
|
@@ -1315,9 +1770,9 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1315
1770
|
const onCompact = async () => {
|
|
1316
1771
|
// no-op (best-effort hook for future pi-sdk/pivot history sync)
|
|
1317
1772
|
};
|
|
1318
|
-
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);
|
|
1319
1774
|
if (result.response) {
|
|
1320
|
-
this.
|
|
1775
|
+
this.pushHistory({ role: 'assistant', content: result.response });
|
|
1321
1776
|
}
|
|
1322
1777
|
// 回溯 + 清场
|
|
1323
1778
|
if (this.judgmentGateUsedIds.length > 0) {
|
|
@@ -1333,7 +1788,9 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1333
1788
|
*/
|
|
1334
1789
|
_harness = null;
|
|
1335
1790
|
/** M2 绑定 GoalStore 后填真值; 在此之前为空 (事件里 goalId 字段已就位) */
|
|
1336
|
-
|
|
1791
|
+
// **K5 步骤③/⑧**: 会话的 Goal 默认绑定本体在 `actor.state.goalBinding` (访问器已删, 直接读本体)。
|
|
1792
|
+
// leo 口径不变: 这只是默认值; 每次执行开始必须把最终绑定写进 RunContext/Run 记录,
|
|
1793
|
+
// 运行中重绑必须走显式 Goal Binding 操作, 不许靠裸字段隐式生效。
|
|
1337
1794
|
/** 2026-09-16 (M2): 本次执行是"从 checkpoint 恢复"的 runId (非空 = 恢复模式, 不再新建 run) */
|
|
1338
1795
|
resumeRunId = '';
|
|
1339
1796
|
resumePlan = null;
|
|
@@ -1356,7 +1813,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1356
1813
|
}
|
|
1357
1814
|
/** CLI / Web 注入"当前目标" (有 goalId 就在该 Goal 下执行) */
|
|
1358
1815
|
setGoalId(goalId) {
|
|
1359
|
-
this.
|
|
1816
|
+
this.actor.state.goalBinding = String(goalId || '');
|
|
1360
1817
|
}
|
|
1361
1818
|
/** 核心状态迁移的兜底: 失败 → 记降级 + 交给循环顶部的持久化硬闸 (不吞错) */
|
|
1362
1819
|
async safeSetRunStatus(runId, to) {
|
|
@@ -1375,13 +1832,13 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1375
1832
|
* 这是"错误恢复闭环"的运行时接线点 (此前 recordRecovery / repeatedFailureCount 只是数据结构)。
|
|
1376
1833
|
*/
|
|
1377
1834
|
async wireToolFailure(tool, errorText, args) {
|
|
1378
|
-
if (!this.
|
|
1835
|
+
if (!this.actor.state.activeRun)
|
|
1379
1836
|
return;
|
|
1380
1837
|
const cls = classifyRunError(errorText);
|
|
1381
1838
|
const digest = argsDigestOf(args);
|
|
1382
1839
|
let repeats = 1;
|
|
1383
1840
|
try {
|
|
1384
|
-
const rec = await readRun(this.
|
|
1841
|
+
const rec = await readRun(this.actor.state.activeRun);
|
|
1385
1842
|
// recordStep 已经把**这次**失败记进去了, 所以"连续失败次数"直接就是当前值 (不再 +1)
|
|
1386
1843
|
const seen = rec ? repeatedFailureCount(rec, tool, digest) : 0;
|
|
1387
1844
|
repeats = Math.max(seen, 1);
|
|
@@ -1392,7 +1849,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1392
1849
|
: cls === 'external_no_reply' ? 'pause'
|
|
1393
1850
|
: 'retry';
|
|
1394
1851
|
try {
|
|
1395
|
-
await recordRecovery(this.
|
|
1852
|
+
await recordRecovery(this.actor.state.activeRun, {
|
|
1396
1853
|
errorClass: cls,
|
|
1397
1854
|
message: `${tool}: ${errorText}`.slice(0, 200),
|
|
1398
1855
|
action,
|
|
@@ -1401,20 +1858,20 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1401
1858
|
});
|
|
1402
1859
|
}
|
|
1403
1860
|
catch (err) {
|
|
1404
|
-
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(() => { });
|
|
1405
1862
|
}
|
|
1406
1863
|
// 外部无响应 → awaiting_external (不算失败; 等回话后回到 running)
|
|
1407
1864
|
if (cls === 'external_no_reply') {
|
|
1408
1865
|
this.awaitingExternal = true;
|
|
1409
|
-
await this.safeSetRunStatus(this.
|
|
1866
|
+
await this.safeSetRunStatus(this.actor.state.activeRun, 'awaiting_external');
|
|
1410
1867
|
// 2026-09-16 (2-C.4): 把"在等什么"写成持久化事实 (来源/关联/过期), 否则真实回包到了也不知道该唤醒谁。
|
|
1411
1868
|
try {
|
|
1412
|
-
if (this.
|
|
1869
|
+
if (this.actor.state.goalBinding) {
|
|
1413
1870
|
const { bindExternalWait, newContinuationId, defaultWaitExpiry } = await import('./external-events.js');
|
|
1414
1871
|
const isDelegate = /delegate/i.test(String(tool || ''));
|
|
1415
|
-
await bindExternalWait(this.
|
|
1416
|
-
requestId: this.pendingExternalRequestId || `${this.
|
|
1417
|
-
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),
|
|
1418
1875
|
expectedSource: isDelegate ? 'delegate' : 'p2p',
|
|
1419
1876
|
expectedEvent: 'result',
|
|
1420
1877
|
createdAt: new Date().toISOString(),
|
|
@@ -1424,31 +1881,31 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1424
1881
|
}
|
|
1425
1882
|
}
|
|
1426
1883
|
catch (e) {
|
|
1427
|
-
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(() => { });
|
|
1428
1885
|
}
|
|
1429
1886
|
return;
|
|
1430
1887
|
}
|
|
1431
1888
|
// 鉴权类: 不重试, 直接交人
|
|
1432
1889
|
if (cls === 'auth') {
|
|
1433
1890
|
this.breakerReason = `鉴权类错误不重试 (${tool}): ${errorText.slice(0, 120)}`;
|
|
1434
|
-
await this.safeSetRunStatus(this.
|
|
1891
|
+
await this.safeSetRunStatus(this.actor.state.activeRun, 'needs_human');
|
|
1435
1892
|
return;
|
|
1436
1893
|
}
|
|
1437
1894
|
// 重复失败熔断: 同一工具 + 同一组参数连续失败达 3 次
|
|
1438
1895
|
if (repeats >= MAX_SAME_TOOL_FAILURES) {
|
|
1439
1896
|
this.breakerReason = `同一工具 ${tool} 连续失败 ${repeats} 次 (${cls}) → 熔断, 不再重试`;
|
|
1440
|
-
await this.safeSetRunStatus(this.
|
|
1897
|
+
await this.safeSetRunStatus(this.actor.state.activeRun, 'needs_human');
|
|
1441
1898
|
}
|
|
1442
1899
|
}
|
|
1443
1900
|
getRunId() {
|
|
1444
|
-
return this.
|
|
1901
|
+
return this.actor.state.activeRun;
|
|
1445
1902
|
}
|
|
1446
1903
|
/**
|
|
1447
1904
|
* 2026-09-16 (M2-B): **上一次**运行的 runId —— prompt 收尾时 `currentRunId` 会被清空,
|
|
1448
1905
|
* 而 Supervisor / 控制面要在运行结束后才知道"刚才跑的是哪条 run" (否则会拿旧 run 做决策)。
|
|
1449
1906
|
*/
|
|
1450
1907
|
getLastRunId() {
|
|
1451
|
-
return this.lastRunId || this.
|
|
1908
|
+
return this.lastRunId || this.actor.state.activeRun;
|
|
1452
1909
|
}
|
|
1453
1910
|
/**
|
|
1454
1911
|
* 2026-09-16 (M2): 从 checkpoint 恢复一次运行 —— **不是**重发原 prompt。
|
|
@@ -1495,7 +1952,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1495
1952
|
}
|
|
1496
1953
|
this.resumeRunId = runId;
|
|
1497
1954
|
this.resumePlan = prep.plan;
|
|
1498
|
-
this.
|
|
1955
|
+
this.actor.state.goalBinding = prep.plan.goalId || this.actor.state.goalBinding;
|
|
1499
1956
|
try {
|
|
1500
1957
|
const reply = await this.prompt(buildResumeInstruction(prep.plan), {});
|
|
1501
1958
|
return { ok: true, reply, ...(modelDrift ? { modelDrift } : {}), ...(modelApplied ? { modelApplied } : {}) };
|
|
@@ -1514,8 +1971,8 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1514
1971
|
// pre-tool-validator 4 步链 (modeGate/blacklist/shell-guard/schema), 经 human-value-pipeline 包装
|
|
1515
1972
|
preToolUse: async (o) => onPreToolUse({ tool: o.tool, args: o.args, permissionMode: o.permissionMode }),
|
|
1516
1973
|
// 事件写 Run: 观测级 (记账失败不改变已做出的决策)
|
|
1517
|
-
events: (e) => { if (this.
|
|
1518
|
-
void recordHarnessEvent(this.
|
|
1974
|
+
events: (e) => { if (this.actor.state.activeRun)
|
|
1975
|
+
void recordHarnessEvent(this.actor.state.activeRun, e); },
|
|
1519
1976
|
});
|
|
1520
1977
|
}
|
|
1521
1978
|
return this._harness;
|
|
@@ -1523,10 +1980,10 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1523
1980
|
/** 每个生命周期事件都带上的运行身份 (runId / goalId / agentId / channelId / surface) */
|
|
1524
1981
|
harnessCtx() {
|
|
1525
1982
|
return {
|
|
1526
|
-
runId: this.
|
|
1527
|
-
goalId: this.
|
|
1528
|
-
agentId: this.
|
|
1529
|
-
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,
|
|
1530
1987
|
surface: this.runSurface,
|
|
1531
1988
|
};
|
|
1532
1989
|
}
|
|
@@ -1552,6 +2009,9 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1552
2009
|
const MAX_TOOL_CALLS_PER_LOOP = 25; // 单轮循环总工具调用上限 → 注入 hint
|
|
1553
2010
|
let totalToolCallsThisLoop = 0;
|
|
1554
2011
|
const lastNTools = []; // 最近 MAX_IDEMPOTENT_TOOL 次工具名, 检测重复
|
|
2012
|
+
// 2026-10-01: 工具停滞观测状态 (分类 + 温和引导 + 重复结果引用) —— 见 agents/tool-loop-guard.ts
|
|
2013
|
+
// 与上面的"同工具 5 次就提示"不同: 它按 (工具名 + 参数 + 结果) 签名判定, 并抓 A→B→A→B 的循环
|
|
2014
|
+
const stallState = new LoopStallState();
|
|
1555
2015
|
// 2026-08-10: unreported 循环逃生门 — LLM 反复不把工具结果写进回复时, 3 次后强制 final (不死板)
|
|
1556
2016
|
const MAX_UNREPORTED_RETRIES = 3;
|
|
1557
2017
|
let unreportedRetries = 0;
|
|
@@ -1590,26 +2050,26 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1590
2050
|
this.awaitingExternal = false;
|
|
1591
2051
|
if (this.resumeRunId) {
|
|
1592
2052
|
// ── 恢复模式: 复用原来的 runId, 不新建 run (历史保留) ──
|
|
1593
|
-
this.
|
|
2053
|
+
this.actor.state.activeRun = this.resumeRunId;
|
|
1594
2054
|
this.lastRunId = this.resumeRunId;
|
|
1595
2055
|
try {
|
|
1596
|
-
await markRunRunning(this.
|
|
2056
|
+
await markRunRunning(this.actor.state.activeRun);
|
|
1597
2057
|
}
|
|
1598
2058
|
catch (err) {
|
|
1599
2059
|
runPersistenceFailure = `恢复时状态迁移失败 (recovering → running): ${String(err?.message || err).slice(0, 180)}`;
|
|
1600
2060
|
}
|
|
1601
2061
|
const doneN = this.resumePlan?.completedSteps.length ?? 0;
|
|
1602
2062
|
const guards = this.resumePlan?.replayGuards.length ?? 0;
|
|
1603
|
-
onStream?.({ type: 'status', internal: true, content: `♻️ 从 checkpoint 恢复运行 ${this.
|
|
2063
|
+
onStream?.({ type: 'status', internal: true, content: `♻️ 从 checkpoint 恢复运行 ${this.actor.state.activeRun} (已完成 ${doneN} 步, 非幂等重放守卫 ${guards} 条)`, tool: 'harness' });
|
|
1604
2064
|
}
|
|
1605
2065
|
else {
|
|
1606
2066
|
// 2026-09-16 (M2): 目标绑定 —— 有 goalId 就在该 Goal 下执行; 没有就建 Goal 再建 Run。
|
|
1607
2067
|
// 延续规则 (确定性, 不靠猜): 该 channel/agent 上已有 open/active Goal, 且它的上一次执行**没收尾**
|
|
1608
2068
|
// (interrupted/stalled/needs_human/paused/awaiting_external/recovering) → 继续该 Goal; 否则新建。
|
|
1609
|
-
let boundGoalId = this.
|
|
2069
|
+
let boundGoalId = this.actor.state.goalBinding;
|
|
1610
2070
|
if (!boundGoalId) {
|
|
1611
2071
|
try {
|
|
1612
|
-
const active = await findActiveGoal({ channelId: this.
|
|
2072
|
+
const active = await findActiveGoal({ channelId: this.actor.state.channelId || undefined, agentId: this.actor.state.agentId || undefined });
|
|
1613
2073
|
if (active?.currentRunId) {
|
|
1614
2074
|
const prev = await readRun(active.currentRunId);
|
|
1615
2075
|
const unfinished = prev && ['interrupted', 'stalled', 'needs_human', 'paused', 'awaiting_external', 'recovering'].includes(prev.status);
|
|
@@ -1624,8 +2084,8 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1624
2084
|
try {
|
|
1625
2085
|
const g = await createGoal({
|
|
1626
2086
|
objective: this.currentUserInput || '(未记录目标)',
|
|
1627
|
-
channelId: this.
|
|
1628
|
-
agentId: this.
|
|
2087
|
+
channelId: this.actor.state.channelId || undefined,
|
|
2088
|
+
agentId: this.actor.state.agentId || undefined,
|
|
1629
2089
|
createdBy: this.runSurface,
|
|
1630
2090
|
});
|
|
1631
2091
|
boundGoalId = g.goalId;
|
|
@@ -1634,25 +2094,25 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1634
2094
|
console.warn('[PiAgent] 创建 Goal 失败 (无目标也要有运行记录):', err?.message);
|
|
1635
2095
|
}
|
|
1636
2096
|
}
|
|
1637
|
-
this.
|
|
2097
|
+
this.actor.state.goalBinding = boundGoalId;
|
|
1638
2098
|
}
|
|
1639
2099
|
try {
|
|
1640
2100
|
const rec = await startRun({
|
|
1641
2101
|
surface: this.runSurface,
|
|
1642
2102
|
goal: this.currentUserInput || '(未记录目标)',
|
|
1643
2103
|
goalId: boundGoalId || undefined,
|
|
1644
|
-
channelId: this.
|
|
1645
|
-
agentId: this.
|
|
2104
|
+
channelId: this.actor.state.channelId || undefined,
|
|
2105
|
+
agentId: this.actor.state.agentId || undefined,
|
|
1646
2106
|
modelConfig: await this.runModelSnapshot(),
|
|
1647
2107
|
});
|
|
1648
|
-
this.
|
|
2108
|
+
this.actor.state.activeRun = rec.runId;
|
|
1649
2109
|
this.lastRunId = rec.runId;
|
|
1650
|
-
this.
|
|
1651
|
-
if (this.
|
|
2110
|
+
this.actor.state.goalBinding = rec.goalId || this.actor.state.goalBinding;
|
|
2111
|
+
if (this.actor.state.goalBinding) {
|
|
1652
2112
|
// Run → Goal 反查链: runId → goalId → objective / success criteria
|
|
1653
|
-
await attachRun(this.
|
|
2113
|
+
await attachRun(this.actor.state.goalBinding, rec.runId).catch((err) => console.warn('[PiAgent] attachRun 失败:', err?.message));
|
|
1654
2114
|
}
|
|
1655
|
-
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' });
|
|
1656
2116
|
}
|
|
1657
2117
|
catch (err) {
|
|
1658
2118
|
// 核心写失败: 不再 warn 后继续 —— 没有运行记录就不执行 (strict 模式默认如此)
|
|
@@ -1682,18 +2142,18 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1682
2142
|
break;
|
|
1683
2143
|
}
|
|
1684
2144
|
// 2026-09-16: 预算闸门 (持久化 harness 的约束面) —— 到点必须**如实**终止, 不许静默算完成
|
|
1685
|
-
if (this.
|
|
2145
|
+
if (this.actor.state.activeRun) {
|
|
1686
2146
|
try {
|
|
1687
|
-
const rec = await readRun(this.
|
|
2147
|
+
const rec = await readRun(this.actor.state.activeRun);
|
|
1688
2148
|
if (!rec)
|
|
1689
|
-
throw new Error(`运行记录读不到: ${this.
|
|
2149
|
+
throw new Error(`运行记录读不到: ${this.actor.state.activeRun}`);
|
|
1690
2150
|
// 2026-09-16 (M5): 外部控制面 (CLI /pause /abort, Web API) 改过状态 → 如实停在那儿。
|
|
1691
2151
|
// 不覆盖成 done/failed: 人按下暂停就是暂停, 人按下中止就是中止。
|
|
1692
2152
|
if (rec.status === 'paused' || rec.status === 'aborted') {
|
|
1693
2153
|
runExternallyPaused = rec.status === 'paused';
|
|
1694
2154
|
runExternallyAborted = rec.status === 'aborted';
|
|
1695
2155
|
runStopReason = `外部请求: ${rec.status}`;
|
|
1696
|
-
onStream?.({ type: 'error', content: `⏹️ 运行被外部${rec.status === 'paused' ? '暂停' : '中止'} (run=${this.
|
|
2156
|
+
onStream?.({ type: 'error', content: `⏹️ 运行被外部${rec.status === 'paused' ? '暂停' : '中止'} (run=${this.actor.state.activeRun})`, tool: 'harness' });
|
|
1697
2157
|
finalResponse = finalResponse || `(运行已${rec.status === 'paused' ? '暂停' : '中止'})`;
|
|
1698
2158
|
break;
|
|
1699
2159
|
}
|
|
@@ -1716,7 +2176,18 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1716
2176
|
}
|
|
1717
2177
|
// 停止条件 1: max turns (fail-safe 10000, 正常任务永远跑不到)
|
|
1718
2178
|
// 2026-07-01 (v0.2.4 子任务 1): 委托给 react-loop.decideMaxIterations 纯函数
|
|
1719
|
-
|
|
2179
|
+
// 2026-10-01 (落实②: 可退还的迭代预算): 惩罚零碎调用, **奖励批处理** ——
|
|
2180
|
+
// 程序化工具(execute_code, 一次能顶多次)调用后归还一次迭代 ⇒ 有效寿命被延长。
|
|
2181
|
+
// 退出条件仍由 decideMaxIterations 决定(读**净**用量), 不做硬刹车。
|
|
2182
|
+
if (iteration === 0 || !this.iterBudget)
|
|
2183
|
+
this.iterBudget = new IterationBudget(this.MAX_REACT_ITERATIONS);
|
|
2184
|
+
const iterBudget = this.iterBudget;
|
|
2185
|
+
iterBudget.consume();
|
|
2186
|
+
if (iterBudget.warn(0.8) && !this.iterBudgetWarned) {
|
|
2187
|
+
this.iterBudgetWarned = true;
|
|
2188
|
+
onStream?.({ type: 'status', internal: true, content: `⏳ 迭代预算已用 ${iterBudget.describe()} (批量工具会退还)` });
|
|
2189
|
+
}
|
|
2190
|
+
const maxIterDecision = decideMaxIterations(iterBudget.used, iterBudget.maxTotal);
|
|
1720
2191
|
if (maxIterDecision.shouldExit) {
|
|
1721
2192
|
console.warn(`[PiAgent] 达到最大循环数 ${this.MAX_REACT_ITERATIONS}, 强制终止 (fail-safe)`);
|
|
1722
2193
|
onStream?.({ type: 'error', content: `⏹️ 达到最大循环数 (${this.MAX_REACT_ITERATIONS}, fail-safe)`, tool: 'loop' });
|
|
@@ -1734,14 +2205,14 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1734
2205
|
if (totalToolCallsThisLoop >= MAX_TOOL_CALLS_PER_LOOP) {
|
|
1735
2206
|
console.warn(`[PiAgent] 单轮工具调用已达 ${MAX_TOOL_CALLS_PER_LOOP}, 注入 hint 让 LLM 总结`);
|
|
1736
2207
|
onStream?.({ type: 'error', content: `⏹️ 工具调用已达上限 (${MAX_TOOL_CALLS_PER_LOOP}), 请基于已有结果回答`, tool: 'loop' });
|
|
1737
|
-
this.
|
|
2208
|
+
this.pushHistory({ role: 'system', content: `[注意] 你已连续调用 ${MAX_TOOL_CALLS_PER_LOOP} 次工具。请基于已有结果直接回答用户, 不要再次调用任何工具。在回答末尾加 <final gen> 标记结束。` });
|
|
1738
2209
|
totalToolCallsThisLoop = 0; // 重置计数器, 只防连续死循环
|
|
1739
2210
|
}
|
|
1740
2211
|
if (lastNTools.length >= MAX_IDEMPOTENT_TOOL && new Set(lastNTools).size === 1) {
|
|
1741
2212
|
const repeatedTool = lastNTools[0];
|
|
1742
2213
|
console.warn(`[PiAgent] 同工具 ${repeatedTool} 连续成功调 ${MAX_IDEMPOTENT_TOOL} 次, 注入 hint 让 LLM 总结`);
|
|
1743
2214
|
onStream?.({ type: 'error', content: `⏹️ 工具 ${repeatedTool} 重复调用 ${MAX_IDEMPOTENT_TOOL} 次, 请基于已有结果回答`, tool: 'loop' });
|
|
1744
|
-
this.
|
|
2215
|
+
this.pushHistory({ role: 'system', content: `[注意] 你已连续 ${MAX_IDEMPOTENT_TOOL} 次调用 ${repeatedTool}。请基于已有结果直接回答用户, 不要再次调用任何工具。在回答末尾加 <final gen> 标记结束。` });
|
|
1745
2216
|
lastNTools.length = 0; // 重置计数器
|
|
1746
2217
|
// 不 break — 让 LLM 在下一轮用已有信息回答
|
|
1747
2218
|
}
|
|
@@ -1845,6 +2316,10 @@ ${toolDefs}
|
|
|
1845
2316
|
|
|
1846
2317
|
${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
1847
2318
|
|
|
2319
|
+
${PROACTIVE_WORK_DISCIPLINE}
|
|
2320
|
+
|
|
2321
|
+
${await this.renderActivePlansSection()}
|
|
2322
|
+
|
|
1848
2323
|
工作模式:
|
|
1849
2324
|
1. 理解用户自然语言请求
|
|
1850
2325
|
2. 分析需要哪些工具来完成
|
|
@@ -1940,7 +2415,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1940
2415
|
break;
|
|
1941
2416
|
}
|
|
1942
2417
|
// 把错误当成 tool 结果 push 进 history, 这样下一轮 LLM 看到错误能调整
|
|
1943
|
-
this.
|
|
2418
|
+
this.pushHistory({
|
|
1944
2419
|
role: 'system',
|
|
1945
2420
|
content: `[Loop 错误恢复 ${totalErrors}/${this.MAX_TOTAL_ERRORS}] ${aiFailureReason}\n\n请基于上轮工具结果继续完成任务, 不要重复调用同一失败操作. 如果工具已成功执行, 请基于 result.output 给用户总结; 如果工具失败, 请换其他方式或重试.`
|
|
1946
2421
|
});
|
|
@@ -2013,7 +2488,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2013
2488
|
}
|
|
2014
2489
|
if (toolCalls.length > 0) {
|
|
2015
2490
|
// 把原始 LLM 回复 push 进 history (仅一次)
|
|
2016
|
-
this.
|
|
2491
|
+
this.pushHistory({
|
|
2017
2492
|
role: 'assistant',
|
|
2018
2493
|
content: reply,
|
|
2019
2494
|
toolCalls: toolCalls.length > 1 ? toolCalls : [toolCalls[0]],
|
|
@@ -2043,12 +2518,12 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2043
2518
|
consecutiveErrors++;
|
|
2044
2519
|
totalErrors++;
|
|
2045
2520
|
const errorResult = { success: false, error: `未知工具: ${toolCall.name}` };
|
|
2046
|
-
this.
|
|
2521
|
+
this.pushHistory({ role: 'tool', content: JSON.stringify(errorResult), toolResult: errorResult });
|
|
2047
2522
|
this.logToHarness(toolCall.name, toolCall.args, errorResult);
|
|
2048
2523
|
// 2026-07-28: 注入 Reflection 帮助 LLM 理解错误
|
|
2049
2524
|
const obs = buildObservation(toolCall.name, toolCall.args, errorResult);
|
|
2050
2525
|
const ref = buildReflection(toolCall.name, errorResult.error, totalErrors, lastFailedToolCount);
|
|
2051
|
-
this.
|
|
2526
|
+
this.pushHistory({ role: 'system', content: formatObservationWithReflection(obs, ref) });
|
|
2052
2527
|
if (onStream)
|
|
2053
2528
|
onStream({ type: 'status', content: `💡 Reflection: ${obs.summary}`, tool: 'system' });
|
|
2054
2529
|
console.warn(`[PiAgent] 未知工具: ${toolCall.name} (累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}),跳过并继续`);
|
|
@@ -2082,7 +2557,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2082
2557
|
consecutiveErrors++;
|
|
2083
2558
|
totalErrors++;
|
|
2084
2559
|
const denyResultMsg = { success: false, error: `拒绝: [${toolDecision.rejectedBy || 'deny-pipeline'}] ${toolDecision.reason}` };
|
|
2085
|
-
this.
|
|
2560
|
+
this.pushHistory({ role: 'tool', content: JSON.stringify(denyResultMsg), toolResult: denyResultMsg });
|
|
2086
2561
|
this.logToHarness(toolCall.name, toolCall.args, denyResultMsg);
|
|
2087
2562
|
return;
|
|
2088
2563
|
}
|
|
@@ -2094,7 +2569,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2094
2569
|
? `Harness gate 拒绝 (${toolDecision.rejectedBy}): ${toolDecision.reason || '未通过安全校验'}`
|
|
2095
2570
|
: `PreToolUse 拒绝: ${toolDecision.reason || '未通过安全校验'}`,
|
|
2096
2571
|
};
|
|
2097
|
-
this.
|
|
2572
|
+
this.pushHistory({ role: 'tool', content: JSON.stringify(deniedResult), toolResult: deniedResult });
|
|
2098
2573
|
this.logToHarness(toolCall.name, toolCall.args, deniedResult);
|
|
2099
2574
|
if (onStream) {
|
|
2100
2575
|
const gateName = isGateDeny ? `Harness ${toolDecision.rejectedBy}` : (isHarnessError ? '核心约束层' : 'PreToolUse');
|
|
@@ -2123,13 +2598,13 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2123
2598
|
? `[注意] 连续 ${consecutiveErrors} 次工具调用因约束层失效被阻止. 请换其他工具或直接回答用户, 末尾加 <final gen>.`
|
|
2124
2599
|
: `[注意] 连续 ${consecutiveErrors} 次工具调用被系统拒绝. 请换其他工具或直接回答用户, 末尾加 <final gen>.`;
|
|
2125
2600
|
if (lastFailedToolCount >= MAX_SAME_TOOL_FAILURES) {
|
|
2126
|
-
this.
|
|
2601
|
+
this.pushHistory({ role: 'system', content: systemMaxMsg });
|
|
2127
2602
|
lastFailedTool = '';
|
|
2128
2603
|
lastFailedToolCount = 0;
|
|
2129
2604
|
consecutiveErrors = 0;
|
|
2130
2605
|
}
|
|
2131
2606
|
else if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
2132
|
-
this.
|
|
2607
|
+
this.pushHistory({ role: 'system', content: systemConsecMsg });
|
|
2133
2608
|
consecutiveErrors = 0;
|
|
2134
2609
|
}
|
|
2135
2610
|
return;
|
|
@@ -2155,7 +2630,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2155
2630
|
}
|
|
2156
2631
|
let result = replaySkip
|
|
2157
2632
|
? { success: true, output: `[恢复保护] ${toolCall.name} 在中断前已成功执行过, 本次不重复执行 (避免重复副作用)。当时结果: ${replaySkip}`, _replaySkipped: true }
|
|
2158
|
-
: await tool.execute(toolCall.args);
|
|
2633
|
+
: await (toolCall.__t0 = Date.now(), tool.execute(expandHomeArgs(toolCall.args)));
|
|
2159
2634
|
const toolDurationMs = Date.now() - toolStart;
|
|
2160
2635
|
if (replaySkip) {
|
|
2161
2636
|
console.log(`[PiAgent] 恢复重放守卫: 跳过已完成的非幂等工具 ${toolCall.name}`);
|
|
@@ -2163,18 +2638,18 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2163
2638
|
}
|
|
2164
2639
|
console.log(`[PiAgent] 工具 ${toolCall.name} 执行完成: success=${result.success} (${toolDurationMs}ms)`);
|
|
2165
2640
|
// 2026-09-16 (M3): 失败接线 —— 分类 + recovery 留痕 + 熔断 / 外部等待
|
|
2166
|
-
if (!result.success && this.
|
|
2641
|
+
if (!result.success && this.actor.state.activeRun && !replaySkip) {
|
|
2167
2642
|
await this.wireToolFailure(toolCall.name, String(result.error || ''), toolCall.args);
|
|
2168
2643
|
}
|
|
2169
|
-
else if (result.success && this.
|
|
2644
|
+
else if (result.success && this.actor.state.activeRun && this.awaitingExternal) {
|
|
2170
2645
|
// 外部回话了: awaiting_external → running (不是"恢复完成", 只是等待结束)
|
|
2171
2646
|
this.awaitingExternal = false;
|
|
2172
|
-
await this.safeSetRunStatus(this.
|
|
2647
|
+
await this.safeSetRunStatus(this.actor.state.activeRun, 'running');
|
|
2173
2648
|
}
|
|
2174
2649
|
// 2026-09-16: 持久化 run harness — 每步工具调用立即落盘 (崩在这里也能看到做到哪步)
|
|
2175
|
-
if (this.
|
|
2650
|
+
if (this.actor.state.activeRun) {
|
|
2176
2651
|
try {
|
|
2177
|
-
await recordStep(this.
|
|
2652
|
+
await recordStep(this.actor.state.activeRun, {
|
|
2178
2653
|
tool: toolCall.name,
|
|
2179
2654
|
ok: !!result.success,
|
|
2180
2655
|
ms: toolDurationMs,
|
|
@@ -2205,7 +2680,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2205
2680
|
ok: !!result.success,
|
|
2206
2681
|
});
|
|
2207
2682
|
if (after.routeHint?.systemAddition) {
|
|
2208
|
-
this.
|
|
2683
|
+
this.pushHistory({ role: 'system', content: `[Harness Router Hint: ${after.routeHint.reason}]\n${after.routeHint.systemAddition}` });
|
|
2209
2684
|
}
|
|
2210
2685
|
if (after.outputBlocked) {
|
|
2211
2686
|
if (onStream) {
|
|
@@ -2214,7 +2689,39 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2214
2689
|
console.warn(`[PiAgent] Harness output denied ${toolCall.name}: ${after.outputBlocked.reason}`);
|
|
2215
2690
|
result = { ...result, output: `[harness output gate: 输出含敏感内容, 已屏蔽. 原因: ${after.outputBlocked.reason}]`, _harnessDenied: true };
|
|
2216
2691
|
}
|
|
2217
|
-
|
|
2692
|
+
// 2026-10-01: 落库前做一次停滞观测 —— 命中的是"引导/引用", 不是"拒绝执行":
|
|
2693
|
+
// 完全相同的返回从第 2 次起折叠成引用 (省上下文); 连续 3 次同参数同结果或检测到循环 ⇒
|
|
2694
|
+
// 在结果尾部追加一条系统提示 (保留模型的选择权, 不硬停 —— 硬停那条已被用户否决)。
|
|
2695
|
+
try {
|
|
2696
|
+
const obs = observeToolCall(stallState, {
|
|
2697
|
+
toolName: toolCall.name,
|
|
2698
|
+
sameSignatureBefore: this.lastToolSig === argsFingerprint(toolCall.args),
|
|
2699
|
+
args: toolCall.args,
|
|
2700
|
+
resultText: String(result.output || (result.success ? '' : String(result.error || ''))),
|
|
2701
|
+
ok: !!result.success,
|
|
2702
|
+
seenResultBefore: stallState.hasSeenResult(String(result.output || '')),
|
|
2703
|
+
});
|
|
2704
|
+
if (obs.stub) {
|
|
2705
|
+
result = { ...result, output: obs.stub, _stubbed: true };
|
|
2706
|
+
}
|
|
2707
|
+
else if (obs.notice) {
|
|
2708
|
+
result = { ...result, output: `${String(result.output || '')}\n\n${obs.notice}` };
|
|
2709
|
+
}
|
|
2710
|
+
if (obs.action === 'warn') {
|
|
2711
|
+
console.warn(`[PiAgent] 工具停滞引导 (${obs.code}, 第 ${obs.count} 次): ${toolCall.name}`);
|
|
2712
|
+
onStream?.({ type: 'status', internal: true, content: `🩺 检测到重复调用 ${toolCall.name} (${obs.code}), 已提示模型换法`, tool: 'loop' });
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
catch { /* 观测失败绝不影响主路径 */ }
|
|
2716
|
+
this.pushHistory({ role: 'tool', content: JSON.stringify(result), toolResult: result, toolCallId: toolCall.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` });
|
|
2717
|
+
// 2026-10-01 (用户: 「为什么这么慢」): 量到的事实 —— 每次 LLM 往返平均 3338ms,
|
|
2718
|
+
// 而"一轮发多个工具"(toolCalls>1) 只占 12/63 ✗ ⇒ 回合时长 ≈ 往返次数 × 3.3 秒 ✓。
|
|
2719
|
+
// 本回合**第一次**且**只发一个**工具时, 附一句带代价的提醒(不是口号 ✓), 只提一次免得刷屏。
|
|
2720
|
+
if (toolCalls.length === 1 && this.successfulToolResults.length <= 1) { // 本回合头一次(免得每轮刷屏)
|
|
2721
|
+
const __batchHint = batchHint(toolCall.name, 1);
|
|
2722
|
+
if (__batchHint)
|
|
2723
|
+
this.pushHistory({ role: 'system', content: __batchHint.trim() });
|
|
2724
|
+
}
|
|
2218
2725
|
this.logToHarness(toolCall.name, toolCall.args, result);
|
|
2219
2726
|
// 2026-08-09: 记录到本轮行动日志 (循环进度 + final 前目标核查用)
|
|
2220
2727
|
// 去重: 同一工具同 args 连续成功只记一次 (防 LLM 重复 react 刷屏)
|
|
@@ -2243,10 +2750,48 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2243
2750
|
onStream({ type: 'step_error', content: `${toolCall.name} 执行失败`, tool: toolCall.name, error: result.error });
|
|
2244
2751
|
}
|
|
2245
2752
|
}
|
|
2753
|
+
// 2026-10-01 (优化 #1/#3/#4): 工具结果的**进上下文闸** + **遥测** + **写操作读回自证**
|
|
2754
|
+
{
|
|
2755
|
+
const __ms = Date.now() - Number(toolCall.__t0 || Date.now());
|
|
2756
|
+
try {
|
|
2757
|
+
// #1 结果闸: 超上限 ⇒ 头尾 + 完整结果落文件(给路径) ⇒ 上下文不再被一条大输出长期占住
|
|
2758
|
+
const capped = capToolResult(String(result.output ?? ''), { tool: toolCall.name });
|
|
2759
|
+
if (capped.capped) {
|
|
2760
|
+
result.output = capped.text;
|
|
2761
|
+
console.warn(`[PiAgent] ${toolCall.name} 结果过长(${capped.originalChars})已截断` + (capped.spilledTo ? `, 完整内容: ${capped.spilledTo}` : ''));
|
|
2762
|
+
}
|
|
2763
|
+
// #4 写操作"读回自证": 写类工具成功后自动核一次(存在? 大小?) ⇒ "工具说成功≠任务成功"从规矩变成机制
|
|
2764
|
+
// 2026-10-01: 改了 TS 源码就记账 ⇒ 回合收尾自动跑类型检查 (把"靠自觉"变成机制)
|
|
2765
|
+
{
|
|
2766
|
+
const tsFile = codeWriteTarget(toolCall.name, toolCall.args);
|
|
2767
|
+
if (tsFile && !this.tsTouchedThisTurn.includes(tsFile))
|
|
2768
|
+
this.tsTouchedThisTurn.push(tsFile);
|
|
2769
|
+
}
|
|
2770
|
+
const verified = verifyWriteOutcome(toolCall.name, toolCall.args, this.cwd);
|
|
2771
|
+
if (verified)
|
|
2772
|
+
result.output = `${String(result.output ?? '')}\n${verified}`;
|
|
2773
|
+
// #3 遥测: 记一行(工具/指纹/耗时/成败/结果大小/是否与上一次同签名) ⇒ 重复率可算
|
|
2774
|
+
recordToolCall({
|
|
2775
|
+
tool: toolCall.name,
|
|
2776
|
+
sig: argsFingerprint(toolCall.args),
|
|
2777
|
+
ms: __ms,
|
|
2778
|
+
ok: true,
|
|
2779
|
+
resultChars: String(result.output ?? '').length,
|
|
2780
|
+
prevSig: this.lastToolSig ?? null,
|
|
2781
|
+
});
|
|
2782
|
+
this.lastToolSig = argsFingerprint(toolCall.args);
|
|
2783
|
+
}
|
|
2784
|
+
catch { /* 任何优化项失败都不影响工具结果本身 */ }
|
|
2785
|
+
}
|
|
2246
2786
|
if (result.success) {
|
|
2247
2787
|
consecutiveErrors = 0;
|
|
2248
2788
|
// 2026-07-29: Hermes 风格硬限制计数
|
|
2249
2789
|
totalToolCallsThisLoop++;
|
|
2790
|
+
// 2026-10-01: 批处理工具(一次顶多次) ⇒ **退还**这次迭代 (奖励批处理, 压零碎调用)
|
|
2791
|
+
if (this.iterBudget && isRefundableTool(toolCall.name)) {
|
|
2792
|
+
this.iterBudget.refund();
|
|
2793
|
+
console.warn(`[PiAgent] ${toolCall.name} 是批量工具 ⇒ 退还 1 次迭代 (现 ${this.iterBudget.describe()})`);
|
|
2794
|
+
}
|
|
2250
2795
|
lastNTools.push(toolCall.name);
|
|
2251
2796
|
if (lastNTools.length > MAX_IDEMPOTENT_TOOL)
|
|
2252
2797
|
lastNTools.shift();
|
|
@@ -2267,6 +2812,14 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2267
2812
|
}
|
|
2268
2813
|
else {
|
|
2269
2814
|
consecutiveErrors++;
|
|
2815
|
+
// 2026-10-01 (优化 #3): 失败也记一行(带错误类别) ⇒ "错工具率"从这类错误里看得出来
|
|
2816
|
+
try {
|
|
2817
|
+
const __cls = classifyError(String(result.error || '')).label || '未分类';
|
|
2818
|
+
recordToolCall({ tool: toolCall.name, sig: argsFingerprint(toolCall.args), ms: 0,
|
|
2819
|
+
ok: false, resultChars: 0, prevSig: this.lastToolSig ?? null, errorClass: __cls });
|
|
2820
|
+
this.lastToolSig = argsFingerprint(toolCall.args);
|
|
2821
|
+
}
|
|
2822
|
+
catch { /* 遥测失败不影响 */ }
|
|
2270
2823
|
totalErrors++;
|
|
2271
2824
|
if (toolCall.name === lastFailedTool) {
|
|
2272
2825
|
lastFailedToolCount++;
|
|
@@ -2279,18 +2832,18 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2279
2832
|
// 2026-07-28: 注入 Observation + Reflection 替代旧 hardcode 提示
|
|
2280
2833
|
const obs = buildObservation(toolCall.name, toolCall.args, { success: false, error: result.error });
|
|
2281
2834
|
const ref = buildReflection(toolCall.name, result.error, totalErrors, lastFailedToolCount);
|
|
2282
|
-
this.
|
|
2835
|
+
this.pushHistory({ role: 'system', content: formatObservationWithReflection(obs, ref) + SHELL_ESCAPE_HINT });
|
|
2283
2836
|
if (onStream)
|
|
2284
2837
|
onStream({ type: 'status', content: `💡 Reflection: ${obs.summary} → ${ref[0]?.action || '放弃'}`, tool: 'system' });
|
|
2285
2838
|
if (lastFailedToolCount >= MAX_SAME_TOOL_FAILURES) {
|
|
2286
|
-
this.
|
|
2839
|
+
this.pushHistory({ role: 'system', content: `[注意] 工具 ${toolCall.name} 在这个上下文中不可用 (连续 ${MAX_SAME_TOOL_FAILURES} 次失败: ${result.error}). 请不要再次调用它, 直接用你已知的信息回答用户, 并在回答开头标记 <final gen>.` });
|
|
2287
2840
|
lastFailedTool = '';
|
|
2288
2841
|
lastFailedToolCount = 0;
|
|
2289
2842
|
consecutiveErrors = 0;
|
|
2290
2843
|
return;
|
|
2291
2844
|
}
|
|
2292
2845
|
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
2293
|
-
this.
|
|
2846
|
+
this.pushHistory({ role: 'system', content: `[注意] 前面的工具调用连续失败。请尝试其他工具或换一种方式完成用户请求, 或用 <final gen> 给出最终回答.` });
|
|
2294
2847
|
consecutiveErrors = 0;
|
|
2295
2848
|
}
|
|
2296
2849
|
}
|
|
@@ -2299,11 +2852,11 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2299
2852
|
consecutiveErrors++;
|
|
2300
2853
|
totalErrors++;
|
|
2301
2854
|
const errorResult = { success: false, error: String(execError) };
|
|
2302
|
-
this.
|
|
2855
|
+
this.pushHistory({ role: 'tool', content: JSON.stringify(errorResult), toolResult: errorResult });
|
|
2303
2856
|
this.logToHarness(toolCall.name, toolCall.args, errorResult);
|
|
2304
2857
|
const obs = buildObservation(toolCall.name, toolCall.args, errorResult);
|
|
2305
2858
|
const ref = buildReflection(toolCall.name, errorResult.error, totalErrors, lastFailedToolCount);
|
|
2306
|
-
this.
|
|
2859
|
+
this.pushHistory({ role: 'system', content: formatObservationWithReflection(obs, ref) + SHELL_ESCAPE_HINT });
|
|
2307
2860
|
if (onStream)
|
|
2308
2861
|
onStream({ type: 'status', content: `💡 Reflection: ${obs.summary}`, tool: 'system' });
|
|
2309
2862
|
console.error(`[PiAgent] 工具执行异常 (累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}): ${execError}`);
|
|
@@ -2314,7 +2867,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2314
2867
|
}
|
|
2315
2868
|
else {
|
|
2316
2869
|
// LLM 返回的不是 tool call 格式
|
|
2317
|
-
this.
|
|
2870
|
+
this.pushHistory({
|
|
2318
2871
|
role: 'assistant',
|
|
2319
2872
|
content: reply,
|
|
2320
2873
|
reasoningContent: response?.reasoningContent,
|
|
@@ -2343,7 +2896,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2343
2896
|
unreportedRetries++;
|
|
2344
2897
|
const unreported = this.successfulToolResults.length;
|
|
2345
2898
|
console.log(`[PiAgent] LLM 想 final_gen 但还有 ${unreported} 个工具结果未汇报 (${unreportedRetries}/${MAX_UNREPORTED_RETRIES}), push hint 让其继续`);
|
|
2346
|
-
this.
|
|
2899
|
+
this.pushHistory({
|
|
2347
2900
|
role: 'system',
|
|
2348
2901
|
content: `[dive-into stop condition] 你之前已成功执行了 ${unreported} 个工具, 但当前回复里没把它们的结果告诉用户. 请基于已有的工具结果 (在 history 里) 写一个完整总结回复给用户, 用 <final gen> 结尾. 不要再调工具.`
|
|
2349
2902
|
});
|
|
@@ -2356,7 +2909,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2356
2909
|
// 反复提示仍未汇报超过上限 → 清空积压强制 final, 不再死循环
|
|
2357
2910
|
console.log(`[PiAgent] unreported 循环超限 (${unreportedRetries} 次), 清空积压强制 final`);
|
|
2358
2911
|
this.successfulToolResults = [];
|
|
2359
|
-
this.
|
|
2912
|
+
this.pushHistory({
|
|
2360
2913
|
role: 'system',
|
|
2361
2914
|
content: `[dive-into stop condition] 已多次提示汇报工具结果仍未完成 (超过 ${MAX_UNREPORTED_RETRIES} 次). 现在直接基于你已知的信息写最终回复给用户, 用 <final gen> 结尾, 不要再调任何工具.`
|
|
2362
2915
|
});
|
|
@@ -2368,7 +2921,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2368
2921
|
// 2026-07-29: 质量门 — 即使 LLM 声称完成, 质量太低也继续
|
|
2369
2922
|
if (lastQualityScore < this.QUALITY_THRESHOLD && refineAttempts < this.MAX_REFINE_ATTEMPTS) {
|
|
2370
2923
|
console.log(`[PiAgent] final gen 质量 ${lastQualityScore.toFixed(2)} < ${this.QUALITY_THRESHOLD}, 注入 refine hint`);
|
|
2371
|
-
this.
|
|
2924
|
+
this.pushHistory({ role: 'system', content: `[质量检查] 你的回答质量评分为 ${(lastQualityScore * 10).toFixed(1)}/10, 低于 ${(this.QUALITY_THRESHOLD * 10).toFixed(1)}/10 阈值。请提供更完整、详细的回答, 包含工具调用获取到的具体信息, 末尾加 <final gen>。` });
|
|
2372
2925
|
refineAttempts++;
|
|
2373
2926
|
continue;
|
|
2374
2927
|
}
|
|
@@ -2383,13 +2936,13 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2383
2936
|
userIntent: this.currentUserInput,
|
|
2384
2937
|
completedTools: Array.from(loopReviewCompletedTools),
|
|
2385
2938
|
actionLog: loopActionLog,
|
|
2386
|
-
runId: this.
|
|
2387
|
-
goalId: this.
|
|
2939
|
+
runId: this.actor.state.activeRun || undefined,
|
|
2940
|
+
goalId: this.actor.state.goalBinding || undefined,
|
|
2388
2941
|
}, DEFAULT_MAX_REVIEWS);
|
|
2389
2942
|
if (reviewDecision.kind === 'continue-review') {
|
|
2390
2943
|
loopReviewCount++;
|
|
2391
2944
|
console.log(`[PiAgent] review ${loopReviewCount}/${DEFAULT_MAX_REVIEWS}: LLM 想 final 但先对齐需求深挖一次`);
|
|
2392
|
-
this.
|
|
2945
|
+
this.pushHistory({ role: 'system', content: reviewDecision.hint });
|
|
2393
2946
|
if (onStream) {
|
|
2394
2947
|
onStream({ type: 'status', internal: true, content: `🔄 目标对齐 review ${loopReviewCount}/${DEFAULT_MAX_REVIEWS}: 深挖续跑`, tool: 'system' });
|
|
2395
2948
|
}
|
|
@@ -2442,16 +2995,16 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2442
2995
|
if (onStream) {
|
|
2443
2996
|
onStream({ type: 'status', internal: true, content: `✅ 处理完成,共 ${iteration - 1} 次循环`, tool: 'system' });
|
|
2444
2997
|
}
|
|
2445
|
-
this.
|
|
2998
|
+
this.pushHistory({ role: 'assistant', content: finalResponse });
|
|
2446
2999
|
// React Harness: 循环结束
|
|
2447
3000
|
// 2026-09-16 (Milestone 1-B): 会话收尾走唯一门面
|
|
2448
3001
|
await this.piHarness().sessionEnd(this.harnessCtx());
|
|
2449
3002
|
// 2026-09-16: 收尾落盘 — done / failed / aborted / needs_human 如实写回 (不留幽灵 running)
|
|
2450
|
-
if (this.
|
|
3003
|
+
if (this.actor.state.activeRun && !runExternallyPaused && !runExternallyAborted) {
|
|
2451
3004
|
try {
|
|
2452
3005
|
// 2026-09-16 (M4): 完成门 —— "模型说完成"不等于"系统确认完成"。
|
|
2453
3006
|
// 证据 = 成功步骤的事实摘要; 末尾还有失败步骤没被后续成功覆盖 → 不许 done。
|
|
2454
|
-
const recBefore = await readRun(this.
|
|
3007
|
+
const recBefore = await readRun(this.actor.state.activeRun).catch(() => null);
|
|
2455
3008
|
const steps = recBefore?.steps || [];
|
|
2456
3009
|
const evidence = steps.filter((s) => s.ok).map((s) => `${s.tool}: ${(s.summary || '').slice(0, 120)}`).slice(-10);
|
|
2457
3010
|
const lastStep = steps[steps.length - 1];
|
|
@@ -2472,7 +3025,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2472
3025
|
: (trailingFailure || noEvidence)
|
|
2473
3026
|
? 'failed'
|
|
2474
3027
|
: 'done';
|
|
2475
|
-
await finishRun(this.
|
|
3028
|
+
await finishRun(this.actor.state.activeRun, {
|
|
2476
3029
|
status,
|
|
2477
3030
|
summary: finalResponse ? String(finalResponse).slice(0, 400) : undefined,
|
|
2478
3031
|
error: errText
|
|
@@ -2485,7 +3038,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2485
3038
|
// 终态也写不下去: 这是最坏情况 —— 除了留痕, 没有别的自愈手段, 所以必须显眼
|
|
2486
3039
|
const message = `收尾落盘失败 (状态无法写回, 记录会停在 running): ${String(err?.message || err).slice(0, 200)}`;
|
|
2487
3040
|
console.error('[PiAgent] run-store finishRun 失败 (核心持久化):', message);
|
|
2488
|
-
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(() => { });
|
|
2489
3042
|
onStream?.({ type: 'error', content: `⚠️ ${message}`, tool: 'harness' });
|
|
2490
3043
|
}
|
|
2491
3044
|
// ★ 2026-09-25 (M0 接线冻结, 规则 ③ + ④): Run 结束**必过收尾漏斗**。
|
|
@@ -2497,14 +3050,14 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2497
3050
|
//
|
|
2498
3051
|
// 现在: `closeRunOnce` (幂等) → 9 步收尾 → 产物落盘; 再由 Goal reducer 把"下一步是什么"
|
|
2499
3052
|
// 落进 Goal (没有 Supervisor 宿主时 Runner 也要留下 continuation, 否则下一次还是从零开始)。
|
|
2500
|
-
if (this.
|
|
3053
|
+
if (this.actor.state.goalBinding) {
|
|
2501
3054
|
try {
|
|
2502
3055
|
const wiring = await import('./goal-flywheel-wiring.js');
|
|
2503
3056
|
const { reduceGoalState } = await import('./goal-state-reducer.js');
|
|
2504
3057
|
const nowIso = new Date().toISOString();
|
|
2505
3058
|
const closed = await wiring.closeRunOnce({
|
|
2506
|
-
goalId: this.
|
|
2507
|
-
runId: this.
|
|
3059
|
+
goalId: this.actor.state.goalBinding,
|
|
3060
|
+
runId: this.actor.state.activeRun,
|
|
2508
3061
|
caller: 'runner',
|
|
2509
3062
|
now: nowIso,
|
|
2510
3063
|
finalReview: finalResponse ? String(finalResponse).slice(0, 2000) : '',
|
|
@@ -2515,7 +3068,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2515
3068
|
else if (closed.outcome) {
|
|
2516
3069
|
const view = closed.outcome;
|
|
2517
3070
|
const applied = await reduceGoalState({
|
|
2518
|
-
goalId: this.
|
|
3071
|
+
goalId: this.actor.state.goalBinding,
|
|
2519
3072
|
intent: 'closure_outcome',
|
|
2520
3073
|
now: nowIso,
|
|
2521
3074
|
by: 'runner',
|
|
@@ -2532,7 +3085,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2532
3085
|
onStream?.({
|
|
2533
3086
|
type: 'status', internal: true,
|
|
2534
3087
|
content: `${line}${applied.gateRejected ? ` [完成门拒绝: ${applied.gateRejected}]` : ''} `
|
|
2535
|
-
+ `(收尾 ${view.steps} 步 · memory ${view.memories} · skill 候选 ${view.candidates} · goal=${this.
|
|
3088
|
+
+ `(收尾 ${view.steps} 步 · memory ${view.memories} · skill 候选 ${view.candidates} · goal=${this.actor.state.goalBinding})`,
|
|
2536
3089
|
tool: 'harness',
|
|
2537
3090
|
});
|
|
2538
3091
|
}
|
|
@@ -2542,15 +3095,15 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2542
3095
|
}
|
|
2543
3096
|
}
|
|
2544
3097
|
catch (err) {
|
|
2545
|
-
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(() => { });
|
|
2546
3099
|
}
|
|
2547
3100
|
}
|
|
2548
|
-
this.
|
|
3101
|
+
this.actor.state.activeRun = '';
|
|
2549
3102
|
}
|
|
2550
|
-
else if (this.
|
|
3103
|
+
else if (this.actor.state.activeRun) {
|
|
2551
3104
|
// 外部暂停/中止: 状态是人定的, 不覆盖 (paused 等 /resume; aborted 是终态)
|
|
2552
3105
|
onStream?.({ type: 'status', content: `⏹️ 运行状态保持为 ${runExternallyPaused ? 'paused' : 'aborted'} (由外部控制面决定)`, tool: 'harness' });
|
|
2553
|
-
this.
|
|
3106
|
+
this.actor.state.activeRun = '';
|
|
2554
3107
|
}
|
|
2555
3108
|
// 2026-06-16: 暴露 aiFailed 标志 — promptStream 据此决定是否自动重试整个 loop
|
|
2556
3109
|
return { reply: finalResponse, aiFailed, aiFailureReason: aiFailureReason || undefined };
|
|
@@ -2604,7 +3157,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2604
3157
|
// 失败静默: 任何 stage 抛错 → 走老 slice(-10) 逻辑
|
|
2605
3158
|
//
|
|
2606
3159
|
// P1.2: 如果 maybeAutoCompact 算过 Context Collapse 投影, 用 this.projectedHistory (读时投影, 非破坏)
|
|
2607
|
-
const source = this.projectedHistory ?? this.messageHistory;
|
|
3160
|
+
const source = this.projectedHistory ?? this.actor.state.messageHistory;
|
|
2608
3161
|
const recentMessages = this.compressHistorySync(source).slice(-10);
|
|
2609
3162
|
return recentMessages.map(m => {
|
|
2610
3163
|
if (m.role === 'user')
|
|
@@ -2631,7 +3184,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2631
3184
|
// 2026-08-06: 来源优先用 projectedHistory (Context Collapse 投影, 非破坏) —
|
|
2632
3185
|
// 与 buildContext 一致; 之前只让字符串路径用投影, messages 数组路径被跳过,
|
|
2633
3186
|
// 导致 LLM 实际看到的还是未压缩的历史.
|
|
2634
|
-
const source = this.projectedHistory ?? this.messageHistory;
|
|
3187
|
+
const source = this.projectedHistory ?? this.actor.state.messageHistory;
|
|
2635
3188
|
const WINDOW = 15;
|
|
2636
3189
|
const out = [];
|
|
2637
3190
|
// 2026-09-28 (前缀 KV 可命中): 带 CURRENT TURN 注入的条目要**原样回带**.
|
|
@@ -2710,11 +3263,11 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2710
3263
|
/**
|
|
2711
3264
|
* 2026-09-28 (前缀 KV 可命中): 把 chat() 回带的当前轮写回自己的 messageHistory.
|
|
2712
3265
|
* 逻辑在模块级 `writeBackCurrentTurnInto` (纯函数, 门可以直接驱它断言);
|
|
2713
|
-
* 这里只负责接到 `
|
|
3266
|
+
* 这里只负责接到 `actor 的 messageHistory` 上, 并保证任何异常都静默 (写回失败不影响对话).
|
|
2714
3267
|
*/
|
|
2715
3268
|
writeBackCurrentTurn(wire) {
|
|
2716
3269
|
try {
|
|
2717
|
-
return writeBackCurrentTurnInto(this.messageHistory, wire);
|
|
3270
|
+
return writeBackCurrentTurnInto(this.actor.state.messageHistory, wire);
|
|
2718
3271
|
}
|
|
2719
3272
|
catch (err) {
|
|
2720
3273
|
console.warn('[PiAgent] writeBackCurrentTurn failed (silent):', err);
|
|
@@ -2728,7 +3281,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2728
3281
|
estimateHistoryTokens() {
|
|
2729
3282
|
try {
|
|
2730
3283
|
const { estimateTokens } = _piRequire('../context-compaction/index.js');
|
|
2731
|
-
return estimateTokens(this.messageHistory);
|
|
3284
|
+
return estimateTokens(this.actor.state.messageHistory);
|
|
2732
3285
|
}
|
|
2733
3286
|
catch {
|
|
2734
3287
|
return 0;
|
|
@@ -2751,8 +3304,10 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2751
3304
|
console.warn(`[PiAgent] reactive compaction pre-check (${estimated} tokens > 80% threshold)`);
|
|
2752
3305
|
onStream?.({ type: 'status', internal: true, content: '⚠️ reactive compaction 预检触发', tool: 'recovery' });
|
|
2753
3306
|
try {
|
|
2754
|
-
|
|
2755
|
-
|
|
3307
|
+
// 同步压缩: 取快照与替换在**同一拍相邻两行** (中间没有 await) ⇒ 不存在"变换期间被追加"的窗口。
|
|
3308
|
+
// 若哪天这里插入 await, 必须改成 `actor.rebaseHistory(…, snapshotLen)` (见 channel-actor.ts)。
|
|
3309
|
+
const compacted = this.compressHistorySync(this.actor.state.messageHistory);
|
|
3310
|
+
this.replaceHistory(compacted);
|
|
2756
3311
|
if (this.estimateHistoryTokens() > this.maxContextTokens() * 0.8) {
|
|
2757
3312
|
await this.maybeAutoCompact(onStream, signal);
|
|
2758
3313
|
}
|
|
@@ -2803,7 +3358,14 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2803
3358
|
// currentTurnContext 是调用方自己的当前轮易变段 (循环进度/改进提示), 走 CURRENT TURN 区.
|
|
2804
3359
|
// 2026-09-28: 回带最终 wire messages (含注入后的当前轮) → 上层据此写回 messageHistory.
|
|
2805
3360
|
const response = await llm.chat(contextOrMessages, systemPrompt, signal, tools, 'main-agent', 'react-loop', undefined, currentTurnContext);
|
|
2806
|
-
// 2026-
|
|
3361
|
+
// 2026-10-01 (用户: 「思考的记录可以也显示出来吗」): 思考模型的思维链原先只被"存起来回带",
|
|
3362
|
+
// **不显示** ✗。这里把它往流里送一份 —— 显示与否由 CLI 侧决定(BOLLOON_SHOW_THINKING, 默认显示) ✓。
|
|
3363
|
+
try {
|
|
3364
|
+
const rc = String(response?.reasoningContent || '').trim();
|
|
3365
|
+
if (rc)
|
|
3366
|
+
this.runCtx.eventSink?.({ type: 'reasoning', content: rc });
|
|
3367
|
+
}
|
|
3368
|
+
catch { /* 送不出去不影响主流程 */ } // 2026-06-30: 透传 toolCalls (OpenAI 协议 native) 给上层, 让 assistant message 能 emit 真 id
|
|
2807
3369
|
return { reply: response.reply || '', toolCalls: response.toolCalls, messages: response.messages };
|
|
2808
3370
|
}
|
|
2809
3371
|
catch (err) {
|
|
@@ -2841,8 +3403,8 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2841
3403
|
if (Array.isArray(contextOrMessages)) {
|
|
2842
3404
|
contextOrMessages = this.buildContext();
|
|
2843
3405
|
// 也清除最近一轮的 toolCalls, 防止再触发
|
|
2844
|
-
if (this.messageHistory.length > 1) {
|
|
2845
|
-
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];
|
|
2846
3408
|
if (last.role === 'assistant' && last.toolCalls) {
|
|
2847
3409
|
delete last.toolCalls;
|
|
2848
3410
|
}
|
|
@@ -2905,7 +3467,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2905
3467
|
* onStream 广播: 跟 computeJudgmentGate 风格一致 (phase 事件供 UI timeline 显示)
|
|
2906
3468
|
*/
|
|
2907
3469
|
async maybeAutoCompact(onStream, signal) {
|
|
2908
|
-
if (this.messageHistory.length < 10)
|
|
3470
|
+
if (this.actor.state.messageHistory.length < 10)
|
|
2909
3471
|
return; // 历史太短, 不值得压
|
|
2910
3472
|
onStream?.({ type: 'status', internal: true, content: '🗜️ 评估是否需要压缩上下文...', tool: 'compactor' });
|
|
2911
3473
|
// 注入 LLM (用 getMinimax().chat, 与 judgment 注入门 / ReAct 循环同一来源)
|
|
@@ -2924,14 +3486,17 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2924
3486
|
const maxTokens = Math.max(4000, Math.round(cfg.maxTokens * cfg.compressionThreshold));
|
|
2925
3487
|
const beforeTokens = this.estimateHistoryTokens();
|
|
2926
3488
|
const { compactPipeline, isContextCollapseEnabled } = await import('../context-compaction/index.js');
|
|
2927
|
-
|
|
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, {
|
|
2928
3493
|
maxTokens,
|
|
2929
3494
|
llmChat,
|
|
2930
3495
|
collapseLlmChat: llmChat, // P1.2: Context Collapse 投影也用同一 LLM
|
|
2931
|
-
cacheScope: this.
|
|
3496
|
+
cacheScope: this.actor.state.channelId || 'default',
|
|
2932
3497
|
});
|
|
2933
|
-
if (result.compacted && result.history.length < this.messageHistory.length) {
|
|
2934
|
-
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;
|
|
2935
3500
|
const stagesApplied = result.stages.filter((s) => s.applied).map((s) => s.stage).join(' → ');
|
|
2936
3501
|
const afterTokens = this.estimateHistoryTokens();
|
|
2937
3502
|
const savedTokens = Math.max(0, beforeTokens - afterTokens);
|
|
@@ -2949,7 +3514,17 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2949
3514
|
// messageHistory 不变 (非破坏)
|
|
2950
3515
|
}
|
|
2951
3516
|
else {
|
|
2952
|
-
|
|
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
|
+
}
|
|
2953
3528
|
this.projectedHistory = null;
|
|
2954
3529
|
}
|
|
2955
3530
|
// 2026-08-06: snapshot 记录 before/after + 摘要 (供恢复/调试/UI), 事件广播
|
|
@@ -2960,10 +3535,10 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2960
3535
|
afterTokens,
|
|
2961
3536
|
summary: `压缩管道: ${summaryLine}; 节省 ${savedTokens} tokens / ${saved} 条消息`,
|
|
2962
3537
|
preservedMemory: [
|
|
2963
|
-
...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)),
|
|
2964
3539
|
],
|
|
2965
|
-
agentId: this.
|
|
2966
|
-
channelId: this.
|
|
3540
|
+
agentId: this.actor.state.agentId,
|
|
3541
|
+
channelId: this.actor.state.channelId,
|
|
2967
3542
|
});
|
|
2968
3543
|
cm.markCompressComplete(snap);
|
|
2969
3544
|
}
|
|
@@ -3349,10 +3924,16 @@ ${this.extractOperationsFromRef(operationsRef)}
|
|
|
3349
3924
|
return { ...this.identity };
|
|
3350
3925
|
}
|
|
3351
3926
|
updateIdentity(updates) {
|
|
3352
|
-
this.identity = { ...this.identity, ...updates }
|
|
3927
|
+
// 2026-10-01 **修切换频道后身份不换的根因**: 上一版是 `this.identity = { ...this.identity, ...updates }`
|
|
3928
|
+
// —— **换成新对象** ✗。而工具上下文是在 registerTools() 时按**对象引用**捕获的 (`identity: this.identity`),
|
|
3929
|
+
// 并且切频道时 session 是**复用**的(pi-sdk-session-factory 的模块级单例, 只调 updateIdentity) ⇒
|
|
3930
|
+
// 工具里的 ctx.identity 一直指向**旧对象** ⇒ `get_identity` 永远返回上一个频道的身份 ✗✗
|
|
3931
|
+
// (实测: xiaomi/智能体频道都答「233 的 DID + 小龙」, 因为 233 是进程启动时的频道)。
|
|
3932
|
+
// ⇒ 改成**原地改**: 所有持有者(工具上下文/会话/状态栏)都看到同一个对象的新值 ✓。
|
|
3933
|
+
Object.assign(this.identity, updates);
|
|
3353
3934
|
}
|
|
3354
3935
|
setCurrentChannelId(channelId) {
|
|
3355
|
-
this.
|
|
3936
|
+
this.actor.state.channelId = channelId;
|
|
3356
3937
|
}
|
|
3357
3938
|
getSessionState() {
|
|
3358
3939
|
return this.sessionManager.getState();
|
|
@@ -3365,6 +3946,17 @@ ${this.extractOperationsFromRef(operationsRef)}
|
|
|
3365
3946
|
}
|
|
3366
3947
|
async setPersona(persona) {
|
|
3367
3948
|
await this.sessionManager.savePersona(persona);
|
|
3949
|
+
// 2026-10-01: **写透** —— 有身份文档的 agent 不套 persona.json, 只写 JSON 等于没改
|
|
3950
|
+
// (用户报"每次让智能体改都是同一个") ⇒ 同步落进它自己的身份文档 (标记区内)
|
|
3951
|
+
if (this.actor.state.agentId) {
|
|
3952
|
+
try {
|
|
3953
|
+
const { applyPersonaToDocs } = await import('../bootstrap/persona-init.js');
|
|
3954
|
+
const wrote = await applyPersonaToDocs(this.actor.state.agentId, persona);
|
|
3955
|
+
if (wrote.length)
|
|
3956
|
+
console.warn(`[persona] 已把 ${this.actor.state.agentId} 的 persona 写进身份文档: ${wrote.join(' · ')}`);
|
|
3957
|
+
}
|
|
3958
|
+
catch { /* 非致命 */ }
|
|
3959
|
+
}
|
|
3368
3960
|
this.persona = persona;
|
|
3369
3961
|
if (persona.name) {
|
|
3370
3962
|
this.identity.name = persona.name;
|