@bolloon/bolloon-agent 0.5.0 → 0.5.1
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/execution-supervisor.js +87 -5
- package/dist/agents/model-policy.js +651 -0
- package/dist/agents/pi-sdk-tools.js +84 -33
- package/dist/agents/pi-sdk.js +83 -23
- package/dist/agents/run-store.js +53 -0
- package/dist/agents/task/task-runner.js +8 -1
- package/dist/agents/workflow-pivot-loop.js +19 -12
- 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/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 +83 -23
- 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 +19 -12
- 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 +70 -11
- 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 +70 -11
- 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 +308 -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
|
@@ -3185,9 +3185,9 @@ export function registerBuiltinTools(ctx) {
|
|
|
3185
3185
|
});
|
|
3186
3186
|
ctx.tools.set('bolloon_config_set', {
|
|
3187
3187
|
name: 'bolloon_config_set',
|
|
3188
|
-
description: '修改 Bolloon 自身配置 (~/.bolloon/bolloon-config.json). 可切换激活供应商 (provider) 或改某供应商的 model/baseUrl/temperature/enabled.
|
|
3188
|
+
description: '修改 Bolloon 自身配置 (~/.bolloon/bolloon-config.json). 可切换激活供应商 (provider) 或改某供应商的 model/baseUrl/temperature/enabled/maxTokens. 切换类改动走**统一入口** (先校验探测, 过了才落盘并重建模型运行时), 所以调用后返回的"当前生效"就是真的生效的那一份. 例: provider=deepseek; provider=minimax, model=MiniMax-M3; deepseek.temperature=0.3',
|
|
3189
3189
|
parameters: {
|
|
3190
|
-
provider: '可选: 切换激活供应商 (
|
|
3190
|
+
provider: '可选: 切换激活供应商 (内置的 13 家 + 已注册的自定义供应商)',
|
|
3191
3191
|
'provider.key': '可选: 修改指定供应商字段, 格式 <供应商>.<字段>=<值>, 如 minimax.model=MiniMax-M3',
|
|
3192
3192
|
model: '可选: 同时设置激活供应商的 model',
|
|
3193
3193
|
temperature: '可选: 同时设置激活供应商的 temperature (0-2)',
|
|
@@ -3195,64 +3195,115 @@ export function registerBuiltinTools(ctx) {
|
|
|
3195
3195
|
execute: async (args) => {
|
|
3196
3196
|
try {
|
|
3197
3197
|
const { llmConfigStore } = await import('../llm/config-store.js');
|
|
3198
|
+
const { selectModel, formatEffectiveModel, listRegisteredProviderIds } = await import('../llm/model-selection.js');
|
|
3198
3199
|
await llmConfigStore.initialize();
|
|
3199
3200
|
const changes = [];
|
|
3200
|
-
//
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3201
|
+
// 切换类改动累积起来**一次性**交给统一入口 (校验+探测+落盘+重建运行时都在它里面)。
|
|
3202
|
+
// `provider.field=` 里 model/baseUrl 是"选择自己的字段": 只在这家**正在用**时才算切换
|
|
3203
|
+
// (改一家没在用的供应商的地址/模型 = 配置编辑, 不该顺手把它切成激活)。
|
|
3204
|
+
const switchReq = {};
|
|
3205
|
+
const activeBefore = await llmConfigStore.getActiveProvider().catch(() => '');
|
|
3206
|
+
const optIn = (provider, field, val) => {
|
|
3207
|
+
if (!switchReq.provider)
|
|
3208
|
+
switchReq.provider = provider;
|
|
3209
|
+
if (switchReq.provider !== provider)
|
|
3210
|
+
return false;
|
|
3211
|
+
if (field === 'model')
|
|
3212
|
+
switchReq.model = val;
|
|
3213
|
+
else if (field === 'baseUrl')
|
|
3214
|
+
switchReq.baseUrl = val;
|
|
3215
|
+
else
|
|
3216
|
+
return false;
|
|
3217
|
+
return true;
|
|
3218
|
+
};
|
|
3219
|
+
const switchProvider = args.provider ? String(args.provider).trim().toLowerCase() : '';
|
|
3220
|
+
if (switchProvider) {
|
|
3221
|
+
switchReq.provider = switchProvider;
|
|
3222
|
+
if (args.model)
|
|
3223
|
+
switchReq.model = String(args.model);
|
|
3224
|
+
if (args.temperature !== undefined) {
|
|
3225
|
+
const t = Number(args.temperature);
|
|
3226
|
+
if (Number.isNaN(t))
|
|
3227
|
+
return { success: false, error: `temperature=${args.temperature} 不是数字` };
|
|
3228
|
+
switchReq.temperature = t;
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
// 1. 非选择的字段 (temperature/maxTokens/enabled/apiKey) 与"没在用那家"的字段 —— 配置编辑
|
|
3210
3232
|
for (const [k, v] of Object.entries(args)) {
|
|
3211
3233
|
if (k === 'provider' || k === 'model' || k === 'temperature')
|
|
3212
3234
|
continue;
|
|
3213
3235
|
if (!k.includes('.'))
|
|
3214
3236
|
continue;
|
|
3215
3237
|
const [prov, field] = k.split('.');
|
|
3238
|
+
const p = prov.trim().toLowerCase();
|
|
3216
3239
|
const val = String(v);
|
|
3240
|
+
const isSelectionField = field === 'model' || field === 'baseUrl';
|
|
3241
|
+
if (isSelectionField && (switchProvider ? switchProvider === p : activeBefore === p)) {
|
|
3242
|
+
optIn(p, field, val); // 改的是"正在用的那一份" → 走入口 (顺带重建运行时)
|
|
3243
|
+
continue;
|
|
3244
|
+
}
|
|
3217
3245
|
if (field === 'temperature' || field === 'maxTokens') {
|
|
3218
3246
|
const num = Number(val);
|
|
3219
3247
|
if (Number.isNaN(num))
|
|
3220
3248
|
return { success: false, error: `${k}=${val} 不是数字` };
|
|
3221
|
-
await llmConfigStore.updateProvider(
|
|
3249
|
+
await llmConfigStore.updateProvider(p, { [field]: num });
|
|
3222
3250
|
}
|
|
3223
3251
|
else if (field === 'enabled') {
|
|
3224
|
-
await llmConfigStore.updateProvider(
|
|
3252
|
+
await llmConfigStore.updateProvider(p, { enabled: val === 'true' || val === '1' });
|
|
3225
3253
|
}
|
|
3226
3254
|
else if (field === 'apiKey') {
|
|
3227
|
-
await llmConfigStore.updateProvider(
|
|
3255
|
+
await llmConfigStore.updateProvider(p, { apiKey: val });
|
|
3228
3256
|
}
|
|
3229
3257
|
else {
|
|
3230
|
-
await llmConfigStore.updateProvider(
|
|
3258
|
+
await llmConfigStore.updateProvider(p, { [field]: val });
|
|
3231
3259
|
}
|
|
3232
|
-
changes.push(`${
|
|
3260
|
+
changes.push(`${p}.${field}=${field === 'apiKey' ? '***' : val}`);
|
|
3233
3261
|
}
|
|
3234
|
-
//
|
|
3235
|
-
if (args.model || args.temperature) {
|
|
3236
|
-
|
|
3237
|
-
const patch = {};
|
|
3262
|
+
// 2. 选择的字段 (model/baseUrl) 单独给 activate 供应商时, 也走入口
|
|
3263
|
+
if (!switchProvider && (args.model || args.temperature !== undefined)) {
|
|
3264
|
+
switchReq.provider = activeBefore;
|
|
3238
3265
|
if (args.model)
|
|
3239
|
-
|
|
3240
|
-
if (args.temperature) {
|
|
3266
|
+
switchReq.model = String(args.model);
|
|
3267
|
+
if (args.temperature !== undefined) {
|
|
3241
3268
|
const t = Number(args.temperature);
|
|
3242
3269
|
if (Number.isNaN(t))
|
|
3243
3270
|
return { success: false, error: `temperature=${args.temperature} 不是数字` };
|
|
3244
|
-
|
|
3271
|
+
switchReq.temperature = t;
|
|
3245
3272
|
}
|
|
3246
|
-
await llmConfigStore.updateProvider(active, patch);
|
|
3247
|
-
if (args.model)
|
|
3248
|
-
changes.push(`${active}.model=${args.model}`);
|
|
3249
|
-
if (args.temperature)
|
|
3250
|
-
changes.push(`${active}.temperature=${args.temperature}`);
|
|
3251
3273
|
}
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3274
|
+
// 3. 一次性提交给统一入口 —— 它是唯一做"校验+探测+落盘+重建运行时"的地方
|
|
3275
|
+
if (switchReq.provider) {
|
|
3276
|
+
const r = await selectModel({ ...switchReq, scope: 'global' });
|
|
3277
|
+
if (!r.ok) {
|
|
3278
|
+
// 失败时入口保证"配置与运行时都没变" —— 如实说, 不假装成功
|
|
3279
|
+
return {
|
|
3280
|
+
success: false,
|
|
3281
|
+
error: `切换 ${switchReq.provider} 未生效 [${r.failureClass || '未知'}]: ${r.message}` +
|
|
3282
|
+
(r.previous ? `\n 仍在用: ${formatEffectiveModel(r.previous)}` : ''),
|
|
3283
|
+
};
|
|
3284
|
+
}
|
|
3285
|
+
const bits = [`activeProvider=${r.effective.provider}`];
|
|
3286
|
+
if (switchReq.model)
|
|
3287
|
+
bits.push(`model=${r.effective.model}`);
|
|
3288
|
+
if (switchReq.baseUrl)
|
|
3289
|
+
bits.push(`baseUrl=${r.effective.baseUrl}`);
|
|
3290
|
+
if (switchReq.temperature !== undefined)
|
|
3291
|
+
bits.push(`temperature=${switchReq.temperature}`);
|
|
3292
|
+
changes.push(...bits);
|
|
3293
|
+
}
|
|
3294
|
+
if (changes.length === 0) {
|
|
3295
|
+
return {
|
|
3296
|
+
success: false,
|
|
3297
|
+
error: `没有要修改的配置项. 例: provider=deepseek 或 minimax.model=MiniMax-M3. 可用供应商: ${listRegisteredProviderIds().join(', ')}`,
|
|
3298
|
+
};
|
|
3299
|
+
}
|
|
3300
|
+
// 结尾一律回**真正生效**的那一份 (入口写完就是它; 只有配置编辑时也从入口读, 免得各读各的)
|
|
3301
|
+
const { effectiveModelConfig } = await import('../llm/model-selection.js');
|
|
3302
|
+
const eff = await effectiveModelConfig({});
|
|
3303
|
+
return {
|
|
3304
|
+
success: true,
|
|
3305
|
+
output: `✅ 配置已更新: ${changes.join(', ')}\n 当前生效: ${eff ? formatEffectiveModel(eff) : '读不出来'}`,
|
|
3306
|
+
};
|
|
3256
3307
|
}
|
|
3257
3308
|
catch (e) {
|
|
3258
3309
|
return { success: false, error: `bolloon_config_set 失败: ${String(e.message || e).slice(0, 200)}` };
|
package/dist/agents/pi-sdk.js
CHANGED
|
@@ -30,6 +30,9 @@ 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';
|
|
33
36
|
import { PiAgentHarness } from './pi-harness.js';
|
|
34
37
|
import { DiscoveredAgentsManager, createSocialHeartbeat } from '../social/heartbeat.js';
|
|
35
38
|
import { SkillRegistry } from '@bolloon/constraint-runtime';
|
|
@@ -645,6 +648,18 @@ export class PiAgentSession {
|
|
|
645
648
|
return false;
|
|
646
649
|
}
|
|
647
650
|
}
|
|
651
|
+
/**
|
|
652
|
+
* 2026-09-26: 这个 Run 开始那一刻**真实生效**的模型配置快照, 由 startRun 落进 Run 记录。
|
|
653
|
+
* 拿不到 (配置读不出来) 就返回 undefined —— 宁可这个字段缺, 也不编一个假的进去。
|
|
654
|
+
*/
|
|
655
|
+
async runModelSnapshot() {
|
|
656
|
+
try {
|
|
657
|
+
return await captureRunModelConfig();
|
|
658
|
+
}
|
|
659
|
+
catch {
|
|
660
|
+
return undefined;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
648
663
|
/** 2026-08-07: prompt 出口统一上报 token 用量到 ContextManager (fallback/pivot/react 全路径覆盖) —
|
|
649
664
|
* 之前只有 runReActLoop 迭代内上报, chitchat/fallback/pivot 路径状态栏恒 0 */
|
|
650
665
|
reportUsageToContextManager() {
|
|
@@ -749,6 +764,7 @@ export class PiAgentSession {
|
|
|
749
764
|
goalId: boundGoalId,
|
|
750
765
|
channelId: this.currentChannelId || undefined,
|
|
751
766
|
agentId: this.currentAgentId || undefined,
|
|
767
|
+
modelConfig: await this.runModelSnapshot(),
|
|
752
768
|
});
|
|
753
769
|
this.lastRunId = rec.runId;
|
|
754
770
|
await attachRun(boundGoalId, rec.runId).catch(() => null);
|
|
@@ -1081,7 +1097,7 @@ export class PiAgentSession {
|
|
|
1081
1097
|
const backoffMs = 1000 * Math.pow(2, attempt - 1); // 1s, 2s, 4s
|
|
1082
1098
|
console.log(`[PiAgent] loop 自动重试 ${attempt}/${MAX_LOOP_RETRIES}, 等待 ${backoffMs}ms`);
|
|
1083
1099
|
if (onStream) {
|
|
1084
|
-
onStream({ type: 'status', content: `↻ 自动重试 loop ${attempt}/${MAX_LOOP_RETRIES} (${(backoffMs / 1000).toFixed(0)}s 后)`, tool: 'system' });
|
|
1100
|
+
onStream({ type: 'status', internal: true, content: `↻ 自动重试 loop ${attempt}/${MAX_LOOP_RETRIES} (${(backoffMs / 1000).toFixed(0)}s 后)`, tool: 'system' });
|
|
1085
1101
|
}
|
|
1086
1102
|
// 中途 abort 也要响应
|
|
1087
1103
|
await new Promise((resolve, reject) => {
|
|
@@ -1381,17 +1397,51 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1381
1397
|
* 2026-09-16 (M2): 从 checkpoint 恢复一次运行 —— **不是**重发原 prompt。
|
|
1382
1398
|
* 语义: prepareResume (校验状态 + 读 checkpoint + 记 recovery + 落 recovering) → 用恢复指令
|
|
1383
1399
|
* 驱动同一个 runId 继续 (历史保留), 非幂等动作由重放守卫挡住。
|
|
1400
|
+
*
|
|
1401
|
+
* 2026-09-26: 恢复前**反查一次模型配置**。Run 记录里存的 `modelConfig` 快照此前只被写进
|
|
1402
|
+
* 快照和回显, 没人拿它核对现状 —— 于是"快照记的是 A, 现在盘上生效的是 B"这种情况会被
|
|
1403
|
+
* 静默继续下去 (长任务的历史执行链就说不清了)。现在: 漂了就 `console.warn` 说清哪几个字段漂了,
|
|
1404
|
+
* 并把结论放进返回值 (`modelDrift`) 让调用方能上报; 一致时也有一句明确结论。
|
|
1405
|
+
* 判断失败 (读不到 Run 等) 不阻塞恢复。
|
|
1384
1406
|
*/
|
|
1385
1407
|
async resumeRun(runId) {
|
|
1408
|
+
let modelDrift;
|
|
1409
|
+
let modelApplied;
|
|
1410
|
+
try {
|
|
1411
|
+
const { detectRunConfigDrift } = await import('../llm/model-selection.js');
|
|
1412
|
+
modelDrift = (await detectRunConfigDrift(runId)) || undefined;
|
|
1413
|
+
if (modelDrift?.drifted) {
|
|
1414
|
+
console.warn('[pi-sdk] 恢复时模型配置已偏离 Run 快照:', modelDrift.message);
|
|
1415
|
+
}
|
|
1416
|
+
else if (modelDrift) {
|
|
1417
|
+
console.log('[pi-sdk] 恢复前核对:', modelDrift.message);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
catch { /* 核对本身失败不阻塞恢复 */ }
|
|
1386
1421
|
const prep = await prepareResume(runId);
|
|
1387
1422
|
if (!prep.ok || !prep.plan)
|
|
1388
|
-
return { ok: false, reason: prep.reason };
|
|
1423
|
+
return { ok: false, reason: prep.reason, ...(modelDrift ? { modelDrift } : {}) };
|
|
1424
|
+
// 2026-09-26 (P6 × P7): 恢复一个在跑的 Run, 用的是**它自己那份快照**, 不是盘上现在的全局默认 ——
|
|
1425
|
+
// 否则这次恢复就是"带着漂移继续跑", 之后再存快照会把漂移固化成历史。
|
|
1426
|
+
// 只在"漂了"或"核对不了"时重装 (一致时运行时本来就是对的, 重装是白跑一趟)。
|
|
1427
|
+
// 装配只有一处实现: `applyRunModelConfigToRuntime` → `installRuntime` (这里不自己 initMinimax)。
|
|
1428
|
+
if (modelDrift && (modelDrift.drifted || !modelDrift.verified)) {
|
|
1429
|
+
try {
|
|
1430
|
+
const { applyRunModelConfigToRuntime } = await import('../llm/model-selection.js');
|
|
1431
|
+
modelApplied = await applyRunModelConfigToRuntime(modelDrift.snapshot);
|
|
1432
|
+
console.log(`[pi-sdk] 已按 Run 快照装配运行时: ${modelApplied.provider}/${modelApplied.model} (configHash ${String(modelApplied.configHash).slice(0, 12)})`);
|
|
1433
|
+
}
|
|
1434
|
+
catch (e) {
|
|
1435
|
+
// 装配失败不阻塞恢复 (如实说; 调用方可以从返回值里看出没装成)
|
|
1436
|
+
console.warn('[pi-sdk] 按 Run 快照装配运行时失败, 保持当前运行时:', String(e?.message || e).slice(0, 160));
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1389
1439
|
this.resumeRunId = runId;
|
|
1390
1440
|
this.resumePlan = prep.plan;
|
|
1391
1441
|
this.currentGoalId = prep.plan.goalId || this.currentGoalId;
|
|
1392
1442
|
try {
|
|
1393
1443
|
const reply = await this.prompt(buildResumeInstruction(prep.plan), {});
|
|
1394
|
-
return { ok: true, reply };
|
|
1444
|
+
return { ok: true, reply, ...(modelDrift ? { modelDrift } : {}), ...(modelApplied ? { modelApplied } : {}) };
|
|
1395
1445
|
}
|
|
1396
1446
|
finally {
|
|
1397
1447
|
this.resumeRunId = '';
|
|
@@ -1461,7 +1511,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1461
1511
|
const loopActionLog = [];
|
|
1462
1512
|
// 发送循环开始的事件
|
|
1463
1513
|
if (onStream) {
|
|
1464
|
-
onStream({ type: 'status', content: '🔄 开始 ReAct 循环...', tool: 'system' });
|
|
1514
|
+
onStream({ type: 'status', internal: true, content: '🔄 开始 ReAct 循环...', tool: 'system' });
|
|
1465
1515
|
}
|
|
1466
1516
|
// React Harness: 循环开始 (重置 turn 计数 + 触发 harness sessionStart)
|
|
1467
1517
|
// 失败静默 (fail-open), 不阻塞主循环
|
|
@@ -1493,7 +1543,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1493
1543
|
}
|
|
1494
1544
|
const doneN = this.resumePlan?.completedSteps.length ?? 0;
|
|
1495
1545
|
const guards = this.resumePlan?.replayGuards.length ?? 0;
|
|
1496
|
-
onStream?.({ type: 'status', content: `♻️ 从 checkpoint 恢复运行 ${this.currentRunId} (已完成 ${doneN} 步, 非幂等重放守卫 ${guards} 条)`, tool: 'harness' });
|
|
1546
|
+
onStream?.({ type: 'status', internal: true, content: `♻️ 从 checkpoint 恢复运行 ${this.currentRunId} (已完成 ${doneN} 步, 非幂等重放守卫 ${guards} 条)`, tool: 'harness' });
|
|
1497
1547
|
}
|
|
1498
1548
|
else {
|
|
1499
1549
|
// 2026-09-16 (M2): 目标绑定 —— 有 goalId 就在该 Goal 下执行; 没有就建 Goal 再建 Run。
|
|
@@ -1536,6 +1586,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1536
1586
|
goalId: boundGoalId || undefined,
|
|
1537
1587
|
channelId: this.currentChannelId || undefined,
|
|
1538
1588
|
agentId: this.currentAgentId || undefined,
|
|
1589
|
+
modelConfig: await this.runModelSnapshot(),
|
|
1539
1590
|
});
|
|
1540
1591
|
this.currentRunId = rec.runId;
|
|
1541
1592
|
this.lastRunId = rec.runId;
|
|
@@ -1544,7 +1595,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1544
1595
|
// Run → Goal 反查链: runId → goalId → objective / success criteria
|
|
1545
1596
|
await attachRun(this.currentGoalId, rec.runId).catch((err) => console.warn('[PiAgent] attachRun 失败:', err?.message));
|
|
1546
1597
|
}
|
|
1547
|
-
onStream?.({ type: 'status', content: `🧷 运行已登记 (run=${rec.runId}${this.currentGoalId ? `, goal=${this.currentGoalId}` : ''}, 预算 ${rec.budget.maxSteps} 步 / ${Math.round(rec.budget.deadlineMs / 60000)} 分钟)`, tool: 'harness' });
|
|
1598
|
+
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
1599
|
}
|
|
1549
1600
|
catch (err) {
|
|
1550
1601
|
// 核心写失败: 不再 warn 后继续 —— 没有运行记录就不执行 (strict 模式默认如此)
|
|
@@ -1662,7 +1713,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1662
1713
|
if (shouldCompactBeforeIteration(estimatedTokensBefore, compactThreshold)) {
|
|
1663
1714
|
const tokensBeforeCompact = estimatedTokensBefore;
|
|
1664
1715
|
console.log(`[PiAgent] loop 入口 token ${tokensBeforeCompact} > ${compactThreshold}, 触发自动压缩`);
|
|
1665
|
-
onStream?.({ type: 'status', content: `🗜️ loop 自动压缩 (token ${tokensBeforeCompact} > ${compactThreshold})`, tool: 'compactor' });
|
|
1716
|
+
onStream?.({ type: 'status', internal: true, content: `🗜️ loop 自动压缩 (token ${tokensBeforeCompact} > ${compactThreshold})`, tool: 'compactor' });
|
|
1666
1717
|
try {
|
|
1667
1718
|
await this.maybeAutoCompact(onStream, signal);
|
|
1668
1719
|
}
|
|
@@ -1684,7 +1735,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1684
1735
|
// 调试日志:显示每次循环开始
|
|
1685
1736
|
console.log(`[PiAgent] 循环 ${iteration}/${this.MAX_REACT_ITERATIONS} 开始`);
|
|
1686
1737
|
if (onStream) {
|
|
1687
|
-
onStream({ type: 'status', content: `🔄 循环 ${iteration}/${this.MAX_REACT_ITERATIONS}`, tool: 'loop' });
|
|
1738
|
+
onStream({ type: 'status', internal: true, content: `🔄 循环 ${iteration}/${this.MAX_REACT_ITERATIONS}`, tool: 'loop' });
|
|
1688
1739
|
}
|
|
1689
1740
|
const context = this.buildContext();
|
|
1690
1741
|
// M3.5 (2026-06-17): 也构造 messages 数组版本, 让 LLM 看到结构化 tool 角色
|
|
@@ -1873,7 +1924,10 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1873
1924
|
}
|
|
1874
1925
|
// 路径 B: 文本解析 (parseAllToolCalls 收集全部)
|
|
1875
1926
|
if (toolCalls.length === 0) {
|
|
1876
|
-
|
|
1927
|
+
// 2026-09-26: 已知名集合 = 注册表原名 + 它们的 API 名 (净化后的).
|
|
1928
|
+
// 出网时工具面给的是 API 名 (见 pi-ai.ts 的唯一净化边界), LLM 在文本里
|
|
1929
|
+
// 回吐的也可能是 API 名 —— 只用原名集合过滤会把这类调用整个丢掉.
|
|
1930
|
+
const knownTools = expandKnownToolNames(this.tools.keys());
|
|
1877
1931
|
toolCalls = parseAllToolCalls(reply, { tools: knownTools });
|
|
1878
1932
|
}
|
|
1879
1933
|
// 回退路径 C: 原生 parseToolCall (单个)
|
|
@@ -1882,6 +1936,12 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1882
1936
|
if (single)
|
|
1883
1937
|
toolCalls.push(single);
|
|
1884
1938
|
}
|
|
1939
|
+
// 2026-09-26: **回程派发的唯一还原点** —— LLM 回吐的是 API 名 (净化过的), 这里
|
|
1940
|
+
// 还原成注册表真名再交给 this.tools.get(); 原名 (LLM 照 system prompt 抄的) 原样穿透.
|
|
1941
|
+
for (const tc of toolCalls) {
|
|
1942
|
+
if (typeof tc.name === 'string' && tc.name)
|
|
1943
|
+
tc.name = resolveApiToolName(tc.name);
|
|
1944
|
+
}
|
|
1885
1945
|
// 给每个 toolCall 分配稳定 id
|
|
1886
1946
|
for (const tc of toolCalls) {
|
|
1887
1947
|
if (!tc.id) {
|
|
@@ -1906,7 +1966,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1906
1966
|
if (onStream) {
|
|
1907
1967
|
onStream({ type: 'tool', content: `🔧 调用工具 (${ti + 1}/${toolCalls.length}): ${toolCall.name}`, tool: toolCall.name });
|
|
1908
1968
|
if (toolCall.args && Object.keys(toolCall.args).length > 0) {
|
|
1909
|
-
onStream({ type: 'status', content: `📋 参数: ${JSON.stringify(toolCall.args)}`, tool: toolCall.name });
|
|
1969
|
+
onStream({ type: 'status', internal: true, content: `📋 参数: ${JSON.stringify(toolCall.args)}`, tool: toolCall.name });
|
|
1910
1970
|
}
|
|
1911
1971
|
onStream({
|
|
1912
1972
|
type: 'step_start',
|
|
@@ -2036,7 +2096,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2036
2096
|
const toolDurationMs = Date.now() - toolStart;
|
|
2037
2097
|
if (replaySkip) {
|
|
2038
2098
|
console.log(`[PiAgent] 恢复重放守卫: 跳过已完成的非幂等工具 ${toolCall.name}`);
|
|
2039
|
-
onStream?.({ type: 'status', content: `🛡️ 恢复保护: ${toolCall.name} 此前已成功执行, 本次不重复执行`, tool: toolCall.name });
|
|
2099
|
+
onStream?.({ type: 'status', internal: true, content: `🛡️ 恢复保护: ${toolCall.name} 此前已成功执行, 本次不重复执行`, tool: toolCall.name });
|
|
2040
2100
|
}
|
|
2041
2101
|
console.log(`[PiAgent] 工具 ${toolCall.name} 执行完成: success=${result.success} (${toolDurationMs}ms)`);
|
|
2042
2102
|
// 2026-09-16 (M3): 失败接线 —— 分类 + recovery 留痕 + 熔断 / 外部等待
|
|
@@ -2109,7 +2169,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2109
2169
|
}
|
|
2110
2170
|
if (onStream) {
|
|
2111
2171
|
if (result.success) {
|
|
2112
|
-
onStream({ type: 'status', content: `✅ ${toolCall.name} 执行成功`, tool: toolCall.name });
|
|
2172
|
+
onStream({ type: 'status', internal: true, content: `✅ ${toolCall.name} 执行成功`, tool: toolCall.name });
|
|
2113
2173
|
if (result.output) {
|
|
2114
2174
|
onStream({ type: 'tool', content: `📤 结果: ${result.output.substring(0, 200)}${result.output.length > 200 ? '...' : ''}`, tool: toolCall.name });
|
|
2115
2175
|
}
|
|
@@ -2139,7 +2199,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2139
2199
|
refineAttempts++;
|
|
2140
2200
|
}
|
|
2141
2201
|
if (onStream) {
|
|
2142
|
-
onStream({ type: 'status', content: `🔄 工具执行完成,继续循环...`, tool: 'loop' });
|
|
2202
|
+
onStream({ type: 'status', internal: true, content: `🔄 工具执行完成,继续循环...`, tool: 'loop' });
|
|
2143
2203
|
}
|
|
2144
2204
|
}
|
|
2145
2205
|
else {
|
|
@@ -2225,7 +2285,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2225
2285
|
content: `[dive-into stop condition] 你之前已成功执行了 ${unreported} 个工具, 但当前回复里没把它们的结果告诉用户. 请基于已有的工具结果 (在 history 里) 写一个完整总结回复给用户, 用 <final gen> 结尾. 不要再调工具.`
|
|
2226
2286
|
});
|
|
2227
2287
|
if (onStream) {
|
|
2228
|
-
onStream({ type: 'status', content: `🔄 还有 ${unreported} 个工具结果未汇报, 让 LLM 继续总结 (${unreportedRetries}/${MAX_UNREPORTED_RETRIES})`, tool: 'system' });
|
|
2288
|
+
onStream({ type: 'status', internal: true, content: `🔄 还有 ${unreported} 个工具结果未汇报, 让 LLM 继续总结 (${unreportedRetries}/${MAX_UNREPORTED_RETRIES})`, tool: 'system' });
|
|
2229
2289
|
}
|
|
2230
2290
|
continue;
|
|
2231
2291
|
}
|
|
@@ -2238,7 +2298,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2238
2298
|
content: `[dive-into stop condition] 已多次提示汇报工具结果仍未完成 (超过 ${MAX_UNREPORTED_RETRIES} 次). 现在直接基于你已知的信息写最终回复给用户, 用 <final gen> 结尾, 不要再调任何工具.`
|
|
2239
2299
|
});
|
|
2240
2300
|
if (onStream) {
|
|
2241
|
-
onStream({ type: 'status', content: `🔄 工具结果汇报超限, 强制收尾`, tool: 'system' });
|
|
2301
|
+
onStream({ type: 'status', internal: true, content: `🔄 工具结果汇报超限, 强制收尾`, tool: 'system' });
|
|
2242
2302
|
}
|
|
2243
2303
|
}
|
|
2244
2304
|
lastQualityScore = this.estimateResponseQuality(reply);
|
|
@@ -2268,7 +2328,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2268
2328
|
console.log(`[PiAgent] review ${loopReviewCount}/${DEFAULT_MAX_REVIEWS}: LLM 想 final 但先对齐需求深挖一次`);
|
|
2269
2329
|
this.messageHistory.push({ role: 'system', content: reviewDecision.hint });
|
|
2270
2330
|
if (onStream) {
|
|
2271
|
-
onStream({ type: 'status', content: `🔄 目标对齐 review ${loopReviewCount}/${DEFAULT_MAX_REVIEWS}: 深挖续跑`, tool: 'system' });
|
|
2331
|
+
onStream({ type: 'status', internal: true, content: `🔄 目标对齐 review ${loopReviewCount}/${DEFAULT_MAX_REVIEWS}: 深挖续跑`, tool: 'system' });
|
|
2272
2332
|
}
|
|
2273
2333
|
continue; // 让 LLM 看到 hint, 深挖或确认完成后再次 final
|
|
2274
2334
|
}
|
|
@@ -2292,14 +2352,14 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2292
2352
|
if (needsMoreWork && iteration < this.MAX_REACT_ITERATIONS) {
|
|
2293
2353
|
console.log(`[PiAgent] 继续循环处理 (${iteration}/${this.MAX_REACT_ITERATIONS}): needsMoreWork=${needsMoreWork}, hasError=${hasError}, containsToolCallIntent=${containsToolCallIntent}`);
|
|
2294
2354
|
if (onStream) {
|
|
2295
|
-
onStream({ type: 'status', content: `🔄 继续处理,循环 ${iteration}...`, tool: 'loop' });
|
|
2355
|
+
onStream({ type: 'status', internal: true, content: `🔄 继续处理,循环 ${iteration}...`, tool: 'loop' });
|
|
2296
2356
|
}
|
|
2297
2357
|
continue;
|
|
2298
2358
|
}
|
|
2299
2359
|
// 否则把这个当作可能的最终回答
|
|
2300
2360
|
finalResponse = reply;
|
|
2301
2361
|
if (onStream) {
|
|
2302
|
-
onStream({ type: 'status', content: `📝 提取最终回答,长度 ${reply.length}`, tool: 'system' });
|
|
2362
|
+
onStream({ type: 'status', internal: true, content: `📝 提取最终回答,长度 ${reply.length}`, tool: 'system' });
|
|
2303
2363
|
}
|
|
2304
2364
|
break;
|
|
2305
2365
|
}
|
|
@@ -2317,7 +2377,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2317
2377
|
}
|
|
2318
2378
|
// 通知前端循环完成
|
|
2319
2379
|
if (onStream) {
|
|
2320
|
-
onStream({ type: 'status', content: `✅ 处理完成,共 ${iteration - 1} 次循环`, tool: 'system' });
|
|
2380
|
+
onStream({ type: 'status', internal: true, content: `✅ 处理完成,共 ${iteration - 1} 次循环`, tool: 'system' });
|
|
2321
2381
|
}
|
|
2322
2382
|
this.messageHistory.push({ role: 'assistant', content: finalResponse });
|
|
2323
2383
|
// React Harness: 循环结束
|
|
@@ -2407,7 +2467,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2407
2467
|
? `🎯 目标已达成 (仍要过完成门): ${view.decision.reason}`
|
|
2408
2468
|
: `🎯 目标仍在进行 (未判完成): ${view.decision.reason}`;
|
|
2409
2469
|
onStream?.({
|
|
2410
|
-
type: 'status',
|
|
2470
|
+
type: 'status', internal: true,
|
|
2411
2471
|
content: `${line}${applied.gateRejected ? ` [完成门拒绝: ${applied.gateRejected}]` : ''} `
|
|
2412
2472
|
+ `(收尾 ${view.steps} 步 · memory ${view.memories} · skill 候选 ${view.candidates} · goal=${this.currentGoalId})`,
|
|
2413
2473
|
tool: 'harness',
|
|
@@ -2597,7 +2657,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2597
2657
|
const estimated = this.estimateHistoryTokens();
|
|
2598
2658
|
if (estimated > this.maxContextTokens() * 0.8) {
|
|
2599
2659
|
console.warn(`[PiAgent] reactive compaction pre-check (${estimated} tokens > 80% threshold)`);
|
|
2600
|
-
onStream?.({ type: 'status', content: '⚠️ reactive compaction 预检触发', tool: 'recovery' });
|
|
2660
|
+
onStream?.({ type: 'status', internal: true, content: '⚠️ reactive compaction 预检触发', tool: 'recovery' });
|
|
2601
2661
|
try {
|
|
2602
2662
|
const compacted = this.compressHistorySync(this.messageHistory);
|
|
2603
2663
|
this.messageHistory = compacted;
|
|
@@ -2751,7 +2811,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2751
2811
|
async maybeAutoCompact(onStream, signal) {
|
|
2752
2812
|
if (this.messageHistory.length < 10)
|
|
2753
2813
|
return; // 历史太短, 不值得压
|
|
2754
|
-
onStream?.({ type: 'status', content: '🗜️ 评估是否需要压缩上下文...', tool: 'compactor' });
|
|
2814
|
+
onStream?.({ type: 'status', internal: true, content: '🗜️ 评估是否需要压缩上下文...', tool: 'compactor' });
|
|
2755
2815
|
// 注入 LLM (用 getMinimax().chat, 与 judgment 注入门 / ReAct 循环同一来源)
|
|
2756
2816
|
// 给 Context Collapse (虚拟投影) 和 Auto-Compact (摘要) 共用
|
|
2757
2817
|
const llm = getMinimax();
|
|
@@ -2779,7 +2839,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2779
2839
|
const savedTokens = Math.max(0, beforeTokens - afterTokens);
|
|
2780
2840
|
cm.markCompressStart(beforeTokens);
|
|
2781
2841
|
onStream?.({
|
|
2782
|
-
type: 'status',
|
|
2842
|
+
type: 'status', internal: true,
|
|
2783
2843
|
content: `🗜️ 上下文压缩: ${stagesApplied || 'no-op'} | 节省 ${saved} 条 / ${savedTokens.toLocaleString()} tokens (剩余 ${result.history.length}, collapse=${isContextCollapseEnabled() ? 'on' : 'off'})`,
|
|
2784
2844
|
tool: 'compactor',
|
|
2785
2845
|
});
|
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);
|