@bolloon/bolloon-agent 0.6.6 → 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/ha-daemon.js +85 -0
- package/dist/agents/ha-supervisor.js +146 -0
- package/dist/agents/memory-store.js +25 -0
- package/dist/agents/opportunity-match.js +15 -3
- package/dist/agents/pi-sdk-session-manager.js +8 -0
- package/dist/agents/pi-sdk.js +67 -494
- package/dist/bootstrap/context-collector.js +65 -3
- package/dist/bootstrap/persona-loader.js +26 -0
- package/dist/electron-build/agents/memory-store.js +26 -0
- package/dist/electron-build/agents/memory-store.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.js +65 -492
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/bootstrap/context-collector.js +66 -3
- package/dist/electron-build/bootstrap/context-collector.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/llm/pi-ai.js +16 -3
- 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/ios/bolloon-web.json +3 -3
- package/dist/ios/index.html +19 -4
- package/dist/ios/mobile-agent.js +267 -41
- package/dist/ios/mobile-contacts.js +0 -4
- package/dist/ios/mobile-core.js +806 -30
- package/dist/ios/mobile-memory.js +317 -0
- package/dist/ios/mobile.html +19 -4
- package/dist/ios/mobile.js +144 -30
- package/dist/ios/routes-contacts.js +94 -1
- package/dist/ios/routes-world.js +9 -1
- package/dist/ios/server.js +51 -40
- package/dist/ios/sw.js +1 -1
- package/dist/kernel/gate-scan.js +1 -1
- package/dist/kernel/ha/ha-arbiter.js +177 -0
- package/dist/kernel/ha/ha-guard.js +151 -0
- package/dist/kernel/index.js +3 -0
- package/dist/kernel/plan.js +5 -5
- package/dist/kernel/roster.js +3 -3
- package/dist/llm/pi-ai.js +16 -3
- package/dist/llm/system-prompt/registry.js +8 -6
- package/dist/web/bolloon-web.json +3 -3
- package/dist/web/mobile-agent.js +265 -44
- package/dist/web/mobile-core.js +681 -32
- package/dist/web/mobile-memory.js +317 -0
- package/dist/web/mobile.html +4 -4
- package/dist/web/mobile.js +36 -27
- package/dist/web/routes-world.js +9 -1
- package/dist/web/server.js +50 -39
- package/dist/web/sw.js +1 -1
- package/package.json +1 -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 留本类经端口注入
|
|
@@ -141,7 +139,6 @@ const index_js_2 = require("../context-compaction/index.js");
|
|
|
141
139
|
const react_harness_js_1 = require("../security/react-harness.js");
|
|
142
140
|
const hooks_engine_js_1 = require("../hooks/hooks-engine.js");
|
|
143
141
|
const deny_pipeline_js_1 = require("./deny-pipeline.js");
|
|
144
|
-
const parse_tool_call_js_1 = require("./parse-tool-call.js");
|
|
145
142
|
const session_store_js_1 = require("./session-store.js");
|
|
146
143
|
const tool_registry_js_1 = require("./tool-registry.js");
|
|
147
144
|
// PiSessionManager 已抽到 ./pi-sdk-session-manager.ts (2026-07-06)
|
|
@@ -151,7 +148,6 @@ const tool_registry_js_1 = require("./tool-registry.js");
|
|
|
151
148
|
* 同一工具 + 同一组参数连续失败上限 (2026-07-01 起) —— 触发两条互不替代的动作:
|
|
152
149
|
* 循环内: 注入 system 提示强制 LLM 收尾; 运行层 (M3): 熔断 → needs_human (交人)。
|
|
153
150
|
*/
|
|
154
|
-
const MAX_SAME_TOOL_FAILURES = 3;
|
|
155
151
|
/**
|
|
156
152
|
* 2026-09-28 (前缀 KV 可命中): 把 chat() 回带的「当前轮已注入」内容写回调用方自己的 history.
|
|
157
153
|
*
|
|
@@ -252,7 +248,6 @@ class PiAgentSession {
|
|
|
252
248
|
pivotCumulativeTokens = 0;
|
|
253
249
|
/** 2026-10-13: 上下文管理周期压缩在飞标志 (防一轮里重复触发) */
|
|
254
250
|
_ctxCycleCompacting = false;
|
|
255
|
-
workflows = new Map();
|
|
256
251
|
constraintLayer;
|
|
257
252
|
workflowEngine;
|
|
258
253
|
sessionManager;
|
|
@@ -380,7 +375,9 @@ class PiAgentSession {
|
|
|
380
375
|
}
|
|
381
376
|
return bg ? `${bg}\n${plans}` : plans;
|
|
382
377
|
}
|
|
378
|
+
/** M2.4 + 2026-10-15: persona section 缓存 (内容 + 源文件指纹)。指纹变了下一轮重建。 */
|
|
383
379
|
cachedPersonaSection = '';
|
|
380
|
+
cachedPersonaFingerprint = '';
|
|
384
381
|
/**
|
|
385
382
|
* 2026-10-13 (leo: 「重新阅读理解,整体开发整个技能的实现方式… 成整个加载循环」):
|
|
386
383
|
* 技能库索引摘要 —— 注入 systemPrompt, 让模型**知道技能存在**(名字+一句话描述),
|
|
@@ -416,17 +413,36 @@ class PiAgentSession {
|
|
|
416
413
|
return '';
|
|
417
414
|
}
|
|
418
415
|
}
|
|
419
|
-
/**
|
|
420
|
-
|
|
421
|
-
|
|
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
|
+
}
|
|
422
440
|
/** 最近一条用户消息 (按需子集档用来判 intent) */
|
|
423
441
|
lastUserIntent = '';
|
|
424
442
|
/** 本回合改过的 TS 文件 (回合收尾自动类型检查用) */
|
|
425
443
|
tsTouchedThisTurn = [];
|
|
426
444
|
/** 本回合是否已跑过类型检查 (一轮只跑一次) */
|
|
427
445
|
typecheckRanThisTurn = false;
|
|
428
|
-
iterBudget = null;
|
|
429
|
-
iterBudgetWarned = false;
|
|
430
446
|
/** 上一次已复盘的**任务签名** (签名变了 ⇒ 立刻复盘, 对应"每次任务都总结" ✓) */
|
|
431
447
|
lastReviewedTaskSig = undefined;
|
|
432
448
|
/** 上一次回合后自审的时间 (节流; 0 = 从未) */
|
|
@@ -435,45 +451,14 @@ class PiAgentSession {
|
|
|
435
451
|
_sessionStore;
|
|
436
452
|
/** 构造期间 fire-and-forget 任务的 promise — whenReady() 等它 */
|
|
437
453
|
_readyPromise = null;
|
|
438
|
-
// 2026-06-16 修: 父要求把 ReAct loop 上限放大到 "几乎无限", 靠自动压缩上下文 + fail-safe 兜底
|
|
439
|
-
// 默认 10000 — 正常任务永远跑不到, 但作为防 LLM 死循环 / 防 OOM 的最后一道闸
|
|
440
|
-
// 旧默认 100 写死导致中等复杂度任务 (10-50 个 tool call + 多步反思) 会被误杀
|
|
441
|
-
MAX_REACT_ITERATIONS = 10_000;
|
|
442
|
-
MAX_REFINE_ATTEMPTS = 3;
|
|
443
|
-
QUALITY_THRESHOLD = 0.6;
|
|
444
|
-
/** P1: 上下文溢出阈值 (单轮估算 token 数, 超过则强制终止防止 prompt-too-long)
|
|
445
|
-
* 2026-08-06: 从 ContextManager 动态读 (默认 1M, env MAX_CONTEXT_TOKENS 可调).
|
|
446
|
-
* 保留字段仅作降级兜底 (ContextManager 初始化失败时用 60K 老行为). */
|
|
447
|
-
MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD = 60_000; // fallback 60K tokens
|
|
448
|
-
/** 2026-08-06: 上下文窗口 (tokens) — 统一走 ContextManager 配置 (1M 默认). */
|
|
449
|
-
maxContextTokens() {
|
|
450
|
-
try {
|
|
451
|
-
const { getContextManager } = _piRequire('../bootstrap/context-manager.js');
|
|
452
|
-
const n = getContextManager().getConfig().maxTokens;
|
|
453
|
-
return Number.isFinite(n) && n > 0 ? n : this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD;
|
|
454
|
-
}
|
|
455
|
-
catch {
|
|
456
|
-
return this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD;
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
/** 2026-06-16 新增: 累计错误总数兜底 (不管是否同工具, 累计 N 次就强制退出)
|
|
460
|
-
* 防 LLM 轮换工具名绕开 MAX_SAME_TOOL_FAILURES 的死循环攻击 */
|
|
461
|
-
MAX_TOTAL_ERRORS = 20;
|
|
462
|
-
/** 2026-06-19: 记录 loop 内成功执行的工具结果, 失败退出时汇总给用户 */
|
|
463
|
-
successfulToolResults = [];
|
|
464
454
|
/** 2026-06-19: Agent Mesh 通信 — 本地 + 远端 inbox 缓存, 给 check_inbox 工具读 */
|
|
465
455
|
_inboxMessages = [];
|
|
466
|
-
/** 2026-06-16 新增: loop 内自动压缩触发阈值 (相对 60K 阈值的比例) */
|
|
467
|
-
LOOP_COMPACT_RATIO = 0.8;
|
|
468
|
-
/** P1: max output token 升级重试 (LLM 截断时重试, 最多 3 次) */
|
|
469
|
-
MAX_OUTPUT_TOKEN_ESCALATION_RETRIES = 3;
|
|
470
456
|
thinkingEngine = new constraint_runtime_1.DeepThinkingEngine(3);
|
|
471
457
|
coordinator = new constraint_runtime_1.AgentCoordinator(3);
|
|
472
458
|
harness = null;
|
|
473
459
|
harnessEnabled = false;
|
|
474
460
|
/** 8-gate + 4-guard 集中调度 (防越权 / 防 prompt 注入) */
|
|
475
461
|
reactHarness = new react_harness_js_1.ReactHarness();
|
|
476
|
-
usePivotLoop = true; // K4-B (2026-10-02): 默认切到 pivot —— 所有入口只跑**一套** loop (老 ReAct loop 待删)
|
|
477
462
|
pivotLoopConfig;
|
|
478
463
|
/** P2: 当前会话的 permission mode (每次 promptStream 入口解析) */
|
|
479
464
|
currentPermissionMode = 'default';
|
|
@@ -491,8 +476,6 @@ class PiAgentSession {
|
|
|
491
476
|
_hooks = new hooks_engine_js_1.HooksEngine();
|
|
492
477
|
/** 2026-07-29: Unified Deny-First Pipeline */
|
|
493
478
|
_denyPipeline = new deny_pipeline_js_1.DenyPipeline();
|
|
494
|
-
/** 2026-07-29: Snip + Context Collapse 启用标志 (默认启用) */
|
|
495
|
-
_enableSnipCollapse = true;
|
|
496
479
|
/** 注册一个或多个工具到拒绝列表 */
|
|
497
480
|
denyTool(...names) {
|
|
498
481
|
for (const name of names)
|
|
@@ -718,7 +701,6 @@ class PiAgentSession {
|
|
|
718
701
|
this.workflowEngine = new workflow_engine_js_1.WorkflowEngine(this.constraintLayer);
|
|
719
702
|
this.sessionManager = new pi_sdk_session_manager_js_2.PiSessionManager(this.identity.did, this.cwd, this.actor.state.agentId);
|
|
720
703
|
this.agentsManager = new heartbeat_js_1.DiscoveredAgentsManager();
|
|
721
|
-
this.usePivotLoop = config.usePivotLoop ?? true; // K4-B: 默认 true (K4-B 之前是 false ⇒ CLI 走老 loop, 与 web 分叉)
|
|
722
704
|
this.pivotLoopConfig = config.pivotLoopConfig;
|
|
723
705
|
this.initSession();
|
|
724
706
|
(0, p2p_document_tools_js_1.initDocumentReceiver)();
|
|
@@ -734,12 +716,6 @@ class PiAgentSession {
|
|
|
734
716
|
}
|
|
735
717
|
}
|
|
736
718
|
catch { /* env 读失败静默 */ }
|
|
737
|
-
// 2026-07-29: 从环境变量控制 Snip/Collapse
|
|
738
|
-
try {
|
|
739
|
-
if (process.env.BOLLOON_SNIP_COLLAPSE === '0')
|
|
740
|
-
this._enableSnipCollapse = false;
|
|
741
|
-
}
|
|
742
|
-
catch { /* 静默 */ }
|
|
743
719
|
// 2026-07-29: 从 ~/.bolloon/hooks.yaml 加载 hook 配置
|
|
744
720
|
// 失败静默 (无 hook 配置也正常)
|
|
745
721
|
try {
|
|
@@ -1035,7 +1011,7 @@ class PiAgentSession {
|
|
|
1035
1011
|
(0, pi_sdk_tools_js_1.registerWalletTools)(toolCtx);
|
|
1036
1012
|
(0, pi_sdk_tools_js_1.setupInboxListener)(toolCtx);
|
|
1037
1013
|
// 镜像到 ToolRegistry (alias resolve 用)
|
|
1038
|
-
for (const [
|
|
1014
|
+
for (const [, tool] of this.tools.entries()) {
|
|
1039
1015
|
this._toolRegistry.register(tool);
|
|
1040
1016
|
}
|
|
1041
1017
|
// M3.3: 副作用工具走幂等性 cache
|
|
@@ -1047,9 +1023,6 @@ class PiAgentSession {
|
|
|
1047
1023
|
}
|
|
1048
1024
|
/** M3.3: 工具结果缓存 — 防止 loop 重试时副作用 (写文件 / 改代码) 执行多次 */
|
|
1049
1025
|
_idempotencyCache = new pi_sdk_tools_js_1.IdempotencyCache();
|
|
1050
|
-
async registerP2PDocumentReceiver() {
|
|
1051
|
-
await (0, p2p_document_tools_js_1.initDocumentReceiver)();
|
|
1052
|
-
}
|
|
1053
1026
|
getToolDefinitions() {
|
|
1054
1027
|
// M2.4 (2026-06-17): 缓存 tool 定义 — registerTools() 在构造时调一次, 此后不变
|
|
1055
1028
|
// 2026-07-29: 拒绝列表变化时清空缓存, 重新生成
|
|
@@ -1990,7 +1963,23 @@ class PiAgentSession {
|
|
|
1990
1963
|
catch (err) {
|
|
1991
1964
|
console.warn('[PiAgent] classifyIntent in pivot failed:', err);
|
|
1992
1965
|
}
|
|
1993
|
-
// 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
|
+
}
|
|
1994
1983
|
if (!this.cachedPersonaSection && this.persona) {
|
|
1995
1984
|
this.cachedPersonaSection = `
|
|
1996
1985
|
角色描述: ${this.persona.description || '无'}
|
|
@@ -2013,6 +2002,8 @@ ${await this.renderActivePlansSection()}
|
|
|
2013
2002
|
|
|
2014
2003
|
${await this.buildSkillsIndex()}
|
|
2015
2004
|
|
|
2005
|
+
${await this.buildStableMemoryBlock()}
|
|
2006
|
+
|
|
2016
2007
|
工作模式:
|
|
2017
2008
|
1. 理解用户自然语言请求
|
|
2018
2009
|
2. 分析需要哪些工具来完成
|
|
@@ -2045,16 +2036,32 @@ ${await this.buildSkillsIndex()}
|
|
|
2045
2036
|
// 2026-07-17 Bug 1 修: 注入 messageHistory (hydrateMessageHistory 从 session JSON 回灌的) 到 system prompt
|
|
2046
2037
|
// pivot loop execute() 内部自己维护 messageHistory, 跟 pi-sdk 的 actor 的 messageHistory 隔离,
|
|
2047
2038
|
// 不注入的话 LLM 看不到历史对话, 每次都是新对话.
|
|
2039
|
+
// 2026-10-16 (leo: prompt 比重降低, 流程由 kernel/意图驱动): 压缩历史注入 —
|
|
2040
|
+
// 20 条 × 2000 = 40K 上限吃满 systemPrompt; 历史只为给 pivot 首轮"上次做到哪"的锚,
|
|
2041
|
+
// 细节靠 contextHint (memory 摘要) 与工具补。8 条 × 300, 首尾保真。
|
|
2048
2042
|
const historyLines = [];
|
|
2049
|
-
const
|
|
2043
|
+
const histAll = this.actor.state.messageHistory;
|
|
2044
|
+
const historyToInject = histAll.slice(-8, -1);
|
|
2050
2045
|
for (const m of historyToInject) {
|
|
2051
2046
|
const roleLabel = m.role === 'user' ? '用户' : m.role === 'assistant' ? '你' : m.role === 'tool' ? '工具结果' : m.role;
|
|
2052
|
-
|
|
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);
|
|
2053
2053
|
if (text)
|
|
2054
2054
|
historyLines.push(`[${roleLabel}]: ${text}`);
|
|
2055
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
|
+
}
|
|
2056
2063
|
const historyBlock = historyLines.length > 0
|
|
2057
|
-
? `\n\n【历史对话 (最近 ${historyLines.length}
|
|
2064
|
+
? `\n\n【历史对话 (最近 ${historyLines.length} 条, 压缩摘要)】\n${historyLines.join('\n')}\n【历史对话结束】`
|
|
2058
2065
|
: '';
|
|
2059
2066
|
const onCompact = async () => {
|
|
2060
2067
|
// no-op (best-effort hook for future pi-sdk/pivot history sync)
|
|
@@ -2136,8 +2143,6 @@ ${await this.buildSkillsIndex()}
|
|
|
2136
2143
|
awaitingExternal = false;
|
|
2137
2144
|
/** 2026-09-16 (M3): 熔断原因 (同一工具连续失败达上限 → needs_human) */
|
|
2138
2145
|
breakerReason = '';
|
|
2139
|
-
/** 2026-09-16 (M3): 运行中核心写失败 (状态迁移等) —— 交给循环顶部硬闸, 不吞 */
|
|
2140
|
-
persistenceFailure = '';
|
|
2141
2146
|
/**
|
|
2142
2147
|
* 2026-09-16 (M2-B): Supervisor 开新 Run 继续同一 Goal 时注入"上一个 Run 已成功的非幂等动作"。
|
|
2143
2148
|
* 与 resume 的重放守卫同源 (run-store 的 buildContinuationPlan), 保证跨 Run 也不重复副作用。
|
|
@@ -2151,103 +2156,16 @@ ${await this.buildSkillsIndex()}
|
|
|
2151
2156
|
setGoalId(goalId) {
|
|
2152
2157
|
this.actor.state.goalBinding = String(goalId || '');
|
|
2153
2158
|
}
|
|
2154
|
-
/**
|
|
2155
|
-
* K10 ①: 控制面的**注入面** —— 写原语来自 run-store, 但调用一律经 `submitRunControl`
|
|
2156
|
-
* (与 `transportPorts()` 同一个模式: 换实现只改这一处, 上层不直接对 store 说话)。
|
|
2157
|
-
*/
|
|
2158
|
-
runControlPorts() {
|
|
2159
|
-
return {
|
|
2160
|
-
setRunStatus: run_store_js_1.setRunStatus,
|
|
2161
|
-
recordRecovery: run_store_js_1.recordRecovery,
|
|
2162
|
-
};
|
|
2163
|
-
}
|
|
2164
2159
|
/**
|
|
2165
2160
|
* 核心状态迁移的兜底: 失败 → 记降级 + 交给循环顶部的持久化硬闸 (不吞错)。
|
|
2166
2161
|
* K10 ①: 迁移动作经内核控制面; **口径不变** ——
|
|
2167
2162
|
* `{ok:false}` 且原因是"端口拒绝"(状态迁移不合法) ⇒ 只返回 false (那不是持久化故障);
|
|
2168
2163
|
* 未注入 / 抛错 ⇒ 才算持久化失败, 记降级 (与旧实现同款)。
|
|
2169
2164
|
*/
|
|
2170
|
-
async safeSetRunStatus(runId, to) {
|
|
2171
|
-
const out = await (0, control_js_1.submitRunControl)({ kind: 'set-run-status', origin: 'pi-session', runId, payload: { status: to } }, this.runControlPorts());
|
|
2172
|
-
if (out.ok)
|
|
2173
|
-
return true;
|
|
2174
|
-
const detail = String(out.detail || '未知原因');
|
|
2175
|
-
if (!detail.startsWith('端口拒绝')) {
|
|
2176
|
-
this.persistenceFailure = `状态迁移失败 (${to}): ${detail.slice(0, 180)}`;
|
|
2177
|
-
await (0, run_store_js_1.recordDegradation)({ kind: 'core', op: 'pi-sdk.setRunStatus', runId, message: this.persistenceFailure }).catch(() => { });
|
|
2178
|
-
}
|
|
2179
|
-
return false;
|
|
2180
|
-
}
|
|
2181
2165
|
/**
|
|
2182
2166
|
* 2026-09-16 (M3): 工具失败接线 —— 分类 → recovery 留痕 → 熔断 / 外部等待。
|
|
2183
2167
|
* 这是"错误恢复闭环"的运行时接线点 (此前 recordRecovery / repeatedFailureCount 只是数据结构)。
|
|
2184
2168
|
*/
|
|
2185
|
-
async wireToolFailure(tool, errorText, args) {
|
|
2186
|
-
if (!this.actor.state.activeRun)
|
|
2187
|
-
return;
|
|
2188
|
-
const cls = (0, run_store_js_1.classifyError)(errorText);
|
|
2189
|
-
const digest = (0, run_store_js_1.argsDigestOf)(args);
|
|
2190
|
-
let repeats = 1;
|
|
2191
|
-
try {
|
|
2192
|
-
const rec = await (0, run_store_js_1.readRun)(this.actor.state.activeRun);
|
|
2193
|
-
// recordStep 已经把**这次**失败记进去了, 所以"连续失败次数"直接就是当前值 (不再 +1)
|
|
2194
|
-
const seen = rec ? (0, run_store_js_1.repeatedFailureCount)(rec, tool, digest) : 0;
|
|
2195
|
-
repeats = Math.max(seen, 1);
|
|
2196
|
-
}
|
|
2197
|
-
catch { /* 读不到就按首次记 */ }
|
|
2198
|
-
const action = cls === 'transient' ? 'backoff'
|
|
2199
|
-
: cls === 'auth' ? 'escalate'
|
|
2200
|
-
: cls === 'external_no_reply' ? 'pause'
|
|
2201
|
-
: 'retry';
|
|
2202
|
-
try {
|
|
2203
|
-
await (0, run_store_js_1.recordRecovery)(this.actor.state.activeRun, {
|
|
2204
|
-
errorClass: cls,
|
|
2205
|
-
message: `${tool}: ${errorText}`.slice(0, 200),
|
|
2206
|
-
action,
|
|
2207
|
-
attempt: repeats,
|
|
2208
|
-
recovered: false,
|
|
2209
|
-
});
|
|
2210
|
-
}
|
|
2211
|
-
catch (err) {
|
|
2212
|
-
await (0, run_store_js_1.recordDegradation)({ kind: 'core', op: 'pi-sdk.recordRecovery', runId: this.actor.state.activeRun, message: String(err?.message || err) }).catch(() => { });
|
|
2213
|
-
}
|
|
2214
|
-
// 外部无响应 → awaiting_external (不算失败; 等回话后回到 running)
|
|
2215
|
-
if (cls === 'external_no_reply') {
|
|
2216
|
-
this.awaitingExternal = true;
|
|
2217
|
-
await this.safeSetRunStatus(this.actor.state.activeRun, 'awaiting_external');
|
|
2218
|
-
// 2026-09-16 (2-C.4): 把"在等什么"写成持久化事实 (来源/关联/过期), 否则真实回包到了也不知道该唤醒谁。
|
|
2219
|
-
try {
|
|
2220
|
-
if (this.actor.state.goalBinding) {
|
|
2221
|
-
const { bindExternalWait, newContinuationId, defaultWaitExpiry } = await Promise.resolve().then(() => __importStar(require('./external-events.js')));
|
|
2222
|
-
const isDelegate = /delegate/i.test(String(tool || ''));
|
|
2223
|
-
await bindExternalWait(this.actor.state.goalBinding, {
|
|
2224
|
-
requestId: this.pendingExternalRequestId || `${this.actor.state.activeRun}:${Date.now().toString(36)}`,
|
|
2225
|
-
continuationId: newContinuationId(this.actor.state.goalBinding),
|
|
2226
|
-
expectedSource: isDelegate ? 'delegate' : 'p2p',
|
|
2227
|
-
expectedEvent: 'result',
|
|
2228
|
-
createdAt: new Date().toISOString(),
|
|
2229
|
-
expiresAt: defaultWaitExpiry(Date.now(), Number(process.env.BOLLOON_EXTERNAL_WAIT_MS || 30 * 60_000)),
|
|
2230
|
-
note: `${isDelegate ? 'delegate 回包' : 'P2P 协作回复'} 等待中 (tool=${tool})`,
|
|
2231
|
-
});
|
|
2232
|
-
}
|
|
2233
|
-
}
|
|
2234
|
-
catch (e) {
|
|
2235
|
-
await (0, run_store_js_1.recordDegradation)({ kind: 'core', op: 'pi-sdk.bindExternalWait', runId: this.actor.state.activeRun, message: String(e?.message || e) }).catch(() => { });
|
|
2236
|
-
}
|
|
2237
|
-
return;
|
|
2238
|
-
}
|
|
2239
|
-
// 鉴权类: 不重试, 直接交人
|
|
2240
|
-
if (cls === 'auth') {
|
|
2241
|
-
this.breakerReason = `鉴权类错误不重试 (${tool}): ${errorText.slice(0, 120)}`;
|
|
2242
|
-
await this.safeSetRunStatus(this.actor.state.activeRun, 'needs_human');
|
|
2243
|
-
return;
|
|
2244
|
-
}
|
|
2245
|
-
// 重复失败熔断: 同一工具 + 同一组参数连续失败达 3 次
|
|
2246
|
-
if (repeats >= MAX_SAME_TOOL_FAILURES) {
|
|
2247
|
-
this.breakerReason = `同一工具 ${tool} 连续失败 ${repeats} 次 (${cls}) → 熔断, 不再重试`;
|
|
2248
|
-
await this.safeSetRunStatus(this.actor.state.activeRun, 'needs_human');
|
|
2249
|
-
}
|
|
2250
|
-
}
|
|
2251
2169
|
getRunId() {
|
|
2252
2170
|
return this.actor.state.activeRun;
|
|
2253
2171
|
}
|
|
@@ -2420,26 +2338,6 @@ ${await this.buildSkillsIndex()}
|
|
|
2420
2338
|
}
|
|
2421
2339
|
return { outputs, success: allSuccess };
|
|
2422
2340
|
}
|
|
2423
|
-
buildContext() {
|
|
2424
|
-
// P1 接入: 同步跑前 3 层压缩 (Budget Reduction / Snip / Microcompact)
|
|
2425
|
-
// 异步层 (Context Collapse / Auto-Compact) 在 promptStream 入口处单独跑 (用 LLM)
|
|
2426
|
-
// 失败静默: 任何 stage 抛错 → 走老 slice(-10) 逻辑
|
|
2427
|
-
//
|
|
2428
|
-
// P1.2: 如果 maybeAutoCompact 算过 Context Collapse 投影, 用 this.projectedHistory (读时投影, 非破坏)
|
|
2429
|
-
const source = this.projectedHistory ?? this.actor.state.messageHistory;
|
|
2430
|
-
const recentMessages = this.compressHistorySync(source).slice(-10);
|
|
2431
|
-
return recentMessages.map(m => {
|
|
2432
|
-
if (m.role === 'user')
|
|
2433
|
-
return `用户: ${m.content}`;
|
|
2434
|
-
if (m.role === 'assistant')
|
|
2435
|
-
return `助手: ${m.content}`;
|
|
2436
|
-
if (m.role === 'tool') {
|
|
2437
|
-
const result = m.toolResult ? JSON.stringify(m.toolResult) : m.content;
|
|
2438
|
-
return `工具结果: ${result}`;
|
|
2439
|
-
}
|
|
2440
|
-
return m.content;
|
|
2441
|
-
}).join('\n');
|
|
2442
|
-
}
|
|
2443
2341
|
/**
|
|
2444
2342
|
* M3.5 (2026-06-17): 把 history 转成 messages 数组, 给 llm.chat() 用.
|
|
2445
2343
|
* 不再用 buildContext() 把所有 role 压成字符串 — LLM 看不到 tool 调用结果.
|
|
@@ -2448,101 +2346,11 @@ ${await this.buildSkillsIndex()}
|
|
|
2448
2346
|
* 取最近 N 条, 同步压缩前 3 层 (跟 buildContext 同步).
|
|
2449
2347
|
* 跳过 projectedHistory 路径 — messages 数组必须真实, 不能用投影.
|
|
2450
2348
|
*/
|
|
2451
|
-
buildMessages() {
|
|
2452
|
-
try {
|
|
2453
|
-
// 2026-08-06: 来源优先用 projectedHistory (Context Collapse 投影, 非破坏) —
|
|
2454
|
-
// 与 buildContext 一致; 之前只让字符串路径用投影, messages 数组路径被跳过,
|
|
2455
|
-
// 导致 LLM 实际看到的还是未压缩的历史.
|
|
2456
|
-
const source = this.projectedHistory ?? this.actor.state.messageHistory;
|
|
2457
|
-
const WINDOW = 15;
|
|
2458
|
-
const out = [];
|
|
2459
|
-
// 2026-09-28 (前缀 KV 可命中): 带 CURRENT TURN 注入的条目要**原样回带**.
|
|
2460
|
-
// 两个理由: (1) 注入块是"当前轮的前缀", 重渲染 (重加 `[工具结果]` 前缀 / 再 slice 截断)
|
|
2461
|
-
// 会改字节 → 下一轮前缀从这条起分叉; (2) 已注入的 tool 条目在 wire 上已经是 user 形状,
|
|
2462
|
-
// 再套一次前缀就是双前缀, 连内容都变了.
|
|
2463
|
-
const isInjectedEntry = (m) => typeof m.content === 'string' && m.content.includes(pi_ai_js_1.CURRENT_TURN_MARKER);
|
|
2464
|
-
// 早期历史压缩: 超过窗口时, 不直接丢弃 — 提取前段用户意图摘要注入 (同步, 无 LLM).
|
|
2465
|
-
// 结构对齐 Context OS: System Prompt(persona) + 压缩摘要 + 最近消息.
|
|
2466
|
-
if (source.length > WINDOW) {
|
|
2467
|
-
const early = source.slice(0, source.length - WINDOW);
|
|
2468
|
-
const slice = source.slice(-WINDOW);
|
|
2469
|
-
const earlyUsers = early.filter(m => m.role === 'user' && (m.content || '').trim());
|
|
2470
|
-
const earlyTools = early.filter(m => m.role === 'tool').length;
|
|
2471
|
-
const earlyAssist = early.filter(m => m.role === 'assistant' && (m.content || '').trim()).length;
|
|
2472
|
-
const snippet = earlyUsers.slice(-5).map(m => `- ${(m.content || '').slice(0, 120).replace(/\n/g, ' ')}`).join('\n') || '- (早期对话无用户文本)';
|
|
2473
|
-
out.push({
|
|
2474
|
-
role: 'system',
|
|
2475
|
-
content: `[上下文压缩] 早期 ${early.length} 条消息已压缩 (用户 ${earlyUsers.length} 条 / AI ${earlyAssist} 条 / 工具结果 ${earlyTools} 条). 关键用户意图摘要:\n${snippet}\n[压缩结束] 以下是最近消息:`,
|
|
2476
|
-
});
|
|
2477
|
-
for (const m of slice) {
|
|
2478
|
-
const r = m.role;
|
|
2479
|
-
if (isInjectedEntry(m)) {
|
|
2480
|
-
out.push({ role: 'user', content: String(m.content) });
|
|
2481
|
-
continue;
|
|
2482
|
-
}
|
|
2483
|
-
if (r === 'tool') {
|
|
2484
|
-
out.push({ role: 'user', content: `[工具结果]\n${(m.content || '').slice(0, 2000)}` });
|
|
2485
|
-
continue;
|
|
2486
|
-
}
|
|
2487
|
-
if (r === 'assistant') {
|
|
2488
|
-
out.push({ role: 'assistant', content: (m.content || '').slice(0, 4000), reasoningContent: m.reasoningContent });
|
|
2489
|
-
continue;
|
|
2490
|
-
}
|
|
2491
|
-
if (r === 'user') {
|
|
2492
|
-
out.push({ role: 'user', content: (m.content || '').slice(0, 2000) });
|
|
2493
|
-
continue;
|
|
2494
|
-
}
|
|
2495
|
-
if (r === 'system') {
|
|
2496
|
-
out.push({ role: 'system', content: (m.content || '').slice(0, 2000) });
|
|
2497
|
-
}
|
|
2498
|
-
}
|
|
2499
|
-
return out;
|
|
2500
|
-
}
|
|
2501
|
-
// 窗口内: 原逻辑 (tool 转 user role, 避免 tool_calls 配对)
|
|
2502
|
-
const slice = source.slice(-WINDOW);
|
|
2503
|
-
for (const m of slice) {
|
|
2504
|
-
const r = m.role;
|
|
2505
|
-
if (isInjectedEntry(m)) {
|
|
2506
|
-
out.push({ role: 'user', content: String(m.content) });
|
|
2507
|
-
continue;
|
|
2508
|
-
}
|
|
2509
|
-
if (r === 'tool') {
|
|
2510
|
-
out.push({ role: 'user', content: `[工具结果]\n${m.content || ''}` });
|
|
2511
|
-
continue;
|
|
2512
|
-
}
|
|
2513
|
-
if (r === 'assistant') {
|
|
2514
|
-
out.push({ role: 'assistant', content: m.content || '', reasoningContent: m.reasoningContent });
|
|
2515
|
-
continue;
|
|
2516
|
-
}
|
|
2517
|
-
if (r === 'user') {
|
|
2518
|
-
out.push({ role: 'user', content: m.content || '' });
|
|
2519
|
-
}
|
|
2520
|
-
if (r === 'system') {
|
|
2521
|
-
out.push({ role: 'system', content: m.content || '' });
|
|
2522
|
-
}
|
|
2523
|
-
}
|
|
2524
|
-
return out;
|
|
2525
|
-
}
|
|
2526
|
-
catch (err) {
|
|
2527
|
-
console.warn('[PiAgent] buildMessages failed (silent, falling back to text):', err);
|
|
2528
|
-
// 退化: 用 buildContext 字符串包装成单 user message
|
|
2529
|
-
return [{ role: 'user', content: this.buildContext() }];
|
|
2530
|
-
}
|
|
2531
|
-
}
|
|
2532
2349
|
/**
|
|
2533
2350
|
* 2026-09-28 (前缀 KV 可命中): 把 chat() 回带的当前轮写回自己的 messageHistory.
|
|
2534
2351
|
* 逻辑在模块级 `writeBackCurrentTurnInto` (纯函数, 门可以直接驱它断言);
|
|
2535
2352
|
* 这里只负责接到 `actor 的 messageHistory` 上, 并保证任何异常都静默 (写回失败不影响对话).
|
|
2536
2353
|
*/
|
|
2537
|
-
writeBackCurrentTurn(wire) {
|
|
2538
|
-
try {
|
|
2539
|
-
return writeBackCurrentTurnInto(this.actor.state.messageHistory, wire);
|
|
2540
|
-
}
|
|
2541
|
-
catch (err) {
|
|
2542
|
-
console.warn('[PiAgent] writeBackCurrentTurn failed (silent):', err);
|
|
2543
|
-
return 0;
|
|
2544
|
-
}
|
|
2545
|
-
}
|
|
2546
2354
|
/**
|
|
2547
2355
|
* 估算 messageHistory 的 token 数 (4 字符 ≈ 1 token, 与 context-compaction 同步).
|
|
2548
2356
|
* 失败静默: 任何异常 → 0 (不阻塞)
|
|
@@ -2564,142 +2372,6 @@ ${await this.buildSkillsIndex()}
|
|
|
2564
2372
|
*
|
|
2565
2373
|
* 失败静默: 全部失败 → 返回空 reply, 让上层 no-tool_use 终止
|
|
2566
2374
|
*/
|
|
2567
|
-
async callLlmWithRecovery(llm, contextOrMessages, systemPrompt, signal, onStream, tools,
|
|
2568
|
-
/** 2026-09-28: 当前轮易变段 (循环进度/改进提示) —— 进 CURRENT TURN 区, 不进 system */
|
|
2569
|
-
currentTurnContext) {
|
|
2570
|
-
// Reactive compaction 预检: 估算 token 超 80% 阈值, 跑一次
|
|
2571
|
-
const estimated = this.estimateHistoryTokens();
|
|
2572
|
-
if (estimated > this.maxContextTokens() * 0.8) {
|
|
2573
|
-
console.warn(`[PiAgent] reactive compaction pre-check (${estimated} tokens > 80% threshold)`);
|
|
2574
|
-
onStream?.({ type: 'status', internal: true, content: '⚠️ reactive compaction 预检触发', tool: 'recovery' });
|
|
2575
|
-
try {
|
|
2576
|
-
// 同步压缩: 取快照与替换在**同一拍相邻两行** (中间没有 await) ⇒ 不存在"变换期间被追加"的窗口。
|
|
2577
|
-
// 若哪天这里插入 await, 必须改成 `actor.rebaseHistory(…, snapshotLen)` (见 channel-actor.ts)。
|
|
2578
|
-
const compacted = this.compressHistorySync(this.actor.state.messageHistory);
|
|
2579
|
-
this.replaceHistory(compacted);
|
|
2580
|
-
if (this.estimateHistoryTokens() > this.maxContextTokens() * 0.8) {
|
|
2581
|
-
await this.maybeAutoCompact(onStream, signal);
|
|
2582
|
-
}
|
|
2583
|
-
}
|
|
2584
|
-
catch (err) {
|
|
2585
|
-
console.warn('[PiAgent] reactive compaction pre-check failed:', err);
|
|
2586
|
-
}
|
|
2587
|
-
}
|
|
2588
|
-
// 错误分级 (M1.3, 2026-06-17):
|
|
2589
|
-
// - 401/403/400 (认证/请求错误): 不重试, 直接 fail-fast
|
|
2590
|
-
// - 429 (rate limit): 重试 2 次, 指数退避
|
|
2591
|
-
// - 5xx (上游错误): 重试 2 次, 指数退避
|
|
2592
|
-
// - network (ECONNRESET / fetch failed / abort/timeout): 重试 2 次
|
|
2593
|
-
// - 4xx prompt-too-long: 走 reactive compaction
|
|
2594
|
-
// 这样以前所有错误都触发整个 runReActLoop 重跑(浪费 token),现在 4xx 直接失败
|
|
2595
|
-
// 让上层把失败原因广播给用户,而不是闷在 loop 里 retry 3 次后给空回复
|
|
2596
|
-
const classifyError = (err) => {
|
|
2597
|
-
const msg = String(err?.message || err || '');
|
|
2598
|
-
// 401/403: 认证失败
|
|
2599
|
-
if (/401|unauthor|invalid api key|api_key|forbidden|403/i.test(msg))
|
|
2600
|
-
return 'auth';
|
|
2601
|
-
// 400 prompt-too-long
|
|
2602
|
-
if (/token|too long|exceed|length|context|4000|413/i.test(msg))
|
|
2603
|
-
return 'prompt_too_long';
|
|
2604
|
-
// 429 rate limit
|
|
2605
|
-
if (/429|rate.?limit|too many requests/i.test(msg))
|
|
2606
|
-
return 'rate_limit';
|
|
2607
|
-
// 5xx
|
|
2608
|
-
if (/5\d\d|internal server|bad gateway|service unavailable|gateway timeout|cloudflare|502|503|504/i.test(msg))
|
|
2609
|
-
return 'server';
|
|
2610
|
-
// network
|
|
2611
|
-
if (/econnreset|econnrefused|enotfound|etimedout|fetch failed|network|aborted|timeout/i.test(msg))
|
|
2612
|
-
return 'network';
|
|
2613
|
-
return 'other';
|
|
2614
|
-
};
|
|
2615
|
-
const isRetryable = (cls) => cls === 'rate_limit' || cls === 'server' || cls === 'network' || cls === 'prompt_too_long';
|
|
2616
|
-
const maxAttempts = (cls) => isRetryable(cls) ? 3 : 1;
|
|
2617
|
-
const backoffMs = (attempt) => Math.min(1000 * 2 ** attempt, 8000); // 1s, 2s, 4s, 8s cap
|
|
2618
|
-
let lastErr = null;
|
|
2619
|
-
let lastClass = 'other';
|
|
2620
|
-
for (let attempt = 0; attempt < 4; attempt++) { // 最多 4 次尝试
|
|
2621
|
-
try {
|
|
2622
|
-
// M3.5 (2026-06-17): 传 messages 数组 (如果 contextOrMessages 是数组) 或字符串
|
|
2623
|
-
// 数组版让 LLM 看到结构化的 user/assistant/tool role, 而不是把 history 拼成单字符串
|
|
2624
|
-
// Bug 5: pass tool IDs for native OpenAI tool calling
|
|
2625
|
-
// 2026-09-28 (前缀 KV 可命中): 显式 purpose/source —— pi-sdk 的 ReAct 循环**就是主对话**
|
|
2626
|
-
// (完整 IMMUTABLE PREFIX + 工具全集 + CURRENT TURN, 也只有它带 cache_prompt);
|
|
2627
|
-
// currentTurnContext 是调用方自己的当前轮易变段 (循环进度/改进提示), 走 CURRENT TURN 区.
|
|
2628
|
-
// 2026-09-28: 回带最终 wire messages (含注入后的当前轮) → 上层据此写回 messageHistory.
|
|
2629
|
-
const response = await llm.chat(contextOrMessages, systemPrompt, signal, tools, 'main-agent', 'react-loop', undefined, currentTurnContext);
|
|
2630
|
-
// 2026-10-01 (用户: 「思考的记录可以也显示出来吗」): 思考模型的思维链原先只被"存起来回带",
|
|
2631
|
-
// **不显示** ✗。这里把它往流里送一份 —— 显示与否由 CLI 侧决定(BOLLOON_SHOW_THINKING, 默认显示) ✓。
|
|
2632
|
-
try {
|
|
2633
|
-
const rc = String(response?.reasoningContent || '').trim();
|
|
2634
|
-
if (rc)
|
|
2635
|
-
this.runCtx.eventSink?.({ type: 'reasoning', content: rc });
|
|
2636
|
-
}
|
|
2637
|
-
catch { /* 送不出去不影响主流程 */ } // 2026-06-30: 透传 toolCalls (OpenAI 协议 native) 给上层, 让 assistant message 能 emit 真 id
|
|
2638
|
-
return { reply: response.reply || '', toolCalls: response.toolCalls, messages: response.messages };
|
|
2639
|
-
}
|
|
2640
|
-
catch (err) {
|
|
2641
|
-
// 用户主动 abort: 不重试, 立即抛
|
|
2642
|
-
if (signal?.aborted || err?.name === 'AbortError')
|
|
2643
|
-
throw err;
|
|
2644
|
-
lastErr = err;
|
|
2645
|
-
lastClass = classifyError(err);
|
|
2646
|
-
const errMsg = String(err?.message || err || '').slice(0, 200);
|
|
2647
|
-
const attempts = maxAttempts(lastClass);
|
|
2648
|
-
if (attempt + 1 >= attempts) {
|
|
2649
|
-
console.warn(`[PiAgent] LLM 调用失败, 不再重试 (class=${lastClass}, attempt=${attempt + 1}/${attempts}): ${errMsg}`);
|
|
2650
|
-
break;
|
|
2651
|
-
}
|
|
2652
|
-
console.warn(`[PiAgent] LLM 调用失败 (class=${lastClass}, attempt=${attempt + 1}/${attempts}), ${backoffMs(attempt)}ms 后重试: ${errMsg}`);
|
|
2653
|
-
onStream?.({ type: 'status', content: `⚠️ LLM 调用失败 (${lastClass}), 重试 ${attempt + 2}/${attempts}...`, tool: 'recovery' });
|
|
2654
|
-
if (lastClass === 'prompt_too_long') {
|
|
2655
|
-
try {
|
|
2656
|
-
await this.maybeAutoCompact(onStream, signal);
|
|
2657
|
-
}
|
|
2658
|
-
catch (compactionErr) {
|
|
2659
|
-
console.warn('[PiAgent] reactive compaction on prompt-too-long failed:', compactionErr);
|
|
2660
|
-
}
|
|
2661
|
-
// 重新生成 context (重试 prompt_too_long 时重建 messages — 包含压缩后的 history)
|
|
2662
|
-
if (Array.isArray(contextOrMessages)) {
|
|
2663
|
-
contextOrMessages = this.buildMessages();
|
|
2664
|
-
}
|
|
2665
|
-
else {
|
|
2666
|
-
contextOrMessages = this.buildContext();
|
|
2667
|
-
}
|
|
2668
|
-
}
|
|
2669
|
-
else if (errMsg.includes('insufficient tool messages') || errMsg.includes('must be followed by tool messages')) {
|
|
2670
|
-
// 2026-07-29: 特殊的 400 错误 — tool_calls 配对异常, 降级为纯文本 context
|
|
2671
|
-
console.warn('[PiAgent] insufficient tool messages — 降级为 buildContext 文本');
|
|
2672
|
-
if (Array.isArray(contextOrMessages)) {
|
|
2673
|
-
contextOrMessages = this.buildContext();
|
|
2674
|
-
// 也清除最近一轮的 toolCalls, 防止再触发
|
|
2675
|
-
if (this.actor.state.messageHistory.length > 1) {
|
|
2676
|
-
const last = this.actor.state.messageHistory[this.actor.state.messageHistory.length - 1];
|
|
2677
|
-
if (last.role === 'assistant' && last.toolCalls) {
|
|
2678
|
-
delete last.toolCalls;
|
|
2679
|
-
}
|
|
2680
|
-
}
|
|
2681
|
-
}
|
|
2682
|
-
}
|
|
2683
|
-
else {
|
|
2684
|
-
// 指数退避
|
|
2685
|
-
await new Promise((r) => setTimeout(r, backoffMs(attempt)));
|
|
2686
|
-
}
|
|
2687
|
-
}
|
|
2688
|
-
}
|
|
2689
|
-
// 失败: 返回结构化错误 reply (而不是空字符串), 上层可识别 + UI 可显示
|
|
2690
|
-
const errMsg = String(lastErr?.message || lastErr || '').slice(0, 300);
|
|
2691
|
-
const userMsg = lastClass === 'auth'
|
|
2692
|
-
? `[AI 服务调用失败] 认证错误: ${errMsg}\n请检查 API key 配置 (env: OPENAI_API_KEY / ANTHROPIC_API_KEY 等)`
|
|
2693
|
-
: lastClass === 'rate_limit'
|
|
2694
|
-
? `[AI 服务调用失败] 上游限流 (429): ${errMsg}\n请稍后重试`
|
|
2695
|
-
: lastClass === 'server'
|
|
2696
|
-
? `[AI 服务调用失败] 上游错误: ${errMsg}\n已重试 2 次仍失败, 可稍后重试`
|
|
2697
|
-
: lastClass === 'network'
|
|
2698
|
-
? `[AI 服务调用失败] 网络错误: ${errMsg}\n请检查网络连接`
|
|
2699
|
-
: `[AI 服务调用失败] ${errMsg}`;
|
|
2700
|
-
console.warn(`[PiAgent] callLlmWithRecovery 全部失败 (class=${lastClass}): ${errMsg}`);
|
|
2701
|
-
return { reply: userMsg };
|
|
2702
|
-
}
|
|
2703
2375
|
/**
|
|
2704
2376
|
* 同步压缩: 跑前 3 层 (Budget Reduction / Snip / Microcompact).
|
|
2705
2377
|
* Context Collapse / Auto-Compact 是 async, 不在 buildContext 同步链里跑.
|
|
@@ -2821,91 +2493,7 @@ ${await this.buildSkillsIndex()}
|
|
|
2821
2493
|
cm.updateUsage(beforeTokens);
|
|
2822
2494
|
}
|
|
2823
2495
|
}
|
|
2824
|
-
isFinalResponse(content) {
|
|
2825
|
-
// 2026-06-30: 抽到 ./parse-tool-call.ts 作为纯函数 — 这里只构建 ctx 并调用
|
|
2826
|
-
return (0, parse_tool_call_js_1.isFinalResponse)(content, this._parseCtx());
|
|
2827
|
-
}
|
|
2828
|
-
extractFinalAnswer(content) {
|
|
2829
|
-
// 抽取实现已挪到 ./parse-tool-call.ts (纯函数, 易测)
|
|
2830
|
-
return (0, parse_tool_call_js_1.extractFinalAnswer)(content);
|
|
2831
|
-
}
|
|
2832
|
-
_parseCtx() {
|
|
2833
|
-
return {
|
|
2834
|
-
tools: new Set(Array.from(this.tools.keys())),
|
|
2835
|
-
resolveAlias: (name) => this.resolveToolName(name),
|
|
2836
|
-
};
|
|
2837
|
-
}
|
|
2838
|
-
parseToolCall(content) {
|
|
2839
|
-
// 2026-06-30: 抽到 ./parse-tool-call.ts 作为纯函数 — 这里只构建 ctx 并调用
|
|
2840
|
-
return (0, parse_tool_call_js_1.parseToolCall)(content, this._parseCtx());
|
|
2841
|
-
}
|
|
2842
|
-
// [debug-2026-06-19] 临时: 打印 parseToolCall 输入和返回
|
|
2843
|
-
_dbgParseToolCall(content) {
|
|
2844
|
-
const r = this.parseToolCall(content);
|
|
2845
|
-
console.log('[DBG parseToolCall] result:', JSON.stringify(r), 'content head:', JSON.stringify(content.substring(0, 200)));
|
|
2846
|
-
return r;
|
|
2847
|
-
}
|
|
2848
|
-
/**
|
|
2849
|
-
* 2026-06-19: 工具名大小写不敏感 + Claude Code 风格别名映射
|
|
2850
|
-
|
|
2851
|
-
/**
|
|
2852
|
-
* 2026-06-19: 工具名大小写不敏感 + Claude Code 风格别名映射
|
|
2853
|
-
* LLM 实际产出 Read/Edit/Write/Bash/Grep/Glob 等大写名 (Claude Code 工具命名)
|
|
2854
|
-
* bolloon 注册的是 read_document / edit_file / write_file / shell_exec / list_files
|
|
2855
|
-
* 返回 this.tools 里的标准名, 或 null 表示未识别
|
|
2856
|
-
*/
|
|
2857
|
-
/**
|
|
2858
|
-
* 把 LLM 给的工具名 (可能大小写不一, 或者 Claude Code 风格的别名) 解析为
|
|
2859
|
-
* bolloon 注册的标准工具名.
|
|
2860
|
-
*
|
|
2861
|
-
* 2026-06-30: 委托给 ToolRegistry 模块 — alias 表在 tool-registry.ts 统一维护,
|
|
2862
|
-
* 这里只做 thin wrapper 保留 backward compat (private API 但其它地方可能用).
|
|
2863
|
-
*/
|
|
2864
|
-
resolveToolName(name) {
|
|
2865
|
-
return this._toolRegistry.resolve(name);
|
|
2866
|
-
}
|
|
2867
|
-
estimateResponseQuality(response) {
|
|
2868
|
-
let score = 0.5;
|
|
2869
|
-
if (response.length > 50)
|
|
2870
|
-
score += 0.1;
|
|
2871
|
-
if (response.length > 200)
|
|
2872
|
-
score += 0.1;
|
|
2873
|
-
if (response.length < 20)
|
|
2874
|
-
score -= 0.3;
|
|
2875
|
-
if (response.includes('\n'))
|
|
2876
|
-
score += 0.1;
|
|
2877
|
-
if (response.includes('-') || response.includes('•'))
|
|
2878
|
-
score += 0.05;
|
|
2879
|
-
if (response.includes('```'))
|
|
2880
|
-
score += 0.1;
|
|
2881
|
-
const conclusionWords = ['完成', '结果', '总结', '所以', '因此', '答案', '推荐'];
|
|
2882
|
-
if (conclusionWords.some(w => response.includes(w)))
|
|
2883
|
-
score += 0.1;
|
|
2884
|
-
if (response.includes('调用工具') || response.includes('tool('))
|
|
2885
|
-
score -= 0.2;
|
|
2886
|
-
return Math.max(0, Math.min(1, score));
|
|
2887
|
-
}
|
|
2888
|
-
estimateToolResultQuality(result) {
|
|
2889
|
-
let score = 0.5;
|
|
2890
|
-
if (!result.success)
|
|
2891
|
-
return 0.2;
|
|
2892
|
-
if (result.output) {
|
|
2893
|
-
score += 0.2;
|
|
2894
|
-
if (result.output.length > 50)
|
|
2895
|
-
score += 0.1;
|
|
2896
|
-
if (result.output.length < 10)
|
|
2897
|
-
score -= 0.1;
|
|
2898
|
-
if (result.output.includes('❌') || result.output.includes('error'))
|
|
2899
|
-
score -= 0.2;
|
|
2900
|
-
if (result.output.includes('✅') || result.output.includes('success'))
|
|
2901
|
-
score += 0.1;
|
|
2902
|
-
}
|
|
2903
|
-
if (result.error)
|
|
2904
|
-
score -= 0.3;
|
|
2905
|
-
return Math.max(0, Math.min(1, score));
|
|
2906
|
-
}
|
|
2907
2496
|
async handleFallback(input) {
|
|
2908
|
-
const lowerInput = input.toLowerCase();
|
|
2909
2497
|
const parts = input.trim().split(/\s+/);
|
|
2910
2498
|
const cmd = parts[0].toLowerCase();
|
|
2911
2499
|
const args = parts.slice(1).join(' ');
|
|
@@ -2988,7 +2576,6 @@ ${this.extractOperationsFromRef(operationsRef)}
|
|
|
2988
2576
|
}
|
|
2989
2577
|
extractOperationsFromRef(ref) {
|
|
2990
2578
|
const lines = ref.split('\n');
|
|
2991
|
-
const inOperationsSection = false;
|
|
2992
2579
|
const operationLines = [];
|
|
2993
2580
|
for (let i = 0; i < lines.length; i++) {
|
|
2994
2581
|
const line = lines[i];
|
|
@@ -3395,20 +2982,6 @@ ${this.extractOperationsFromRef(operationsRef)}
|
|
|
3395
2982
|
}
|
|
3396
2983
|
// ==================== Harness Integration ====================
|
|
3397
2984
|
operationLog = [];
|
|
3398
|
-
logToHarness(action, args, result) {
|
|
3399
|
-
if (!this.harnessEnabled || !this.harness)
|
|
3400
|
-
return;
|
|
3401
|
-
this.operationLog.push({
|
|
3402
|
-
timestamp: Date.now(),
|
|
3403
|
-
action,
|
|
3404
|
-
args,
|
|
3405
|
-
result,
|
|
3406
|
-
status: result.success ? 'ok' : 'error'
|
|
3407
|
-
});
|
|
3408
|
-
if (this.operationLog.length >= 10) {
|
|
3409
|
-
this.archiveToHarness();
|
|
3410
|
-
}
|
|
3411
|
-
}
|
|
3412
2985
|
archiveToHarness() {
|
|
3413
2986
|
if (!this.harnessEnabled || !this.harness || this.operationLog.length === 0)
|
|
3414
2987
|
return;
|