@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
|
@@ -58,8 +58,6 @@ const channel_actor_js_1 = require("../kernel/channel-actor.js");
|
|
|
58
58
|
const run_lifecycle_js_1 = require("../kernel/run-lifecycle.js");
|
|
59
59
|
// K10 ④: 通信发送入口经内核端口 (上层不再直接对某条传输说话; 换传输只改这一处注入)
|
|
60
60
|
const transport_js_1 = require("../kernel/transport.js");
|
|
61
|
-
// K10 ①: run 状态迁移经内核**控制面**端口 (它本来就管"谁命令这条 run 改状态": 带 origin 审计 + 拒绝归一化)
|
|
62
|
-
const control_js_1 = require("../kernel/control.js");
|
|
63
61
|
// K10 ②: 回合的模型经**内核运行时**只读取租约 (传输仍是 Pi 客户端 ⇒ 行为不变; 取不到如实回落)
|
|
64
62
|
const model_runtime_js_1 = require("../kernel/model-runtime.js");
|
|
65
63
|
// K10 余项: 会话生命周期的**规则**归内核 (落盘映射 · key 校验 · 读回过滤/水合 · 运行种子合并顺序), I/O 留本类经端口注入
|
|
@@ -101,6 +99,7 @@ const p2p_document_tools_js_1 = require("./p2p-document-tools.js");
|
|
|
101
99
|
const run_store_js_1 = require("./run-store.js");
|
|
102
100
|
const goal_store_js_1 = require("./goal-store.js");
|
|
103
101
|
const model_selection_js_1 = require("../llm/model-selection.js");
|
|
102
|
+
// 2026-09-26: 工具名出网净化 (pi-ai.ts 唯一边界) + 回程派发还原 (原名 ↔ API 名)
|
|
104
103
|
// 2026-09-28 (前缀 KV 可命中): CURRENT TURN 注入标记 —— "当前轮是否已注入"的唯一判据
|
|
105
104
|
const pi_ai_js_1 = require("../llm/pi-ai.js");
|
|
106
105
|
const pi_harness_js_1 = require("./pi-harness.js");
|
|
@@ -140,7 +139,6 @@ const index_js_2 = require("../context-compaction/index.js");
|
|
|
140
139
|
const react_harness_js_1 = require("../security/react-harness.js");
|
|
141
140
|
const hooks_engine_js_1 = require("../hooks/hooks-engine.js");
|
|
142
141
|
const deny_pipeline_js_1 = require("./deny-pipeline.js");
|
|
143
|
-
const parse_tool_call_js_1 = require("./parse-tool-call.js");
|
|
144
142
|
const session_store_js_1 = require("./session-store.js");
|
|
145
143
|
const tool_registry_js_1 = require("./tool-registry.js");
|
|
146
144
|
// PiSessionManager 已抽到 ./pi-sdk-session-manager.ts (2026-07-06)
|
|
@@ -150,7 +148,6 @@ const tool_registry_js_1 = require("./tool-registry.js");
|
|
|
150
148
|
* 同一工具 + 同一组参数连续失败上限 (2026-07-01 起) —— 触发两条互不替代的动作:
|
|
151
149
|
* 循环内: 注入 system 提示强制 LLM 收尾; 运行层 (M3): 熔断 → needs_human (交人)。
|
|
152
150
|
*/
|
|
153
|
-
const MAX_SAME_TOOL_FAILURES = 3;
|
|
154
151
|
/**
|
|
155
152
|
* 2026-09-28 (前缀 KV 可命中): 把 chat() 回带的「当前轮已注入」内容写回调用方自己的 history.
|
|
156
153
|
*
|
|
@@ -247,7 +244,10 @@ class PiAgentSession {
|
|
|
247
244
|
identity;
|
|
248
245
|
persona = null;
|
|
249
246
|
minimaxAvailable = false;
|
|
250
|
-
|
|
247
|
+
/** 2026-10-13: pivot 路径会话累计真实消耗 (每轮 loop 的 totalTokens 累加, 刻度随会话单调增长) */
|
|
248
|
+
pivotCumulativeTokens = 0;
|
|
249
|
+
/** 2026-10-13: 上下文管理周期压缩在飞标志 (防一轮里重复触发) */
|
|
250
|
+
_ctxCycleCompacting = false;
|
|
251
251
|
constraintLayer;
|
|
252
252
|
workflowEngine;
|
|
253
253
|
sessionManager;
|
|
@@ -341,6 +341,8 @@ class PiAgentSession {
|
|
|
341
341
|
/** 2026-06-30: tool registry 模块 — 独立 alias resolve, 测试可消融. */
|
|
342
342
|
_toolRegistry = new tool_registry_js_1.ToolRegistry();
|
|
343
343
|
skillRegistry = new constraint_runtime_2.SkillRegistry();
|
|
344
|
+
/** 2026-10-13: 技能扫描 promise (loadSkills 保存, buildSkillsIndex await) — 确保首次对话前技能已就绪 */
|
|
345
|
+
_skillsReady = Promise.resolve();
|
|
344
346
|
/** M2.4: 缓存 tool 列表, registerTools() 之后不变, runReActLoop 多次循环复用 */
|
|
345
347
|
cachedToolDefinitions = '';
|
|
346
348
|
/** M2.4: 缓存 persona section */
|
|
@@ -373,18 +375,74 @@ class PiAgentSession {
|
|
|
373
375
|
}
|
|
374
376
|
return bg ? `${bg}\n${plans}` : plans;
|
|
375
377
|
}
|
|
378
|
+
/** M2.4 + 2026-10-15: persona section 缓存 (内容 + 源文件指纹)。指纹变了下一轮重建。 */
|
|
376
379
|
cachedPersonaSection = '';
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
|
|
380
|
+
cachedPersonaFingerprint = '';
|
|
381
|
+
/**
|
|
382
|
+
* 2026-10-13 (leo: 「重新阅读理解,整体开发整个技能的实现方式… 成整个加载循环」):
|
|
383
|
+
* 技能库索引摘要 —— 注入 systemPrompt, 让模型**知道技能存在**(名字+一句话描述),
|
|
384
|
+
* 需要时用 `list_skills`(按关键词找) → `read_skill`(读全文, 结果进上下文) 按需加载。
|
|
385
|
+
* 为什么必须有: 技能描述**从不进提示** ⇒ 模型不知道 `read_skill` 该读什么 ⇒ 技能加载循环
|
|
386
|
+
* 断在起点 (此前 `describeSkill` 只 import 从不调用, 见 pi-sdk-tools.ts:915 注释)。
|
|
387
|
+
* 预算控制: 只列前 40 个 + 总数, 完整清单由 `list_skills` 按需取 —— 不撑爆 systemPrompt。
|
|
388
|
+
* 失败静默(返回 ''): 技能库读不到时循环退化成 "无技能", 不阻塞主流程。
|
|
389
|
+
*/
|
|
390
|
+
async buildSkillsIndex() {
|
|
391
|
+
// 2026-10-13 (leo: 「成整个加载循环」): 先等技能扫描 promise 落地再读 registry ——
|
|
392
|
+
// loadSkills 是构造期 fire-and-forget, 不 await 的话首次对话时 registry 还是空的。
|
|
393
|
+
try {
|
|
394
|
+
await this._skillsReady;
|
|
395
|
+
}
|
|
396
|
+
catch { /* 扫描失败不影响 (registry 为空时返回 '') */ }
|
|
397
|
+
try {
|
|
398
|
+
const all = this.skillRegistry.list();
|
|
399
|
+
if (!all.length)
|
|
400
|
+
return '';
|
|
401
|
+
const budget = 40;
|
|
402
|
+
const lines = all.slice(0, budget).map((sk) => {
|
|
403
|
+
const name = String(sk?.name ?? '').trim();
|
|
404
|
+
const desc = String(sk?.description ?? '').trim().replace(/\s+/g, ' ').slice(0, 100);
|
|
405
|
+
return name ? `- ${name}: ${desc}` : null;
|
|
406
|
+
}).filter(Boolean);
|
|
407
|
+
if (!lines.length)
|
|
408
|
+
return '';
|
|
409
|
+
const more = all.length > budget ? `\n…还有 ${all.length - budget} 个 (用 list_skills 按关键词找)` : '';
|
|
410
|
+
return `## 技能库 (${all.length} 个技能 — 需要时用 list_skills 找, read_skill 读全文执行; 内容进上下文, 按需加载不占首屏)\n${lines.join('\n')}${more}`;
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
return '';
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* 2026-10-15 (对齐 Hermes volatile 层 memory 整块注入): 稳态记忆块 ——
|
|
418
|
+
* 每轮 build systemPrompt 时把「重要且稳定」的结构化记忆整块给出 (crystallized 优先
|
|
419
|
+
* + importance 降序, 见 memory-store.formatMemorySystemBlock), 不依赖当轮 query 召回。
|
|
420
|
+
* 与 recallMemory (memory-recall.ts 的 session 摘要召回, query 驱动) 互补:
|
|
421
|
+
* 召回 = 即时相关的历史背景; 这块 = 始终可见的用户事实/偏好/固化结论。
|
|
422
|
+
* 失败静默 (返回 ''): 记忆读不到 = 无记忆, 不阻塞主流程。缓存: 按 agentId 单例 store,
|
|
423
|
+
* 每轮 build 都 load 一次 (JSONL 小文件, load 已是最轻读盘; 避免 stale 记忆进 prompt)。
|
|
424
|
+
*/
|
|
425
|
+
async buildStableMemoryBlock() {
|
|
426
|
+
try {
|
|
427
|
+
const agentId = this.actor?.state?.agentId || this.peerId || '';
|
|
428
|
+
if (!agentId)
|
|
429
|
+
return '';
|
|
430
|
+
const { getMemoryStore, formatMemorySystemBlock } = await Promise.resolve().then(() => __importStar(require('./memory-store.js')));
|
|
431
|
+
const store = getMemoryStore(agentId);
|
|
432
|
+
await store.load();
|
|
433
|
+
const all = await store.list(50);
|
|
434
|
+
return formatMemorySystemBlock(all, 6);
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
return '';
|
|
438
|
+
}
|
|
439
|
+
}
|
|
380
440
|
/** 最近一条用户消息 (按需子集档用来判 intent) */
|
|
381
441
|
lastUserIntent = '';
|
|
382
442
|
/** 本回合改过的 TS 文件 (回合收尾自动类型检查用) */
|
|
383
443
|
tsTouchedThisTurn = [];
|
|
384
444
|
/** 本回合是否已跑过类型检查 (一轮只跑一次) */
|
|
385
445
|
typecheckRanThisTurn = false;
|
|
386
|
-
iterBudget = null;
|
|
387
|
-
iterBudgetWarned = false;
|
|
388
446
|
/** 上一次已复盘的**任务签名** (签名变了 ⇒ 立刻复盘, 对应"每次任务都总结" ✓) */
|
|
389
447
|
lastReviewedTaskSig = undefined;
|
|
390
448
|
/** 上一次回合后自审的时间 (节流; 0 = 从未) */
|
|
@@ -393,45 +451,14 @@ class PiAgentSession {
|
|
|
393
451
|
_sessionStore;
|
|
394
452
|
/** 构造期间 fire-and-forget 任务的 promise — whenReady() 等它 */
|
|
395
453
|
_readyPromise = null;
|
|
396
|
-
// 2026-06-16 修: 父要求把 ReAct loop 上限放大到 "几乎无限", 靠自动压缩上下文 + fail-safe 兜底
|
|
397
|
-
// 默认 10000 — 正常任务永远跑不到, 但作为防 LLM 死循环 / 防 OOM 的最后一道闸
|
|
398
|
-
// 旧默认 100 写死导致中等复杂度任务 (10-50 个 tool call + 多步反思) 会被误杀
|
|
399
|
-
MAX_REACT_ITERATIONS = 10_000;
|
|
400
|
-
MAX_REFINE_ATTEMPTS = 3;
|
|
401
|
-
QUALITY_THRESHOLD = 0.6;
|
|
402
|
-
/** P1: 上下文溢出阈值 (单轮估算 token 数, 超过则强制终止防止 prompt-too-long)
|
|
403
|
-
* 2026-08-06: 从 ContextManager 动态读 (默认 1M, env MAX_CONTEXT_TOKENS 可调).
|
|
404
|
-
* 保留字段仅作降级兜底 (ContextManager 初始化失败时用 60K 老行为). */
|
|
405
|
-
MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD = 60_000; // fallback 60K tokens
|
|
406
|
-
/** 2026-08-06: 上下文窗口 (tokens) — 统一走 ContextManager 配置 (1M 默认). */
|
|
407
|
-
maxContextTokens() {
|
|
408
|
-
try {
|
|
409
|
-
const { getContextManager } = _piRequire('../bootstrap/context-manager.js');
|
|
410
|
-
const n = getContextManager().getConfig().maxTokens;
|
|
411
|
-
return Number.isFinite(n) && n > 0 ? n : this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD;
|
|
412
|
-
}
|
|
413
|
-
catch {
|
|
414
|
-
return this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
/** 2026-06-16 新增: 累计错误总数兜底 (不管是否同工具, 累计 N 次就强制退出)
|
|
418
|
-
* 防 LLM 轮换工具名绕开 MAX_SAME_TOOL_FAILURES 的死循环攻击 */
|
|
419
|
-
MAX_TOTAL_ERRORS = 20;
|
|
420
|
-
/** 2026-06-19: 记录 loop 内成功执行的工具结果, 失败退出时汇总给用户 */
|
|
421
|
-
successfulToolResults = [];
|
|
422
454
|
/** 2026-06-19: Agent Mesh 通信 — 本地 + 远端 inbox 缓存, 给 check_inbox 工具读 */
|
|
423
455
|
_inboxMessages = [];
|
|
424
|
-
/** 2026-06-16 新增: loop 内自动压缩触发阈值 (相对 60K 阈值的比例) */
|
|
425
|
-
LOOP_COMPACT_RATIO = 0.8;
|
|
426
|
-
/** P1: max output token 升级重试 (LLM 截断时重试, 最多 3 次) */
|
|
427
|
-
MAX_OUTPUT_TOKEN_ESCALATION_RETRIES = 3;
|
|
428
456
|
thinkingEngine = new constraint_runtime_1.DeepThinkingEngine(3);
|
|
429
457
|
coordinator = new constraint_runtime_1.AgentCoordinator(3);
|
|
430
458
|
harness = null;
|
|
431
459
|
harnessEnabled = false;
|
|
432
460
|
/** 8-gate + 4-guard 集中调度 (防越权 / 防 prompt 注入) */
|
|
433
461
|
reactHarness = new react_harness_js_1.ReactHarness();
|
|
434
|
-
usePivotLoop = true; // K4-B (2026-10-02): 默认切到 pivot —— 所有入口只跑**一套** loop (老 ReAct loop 待删)
|
|
435
462
|
pivotLoopConfig;
|
|
436
463
|
/** P2: 当前会话的 permission mode (每次 promptStream 入口解析) */
|
|
437
464
|
currentPermissionMode = 'default';
|
|
@@ -449,8 +476,6 @@ class PiAgentSession {
|
|
|
449
476
|
_hooks = new hooks_engine_js_1.HooksEngine();
|
|
450
477
|
/** 2026-07-29: Unified Deny-First Pipeline */
|
|
451
478
|
_denyPipeline = new deny_pipeline_js_1.DenyPipeline();
|
|
452
|
-
/** 2026-07-29: Snip + Context Collapse 启用标志 (默认启用) */
|
|
453
|
-
_enableSnipCollapse = true;
|
|
454
479
|
/** 注册一个或多个工具到拒绝列表 */
|
|
455
480
|
denyTool(...names) {
|
|
456
481
|
for (const name of names)
|
|
@@ -676,7 +701,6 @@ class PiAgentSession {
|
|
|
676
701
|
this.workflowEngine = new workflow_engine_js_1.WorkflowEngine(this.constraintLayer);
|
|
677
702
|
this.sessionManager = new pi_sdk_session_manager_js_2.PiSessionManager(this.identity.did, this.cwd, this.actor.state.agentId);
|
|
678
703
|
this.agentsManager = new heartbeat_js_1.DiscoveredAgentsManager();
|
|
679
|
-
this.usePivotLoop = config.usePivotLoop ?? true; // K4-B: 默认 true (K4-B 之前是 false ⇒ CLI 走老 loop, 与 web 分叉)
|
|
680
704
|
this.pivotLoopConfig = config.pivotLoopConfig;
|
|
681
705
|
this.initSession();
|
|
682
706
|
(0, p2p_document_tools_js_1.initDocumentReceiver)();
|
|
@@ -692,12 +716,6 @@ class PiAgentSession {
|
|
|
692
716
|
}
|
|
693
717
|
}
|
|
694
718
|
catch { /* env 读失败静默 */ }
|
|
695
|
-
// 2026-07-29: 从环境变量控制 Snip/Collapse
|
|
696
|
-
try {
|
|
697
|
-
if (process.env.BOLLOON_SNIP_COLLAPSE === '0')
|
|
698
|
-
this._enableSnipCollapse = false;
|
|
699
|
-
}
|
|
700
|
-
catch { /* 静默 */ }
|
|
701
719
|
// 2026-07-29: 从 ~/.bolloon/hooks.yaml 加载 hook 配置
|
|
702
720
|
// 失败静默 (无 hook 配置也正常)
|
|
703
721
|
try {
|
|
@@ -855,14 +873,16 @@ class PiAgentSession {
|
|
|
855
873
|
* 2026-07-04: 移除 18 个 bollharness builtin skill (findBolloonBuiltinSkillsPath).
|
|
856
874
|
* 历史遗留: 写 pi-sdk 时为方便演示, 把 bolloon 项目里的 19 个 skill 强制注入到 system prompt.
|
|
857
875
|
* 问题: system prompt 涨到 22K chars, LLM (minimax M3) 在 pivot loop 里反复 think 不输出
|
|
858
|
-
*
|
|
876
|
+
* 最终回答, session 落盘拿不到最终回答.
|
|
859
877
|
* 现在: 只让用户放 .bolloon/skills/SKILL.md 才生效, 干净且 project-owned.
|
|
860
878
|
*
|
|
861
879
|
* 静默忽略不存在的目录.
|
|
862
880
|
*/
|
|
863
881
|
loadSkills(paths) {
|
|
864
882
|
const resolved = (paths && paths.length > 0) ? paths : (0, skill_loader_js_1.defaultSkillPaths)(os.homedir(), this.cwd);
|
|
865
|
-
(
|
|
883
|
+
// 2026-10-13 (leo: 「成整个加载循环」): 保存 promise 供 buildSkillsIndex await ——
|
|
884
|
+
// 原来 fire-and-forget, 构造后立即读 registry = 0 ⇒ 技能索引空 ⇒ 模型不知道技能 ⇒ 循环断在起点。
|
|
885
|
+
this._skillsReady = (0, skill_loader_js_1.loadSkillsFromPaths)(resolved)
|
|
866
886
|
.then((skills) => {
|
|
867
887
|
for (const s of skills) {
|
|
868
888
|
if (this.skillRegistry.has(s.name)) {
|
|
@@ -959,7 +979,7 @@ class PiAgentSession {
|
|
|
959
979
|
_inboxMessages: this._inboxMessages,
|
|
960
980
|
getChannelWallet: async () => {
|
|
961
981
|
try {
|
|
962
|
-
const {
|
|
982
|
+
const {} = await Promise.resolve().then(() => __importStar(require('../web/server-types.js')));
|
|
963
983
|
const { loadChannels } = await Promise.resolve().then(() => __importStar(require('../web/server-storage.js')));
|
|
964
984
|
const channels = await loadChannels();
|
|
965
985
|
const ch = channels.find((c) => c.id === this.actor.state.channelId);
|
|
@@ -991,7 +1011,7 @@ class PiAgentSession {
|
|
|
991
1011
|
(0, pi_sdk_tools_js_1.registerWalletTools)(toolCtx);
|
|
992
1012
|
(0, pi_sdk_tools_js_1.setupInboxListener)(toolCtx);
|
|
993
1013
|
// 镜像到 ToolRegistry (alias resolve 用)
|
|
994
|
-
for (const [
|
|
1014
|
+
for (const [, tool] of this.tools.entries()) {
|
|
995
1015
|
this._toolRegistry.register(tool);
|
|
996
1016
|
}
|
|
997
1017
|
// M3.3: 副作用工具走幂等性 cache
|
|
@@ -1003,9 +1023,6 @@ class PiAgentSession {
|
|
|
1003
1023
|
}
|
|
1004
1024
|
/** M3.3: 工具结果缓存 — 防止 loop 重试时副作用 (写文件 / 改代码) 执行多次 */
|
|
1005
1025
|
_idempotencyCache = new pi_sdk_tools_js_1.IdempotencyCache();
|
|
1006
|
-
async registerP2PDocumentReceiver() {
|
|
1007
|
-
await (0, p2p_document_tools_js_1.initDocumentReceiver)();
|
|
1008
|
-
}
|
|
1009
1026
|
getToolDefinitions() {
|
|
1010
1027
|
// M2.4 (2026-06-17): 缓存 tool 定义 — registerTools() 在构造时调一次, 此后不变
|
|
1011
1028
|
// 2026-07-29: 拒绝列表变化时清空缓存, 重新生成
|
|
@@ -1082,10 +1099,50 @@ class PiAgentSession {
|
|
|
1082
1099
|
* 之前只有 runReActLoop 迭代内上报, chitchat/fallback/pivot 路径状态栏恒 0 */
|
|
1083
1100
|
reportUsageToContextManager() {
|
|
1084
1101
|
try {
|
|
1085
|
-
(0, context_manager_js_1.getContextManager)()
|
|
1102
|
+
const cm = (0, context_manager_js_1.getContextManager)();
|
|
1103
|
+
const estimated = this.estimateHistoryTokens();
|
|
1104
|
+
// 2026-10-13 (leo: 刻度被覆盖回 0): pivot 路径在本轮内已上报真实消耗
|
|
1105
|
+
// (promptWithPivotLoop 内 updateUsage(pivotCumulative)); 这里若再用 actor 历史估算**覆盖**,
|
|
1106
|
+
// 刻度会被拉回 ~0 (pivot 路径 actor 历史每轮只有 1-2 条)。⇒ 取 max, 只升不降。
|
|
1107
|
+
cm.updateUsage(Math.max(estimated, cm.getUsage().usedTokens));
|
|
1086
1108
|
}
|
|
1087
1109
|
catch { /* 非致命 */ }
|
|
1088
1110
|
}
|
|
1111
|
+
/**
|
|
1112
|
+
* 2026-10-13: 上下文管理周期 — 阈值触发压缩 (pivot 路径)。
|
|
1113
|
+
* ContextManager 的 markCompressStart/Complete + snapshot 原先只在 promptStream 的
|
|
1114
|
+
* maybeAutoCompact 里被调用; CLI 主路径 (prompt → promptWithPivotLoop) 从没触发过 ⇒
|
|
1115
|
+
* 刻度显示到 'compressing' 但永远不会真压缩。这里补上最小周期:
|
|
1116
|
+
* 累计上报 → stage 达压缩阈值 → 同步前 3 层压缩 (compressHistorySync, 零 LLM 成本) → snapshot。
|
|
1117
|
+
* 失败静默, 不阻塞主流程。
|
|
1118
|
+
*/
|
|
1119
|
+
async runContextCycle(usage) {
|
|
1120
|
+
try {
|
|
1121
|
+
const cm = (0, context_manager_js_1.getContextManager)();
|
|
1122
|
+
const history = this.actor.state.messageHistory;
|
|
1123
|
+
if (history.length < 10) {
|
|
1124
|
+
console.log(`[PiAgent] 上下文管理周期: 刻度达压缩阈值 (${Math.round(usage.pct * 100)}%), 历史仅 ${history.length} 条, 跳过压缩 (stage=compressing 留作警示)`);
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
const beforeTokens = this.estimateHistoryTokens();
|
|
1128
|
+
cm.markCompressStart(beforeTokens);
|
|
1129
|
+
const compacted = this.compressHistorySync(history);
|
|
1130
|
+
this.replaceHistory(compacted);
|
|
1131
|
+
const afterTokens = this.estimateHistoryTokens();
|
|
1132
|
+
const snap = cm.makeSnapshot({
|
|
1133
|
+
beforeTokens,
|
|
1134
|
+
afterTokens,
|
|
1135
|
+
summary: `pivot 阈值压缩 (同步前3层): ${beforeTokens.toLocaleString()} → ${afterTokens.toLocaleString()} tokens`,
|
|
1136
|
+
agentId: this.actor.state.agentId,
|
|
1137
|
+
channelId: this.actor.state.channelId,
|
|
1138
|
+
});
|
|
1139
|
+
cm.markCompressComplete(snap);
|
|
1140
|
+
console.log(`[PiAgent] 上下文管理周期: 压缩完成 ${beforeTokens.toLocaleString()}→${afterTokens.toLocaleString()} tokens, stage=${cm.getUsage().stage}, saved=${cm.getUsage().lastSavedTokens.toLocaleString()}`);
|
|
1141
|
+
}
|
|
1142
|
+
catch (err) {
|
|
1143
|
+
console.warn('[PiAgent] 上下文管理周期: 压缩失败 (不阻塞主流程):', String(err?.message ?? err).slice(0, 160));
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1089
1146
|
// ============================================================
|
|
1090
1147
|
// 2026-08-08: 运行轨迹采集 (trajectory) — 每轮运行 → 落盘 + OrbitDB, 失败静默
|
|
1091
1148
|
// ============================================================
|
|
@@ -1166,6 +1223,32 @@ class PiAgentSession {
|
|
|
1166
1223
|
role: 'user',
|
|
1167
1224
|
content: input
|
|
1168
1225
|
});
|
|
1226
|
+
// 2026-10-13 (leo: 「记忆加载和管理要变成一种执行的规范, 我刚开始就被复利性质地使用过, 所以这个没有实现」):
|
|
1227
|
+
// 记忆加载原来只在 promptStream 调用 (1514), CLI 主路径 prompt() 从不召回 ⇒ 命令行 Agent 根本没有"记忆"
|
|
1228
|
+
// (用户贴屏: 「没有在工具的调用初始化过程当中看到记忆加载和记忆管理」)。这里与 promptStream 同款召回,
|
|
1229
|
+
// 注入 systemPrompt 的 contextHintAddition —— 每轮执行都过这道闸, 就是"执行的规范"。
|
|
1230
|
+
try {
|
|
1231
|
+
const { recallMemory } = await Promise.resolve().then(() => __importStar(require('./memory-recall.js')));
|
|
1232
|
+
const recalled = await recallMemory({ query: input, agentId: this.actor.state.agentId || this.peerId || '' });
|
|
1233
|
+
if (recalled) {
|
|
1234
|
+
this.contextHintAddition = [this.contextHintAddition, recalled].filter(Boolean).join('\n\n');
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
catch { /* 记忆召回失败静默 (增强层) */ }
|
|
1238
|
+
// 2026-10-14 (P0, decentralized-memory-design §2.7): 结构化记忆 (MemoryEntry) 每轮同步召回 —
|
|
1239
|
+
// 与上面会话摘要召回互补: 摘要是「整段会话的压缩」, 这里是「单条结构化记忆」 (带 kind/importance)。
|
|
1240
|
+
// 借鉴 Hermes 冻结快照: 写入落盘不改 prompt, 召回走每轮注入 (progressive disclosure: 只带摘要+权重)。
|
|
1241
|
+
try {
|
|
1242
|
+
const { getMemoryStore, formatRecallBlock } = await Promise.resolve().then(() => __importStar(require('./memory-store.js')));
|
|
1243
|
+
const store = getMemoryStore(this.actor.state.agentId || this.peerId || '');
|
|
1244
|
+
await store.load();
|
|
1245
|
+
const hits = await store.recall({ query: input, limit: 3 });
|
|
1246
|
+
const block = formatRecallBlock(hits);
|
|
1247
|
+
if (block) {
|
|
1248
|
+
this.contextHintAddition = [this.contextHintAddition, block].filter(Boolean).join('\n\n');
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
catch { /* 结构化记忆召回失败静默 (增强层, 不阻塞对话) */ }
|
|
1169
1252
|
// K9 (2026-10-02): 同 `promptWithPivotLoop` —— **回合入口的可用性判据必须问当前适配器**。
|
|
1170
1253
|
// 这类 `!minimaxAvailable` 闸一共 6 处: 1223(prompt)/1466(promptStream)/1714(pivot, 已修)
|
|
1171
1254
|
// 是**回合入口** ⇒ 全部改用 `inferenceAvailable()`; 其余三处是 Pi 自己的文档类功能
|
|
@@ -1573,7 +1656,11 @@ class PiAgentSession {
|
|
|
1573
1656
|
if (attempt > MAX_LOOP_RETRIES) {
|
|
1574
1657
|
console.warn(`[PiAgent] loop 自动重试 ${MAX_LOOP_RETRIES} 次后仍失败, 终止`);
|
|
1575
1658
|
if (onStream) {
|
|
1576
|
-
|
|
1659
|
+
// 2026-10-14 (leo: 「loop 自动重试提示漏进内容栏」): 老代码这条**漏标 internal** ——
|
|
1660
|
+
// `onStream({type:'status'})` 事件在 CLI 渲染层 (index.ts) 只认发送点的显式 internal 声明,
|
|
1661
|
+
// 没标 → 走 appendLine 上内容栏。与下面 1717 行的「重试中」事件对齐, 补标 internal:true
|
|
1662
|
+
// ⇒ 同一判据 (appendInternalRunLog: 落盘 + --verbose 上屏), 不进对话流。
|
|
1663
|
+
onStream({ type: 'status', internal: true, content: `⛔ loop 自动重试 ${MAX_LOOP_RETRIES} 次后仍失败: ${lastAiFailureReason}`, tool: 'system' });
|
|
1577
1664
|
}
|
|
1578
1665
|
result = lastAiFailureReason || 'AI 服务调用失败, 自动重试后仍不可用';
|
|
1579
1666
|
break;
|
|
@@ -1876,7 +1963,23 @@ class PiAgentSession {
|
|
|
1876
1963
|
catch (err) {
|
|
1877
1964
|
console.warn('[PiAgent] classifyIntent in pivot failed:', err);
|
|
1878
1965
|
}
|
|
1879
|
-
// M2.4: persona 缓存
|
|
1966
|
+
// M2.4: persona 缓存 — 2026-10-15 加指纹失效: persona 文档在外改了, 下一轮就重建
|
|
1967
|
+
// (对齐 Hermes invalidate_system_prompt 的 persona 侧; 与 systemPrompt 指纹是两条独立链路,
|
|
1968
|
+
// persona 走 personaFingerprint, Bolloon.md/AGENTS.md 走 contextFingerprint)
|
|
1969
|
+
const agentId = this.actor?.state?.agentId || this.peerId || '';
|
|
1970
|
+
if (agentId) {
|
|
1971
|
+
try {
|
|
1972
|
+
const { personaFingerprint } = await Promise.resolve().then(() => __importStar(require('../bootstrap/persona-loader.js')));
|
|
1973
|
+
const fp = await personaFingerprint(agentId);
|
|
1974
|
+
if (fp !== this.cachedPersonaFingerprint) {
|
|
1975
|
+
await this.sessionManager.reloadPersona();
|
|
1976
|
+
this.persona = this.sessionManager.getPersona();
|
|
1977
|
+
this.cachedPersonaFingerprint = fp;
|
|
1978
|
+
this.cachedPersonaSection = '';
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
catch { /* 指纹不可得 → 用旧缓存 (不阻塞) */ }
|
|
1982
|
+
}
|
|
1880
1983
|
if (!this.cachedPersonaSection && this.persona) {
|
|
1881
1984
|
this.cachedPersonaSection = `
|
|
1882
1985
|
角色描述: ${this.persona.description || '无'}
|
|
@@ -1897,6 +2000,10 @@ ${exports.PROACTIVE_WORK_DISCIPLINE}
|
|
|
1897
2000
|
|
|
1898
2001
|
${await this.renderActivePlansSection()}
|
|
1899
2002
|
|
|
2003
|
+
${await this.buildSkillsIndex()}
|
|
2004
|
+
|
|
2005
|
+
${await this.buildStableMemoryBlock()}
|
|
2006
|
+
|
|
1900
2007
|
工作模式:
|
|
1901
2008
|
1. 理解用户自然语言请求
|
|
1902
2009
|
2. 分析需要哪些工具来完成
|
|
@@ -1907,7 +2014,7 @@ ${await this.renderActivePlansSection()}
|
|
|
1907
2014
|
重要 (一次命中要求):
|
|
1908
2015
|
- 每次只调用一个工具
|
|
1909
2016
|
- 仔细分析工具返回结果
|
|
1910
|
-
-
|
|
2017
|
+
- 任务未完成就继续调用工具推进, 直到全部完成
|
|
1911
2018
|
- 如果需要更多信息,继续调用工具
|
|
1912
2019
|
|
|
1913
2020
|
【工具调用格式 (严格遵守, 否则系统无法解析)】
|
|
@@ -1915,8 +2022,7 @@ ${await this.renderActivePlansSection()}
|
|
|
1915
2022
|
- 工具调用格式: {"name":"<tool_name>","input":{"arg1":"value1"}}
|
|
1916
2023
|
- 用 markdown json code block 包裹: \`\`\`json\n{"name":"X","input":{...}}\n\`\`\`
|
|
1917
2024
|
- 工具调用前可以简短思考 (1-2 句话), 但**不要写长篇 thinking** (会撞 max_tokens)
|
|
1918
|
-
- 工具调用后必须等结果,
|
|
1919
|
-
- <final gen> 只在**真完成所有任务**时输出, 不要在工具调用前/中输出${this.judgmentGateAddition}${this.contextHintAddition}`;
|
|
2025
|
+
- 工具调用后必须等结果, 不要在同一个回复里继续输出${this.judgmentGateAddition}${this.contextHintAddition}`;
|
|
1920
2026
|
// 2026-06-15: 把 runCtx.eventSink 传给 loop, 让 step-timeline 在 pivot 循环里也能 emit step_start/done
|
|
1921
2027
|
// 之前 loop.execute() 不接 streamCallback, 导致 step-timeline 只能看到老 runReActLoop 路径
|
|
1922
2028
|
// promptWithPivotLoop 路径 0 step events — UI 显示 timeline 但永远是空
|
|
@@ -1930,16 +2036,32 @@ ${await this.renderActivePlansSection()}
|
|
|
1930
2036
|
// 2026-07-17 Bug 1 修: 注入 messageHistory (hydrateMessageHistory 从 session JSON 回灌的) 到 system prompt
|
|
1931
2037
|
// pivot loop execute() 内部自己维护 messageHistory, 跟 pi-sdk 的 actor 的 messageHistory 隔离,
|
|
1932
2038
|
// 不注入的话 LLM 看不到历史对话, 每次都是新对话.
|
|
2039
|
+
// 2026-10-16 (leo: prompt 比重降低, 流程由 kernel/意图驱动): 压缩历史注入 —
|
|
2040
|
+
// 20 条 × 2000 = 40K 上限吃满 systemPrompt; 历史只为给 pivot 首轮"上次做到哪"的锚,
|
|
2041
|
+
// 细节靠 contextHint (memory 摘要) 与工具补。8 条 × 300, 首尾保真。
|
|
1933
2042
|
const historyLines = [];
|
|
1934
|
-
const
|
|
2043
|
+
const histAll = this.actor.state.messageHistory;
|
|
2044
|
+
const historyToInject = histAll.slice(-8, -1);
|
|
1935
2045
|
for (const m of historyToInject) {
|
|
1936
2046
|
const roleLabel = m.role === 'user' ? '用户' : m.role === 'assistant' ? '你' : m.role === 'tool' ? '工具结果' : m.role;
|
|
1937
|
-
|
|
2047
|
+
// 工具结果只留首行 (JSON 常上千字符, 全量塞进 system 是浪费)
|
|
2048
|
+
let text = String(m.content || '');
|
|
2049
|
+
if (m.role === 'tool')
|
|
2050
|
+
text = text.split('\n')[0].slice(0, 150);
|
|
2051
|
+
else
|
|
2052
|
+
text = text.slice(0, 300);
|
|
1938
2053
|
if (text)
|
|
1939
2054
|
historyLines.push(`[${roleLabel}]: ${text}`);
|
|
1940
2055
|
}
|
|
2056
|
+
// 首条消息保留完整一点 (会话起点 / 用户最初意图, 是"根目标"锚)
|
|
2057
|
+
if (histAll.length > 8) {
|
|
2058
|
+
const first = histAll[0];
|
|
2059
|
+
const firstText = String(first?.content || '').slice(0, 400);
|
|
2060
|
+
if (firstText)
|
|
2061
|
+
historyLines.unshift(`[${first?.role === 'user' ? '用户' : '你'}]: ${firstText}`);
|
|
2062
|
+
}
|
|
1941
2063
|
const historyBlock = historyLines.length > 0
|
|
1942
|
-
? `\n\n【历史对话 (最近 ${historyLines.length}
|
|
2064
|
+
? `\n\n【历史对话 (最近 ${historyLines.length} 条, 压缩摘要)】\n${historyLines.join('\n')}\n【历史对话结束】`
|
|
1943
2065
|
: '';
|
|
1944
2066
|
const onCompact = async () => {
|
|
1945
2067
|
// no-op (best-effort hook for future pi-sdk/pivot history sync)
|
|
@@ -1950,6 +2072,25 @@ ${await this.renderActivePlansSection()}
|
|
|
1950
2072
|
let result;
|
|
1951
2073
|
try {
|
|
1952
2074
|
result = await loop.execute(input, llmWithHarness, systemPrompt + historyBlock, this.runCtx.eventSink ?? undefined, this.runCtx.abortSignal ?? undefined, onCompact);
|
|
2075
|
+
// 2026-10-12 (leo: 「没有根据上下文加载刻度」): pivot 的 messageHistory 是 process-local,
|
|
2076
|
+
// `estimateHistoryTokens()` 读 actor 的旧历史 ⇒ 状态栏上下文刻度恒 0.00% 的根因。
|
|
2077
|
+
// pivot 自己累计的 `state.totalTokens` 才是这一轮**真实**消耗 —— 直接用它上报 ContextManager,
|
|
2078
|
+
// 刻度立刻有数 (旧: 只在上面的 reportUsageToContextManager 里用 actor 历史估, 恒 0)。
|
|
2079
|
+
try {
|
|
2080
|
+
const pivotTokens = Number(result?.state?.totalTokens);
|
|
2081
|
+
if (Number.isFinite(pivotTokens) && pivotTokens > 0) {
|
|
2082
|
+
// 2026-10-13: 会话累计 (多轮对话刻度单调增长; 单轮 totalTokens 只是本轮)
|
|
2083
|
+
this.pivotCumulativeTokens += pivotTokens;
|
|
2084
|
+
const cm = (0, context_manager_js_1.getContextManager)();
|
|
2085
|
+
const usage = cm.updateUsage(this.pivotCumulativeTokens);
|
|
2086
|
+
// 上下文管理周期: 上报 → 阈值判断 → 触发压缩 (刻度有数之外, 到阈值真的会动)
|
|
2087
|
+
if (usage.stage === 'compressing' && !this._ctxCycleCompacting) {
|
|
2088
|
+
this._ctxCycleCompacting = true;
|
|
2089
|
+
void this.runContextCycle(usage).finally(() => { this._ctxCycleCompacting = false; });
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
catch { /* 上报失败不阻塞主流程 */ }
|
|
1953
2094
|
}
|
|
1954
2095
|
finally {
|
|
1955
2096
|
try {
|
|
@@ -1970,7 +2111,7 @@ ${await this.renderActivePlansSection()}
|
|
|
1970
2111
|
runId: this.actor.state.activeRun || undefined,
|
|
1971
2112
|
goalId: this.actor.state.goalBinding || undefined,
|
|
1972
2113
|
});
|
|
1973
|
-
console.log(`[PiAgent] reviewFinal (K4-B 恢复调用, 判定不参与流程):
|
|
2114
|
+
console.log(`[PiAgent] reviewFinal (K4-B 恢复调用, 判定不参与流程): kind=${reviewDecision?.kind} reason=${String(reviewDecision?.reason ?? '').slice(0, 60)}`);
|
|
1974
2115
|
}
|
|
1975
2116
|
catch { /* 审查异常不改变本次结论 (门面内部对"审查失效"自有留痕策略) */ }
|
|
1976
2117
|
if (result.response) {
|
|
@@ -2002,8 +2143,6 @@ ${await this.renderActivePlansSection()}
|
|
|
2002
2143
|
awaitingExternal = false;
|
|
2003
2144
|
/** 2026-09-16 (M3): 熔断原因 (同一工具连续失败达上限 → needs_human) */
|
|
2004
2145
|
breakerReason = '';
|
|
2005
|
-
/** 2026-09-16 (M3): 运行中核心写失败 (状态迁移等) —— 交给循环顶部硬闸, 不吞 */
|
|
2006
|
-
persistenceFailure = '';
|
|
2007
2146
|
/**
|
|
2008
2147
|
* 2026-09-16 (M2-B): Supervisor 开新 Run 继续同一 Goal 时注入"上一个 Run 已成功的非幂等动作"。
|
|
2009
2148
|
* 与 resume 的重放守卫同源 (run-store 的 buildContinuationPlan), 保证跨 Run 也不重复副作用。
|
|
@@ -2017,103 +2156,16 @@ ${await this.renderActivePlansSection()}
|
|
|
2017
2156
|
setGoalId(goalId) {
|
|
2018
2157
|
this.actor.state.goalBinding = String(goalId || '');
|
|
2019
2158
|
}
|
|
2020
|
-
/**
|
|
2021
|
-
* K10 ①: 控制面的**注入面** —— 写原语来自 run-store, 但调用一律经 `submitRunControl`
|
|
2022
|
-
* (与 `transportPorts()` 同一个模式: 换实现只改这一处, 上层不直接对 store 说话)。
|
|
2023
|
-
*/
|
|
2024
|
-
runControlPorts() {
|
|
2025
|
-
return {
|
|
2026
|
-
setRunStatus: run_store_js_1.setRunStatus,
|
|
2027
|
-
recordRecovery: run_store_js_1.recordRecovery,
|
|
2028
|
-
};
|
|
2029
|
-
}
|
|
2030
2159
|
/**
|
|
2031
2160
|
* 核心状态迁移的兜底: 失败 → 记降级 + 交给循环顶部的持久化硬闸 (不吞错)。
|
|
2032
2161
|
* K10 ①: 迁移动作经内核控制面; **口径不变** ——
|
|
2033
2162
|
* `{ok:false}` 且原因是"端口拒绝"(状态迁移不合法) ⇒ 只返回 false (那不是持久化故障);
|
|
2034
2163
|
* 未注入 / 抛错 ⇒ 才算持久化失败, 记降级 (与旧实现同款)。
|
|
2035
2164
|
*/
|
|
2036
|
-
async safeSetRunStatus(runId, to) {
|
|
2037
|
-
const out = await (0, control_js_1.submitRunControl)({ kind: 'set-run-status', origin: 'pi-session', runId, payload: { status: to } }, this.runControlPorts());
|
|
2038
|
-
if (out.ok)
|
|
2039
|
-
return true;
|
|
2040
|
-
const detail = String(out.detail || '未知原因');
|
|
2041
|
-
if (!detail.startsWith('端口拒绝')) {
|
|
2042
|
-
this.persistenceFailure = `状态迁移失败 (${to}): ${detail.slice(0, 180)}`;
|
|
2043
|
-
await (0, run_store_js_1.recordDegradation)({ kind: 'core', op: 'pi-sdk.setRunStatus', runId, message: this.persistenceFailure }).catch(() => { });
|
|
2044
|
-
}
|
|
2045
|
-
return false;
|
|
2046
|
-
}
|
|
2047
2165
|
/**
|
|
2048
2166
|
* 2026-09-16 (M3): 工具失败接线 —— 分类 → recovery 留痕 → 熔断 / 外部等待。
|
|
2049
2167
|
* 这是"错误恢复闭环"的运行时接线点 (此前 recordRecovery / repeatedFailureCount 只是数据结构)。
|
|
2050
2168
|
*/
|
|
2051
|
-
async wireToolFailure(tool, errorText, args) {
|
|
2052
|
-
if (!this.actor.state.activeRun)
|
|
2053
|
-
return;
|
|
2054
|
-
const cls = (0, run_store_js_1.classifyError)(errorText);
|
|
2055
|
-
const digest = (0, run_store_js_1.argsDigestOf)(args);
|
|
2056
|
-
let repeats = 1;
|
|
2057
|
-
try {
|
|
2058
|
-
const rec = await (0, run_store_js_1.readRun)(this.actor.state.activeRun);
|
|
2059
|
-
// recordStep 已经把**这次**失败记进去了, 所以"连续失败次数"直接就是当前值 (不再 +1)
|
|
2060
|
-
const seen = rec ? (0, run_store_js_1.repeatedFailureCount)(rec, tool, digest) : 0;
|
|
2061
|
-
repeats = Math.max(seen, 1);
|
|
2062
|
-
}
|
|
2063
|
-
catch { /* 读不到就按首次记 */ }
|
|
2064
|
-
const action = cls === 'transient' ? 'backoff'
|
|
2065
|
-
: cls === 'auth' ? 'escalate'
|
|
2066
|
-
: cls === 'external_no_reply' ? 'pause'
|
|
2067
|
-
: 'retry';
|
|
2068
|
-
try {
|
|
2069
|
-
await (0, run_store_js_1.recordRecovery)(this.actor.state.activeRun, {
|
|
2070
|
-
errorClass: cls,
|
|
2071
|
-
message: `${tool}: ${errorText}`.slice(0, 200),
|
|
2072
|
-
action,
|
|
2073
|
-
attempt: repeats,
|
|
2074
|
-
recovered: false,
|
|
2075
|
-
});
|
|
2076
|
-
}
|
|
2077
|
-
catch (err) {
|
|
2078
|
-
await (0, run_store_js_1.recordDegradation)({ kind: 'core', op: 'pi-sdk.recordRecovery', runId: this.actor.state.activeRun, message: String(err?.message || err) }).catch(() => { });
|
|
2079
|
-
}
|
|
2080
|
-
// 外部无响应 → awaiting_external (不算失败; 等回话后回到 running)
|
|
2081
|
-
if (cls === 'external_no_reply') {
|
|
2082
|
-
this.awaitingExternal = true;
|
|
2083
|
-
await this.safeSetRunStatus(this.actor.state.activeRun, 'awaiting_external');
|
|
2084
|
-
// 2026-09-16 (2-C.4): 把"在等什么"写成持久化事实 (来源/关联/过期), 否则真实回包到了也不知道该唤醒谁。
|
|
2085
|
-
try {
|
|
2086
|
-
if (this.actor.state.goalBinding) {
|
|
2087
|
-
const { bindExternalWait, newContinuationId, defaultWaitExpiry } = await Promise.resolve().then(() => __importStar(require('./external-events.js')));
|
|
2088
|
-
const isDelegate = /delegate/i.test(String(tool || ''));
|
|
2089
|
-
await bindExternalWait(this.actor.state.goalBinding, {
|
|
2090
|
-
requestId: this.pendingExternalRequestId || `${this.actor.state.activeRun}:${Date.now().toString(36)}`,
|
|
2091
|
-
continuationId: newContinuationId(this.actor.state.goalBinding),
|
|
2092
|
-
expectedSource: isDelegate ? 'delegate' : 'p2p',
|
|
2093
|
-
expectedEvent: 'result',
|
|
2094
|
-
createdAt: new Date().toISOString(),
|
|
2095
|
-
expiresAt: defaultWaitExpiry(Date.now(), Number(process.env.BOLLOON_EXTERNAL_WAIT_MS || 30 * 60_000)),
|
|
2096
|
-
note: `${isDelegate ? 'delegate 回包' : 'P2P 协作回复'} 等待中 (tool=${tool})`,
|
|
2097
|
-
});
|
|
2098
|
-
}
|
|
2099
|
-
}
|
|
2100
|
-
catch (e) {
|
|
2101
|
-
await (0, run_store_js_1.recordDegradation)({ kind: 'core', op: 'pi-sdk.bindExternalWait', runId: this.actor.state.activeRun, message: String(e?.message || e) }).catch(() => { });
|
|
2102
|
-
}
|
|
2103
|
-
return;
|
|
2104
|
-
}
|
|
2105
|
-
// 鉴权类: 不重试, 直接交人
|
|
2106
|
-
if (cls === 'auth') {
|
|
2107
|
-
this.breakerReason = `鉴权类错误不重试 (${tool}): ${errorText.slice(0, 120)}`;
|
|
2108
|
-
await this.safeSetRunStatus(this.actor.state.activeRun, 'needs_human');
|
|
2109
|
-
return;
|
|
2110
|
-
}
|
|
2111
|
-
// 重复失败熔断: 同一工具 + 同一组参数连续失败达 3 次
|
|
2112
|
-
if (repeats >= MAX_SAME_TOOL_FAILURES) {
|
|
2113
|
-
this.breakerReason = `同一工具 ${tool} 连续失败 ${repeats} 次 (${cls}) → 熔断, 不再重试`;
|
|
2114
|
-
await this.safeSetRunStatus(this.actor.state.activeRun, 'needs_human');
|
|
2115
|
-
}
|
|
2116
|
-
}
|
|
2117
2169
|
getRunId() {
|
|
2118
2170
|
return this.actor.state.activeRun;
|
|
2119
2171
|
}
|
|
@@ -2286,26 +2338,6 @@ ${await this.renderActivePlansSection()}
|
|
|
2286
2338
|
}
|
|
2287
2339
|
return { outputs, success: allSuccess };
|
|
2288
2340
|
}
|
|
2289
|
-
buildContext() {
|
|
2290
|
-
// P1 接入: 同步跑前 3 层压缩 (Budget Reduction / Snip / Microcompact)
|
|
2291
|
-
// 异步层 (Context Collapse / Auto-Compact) 在 promptStream 入口处单独跑 (用 LLM)
|
|
2292
|
-
// 失败静默: 任何 stage 抛错 → 走老 slice(-10) 逻辑
|
|
2293
|
-
//
|
|
2294
|
-
// P1.2: 如果 maybeAutoCompact 算过 Context Collapse 投影, 用 this.projectedHistory (读时投影, 非破坏)
|
|
2295
|
-
const source = this.projectedHistory ?? this.actor.state.messageHistory;
|
|
2296
|
-
const recentMessages = this.compressHistorySync(source).slice(-10);
|
|
2297
|
-
return recentMessages.map(m => {
|
|
2298
|
-
if (m.role === 'user')
|
|
2299
|
-
return `用户: ${m.content}`;
|
|
2300
|
-
if (m.role === 'assistant')
|
|
2301
|
-
return `助手: ${m.content}`;
|
|
2302
|
-
if (m.role === 'tool') {
|
|
2303
|
-
const result = m.toolResult ? JSON.stringify(m.toolResult) : m.content;
|
|
2304
|
-
return `工具结果: ${result}`;
|
|
2305
|
-
}
|
|
2306
|
-
return m.content;
|
|
2307
|
-
}).join('\n');
|
|
2308
|
-
}
|
|
2309
2341
|
/**
|
|
2310
2342
|
* M3.5 (2026-06-17): 把 history 转成 messages 数组, 给 llm.chat() 用.
|
|
2311
2343
|
* 不再用 buildContext() 把所有 role 压成字符串 — LLM 看不到 tool 调用结果.
|
|
@@ -2314,101 +2346,11 @@ ${await this.renderActivePlansSection()}
|
|
|
2314
2346
|
* 取最近 N 条, 同步压缩前 3 层 (跟 buildContext 同步).
|
|
2315
2347
|
* 跳过 projectedHistory 路径 — messages 数组必须真实, 不能用投影.
|
|
2316
2348
|
*/
|
|
2317
|
-
buildMessages() {
|
|
2318
|
-
try {
|
|
2319
|
-
// 2026-08-06: 来源优先用 projectedHistory (Context Collapse 投影, 非破坏) —
|
|
2320
|
-
// 与 buildContext 一致; 之前只让字符串路径用投影, messages 数组路径被跳过,
|
|
2321
|
-
// 导致 LLM 实际看到的还是未压缩的历史.
|
|
2322
|
-
const source = this.projectedHistory ?? this.actor.state.messageHistory;
|
|
2323
|
-
const WINDOW = 15;
|
|
2324
|
-
const out = [];
|
|
2325
|
-
// 2026-09-28 (前缀 KV 可命中): 带 CURRENT TURN 注入的条目要**原样回带**.
|
|
2326
|
-
// 两个理由: (1) 注入块是"当前轮的前缀", 重渲染 (重加 `[工具结果]` 前缀 / 再 slice 截断)
|
|
2327
|
-
// 会改字节 → 下一轮前缀从这条起分叉; (2) 已注入的 tool 条目在 wire 上已经是 user 形状,
|
|
2328
|
-
// 再套一次前缀就是双前缀, 连内容都变了.
|
|
2329
|
-
const isInjectedEntry = (m) => typeof m.content === 'string' && m.content.includes(pi_ai_js_1.CURRENT_TURN_MARKER);
|
|
2330
|
-
// 早期历史压缩: 超过窗口时, 不直接丢弃 — 提取前段用户意图摘要注入 (同步, 无 LLM).
|
|
2331
|
-
// 结构对齐 Context OS: System Prompt(persona) + 压缩摘要 + 最近消息.
|
|
2332
|
-
if (source.length > WINDOW) {
|
|
2333
|
-
const early = source.slice(0, source.length - WINDOW);
|
|
2334
|
-
const slice = source.slice(-WINDOW);
|
|
2335
|
-
const earlyUsers = early.filter(m => m.role === 'user' && (m.content || '').trim());
|
|
2336
|
-
const earlyTools = early.filter(m => m.role === 'tool').length;
|
|
2337
|
-
const earlyAssist = early.filter(m => m.role === 'assistant' && (m.content || '').trim()).length;
|
|
2338
|
-
const snippet = earlyUsers.slice(-5).map(m => `- ${(m.content || '').slice(0, 120).replace(/\n/g, ' ')}`).join('\n') || '- (早期对话无用户文本)';
|
|
2339
|
-
out.push({
|
|
2340
|
-
role: 'system',
|
|
2341
|
-
content: `[上下文压缩] 早期 ${early.length} 条消息已压缩 (用户 ${earlyUsers.length} 条 / AI ${earlyAssist} 条 / 工具结果 ${earlyTools} 条). 关键用户意图摘要:\n${snippet}\n[压缩结束] 以下是最近消息:`,
|
|
2342
|
-
});
|
|
2343
|
-
for (const m of slice) {
|
|
2344
|
-
const r = m.role;
|
|
2345
|
-
if (isInjectedEntry(m)) {
|
|
2346
|
-
out.push({ role: 'user', content: String(m.content) });
|
|
2347
|
-
continue;
|
|
2348
|
-
}
|
|
2349
|
-
if (r === 'tool') {
|
|
2350
|
-
out.push({ role: 'user', content: `[工具结果]\n${(m.content || '').slice(0, 2000)}` });
|
|
2351
|
-
continue;
|
|
2352
|
-
}
|
|
2353
|
-
if (r === 'assistant') {
|
|
2354
|
-
out.push({ role: 'assistant', content: (m.content || '').slice(0, 4000), reasoningContent: m.reasoningContent });
|
|
2355
|
-
continue;
|
|
2356
|
-
}
|
|
2357
|
-
if (r === 'user') {
|
|
2358
|
-
out.push({ role: 'user', content: (m.content || '').slice(0, 2000) });
|
|
2359
|
-
continue;
|
|
2360
|
-
}
|
|
2361
|
-
if (r === 'system') {
|
|
2362
|
-
out.push({ role: 'system', content: (m.content || '').slice(0, 2000) });
|
|
2363
|
-
}
|
|
2364
|
-
}
|
|
2365
|
-
return out;
|
|
2366
|
-
}
|
|
2367
|
-
// 窗口内: 原逻辑 (tool 转 user role, 避免 tool_calls 配对)
|
|
2368
|
-
const slice = source.slice(-WINDOW);
|
|
2369
|
-
for (const m of slice) {
|
|
2370
|
-
const r = m.role;
|
|
2371
|
-
if (isInjectedEntry(m)) {
|
|
2372
|
-
out.push({ role: 'user', content: String(m.content) });
|
|
2373
|
-
continue;
|
|
2374
|
-
}
|
|
2375
|
-
if (r === 'tool') {
|
|
2376
|
-
out.push({ role: 'user', content: `[工具结果]\n${m.content || ''}` });
|
|
2377
|
-
continue;
|
|
2378
|
-
}
|
|
2379
|
-
if (r === 'assistant') {
|
|
2380
|
-
out.push({ role: 'assistant', content: m.content || '', reasoningContent: m.reasoningContent });
|
|
2381
|
-
continue;
|
|
2382
|
-
}
|
|
2383
|
-
if (r === 'user') {
|
|
2384
|
-
out.push({ role: 'user', content: m.content || '' });
|
|
2385
|
-
}
|
|
2386
|
-
if (r === 'system') {
|
|
2387
|
-
out.push({ role: 'system', content: m.content || '' });
|
|
2388
|
-
}
|
|
2389
|
-
}
|
|
2390
|
-
return out;
|
|
2391
|
-
}
|
|
2392
|
-
catch (err) {
|
|
2393
|
-
console.warn('[PiAgent] buildMessages failed (silent, falling back to text):', err);
|
|
2394
|
-
// 退化: 用 buildContext 字符串包装成单 user message
|
|
2395
|
-
return [{ role: 'user', content: this.buildContext() }];
|
|
2396
|
-
}
|
|
2397
|
-
}
|
|
2398
2349
|
/**
|
|
2399
2350
|
* 2026-09-28 (前缀 KV 可命中): 把 chat() 回带的当前轮写回自己的 messageHistory.
|
|
2400
2351
|
* 逻辑在模块级 `writeBackCurrentTurnInto` (纯函数, 门可以直接驱它断言);
|
|
2401
2352
|
* 这里只负责接到 `actor 的 messageHistory` 上, 并保证任何异常都静默 (写回失败不影响对话).
|
|
2402
2353
|
*/
|
|
2403
|
-
writeBackCurrentTurn(wire) {
|
|
2404
|
-
try {
|
|
2405
|
-
return writeBackCurrentTurnInto(this.actor.state.messageHistory, wire);
|
|
2406
|
-
}
|
|
2407
|
-
catch (err) {
|
|
2408
|
-
console.warn('[PiAgent] writeBackCurrentTurn failed (silent):', err);
|
|
2409
|
-
return 0;
|
|
2410
|
-
}
|
|
2411
|
-
}
|
|
2412
2354
|
/**
|
|
2413
2355
|
* 估算 messageHistory 的 token 数 (4 字符 ≈ 1 token, 与 context-compaction 同步).
|
|
2414
2356
|
* 失败静默: 任何异常 → 0 (不阻塞)
|
|
@@ -2430,142 +2372,6 @@ ${await this.renderActivePlansSection()}
|
|
|
2430
2372
|
*
|
|
2431
2373
|
* 失败静默: 全部失败 → 返回空 reply, 让上层 no-tool_use 终止
|
|
2432
2374
|
*/
|
|
2433
|
-
async callLlmWithRecovery(llm, contextOrMessages, systemPrompt, signal, onStream, tools,
|
|
2434
|
-
/** 2026-09-28: 当前轮易变段 (循环进度/改进提示) —— 进 CURRENT TURN 区, 不进 system */
|
|
2435
|
-
currentTurnContext) {
|
|
2436
|
-
// Reactive compaction 预检: 估算 token 超 80% 阈值, 跑一次
|
|
2437
|
-
const estimated = this.estimateHistoryTokens();
|
|
2438
|
-
if (estimated > this.maxContextTokens() * 0.8) {
|
|
2439
|
-
console.warn(`[PiAgent] reactive compaction pre-check (${estimated} tokens > 80% threshold)`);
|
|
2440
|
-
onStream?.({ type: 'status', internal: true, content: '⚠️ reactive compaction 预检触发', tool: 'recovery' });
|
|
2441
|
-
try {
|
|
2442
|
-
// 同步压缩: 取快照与替换在**同一拍相邻两行** (中间没有 await) ⇒ 不存在"变换期间被追加"的窗口。
|
|
2443
|
-
// 若哪天这里插入 await, 必须改成 `actor.rebaseHistory(…, snapshotLen)` (见 channel-actor.ts)。
|
|
2444
|
-
const compacted = this.compressHistorySync(this.actor.state.messageHistory);
|
|
2445
|
-
this.replaceHistory(compacted);
|
|
2446
|
-
if (this.estimateHistoryTokens() > this.maxContextTokens() * 0.8) {
|
|
2447
|
-
await this.maybeAutoCompact(onStream, signal);
|
|
2448
|
-
}
|
|
2449
|
-
}
|
|
2450
|
-
catch (err) {
|
|
2451
|
-
console.warn('[PiAgent] reactive compaction pre-check failed:', err);
|
|
2452
|
-
}
|
|
2453
|
-
}
|
|
2454
|
-
// 错误分级 (M1.3, 2026-06-17):
|
|
2455
|
-
// - 401/403/400 (认证/请求错误): 不重试, 直接 fail-fast
|
|
2456
|
-
// - 429 (rate limit): 重试 2 次, 指数退避
|
|
2457
|
-
// - 5xx (上游错误): 重试 2 次, 指数退避
|
|
2458
|
-
// - network (ECONNRESET / fetch failed / abort/timeout): 重试 2 次
|
|
2459
|
-
// - 4xx prompt-too-long: 走 reactive compaction
|
|
2460
|
-
// 这样以前所有错误都触发整个 runReActLoop 重跑(浪费 token),现在 4xx 直接失败
|
|
2461
|
-
// 让上层把失败原因广播给用户,而不是闷在 loop 里 retry 3 次后给空回复
|
|
2462
|
-
const classifyError = (err) => {
|
|
2463
|
-
const msg = String(err?.message || err || '');
|
|
2464
|
-
// 401/403: 认证失败
|
|
2465
|
-
if (/401|unauthor|invalid api key|api_key|forbidden|403/i.test(msg))
|
|
2466
|
-
return 'auth';
|
|
2467
|
-
// 400 prompt-too-long
|
|
2468
|
-
if (/token|too long|exceed|length|context|4000|413/i.test(msg))
|
|
2469
|
-
return 'prompt_too_long';
|
|
2470
|
-
// 429 rate limit
|
|
2471
|
-
if (/429|rate.?limit|too many requests/i.test(msg))
|
|
2472
|
-
return 'rate_limit';
|
|
2473
|
-
// 5xx
|
|
2474
|
-
if (/5\d\d|internal server|bad gateway|service unavailable|gateway timeout|cloudflare|502|503|504/i.test(msg))
|
|
2475
|
-
return 'server';
|
|
2476
|
-
// network
|
|
2477
|
-
if (/econnreset|econnrefused|enotfound|etimedout|fetch failed|network|aborted|timeout/i.test(msg))
|
|
2478
|
-
return 'network';
|
|
2479
|
-
return 'other';
|
|
2480
|
-
};
|
|
2481
|
-
const isRetryable = (cls) => cls === 'rate_limit' || cls === 'server' || cls === 'network' || cls === 'prompt_too_long';
|
|
2482
|
-
const maxAttempts = (cls) => isRetryable(cls) ? 3 : 1;
|
|
2483
|
-
const backoffMs = (attempt) => Math.min(1000 * 2 ** attempt, 8000); // 1s, 2s, 4s, 8s cap
|
|
2484
|
-
let lastErr = null;
|
|
2485
|
-
let lastClass = 'other';
|
|
2486
|
-
for (let attempt = 0; attempt < 4; attempt++) { // 最多 4 次尝试
|
|
2487
|
-
try {
|
|
2488
|
-
// M3.5 (2026-06-17): 传 messages 数组 (如果 contextOrMessages 是数组) 或字符串
|
|
2489
|
-
// 数组版让 LLM 看到结构化的 user/assistant/tool role, 而不是把 history 拼成单字符串
|
|
2490
|
-
// Bug 5: pass tool IDs for native OpenAI tool calling
|
|
2491
|
-
// 2026-09-28 (前缀 KV 可命中): 显式 purpose/source —— pi-sdk 的 ReAct 循环**就是主对话**
|
|
2492
|
-
// (完整 IMMUTABLE PREFIX + 工具全集 + CURRENT TURN, 也只有它带 cache_prompt);
|
|
2493
|
-
// currentTurnContext 是调用方自己的当前轮易变段 (循环进度/改进提示), 走 CURRENT TURN 区.
|
|
2494
|
-
// 2026-09-28: 回带最终 wire messages (含注入后的当前轮) → 上层据此写回 messageHistory.
|
|
2495
|
-
const response = await llm.chat(contextOrMessages, systemPrompt, signal, tools, 'main-agent', 'react-loop', undefined, currentTurnContext);
|
|
2496
|
-
// 2026-10-01 (用户: 「思考的记录可以也显示出来吗」): 思考模型的思维链原先只被"存起来回带",
|
|
2497
|
-
// **不显示** ✗。这里把它往流里送一份 —— 显示与否由 CLI 侧决定(BOLLOON_SHOW_THINKING, 默认显示) ✓。
|
|
2498
|
-
try {
|
|
2499
|
-
const rc = String(response?.reasoningContent || '').trim();
|
|
2500
|
-
if (rc)
|
|
2501
|
-
this.runCtx.eventSink?.({ type: 'reasoning', content: rc });
|
|
2502
|
-
}
|
|
2503
|
-
catch { /* 送不出去不影响主流程 */ } // 2026-06-30: 透传 toolCalls (OpenAI 协议 native) 给上层, 让 assistant message 能 emit 真 id
|
|
2504
|
-
return { reply: response.reply || '', toolCalls: response.toolCalls, messages: response.messages };
|
|
2505
|
-
}
|
|
2506
|
-
catch (err) {
|
|
2507
|
-
// 用户主动 abort: 不重试, 立即抛
|
|
2508
|
-
if (signal?.aborted || err?.name === 'AbortError')
|
|
2509
|
-
throw err;
|
|
2510
|
-
lastErr = err;
|
|
2511
|
-
lastClass = classifyError(err);
|
|
2512
|
-
const errMsg = String(err?.message || err || '').slice(0, 200);
|
|
2513
|
-
const attempts = maxAttempts(lastClass);
|
|
2514
|
-
if (attempt + 1 >= attempts) {
|
|
2515
|
-
console.warn(`[PiAgent] LLM 调用失败, 不再重试 (class=${lastClass}, attempt=${attempt + 1}/${attempts}): ${errMsg}`);
|
|
2516
|
-
break;
|
|
2517
|
-
}
|
|
2518
|
-
console.warn(`[PiAgent] LLM 调用失败 (class=${lastClass}, attempt=${attempt + 1}/${attempts}), ${backoffMs(attempt)}ms 后重试: ${errMsg}`);
|
|
2519
|
-
onStream?.({ type: 'status', content: `⚠️ LLM 调用失败 (${lastClass}), 重试 ${attempt + 2}/${attempts}...`, tool: 'recovery' });
|
|
2520
|
-
if (lastClass === 'prompt_too_long') {
|
|
2521
|
-
try {
|
|
2522
|
-
await this.maybeAutoCompact(onStream, signal);
|
|
2523
|
-
}
|
|
2524
|
-
catch (compactionErr) {
|
|
2525
|
-
console.warn('[PiAgent] reactive compaction on prompt-too-long failed:', compactionErr);
|
|
2526
|
-
}
|
|
2527
|
-
// 重新生成 context (重试 prompt_too_long 时重建 messages — 包含压缩后的 history)
|
|
2528
|
-
if (Array.isArray(contextOrMessages)) {
|
|
2529
|
-
contextOrMessages = this.buildMessages();
|
|
2530
|
-
}
|
|
2531
|
-
else {
|
|
2532
|
-
contextOrMessages = this.buildContext();
|
|
2533
|
-
}
|
|
2534
|
-
}
|
|
2535
|
-
else if (errMsg.includes('insufficient tool messages') || errMsg.includes('must be followed by tool messages')) {
|
|
2536
|
-
// 2026-07-29: 特殊的 400 错误 — tool_calls 配对异常, 降级为纯文本 context
|
|
2537
|
-
console.warn('[PiAgent] insufficient tool messages — 降级为 buildContext 文本');
|
|
2538
|
-
if (Array.isArray(contextOrMessages)) {
|
|
2539
|
-
contextOrMessages = this.buildContext();
|
|
2540
|
-
// 也清除最近一轮的 toolCalls, 防止再触发
|
|
2541
|
-
if (this.actor.state.messageHistory.length > 1) {
|
|
2542
|
-
const last = this.actor.state.messageHistory[this.actor.state.messageHistory.length - 1];
|
|
2543
|
-
if (last.role === 'assistant' && last.toolCalls) {
|
|
2544
|
-
delete last.toolCalls;
|
|
2545
|
-
}
|
|
2546
|
-
}
|
|
2547
|
-
}
|
|
2548
|
-
}
|
|
2549
|
-
else {
|
|
2550
|
-
// 指数退避
|
|
2551
|
-
await new Promise((r) => setTimeout(r, backoffMs(attempt)));
|
|
2552
|
-
}
|
|
2553
|
-
}
|
|
2554
|
-
}
|
|
2555
|
-
// 失败: 返回结构化错误 reply (而不是空字符串), 上层可识别 + UI 可显示
|
|
2556
|
-
const errMsg = String(lastErr?.message || lastErr || '').slice(0, 300);
|
|
2557
|
-
const userMsg = lastClass === 'auth'
|
|
2558
|
-
? `[AI 服务调用失败] 认证错误: ${errMsg}\n请检查 API key 配置 (env: OPENAI_API_KEY / ANTHROPIC_API_KEY 等)`
|
|
2559
|
-
: lastClass === 'rate_limit'
|
|
2560
|
-
? `[AI 服务调用失败] 上游限流 (429): ${errMsg}\n请稍后重试`
|
|
2561
|
-
: lastClass === 'server'
|
|
2562
|
-
? `[AI 服务调用失败] 上游错误: ${errMsg}\n已重试 2 次仍失败, 可稍后重试`
|
|
2563
|
-
: lastClass === 'network'
|
|
2564
|
-
? `[AI 服务调用失败] 网络错误: ${errMsg}\n请检查网络连接`
|
|
2565
|
-
: `[AI 服务调用失败] ${errMsg}`;
|
|
2566
|
-
console.warn(`[PiAgent] callLlmWithRecovery 全部失败 (class=${lastClass}): ${errMsg}`);
|
|
2567
|
-
return { reply: userMsg };
|
|
2568
|
-
}
|
|
2569
2375
|
/**
|
|
2570
2376
|
* 同步压缩: 跑前 3 层 (Budget Reduction / Snip / Microcompact).
|
|
2571
2377
|
* Context Collapse / Auto-Compact 是 async, 不在 buildContext 同步链里跑.
|
|
@@ -2687,91 +2493,7 @@ ${await this.renderActivePlansSection()}
|
|
|
2687
2493
|
cm.updateUsage(beforeTokens);
|
|
2688
2494
|
}
|
|
2689
2495
|
}
|
|
2690
|
-
isFinalResponse(content) {
|
|
2691
|
-
// 2026-06-30: 抽到 ./parse-tool-call.ts 作为纯函数 — 这里只构建 ctx 并调用
|
|
2692
|
-
return (0, parse_tool_call_js_1.isFinalResponse)(content, this._parseCtx());
|
|
2693
|
-
}
|
|
2694
|
-
extractFinalAnswer(content) {
|
|
2695
|
-
// 抽取实现已挪到 ./parse-tool-call.ts (纯函数, 易测)
|
|
2696
|
-
return (0, parse_tool_call_js_1.extractFinalAnswer)(content);
|
|
2697
|
-
}
|
|
2698
|
-
_parseCtx() {
|
|
2699
|
-
return {
|
|
2700
|
-
tools: new Set(Array.from(this.tools.keys())),
|
|
2701
|
-
resolveAlias: (name) => this.resolveToolName(name),
|
|
2702
|
-
};
|
|
2703
|
-
}
|
|
2704
|
-
parseToolCall(content) {
|
|
2705
|
-
// 2026-06-30: 抽到 ./parse-tool-call.ts 作为纯函数 — 这里只构建 ctx 并调用
|
|
2706
|
-
return (0, parse_tool_call_js_1.parseToolCall)(content, this._parseCtx());
|
|
2707
|
-
}
|
|
2708
|
-
// [debug-2026-06-19] 临时: 打印 parseToolCall 输入和返回
|
|
2709
|
-
_dbgParseToolCall(content) {
|
|
2710
|
-
const r = this.parseToolCall(content);
|
|
2711
|
-
console.log('[DBG parseToolCall] result:', JSON.stringify(r), 'content head:', JSON.stringify(content.substring(0, 200)));
|
|
2712
|
-
return r;
|
|
2713
|
-
}
|
|
2714
|
-
/**
|
|
2715
|
-
* 2026-06-19: 工具名大小写不敏感 + Claude Code 风格别名映射
|
|
2716
|
-
|
|
2717
|
-
/**
|
|
2718
|
-
* 2026-06-19: 工具名大小写不敏感 + Claude Code 风格别名映射
|
|
2719
|
-
* LLM 实际产出 Read/Edit/Write/Bash/Grep/Glob 等大写名 (Claude Code 工具命名)
|
|
2720
|
-
* bolloon 注册的是 read_document / edit_file / write_file / shell_exec / list_files
|
|
2721
|
-
* 返回 this.tools 里的标准名, 或 null 表示未识别
|
|
2722
|
-
*/
|
|
2723
|
-
/**
|
|
2724
|
-
* 把 LLM 给的工具名 (可能大小写不一, 或者 Claude Code 风格的别名) 解析为
|
|
2725
|
-
* bolloon 注册的标准工具名.
|
|
2726
|
-
*
|
|
2727
|
-
* 2026-06-30: 委托给 ToolRegistry 模块 — alias 表在 tool-registry.ts 统一维护,
|
|
2728
|
-
* 这里只做 thin wrapper 保留 backward compat (private API 但其它地方可能用).
|
|
2729
|
-
*/
|
|
2730
|
-
resolveToolName(name) {
|
|
2731
|
-
return this._toolRegistry.resolve(name);
|
|
2732
|
-
}
|
|
2733
|
-
estimateResponseQuality(response) {
|
|
2734
|
-
let score = 0.5;
|
|
2735
|
-
if (response.length > 50)
|
|
2736
|
-
score += 0.1;
|
|
2737
|
-
if (response.length > 200)
|
|
2738
|
-
score += 0.1;
|
|
2739
|
-
if (response.length < 20)
|
|
2740
|
-
score -= 0.3;
|
|
2741
|
-
if (response.includes('\n'))
|
|
2742
|
-
score += 0.1;
|
|
2743
|
-
if (response.includes('-') || response.includes('•'))
|
|
2744
|
-
score += 0.05;
|
|
2745
|
-
if (response.includes('```'))
|
|
2746
|
-
score += 0.1;
|
|
2747
|
-
const conclusionWords = ['完成', '结果', '总结', '所以', '因此', '答案', '推荐'];
|
|
2748
|
-
if (conclusionWords.some(w => response.includes(w)))
|
|
2749
|
-
score += 0.1;
|
|
2750
|
-
if (response.includes('调用工具') || response.includes('tool('))
|
|
2751
|
-
score -= 0.2;
|
|
2752
|
-
return Math.max(0, Math.min(1, score));
|
|
2753
|
-
}
|
|
2754
|
-
estimateToolResultQuality(result) {
|
|
2755
|
-
let score = 0.5;
|
|
2756
|
-
if (!result.success)
|
|
2757
|
-
return 0.2;
|
|
2758
|
-
if (result.output) {
|
|
2759
|
-
score += 0.2;
|
|
2760
|
-
if (result.output.length > 50)
|
|
2761
|
-
score += 0.1;
|
|
2762
|
-
if (result.output.length < 10)
|
|
2763
|
-
score -= 0.1;
|
|
2764
|
-
if (result.output.includes('❌') || result.output.includes('error'))
|
|
2765
|
-
score -= 0.2;
|
|
2766
|
-
if (result.output.includes('✅') || result.output.includes('success'))
|
|
2767
|
-
score += 0.1;
|
|
2768
|
-
}
|
|
2769
|
-
if (result.error)
|
|
2770
|
-
score -= 0.3;
|
|
2771
|
-
return Math.max(0, Math.min(1, score));
|
|
2772
|
-
}
|
|
2773
2496
|
async handleFallback(input) {
|
|
2774
|
-
const lowerInput = input.toLowerCase();
|
|
2775
2497
|
const parts = input.trim().split(/\s+/);
|
|
2776
2498
|
const cmd = parts[0].toLowerCase();
|
|
2777
2499
|
const args = parts.slice(1).join(' ');
|
|
@@ -2854,7 +2576,6 @@ ${this.extractOperationsFromRef(operationsRef)}
|
|
|
2854
2576
|
}
|
|
2855
2577
|
extractOperationsFromRef(ref) {
|
|
2856
2578
|
const lines = ref.split('\n');
|
|
2857
|
-
const inOperationsSection = false;
|
|
2858
2579
|
const operationLines = [];
|
|
2859
2580
|
for (let i = 0; i < lines.length; i++) {
|
|
2860
2581
|
const line = lines[i];
|
|
@@ -3261,20 +2982,6 @@ ${this.extractOperationsFromRef(operationsRef)}
|
|
|
3261
2982
|
}
|
|
3262
2983
|
// ==================== Harness Integration ====================
|
|
3263
2984
|
operationLog = [];
|
|
3264
|
-
logToHarness(action, args, result) {
|
|
3265
|
-
if (!this.harnessEnabled || !this.harness)
|
|
3266
|
-
return;
|
|
3267
|
-
this.operationLog.push({
|
|
3268
|
-
timestamp: Date.now(),
|
|
3269
|
-
action,
|
|
3270
|
-
args,
|
|
3271
|
-
result,
|
|
3272
|
-
status: result.success ? 'ok' : 'error'
|
|
3273
|
-
});
|
|
3274
|
-
if (this.operationLog.length >= 10) {
|
|
3275
|
-
this.archiveToHarness();
|
|
3276
|
-
}
|
|
3277
|
-
}
|
|
3278
2985
|
archiveToHarness() {
|
|
3279
2986
|
if (!this.harnessEnabled || !this.harness || this.operationLog.length === 0)
|
|
3280
2987
|
return;
|