@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
package/dist/agents/pi-sdk.js
CHANGED
|
@@ -20,8 +20,6 @@ import { createPrivateActor } from '../kernel/channel-actor.js';
|
|
|
20
20
|
import { submitRunLifecycle, assertLifecycleOk, pickRunIdForResume } from '../kernel/run-lifecycle.js';
|
|
21
21
|
// K10 ④: 通信发送入口经内核端口 (上层不再直接对某条传输说话; 换传输只改这一处注入)
|
|
22
22
|
import { submitTransport, transportPeersSync } from '../kernel/transport.js';
|
|
23
|
-
// K10 ①: run 状态迁移经内核**控制面**端口 (它本来就管"谁命令这条 run 改状态": 带 origin 审计 + 拒绝归一化)
|
|
24
|
-
import { submitRunControl } from '../kernel/control.js';
|
|
25
23
|
// K10 ②: 回合的模型经**内核运行时**只读取租约 (传输仍是 Pi 客户端 ⇒ 行为不变; 取不到如实回落)
|
|
26
24
|
import { ModelRuntime, snapshotFromSelection } from '../kernel/model-runtime.js';
|
|
27
25
|
// K10 余项: 会话生命周期的**规则**归内核 (落盘映射 · key 校验 · 读回过滤/水合 · 运行种子合并顺序), I/O 留本类经端口注入
|
|
@@ -60,14 +58,14 @@ import { DeepThinkingEngine, AgentCoordinator } from '@bolloon/constraint-runtim
|
|
|
60
58
|
import { WorkflowPivotLoop, createDefaultPivotConfig } from './workflow-pivot-loop.js';
|
|
61
59
|
import { nativeAdapterFromEnv } from '../llm/native-adapter.js'; // K9: 第二个 (非 Pi) 推理适配器
|
|
62
60
|
import { initDocumentReceiver } from './p2p-document-tools.js';
|
|
63
|
-
import { startRun, recordStep, finishRun,
|
|
61
|
+
import { startRun, recordStep, finishRun, recordDegradation, recordHarnessEvent, prepareResume, buildResumeInstruction } from './run-store.js';
|
|
64
62
|
import { createGoal, attachRun } from './goal-store.js';
|
|
65
63
|
import { captureRunModelConfig } from '../llm/model-selection.js';
|
|
66
64
|
// 2026-09-26: 工具名出网净化 (pi-ai.ts 唯一边界) + 回程派发还原 (原名 ↔ API 名)
|
|
67
65
|
// 2026-09-28 (前缀 KV 可命中): CURRENT TURN 注入标记 —— "当前轮是否已注入"的唯一判据
|
|
68
66
|
import { CURRENT_TURN_MARKER } from '../llm/pi-ai.js';
|
|
69
67
|
import { PiAgentHarness } from './pi-harness.js';
|
|
70
|
-
import { DiscoveredAgentsManager, createSocialHeartbeat } from '../social/heartbeat.js';
|
|
68
|
+
import { DiscoveredAgentsManager, createSocialHeartbeat, } from '../social/heartbeat.js';
|
|
71
69
|
import { SkillRegistry } from '@bolloon/constraint-runtime';
|
|
72
70
|
import { loadSkillsFromPaths, defaultSkillPaths } from './skill-loader.js';
|
|
73
71
|
/** 2026-08-10: unreported 逃生门判定 — LLM 反复不把工具结果写进回复时, 超过上限强制收尾 (防死循环) */
|
|
@@ -97,7 +95,6 @@ import { budgetReduce, snip, microcompact } from '../context-compaction/index.js
|
|
|
97
95
|
import { ReactHarness } from '../security/react-harness.js';
|
|
98
96
|
import { HooksEngine } from '../hooks/hooks-engine.js';
|
|
99
97
|
import { DenyPipeline } from './deny-pipeline.js';
|
|
100
|
-
import { parseToolCall as parseToolCallImpl, isFinalResponse as isFinalResponseImpl, extractFinalAnswer as extractFinalAnswerImpl } from './parse-tool-call.js';
|
|
101
98
|
import { sessionStore as defaultSessionStore } from './session-store.js';
|
|
102
99
|
import { ToolRegistry } from './tool-registry.js';
|
|
103
100
|
// PiSessionManager 已抽到 ./pi-sdk-session-manager.ts (2026-07-06)
|
|
@@ -107,7 +104,6 @@ import { ToolRegistry } from './tool-registry.js';
|
|
|
107
104
|
* 同一工具 + 同一组参数连续失败上限 (2026-07-01 起) —— 触发两条互不替代的动作:
|
|
108
105
|
* 循环内: 注入 system 提示强制 LLM 收尾; 运行层 (M3): 熔断 → needs_human (交人)。
|
|
109
106
|
*/
|
|
110
|
-
const MAX_SAME_TOOL_FAILURES = 3;
|
|
111
107
|
/**
|
|
112
108
|
* 2026-09-28 (前缀 KV 可命中): 把 chat() 回带的「当前轮已注入」内容写回调用方自己的 history.
|
|
113
109
|
*
|
|
@@ -205,7 +201,6 @@ export class PiAgentSession {
|
|
|
205
201
|
pivotCumulativeTokens = 0;
|
|
206
202
|
/** 2026-10-13: 上下文管理周期压缩在飞标志 (防一轮里重复触发) */
|
|
207
203
|
_ctxCycleCompacting = false;
|
|
208
|
-
workflows = new Map();
|
|
209
204
|
constraintLayer;
|
|
210
205
|
workflowEngine;
|
|
211
206
|
sessionManager;
|
|
@@ -333,7 +328,9 @@ export class PiAgentSession {
|
|
|
333
328
|
}
|
|
334
329
|
return bg ? `${bg}\n${plans}` : plans;
|
|
335
330
|
}
|
|
331
|
+
/** M2.4 + 2026-10-15: persona section 缓存 (内容 + 源文件指纹)。指纹变了下一轮重建。 */
|
|
336
332
|
cachedPersonaSection = '';
|
|
333
|
+
cachedPersonaFingerprint = '';
|
|
337
334
|
/**
|
|
338
335
|
* 2026-10-13 (leo: 「重新阅读理解,整体开发整个技能的实现方式… 成整个加载循环」):
|
|
339
336
|
* 技能库索引摘要 —— 注入 systemPrompt, 让模型**知道技能存在**(名字+一句话描述),
|
|
@@ -369,17 +366,36 @@ export class PiAgentSession {
|
|
|
369
366
|
return '';
|
|
370
367
|
}
|
|
371
368
|
}
|
|
372
|
-
/**
|
|
373
|
-
|
|
374
|
-
|
|
369
|
+
/**
|
|
370
|
+
* 2026-10-15 (对齐 Hermes volatile 层 memory 整块注入): 稳态记忆块 ——
|
|
371
|
+
* 每轮 build systemPrompt 时把「重要且稳定」的结构化记忆整块给出 (crystallized 优先
|
|
372
|
+
* + importance 降序, 见 memory-store.formatMemorySystemBlock), 不依赖当轮 query 召回。
|
|
373
|
+
* 与 recallMemory (memory-recall.ts 的 session 摘要召回, query 驱动) 互补:
|
|
374
|
+
* 召回 = 即时相关的历史背景; 这块 = 始终可见的用户事实/偏好/固化结论。
|
|
375
|
+
* 失败静默 (返回 ''): 记忆读不到 = 无记忆, 不阻塞主流程。缓存: 按 agentId 单例 store,
|
|
376
|
+
* 每轮 build 都 load 一次 (JSONL 小文件, load 已是最轻读盘; 避免 stale 记忆进 prompt)。
|
|
377
|
+
*/
|
|
378
|
+
async buildStableMemoryBlock() {
|
|
379
|
+
try {
|
|
380
|
+
const agentId = this.actor?.state?.agentId || this.peerId || '';
|
|
381
|
+
if (!agentId)
|
|
382
|
+
return '';
|
|
383
|
+
const { getMemoryStore, formatMemorySystemBlock } = await import('./memory-store.js');
|
|
384
|
+
const store = getMemoryStore(agentId);
|
|
385
|
+
await store.load();
|
|
386
|
+
const all = await store.list(50);
|
|
387
|
+
return formatMemorySystemBlock(all, 6);
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
return '';
|
|
391
|
+
}
|
|
392
|
+
}
|
|
375
393
|
/** 最近一条用户消息 (按需子集档用来判 intent) */
|
|
376
394
|
lastUserIntent = '';
|
|
377
395
|
/** 本回合改过的 TS 文件 (回合收尾自动类型检查用) */
|
|
378
396
|
tsTouchedThisTurn = [];
|
|
379
397
|
/** 本回合是否已跑过类型检查 (一轮只跑一次) */
|
|
380
398
|
typecheckRanThisTurn = false;
|
|
381
|
-
iterBudget = null;
|
|
382
|
-
iterBudgetWarned = false;
|
|
383
399
|
/** 上一次已复盘的**任务签名** (签名变了 ⇒ 立刻复盘, 对应"每次任务都总结" ✓) */
|
|
384
400
|
lastReviewedTaskSig = undefined;
|
|
385
401
|
/** 上一次回合后自审的时间 (节流; 0 = 从未) */
|
|
@@ -388,45 +404,14 @@ export class PiAgentSession {
|
|
|
388
404
|
_sessionStore;
|
|
389
405
|
/** 构造期间 fire-and-forget 任务的 promise — whenReady() 等它 */
|
|
390
406
|
_readyPromise = null;
|
|
391
|
-
// 2026-06-16 修: 父要求把 ReAct loop 上限放大到 "几乎无限", 靠自动压缩上下文 + fail-safe 兜底
|
|
392
|
-
// 默认 10000 — 正常任务永远跑不到, 但作为防 LLM 死循环 / 防 OOM 的最后一道闸
|
|
393
|
-
// 旧默认 100 写死导致中等复杂度任务 (10-50 个 tool call + 多步反思) 会被误杀
|
|
394
|
-
MAX_REACT_ITERATIONS = 10_000;
|
|
395
|
-
MAX_REFINE_ATTEMPTS = 3;
|
|
396
|
-
QUALITY_THRESHOLD = 0.6;
|
|
397
|
-
/** P1: 上下文溢出阈值 (单轮估算 token 数, 超过则强制终止防止 prompt-too-long)
|
|
398
|
-
* 2026-08-06: 从 ContextManager 动态读 (默认 1M, env MAX_CONTEXT_TOKENS 可调).
|
|
399
|
-
* 保留字段仅作降级兜底 (ContextManager 初始化失败时用 60K 老行为). */
|
|
400
|
-
MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD = 60_000; // fallback 60K tokens
|
|
401
|
-
/** 2026-08-06: 上下文窗口 (tokens) — 统一走 ContextManager 配置 (1M 默认). */
|
|
402
|
-
maxContextTokens() {
|
|
403
|
-
try {
|
|
404
|
-
const { getContextManager } = _piRequire('../bootstrap/context-manager.js');
|
|
405
|
-
const n = getContextManager().getConfig().maxTokens;
|
|
406
|
-
return Number.isFinite(n) && n > 0 ? n : this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD;
|
|
407
|
-
}
|
|
408
|
-
catch {
|
|
409
|
-
return this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
/** 2026-06-16 新增: 累计错误总数兜底 (不管是否同工具, 累计 N 次就强制退出)
|
|
413
|
-
* 防 LLM 轮换工具名绕开 MAX_SAME_TOOL_FAILURES 的死循环攻击 */
|
|
414
|
-
MAX_TOTAL_ERRORS = 20;
|
|
415
|
-
/** 2026-06-19: 记录 loop 内成功执行的工具结果, 失败退出时汇总给用户 */
|
|
416
|
-
successfulToolResults = [];
|
|
417
407
|
/** 2026-06-19: Agent Mesh 通信 — 本地 + 远端 inbox 缓存, 给 check_inbox 工具读 */
|
|
418
408
|
_inboxMessages = [];
|
|
419
|
-
/** 2026-06-16 新增: loop 内自动压缩触发阈值 (相对 60K 阈值的比例) */
|
|
420
|
-
LOOP_COMPACT_RATIO = 0.8;
|
|
421
|
-
/** P1: max output token 升级重试 (LLM 截断时重试, 最多 3 次) */
|
|
422
|
-
MAX_OUTPUT_TOKEN_ESCALATION_RETRIES = 3;
|
|
423
409
|
thinkingEngine = new DeepThinkingEngine(3);
|
|
424
410
|
coordinator = new AgentCoordinator(3);
|
|
425
411
|
harness = null;
|
|
426
412
|
harnessEnabled = false;
|
|
427
413
|
/** 8-gate + 4-guard 集中调度 (防越权 / 防 prompt 注入) */
|
|
428
414
|
reactHarness = new ReactHarness();
|
|
429
|
-
usePivotLoop = true; // K4-B (2026-10-02): 默认切到 pivot —— 所有入口只跑**一套** loop (老 ReAct loop 待删)
|
|
430
415
|
pivotLoopConfig;
|
|
431
416
|
/** P2: 当前会话的 permission mode (每次 promptStream 入口解析) */
|
|
432
417
|
currentPermissionMode = 'default';
|
|
@@ -444,8 +429,6 @@ export class PiAgentSession {
|
|
|
444
429
|
_hooks = new HooksEngine();
|
|
445
430
|
/** 2026-07-29: Unified Deny-First Pipeline */
|
|
446
431
|
_denyPipeline = new DenyPipeline();
|
|
447
|
-
/** 2026-07-29: Snip + Context Collapse 启用标志 (默认启用) */
|
|
448
|
-
_enableSnipCollapse = true;
|
|
449
432
|
/** 注册一个或多个工具到拒绝列表 */
|
|
450
433
|
denyTool(...names) {
|
|
451
434
|
for (const name of names)
|
|
@@ -671,7 +654,6 @@ export class PiAgentSession {
|
|
|
671
654
|
this.workflowEngine = new WorkflowEngine(this.constraintLayer);
|
|
672
655
|
this.sessionManager = new PiSessionManager(this.identity.did, this.cwd, this.actor.state.agentId);
|
|
673
656
|
this.agentsManager = new DiscoveredAgentsManager();
|
|
674
|
-
this.usePivotLoop = config.usePivotLoop ?? true; // K4-B: 默认 true (K4-B 之前是 false ⇒ CLI 走老 loop, 与 web 分叉)
|
|
675
657
|
this.pivotLoopConfig = config.pivotLoopConfig;
|
|
676
658
|
this.initSession();
|
|
677
659
|
initDocumentReceiver();
|
|
@@ -687,12 +669,6 @@ export class PiAgentSession {
|
|
|
687
669
|
}
|
|
688
670
|
}
|
|
689
671
|
catch { /* env 读失败静默 */ }
|
|
690
|
-
// 2026-07-29: 从环境变量控制 Snip/Collapse
|
|
691
|
-
try {
|
|
692
|
-
if (process.env.BOLLOON_SNIP_COLLAPSE === '0')
|
|
693
|
-
this._enableSnipCollapse = false;
|
|
694
|
-
}
|
|
695
|
-
catch { /* 静默 */ }
|
|
696
672
|
// 2026-07-29: 从 ~/.bolloon/hooks.yaml 加载 hook 配置
|
|
697
673
|
// 失败静默 (无 hook 配置也正常)
|
|
698
674
|
try {
|
|
@@ -988,7 +964,7 @@ export class PiAgentSession {
|
|
|
988
964
|
registerWalletTools(toolCtx);
|
|
989
965
|
setupInboxListener(toolCtx);
|
|
990
966
|
// 镜像到 ToolRegistry (alias resolve 用)
|
|
991
|
-
for (const [
|
|
967
|
+
for (const [, tool] of this.tools.entries()) {
|
|
992
968
|
this._toolRegistry.register(tool);
|
|
993
969
|
}
|
|
994
970
|
// M3.3: 副作用工具走幂等性 cache
|
|
@@ -1000,9 +976,6 @@ export class PiAgentSession {
|
|
|
1000
976
|
}
|
|
1001
977
|
/** M3.3: 工具结果缓存 — 防止 loop 重试时副作用 (写文件 / 改代码) 执行多次 */
|
|
1002
978
|
_idempotencyCache = new IdempotencyCache();
|
|
1003
|
-
async registerP2PDocumentReceiver() {
|
|
1004
|
-
await initDocumentReceiver();
|
|
1005
|
-
}
|
|
1006
979
|
getToolDefinitions() {
|
|
1007
980
|
// M2.4 (2026-06-17): 缓存 tool 定义 — registerTools() 在构造时调一次, 此后不变
|
|
1008
981
|
// 2026-07-29: 拒绝列表变化时清空缓存, 重新生成
|
|
@@ -1943,7 +1916,23 @@ export class PiAgentSession {
|
|
|
1943
1916
|
catch (err) {
|
|
1944
1917
|
console.warn('[PiAgent] classifyIntent in pivot failed:', err);
|
|
1945
1918
|
}
|
|
1946
|
-
// M2.4: persona 缓存
|
|
1919
|
+
// M2.4: persona 缓存 — 2026-10-15 加指纹失效: persona 文档在外改了, 下一轮就重建
|
|
1920
|
+
// (对齐 Hermes invalidate_system_prompt 的 persona 侧; 与 systemPrompt 指纹是两条独立链路,
|
|
1921
|
+
// persona 走 personaFingerprint, Bolloon.md/AGENTS.md 走 contextFingerprint)
|
|
1922
|
+
const agentId = this.actor?.state?.agentId || this.peerId || '';
|
|
1923
|
+
if (agentId) {
|
|
1924
|
+
try {
|
|
1925
|
+
const { personaFingerprint } = await import('../bootstrap/persona-loader.js');
|
|
1926
|
+
const fp = await personaFingerprint(agentId);
|
|
1927
|
+
if (fp !== this.cachedPersonaFingerprint) {
|
|
1928
|
+
await this.sessionManager.reloadPersona();
|
|
1929
|
+
this.persona = this.sessionManager.getPersona();
|
|
1930
|
+
this.cachedPersonaFingerprint = fp;
|
|
1931
|
+
this.cachedPersonaSection = '';
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
catch { /* 指纹不可得 → 用旧缓存 (不阻塞) */ }
|
|
1935
|
+
}
|
|
1947
1936
|
if (!this.cachedPersonaSection && this.persona) {
|
|
1948
1937
|
this.cachedPersonaSection = `
|
|
1949
1938
|
角色描述: ${this.persona.description || '无'}
|
|
@@ -1966,6 +1955,8 @@ ${await this.renderActivePlansSection()}
|
|
|
1966
1955
|
|
|
1967
1956
|
${await this.buildSkillsIndex()}
|
|
1968
1957
|
|
|
1958
|
+
${await this.buildStableMemoryBlock()}
|
|
1959
|
+
|
|
1969
1960
|
工作模式:
|
|
1970
1961
|
1. 理解用户自然语言请求
|
|
1971
1962
|
2. 分析需要哪些工具来完成
|
|
@@ -1998,16 +1989,32 @@ ${await this.buildSkillsIndex()}
|
|
|
1998
1989
|
// 2026-07-17 Bug 1 修: 注入 messageHistory (hydrateMessageHistory 从 session JSON 回灌的) 到 system prompt
|
|
1999
1990
|
// pivot loop execute() 内部自己维护 messageHistory, 跟 pi-sdk 的 actor 的 messageHistory 隔离,
|
|
2000
1991
|
// 不注入的话 LLM 看不到历史对话, 每次都是新对话.
|
|
1992
|
+
// 2026-10-16 (leo: prompt 比重降低, 流程由 kernel/意图驱动): 压缩历史注入 —
|
|
1993
|
+
// 20 条 × 2000 = 40K 上限吃满 systemPrompt; 历史只为给 pivot 首轮"上次做到哪"的锚,
|
|
1994
|
+
// 细节靠 contextHint (memory 摘要) 与工具补。8 条 × 300, 首尾保真。
|
|
2001
1995
|
const historyLines = [];
|
|
2002
|
-
const
|
|
1996
|
+
const histAll = this.actor.state.messageHistory;
|
|
1997
|
+
const historyToInject = histAll.slice(-8, -1);
|
|
2003
1998
|
for (const m of historyToInject) {
|
|
2004
1999
|
const roleLabel = m.role === 'user' ? '用户' : m.role === 'assistant' ? '你' : m.role === 'tool' ? '工具结果' : m.role;
|
|
2005
|
-
|
|
2000
|
+
// 工具结果只留首行 (JSON 常上千字符, 全量塞进 system 是浪费)
|
|
2001
|
+
let text = String(m.content || '');
|
|
2002
|
+
if (m.role === 'tool')
|
|
2003
|
+
text = text.split('\n')[0].slice(0, 150);
|
|
2004
|
+
else
|
|
2005
|
+
text = text.slice(0, 300);
|
|
2006
2006
|
if (text)
|
|
2007
2007
|
historyLines.push(`[${roleLabel}]: ${text}`);
|
|
2008
2008
|
}
|
|
2009
|
+
// 首条消息保留完整一点 (会话起点 / 用户最初意图, 是"根目标"锚)
|
|
2010
|
+
if (histAll.length > 8) {
|
|
2011
|
+
const first = histAll[0];
|
|
2012
|
+
const firstText = String(first?.content || '').slice(0, 400);
|
|
2013
|
+
if (firstText)
|
|
2014
|
+
historyLines.unshift(`[${first?.role === 'user' ? '用户' : '你'}]: ${firstText}`);
|
|
2015
|
+
}
|
|
2009
2016
|
const historyBlock = historyLines.length > 0
|
|
2010
|
-
? `\n\n【历史对话 (最近 ${historyLines.length}
|
|
2017
|
+
? `\n\n【历史对话 (最近 ${historyLines.length} 条, 压缩摘要)】\n${historyLines.join('\n')}\n【历史对话结束】`
|
|
2011
2018
|
: '';
|
|
2012
2019
|
const onCompact = async () => {
|
|
2013
2020
|
// no-op (best-effort hook for future pi-sdk/pivot history sync)
|
|
@@ -2089,8 +2096,6 @@ ${await this.buildSkillsIndex()}
|
|
|
2089
2096
|
awaitingExternal = false;
|
|
2090
2097
|
/** 2026-09-16 (M3): 熔断原因 (同一工具连续失败达上限 → needs_human) */
|
|
2091
2098
|
breakerReason = '';
|
|
2092
|
-
/** 2026-09-16 (M3): 运行中核心写失败 (状态迁移等) —— 交给循环顶部硬闸, 不吞 */
|
|
2093
|
-
persistenceFailure = '';
|
|
2094
2099
|
/**
|
|
2095
2100
|
* 2026-09-16 (M2-B): Supervisor 开新 Run 继续同一 Goal 时注入"上一个 Run 已成功的非幂等动作"。
|
|
2096
2101
|
* 与 resume 的重放守卫同源 (run-store 的 buildContinuationPlan), 保证跨 Run 也不重复副作用。
|
|
@@ -2104,103 +2109,16 @@ ${await this.buildSkillsIndex()}
|
|
|
2104
2109
|
setGoalId(goalId) {
|
|
2105
2110
|
this.actor.state.goalBinding = String(goalId || '');
|
|
2106
2111
|
}
|
|
2107
|
-
/**
|
|
2108
|
-
* K10 ①: 控制面的**注入面** —— 写原语来自 run-store, 但调用一律经 `submitRunControl`
|
|
2109
|
-
* (与 `transportPorts()` 同一个模式: 换实现只改这一处, 上层不直接对 store 说话)。
|
|
2110
|
-
*/
|
|
2111
|
-
runControlPorts() {
|
|
2112
|
-
return {
|
|
2113
|
-
setRunStatus: setRunStatus,
|
|
2114
|
-
recordRecovery: recordRecovery,
|
|
2115
|
-
};
|
|
2116
|
-
}
|
|
2117
2112
|
/**
|
|
2118
2113
|
* 核心状态迁移的兜底: 失败 → 记降级 + 交给循环顶部的持久化硬闸 (不吞错)。
|
|
2119
2114
|
* K10 ①: 迁移动作经内核控制面; **口径不变** ——
|
|
2120
2115
|
* `{ok:false}` 且原因是"端口拒绝"(状态迁移不合法) ⇒ 只返回 false (那不是持久化故障);
|
|
2121
2116
|
* 未注入 / 抛错 ⇒ 才算持久化失败, 记降级 (与旧实现同款)。
|
|
2122
2117
|
*/
|
|
2123
|
-
async safeSetRunStatus(runId, to) {
|
|
2124
|
-
const out = await submitRunControl({ kind: 'set-run-status', origin: 'pi-session', runId, payload: { status: to } }, this.runControlPorts());
|
|
2125
|
-
if (out.ok)
|
|
2126
|
-
return true;
|
|
2127
|
-
const detail = String(out.detail || '未知原因');
|
|
2128
|
-
if (!detail.startsWith('端口拒绝')) {
|
|
2129
|
-
this.persistenceFailure = `状态迁移失败 (${to}): ${detail.slice(0, 180)}`;
|
|
2130
|
-
await recordDegradation({ kind: 'core', op: 'pi-sdk.setRunStatus', runId, message: this.persistenceFailure }).catch(() => { });
|
|
2131
|
-
}
|
|
2132
|
-
return false;
|
|
2133
|
-
}
|
|
2134
2118
|
/**
|
|
2135
2119
|
* 2026-09-16 (M3): 工具失败接线 —— 分类 → recovery 留痕 → 熔断 / 外部等待。
|
|
2136
2120
|
* 这是"错误恢复闭环"的运行时接线点 (此前 recordRecovery / repeatedFailureCount 只是数据结构)。
|
|
2137
2121
|
*/
|
|
2138
|
-
async wireToolFailure(tool, errorText, args) {
|
|
2139
|
-
if (!this.actor.state.activeRun)
|
|
2140
|
-
return;
|
|
2141
|
-
const cls = classifyRunError(errorText);
|
|
2142
|
-
const digest = argsDigestOf(args);
|
|
2143
|
-
let repeats = 1;
|
|
2144
|
-
try {
|
|
2145
|
-
const rec = await readRun(this.actor.state.activeRun);
|
|
2146
|
-
// recordStep 已经把**这次**失败记进去了, 所以"连续失败次数"直接就是当前值 (不再 +1)
|
|
2147
|
-
const seen = rec ? repeatedFailureCount(rec, tool, digest) : 0;
|
|
2148
|
-
repeats = Math.max(seen, 1);
|
|
2149
|
-
}
|
|
2150
|
-
catch { /* 读不到就按首次记 */ }
|
|
2151
|
-
const action = cls === 'transient' ? 'backoff'
|
|
2152
|
-
: cls === 'auth' ? 'escalate'
|
|
2153
|
-
: cls === 'external_no_reply' ? 'pause'
|
|
2154
|
-
: 'retry';
|
|
2155
|
-
try {
|
|
2156
|
-
await recordRecovery(this.actor.state.activeRun, {
|
|
2157
|
-
errorClass: cls,
|
|
2158
|
-
message: `${tool}: ${errorText}`.slice(0, 200),
|
|
2159
|
-
action,
|
|
2160
|
-
attempt: repeats,
|
|
2161
|
-
recovered: false,
|
|
2162
|
-
});
|
|
2163
|
-
}
|
|
2164
|
-
catch (err) {
|
|
2165
|
-
await recordDegradation({ kind: 'core', op: 'pi-sdk.recordRecovery', runId: this.actor.state.activeRun, message: String(err?.message || err) }).catch(() => { });
|
|
2166
|
-
}
|
|
2167
|
-
// 外部无响应 → awaiting_external (不算失败; 等回话后回到 running)
|
|
2168
|
-
if (cls === 'external_no_reply') {
|
|
2169
|
-
this.awaitingExternal = true;
|
|
2170
|
-
await this.safeSetRunStatus(this.actor.state.activeRun, 'awaiting_external');
|
|
2171
|
-
// 2026-09-16 (2-C.4): 把"在等什么"写成持久化事实 (来源/关联/过期), 否则真实回包到了也不知道该唤醒谁。
|
|
2172
|
-
try {
|
|
2173
|
-
if (this.actor.state.goalBinding) {
|
|
2174
|
-
const { bindExternalWait, newContinuationId, defaultWaitExpiry } = await import('./external-events.js');
|
|
2175
|
-
const isDelegate = /delegate/i.test(String(tool || ''));
|
|
2176
|
-
await bindExternalWait(this.actor.state.goalBinding, {
|
|
2177
|
-
requestId: this.pendingExternalRequestId || `${this.actor.state.activeRun}:${Date.now().toString(36)}`,
|
|
2178
|
-
continuationId: newContinuationId(this.actor.state.goalBinding),
|
|
2179
|
-
expectedSource: isDelegate ? 'delegate' : 'p2p',
|
|
2180
|
-
expectedEvent: 'result',
|
|
2181
|
-
createdAt: new Date().toISOString(),
|
|
2182
|
-
expiresAt: defaultWaitExpiry(Date.now(), Number(process.env.BOLLOON_EXTERNAL_WAIT_MS || 30 * 60_000)),
|
|
2183
|
-
note: `${isDelegate ? 'delegate 回包' : 'P2P 协作回复'} 等待中 (tool=${tool})`,
|
|
2184
|
-
});
|
|
2185
|
-
}
|
|
2186
|
-
}
|
|
2187
|
-
catch (e) {
|
|
2188
|
-
await recordDegradation({ kind: 'core', op: 'pi-sdk.bindExternalWait', runId: this.actor.state.activeRun, message: String(e?.message || e) }).catch(() => { });
|
|
2189
|
-
}
|
|
2190
|
-
return;
|
|
2191
|
-
}
|
|
2192
|
-
// 鉴权类: 不重试, 直接交人
|
|
2193
|
-
if (cls === 'auth') {
|
|
2194
|
-
this.breakerReason = `鉴权类错误不重试 (${tool}): ${errorText.slice(0, 120)}`;
|
|
2195
|
-
await this.safeSetRunStatus(this.actor.state.activeRun, 'needs_human');
|
|
2196
|
-
return;
|
|
2197
|
-
}
|
|
2198
|
-
// 重复失败熔断: 同一工具 + 同一组参数连续失败达 3 次
|
|
2199
|
-
if (repeats >= MAX_SAME_TOOL_FAILURES) {
|
|
2200
|
-
this.breakerReason = `同一工具 ${tool} 连续失败 ${repeats} 次 (${cls}) → 熔断, 不再重试`;
|
|
2201
|
-
await this.safeSetRunStatus(this.actor.state.activeRun, 'needs_human');
|
|
2202
|
-
}
|
|
2203
|
-
}
|
|
2204
2122
|
getRunId() {
|
|
2205
2123
|
return this.actor.state.activeRun;
|
|
2206
2124
|
}
|
|
@@ -2373,26 +2291,6 @@ ${await this.buildSkillsIndex()}
|
|
|
2373
2291
|
}
|
|
2374
2292
|
return { outputs, success: allSuccess };
|
|
2375
2293
|
}
|
|
2376
|
-
buildContext() {
|
|
2377
|
-
// P1 接入: 同步跑前 3 层压缩 (Budget Reduction / Snip / Microcompact)
|
|
2378
|
-
// 异步层 (Context Collapse / Auto-Compact) 在 promptStream 入口处单独跑 (用 LLM)
|
|
2379
|
-
// 失败静默: 任何 stage 抛错 → 走老 slice(-10) 逻辑
|
|
2380
|
-
//
|
|
2381
|
-
// P1.2: 如果 maybeAutoCompact 算过 Context Collapse 投影, 用 this.projectedHistory (读时投影, 非破坏)
|
|
2382
|
-
const source = this.projectedHistory ?? this.actor.state.messageHistory;
|
|
2383
|
-
const recentMessages = this.compressHistorySync(source).slice(-10);
|
|
2384
|
-
return recentMessages.map(m => {
|
|
2385
|
-
if (m.role === 'user')
|
|
2386
|
-
return `用户: ${m.content}`;
|
|
2387
|
-
if (m.role === 'assistant')
|
|
2388
|
-
return `助手: ${m.content}`;
|
|
2389
|
-
if (m.role === 'tool') {
|
|
2390
|
-
const result = m.toolResult ? JSON.stringify(m.toolResult) : m.content;
|
|
2391
|
-
return `工具结果: ${result}`;
|
|
2392
|
-
}
|
|
2393
|
-
return m.content;
|
|
2394
|
-
}).join('\n');
|
|
2395
|
-
}
|
|
2396
2294
|
/**
|
|
2397
2295
|
* M3.5 (2026-06-17): 把 history 转成 messages 数组, 给 llm.chat() 用.
|
|
2398
2296
|
* 不再用 buildContext() 把所有 role 压成字符串 — LLM 看不到 tool 调用结果.
|
|
@@ -2401,101 +2299,11 @@ ${await this.buildSkillsIndex()}
|
|
|
2401
2299
|
* 取最近 N 条, 同步压缩前 3 层 (跟 buildContext 同步).
|
|
2402
2300
|
* 跳过 projectedHistory 路径 — messages 数组必须真实, 不能用投影.
|
|
2403
2301
|
*/
|
|
2404
|
-
buildMessages() {
|
|
2405
|
-
try {
|
|
2406
|
-
// 2026-08-06: 来源优先用 projectedHistory (Context Collapse 投影, 非破坏) —
|
|
2407
|
-
// 与 buildContext 一致; 之前只让字符串路径用投影, messages 数组路径被跳过,
|
|
2408
|
-
// 导致 LLM 实际看到的还是未压缩的历史.
|
|
2409
|
-
const source = this.projectedHistory ?? this.actor.state.messageHistory;
|
|
2410
|
-
const WINDOW = 15;
|
|
2411
|
-
const out = [];
|
|
2412
|
-
// 2026-09-28 (前缀 KV 可命中): 带 CURRENT TURN 注入的条目要**原样回带**.
|
|
2413
|
-
// 两个理由: (1) 注入块是"当前轮的前缀", 重渲染 (重加 `[工具结果]` 前缀 / 再 slice 截断)
|
|
2414
|
-
// 会改字节 → 下一轮前缀从这条起分叉; (2) 已注入的 tool 条目在 wire 上已经是 user 形状,
|
|
2415
|
-
// 再套一次前缀就是双前缀, 连内容都变了.
|
|
2416
|
-
const isInjectedEntry = (m) => typeof m.content === 'string' && m.content.includes(CURRENT_TURN_MARKER);
|
|
2417
|
-
// 早期历史压缩: 超过窗口时, 不直接丢弃 — 提取前段用户意图摘要注入 (同步, 无 LLM).
|
|
2418
|
-
// 结构对齐 Context OS: System Prompt(persona) + 压缩摘要 + 最近消息.
|
|
2419
|
-
if (source.length > WINDOW) {
|
|
2420
|
-
const early = source.slice(0, source.length - WINDOW);
|
|
2421
|
-
const slice = source.slice(-WINDOW);
|
|
2422
|
-
const earlyUsers = early.filter(m => m.role === 'user' && (m.content || '').trim());
|
|
2423
|
-
const earlyTools = early.filter(m => m.role === 'tool').length;
|
|
2424
|
-
const earlyAssist = early.filter(m => m.role === 'assistant' && (m.content || '').trim()).length;
|
|
2425
|
-
const snippet = earlyUsers.slice(-5).map(m => `- ${(m.content || '').slice(0, 120).replace(/\n/g, ' ')}`).join('\n') || '- (早期对话无用户文本)';
|
|
2426
|
-
out.push({
|
|
2427
|
-
role: 'system',
|
|
2428
|
-
content: `[上下文压缩] 早期 ${early.length} 条消息已压缩 (用户 ${earlyUsers.length} 条 / AI ${earlyAssist} 条 / 工具结果 ${earlyTools} 条). 关键用户意图摘要:\n${snippet}\n[压缩结束] 以下是最近消息:`,
|
|
2429
|
-
});
|
|
2430
|
-
for (const m of slice) {
|
|
2431
|
-
const r = m.role;
|
|
2432
|
-
if (isInjectedEntry(m)) {
|
|
2433
|
-
out.push({ role: 'user', content: String(m.content) });
|
|
2434
|
-
continue;
|
|
2435
|
-
}
|
|
2436
|
-
if (r === 'tool') {
|
|
2437
|
-
out.push({ role: 'user', content: `[工具结果]\n${(m.content || '').slice(0, 2000)}` });
|
|
2438
|
-
continue;
|
|
2439
|
-
}
|
|
2440
|
-
if (r === 'assistant') {
|
|
2441
|
-
out.push({ role: 'assistant', content: (m.content || '').slice(0, 4000), reasoningContent: m.reasoningContent });
|
|
2442
|
-
continue;
|
|
2443
|
-
}
|
|
2444
|
-
if (r === 'user') {
|
|
2445
|
-
out.push({ role: 'user', content: (m.content || '').slice(0, 2000) });
|
|
2446
|
-
continue;
|
|
2447
|
-
}
|
|
2448
|
-
if (r === 'system') {
|
|
2449
|
-
out.push({ role: 'system', content: (m.content || '').slice(0, 2000) });
|
|
2450
|
-
}
|
|
2451
|
-
}
|
|
2452
|
-
return out;
|
|
2453
|
-
}
|
|
2454
|
-
// 窗口内: 原逻辑 (tool 转 user role, 避免 tool_calls 配对)
|
|
2455
|
-
const slice = source.slice(-WINDOW);
|
|
2456
|
-
for (const m of slice) {
|
|
2457
|
-
const r = m.role;
|
|
2458
|
-
if (isInjectedEntry(m)) {
|
|
2459
|
-
out.push({ role: 'user', content: String(m.content) });
|
|
2460
|
-
continue;
|
|
2461
|
-
}
|
|
2462
|
-
if (r === 'tool') {
|
|
2463
|
-
out.push({ role: 'user', content: `[工具结果]\n${m.content || ''}` });
|
|
2464
|
-
continue;
|
|
2465
|
-
}
|
|
2466
|
-
if (r === 'assistant') {
|
|
2467
|
-
out.push({ role: 'assistant', content: m.content || '', reasoningContent: m.reasoningContent });
|
|
2468
|
-
continue;
|
|
2469
|
-
}
|
|
2470
|
-
if (r === 'user') {
|
|
2471
|
-
out.push({ role: 'user', content: m.content || '' });
|
|
2472
|
-
}
|
|
2473
|
-
if (r === 'system') {
|
|
2474
|
-
out.push({ role: 'system', content: m.content || '' });
|
|
2475
|
-
}
|
|
2476
|
-
}
|
|
2477
|
-
return out;
|
|
2478
|
-
}
|
|
2479
|
-
catch (err) {
|
|
2480
|
-
console.warn('[PiAgent] buildMessages failed (silent, falling back to text):', err);
|
|
2481
|
-
// 退化: 用 buildContext 字符串包装成单 user message
|
|
2482
|
-
return [{ role: 'user', content: this.buildContext() }];
|
|
2483
|
-
}
|
|
2484
|
-
}
|
|
2485
2302
|
/**
|
|
2486
2303
|
* 2026-09-28 (前缀 KV 可命中): 把 chat() 回带的当前轮写回自己的 messageHistory.
|
|
2487
2304
|
* 逻辑在模块级 `writeBackCurrentTurnInto` (纯函数, 门可以直接驱它断言);
|
|
2488
2305
|
* 这里只负责接到 `actor 的 messageHistory` 上, 并保证任何异常都静默 (写回失败不影响对话).
|
|
2489
2306
|
*/
|
|
2490
|
-
writeBackCurrentTurn(wire) {
|
|
2491
|
-
try {
|
|
2492
|
-
return writeBackCurrentTurnInto(this.actor.state.messageHistory, wire);
|
|
2493
|
-
}
|
|
2494
|
-
catch (err) {
|
|
2495
|
-
console.warn('[PiAgent] writeBackCurrentTurn failed (silent):', err);
|
|
2496
|
-
return 0;
|
|
2497
|
-
}
|
|
2498
|
-
}
|
|
2499
2307
|
/**
|
|
2500
2308
|
* 估算 messageHistory 的 token 数 (4 字符 ≈ 1 token, 与 context-compaction 同步).
|
|
2501
2309
|
* 失败静默: 任何异常 → 0 (不阻塞)
|
|
@@ -2517,142 +2325,6 @@ ${await this.buildSkillsIndex()}
|
|
|
2517
2325
|
*
|
|
2518
2326
|
* 失败静默: 全部失败 → 返回空 reply, 让上层 no-tool_use 终止
|
|
2519
2327
|
*/
|
|
2520
|
-
async callLlmWithRecovery(llm, contextOrMessages, systemPrompt, signal, onStream, tools,
|
|
2521
|
-
/** 2026-09-28: 当前轮易变段 (循环进度/改进提示) —— 进 CURRENT TURN 区, 不进 system */
|
|
2522
|
-
currentTurnContext) {
|
|
2523
|
-
// Reactive compaction 预检: 估算 token 超 80% 阈值, 跑一次
|
|
2524
|
-
const estimated = this.estimateHistoryTokens();
|
|
2525
|
-
if (estimated > this.maxContextTokens() * 0.8) {
|
|
2526
|
-
console.warn(`[PiAgent] reactive compaction pre-check (${estimated} tokens > 80% threshold)`);
|
|
2527
|
-
onStream?.({ type: 'status', internal: true, content: '⚠️ reactive compaction 预检触发', tool: 'recovery' });
|
|
2528
|
-
try {
|
|
2529
|
-
// 同步压缩: 取快照与替换在**同一拍相邻两行** (中间没有 await) ⇒ 不存在"变换期间被追加"的窗口。
|
|
2530
|
-
// 若哪天这里插入 await, 必须改成 `actor.rebaseHistory(…, snapshotLen)` (见 channel-actor.ts)。
|
|
2531
|
-
const compacted = this.compressHistorySync(this.actor.state.messageHistory);
|
|
2532
|
-
this.replaceHistory(compacted);
|
|
2533
|
-
if (this.estimateHistoryTokens() > this.maxContextTokens() * 0.8) {
|
|
2534
|
-
await this.maybeAutoCompact(onStream, signal);
|
|
2535
|
-
}
|
|
2536
|
-
}
|
|
2537
|
-
catch (err) {
|
|
2538
|
-
console.warn('[PiAgent] reactive compaction pre-check failed:', err);
|
|
2539
|
-
}
|
|
2540
|
-
}
|
|
2541
|
-
// 错误分级 (M1.3, 2026-06-17):
|
|
2542
|
-
// - 401/403/400 (认证/请求错误): 不重试, 直接 fail-fast
|
|
2543
|
-
// - 429 (rate limit): 重试 2 次, 指数退避
|
|
2544
|
-
// - 5xx (上游错误): 重试 2 次, 指数退避
|
|
2545
|
-
// - network (ECONNRESET / fetch failed / abort/timeout): 重试 2 次
|
|
2546
|
-
// - 4xx prompt-too-long: 走 reactive compaction
|
|
2547
|
-
// 这样以前所有错误都触发整个 runReActLoop 重跑(浪费 token),现在 4xx 直接失败
|
|
2548
|
-
// 让上层把失败原因广播给用户,而不是闷在 loop 里 retry 3 次后给空回复
|
|
2549
|
-
const classifyError = (err) => {
|
|
2550
|
-
const msg = String(err?.message || err || '');
|
|
2551
|
-
// 401/403: 认证失败
|
|
2552
|
-
if (/401|unauthor|invalid api key|api_key|forbidden|403/i.test(msg))
|
|
2553
|
-
return 'auth';
|
|
2554
|
-
// 400 prompt-too-long
|
|
2555
|
-
if (/token|too long|exceed|length|context|4000|413/i.test(msg))
|
|
2556
|
-
return 'prompt_too_long';
|
|
2557
|
-
// 429 rate limit
|
|
2558
|
-
if (/429|rate.?limit|too many requests/i.test(msg))
|
|
2559
|
-
return 'rate_limit';
|
|
2560
|
-
// 5xx
|
|
2561
|
-
if (/5\d\d|internal server|bad gateway|service unavailable|gateway timeout|cloudflare|502|503|504/i.test(msg))
|
|
2562
|
-
return 'server';
|
|
2563
|
-
// network
|
|
2564
|
-
if (/econnreset|econnrefused|enotfound|etimedout|fetch failed|network|aborted|timeout/i.test(msg))
|
|
2565
|
-
return 'network';
|
|
2566
|
-
return 'other';
|
|
2567
|
-
};
|
|
2568
|
-
const isRetryable = (cls) => cls === 'rate_limit' || cls === 'server' || cls === 'network' || cls === 'prompt_too_long';
|
|
2569
|
-
const maxAttempts = (cls) => isRetryable(cls) ? 3 : 1;
|
|
2570
|
-
const backoffMs = (attempt) => Math.min(1000 * 2 ** attempt, 8000); // 1s, 2s, 4s, 8s cap
|
|
2571
|
-
let lastErr = null;
|
|
2572
|
-
let lastClass = 'other';
|
|
2573
|
-
for (let attempt = 0; attempt < 4; attempt++) { // 最多 4 次尝试
|
|
2574
|
-
try {
|
|
2575
|
-
// M3.5 (2026-06-17): 传 messages 数组 (如果 contextOrMessages 是数组) 或字符串
|
|
2576
|
-
// 数组版让 LLM 看到结构化的 user/assistant/tool role, 而不是把 history 拼成单字符串
|
|
2577
|
-
// Bug 5: pass tool IDs for native OpenAI tool calling
|
|
2578
|
-
// 2026-09-28 (前缀 KV 可命中): 显式 purpose/source —— pi-sdk 的 ReAct 循环**就是主对话**
|
|
2579
|
-
// (完整 IMMUTABLE PREFIX + 工具全集 + CURRENT TURN, 也只有它带 cache_prompt);
|
|
2580
|
-
// currentTurnContext 是调用方自己的当前轮易变段 (循环进度/改进提示), 走 CURRENT TURN 区.
|
|
2581
|
-
// 2026-09-28: 回带最终 wire messages (含注入后的当前轮) → 上层据此写回 messageHistory.
|
|
2582
|
-
const response = await llm.chat(contextOrMessages, systemPrompt, signal, tools, 'main-agent', 'react-loop', undefined, currentTurnContext);
|
|
2583
|
-
// 2026-10-01 (用户: 「思考的记录可以也显示出来吗」): 思考模型的思维链原先只被"存起来回带",
|
|
2584
|
-
// **不显示** ✗。这里把它往流里送一份 —— 显示与否由 CLI 侧决定(BOLLOON_SHOW_THINKING, 默认显示) ✓。
|
|
2585
|
-
try {
|
|
2586
|
-
const rc = String(response?.reasoningContent || '').trim();
|
|
2587
|
-
if (rc)
|
|
2588
|
-
this.runCtx.eventSink?.({ type: 'reasoning', content: rc });
|
|
2589
|
-
}
|
|
2590
|
-
catch { /* 送不出去不影响主流程 */ } // 2026-06-30: 透传 toolCalls (OpenAI 协议 native) 给上层, 让 assistant message 能 emit 真 id
|
|
2591
|
-
return { reply: response.reply || '', toolCalls: response.toolCalls, messages: response.messages };
|
|
2592
|
-
}
|
|
2593
|
-
catch (err) {
|
|
2594
|
-
// 用户主动 abort: 不重试, 立即抛
|
|
2595
|
-
if (signal?.aborted || err?.name === 'AbortError')
|
|
2596
|
-
throw err;
|
|
2597
|
-
lastErr = err;
|
|
2598
|
-
lastClass = classifyError(err);
|
|
2599
|
-
const errMsg = String(err?.message || err || '').slice(0, 200);
|
|
2600
|
-
const attempts = maxAttempts(lastClass);
|
|
2601
|
-
if (attempt + 1 >= attempts) {
|
|
2602
|
-
console.warn(`[PiAgent] LLM 调用失败, 不再重试 (class=${lastClass}, attempt=${attempt + 1}/${attempts}): ${errMsg}`);
|
|
2603
|
-
break;
|
|
2604
|
-
}
|
|
2605
|
-
console.warn(`[PiAgent] LLM 调用失败 (class=${lastClass}, attempt=${attempt + 1}/${attempts}), ${backoffMs(attempt)}ms 后重试: ${errMsg}`);
|
|
2606
|
-
onStream?.({ type: 'status', content: `⚠️ LLM 调用失败 (${lastClass}), 重试 ${attempt + 2}/${attempts}...`, tool: 'recovery' });
|
|
2607
|
-
if (lastClass === 'prompt_too_long') {
|
|
2608
|
-
try {
|
|
2609
|
-
await this.maybeAutoCompact(onStream, signal);
|
|
2610
|
-
}
|
|
2611
|
-
catch (compactionErr) {
|
|
2612
|
-
console.warn('[PiAgent] reactive compaction on prompt-too-long failed:', compactionErr);
|
|
2613
|
-
}
|
|
2614
|
-
// 重新生成 context (重试 prompt_too_long 时重建 messages — 包含压缩后的 history)
|
|
2615
|
-
if (Array.isArray(contextOrMessages)) {
|
|
2616
|
-
contextOrMessages = this.buildMessages();
|
|
2617
|
-
}
|
|
2618
|
-
else {
|
|
2619
|
-
contextOrMessages = this.buildContext();
|
|
2620
|
-
}
|
|
2621
|
-
}
|
|
2622
|
-
else if (errMsg.includes('insufficient tool messages') || errMsg.includes('must be followed by tool messages')) {
|
|
2623
|
-
// 2026-07-29: 特殊的 400 错误 — tool_calls 配对异常, 降级为纯文本 context
|
|
2624
|
-
console.warn('[PiAgent] insufficient tool messages — 降级为 buildContext 文本');
|
|
2625
|
-
if (Array.isArray(contextOrMessages)) {
|
|
2626
|
-
contextOrMessages = this.buildContext();
|
|
2627
|
-
// 也清除最近一轮的 toolCalls, 防止再触发
|
|
2628
|
-
if (this.actor.state.messageHistory.length > 1) {
|
|
2629
|
-
const last = this.actor.state.messageHistory[this.actor.state.messageHistory.length - 1];
|
|
2630
|
-
if (last.role === 'assistant' && last.toolCalls) {
|
|
2631
|
-
delete last.toolCalls;
|
|
2632
|
-
}
|
|
2633
|
-
}
|
|
2634
|
-
}
|
|
2635
|
-
}
|
|
2636
|
-
else {
|
|
2637
|
-
// 指数退避
|
|
2638
|
-
await new Promise((r) => setTimeout(r, backoffMs(attempt)));
|
|
2639
|
-
}
|
|
2640
|
-
}
|
|
2641
|
-
}
|
|
2642
|
-
// 失败: 返回结构化错误 reply (而不是空字符串), 上层可识别 + UI 可显示
|
|
2643
|
-
const errMsg = String(lastErr?.message || lastErr || '').slice(0, 300);
|
|
2644
|
-
const userMsg = lastClass === 'auth'
|
|
2645
|
-
? `[AI 服务调用失败] 认证错误: ${errMsg}\n请检查 API key 配置 (env: OPENAI_API_KEY / ANTHROPIC_API_KEY 等)`
|
|
2646
|
-
: lastClass === 'rate_limit'
|
|
2647
|
-
? `[AI 服务调用失败] 上游限流 (429): ${errMsg}\n请稍后重试`
|
|
2648
|
-
: lastClass === 'server'
|
|
2649
|
-
? `[AI 服务调用失败] 上游错误: ${errMsg}\n已重试 2 次仍失败, 可稍后重试`
|
|
2650
|
-
: lastClass === 'network'
|
|
2651
|
-
? `[AI 服务调用失败] 网络错误: ${errMsg}\n请检查网络连接`
|
|
2652
|
-
: `[AI 服务调用失败] ${errMsg}`;
|
|
2653
|
-
console.warn(`[PiAgent] callLlmWithRecovery 全部失败 (class=${lastClass}): ${errMsg}`);
|
|
2654
|
-
return { reply: userMsg };
|
|
2655
|
-
}
|
|
2656
2328
|
/**
|
|
2657
2329
|
* 同步压缩: 跑前 3 层 (Budget Reduction / Snip / Microcompact).
|
|
2658
2330
|
* Context Collapse / Auto-Compact 是 async, 不在 buildContext 同步链里跑.
|
|
@@ -2774,91 +2446,7 @@ ${await this.buildSkillsIndex()}
|
|
|
2774
2446
|
cm.updateUsage(beforeTokens);
|
|
2775
2447
|
}
|
|
2776
2448
|
}
|
|
2777
|
-
isFinalResponse(content) {
|
|
2778
|
-
// 2026-06-30: 抽到 ./parse-tool-call.ts 作为纯函数 — 这里只构建 ctx 并调用
|
|
2779
|
-
return isFinalResponseImpl(content, this._parseCtx());
|
|
2780
|
-
}
|
|
2781
|
-
extractFinalAnswer(content) {
|
|
2782
|
-
// 抽取实现已挪到 ./parse-tool-call.ts (纯函数, 易测)
|
|
2783
|
-
return extractFinalAnswerImpl(content);
|
|
2784
|
-
}
|
|
2785
|
-
_parseCtx() {
|
|
2786
|
-
return {
|
|
2787
|
-
tools: new Set(Array.from(this.tools.keys())),
|
|
2788
|
-
resolveAlias: (name) => this.resolveToolName(name),
|
|
2789
|
-
};
|
|
2790
|
-
}
|
|
2791
|
-
parseToolCall(content) {
|
|
2792
|
-
// 2026-06-30: 抽到 ./parse-tool-call.ts 作为纯函数 — 这里只构建 ctx 并调用
|
|
2793
|
-
return parseToolCallImpl(content, this._parseCtx());
|
|
2794
|
-
}
|
|
2795
|
-
// [debug-2026-06-19] 临时: 打印 parseToolCall 输入和返回
|
|
2796
|
-
_dbgParseToolCall(content) {
|
|
2797
|
-
const r = this.parseToolCall(content);
|
|
2798
|
-
console.log('[DBG parseToolCall] result:', JSON.stringify(r), 'content head:', JSON.stringify(content.substring(0, 200)));
|
|
2799
|
-
return r;
|
|
2800
|
-
}
|
|
2801
|
-
/**
|
|
2802
|
-
* 2026-06-19: 工具名大小写不敏感 + Claude Code 风格别名映射
|
|
2803
|
-
|
|
2804
|
-
/**
|
|
2805
|
-
* 2026-06-19: 工具名大小写不敏感 + Claude Code 风格别名映射
|
|
2806
|
-
* LLM 实际产出 Read/Edit/Write/Bash/Grep/Glob 等大写名 (Claude Code 工具命名)
|
|
2807
|
-
* bolloon 注册的是 read_document / edit_file / write_file / shell_exec / list_files
|
|
2808
|
-
* 返回 this.tools 里的标准名, 或 null 表示未识别
|
|
2809
|
-
*/
|
|
2810
|
-
/**
|
|
2811
|
-
* 把 LLM 给的工具名 (可能大小写不一, 或者 Claude Code 风格的别名) 解析为
|
|
2812
|
-
* bolloon 注册的标准工具名.
|
|
2813
|
-
*
|
|
2814
|
-
* 2026-06-30: 委托给 ToolRegistry 模块 — alias 表在 tool-registry.ts 统一维护,
|
|
2815
|
-
* 这里只做 thin wrapper 保留 backward compat (private API 但其它地方可能用).
|
|
2816
|
-
*/
|
|
2817
|
-
resolveToolName(name) {
|
|
2818
|
-
return this._toolRegistry.resolve(name);
|
|
2819
|
-
}
|
|
2820
|
-
estimateResponseQuality(response) {
|
|
2821
|
-
let score = 0.5;
|
|
2822
|
-
if (response.length > 50)
|
|
2823
|
-
score += 0.1;
|
|
2824
|
-
if (response.length > 200)
|
|
2825
|
-
score += 0.1;
|
|
2826
|
-
if (response.length < 20)
|
|
2827
|
-
score -= 0.3;
|
|
2828
|
-
if (response.includes('\n'))
|
|
2829
|
-
score += 0.1;
|
|
2830
|
-
if (response.includes('-') || response.includes('•'))
|
|
2831
|
-
score += 0.05;
|
|
2832
|
-
if (response.includes('```'))
|
|
2833
|
-
score += 0.1;
|
|
2834
|
-
const conclusionWords = ['完成', '结果', '总结', '所以', '因此', '答案', '推荐'];
|
|
2835
|
-
if (conclusionWords.some(w => response.includes(w)))
|
|
2836
|
-
score += 0.1;
|
|
2837
|
-
if (response.includes('调用工具') || response.includes('tool('))
|
|
2838
|
-
score -= 0.2;
|
|
2839
|
-
return Math.max(0, Math.min(1, score));
|
|
2840
|
-
}
|
|
2841
|
-
estimateToolResultQuality(result) {
|
|
2842
|
-
let score = 0.5;
|
|
2843
|
-
if (!result.success)
|
|
2844
|
-
return 0.2;
|
|
2845
|
-
if (result.output) {
|
|
2846
|
-
score += 0.2;
|
|
2847
|
-
if (result.output.length > 50)
|
|
2848
|
-
score += 0.1;
|
|
2849
|
-
if (result.output.length < 10)
|
|
2850
|
-
score -= 0.1;
|
|
2851
|
-
if (result.output.includes('❌') || result.output.includes('error'))
|
|
2852
|
-
score -= 0.2;
|
|
2853
|
-
if (result.output.includes('✅') || result.output.includes('success'))
|
|
2854
|
-
score += 0.1;
|
|
2855
|
-
}
|
|
2856
|
-
if (result.error)
|
|
2857
|
-
score -= 0.3;
|
|
2858
|
-
return Math.max(0, Math.min(1, score));
|
|
2859
|
-
}
|
|
2860
2449
|
async handleFallback(input) {
|
|
2861
|
-
const lowerInput = input.toLowerCase();
|
|
2862
2450
|
const parts = input.trim().split(/\s+/);
|
|
2863
2451
|
const cmd = parts[0].toLowerCase();
|
|
2864
2452
|
const args = parts.slice(1).join(' ');
|
|
@@ -2941,7 +2529,6 @@ ${this.extractOperationsFromRef(operationsRef)}
|
|
|
2941
2529
|
}
|
|
2942
2530
|
extractOperationsFromRef(ref) {
|
|
2943
2531
|
const lines = ref.split('\n');
|
|
2944
|
-
const inOperationsSection = false;
|
|
2945
2532
|
const operationLines = [];
|
|
2946
2533
|
for (let i = 0; i < lines.length; i++) {
|
|
2947
2534
|
const line = lines[i];
|
|
@@ -3348,20 +2935,6 @@ ${this.extractOperationsFromRef(operationsRef)}
|
|
|
3348
2935
|
}
|
|
3349
2936
|
// ==================== Harness Integration ====================
|
|
3350
2937
|
operationLog = [];
|
|
3351
|
-
logToHarness(action, args, result) {
|
|
3352
|
-
if (!this.harnessEnabled || !this.harness)
|
|
3353
|
-
return;
|
|
3354
|
-
this.operationLog.push({
|
|
3355
|
-
timestamp: Date.now(),
|
|
3356
|
-
action,
|
|
3357
|
-
args,
|
|
3358
|
-
result,
|
|
3359
|
-
status: result.success ? 'ok' : 'error'
|
|
3360
|
-
});
|
|
3361
|
-
if (this.operationLog.length >= 10) {
|
|
3362
|
-
this.archiveToHarness();
|
|
3363
|
-
}
|
|
3364
|
-
}
|
|
3365
2938
|
archiveToHarness() {
|
|
3366
2939
|
if (!this.harnessEnabled || !this.harness || this.operationLog.length === 0)
|
|
3367
2940
|
return;
|