@bolloon/bolloon-agent 0.6.5 → 0.6.7
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/chain/chain-settlement.js +0 -1
- package/dist/agents/chat-segmenter.js +4 -16
- package/dist/agents/contacts/human-directory.js +155 -0
- package/dist/agents/contacts/tools.js +79 -0
- package/dist/agents/experience-review.js +0 -3
- package/dist/agents/goal-flywheel/goal-change.js +3 -4
- package/dist/agents/goal-resume.js +0 -4
- package/dist/agents/ha-daemon.js +85 -0
- package/dist/agents/ha-supervisor.js +146 -0
- package/dist/agents/intent-classifier.js +1 -1
- package/dist/agents/iteration-budget.js +12 -0
- package/dist/agents/knowledge-organizer.js +0 -2
- package/dist/agents/lesson-to-skill.js +0 -1
- package/dist/agents/loop-review.js +2 -2
- package/dist/agents/memory-store.js +384 -0
- package/dist/agents/opportunity-match.js +15 -3
- package/dist/agents/opportunity-web-search.js +39 -0
- package/dist/agents/p2p-chat-tools.js +1 -2
- package/dist/agents/parse-tool-call.js +9 -22
- package/dist/agents/pi-harness.js +1 -2
- package/dist/agents/pi-sdk-session-manager.js +8 -0
- package/dist/agents/pi-sdk-tools.js +180 -5
- package/dist/agents/pi-sdk.js +210 -503
- package/dist/agents/process-runner.js +28 -9
- package/dist/agents/react-loop.js +9 -17
- package/dist/agents/runner-resolver.js +4 -3
- package/dist/agents/skill-loader.js +22 -15
- package/dist/agents/skills-manager.js +1 -25
- package/dist/agents/workflow-pivot-loop.js +152 -61
- package/dist/bootstrap/context-collector.js +67 -7
- package/dist/bootstrap/context-hierarchy.js +13 -1
- package/dist/bootstrap/persona-loader.js +26 -0
- package/dist/cli/ink-app.js +44 -17
- package/dist/cli/intent-command.js +22 -12
- package/dist/cli/loading-tui.js +8 -2
- package/dist/cli/log-gate.js +40 -8
- package/dist/cli/mention-data.js +1 -0
- package/dist/cli/reply-hygiene.js +22 -0
- package/dist/cli/trace-line.js +61 -2
- package/dist/electron-build/agents/agent-gateway.js.map +1 -1
- package/dist/electron-build/agents/chain/chain-indexer.js.map +1 -1
- package/dist/electron-build/agents/chain/chain-settlement.js +0 -1
- package/dist/electron-build/agents/chain/chain-settlement.js.map +1 -1
- package/dist/electron-build/agents/contacts/chain.js.map +1 -1
- package/dist/electron-build/agents/contacts/human-directory.js +204 -0
- package/dist/electron-build/agents/contacts/human-directory.js.map +1 -0
- package/dist/electron-build/agents/contacts/policy.js.map +1 -1
- package/dist/electron-build/agents/contacts/tools.js +112 -0
- package/dist/electron-build/agents/contacts/tools.js.map +1 -1
- package/dist/electron-build/agents/experience-review.js +0 -3
- package/dist/electron-build/agents/experience-review.js.map +1 -1
- package/dist/electron-build/agents/external-events.js.map +1 -1
- package/dist/electron-build/agents/goal-flywheel/goal-change.js +3 -4
- package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -1
- package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -1
- package/dist/electron-build/agents/goal-resume.js +0 -4
- package/dist/electron-build/agents/goal-resume.js.map +1 -1
- package/dist/electron-build/agents/intent-classifier.js +1 -1
- package/dist/electron-build/agents/lesson-to-skill.js +0 -1
- package/dist/electron-build/agents/lesson-to-skill.js.map +1 -1
- package/dist/electron-build/agents/loop-review.js +2 -2
- package/dist/electron-build/agents/loop-review.js.map +1 -1
- package/dist/electron-build/agents/memory-recall.js.map +1 -1
- package/dist/electron-build/agents/memory-store.js +430 -0
- package/dist/electron-build/agents/memory-store.js.map +1 -0
- package/dist/electron-build/agents/parse-tool-call.js +9 -22
- package/dist/electron-build/agents/parse-tool-call.js.map +1 -1
- package/dist/electron-build/agents/pi-harness.js +1 -2
- package/dist/electron-build/agents/pi-harness.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-session-manager.js +8 -0
- package/dist/electron-build/agents/pi-sdk-session-manager.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-tools.js +180 -5
- package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +208 -501
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/pre-tool-validator.js.map +1 -1
- package/dist/electron-build/agents/process-runner.js +28 -9
- package/dist/electron-build/agents/process-runner.js.map +1 -1
- package/dist/electron-build/agents/runner-resolver.js +4 -3
- package/dist/electron-build/agents/runner-resolver.js.map +1 -1
- package/dist/electron-build/agents/skill-loader.js +22 -16
- package/dist/electron-build/agents/skill-loader.js.map +1 -1
- package/dist/electron-build/agents/skill-supervisor-link.js.map +1 -1
- package/dist/electron-build/agents/skills-manager.js +0 -24
- package/dist/electron-build/agents/skills-manager.js.map +1 -1
- package/dist/electron-build/agents/task/task-runner.js.map +1 -1
- package/dist/electron-build/agents/workflow-engine.js.map +1 -1
- package/dist/electron-build/agents/workflow-pivot-loop.js +155 -62
- package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
- package/dist/electron-build/agents/x402/goal-run-bridge.js.map +1 -1
- package/dist/electron-build/bollharness-integration/gate-state-machine.js.map +1 -1
- package/dist/electron-build/bootstrap/context-collector.js +68 -7
- package/dist/electron-build/bootstrap/context-collector.js.map +1 -1
- package/dist/electron-build/bootstrap/context-hierarchy.js +13 -1
- package/dist/electron-build/bootstrap/context-hierarchy.js.map +1 -1
- package/dist/electron-build/bootstrap/persona-loader.js +27 -0
- package/dist/electron-build/bootstrap/persona-loader.js.map +1 -1
- package/dist/electron-build/electron-preload.js.map +1 -1
- package/dist/electron-build/external-engines/delegate.js.map +1 -1
- package/dist/electron-build/kernel/channel-actor.js +31 -2
- package/dist/electron-build/kernel/channel-actor.js.map +1 -1
- package/dist/electron-build/kernel/model-runtime.js +8 -1
- package/dist/electron-build/kernel/model-runtime.js.map +1 -1
- package/dist/electron-build/llm/config-store.js +20 -0
- package/dist/electron-build/llm/config-store.js.map +1 -1
- package/dist/electron-build/llm/embedding-adapter.js +75 -0
- package/dist/electron-build/llm/embedding-adapter.js.map +1 -0
- package/dist/electron-build/llm/pi-ai.js +112 -38
- package/dist/electron-build/llm/pi-ai.js.map +1 -1
- package/dist/electron-build/llm/system-prompt/registry.js +8 -6
- package/dist/electron-build/llm/system-prompt/registry.js.map +1 -1
- package/dist/electron-build/network/agent-network.js.map +1 -1
- package/dist/electron-build/orbitdb/memory-sync.js +76 -0
- package/dist/electron-build/orbitdb/memory-sync.js.map +1 -0
- package/dist/electron-build/pi-ecosystem-judgment/adaptive-scan.js.map +1 -1
- package/dist/electron-build/security/tool-gate.js +2 -0
- package/dist/electron-build/security/tool-gate.js.map +1 -1
- package/dist/electron-build/setup/setup-store.js +34 -6
- package/dist/electron-build/setup/setup-store.js.map +1 -1
- package/dist/electron-build/social/heartbeat.js +18 -5
- package/dist/electron-build/social/heartbeat.js.map +1 -1
- package/dist/electron-build/utils/update-manager.js.map +1 -1
- package/dist/index.js +250 -12
- package/dist/ios/bolloon-web.json +3 -3
- package/dist/ios/client.js +74 -35
- package/dist/ios/icons/icon-1400x1400.png +0 -0
- package/dist/ios/icons/icon-1400x1400.webp +0 -0
- package/dist/ios/index.html +122 -108
- package/dist/ios/mobile-agent.js +572 -45
- package/dist/ios/mobile-contacts.js +0 -4
- package/dist/ios/mobile-core.js +8079 -4590
- package/dist/ios/mobile-harness.js +1004 -0
- package/dist/ios/mobile-memory.js +317 -0
- package/dist/ios/mobile-orbitdb.js +194 -0
- package/dist/ios/mobile-persona.js +80 -0
- package/dist/ios/mobile-privacy.js +2 -3
- package/dist/ios/mobile.css +286 -21
- package/dist/ios/mobile.html +122 -108
- package/dist/ios/mobile.js +434 -152
- package/dist/ios/routes-contacts.js +94 -1
- package/dist/ios/routes-world.js +13 -2
- package/dist/ios/server.js +54 -41
- package/dist/ios/style.css +13 -5
- package/dist/ios/sw.js +1 -1
- package/dist/ios/ui/message-renderer.js +4 -18
- package/dist/ios/ui/step-timeline.js +14 -1
- package/dist/kernel/channel-actor.js +30 -2
- package/dist/kernel/gate-scan.js +2 -2
- package/dist/kernel/ha/ha-arbiter.js +177 -0
- package/dist/kernel/ha/ha-guard.js +151 -0
- package/dist/kernel/index.js +36 -0
- package/dist/kernel/model-runtime.js +8 -1
- package/dist/kernel/plan.js +5 -5
- package/dist/kernel/roster.js +3 -3
- package/dist/llm/config-store.js +20 -0
- package/dist/llm/embedding-adapter.js +69 -0
- package/dist/llm/pi-ai.js +110 -37
- package/dist/llm/system-prompt/registry.js +8 -6
- package/dist/orbitdb/memory-sync.js +70 -0
- package/dist/pi-ecosystem-judgment/adaptive-scan.js +1 -1
- package/dist/security/tool-gate.js +2 -0
- package/dist/setup/setup-store.js +34 -6
- package/dist/social/heartbeat.js +18 -5
- package/dist/web/bolloon-web.json +3 -3
- package/dist/web/client.js +74 -35
- package/dist/web/icons/icon-1400x1400.png +0 -0
- package/dist/web/icons/icon-1400x1400.webp +0 -0
- package/dist/web/index.html +37 -25
- package/dist/web/mobile-agent.js +572 -45
- package/dist/web/mobile-contacts.js +0 -4
- package/dist/web/mobile-core.js +8079 -4590
- package/dist/web/mobile-harness.js +1004 -0
- package/dist/web/mobile-memory.js +317 -0
- package/dist/web/mobile-orbitdb.js +194 -0
- package/dist/web/mobile-persona.js +80 -0
- package/dist/web/mobile-privacy.js +2 -3
- package/dist/web/mobile.css +286 -21
- package/dist/web/mobile.html +122 -108
- package/dist/web/mobile.js +434 -152
- package/dist/web/routes-contacts.js +94 -1
- package/dist/web/routes-world.js +13 -2
- package/dist/web/server.js +54 -41
- package/dist/web/style.css +13 -5
- package/dist/web/sw.js +1 -1
- package/dist/web/ui/message-renderer.js +4 -18
- package/dist/web/ui/step-timeline.js +14 -1
- package/package.json +3 -2
- package/scripts/build-web.ts +16 -1
package/dist/agents/pi-sdk.js
CHANGED
|
@@ -20,8 +20,6 @@ import { createPrivateActor } from '../kernel/channel-actor.js';
|
|
|
20
20
|
import { submitRunLifecycle, assertLifecycleOk, pickRunIdForResume } from '../kernel/run-lifecycle.js';
|
|
21
21
|
// K10 ④: 通信发送入口经内核端口 (上层不再直接对某条传输说话; 换传输只改这一处注入)
|
|
22
22
|
import { submitTransport, transportPeersSync } from '../kernel/transport.js';
|
|
23
|
-
// K10 ①: run 状态迁移经内核**控制面**端口 (它本来就管"谁命令这条 run 改状态": 带 origin 审计 + 拒绝归一化)
|
|
24
|
-
import { submitRunControl } from '../kernel/control.js';
|
|
25
23
|
// K10 ②: 回合的模型经**内核运行时**只读取租约 (传输仍是 Pi 客户端 ⇒ 行为不变; 取不到如实回落)
|
|
26
24
|
import { ModelRuntime, snapshotFromSelection } from '../kernel/model-runtime.js';
|
|
27
25
|
// K10 余项: 会话生命周期的**规则**归内核 (落盘映射 · key 校验 · 读回过滤/水合 · 运行种子合并顺序), I/O 留本类经端口注入
|
|
@@ -60,13 +58,14 @@ import { DeepThinkingEngine, AgentCoordinator } from '@bolloon/constraint-runtim
|
|
|
60
58
|
import { WorkflowPivotLoop, createDefaultPivotConfig } from './workflow-pivot-loop.js';
|
|
61
59
|
import { nativeAdapterFromEnv } from '../llm/native-adapter.js'; // K9: 第二个 (非 Pi) 推理适配器
|
|
62
60
|
import { initDocumentReceiver } from './p2p-document-tools.js';
|
|
63
|
-
import { startRun, recordStep, finishRun,
|
|
61
|
+
import { startRun, recordStep, finishRun, recordDegradation, recordHarnessEvent, prepareResume, buildResumeInstruction } from './run-store.js';
|
|
64
62
|
import { createGoal, attachRun } from './goal-store.js';
|
|
65
63
|
import { captureRunModelConfig } from '../llm/model-selection.js';
|
|
64
|
+
// 2026-09-26: 工具名出网净化 (pi-ai.ts 唯一边界) + 回程派发还原 (原名 ↔ API 名)
|
|
66
65
|
// 2026-09-28 (前缀 KV 可命中): CURRENT TURN 注入标记 —— "当前轮是否已注入"的唯一判据
|
|
67
66
|
import { CURRENT_TURN_MARKER } from '../llm/pi-ai.js';
|
|
68
67
|
import { PiAgentHarness } from './pi-harness.js';
|
|
69
|
-
import { DiscoveredAgentsManager, createSocialHeartbeat } from '../social/heartbeat.js';
|
|
68
|
+
import { DiscoveredAgentsManager, createSocialHeartbeat, } from '../social/heartbeat.js';
|
|
70
69
|
import { SkillRegistry } from '@bolloon/constraint-runtime';
|
|
71
70
|
import { loadSkillsFromPaths, defaultSkillPaths } from './skill-loader.js';
|
|
72
71
|
/** 2026-08-10: unreported 逃生门判定 — LLM 反复不把工具结果写进回复时, 超过上限强制收尾 (防死循环) */
|
|
@@ -96,7 +95,6 @@ import { budgetReduce, snip, microcompact } from '../context-compaction/index.js
|
|
|
96
95
|
import { ReactHarness } from '../security/react-harness.js';
|
|
97
96
|
import { HooksEngine } from '../hooks/hooks-engine.js';
|
|
98
97
|
import { DenyPipeline } from './deny-pipeline.js';
|
|
99
|
-
import { parseToolCall as parseToolCallImpl, isFinalResponse as isFinalResponseImpl, extractFinalAnswer as extractFinalAnswerImpl } from './parse-tool-call.js';
|
|
100
98
|
import { sessionStore as defaultSessionStore } from './session-store.js';
|
|
101
99
|
import { ToolRegistry } from './tool-registry.js';
|
|
102
100
|
// PiSessionManager 已抽到 ./pi-sdk-session-manager.ts (2026-07-06)
|
|
@@ -106,7 +104,6 @@ import { ToolRegistry } from './tool-registry.js';
|
|
|
106
104
|
* 同一工具 + 同一组参数连续失败上限 (2026-07-01 起) —— 触发两条互不替代的动作:
|
|
107
105
|
* 循环内: 注入 system 提示强制 LLM 收尾; 运行层 (M3): 熔断 → needs_human (交人)。
|
|
108
106
|
*/
|
|
109
|
-
const MAX_SAME_TOOL_FAILURES = 3;
|
|
110
107
|
/**
|
|
111
108
|
* 2026-09-28 (前缀 KV 可命中): 把 chat() 回带的「当前轮已注入」内容写回调用方自己的 history.
|
|
112
109
|
*
|
|
@@ -200,7 +197,10 @@ export class PiAgentSession {
|
|
|
200
197
|
identity;
|
|
201
198
|
persona = null;
|
|
202
199
|
minimaxAvailable = false;
|
|
203
|
-
|
|
200
|
+
/** 2026-10-13: pivot 路径会话累计真实消耗 (每轮 loop 的 totalTokens 累加, 刻度随会话单调增长) */
|
|
201
|
+
pivotCumulativeTokens = 0;
|
|
202
|
+
/** 2026-10-13: 上下文管理周期压缩在飞标志 (防一轮里重复触发) */
|
|
203
|
+
_ctxCycleCompacting = false;
|
|
204
204
|
constraintLayer;
|
|
205
205
|
workflowEngine;
|
|
206
206
|
sessionManager;
|
|
@@ -294,6 +294,8 @@ export class PiAgentSession {
|
|
|
294
294
|
/** 2026-06-30: tool registry 模块 — 独立 alias resolve, 测试可消融. */
|
|
295
295
|
_toolRegistry = new ToolRegistry();
|
|
296
296
|
skillRegistry = new SkillRegistry();
|
|
297
|
+
/** 2026-10-13: 技能扫描 promise (loadSkills 保存, buildSkillsIndex await) — 确保首次对话前技能已就绪 */
|
|
298
|
+
_skillsReady = Promise.resolve();
|
|
297
299
|
/** M2.4: 缓存 tool 列表, registerTools() 之后不变, runReActLoop 多次循环复用 */
|
|
298
300
|
cachedToolDefinitions = '';
|
|
299
301
|
/** M2.4: 缓存 persona section */
|
|
@@ -326,18 +328,74 @@ export class PiAgentSession {
|
|
|
326
328
|
}
|
|
327
329
|
return bg ? `${bg}\n${plans}` : plans;
|
|
328
330
|
}
|
|
331
|
+
/** M2.4 + 2026-10-15: persona section 缓存 (内容 + 源文件指纹)。指纹变了下一轮重建。 */
|
|
329
332
|
cachedPersonaSection = '';
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
|
|
333
|
+
cachedPersonaFingerprint = '';
|
|
334
|
+
/**
|
|
335
|
+
* 2026-10-13 (leo: 「重新阅读理解,整体开发整个技能的实现方式… 成整个加载循环」):
|
|
336
|
+
* 技能库索引摘要 —— 注入 systemPrompt, 让模型**知道技能存在**(名字+一句话描述),
|
|
337
|
+
* 需要时用 `list_skills`(按关键词找) → `read_skill`(读全文, 结果进上下文) 按需加载。
|
|
338
|
+
* 为什么必须有: 技能描述**从不进提示** ⇒ 模型不知道 `read_skill` 该读什么 ⇒ 技能加载循环
|
|
339
|
+
* 断在起点 (此前 `describeSkill` 只 import 从不调用, 见 pi-sdk-tools.ts:915 注释)。
|
|
340
|
+
* 预算控制: 只列前 40 个 + 总数, 完整清单由 `list_skills` 按需取 —— 不撑爆 systemPrompt。
|
|
341
|
+
* 失败静默(返回 ''): 技能库读不到时循环退化成 "无技能", 不阻塞主流程。
|
|
342
|
+
*/
|
|
343
|
+
async buildSkillsIndex() {
|
|
344
|
+
// 2026-10-13 (leo: 「成整个加载循环」): 先等技能扫描 promise 落地再读 registry ——
|
|
345
|
+
// loadSkills 是构造期 fire-and-forget, 不 await 的话首次对话时 registry 还是空的。
|
|
346
|
+
try {
|
|
347
|
+
await this._skillsReady;
|
|
348
|
+
}
|
|
349
|
+
catch { /* 扫描失败不影响 (registry 为空时返回 '') */ }
|
|
350
|
+
try {
|
|
351
|
+
const all = this.skillRegistry.list();
|
|
352
|
+
if (!all.length)
|
|
353
|
+
return '';
|
|
354
|
+
const budget = 40;
|
|
355
|
+
const lines = all.slice(0, budget).map((sk) => {
|
|
356
|
+
const name = String(sk?.name ?? '').trim();
|
|
357
|
+
const desc = String(sk?.description ?? '').trim().replace(/\s+/g, ' ').slice(0, 100);
|
|
358
|
+
return name ? `- ${name}: ${desc}` : null;
|
|
359
|
+
}).filter(Boolean);
|
|
360
|
+
if (!lines.length)
|
|
361
|
+
return '';
|
|
362
|
+
const more = all.length > budget ? `\n…还有 ${all.length - budget} 个 (用 list_skills 按关键词找)` : '';
|
|
363
|
+
return `## 技能库 (${all.length} 个技能 — 需要时用 list_skills 找, read_skill 读全文执行; 内容进上下文, 按需加载不占首屏)\n${lines.join('\n')}${more}`;
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return '';
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* 2026-10-15 (对齐 Hermes volatile 层 memory 整块注入): 稳态记忆块 ——
|
|
371
|
+
* 每轮 build systemPrompt 时把「重要且稳定」的结构化记忆整块给出 (crystallized 优先
|
|
372
|
+
* + importance 降序, 见 memory-store.formatMemorySystemBlock), 不依赖当轮 query 召回。
|
|
373
|
+
* 与 recallMemory (memory-recall.ts 的 session 摘要召回, query 驱动) 互补:
|
|
374
|
+
* 召回 = 即时相关的历史背景; 这块 = 始终可见的用户事实/偏好/固化结论。
|
|
375
|
+
* 失败静默 (返回 ''): 记忆读不到 = 无记忆, 不阻塞主流程。缓存: 按 agentId 单例 store,
|
|
376
|
+
* 每轮 build 都 load 一次 (JSONL 小文件, load 已是最轻读盘; 避免 stale 记忆进 prompt)。
|
|
377
|
+
*/
|
|
378
|
+
async buildStableMemoryBlock() {
|
|
379
|
+
try {
|
|
380
|
+
const agentId = this.actor?.state?.agentId || this.peerId || '';
|
|
381
|
+
if (!agentId)
|
|
382
|
+
return '';
|
|
383
|
+
const { getMemoryStore, formatMemorySystemBlock } = await import('./memory-store.js');
|
|
384
|
+
const store = getMemoryStore(agentId);
|
|
385
|
+
await store.load();
|
|
386
|
+
const all = await store.list(50);
|
|
387
|
+
return formatMemorySystemBlock(all, 6);
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
return '';
|
|
391
|
+
}
|
|
392
|
+
}
|
|
333
393
|
/** 最近一条用户消息 (按需子集档用来判 intent) */
|
|
334
394
|
lastUserIntent = '';
|
|
335
395
|
/** 本回合改过的 TS 文件 (回合收尾自动类型检查用) */
|
|
336
396
|
tsTouchedThisTurn = [];
|
|
337
397
|
/** 本回合是否已跑过类型检查 (一轮只跑一次) */
|
|
338
398
|
typecheckRanThisTurn = false;
|
|
339
|
-
iterBudget = null;
|
|
340
|
-
iterBudgetWarned = false;
|
|
341
399
|
/** 上一次已复盘的**任务签名** (签名变了 ⇒ 立刻复盘, 对应"每次任务都总结" ✓) */
|
|
342
400
|
lastReviewedTaskSig = undefined;
|
|
343
401
|
/** 上一次回合后自审的时间 (节流; 0 = 从未) */
|
|
@@ -346,45 +404,14 @@ export class PiAgentSession {
|
|
|
346
404
|
_sessionStore;
|
|
347
405
|
/** 构造期间 fire-and-forget 任务的 promise — whenReady() 等它 */
|
|
348
406
|
_readyPromise = null;
|
|
349
|
-
// 2026-06-16 修: 父要求把 ReAct loop 上限放大到 "几乎无限", 靠自动压缩上下文 + fail-safe 兜底
|
|
350
|
-
// 默认 10000 — 正常任务永远跑不到, 但作为防 LLM 死循环 / 防 OOM 的最后一道闸
|
|
351
|
-
// 旧默认 100 写死导致中等复杂度任务 (10-50 个 tool call + 多步反思) 会被误杀
|
|
352
|
-
MAX_REACT_ITERATIONS = 10_000;
|
|
353
|
-
MAX_REFINE_ATTEMPTS = 3;
|
|
354
|
-
QUALITY_THRESHOLD = 0.6;
|
|
355
|
-
/** P1: 上下文溢出阈值 (单轮估算 token 数, 超过则强制终止防止 prompt-too-long)
|
|
356
|
-
* 2026-08-06: 从 ContextManager 动态读 (默认 1M, env MAX_CONTEXT_TOKENS 可调).
|
|
357
|
-
* 保留字段仅作降级兜底 (ContextManager 初始化失败时用 60K 老行为). */
|
|
358
|
-
MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD = 60_000; // fallback 60K tokens
|
|
359
|
-
/** 2026-08-06: 上下文窗口 (tokens) — 统一走 ContextManager 配置 (1M 默认). */
|
|
360
|
-
maxContextTokens() {
|
|
361
|
-
try {
|
|
362
|
-
const { getContextManager } = _piRequire('../bootstrap/context-manager.js');
|
|
363
|
-
const n = getContextManager().getConfig().maxTokens;
|
|
364
|
-
return Number.isFinite(n) && n > 0 ? n : this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD;
|
|
365
|
-
}
|
|
366
|
-
catch {
|
|
367
|
-
return this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
/** 2026-06-16 新增: 累计错误总数兜底 (不管是否同工具, 累计 N 次就强制退出)
|
|
371
|
-
* 防 LLM 轮换工具名绕开 MAX_SAME_TOOL_FAILURES 的死循环攻击 */
|
|
372
|
-
MAX_TOTAL_ERRORS = 20;
|
|
373
|
-
/** 2026-06-19: 记录 loop 内成功执行的工具结果, 失败退出时汇总给用户 */
|
|
374
|
-
successfulToolResults = [];
|
|
375
407
|
/** 2026-06-19: Agent Mesh 通信 — 本地 + 远端 inbox 缓存, 给 check_inbox 工具读 */
|
|
376
408
|
_inboxMessages = [];
|
|
377
|
-
/** 2026-06-16 新增: loop 内自动压缩触发阈值 (相对 60K 阈值的比例) */
|
|
378
|
-
LOOP_COMPACT_RATIO = 0.8;
|
|
379
|
-
/** P1: max output token 升级重试 (LLM 截断时重试, 最多 3 次) */
|
|
380
|
-
MAX_OUTPUT_TOKEN_ESCALATION_RETRIES = 3;
|
|
381
409
|
thinkingEngine = new DeepThinkingEngine(3);
|
|
382
410
|
coordinator = new AgentCoordinator(3);
|
|
383
411
|
harness = null;
|
|
384
412
|
harnessEnabled = false;
|
|
385
413
|
/** 8-gate + 4-guard 集中调度 (防越权 / 防 prompt 注入) */
|
|
386
414
|
reactHarness = new ReactHarness();
|
|
387
|
-
usePivotLoop = true; // K4-B (2026-10-02): 默认切到 pivot —— 所有入口只跑**一套** loop (老 ReAct loop 待删)
|
|
388
415
|
pivotLoopConfig;
|
|
389
416
|
/** P2: 当前会话的 permission mode (每次 promptStream 入口解析) */
|
|
390
417
|
currentPermissionMode = 'default';
|
|
@@ -402,8 +429,6 @@ export class PiAgentSession {
|
|
|
402
429
|
_hooks = new HooksEngine();
|
|
403
430
|
/** 2026-07-29: Unified Deny-First Pipeline */
|
|
404
431
|
_denyPipeline = new DenyPipeline();
|
|
405
|
-
/** 2026-07-29: Snip + Context Collapse 启用标志 (默认启用) */
|
|
406
|
-
_enableSnipCollapse = true;
|
|
407
432
|
/** 注册一个或多个工具到拒绝列表 */
|
|
408
433
|
denyTool(...names) {
|
|
409
434
|
for (const name of names)
|
|
@@ -629,7 +654,6 @@ export class PiAgentSession {
|
|
|
629
654
|
this.workflowEngine = new WorkflowEngine(this.constraintLayer);
|
|
630
655
|
this.sessionManager = new PiSessionManager(this.identity.did, this.cwd, this.actor.state.agentId);
|
|
631
656
|
this.agentsManager = new DiscoveredAgentsManager();
|
|
632
|
-
this.usePivotLoop = config.usePivotLoop ?? true; // K4-B: 默认 true (K4-B 之前是 false ⇒ CLI 走老 loop, 与 web 分叉)
|
|
633
657
|
this.pivotLoopConfig = config.pivotLoopConfig;
|
|
634
658
|
this.initSession();
|
|
635
659
|
initDocumentReceiver();
|
|
@@ -645,12 +669,6 @@ export class PiAgentSession {
|
|
|
645
669
|
}
|
|
646
670
|
}
|
|
647
671
|
catch { /* env 读失败静默 */ }
|
|
648
|
-
// 2026-07-29: 从环境变量控制 Snip/Collapse
|
|
649
|
-
try {
|
|
650
|
-
if (process.env.BOLLOON_SNIP_COLLAPSE === '0')
|
|
651
|
-
this._enableSnipCollapse = false;
|
|
652
|
-
}
|
|
653
|
-
catch { /* 静默 */ }
|
|
654
672
|
// 2026-07-29: 从 ~/.bolloon/hooks.yaml 加载 hook 配置
|
|
655
673
|
// 失败静默 (无 hook 配置也正常)
|
|
656
674
|
try {
|
|
@@ -808,14 +826,16 @@ export class PiAgentSession {
|
|
|
808
826
|
* 2026-07-04: 移除 18 个 bollharness builtin skill (findBolloonBuiltinSkillsPath).
|
|
809
827
|
* 历史遗留: 写 pi-sdk 时为方便演示, 把 bolloon 项目里的 19 个 skill 强制注入到 system prompt.
|
|
810
828
|
* 问题: system prompt 涨到 22K chars, LLM (minimax M3) 在 pivot loop 里反复 think 不输出
|
|
811
|
-
*
|
|
829
|
+
* 最终回答, session 落盘拿不到最终回答.
|
|
812
830
|
* 现在: 只让用户放 .bolloon/skills/SKILL.md 才生效, 干净且 project-owned.
|
|
813
831
|
*
|
|
814
832
|
* 静默忽略不存在的目录.
|
|
815
833
|
*/
|
|
816
834
|
loadSkills(paths) {
|
|
817
835
|
const resolved = (paths && paths.length > 0) ? paths : defaultSkillPaths(os.homedir(), this.cwd);
|
|
818
|
-
|
|
836
|
+
// 2026-10-13 (leo: 「成整个加载循环」): 保存 promise 供 buildSkillsIndex await ——
|
|
837
|
+
// 原来 fire-and-forget, 构造后立即读 registry = 0 ⇒ 技能索引空 ⇒ 模型不知道技能 ⇒ 循环断在起点。
|
|
838
|
+
this._skillsReady = loadSkillsFromPaths(resolved)
|
|
819
839
|
.then((skills) => {
|
|
820
840
|
for (const s of skills) {
|
|
821
841
|
if (this.skillRegistry.has(s.name)) {
|
|
@@ -912,7 +932,7 @@ export class PiAgentSession {
|
|
|
912
932
|
_inboxMessages: this._inboxMessages,
|
|
913
933
|
getChannelWallet: async () => {
|
|
914
934
|
try {
|
|
915
|
-
const {
|
|
935
|
+
const {} = await import('../web/server-types.js');
|
|
916
936
|
const { loadChannels } = await import('../web/server-storage.js');
|
|
917
937
|
const channels = await loadChannels();
|
|
918
938
|
const ch = channels.find((c) => c.id === this.actor.state.channelId);
|
|
@@ -944,7 +964,7 @@ export class PiAgentSession {
|
|
|
944
964
|
registerWalletTools(toolCtx);
|
|
945
965
|
setupInboxListener(toolCtx);
|
|
946
966
|
// 镜像到 ToolRegistry (alias resolve 用)
|
|
947
|
-
for (const [
|
|
967
|
+
for (const [, tool] of this.tools.entries()) {
|
|
948
968
|
this._toolRegistry.register(tool);
|
|
949
969
|
}
|
|
950
970
|
// M3.3: 副作用工具走幂等性 cache
|
|
@@ -956,9 +976,6 @@ export class PiAgentSession {
|
|
|
956
976
|
}
|
|
957
977
|
/** M3.3: 工具结果缓存 — 防止 loop 重试时副作用 (写文件 / 改代码) 执行多次 */
|
|
958
978
|
_idempotencyCache = new IdempotencyCache();
|
|
959
|
-
async registerP2PDocumentReceiver() {
|
|
960
|
-
await initDocumentReceiver();
|
|
961
|
-
}
|
|
962
979
|
getToolDefinitions() {
|
|
963
980
|
// M2.4 (2026-06-17): 缓存 tool 定义 — registerTools() 在构造时调一次, 此后不变
|
|
964
981
|
// 2026-07-29: 拒绝列表变化时清空缓存, 重新生成
|
|
@@ -1035,10 +1052,50 @@ export class PiAgentSession {
|
|
|
1035
1052
|
* 之前只有 runReActLoop 迭代内上报, chitchat/fallback/pivot 路径状态栏恒 0 */
|
|
1036
1053
|
reportUsageToContextManager() {
|
|
1037
1054
|
try {
|
|
1038
|
-
getContextManager()
|
|
1055
|
+
const cm = getContextManager();
|
|
1056
|
+
const estimated = this.estimateHistoryTokens();
|
|
1057
|
+
// 2026-10-13 (leo: 刻度被覆盖回 0): pivot 路径在本轮内已上报真实消耗
|
|
1058
|
+
// (promptWithPivotLoop 内 updateUsage(pivotCumulative)); 这里若再用 actor 历史估算**覆盖**,
|
|
1059
|
+
// 刻度会被拉回 ~0 (pivot 路径 actor 历史每轮只有 1-2 条)。⇒ 取 max, 只升不降。
|
|
1060
|
+
cm.updateUsage(Math.max(estimated, cm.getUsage().usedTokens));
|
|
1039
1061
|
}
|
|
1040
1062
|
catch { /* 非致命 */ }
|
|
1041
1063
|
}
|
|
1064
|
+
/**
|
|
1065
|
+
* 2026-10-13: 上下文管理周期 — 阈值触发压缩 (pivot 路径)。
|
|
1066
|
+
* ContextManager 的 markCompressStart/Complete + snapshot 原先只在 promptStream 的
|
|
1067
|
+
* maybeAutoCompact 里被调用; CLI 主路径 (prompt → promptWithPivotLoop) 从没触发过 ⇒
|
|
1068
|
+
* 刻度显示到 'compressing' 但永远不会真压缩。这里补上最小周期:
|
|
1069
|
+
* 累计上报 → stage 达压缩阈值 → 同步前 3 层压缩 (compressHistorySync, 零 LLM 成本) → snapshot。
|
|
1070
|
+
* 失败静默, 不阻塞主流程。
|
|
1071
|
+
*/
|
|
1072
|
+
async runContextCycle(usage) {
|
|
1073
|
+
try {
|
|
1074
|
+
const cm = getContextManager();
|
|
1075
|
+
const history = this.actor.state.messageHistory;
|
|
1076
|
+
if (history.length < 10) {
|
|
1077
|
+
console.log(`[PiAgent] 上下文管理周期: 刻度达压缩阈值 (${Math.round(usage.pct * 100)}%), 历史仅 ${history.length} 条, 跳过压缩 (stage=compressing 留作警示)`);
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
const beforeTokens = this.estimateHistoryTokens();
|
|
1081
|
+
cm.markCompressStart(beforeTokens);
|
|
1082
|
+
const compacted = this.compressHistorySync(history);
|
|
1083
|
+
this.replaceHistory(compacted);
|
|
1084
|
+
const afterTokens = this.estimateHistoryTokens();
|
|
1085
|
+
const snap = cm.makeSnapshot({
|
|
1086
|
+
beforeTokens,
|
|
1087
|
+
afterTokens,
|
|
1088
|
+
summary: `pivot 阈值压缩 (同步前3层): ${beforeTokens.toLocaleString()} → ${afterTokens.toLocaleString()} tokens`,
|
|
1089
|
+
agentId: this.actor.state.agentId,
|
|
1090
|
+
channelId: this.actor.state.channelId,
|
|
1091
|
+
});
|
|
1092
|
+
cm.markCompressComplete(snap);
|
|
1093
|
+
console.log(`[PiAgent] 上下文管理周期: 压缩完成 ${beforeTokens.toLocaleString()}→${afterTokens.toLocaleString()} tokens, stage=${cm.getUsage().stage}, saved=${cm.getUsage().lastSavedTokens.toLocaleString()}`);
|
|
1094
|
+
}
|
|
1095
|
+
catch (err) {
|
|
1096
|
+
console.warn('[PiAgent] 上下文管理周期: 压缩失败 (不阻塞主流程):', String(err?.message ?? err).slice(0, 160));
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1042
1099
|
// ============================================================
|
|
1043
1100
|
// 2026-08-08: 运行轨迹采集 (trajectory) — 每轮运行 → 落盘 + OrbitDB, 失败静默
|
|
1044
1101
|
// ============================================================
|
|
@@ -1119,6 +1176,32 @@ export class PiAgentSession {
|
|
|
1119
1176
|
role: 'user',
|
|
1120
1177
|
content: input
|
|
1121
1178
|
});
|
|
1179
|
+
// 2026-10-13 (leo: 「记忆加载和管理要变成一种执行的规范, 我刚开始就被复利性质地使用过, 所以这个没有实现」):
|
|
1180
|
+
// 记忆加载原来只在 promptStream 调用 (1514), CLI 主路径 prompt() 从不召回 ⇒ 命令行 Agent 根本没有"记忆"
|
|
1181
|
+
// (用户贴屏: 「没有在工具的调用初始化过程当中看到记忆加载和记忆管理」)。这里与 promptStream 同款召回,
|
|
1182
|
+
// 注入 systemPrompt 的 contextHintAddition —— 每轮执行都过这道闸, 就是"执行的规范"。
|
|
1183
|
+
try {
|
|
1184
|
+
const { recallMemory } = await import('./memory-recall.js');
|
|
1185
|
+
const recalled = await recallMemory({ query: input, agentId: this.actor.state.agentId || this.peerId || '' });
|
|
1186
|
+
if (recalled) {
|
|
1187
|
+
this.contextHintAddition = [this.contextHintAddition, recalled].filter(Boolean).join('\n\n');
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
catch { /* 记忆召回失败静默 (增强层) */ }
|
|
1191
|
+
// 2026-10-14 (P0, decentralized-memory-design §2.7): 结构化记忆 (MemoryEntry) 每轮同步召回 —
|
|
1192
|
+
// 与上面会话摘要召回互补: 摘要是「整段会话的压缩」, 这里是「单条结构化记忆」 (带 kind/importance)。
|
|
1193
|
+
// 借鉴 Hermes 冻结快照: 写入落盘不改 prompt, 召回走每轮注入 (progressive disclosure: 只带摘要+权重)。
|
|
1194
|
+
try {
|
|
1195
|
+
const { getMemoryStore, formatRecallBlock } = await import('./memory-store.js');
|
|
1196
|
+
const store = getMemoryStore(this.actor.state.agentId || this.peerId || '');
|
|
1197
|
+
await store.load();
|
|
1198
|
+
const hits = await store.recall({ query: input, limit: 3 });
|
|
1199
|
+
const block = formatRecallBlock(hits);
|
|
1200
|
+
if (block) {
|
|
1201
|
+
this.contextHintAddition = [this.contextHintAddition, block].filter(Boolean).join('\n\n');
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
catch { /* 结构化记忆召回失败静默 (增强层, 不阻塞对话) */ }
|
|
1122
1205
|
// K9 (2026-10-02): 同 `promptWithPivotLoop` —— **回合入口的可用性判据必须问当前适配器**。
|
|
1123
1206
|
// 这类 `!minimaxAvailable` 闸一共 6 处: 1223(prompt)/1466(promptStream)/1714(pivot, 已修)
|
|
1124
1207
|
// 是**回合入口** ⇒ 全部改用 `inferenceAvailable()`; 其余三处是 Pi 自己的文档类功能
|
|
@@ -1526,7 +1609,11 @@ export class PiAgentSession {
|
|
|
1526
1609
|
if (attempt > MAX_LOOP_RETRIES) {
|
|
1527
1610
|
console.warn(`[PiAgent] loop 自动重试 ${MAX_LOOP_RETRIES} 次后仍失败, 终止`);
|
|
1528
1611
|
if (onStream) {
|
|
1529
|
-
|
|
1612
|
+
// 2026-10-14 (leo: 「loop 自动重试提示漏进内容栏」): 老代码这条**漏标 internal** ——
|
|
1613
|
+
// `onStream({type:'status'})` 事件在 CLI 渲染层 (index.ts) 只认发送点的显式 internal 声明,
|
|
1614
|
+
// 没标 → 走 appendLine 上内容栏。与下面 1717 行的「重试中」事件对齐, 补标 internal:true
|
|
1615
|
+
// ⇒ 同一判据 (appendInternalRunLog: 落盘 + --verbose 上屏), 不进对话流。
|
|
1616
|
+
onStream({ type: 'status', internal: true, content: `⛔ loop 自动重试 ${MAX_LOOP_RETRIES} 次后仍失败: ${lastAiFailureReason}`, tool: 'system' });
|
|
1530
1617
|
}
|
|
1531
1618
|
result = lastAiFailureReason || 'AI 服务调用失败, 自动重试后仍不可用';
|
|
1532
1619
|
break;
|
|
@@ -1829,7 +1916,23 @@ export class PiAgentSession {
|
|
|
1829
1916
|
catch (err) {
|
|
1830
1917
|
console.warn('[PiAgent] classifyIntent in pivot failed:', err);
|
|
1831
1918
|
}
|
|
1832
|
-
// M2.4: persona 缓存
|
|
1919
|
+
// M2.4: persona 缓存 — 2026-10-15 加指纹失效: persona 文档在外改了, 下一轮就重建
|
|
1920
|
+
// (对齐 Hermes invalidate_system_prompt 的 persona 侧; 与 systemPrompt 指纹是两条独立链路,
|
|
1921
|
+
// persona 走 personaFingerprint, Bolloon.md/AGENTS.md 走 contextFingerprint)
|
|
1922
|
+
const agentId = this.actor?.state?.agentId || this.peerId || '';
|
|
1923
|
+
if (agentId) {
|
|
1924
|
+
try {
|
|
1925
|
+
const { personaFingerprint } = await import('../bootstrap/persona-loader.js');
|
|
1926
|
+
const fp = await personaFingerprint(agentId);
|
|
1927
|
+
if (fp !== this.cachedPersonaFingerprint) {
|
|
1928
|
+
await this.sessionManager.reloadPersona();
|
|
1929
|
+
this.persona = this.sessionManager.getPersona();
|
|
1930
|
+
this.cachedPersonaFingerprint = fp;
|
|
1931
|
+
this.cachedPersonaSection = '';
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
catch { /* 指纹不可得 → 用旧缓存 (不阻塞) */ }
|
|
1935
|
+
}
|
|
1833
1936
|
if (!this.cachedPersonaSection && this.persona) {
|
|
1834
1937
|
this.cachedPersonaSection = `
|
|
1835
1938
|
角色描述: ${this.persona.description || '无'}
|
|
@@ -1850,6 +1953,10 @@ ${PROACTIVE_WORK_DISCIPLINE}
|
|
|
1850
1953
|
|
|
1851
1954
|
${await this.renderActivePlansSection()}
|
|
1852
1955
|
|
|
1956
|
+
${await this.buildSkillsIndex()}
|
|
1957
|
+
|
|
1958
|
+
${await this.buildStableMemoryBlock()}
|
|
1959
|
+
|
|
1853
1960
|
工作模式:
|
|
1854
1961
|
1. 理解用户自然语言请求
|
|
1855
1962
|
2. 分析需要哪些工具来完成
|
|
@@ -1860,7 +1967,7 @@ ${await this.renderActivePlansSection()}
|
|
|
1860
1967
|
重要 (一次命中要求):
|
|
1861
1968
|
- 每次只调用一个工具
|
|
1862
1969
|
- 仔细分析工具返回结果
|
|
1863
|
-
-
|
|
1970
|
+
- 任务未完成就继续调用工具推进, 直到全部完成
|
|
1864
1971
|
- 如果需要更多信息,继续调用工具
|
|
1865
1972
|
|
|
1866
1973
|
【工具调用格式 (严格遵守, 否则系统无法解析)】
|
|
@@ -1868,8 +1975,7 @@ ${await this.renderActivePlansSection()}
|
|
|
1868
1975
|
- 工具调用格式: {"name":"<tool_name>","input":{"arg1":"value1"}}
|
|
1869
1976
|
- 用 markdown json code block 包裹: \`\`\`json\n{"name":"X","input":{...}}\n\`\`\`
|
|
1870
1977
|
- 工具调用前可以简短思考 (1-2 句话), 但**不要写长篇 thinking** (会撞 max_tokens)
|
|
1871
|
-
- 工具调用后必须等结果,
|
|
1872
|
-
- <final gen> 只在**真完成所有任务**时输出, 不要在工具调用前/中输出${this.judgmentGateAddition}${this.contextHintAddition}`;
|
|
1978
|
+
- 工具调用后必须等结果, 不要在同一个回复里继续输出${this.judgmentGateAddition}${this.contextHintAddition}`;
|
|
1873
1979
|
// 2026-06-15: 把 runCtx.eventSink 传给 loop, 让 step-timeline 在 pivot 循环里也能 emit step_start/done
|
|
1874
1980
|
// 之前 loop.execute() 不接 streamCallback, 导致 step-timeline 只能看到老 runReActLoop 路径
|
|
1875
1981
|
// promptWithPivotLoop 路径 0 step events — UI 显示 timeline 但永远是空
|
|
@@ -1883,16 +1989,32 @@ ${await this.renderActivePlansSection()}
|
|
|
1883
1989
|
// 2026-07-17 Bug 1 修: 注入 messageHistory (hydrateMessageHistory 从 session JSON 回灌的) 到 system prompt
|
|
1884
1990
|
// pivot loop execute() 内部自己维护 messageHistory, 跟 pi-sdk 的 actor 的 messageHistory 隔离,
|
|
1885
1991
|
// 不注入的话 LLM 看不到历史对话, 每次都是新对话.
|
|
1992
|
+
// 2026-10-16 (leo: prompt 比重降低, 流程由 kernel/意图驱动): 压缩历史注入 —
|
|
1993
|
+
// 20 条 × 2000 = 40K 上限吃满 systemPrompt; 历史只为给 pivot 首轮"上次做到哪"的锚,
|
|
1994
|
+
// 细节靠 contextHint (memory 摘要) 与工具补。8 条 × 300, 首尾保真。
|
|
1886
1995
|
const historyLines = [];
|
|
1887
|
-
const
|
|
1996
|
+
const histAll = this.actor.state.messageHistory;
|
|
1997
|
+
const historyToInject = histAll.slice(-8, -1);
|
|
1888
1998
|
for (const m of historyToInject) {
|
|
1889
1999
|
const roleLabel = m.role === 'user' ? '用户' : m.role === 'assistant' ? '你' : m.role === 'tool' ? '工具结果' : m.role;
|
|
1890
|
-
|
|
2000
|
+
// 工具结果只留首行 (JSON 常上千字符, 全量塞进 system 是浪费)
|
|
2001
|
+
let text = String(m.content || '');
|
|
2002
|
+
if (m.role === 'tool')
|
|
2003
|
+
text = text.split('\n')[0].slice(0, 150);
|
|
2004
|
+
else
|
|
2005
|
+
text = text.slice(0, 300);
|
|
1891
2006
|
if (text)
|
|
1892
2007
|
historyLines.push(`[${roleLabel}]: ${text}`);
|
|
1893
2008
|
}
|
|
2009
|
+
// 首条消息保留完整一点 (会话起点 / 用户最初意图, 是"根目标"锚)
|
|
2010
|
+
if (histAll.length > 8) {
|
|
2011
|
+
const first = histAll[0];
|
|
2012
|
+
const firstText = String(first?.content || '').slice(0, 400);
|
|
2013
|
+
if (firstText)
|
|
2014
|
+
historyLines.unshift(`[${first?.role === 'user' ? '用户' : '你'}]: ${firstText}`);
|
|
2015
|
+
}
|
|
1894
2016
|
const historyBlock = historyLines.length > 0
|
|
1895
|
-
? `\n\n【历史对话 (最近 ${historyLines.length}
|
|
2017
|
+
? `\n\n【历史对话 (最近 ${historyLines.length} 条, 压缩摘要)】\n${historyLines.join('\n')}\n【历史对话结束】`
|
|
1896
2018
|
: '';
|
|
1897
2019
|
const onCompact = async () => {
|
|
1898
2020
|
// no-op (best-effort hook for future pi-sdk/pivot history sync)
|
|
@@ -1903,6 +2025,25 @@ ${await this.renderActivePlansSection()}
|
|
|
1903
2025
|
let result;
|
|
1904
2026
|
try {
|
|
1905
2027
|
result = await loop.execute(input, llmWithHarness, systemPrompt + historyBlock, this.runCtx.eventSink ?? undefined, this.runCtx.abortSignal ?? undefined, onCompact);
|
|
2028
|
+
// 2026-10-12 (leo: 「没有根据上下文加载刻度」): pivot 的 messageHistory 是 process-local,
|
|
2029
|
+
// `estimateHistoryTokens()` 读 actor 的旧历史 ⇒ 状态栏上下文刻度恒 0.00% 的根因。
|
|
2030
|
+
// pivot 自己累计的 `state.totalTokens` 才是这一轮**真实**消耗 —— 直接用它上报 ContextManager,
|
|
2031
|
+
// 刻度立刻有数 (旧: 只在上面的 reportUsageToContextManager 里用 actor 历史估, 恒 0)。
|
|
2032
|
+
try {
|
|
2033
|
+
const pivotTokens = Number(result?.state?.totalTokens);
|
|
2034
|
+
if (Number.isFinite(pivotTokens) && pivotTokens > 0) {
|
|
2035
|
+
// 2026-10-13: 会话累计 (多轮对话刻度单调增长; 单轮 totalTokens 只是本轮)
|
|
2036
|
+
this.pivotCumulativeTokens += pivotTokens;
|
|
2037
|
+
const cm = getContextManager();
|
|
2038
|
+
const usage = cm.updateUsage(this.pivotCumulativeTokens);
|
|
2039
|
+
// 上下文管理周期: 上报 → 阈值判断 → 触发压缩 (刻度有数之外, 到阈值真的会动)
|
|
2040
|
+
if (usage.stage === 'compressing' && !this._ctxCycleCompacting) {
|
|
2041
|
+
this._ctxCycleCompacting = true;
|
|
2042
|
+
void this.runContextCycle(usage).finally(() => { this._ctxCycleCompacting = false; });
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
catch { /* 上报失败不阻塞主流程 */ }
|
|
1906
2047
|
}
|
|
1907
2048
|
finally {
|
|
1908
2049
|
try {
|
|
@@ -1923,7 +2064,7 @@ ${await this.renderActivePlansSection()}
|
|
|
1923
2064
|
runId: this.actor.state.activeRun || undefined,
|
|
1924
2065
|
goalId: this.actor.state.goalBinding || undefined,
|
|
1925
2066
|
});
|
|
1926
|
-
console.log(`[PiAgent] reviewFinal (K4-B 恢复调用, 判定不参与流程):
|
|
2067
|
+
console.log(`[PiAgent] reviewFinal (K4-B 恢复调用, 判定不参与流程): kind=${reviewDecision?.kind} reason=${String(reviewDecision?.reason ?? '').slice(0, 60)}`);
|
|
1927
2068
|
}
|
|
1928
2069
|
catch { /* 审查异常不改变本次结论 (门面内部对"审查失效"自有留痕策略) */ }
|
|
1929
2070
|
if (result.response) {
|
|
@@ -1955,8 +2096,6 @@ ${await this.renderActivePlansSection()}
|
|
|
1955
2096
|
awaitingExternal = false;
|
|
1956
2097
|
/** 2026-09-16 (M3): 熔断原因 (同一工具连续失败达上限 → needs_human) */
|
|
1957
2098
|
breakerReason = '';
|
|
1958
|
-
/** 2026-09-16 (M3): 运行中核心写失败 (状态迁移等) —— 交给循环顶部硬闸, 不吞 */
|
|
1959
|
-
persistenceFailure = '';
|
|
1960
2099
|
/**
|
|
1961
2100
|
* 2026-09-16 (M2-B): Supervisor 开新 Run 继续同一 Goal 时注入"上一个 Run 已成功的非幂等动作"。
|
|
1962
2101
|
* 与 resume 的重放守卫同源 (run-store 的 buildContinuationPlan), 保证跨 Run 也不重复副作用。
|
|
@@ -1970,103 +2109,16 @@ ${await this.renderActivePlansSection()}
|
|
|
1970
2109
|
setGoalId(goalId) {
|
|
1971
2110
|
this.actor.state.goalBinding = String(goalId || '');
|
|
1972
2111
|
}
|
|
1973
|
-
/**
|
|
1974
|
-
* K10 ①: 控制面的**注入面** —— 写原语来自 run-store, 但调用一律经 `submitRunControl`
|
|
1975
|
-
* (与 `transportPorts()` 同一个模式: 换实现只改这一处, 上层不直接对 store 说话)。
|
|
1976
|
-
*/
|
|
1977
|
-
runControlPorts() {
|
|
1978
|
-
return {
|
|
1979
|
-
setRunStatus: setRunStatus,
|
|
1980
|
-
recordRecovery: recordRecovery,
|
|
1981
|
-
};
|
|
1982
|
-
}
|
|
1983
2112
|
/**
|
|
1984
2113
|
* 核心状态迁移的兜底: 失败 → 记降级 + 交给循环顶部的持久化硬闸 (不吞错)。
|
|
1985
2114
|
* K10 ①: 迁移动作经内核控制面; **口径不变** ——
|
|
1986
2115
|
* `{ok:false}` 且原因是"端口拒绝"(状态迁移不合法) ⇒ 只返回 false (那不是持久化故障);
|
|
1987
2116
|
* 未注入 / 抛错 ⇒ 才算持久化失败, 记降级 (与旧实现同款)。
|
|
1988
2117
|
*/
|
|
1989
|
-
async safeSetRunStatus(runId, to) {
|
|
1990
|
-
const out = await submitRunControl({ kind: 'set-run-status', origin: 'pi-session', runId, payload: { status: to } }, this.runControlPorts());
|
|
1991
|
-
if (out.ok)
|
|
1992
|
-
return true;
|
|
1993
|
-
const detail = String(out.detail || '未知原因');
|
|
1994
|
-
if (!detail.startsWith('端口拒绝')) {
|
|
1995
|
-
this.persistenceFailure = `状态迁移失败 (${to}): ${detail.slice(0, 180)}`;
|
|
1996
|
-
await recordDegradation({ kind: 'core', op: 'pi-sdk.setRunStatus', runId, message: this.persistenceFailure }).catch(() => { });
|
|
1997
|
-
}
|
|
1998
|
-
return false;
|
|
1999
|
-
}
|
|
2000
2118
|
/**
|
|
2001
2119
|
* 2026-09-16 (M3): 工具失败接线 —— 分类 → recovery 留痕 → 熔断 / 外部等待。
|
|
2002
2120
|
* 这是"错误恢复闭环"的运行时接线点 (此前 recordRecovery / repeatedFailureCount 只是数据结构)。
|
|
2003
2121
|
*/
|
|
2004
|
-
async wireToolFailure(tool, errorText, args) {
|
|
2005
|
-
if (!this.actor.state.activeRun)
|
|
2006
|
-
return;
|
|
2007
|
-
const cls = classifyRunError(errorText);
|
|
2008
|
-
const digest = argsDigestOf(args);
|
|
2009
|
-
let repeats = 1;
|
|
2010
|
-
try {
|
|
2011
|
-
const rec = await readRun(this.actor.state.activeRun);
|
|
2012
|
-
// recordStep 已经把**这次**失败记进去了, 所以"连续失败次数"直接就是当前值 (不再 +1)
|
|
2013
|
-
const seen = rec ? repeatedFailureCount(rec, tool, digest) : 0;
|
|
2014
|
-
repeats = Math.max(seen, 1);
|
|
2015
|
-
}
|
|
2016
|
-
catch { /* 读不到就按首次记 */ }
|
|
2017
|
-
const action = cls === 'transient' ? 'backoff'
|
|
2018
|
-
: cls === 'auth' ? 'escalate'
|
|
2019
|
-
: cls === 'external_no_reply' ? 'pause'
|
|
2020
|
-
: 'retry';
|
|
2021
|
-
try {
|
|
2022
|
-
await recordRecovery(this.actor.state.activeRun, {
|
|
2023
|
-
errorClass: cls,
|
|
2024
|
-
message: `${tool}: ${errorText}`.slice(0, 200),
|
|
2025
|
-
action,
|
|
2026
|
-
attempt: repeats,
|
|
2027
|
-
recovered: false,
|
|
2028
|
-
});
|
|
2029
|
-
}
|
|
2030
|
-
catch (err) {
|
|
2031
|
-
await recordDegradation({ kind: 'core', op: 'pi-sdk.recordRecovery', runId: this.actor.state.activeRun, message: String(err?.message || err) }).catch(() => { });
|
|
2032
|
-
}
|
|
2033
|
-
// 外部无响应 → awaiting_external (不算失败; 等回话后回到 running)
|
|
2034
|
-
if (cls === 'external_no_reply') {
|
|
2035
|
-
this.awaitingExternal = true;
|
|
2036
|
-
await this.safeSetRunStatus(this.actor.state.activeRun, 'awaiting_external');
|
|
2037
|
-
// 2026-09-16 (2-C.4): 把"在等什么"写成持久化事实 (来源/关联/过期), 否则真实回包到了也不知道该唤醒谁。
|
|
2038
|
-
try {
|
|
2039
|
-
if (this.actor.state.goalBinding) {
|
|
2040
|
-
const { bindExternalWait, newContinuationId, defaultWaitExpiry } = await import('./external-events.js');
|
|
2041
|
-
const isDelegate = /delegate/i.test(String(tool || ''));
|
|
2042
|
-
await bindExternalWait(this.actor.state.goalBinding, {
|
|
2043
|
-
requestId: this.pendingExternalRequestId || `${this.actor.state.activeRun}:${Date.now().toString(36)}`,
|
|
2044
|
-
continuationId: newContinuationId(this.actor.state.goalBinding),
|
|
2045
|
-
expectedSource: isDelegate ? 'delegate' : 'p2p',
|
|
2046
|
-
expectedEvent: 'result',
|
|
2047
|
-
createdAt: new Date().toISOString(),
|
|
2048
|
-
expiresAt: defaultWaitExpiry(Date.now(), Number(process.env.BOLLOON_EXTERNAL_WAIT_MS || 30 * 60_000)),
|
|
2049
|
-
note: `${isDelegate ? 'delegate 回包' : 'P2P 协作回复'} 等待中 (tool=${tool})`,
|
|
2050
|
-
});
|
|
2051
|
-
}
|
|
2052
|
-
}
|
|
2053
|
-
catch (e) {
|
|
2054
|
-
await recordDegradation({ kind: 'core', op: 'pi-sdk.bindExternalWait', runId: this.actor.state.activeRun, message: String(e?.message || e) }).catch(() => { });
|
|
2055
|
-
}
|
|
2056
|
-
return;
|
|
2057
|
-
}
|
|
2058
|
-
// 鉴权类: 不重试, 直接交人
|
|
2059
|
-
if (cls === 'auth') {
|
|
2060
|
-
this.breakerReason = `鉴权类错误不重试 (${tool}): ${errorText.slice(0, 120)}`;
|
|
2061
|
-
await this.safeSetRunStatus(this.actor.state.activeRun, 'needs_human');
|
|
2062
|
-
return;
|
|
2063
|
-
}
|
|
2064
|
-
// 重复失败熔断: 同一工具 + 同一组参数连续失败达 3 次
|
|
2065
|
-
if (repeats >= MAX_SAME_TOOL_FAILURES) {
|
|
2066
|
-
this.breakerReason = `同一工具 ${tool} 连续失败 ${repeats} 次 (${cls}) → 熔断, 不再重试`;
|
|
2067
|
-
await this.safeSetRunStatus(this.actor.state.activeRun, 'needs_human');
|
|
2068
|
-
}
|
|
2069
|
-
}
|
|
2070
2122
|
getRunId() {
|
|
2071
2123
|
return this.actor.state.activeRun;
|
|
2072
2124
|
}
|
|
@@ -2239,26 +2291,6 @@ ${await this.renderActivePlansSection()}
|
|
|
2239
2291
|
}
|
|
2240
2292
|
return { outputs, success: allSuccess };
|
|
2241
2293
|
}
|
|
2242
|
-
buildContext() {
|
|
2243
|
-
// P1 接入: 同步跑前 3 层压缩 (Budget Reduction / Snip / Microcompact)
|
|
2244
|
-
// 异步层 (Context Collapse / Auto-Compact) 在 promptStream 入口处单独跑 (用 LLM)
|
|
2245
|
-
// 失败静默: 任何 stage 抛错 → 走老 slice(-10) 逻辑
|
|
2246
|
-
//
|
|
2247
|
-
// P1.2: 如果 maybeAutoCompact 算过 Context Collapse 投影, 用 this.projectedHistory (读时投影, 非破坏)
|
|
2248
|
-
const source = this.projectedHistory ?? this.actor.state.messageHistory;
|
|
2249
|
-
const recentMessages = this.compressHistorySync(source).slice(-10);
|
|
2250
|
-
return recentMessages.map(m => {
|
|
2251
|
-
if (m.role === 'user')
|
|
2252
|
-
return `用户: ${m.content}`;
|
|
2253
|
-
if (m.role === 'assistant')
|
|
2254
|
-
return `助手: ${m.content}`;
|
|
2255
|
-
if (m.role === 'tool') {
|
|
2256
|
-
const result = m.toolResult ? JSON.stringify(m.toolResult) : m.content;
|
|
2257
|
-
return `工具结果: ${result}`;
|
|
2258
|
-
}
|
|
2259
|
-
return m.content;
|
|
2260
|
-
}).join('\n');
|
|
2261
|
-
}
|
|
2262
2294
|
/**
|
|
2263
2295
|
* M3.5 (2026-06-17): 把 history 转成 messages 数组, 给 llm.chat() 用.
|
|
2264
2296
|
* 不再用 buildContext() 把所有 role 压成字符串 — LLM 看不到 tool 调用结果.
|
|
@@ -2267,101 +2299,11 @@ ${await this.renderActivePlansSection()}
|
|
|
2267
2299
|
* 取最近 N 条, 同步压缩前 3 层 (跟 buildContext 同步).
|
|
2268
2300
|
* 跳过 projectedHistory 路径 — messages 数组必须真实, 不能用投影.
|
|
2269
2301
|
*/
|
|
2270
|
-
buildMessages() {
|
|
2271
|
-
try {
|
|
2272
|
-
// 2026-08-06: 来源优先用 projectedHistory (Context Collapse 投影, 非破坏) —
|
|
2273
|
-
// 与 buildContext 一致; 之前只让字符串路径用投影, messages 数组路径被跳过,
|
|
2274
|
-
// 导致 LLM 实际看到的还是未压缩的历史.
|
|
2275
|
-
const source = this.projectedHistory ?? this.actor.state.messageHistory;
|
|
2276
|
-
const WINDOW = 15;
|
|
2277
|
-
const out = [];
|
|
2278
|
-
// 2026-09-28 (前缀 KV 可命中): 带 CURRENT TURN 注入的条目要**原样回带**.
|
|
2279
|
-
// 两个理由: (1) 注入块是"当前轮的前缀", 重渲染 (重加 `[工具结果]` 前缀 / 再 slice 截断)
|
|
2280
|
-
// 会改字节 → 下一轮前缀从这条起分叉; (2) 已注入的 tool 条目在 wire 上已经是 user 形状,
|
|
2281
|
-
// 再套一次前缀就是双前缀, 连内容都变了.
|
|
2282
|
-
const isInjectedEntry = (m) => typeof m.content === 'string' && m.content.includes(CURRENT_TURN_MARKER);
|
|
2283
|
-
// 早期历史压缩: 超过窗口时, 不直接丢弃 — 提取前段用户意图摘要注入 (同步, 无 LLM).
|
|
2284
|
-
// 结构对齐 Context OS: System Prompt(persona) + 压缩摘要 + 最近消息.
|
|
2285
|
-
if (source.length > WINDOW) {
|
|
2286
|
-
const early = source.slice(0, source.length - WINDOW);
|
|
2287
|
-
const slice = source.slice(-WINDOW);
|
|
2288
|
-
const earlyUsers = early.filter(m => m.role === 'user' && (m.content || '').trim());
|
|
2289
|
-
const earlyTools = early.filter(m => m.role === 'tool').length;
|
|
2290
|
-
const earlyAssist = early.filter(m => m.role === 'assistant' && (m.content || '').trim()).length;
|
|
2291
|
-
const snippet = earlyUsers.slice(-5).map(m => `- ${(m.content || '').slice(0, 120).replace(/\n/g, ' ')}`).join('\n') || '- (早期对话无用户文本)';
|
|
2292
|
-
out.push({
|
|
2293
|
-
role: 'system',
|
|
2294
|
-
content: `[上下文压缩] 早期 ${early.length} 条消息已压缩 (用户 ${earlyUsers.length} 条 / AI ${earlyAssist} 条 / 工具结果 ${earlyTools} 条). 关键用户意图摘要:\n${snippet}\n[压缩结束] 以下是最近消息:`,
|
|
2295
|
-
});
|
|
2296
|
-
for (const m of slice) {
|
|
2297
|
-
const r = m.role;
|
|
2298
|
-
if (isInjectedEntry(m)) {
|
|
2299
|
-
out.push({ role: 'user', content: String(m.content) });
|
|
2300
|
-
continue;
|
|
2301
|
-
}
|
|
2302
|
-
if (r === 'tool') {
|
|
2303
|
-
out.push({ role: 'user', content: `[工具结果]\n${(m.content || '').slice(0, 2000)}` });
|
|
2304
|
-
continue;
|
|
2305
|
-
}
|
|
2306
|
-
if (r === 'assistant') {
|
|
2307
|
-
out.push({ role: 'assistant', content: (m.content || '').slice(0, 4000), reasoningContent: m.reasoningContent });
|
|
2308
|
-
continue;
|
|
2309
|
-
}
|
|
2310
|
-
if (r === 'user') {
|
|
2311
|
-
out.push({ role: 'user', content: (m.content || '').slice(0, 2000) });
|
|
2312
|
-
continue;
|
|
2313
|
-
}
|
|
2314
|
-
if (r === 'system') {
|
|
2315
|
-
out.push({ role: 'system', content: (m.content || '').slice(0, 2000) });
|
|
2316
|
-
}
|
|
2317
|
-
}
|
|
2318
|
-
return out;
|
|
2319
|
-
}
|
|
2320
|
-
// 窗口内: 原逻辑 (tool 转 user role, 避免 tool_calls 配对)
|
|
2321
|
-
const slice = source.slice(-WINDOW);
|
|
2322
|
-
for (const m of slice) {
|
|
2323
|
-
const r = m.role;
|
|
2324
|
-
if (isInjectedEntry(m)) {
|
|
2325
|
-
out.push({ role: 'user', content: String(m.content) });
|
|
2326
|
-
continue;
|
|
2327
|
-
}
|
|
2328
|
-
if (r === 'tool') {
|
|
2329
|
-
out.push({ role: 'user', content: `[工具结果]\n${m.content || ''}` });
|
|
2330
|
-
continue;
|
|
2331
|
-
}
|
|
2332
|
-
if (r === 'assistant') {
|
|
2333
|
-
out.push({ role: 'assistant', content: m.content || '', reasoningContent: m.reasoningContent });
|
|
2334
|
-
continue;
|
|
2335
|
-
}
|
|
2336
|
-
if (r === 'user') {
|
|
2337
|
-
out.push({ role: 'user', content: m.content || '' });
|
|
2338
|
-
}
|
|
2339
|
-
if (r === 'system') {
|
|
2340
|
-
out.push({ role: 'system', content: m.content || '' });
|
|
2341
|
-
}
|
|
2342
|
-
}
|
|
2343
|
-
return out;
|
|
2344
|
-
}
|
|
2345
|
-
catch (err) {
|
|
2346
|
-
console.warn('[PiAgent] buildMessages failed (silent, falling back to text):', err);
|
|
2347
|
-
// 退化: 用 buildContext 字符串包装成单 user message
|
|
2348
|
-
return [{ role: 'user', content: this.buildContext() }];
|
|
2349
|
-
}
|
|
2350
|
-
}
|
|
2351
2302
|
/**
|
|
2352
2303
|
* 2026-09-28 (前缀 KV 可命中): 把 chat() 回带的当前轮写回自己的 messageHistory.
|
|
2353
2304
|
* 逻辑在模块级 `writeBackCurrentTurnInto` (纯函数, 门可以直接驱它断言);
|
|
2354
2305
|
* 这里只负责接到 `actor 的 messageHistory` 上, 并保证任何异常都静默 (写回失败不影响对话).
|
|
2355
2306
|
*/
|
|
2356
|
-
writeBackCurrentTurn(wire) {
|
|
2357
|
-
try {
|
|
2358
|
-
return writeBackCurrentTurnInto(this.actor.state.messageHistory, wire);
|
|
2359
|
-
}
|
|
2360
|
-
catch (err) {
|
|
2361
|
-
console.warn('[PiAgent] writeBackCurrentTurn failed (silent):', err);
|
|
2362
|
-
return 0;
|
|
2363
|
-
}
|
|
2364
|
-
}
|
|
2365
2307
|
/**
|
|
2366
2308
|
* 估算 messageHistory 的 token 数 (4 字符 ≈ 1 token, 与 context-compaction 同步).
|
|
2367
2309
|
* 失败静默: 任何异常 → 0 (不阻塞)
|
|
@@ -2383,142 +2325,6 @@ ${await this.renderActivePlansSection()}
|
|
|
2383
2325
|
*
|
|
2384
2326
|
* 失败静默: 全部失败 → 返回空 reply, 让上层 no-tool_use 终止
|
|
2385
2327
|
*/
|
|
2386
|
-
async callLlmWithRecovery(llm, contextOrMessages, systemPrompt, signal, onStream, tools,
|
|
2387
|
-
/** 2026-09-28: 当前轮易变段 (循环进度/改进提示) —— 进 CURRENT TURN 区, 不进 system */
|
|
2388
|
-
currentTurnContext) {
|
|
2389
|
-
// Reactive compaction 预检: 估算 token 超 80% 阈值, 跑一次
|
|
2390
|
-
const estimated = this.estimateHistoryTokens();
|
|
2391
|
-
if (estimated > this.maxContextTokens() * 0.8) {
|
|
2392
|
-
console.warn(`[PiAgent] reactive compaction pre-check (${estimated} tokens > 80% threshold)`);
|
|
2393
|
-
onStream?.({ type: 'status', internal: true, content: '⚠️ reactive compaction 预检触发', tool: 'recovery' });
|
|
2394
|
-
try {
|
|
2395
|
-
// 同步压缩: 取快照与替换在**同一拍相邻两行** (中间没有 await) ⇒ 不存在"变换期间被追加"的窗口。
|
|
2396
|
-
// 若哪天这里插入 await, 必须改成 `actor.rebaseHistory(…, snapshotLen)` (见 channel-actor.ts)。
|
|
2397
|
-
const compacted = this.compressHistorySync(this.actor.state.messageHistory);
|
|
2398
|
-
this.replaceHistory(compacted);
|
|
2399
|
-
if (this.estimateHistoryTokens() > this.maxContextTokens() * 0.8) {
|
|
2400
|
-
await this.maybeAutoCompact(onStream, signal);
|
|
2401
|
-
}
|
|
2402
|
-
}
|
|
2403
|
-
catch (err) {
|
|
2404
|
-
console.warn('[PiAgent] reactive compaction pre-check failed:', err);
|
|
2405
|
-
}
|
|
2406
|
-
}
|
|
2407
|
-
// 错误分级 (M1.3, 2026-06-17):
|
|
2408
|
-
// - 401/403/400 (认证/请求错误): 不重试, 直接 fail-fast
|
|
2409
|
-
// - 429 (rate limit): 重试 2 次, 指数退避
|
|
2410
|
-
// - 5xx (上游错误): 重试 2 次, 指数退避
|
|
2411
|
-
// - network (ECONNRESET / fetch failed / abort/timeout): 重试 2 次
|
|
2412
|
-
// - 4xx prompt-too-long: 走 reactive compaction
|
|
2413
|
-
// 这样以前所有错误都触发整个 runReActLoop 重跑(浪费 token),现在 4xx 直接失败
|
|
2414
|
-
// 让上层把失败原因广播给用户,而不是闷在 loop 里 retry 3 次后给空回复
|
|
2415
|
-
const classifyError = (err) => {
|
|
2416
|
-
const msg = String(err?.message || err || '');
|
|
2417
|
-
// 401/403: 认证失败
|
|
2418
|
-
if (/401|unauthor|invalid api key|api_key|forbidden|403/i.test(msg))
|
|
2419
|
-
return 'auth';
|
|
2420
|
-
// 400 prompt-too-long
|
|
2421
|
-
if (/token|too long|exceed|length|context|4000|413/i.test(msg))
|
|
2422
|
-
return 'prompt_too_long';
|
|
2423
|
-
// 429 rate limit
|
|
2424
|
-
if (/429|rate.?limit|too many requests/i.test(msg))
|
|
2425
|
-
return 'rate_limit';
|
|
2426
|
-
// 5xx
|
|
2427
|
-
if (/5\d\d|internal server|bad gateway|service unavailable|gateway timeout|cloudflare|502|503|504/i.test(msg))
|
|
2428
|
-
return 'server';
|
|
2429
|
-
// network
|
|
2430
|
-
if (/econnreset|econnrefused|enotfound|etimedout|fetch failed|network|aborted|timeout/i.test(msg))
|
|
2431
|
-
return 'network';
|
|
2432
|
-
return 'other';
|
|
2433
|
-
};
|
|
2434
|
-
const isRetryable = (cls) => cls === 'rate_limit' || cls === 'server' || cls === 'network' || cls === 'prompt_too_long';
|
|
2435
|
-
const maxAttempts = (cls) => isRetryable(cls) ? 3 : 1;
|
|
2436
|
-
const backoffMs = (attempt) => Math.min(1000 * 2 ** attempt, 8000); // 1s, 2s, 4s, 8s cap
|
|
2437
|
-
let lastErr = null;
|
|
2438
|
-
let lastClass = 'other';
|
|
2439
|
-
for (let attempt = 0; attempt < 4; attempt++) { // 最多 4 次尝试
|
|
2440
|
-
try {
|
|
2441
|
-
// M3.5 (2026-06-17): 传 messages 数组 (如果 contextOrMessages 是数组) 或字符串
|
|
2442
|
-
// 数组版让 LLM 看到结构化的 user/assistant/tool role, 而不是把 history 拼成单字符串
|
|
2443
|
-
// Bug 5: pass tool IDs for native OpenAI tool calling
|
|
2444
|
-
// 2026-09-28 (前缀 KV 可命中): 显式 purpose/source —— pi-sdk 的 ReAct 循环**就是主对话**
|
|
2445
|
-
// (完整 IMMUTABLE PREFIX + 工具全集 + CURRENT TURN, 也只有它带 cache_prompt);
|
|
2446
|
-
// currentTurnContext 是调用方自己的当前轮易变段 (循环进度/改进提示), 走 CURRENT TURN 区.
|
|
2447
|
-
// 2026-09-28: 回带最终 wire messages (含注入后的当前轮) → 上层据此写回 messageHistory.
|
|
2448
|
-
const response = await llm.chat(contextOrMessages, systemPrompt, signal, tools, 'main-agent', 'react-loop', undefined, currentTurnContext);
|
|
2449
|
-
// 2026-10-01 (用户: 「思考的记录可以也显示出来吗」): 思考模型的思维链原先只被"存起来回带",
|
|
2450
|
-
// **不显示** ✗。这里把它往流里送一份 —— 显示与否由 CLI 侧决定(BOLLOON_SHOW_THINKING, 默认显示) ✓。
|
|
2451
|
-
try {
|
|
2452
|
-
const rc = String(response?.reasoningContent || '').trim();
|
|
2453
|
-
if (rc)
|
|
2454
|
-
this.runCtx.eventSink?.({ type: 'reasoning', content: rc });
|
|
2455
|
-
}
|
|
2456
|
-
catch { /* 送不出去不影响主流程 */ } // 2026-06-30: 透传 toolCalls (OpenAI 协议 native) 给上层, 让 assistant message 能 emit 真 id
|
|
2457
|
-
return { reply: response.reply || '', toolCalls: response.toolCalls, messages: response.messages };
|
|
2458
|
-
}
|
|
2459
|
-
catch (err) {
|
|
2460
|
-
// 用户主动 abort: 不重试, 立即抛
|
|
2461
|
-
if (signal?.aborted || err?.name === 'AbortError')
|
|
2462
|
-
throw err;
|
|
2463
|
-
lastErr = err;
|
|
2464
|
-
lastClass = classifyError(err);
|
|
2465
|
-
const errMsg = String(err?.message || err || '').slice(0, 200);
|
|
2466
|
-
const attempts = maxAttempts(lastClass);
|
|
2467
|
-
if (attempt + 1 >= attempts) {
|
|
2468
|
-
console.warn(`[PiAgent] LLM 调用失败, 不再重试 (class=${lastClass}, attempt=${attempt + 1}/${attempts}): ${errMsg}`);
|
|
2469
|
-
break;
|
|
2470
|
-
}
|
|
2471
|
-
console.warn(`[PiAgent] LLM 调用失败 (class=${lastClass}, attempt=${attempt + 1}/${attempts}), ${backoffMs(attempt)}ms 后重试: ${errMsg}`);
|
|
2472
|
-
onStream?.({ type: 'status', content: `⚠️ LLM 调用失败 (${lastClass}), 重试 ${attempt + 2}/${attempts}...`, tool: 'recovery' });
|
|
2473
|
-
if (lastClass === 'prompt_too_long') {
|
|
2474
|
-
try {
|
|
2475
|
-
await this.maybeAutoCompact(onStream, signal);
|
|
2476
|
-
}
|
|
2477
|
-
catch (compactionErr) {
|
|
2478
|
-
console.warn('[PiAgent] reactive compaction on prompt-too-long failed:', compactionErr);
|
|
2479
|
-
}
|
|
2480
|
-
// 重新生成 context (重试 prompt_too_long 时重建 messages — 包含压缩后的 history)
|
|
2481
|
-
if (Array.isArray(contextOrMessages)) {
|
|
2482
|
-
contextOrMessages = this.buildMessages();
|
|
2483
|
-
}
|
|
2484
|
-
else {
|
|
2485
|
-
contextOrMessages = this.buildContext();
|
|
2486
|
-
}
|
|
2487
|
-
}
|
|
2488
|
-
else if (errMsg.includes('insufficient tool messages') || errMsg.includes('must be followed by tool messages')) {
|
|
2489
|
-
// 2026-07-29: 特殊的 400 错误 — tool_calls 配对异常, 降级为纯文本 context
|
|
2490
|
-
console.warn('[PiAgent] insufficient tool messages — 降级为 buildContext 文本');
|
|
2491
|
-
if (Array.isArray(contextOrMessages)) {
|
|
2492
|
-
contextOrMessages = this.buildContext();
|
|
2493
|
-
// 也清除最近一轮的 toolCalls, 防止再触发
|
|
2494
|
-
if (this.actor.state.messageHistory.length > 1) {
|
|
2495
|
-
const last = this.actor.state.messageHistory[this.actor.state.messageHistory.length - 1];
|
|
2496
|
-
if (last.role === 'assistant' && last.toolCalls) {
|
|
2497
|
-
delete last.toolCalls;
|
|
2498
|
-
}
|
|
2499
|
-
}
|
|
2500
|
-
}
|
|
2501
|
-
}
|
|
2502
|
-
else {
|
|
2503
|
-
// 指数退避
|
|
2504
|
-
await new Promise((r) => setTimeout(r, backoffMs(attempt)));
|
|
2505
|
-
}
|
|
2506
|
-
}
|
|
2507
|
-
}
|
|
2508
|
-
// 失败: 返回结构化错误 reply (而不是空字符串), 上层可识别 + UI 可显示
|
|
2509
|
-
const errMsg = String(lastErr?.message || lastErr || '').slice(0, 300);
|
|
2510
|
-
const userMsg = lastClass === 'auth'
|
|
2511
|
-
? `[AI 服务调用失败] 认证错误: ${errMsg}\n请检查 API key 配置 (env: OPENAI_API_KEY / ANTHROPIC_API_KEY 等)`
|
|
2512
|
-
: lastClass === 'rate_limit'
|
|
2513
|
-
? `[AI 服务调用失败] 上游限流 (429): ${errMsg}\n请稍后重试`
|
|
2514
|
-
: lastClass === 'server'
|
|
2515
|
-
? `[AI 服务调用失败] 上游错误: ${errMsg}\n已重试 2 次仍失败, 可稍后重试`
|
|
2516
|
-
: lastClass === 'network'
|
|
2517
|
-
? `[AI 服务调用失败] 网络错误: ${errMsg}\n请检查网络连接`
|
|
2518
|
-
: `[AI 服务调用失败] ${errMsg}`;
|
|
2519
|
-
console.warn(`[PiAgent] callLlmWithRecovery 全部失败 (class=${lastClass}): ${errMsg}`);
|
|
2520
|
-
return { reply: userMsg };
|
|
2521
|
-
}
|
|
2522
2328
|
/**
|
|
2523
2329
|
* 同步压缩: 跑前 3 层 (Budget Reduction / Snip / Microcompact).
|
|
2524
2330
|
* Context Collapse / Auto-Compact 是 async, 不在 buildContext 同步链里跑.
|
|
@@ -2640,91 +2446,7 @@ ${await this.renderActivePlansSection()}
|
|
|
2640
2446
|
cm.updateUsage(beforeTokens);
|
|
2641
2447
|
}
|
|
2642
2448
|
}
|
|
2643
|
-
isFinalResponse(content) {
|
|
2644
|
-
// 2026-06-30: 抽到 ./parse-tool-call.ts 作为纯函数 — 这里只构建 ctx 并调用
|
|
2645
|
-
return isFinalResponseImpl(content, this._parseCtx());
|
|
2646
|
-
}
|
|
2647
|
-
extractFinalAnswer(content) {
|
|
2648
|
-
// 抽取实现已挪到 ./parse-tool-call.ts (纯函数, 易测)
|
|
2649
|
-
return extractFinalAnswerImpl(content);
|
|
2650
|
-
}
|
|
2651
|
-
_parseCtx() {
|
|
2652
|
-
return {
|
|
2653
|
-
tools: new Set(Array.from(this.tools.keys())),
|
|
2654
|
-
resolveAlias: (name) => this.resolveToolName(name),
|
|
2655
|
-
};
|
|
2656
|
-
}
|
|
2657
|
-
parseToolCall(content) {
|
|
2658
|
-
// 2026-06-30: 抽到 ./parse-tool-call.ts 作为纯函数 — 这里只构建 ctx 并调用
|
|
2659
|
-
return parseToolCallImpl(content, this._parseCtx());
|
|
2660
|
-
}
|
|
2661
|
-
// [debug-2026-06-19] 临时: 打印 parseToolCall 输入和返回
|
|
2662
|
-
_dbgParseToolCall(content) {
|
|
2663
|
-
const r = this.parseToolCall(content);
|
|
2664
|
-
console.log('[DBG parseToolCall] result:', JSON.stringify(r), 'content head:', JSON.stringify(content.substring(0, 200)));
|
|
2665
|
-
return r;
|
|
2666
|
-
}
|
|
2667
|
-
/**
|
|
2668
|
-
* 2026-06-19: 工具名大小写不敏感 + Claude Code 风格别名映射
|
|
2669
|
-
|
|
2670
|
-
/**
|
|
2671
|
-
* 2026-06-19: 工具名大小写不敏感 + Claude Code 风格别名映射
|
|
2672
|
-
* LLM 实际产出 Read/Edit/Write/Bash/Grep/Glob 等大写名 (Claude Code 工具命名)
|
|
2673
|
-
* bolloon 注册的是 read_document / edit_file / write_file / shell_exec / list_files
|
|
2674
|
-
* 返回 this.tools 里的标准名, 或 null 表示未识别
|
|
2675
|
-
*/
|
|
2676
|
-
/**
|
|
2677
|
-
* 把 LLM 给的工具名 (可能大小写不一, 或者 Claude Code 风格的别名) 解析为
|
|
2678
|
-
* bolloon 注册的标准工具名.
|
|
2679
|
-
*
|
|
2680
|
-
* 2026-06-30: 委托给 ToolRegistry 模块 — alias 表在 tool-registry.ts 统一维护,
|
|
2681
|
-
* 这里只做 thin wrapper 保留 backward compat (private API 但其它地方可能用).
|
|
2682
|
-
*/
|
|
2683
|
-
resolveToolName(name) {
|
|
2684
|
-
return this._toolRegistry.resolve(name);
|
|
2685
|
-
}
|
|
2686
|
-
estimateResponseQuality(response) {
|
|
2687
|
-
let score = 0.5;
|
|
2688
|
-
if (response.length > 50)
|
|
2689
|
-
score += 0.1;
|
|
2690
|
-
if (response.length > 200)
|
|
2691
|
-
score += 0.1;
|
|
2692
|
-
if (response.length < 20)
|
|
2693
|
-
score -= 0.3;
|
|
2694
|
-
if (response.includes('\n'))
|
|
2695
|
-
score += 0.1;
|
|
2696
|
-
if (response.includes('-') || response.includes('•'))
|
|
2697
|
-
score += 0.05;
|
|
2698
|
-
if (response.includes('```'))
|
|
2699
|
-
score += 0.1;
|
|
2700
|
-
const conclusionWords = ['完成', '结果', '总结', '所以', '因此', '答案', '推荐'];
|
|
2701
|
-
if (conclusionWords.some(w => response.includes(w)))
|
|
2702
|
-
score += 0.1;
|
|
2703
|
-
if (response.includes('调用工具') || response.includes('tool('))
|
|
2704
|
-
score -= 0.2;
|
|
2705
|
-
return Math.max(0, Math.min(1, score));
|
|
2706
|
-
}
|
|
2707
|
-
estimateToolResultQuality(result) {
|
|
2708
|
-
let score = 0.5;
|
|
2709
|
-
if (!result.success)
|
|
2710
|
-
return 0.2;
|
|
2711
|
-
if (result.output) {
|
|
2712
|
-
score += 0.2;
|
|
2713
|
-
if (result.output.length > 50)
|
|
2714
|
-
score += 0.1;
|
|
2715
|
-
if (result.output.length < 10)
|
|
2716
|
-
score -= 0.1;
|
|
2717
|
-
if (result.output.includes('❌') || result.output.includes('error'))
|
|
2718
|
-
score -= 0.2;
|
|
2719
|
-
if (result.output.includes('✅') || result.output.includes('success'))
|
|
2720
|
-
score += 0.1;
|
|
2721
|
-
}
|
|
2722
|
-
if (result.error)
|
|
2723
|
-
score -= 0.3;
|
|
2724
|
-
return Math.max(0, Math.min(1, score));
|
|
2725
|
-
}
|
|
2726
2449
|
async handleFallback(input) {
|
|
2727
|
-
const lowerInput = input.toLowerCase();
|
|
2728
2450
|
const parts = input.trim().split(/\s+/);
|
|
2729
2451
|
const cmd = parts[0].toLowerCase();
|
|
2730
2452
|
const args = parts.slice(1).join(' ');
|
|
@@ -2807,7 +2529,6 @@ ${this.extractOperationsFromRef(operationsRef)}
|
|
|
2807
2529
|
}
|
|
2808
2530
|
extractOperationsFromRef(ref) {
|
|
2809
2531
|
const lines = ref.split('\n');
|
|
2810
|
-
const inOperationsSection = false;
|
|
2811
2532
|
const operationLines = [];
|
|
2812
2533
|
for (let i = 0; i < lines.length; i++) {
|
|
2813
2534
|
const line = lines[i];
|
|
@@ -3214,20 +2935,6 @@ ${this.extractOperationsFromRef(operationsRef)}
|
|
|
3214
2935
|
}
|
|
3215
2936
|
// ==================== Harness Integration ====================
|
|
3216
2937
|
operationLog = [];
|
|
3217
|
-
logToHarness(action, args, result) {
|
|
3218
|
-
if (!this.harnessEnabled || !this.harness)
|
|
3219
|
-
return;
|
|
3220
|
-
this.operationLog.push({
|
|
3221
|
-
timestamp: Date.now(),
|
|
3222
|
-
action,
|
|
3223
|
-
args,
|
|
3224
|
-
result,
|
|
3225
|
-
status: result.success ? 'ok' : 'error'
|
|
3226
|
-
});
|
|
3227
|
-
if (this.operationLog.length >= 10) {
|
|
3228
|
-
this.archiveToHarness();
|
|
3229
|
-
}
|
|
3230
|
-
}
|
|
3231
2938
|
archiveToHarness() {
|
|
3232
2939
|
if (!this.harnessEnabled || !this.harness || this.operationLog.length === 0)
|
|
3233
2940
|
return;
|