@bolloon/bolloon-agent 0.5.0 → 0.5.2
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/README.md +39 -0
- package/dist/agents/agent-lang.js +101 -0
- package/dist/agents/agent-manifest-protocol.js +4 -0
- package/dist/agents/execution-supervisor.js +87 -5
- package/dist/agents/gateway-group.js +44 -3
- package/dist/agents/model-policy.js +651 -0
- package/dist/agents/pi-sdk-tools.js +84 -33
- package/dist/agents/pi-sdk.js +184 -31
- package/dist/agents/run-store.js +53 -0
- package/dist/agents/task/task-runner.js +8 -1
- package/dist/agents/workflow-pivot-loop.js +22 -13
- package/dist/cli/clipboard.js +65 -0
- package/dist/cli/ink-app.js +145 -17
- package/dist/cli/log-gate.js +386 -0
- package/dist/cli/mention-data.js +2 -0
- package/dist/cli/model-selector.js +538 -0
- package/dist/cli/reply-hygiene.js +54 -0
- package/dist/cli/setup-wizard.js +686 -69
- package/dist/cli/startup-notice.js +160 -0
- package/dist/cli/stores.js +22 -1
- package/dist/cli/theme.js +49 -0
- package/dist/cli/tui-select.js +970 -0
- package/dist/cli-entry.js +125 -18
- package/dist/efficode/compress.js +78 -0
- package/dist/efficode/crc32.js +38 -0
- package/dist/efficode/did.js +66 -0
- package/dist/efficode/index.js +24 -0
- package/dist/efficode/lz77.js +170 -0
- package/dist/efficode/negotiate.js +187 -0
- package/dist/efficode/ops.js +87 -0
- package/dist/efficode/packet.js +271 -0
- package/dist/efficode/types.js +45 -0
- package/dist/electron-build/agents/agent-manifest-protocol.js +4 -0
- package/dist/electron-build/agents/agent-manifest-protocol.js.map +1 -1
- package/dist/electron-build/agents/execution-supervisor.js +86 -4
- package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
- package/dist/electron-build/agents/goal-store.js.map +1 -1
- package/dist/electron-build/agents/model-policy.js +706 -0
- package/dist/electron-build/agents/model-policy.js.map +1 -0
- package/dist/electron-build/agents/pi-sdk-tools.js +84 -33
- package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +185 -31
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/run-store.js +55 -1
- package/dist/electron-build/agents/run-store.js.map +1 -1
- package/dist/electron-build/agents/task/task-runner.js +8 -1
- package/dist/electron-build/agents/task/task-runner.js.map +1 -1
- package/dist/electron-build/agents/workflow-pivot-loop.js +22 -13
- package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
- package/dist/electron-build/llm/config-store.js +146 -6
- package/dist/electron-build/llm/config-store.js.map +1 -1
- package/dist/electron-build/llm/connection-probe.js +764 -0
- package/dist/electron-build/llm/connection-probe.js.map +1 -0
- package/dist/electron-build/llm/custom-provider-store.js +272 -0
- package/dist/electron-build/llm/custom-provider-store.js.map +1 -0
- package/dist/electron-build/llm/data/provider-catalog-baked.js +23 -0
- package/dist/electron-build/llm/data/provider-catalog-baked.js.map +1 -0
- package/dist/electron-build/llm/model-catalog.js +778 -0
- package/dist/electron-build/llm/model-catalog.js.map +1 -0
- package/dist/electron-build/llm/model-discovery.js +1202 -0
- package/dist/electron-build/llm/model-discovery.js.map +1 -0
- package/dist/electron-build/llm/model-selection.js +1315 -0
- package/dist/electron-build/llm/model-selection.js.map +1 -0
- package/dist/electron-build/llm/pi-ai.js +751 -55
- package/dist/electron-build/llm/pi-ai.js.map +1 -1
- package/dist/electron-build/llm/provider-catalog.js +969 -0
- package/dist/electron-build/llm/provider-catalog.js.map +1 -0
- package/dist/electron-build/llm/provider-registry.js +950 -0
- package/dist/electron-build/llm/provider-registry.js.map +1 -0
- package/dist/electron-build/llm/tool-name.js +266 -0
- package/dist/electron-build/llm/tool-name.js.map +1 -0
- package/dist/electron-build/utils/dual-source-facts.js +236 -0
- package/dist/electron-build/utils/dual-source-facts.js.map +1 -0
- package/dist/electron-build/utils/dual-source.js +32 -156
- package/dist/electron-build/utils/dual-source.js.map +1 -1
- package/dist/electron-build/utils/update-manager.js +22 -72
- package/dist/electron-build/utils/update-manager.js.map +1 -1
- package/dist/electron-build/utils/update-state.js +2 -19
- package/dist/electron-build/utils/update-state.js.map +1 -1
- package/dist/electron-build/utils/version-identity.js +193 -0
- package/dist/electron-build/utils/version-identity.js.map +1 -0
- package/dist/electron-build/utils/version-info.js +47 -101
- package/dist/electron-build/utils/version-info.js.map +1 -1
- package/dist/index.js +282 -96
- package/dist/ios/bolloon-web.json +10 -0
- package/dist/ios/mobile-contacts.js +44 -11
- package/dist/ios/mobile-core.js +1723 -84
- package/dist/ios/mobile-tasks.js +339 -0
- package/dist/ios/mobile-update.js +1442 -0
- package/dist/ios/mobile.js +308 -35
- package/dist/ios/routes-llm-config.js +344 -52
- package/dist/ios/routes-mobile-tasks.js +353 -0
- package/dist/ios/server.js +163 -4
- package/dist/ios/sw.js +1 -1
- package/dist/llm/config-store.js +146 -6
- package/dist/llm/connection-probe.js +744 -0
- package/dist/llm/custom-provider-store.js +224 -0
- package/dist/llm/data/provider-catalog-baked.js +19 -0
- package/dist/llm/model-catalog.js +717 -0
- package/dist/llm/model-discovery.js +1137 -0
- package/dist/llm/model-selection.js +1241 -0
- package/dist/llm/pi-ai.js +732 -54
- package/dist/llm/provider-catalog.js +898 -0
- package/dist/llm/provider-registry.js +917 -0
- package/dist/llm/tool-name.js +220 -0
- package/dist/setup/onboard.js +34 -8
- package/dist/utils/dual-source-facts.js +223 -0
- package/dist/utils/dual-source.js +12 -141
- package/dist/utils/update-manager.js +16 -59
- package/dist/utils/update-state.js +6 -18
- package/dist/utils/version-identity.js +176 -0
- package/dist/utils/version-info.js +9 -78
- package/dist/web/bolloon-web.json +10 -0
- package/dist/web/mobile-core.js +1721 -84
- package/dist/web/mobile-update.js +1442 -0
- package/dist/web/mobile.js +372 -35
- package/dist/web/routes-llm-config.js +344 -52
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/scripts/build-web.ts +36 -0
package/dist/agents/pi-sdk.js
CHANGED
|
@@ -30,6 +30,11 @@ import { WorkflowPivotLoop, createDefaultPivotConfig } from './workflow-pivot-lo
|
|
|
30
30
|
import { initDocumentReceiver } from './p2p-document-tools.js';
|
|
31
31
|
import { startRun, recordStep, finishRun, readRun, budgetVerdict, recordDegradation, recordHarnessEvent, recordRecovery, setRunStatus, prepareResume, markRunRunning, buildResumeInstruction, argsDigestOf, repeatedFailureCount, classifyError as classifyRunError } from './run-store.js';
|
|
32
32
|
import { createGoal, attachRun, findActiveGoal } from './goal-store.js';
|
|
33
|
+
import { captureRunModelConfig } from '../llm/model-selection.js';
|
|
34
|
+
// 2026-09-26: 工具名出网净化 (pi-ai.ts 唯一边界) + 回程派发还原 (原名 ↔ API 名)
|
|
35
|
+
import { resolveApiToolName, expandKnownToolNames } from '../llm/tool-name.js';
|
|
36
|
+
// 2026-09-28 (前缀 KV 可命中): CURRENT TURN 注入标记 —— "当前轮是否已注入"的唯一判据
|
|
37
|
+
import { CURRENT_TURN_MARKER } from '../llm/pi-ai.js';
|
|
33
38
|
import { PiAgentHarness } from './pi-harness.js';
|
|
34
39
|
import { DiscoveredAgentsManager, createSocialHeartbeat } from '../social/heartbeat.js';
|
|
35
40
|
import { SkillRegistry } from '@bolloon/constraint-runtime';
|
|
@@ -76,6 +81,55 @@ import { DEFAULT_MAX_REVIEWS } from './loop-review.js';
|
|
|
76
81
|
* 循环内: 注入 system 提示强制 LLM 收尾; 运行层 (M3): 熔断 → needs_human (交人)。
|
|
77
82
|
*/
|
|
78
83
|
const MAX_SAME_TOOL_FAILURES = 3;
|
|
84
|
+
/**
|
|
85
|
+
* 2026-09-28 (前缀 KV 可命中): 把 chat() 回带的「当前轮已注入」内容写回调用方自己的 history.
|
|
86
|
+
*
|
|
87
|
+
* 谁是调用方: ReAct 循环每轮用 `buildMessages()` **重建全新对象** —— 注入只活在那一份里.
|
|
88
|
+
* 不写回的话下一轮那条 user 就退回注入前, 前缀从它起分叉:
|
|
89
|
+
* 第 1 次 `S H [D1 U1]` 第 2 次 `S H U1 U2` ← U1 丢了 D1
|
|
90
|
+
* 正确形态: `S H [D1 U1] U2` (D1 成了 U1 的一部分, 于是成为后续轮的稳定前缀).
|
|
91
|
+
*
|
|
92
|
+
* 为什么是纯函数 (不塞进类里): 门 `scripts/verify-kv-prefix.ts` 要能**直接驱它**断言,
|
|
93
|
+
* 而不是绕一整条 ReAct 循环才验到"写回"这一下.
|
|
94
|
+
*
|
|
95
|
+
* 匹配判据 (只做加法, 绝不乱改历史):
|
|
96
|
+
* 1. wire 最后一条 content 必须以 CURRENT TURN 标记开头 (没注入过 → 0, 不动);
|
|
97
|
+
* 2. 从 history **末尾往前**找第一条 `content` 是它后缀的条目 ——
|
|
98
|
+
* 去掉后缀剩下的那截必须仍以标记开头 (证明"注入是前部加了一段");
|
|
99
|
+
* 3. 命中就整段写回 (tool 条目拿到的是含 `[工具结果]\n` 前缀 + 注入的整段),
|
|
100
|
+
* 写回后该条 `content` 已等于 wire → 第二次调用前缀为空, 自然返回 0 (幂等).
|
|
101
|
+
*
|
|
102
|
+
* @returns 真写回的条数 (0 = 没注入过 / 匹配不上 / 已经写回过)
|
|
103
|
+
*/
|
|
104
|
+
export function writeBackCurrentTurnInto(history, wire) {
|
|
105
|
+
try {
|
|
106
|
+
if (!Array.isArray(history) || history.length === 0)
|
|
107
|
+
return 0;
|
|
108
|
+
if (!Array.isArray(wire) || wire.length === 0)
|
|
109
|
+
return 0;
|
|
110
|
+
const last = wire[wire.length - 1];
|
|
111
|
+
const wireContent = typeof last?.content === 'string' ? last.content : '';
|
|
112
|
+
if (!wireContent.startsWith(CURRENT_TURN_MARKER))
|
|
113
|
+
return 0; // 没注入过 → 不动
|
|
114
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
115
|
+
const entry = history[i];
|
|
116
|
+
const inner = typeof entry?.content === 'string' ? entry.content : '';
|
|
117
|
+
if (!inner)
|
|
118
|
+
continue;
|
|
119
|
+
if (!wireContent.endsWith(inner))
|
|
120
|
+
continue;
|
|
121
|
+
const prefix = wireContent.slice(0, wireContent.length - inner.length);
|
|
122
|
+
if (!prefix || !prefix.startsWith(CURRENT_TURN_MARKER))
|
|
123
|
+
continue;
|
|
124
|
+
entry.content = wireContent;
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return 0; // 写回失败绝不影响对话本身
|
|
131
|
+
}
|
|
132
|
+
}
|
|
79
133
|
export class PiAgentSession {
|
|
80
134
|
cwd;
|
|
81
135
|
peerId;
|
|
@@ -645,6 +699,18 @@ export class PiAgentSession {
|
|
|
645
699
|
return false;
|
|
646
700
|
}
|
|
647
701
|
}
|
|
702
|
+
/**
|
|
703
|
+
* 2026-09-26: 这个 Run 开始那一刻**真实生效**的模型配置快照, 由 startRun 落进 Run 记录。
|
|
704
|
+
* 拿不到 (配置读不出来) 就返回 undefined —— 宁可这个字段缺, 也不编一个假的进去。
|
|
705
|
+
*/
|
|
706
|
+
async runModelSnapshot() {
|
|
707
|
+
try {
|
|
708
|
+
return await captureRunModelConfig();
|
|
709
|
+
}
|
|
710
|
+
catch {
|
|
711
|
+
return undefined;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
648
714
|
/** 2026-08-07: prompt 出口统一上报 token 用量到 ContextManager (fallback/pivot/react 全路径覆盖) —
|
|
649
715
|
* 之前只有 runReActLoop 迭代内上报, chitchat/fallback/pivot 路径状态栏恒 0 */
|
|
650
716
|
reportUsageToContextManager() {
|
|
@@ -749,6 +815,7 @@ export class PiAgentSession {
|
|
|
749
815
|
goalId: boundGoalId,
|
|
750
816
|
channelId: this.currentChannelId || undefined,
|
|
751
817
|
agentId: this.currentAgentId || undefined,
|
|
818
|
+
modelConfig: await this.runModelSnapshot(),
|
|
752
819
|
});
|
|
753
820
|
this.lastRunId = rec.runId;
|
|
754
821
|
await attachRun(boundGoalId, rec.runId).catch(() => null);
|
|
@@ -1081,7 +1148,7 @@ export class PiAgentSession {
|
|
|
1081
1148
|
const backoffMs = 1000 * Math.pow(2, attempt - 1); // 1s, 2s, 4s
|
|
1082
1149
|
console.log(`[PiAgent] loop 自动重试 ${attempt}/${MAX_LOOP_RETRIES}, 等待 ${backoffMs}ms`);
|
|
1083
1150
|
if (onStream) {
|
|
1084
|
-
onStream({ type: 'status', content: `↻ 自动重试 loop ${attempt}/${MAX_LOOP_RETRIES} (${(backoffMs / 1000).toFixed(0)}s 后)`, tool: 'system' });
|
|
1151
|
+
onStream({ type: 'status', internal: true, content: `↻ 自动重试 loop ${attempt}/${MAX_LOOP_RETRIES} (${(backoffMs / 1000).toFixed(0)}s 后)`, tool: 'system' });
|
|
1085
1152
|
}
|
|
1086
1153
|
// 中途 abort 也要响应
|
|
1087
1154
|
await new Promise((resolve, reject) => {
|
|
@@ -1381,17 +1448,51 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1381
1448
|
* 2026-09-16 (M2): 从 checkpoint 恢复一次运行 —— **不是**重发原 prompt。
|
|
1382
1449
|
* 语义: prepareResume (校验状态 + 读 checkpoint + 记 recovery + 落 recovering) → 用恢复指令
|
|
1383
1450
|
* 驱动同一个 runId 继续 (历史保留), 非幂等动作由重放守卫挡住。
|
|
1451
|
+
*
|
|
1452
|
+
* 2026-09-26: 恢复前**反查一次模型配置**。Run 记录里存的 `modelConfig` 快照此前只被写进
|
|
1453
|
+
* 快照和回显, 没人拿它核对现状 —— 于是"快照记的是 A, 现在盘上生效的是 B"这种情况会被
|
|
1454
|
+
* 静默继续下去 (长任务的历史执行链就说不清了)。现在: 漂了就 `console.warn` 说清哪几个字段漂了,
|
|
1455
|
+
* 并把结论放进返回值 (`modelDrift`) 让调用方能上报; 一致时也有一句明确结论。
|
|
1456
|
+
* 判断失败 (读不到 Run 等) 不阻塞恢复。
|
|
1384
1457
|
*/
|
|
1385
1458
|
async resumeRun(runId) {
|
|
1459
|
+
let modelDrift;
|
|
1460
|
+
let modelApplied;
|
|
1461
|
+
try {
|
|
1462
|
+
const { detectRunConfigDrift } = await import('../llm/model-selection.js');
|
|
1463
|
+
modelDrift = (await detectRunConfigDrift(runId)) || undefined;
|
|
1464
|
+
if (modelDrift?.drifted) {
|
|
1465
|
+
console.warn('[pi-sdk] 恢复时模型配置已偏离 Run 快照:', modelDrift.message);
|
|
1466
|
+
}
|
|
1467
|
+
else if (modelDrift) {
|
|
1468
|
+
console.log('[pi-sdk] 恢复前核对:', modelDrift.message);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
catch { /* 核对本身失败不阻塞恢复 */ }
|
|
1386
1472
|
const prep = await prepareResume(runId);
|
|
1387
1473
|
if (!prep.ok || !prep.plan)
|
|
1388
|
-
return { ok: false, reason: prep.reason };
|
|
1474
|
+
return { ok: false, reason: prep.reason, ...(modelDrift ? { modelDrift } : {}) };
|
|
1475
|
+
// 2026-09-26 (P6 × P7): 恢复一个在跑的 Run, 用的是**它自己那份快照**, 不是盘上现在的全局默认 ——
|
|
1476
|
+
// 否则这次恢复就是"带着漂移继续跑", 之后再存快照会把漂移固化成历史。
|
|
1477
|
+
// 只在"漂了"或"核对不了"时重装 (一致时运行时本来就是对的, 重装是白跑一趟)。
|
|
1478
|
+
// 装配只有一处实现: `applyRunModelConfigToRuntime` → `installRuntime` (这里不自己 initMinimax)。
|
|
1479
|
+
if (modelDrift && (modelDrift.drifted || !modelDrift.verified)) {
|
|
1480
|
+
try {
|
|
1481
|
+
const { applyRunModelConfigToRuntime } = await import('../llm/model-selection.js');
|
|
1482
|
+
modelApplied = await applyRunModelConfigToRuntime(modelDrift.snapshot);
|
|
1483
|
+
console.log(`[pi-sdk] 已按 Run 快照装配运行时: ${modelApplied.provider}/${modelApplied.model} (configHash ${String(modelApplied.configHash).slice(0, 12)})`);
|
|
1484
|
+
}
|
|
1485
|
+
catch (e) {
|
|
1486
|
+
// 装配失败不阻塞恢复 (如实说; 调用方可以从返回值里看出没装成)
|
|
1487
|
+
console.warn('[pi-sdk] 按 Run 快照装配运行时失败, 保持当前运行时:', String(e?.message || e).slice(0, 160));
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1389
1490
|
this.resumeRunId = runId;
|
|
1390
1491
|
this.resumePlan = prep.plan;
|
|
1391
1492
|
this.currentGoalId = prep.plan.goalId || this.currentGoalId;
|
|
1392
1493
|
try {
|
|
1393
1494
|
const reply = await this.prompt(buildResumeInstruction(prep.plan), {});
|
|
1394
|
-
return { ok: true, reply };
|
|
1495
|
+
return { ok: true, reply, ...(modelDrift ? { modelDrift } : {}), ...(modelApplied ? { modelApplied } : {}) };
|
|
1395
1496
|
}
|
|
1396
1497
|
finally {
|
|
1397
1498
|
this.resumeRunId = '';
|
|
@@ -1461,7 +1562,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1461
1562
|
const loopActionLog = [];
|
|
1462
1563
|
// 发送循环开始的事件
|
|
1463
1564
|
if (onStream) {
|
|
1464
|
-
onStream({ type: 'status', content: '🔄 开始 ReAct 循环...', tool: 'system' });
|
|
1565
|
+
onStream({ type: 'status', internal: true, content: '🔄 开始 ReAct 循环...', tool: 'system' });
|
|
1465
1566
|
}
|
|
1466
1567
|
// React Harness: 循环开始 (重置 turn 计数 + 触发 harness sessionStart)
|
|
1467
1568
|
// 失败静默 (fail-open), 不阻塞主循环
|
|
@@ -1493,7 +1594,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1493
1594
|
}
|
|
1494
1595
|
const doneN = this.resumePlan?.completedSteps.length ?? 0;
|
|
1495
1596
|
const guards = this.resumePlan?.replayGuards.length ?? 0;
|
|
1496
|
-
onStream?.({ type: 'status', content: `♻️ 从 checkpoint 恢复运行 ${this.currentRunId} (已完成 ${doneN} 步, 非幂等重放守卫 ${guards} 条)`, tool: 'harness' });
|
|
1597
|
+
onStream?.({ type: 'status', internal: true, content: `♻️ 从 checkpoint 恢复运行 ${this.currentRunId} (已完成 ${doneN} 步, 非幂等重放守卫 ${guards} 条)`, tool: 'harness' });
|
|
1497
1598
|
}
|
|
1498
1599
|
else {
|
|
1499
1600
|
// 2026-09-16 (M2): 目标绑定 —— 有 goalId 就在该 Goal 下执行; 没有就建 Goal 再建 Run。
|
|
@@ -1536,6 +1637,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1536
1637
|
goalId: boundGoalId || undefined,
|
|
1537
1638
|
channelId: this.currentChannelId || undefined,
|
|
1538
1639
|
agentId: this.currentAgentId || undefined,
|
|
1640
|
+
modelConfig: await this.runModelSnapshot(),
|
|
1539
1641
|
});
|
|
1540
1642
|
this.currentRunId = rec.runId;
|
|
1541
1643
|
this.lastRunId = rec.runId;
|
|
@@ -1544,7 +1646,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1544
1646
|
// Run → Goal 反查链: runId → goalId → objective / success criteria
|
|
1545
1647
|
await attachRun(this.currentGoalId, rec.runId).catch((err) => console.warn('[PiAgent] attachRun 失败:', err?.message));
|
|
1546
1648
|
}
|
|
1547
|
-
onStream?.({ type: 'status', content: `🧷 运行已登记 (run=${rec.runId}${this.currentGoalId ? `, goal=${this.currentGoalId}` : ''}, 预算 ${rec.budget.maxSteps} 步 / ${Math.round(rec.budget.deadlineMs / 60000)} 分钟)`, tool: 'harness' });
|
|
1649
|
+
onStream?.({ type: 'status', internal: true, content: `🧷 运行已登记 (run=${rec.runId}${this.currentGoalId ? `, goal=${this.currentGoalId}` : ''}, 预算 ${rec.budget.maxSteps} 步 / ${Math.round(rec.budget.deadlineMs / 60000)} 分钟)`, tool: 'harness' });
|
|
1548
1650
|
}
|
|
1549
1651
|
catch (err) {
|
|
1550
1652
|
// 核心写失败: 不再 warn 后继续 —— 没有运行记录就不执行 (strict 模式默认如此)
|
|
@@ -1662,7 +1764,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1662
1764
|
if (shouldCompactBeforeIteration(estimatedTokensBefore, compactThreshold)) {
|
|
1663
1765
|
const tokensBeforeCompact = estimatedTokensBefore;
|
|
1664
1766
|
console.log(`[PiAgent] loop 入口 token ${tokensBeforeCompact} > ${compactThreshold}, 触发自动压缩`);
|
|
1665
|
-
onStream?.({ type: 'status', content: `🗜️ loop 自动压缩 (token ${tokensBeforeCompact} > ${compactThreshold})`, tool: 'compactor' });
|
|
1767
|
+
onStream?.({ type: 'status', internal: true, content: `🗜️ loop 自动压缩 (token ${tokensBeforeCompact} > ${compactThreshold})`, tool: 'compactor' });
|
|
1666
1768
|
try {
|
|
1667
1769
|
await this.maybeAutoCompact(onStream, signal);
|
|
1668
1770
|
}
|
|
@@ -1684,7 +1786,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1684
1786
|
// 调试日志:显示每次循环开始
|
|
1685
1787
|
console.log(`[PiAgent] 循环 ${iteration}/${this.MAX_REACT_ITERATIONS} 开始`);
|
|
1686
1788
|
if (onStream) {
|
|
1687
|
-
onStream({ type: 'status', content: `🔄 循环 ${iteration}/${this.MAX_REACT_ITERATIONS}`, tool: 'loop' });
|
|
1789
|
+
onStream({ type: 'status', internal: true, content: `🔄 循环 ${iteration}/${this.MAX_REACT_ITERATIONS}`, tool: 'loop' });
|
|
1688
1790
|
}
|
|
1689
1791
|
const context = this.buildContext();
|
|
1690
1792
|
// M3.5 (2026-06-17): 也构造 messages 数组版本, 让 LLM 看到结构化 tool 角色
|
|
@@ -1724,12 +1826,14 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1724
1826
|
.join('\n');
|
|
1725
1827
|
loopProgressSection = `\n【本轮循环进度】你已完成以下 ${loopActionLog.length} 个动作, 这是连续执行的同一轮任务:\n${actionLines}\n请基于已有结果继续推进, 不要重复执行上面已成功的动作. 全部完成后用 <final gen> 结束.\n`;
|
|
1726
1828
|
}
|
|
1829
|
+
// 2026-09-28 (前缀 KV 可命中): `refineContext` / `loopProgressSection` 是**每轮都在变**的段
|
|
1830
|
+
// (质量分 / 连续错误数 / 本轮已完成动作). 留在 system 里 = 每轮把 IMMUTABLE PREFIX 打碎 =
|
|
1831
|
+
// 服务端前缀 KV 永远 miss. 移到 CURRENT TURN 区 (注入到最后一条 user 前部) —— 文本一字未改, 只换位置.
|
|
1832
|
+
const currentTurnContext = `${refineContext}${loopProgressSection}`;
|
|
1727
1833
|
const systemPrompt = `${this.bootstrapAddition}你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${personaSection}
|
|
1728
1834
|
当前工作目录: ${this.cwd}
|
|
1729
1835
|
当前身份: ${this.identity.name} (${this.identity.did})
|
|
1730
|
-
${refineContext}
|
|
1731
1836
|
${this.currentIntentHint}
|
|
1732
|
-
${loopProgressSection}
|
|
1733
1837
|
|
|
1734
1838
|
${toolDefs}
|
|
1735
1839
|
|
|
@@ -1779,7 +1883,11 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1779
1883
|
// 2026-09-16 (Milestone 1-B): 模型调用前后走唯一门面 (扩展点 + 计数留痕; 默认不加新 hook 事件, 避免改变现有触发次数)
|
|
1780
1884
|
this.piHarness().beforeModelCall(this.harnessCtx());
|
|
1781
1885
|
const _modelCallT0 = Date.now();
|
|
1782
|
-
const response = await this.callLlmWithRecovery(llm, messages, systemPrompt, signal, onStream, openaiFormattedTools);
|
|
1886
|
+
const response = await this.callLlmWithRecovery(llm, messages, systemPrompt, signal, onStream, openaiFormattedTools, currentTurnContext);
|
|
1887
|
+
// 2026-09-28 (前缀 KV 可命中): 把注入了 CURRENT TURN 的当前轮**原样写回自己的 messageHistory** ——
|
|
1888
|
+
// buildMessages() 每轮重建全新对象, 不写回的话下一轮那条 user 就退回注入前, 前缀从那里分叉.
|
|
1889
|
+
// 用 chat() 回带的 wire (真正发出去的那一份), 而不是把 messages 再拼一遍.
|
|
1890
|
+
this.writeBackCurrentTurn(response.messages);
|
|
1783
1891
|
this.piHarness().afterModelCall(this.harnessCtx(), { ms: Date.now() - _modelCallT0 });
|
|
1784
1892
|
const reply = (response.reply || '').trim();
|
|
1785
1893
|
// 2026-06-30: OpenAI 协议 native tool_calls (LLM 真产了 tool_call 时, minimax/M3 会返回 id)
|
|
@@ -1873,7 +1981,10 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1873
1981
|
}
|
|
1874
1982
|
// 路径 B: 文本解析 (parseAllToolCalls 收集全部)
|
|
1875
1983
|
if (toolCalls.length === 0) {
|
|
1876
|
-
|
|
1984
|
+
// 2026-09-26: 已知名集合 = 注册表原名 + 它们的 API 名 (净化后的).
|
|
1985
|
+
// 出网时工具面给的是 API 名 (见 pi-ai.ts 的唯一净化边界), LLM 在文本里
|
|
1986
|
+
// 回吐的也可能是 API 名 —— 只用原名集合过滤会把这类调用整个丢掉.
|
|
1987
|
+
const knownTools = expandKnownToolNames(this.tools.keys());
|
|
1877
1988
|
toolCalls = parseAllToolCalls(reply, { tools: knownTools });
|
|
1878
1989
|
}
|
|
1879
1990
|
// 回退路径 C: 原生 parseToolCall (单个)
|
|
@@ -1882,6 +1993,12 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1882
1993
|
if (single)
|
|
1883
1994
|
toolCalls.push(single);
|
|
1884
1995
|
}
|
|
1996
|
+
// 2026-09-26: **回程派发的唯一还原点** —— LLM 回吐的是 API 名 (净化过的), 这里
|
|
1997
|
+
// 还原成注册表真名再交给 this.tools.get(); 原名 (LLM 照 system prompt 抄的) 原样穿透.
|
|
1998
|
+
for (const tc of toolCalls) {
|
|
1999
|
+
if (typeof tc.name === 'string' && tc.name)
|
|
2000
|
+
tc.name = resolveApiToolName(tc.name);
|
|
2001
|
+
}
|
|
1885
2002
|
// 给每个 toolCall 分配稳定 id
|
|
1886
2003
|
for (const tc of toolCalls) {
|
|
1887
2004
|
if (!tc.id) {
|
|
@@ -1906,7 +2023,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1906
2023
|
if (onStream) {
|
|
1907
2024
|
onStream({ type: 'tool', content: `🔧 调用工具 (${ti + 1}/${toolCalls.length}): ${toolCall.name}`, tool: toolCall.name });
|
|
1908
2025
|
if (toolCall.args && Object.keys(toolCall.args).length > 0) {
|
|
1909
|
-
onStream({ type: 'status', content: `📋 参数: ${JSON.stringify(toolCall.args)}`, tool: toolCall.name });
|
|
2026
|
+
onStream({ type: 'status', internal: true, content: `📋 参数: ${JSON.stringify(toolCall.args)}`, tool: toolCall.name });
|
|
1910
2027
|
}
|
|
1911
2028
|
onStream({
|
|
1912
2029
|
type: 'step_start',
|
|
@@ -2036,7 +2153,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2036
2153
|
const toolDurationMs = Date.now() - toolStart;
|
|
2037
2154
|
if (replaySkip) {
|
|
2038
2155
|
console.log(`[PiAgent] 恢复重放守卫: 跳过已完成的非幂等工具 ${toolCall.name}`);
|
|
2039
|
-
onStream?.({ type: 'status', content: `🛡️ 恢复保护: ${toolCall.name} 此前已成功执行, 本次不重复执行`, tool: toolCall.name });
|
|
2156
|
+
onStream?.({ type: 'status', internal: true, content: `🛡️ 恢复保护: ${toolCall.name} 此前已成功执行, 本次不重复执行`, tool: toolCall.name });
|
|
2040
2157
|
}
|
|
2041
2158
|
console.log(`[PiAgent] 工具 ${toolCall.name} 执行完成: success=${result.success} (${toolDurationMs}ms)`);
|
|
2042
2159
|
// 2026-09-16 (M3): 失败接线 —— 分类 + recovery 留痕 + 熔断 / 外部等待
|
|
@@ -2109,7 +2226,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2109
2226
|
}
|
|
2110
2227
|
if (onStream) {
|
|
2111
2228
|
if (result.success) {
|
|
2112
|
-
onStream({ type: 'status', content: `✅ ${toolCall.name} 执行成功`, tool: toolCall.name });
|
|
2229
|
+
onStream({ type: 'status', internal: true, content: `✅ ${toolCall.name} 执行成功`, tool: toolCall.name });
|
|
2113
2230
|
if (result.output) {
|
|
2114
2231
|
onStream({ type: 'tool', content: `📤 结果: ${result.output.substring(0, 200)}${result.output.length > 200 ? '...' : ''}`, tool: toolCall.name });
|
|
2115
2232
|
}
|
|
@@ -2139,7 +2256,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2139
2256
|
refineAttempts++;
|
|
2140
2257
|
}
|
|
2141
2258
|
if (onStream) {
|
|
2142
|
-
onStream({ type: 'status', content: `🔄 工具执行完成,继续循环...`, tool: 'loop' });
|
|
2259
|
+
onStream({ type: 'status', internal: true, content: `🔄 工具执行完成,继续循环...`, tool: 'loop' });
|
|
2143
2260
|
}
|
|
2144
2261
|
}
|
|
2145
2262
|
else {
|
|
@@ -2225,7 +2342,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2225
2342
|
content: `[dive-into stop condition] 你之前已成功执行了 ${unreported} 个工具, 但当前回复里没把它们的结果告诉用户. 请基于已有的工具结果 (在 history 里) 写一个完整总结回复给用户, 用 <final gen> 结尾. 不要再调工具.`
|
|
2226
2343
|
});
|
|
2227
2344
|
if (onStream) {
|
|
2228
|
-
onStream({ type: 'status', content: `🔄 还有 ${unreported} 个工具结果未汇报, 让 LLM 继续总结 (${unreportedRetries}/${MAX_UNREPORTED_RETRIES})`, tool: 'system' });
|
|
2345
|
+
onStream({ type: 'status', internal: true, content: `🔄 还有 ${unreported} 个工具结果未汇报, 让 LLM 继续总结 (${unreportedRetries}/${MAX_UNREPORTED_RETRIES})`, tool: 'system' });
|
|
2229
2346
|
}
|
|
2230
2347
|
continue;
|
|
2231
2348
|
}
|
|
@@ -2238,7 +2355,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2238
2355
|
content: `[dive-into stop condition] 已多次提示汇报工具结果仍未完成 (超过 ${MAX_UNREPORTED_RETRIES} 次). 现在直接基于你已知的信息写最终回复给用户, 用 <final gen> 结尾, 不要再调任何工具.`
|
|
2239
2356
|
});
|
|
2240
2357
|
if (onStream) {
|
|
2241
|
-
onStream({ type: 'status', content: `🔄 工具结果汇报超限, 强制收尾`, tool: 'system' });
|
|
2358
|
+
onStream({ type: 'status', internal: true, content: `🔄 工具结果汇报超限, 强制收尾`, tool: 'system' });
|
|
2242
2359
|
}
|
|
2243
2360
|
}
|
|
2244
2361
|
lastQualityScore = this.estimateResponseQuality(reply);
|
|
@@ -2268,7 +2385,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2268
2385
|
console.log(`[PiAgent] review ${loopReviewCount}/${DEFAULT_MAX_REVIEWS}: LLM 想 final 但先对齐需求深挖一次`);
|
|
2269
2386
|
this.messageHistory.push({ role: 'system', content: reviewDecision.hint });
|
|
2270
2387
|
if (onStream) {
|
|
2271
|
-
onStream({ type: 'status', content: `🔄 目标对齐 review ${loopReviewCount}/${DEFAULT_MAX_REVIEWS}: 深挖续跑`, tool: 'system' });
|
|
2388
|
+
onStream({ type: 'status', internal: true, content: `🔄 目标对齐 review ${loopReviewCount}/${DEFAULT_MAX_REVIEWS}: 深挖续跑`, tool: 'system' });
|
|
2272
2389
|
}
|
|
2273
2390
|
continue; // 让 LLM 看到 hint, 深挖或确认完成后再次 final
|
|
2274
2391
|
}
|
|
@@ -2292,14 +2409,14 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2292
2409
|
if (needsMoreWork && iteration < this.MAX_REACT_ITERATIONS) {
|
|
2293
2410
|
console.log(`[PiAgent] 继续循环处理 (${iteration}/${this.MAX_REACT_ITERATIONS}): needsMoreWork=${needsMoreWork}, hasError=${hasError}, containsToolCallIntent=${containsToolCallIntent}`);
|
|
2294
2411
|
if (onStream) {
|
|
2295
|
-
onStream({ type: 'status', content: `🔄 继续处理,循环 ${iteration}...`, tool: 'loop' });
|
|
2412
|
+
onStream({ type: 'status', internal: true, content: `🔄 继续处理,循环 ${iteration}...`, tool: 'loop' });
|
|
2296
2413
|
}
|
|
2297
2414
|
continue;
|
|
2298
2415
|
}
|
|
2299
2416
|
// 否则把这个当作可能的最终回答
|
|
2300
2417
|
finalResponse = reply;
|
|
2301
2418
|
if (onStream) {
|
|
2302
|
-
onStream({ type: 'status', content: `📝 提取最终回答,长度 ${reply.length}`, tool: 'system' });
|
|
2419
|
+
onStream({ type: 'status', internal: true, content: `📝 提取最终回答,长度 ${reply.length}`, tool: 'system' });
|
|
2303
2420
|
}
|
|
2304
2421
|
break;
|
|
2305
2422
|
}
|
|
@@ -2317,7 +2434,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2317
2434
|
}
|
|
2318
2435
|
// 通知前端循环完成
|
|
2319
2436
|
if (onStream) {
|
|
2320
|
-
onStream({ type: 'status', content: `✅ 处理完成,共 ${iteration - 1} 次循环`, tool: 'system' });
|
|
2437
|
+
onStream({ type: 'status', internal: true, content: `✅ 处理完成,共 ${iteration - 1} 次循环`, tool: 'system' });
|
|
2321
2438
|
}
|
|
2322
2439
|
this.messageHistory.push({ role: 'assistant', content: finalResponse });
|
|
2323
2440
|
// React Harness: 循环结束
|
|
@@ -2407,7 +2524,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2407
2524
|
? `🎯 目标已达成 (仍要过完成门): ${view.decision.reason}`
|
|
2408
2525
|
: `🎯 目标仍在进行 (未判完成): ${view.decision.reason}`;
|
|
2409
2526
|
onStream?.({
|
|
2410
|
-
type: 'status',
|
|
2527
|
+
type: 'status', internal: true,
|
|
2411
2528
|
content: `${line}${applied.gateRejected ? ` [完成门拒绝: ${applied.gateRejected}]` : ''} `
|
|
2412
2529
|
+ `(收尾 ${view.steps} 步 · memory ${view.memories} · skill 候选 ${view.candidates} · goal=${this.currentGoalId})`,
|
|
2413
2530
|
tool: 'harness',
|
|
@@ -2511,6 +2628,11 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2511
2628
|
const source = this.projectedHistory ?? this.messageHistory;
|
|
2512
2629
|
const WINDOW = 15;
|
|
2513
2630
|
const out = [];
|
|
2631
|
+
// 2026-09-28 (前缀 KV 可命中): 带 CURRENT TURN 注入的条目要**原样回带**.
|
|
2632
|
+
// 两个理由: (1) 注入块是"当前轮的前缀", 重渲染 (重加 `[工具结果]` 前缀 / 再 slice 截断)
|
|
2633
|
+
// 会改字节 → 下一轮前缀从这条起分叉; (2) 已注入的 tool 条目在 wire 上已经是 user 形状,
|
|
2634
|
+
// 再套一次前缀就是双前缀, 连内容都变了.
|
|
2635
|
+
const isInjectedEntry = (m) => typeof m.content === 'string' && m.content.includes(CURRENT_TURN_MARKER);
|
|
2514
2636
|
// 早期历史压缩: 超过窗口时, 不直接丢弃 — 提取前段用户意图摘要注入 (同步, 无 LLM).
|
|
2515
2637
|
// 结构对齐 Context OS: System Prompt(persona) + 压缩摘要 + 最近消息.
|
|
2516
2638
|
if (source.length > WINDOW) {
|
|
@@ -2526,6 +2648,10 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2526
2648
|
});
|
|
2527
2649
|
for (const m of slice) {
|
|
2528
2650
|
const r = m.role;
|
|
2651
|
+
if (isInjectedEntry(m)) {
|
|
2652
|
+
out.push({ role: 'user', content: String(m.content) });
|
|
2653
|
+
continue;
|
|
2654
|
+
}
|
|
2529
2655
|
if (r === 'tool') {
|
|
2530
2656
|
out.push({ role: 'user', content: `[工具结果]\n${(m.content || '').slice(0, 2000)}` });
|
|
2531
2657
|
continue;
|
|
@@ -2548,6 +2674,10 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2548
2674
|
const slice = source.slice(-WINDOW);
|
|
2549
2675
|
for (const m of slice) {
|
|
2550
2676
|
const r = m.role;
|
|
2677
|
+
if (isInjectedEntry(m)) {
|
|
2678
|
+
out.push({ role: 'user', content: String(m.content) });
|
|
2679
|
+
continue;
|
|
2680
|
+
}
|
|
2551
2681
|
if (r === 'tool') {
|
|
2552
2682
|
out.push({ role: 'user', content: `[工具结果]\n${m.content || ''}` });
|
|
2553
2683
|
continue;
|
|
@@ -2571,6 +2701,20 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2571
2701
|
return [{ role: 'user', content: this.buildContext() }];
|
|
2572
2702
|
}
|
|
2573
2703
|
}
|
|
2704
|
+
/**
|
|
2705
|
+
* 2026-09-28 (前缀 KV 可命中): 把 chat() 回带的当前轮写回自己的 messageHistory.
|
|
2706
|
+
* 逻辑在模块级 `writeBackCurrentTurnInto` (纯函数, 门可以直接驱它断言);
|
|
2707
|
+
* 这里只负责接到 `this.messageHistory` 上, 并保证任何异常都静默 (写回失败不影响对话).
|
|
2708
|
+
*/
|
|
2709
|
+
writeBackCurrentTurn(wire) {
|
|
2710
|
+
try {
|
|
2711
|
+
return writeBackCurrentTurnInto(this.messageHistory, wire);
|
|
2712
|
+
}
|
|
2713
|
+
catch (err) {
|
|
2714
|
+
console.warn('[PiAgent] writeBackCurrentTurn failed (silent):', err);
|
|
2715
|
+
return 0;
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2574
2718
|
/**
|
|
2575
2719
|
* 估算 messageHistory 的 token 数 (4 字符 ≈ 1 token, 与 context-compaction 同步).
|
|
2576
2720
|
* 失败静默: 任何异常 → 0 (不阻塞)
|
|
@@ -2592,12 +2736,14 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2592
2736
|
*
|
|
2593
2737
|
* 失败静默: 全部失败 → 返回空 reply, 让上层 no-tool_use 终止
|
|
2594
2738
|
*/
|
|
2595
|
-
async callLlmWithRecovery(llm, contextOrMessages, systemPrompt, signal, onStream, tools
|
|
2739
|
+
async callLlmWithRecovery(llm, contextOrMessages, systemPrompt, signal, onStream, tools,
|
|
2740
|
+
/** 2026-09-28: 当前轮易变段 (循环进度/改进提示) —— 进 CURRENT TURN 区, 不进 system */
|
|
2741
|
+
currentTurnContext) {
|
|
2596
2742
|
// Reactive compaction 预检: 估算 token 超 80% 阈值, 跑一次
|
|
2597
2743
|
const estimated = this.estimateHistoryTokens();
|
|
2598
2744
|
if (estimated > this.maxContextTokens() * 0.8) {
|
|
2599
2745
|
console.warn(`[PiAgent] reactive compaction pre-check (${estimated} tokens > 80% threshold)`);
|
|
2600
|
-
onStream?.({ type: 'status', content: '⚠️ reactive compaction 预检触发', tool: 'recovery' });
|
|
2746
|
+
onStream?.({ type: 'status', internal: true, content: '⚠️ reactive compaction 预检触发', tool: 'recovery' });
|
|
2601
2747
|
try {
|
|
2602
2748
|
const compacted = this.compressHistorySync(this.messageHistory);
|
|
2603
2749
|
this.messageHistory = compacted;
|
|
@@ -2646,9 +2792,13 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2646
2792
|
// M3.5 (2026-06-17): 传 messages 数组 (如果 contextOrMessages 是数组) 或字符串
|
|
2647
2793
|
// 数组版让 LLM 看到结构化的 user/assistant/tool role, 而不是把 history 拼成单字符串
|
|
2648
2794
|
// Bug 5: pass tool IDs for native OpenAI tool calling
|
|
2649
|
-
|
|
2795
|
+
// 2026-09-28 (前缀 KV 可命中): 显式 purpose/source —— pi-sdk 的 ReAct 循环**就是主对话**
|
|
2796
|
+
// (完整 IMMUTABLE PREFIX + 工具全集 + CURRENT TURN, 也只有它带 cache_prompt);
|
|
2797
|
+
// currentTurnContext 是调用方自己的当前轮易变段 (循环进度/改进提示), 走 CURRENT TURN 区.
|
|
2798
|
+
// 2026-09-28: 回带最终 wire messages (含注入后的当前轮) → 上层据此写回 messageHistory.
|
|
2799
|
+
const response = await llm.chat(contextOrMessages, systemPrompt, signal, tools, 'main-agent', 'react-loop', undefined, currentTurnContext);
|
|
2650
2800
|
// 2026-06-30: 透传 toolCalls (OpenAI 协议 native) 给上层, 让 assistant message 能 emit 真 id
|
|
2651
|
-
return { reply: response.reply || '', toolCalls: response.toolCalls };
|
|
2801
|
+
return { reply: response.reply || '', toolCalls: response.toolCalls, messages: response.messages };
|
|
2652
2802
|
}
|
|
2653
2803
|
catch (err) {
|
|
2654
2804
|
// 用户主动 abort: 不重试, 立即抛
|
|
@@ -2751,12 +2901,14 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2751
2901
|
async maybeAutoCompact(onStream, signal) {
|
|
2752
2902
|
if (this.messageHistory.length < 10)
|
|
2753
2903
|
return; // 历史太短, 不值得压
|
|
2754
|
-
onStream?.({ type: 'status', content: '🗜️ 评估是否需要压缩上下文...', tool: 'compactor' });
|
|
2904
|
+
onStream?.({ type: 'status', internal: true, content: '🗜️ 评估是否需要压缩上下文...', tool: 'compactor' });
|
|
2755
2905
|
// 注入 LLM (用 getMinimax().chat, 与 judgment 注入门 / ReAct 循环同一来源)
|
|
2756
2906
|
// 给 Context Collapse (虚拟投影) 和 Auto-Compact (摘要) 共用
|
|
2757
2907
|
const llm = getMinimax();
|
|
2758
2908
|
const llmChat = async (systemPrompt, userPrompt) => {
|
|
2759
|
-
|
|
2909
|
+
// 2026-09-28 (前缀 KV 可命中): 压缩摘要是**非主对话的一次性调用** → 轻量前缀
|
|
2910
|
+
// (它不该拖满 IMMUTABLE PREFIX, 更不该把主对话的 KV slot 顶掉).
|
|
2911
|
+
const r = await llm.chat(userPrompt, systemPrompt, signal, undefined, 'auto-compact', 'pi-sdk-compact');
|
|
2760
2912
|
return r.reply;
|
|
2761
2913
|
};
|
|
2762
2914
|
// 2026-08-06: 预算 = ContextManager 配置 (1M * 55% ≈ 550K), 不再写死 8000.
|
|
@@ -2779,7 +2931,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2779
2931
|
const savedTokens = Math.max(0, beforeTokens - afterTokens);
|
|
2780
2932
|
cm.markCompressStart(beforeTokens);
|
|
2781
2933
|
onStream?.({
|
|
2782
|
-
type: 'status',
|
|
2934
|
+
type: 'status', internal: true,
|
|
2783
2935
|
content: `🗜️ 上下文压缩: ${stagesApplied || 'no-op'} | 节省 ${saved} 条 / ${savedTokens.toLocaleString()} tokens (剩余 ${result.history.length}, collapse=${isContextCollapseEnabled() ? 'on' : 'off'})`,
|
|
2784
2936
|
tool: 'compactor',
|
|
2785
2937
|
});
|
|
@@ -3022,7 +3174,8 @@ ${this.extractOperationsFromRef(operationsRef)}
|
|
|
3022
3174
|
const conversation = messages.map(m => `${m.type === 'user' ? '用户' : '助手'}: ${m.content}`).join('\n');
|
|
3023
3175
|
const llm = getMinimax();
|
|
3024
3176
|
try {
|
|
3025
|
-
const response = await llm.chat(`根据以下对话内容,为这个对话生成一个简短的名称(不超过20个字):\n\n${conversation}\n\n直接输出名称,不要其他解释。`, '命名建议'
|
|
3177
|
+
const response = await llm.chat(`根据以下对话内容,为这个对话生成一个简短的名称(不超过20个字):\n\n${conversation}\n\n直接输出名称,不要其他解释。`, '命名建议', undefined, undefined, 'chat', 'session-namer' // 非主对话: 轻量前缀, 不抢主对话的 KV slot
|
|
3178
|
+
);
|
|
3026
3179
|
const name = response.reply.trim();
|
|
3027
3180
|
// 拒绝错误回退串 (LLM 不可用时返回的占位文本)
|
|
3028
3181
|
if (!name)
|
package/dist/agents/run-store.js
CHANGED
|
@@ -39,6 +39,8 @@ export function canTransition(from, to) {
|
|
|
39
39
|
return (RUN_TRANSITIONS[from] || []).includes(to);
|
|
40
40
|
}
|
|
41
41
|
export const MAX_HARNESS_EVENTS = 50;
|
|
42
|
+
/** 模型切换账本只留最近 N 条 (审计账本, 不是全量日志) */
|
|
43
|
+
export const MAX_MODEL_SWITCHES = 20;
|
|
42
44
|
const DEFAULT_HARNESS = {
|
|
43
45
|
maxSteps: Number(process.env.BOLLOON_RUN_MAX_STEPS || 60),
|
|
44
46
|
deadlineMs: Number(process.env.BOLLOON_RUN_DEADLINE_MS || 30 * 60_000),
|
|
@@ -343,6 +345,7 @@ export async function startRun(opts) {
|
|
|
343
345
|
steps: [],
|
|
344
346
|
budget: { maxSteps: cfg.maxSteps, deadlineMs: cfg.deadlineMs },
|
|
345
347
|
recovery: [],
|
|
348
|
+
modelConfig: opts.modelConfig,
|
|
346
349
|
};
|
|
347
350
|
await coreWrite('startRun', rec.runId, () => writeRun(rec));
|
|
348
351
|
return rec;
|
|
@@ -453,6 +456,56 @@ export async function recordHarnessEvent(runId, evt) {
|
|
|
453
456
|
await recordDegradation({ kind: 'observational', op: 'recordHarnessEvent', runId, message: `${evt.event}/${evt.kind} 写入失败: ${String(err?.message || err).slice(0, 150)}` });
|
|
454
457
|
}
|
|
455
458
|
}
|
|
459
|
+
/**
|
|
460
|
+
* 2026-09-26 (P7): 记一次模型切换决定 (Run 事件)。
|
|
461
|
+
*
|
|
462
|
+
* 与 `recordHarnessEvent` 同一纪律: 决策**已经**做出, 记账失败绝不反过来改决策 ——
|
|
463
|
+
* 所以这里只记降级 + 返回 `{ok:false}` 让调用方能如实上报, 不抛。
|
|
464
|
+
* 写两处事实: ① `modelSwitches` 账本 (机器可查的切换链); ② `harness` 事件 (人工审计的那本账)。
|
|
465
|
+
* 本次写入**不改** `modelConfig` —— Run 快照一旦定稿就不再改写。
|
|
466
|
+
*/
|
|
467
|
+
export async function recordModelSwitch(runId, evt) {
|
|
468
|
+
const full = {
|
|
469
|
+
ts: evt.ts || new Date().toISOString(),
|
|
470
|
+
...evt,
|
|
471
|
+
runId: evt.runId || runId,
|
|
472
|
+
};
|
|
473
|
+
try {
|
|
474
|
+
const rec = await withRunLock(runId, async () => {
|
|
475
|
+
const cur = await readRun(runId);
|
|
476
|
+
if (!cur)
|
|
477
|
+
return null;
|
|
478
|
+
cur.modelSwitches = cur.modelSwitches || [];
|
|
479
|
+
cur.modelSwitches.push(full);
|
|
480
|
+
if (cur.modelSwitches.length > MAX_MODEL_SWITCHES) {
|
|
481
|
+
cur.modelSwitches = cur.modelSwitches.slice(-MAX_MODEL_SWITCHES);
|
|
482
|
+
}
|
|
483
|
+
// 人工审计账本: 同一件事在 harness 事件里也留一条 (kind 按结果映射, 不美化)
|
|
484
|
+
cur.harness = cur.harness || [];
|
|
485
|
+
cur.harness.push({
|
|
486
|
+
ts: full.ts,
|
|
487
|
+
event: 'model.switch',
|
|
488
|
+
kind: full.outcome === 'switched' ? 'note' : 'deny',
|
|
489
|
+
reason: `${full.outcome} mode=${full.mode} source=${full.source} → ${full.to.provider}/${full.to.model}#${full.to.configHash} · 守卫 ${full.guardsCarried} 条 · ${full.reason}`.slice(0, 400),
|
|
490
|
+
runId,
|
|
491
|
+
goalId: full.goalId,
|
|
492
|
+
});
|
|
493
|
+
if (cur.harness.length > MAX_HARNESS_EVENTS)
|
|
494
|
+
cur.harness = cur.harness.slice(-MAX_HARNESS_EVENTS);
|
|
495
|
+
cur.updatedAt = new Date().toISOString();
|
|
496
|
+
await writeRun(cur);
|
|
497
|
+
return cur;
|
|
498
|
+
});
|
|
499
|
+
if (!rec)
|
|
500
|
+
return { ok: false, reason: `run 不存在: ${runId}` };
|
|
501
|
+
return { ok: true, record: rec };
|
|
502
|
+
}
|
|
503
|
+
catch (err) {
|
|
504
|
+
const reason = `模型切换事件写入失败: ${String(err?.message || err).slice(0, 160)}`;
|
|
505
|
+
await recordDegradation({ kind: 'observational', op: 'recordModelSwitch', runId, message: reason });
|
|
506
|
+
return { ok: false, reason };
|
|
507
|
+
}
|
|
508
|
+
}
|
|
456
509
|
/** 防止"同一个工具 + 同一组参数"无限重试: 返回该指纹最近的连续失败次数 */
|
|
457
510
|
export function repeatedFailureCount(rec, tool, argsDigest) {
|
|
458
511
|
let n = 0;
|
|
@@ -19,6 +19,7 @@ import * as fs from 'fs';
|
|
|
19
19
|
import * as crypto from 'crypto';
|
|
20
20
|
import { createGoal, attachRun, markCriterion, readGoal, setCriteria, } from '../goal-store.js';
|
|
21
21
|
import { startRun, recordStep, finishRun, readRun } from '../run-store.js';
|
|
22
|
+
import { captureRunModelConfig } from '../../llm/model-selection.js';
|
|
22
23
|
import { listTransactions, updateTransaction, setTransactionStatus } from '../x402/transaction-store.js';
|
|
23
24
|
import { bridgeTransactionToRunGoal } from '../x402/goal-run-bridge.js';
|
|
24
25
|
import { buyInfoAsTransaction } from '../x402/trade.js';
|
|
@@ -315,7 +316,13 @@ export async function runTask(opts) {
|
|
|
315
316
|
return { ok: false, card, text: renderReportCard(card), stages, budget, outputIssues: [] };
|
|
316
317
|
}
|
|
317
318
|
}
|
|
318
|
-
const run = await startRun({
|
|
319
|
+
const run = await startRun({
|
|
320
|
+
surface: 'cli',
|
|
321
|
+
goal: opts.task,
|
|
322
|
+
goalId: goal.goalId,
|
|
323
|
+
// 2026-09-26: 任务执行链同样要记下"这一跑用的是哪一份模型配置"
|
|
324
|
+
modelConfig: await captureRunModelConfig().catch(() => undefined),
|
|
325
|
+
});
|
|
319
326
|
await attachRun(goal.goalId, run.runId);
|
|
320
327
|
const fail = async (stage, note, cardOpts, runStatus = 'needs_human') => {
|
|
321
328
|
mark(stage, note);
|