@bolloon/bolloon-agent 0.4.23 → 0.4.25
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 +391 -0
- package/dist/agents/goal-store.js +451 -0
- package/dist/agents/pi-harness.js +263 -0
- package/dist/agents/pi-sdk.js +568 -126
- package/dist/agents/run-store.js +772 -0
- package/dist/agents/runner-resolver.js +225 -0
- package/dist/agents/skills-manager.js +435 -0
- package/dist/agents/supervisor-host.js +249 -0
- package/dist/cron/tick-lock.js +1 -1
- package/dist/index.js +428 -22
- package/dist/ios/agent-delegate-server.js +58 -12
- package/dist/ios/icons/icon-1024x1024.png +0 -0
- package/dist/ios/icons/icon-1024x1024.webp +0 -0
- package/dist/ios/icons/icon-216x216.png +0 -0
- package/dist/ios/icons/icon-216x216.webp +0 -0
- package/dist/ios/index.html +95 -18
- package/dist/ios/manifest.json +1 -1
- package/dist/ios/mobile-agent.js +244 -5
- package/dist/ios/mobile-chain.js +481 -0
- package/dist/ios/mobile-core.js +25177 -24664
- package/dist/ios/mobile-helia.js +683 -0
- package/dist/ios/mobile-ipfs.js +680 -0
- package/dist/ios/mobile-orbit.js +268 -0
- package/dist/ios/mobile-p2p.js +130 -1
- package/dist/ios/mobile-social.js +624 -0
- package/dist/ios/mobile-sync.js +193 -0
- package/dist/ios/mobile-trade.js +492 -0
- package/dist/ios/mobile-wallet.js +11 -0
- package/dist/ios/mobile.css +39 -0
- package/dist/ios/mobile.html +95 -18
- package/dist/ios/mobile.js +1054 -55
- package/dist/ios/routes-x402-info.js +194 -0
- package/dist/ios/server.js +350 -9
- package/dist/ios/sw.js +26 -2
- package/dist/web/agent-delegate-server.js +58 -12
- package/dist/web/icons/icon-1024x1024.png +0 -0
- package/dist/web/icons/icon-1024x1024.webp +0 -0
- package/dist/web/icons/icon-216x216.png +0 -0
- package/dist/web/icons/icon-216x216.webp +0 -0
- package/dist/web/manifest.json +1 -1
- package/dist/web/mobile-agent.js +197 -3
- package/dist/web/mobile-core.js +16417 -16090
- package/dist/web/mobile-privacy.js +185 -0
- package/dist/web/mobile.css +15 -0
- package/dist/web/mobile.html +21 -1
- package/dist/web/mobile.js +179 -6
- package/dist/web/server.js +437 -4
- package/package.json +2 -2
package/dist/agents/pi-sdk.js
CHANGED
|
@@ -25,6 +25,9 @@ import { WorkflowEngine } from './workflow-engine.js';
|
|
|
25
25
|
import { DeepThinkingEngine, AgentCoordinator } from '@bolloon/constraint-runtime';
|
|
26
26
|
import { WorkflowPivotLoop, createDefaultPivotConfig } from './workflow-pivot-loop.js';
|
|
27
27
|
import { initDocumentReceiver } from './p2p-document-tools.js';
|
|
28
|
+
import { startRun, recordStep, finishRun, readRun, budgetVerdict, recordDegradation, recordHarnessEvent, recordRecovery, setRunStatus, prepareResume, markRunRunning, buildResumeInstruction, argsDigestOf, repeatedFailureCount, classifyError as classifyRunError } from './run-store.js';
|
|
29
|
+
import { createGoal, readGoal, attachRun, findActiveGoal, completeGoalIfEligible, setUnresolved, addEvidence, evaluateGoalCompletion } from './goal-store.js';
|
|
30
|
+
import { PiAgentHarness } from './pi-harness.js';
|
|
28
31
|
import { DiscoveredAgentsManager, createSocialHeartbeat } from '../social/heartbeat.js';
|
|
29
32
|
import { SkillRegistry } from '@bolloon/constraint-runtime';
|
|
30
33
|
import { loadSkillsFromPaths, defaultSkillPaths } from './skill-loader.js';
|
|
@@ -59,10 +62,15 @@ import { buildObservation, buildReflection, formatObservationWithReflection } fr
|
|
|
59
62
|
import { sessionStore as defaultSessionStore } from './session-store.js';
|
|
60
63
|
import { ToolRegistry } from './tool-registry.js';
|
|
61
64
|
import { decideMaxIterations, decideContextOverflow, shouldCompactBeforeIteration } from './react-loop.js';
|
|
62
|
-
import {
|
|
65
|
+
import { DEFAULT_MAX_REVIEWS } from './loop-review.js';
|
|
63
66
|
// PiSessionManager 已抽到 ./pi-sdk-session-manager.ts (2026-07-06)
|
|
64
67
|
// Tool / ToolResult / Message / StreamCallback / StreamEvent / HeartbeatConfig / AgentSession / TOOL_DEFINITIONS
|
|
65
68
|
// 已抽到 ./pi-sdk-types.ts (2026-07-06)
|
|
69
|
+
/**
|
|
70
|
+
* 同一工具 + 同一组参数连续失败上限 (2026-07-01 起) —— 触发两条互不替代的动作:
|
|
71
|
+
* 循环内: 注入 system 提示强制 LLM 收尾; 运行层 (M3): 熔断 → needs_human (交人)。
|
|
72
|
+
*/
|
|
73
|
+
const MAX_SAME_TOOL_FAILURES = 3;
|
|
66
74
|
export class PiAgentSession {
|
|
67
75
|
cwd;
|
|
68
76
|
peerId;
|
|
@@ -220,6 +228,25 @@ export class PiAgentSession {
|
|
|
220
228
|
currentIntent = 'chitchat';
|
|
221
229
|
/** 2026-08-10: 本轮用户原始输入 (loop-review 任务动词兜底检测用) */
|
|
222
230
|
currentUserInput = '';
|
|
231
|
+
/**
|
|
232
|
+
* 2026-09-16: 持久化 run harness — 本次运行的落盘记录 id + 表面 (cli/web/cron/delegate)。
|
|
233
|
+
* 之前只有"跑完才写"的轨迹 (trajectory-store, fire-and-forget, 崩了就没有), 没有跨重载可见的
|
|
234
|
+
* 运行事实, 也没有预算闸门/失速巡检; 这里补的就是那一层。
|
|
235
|
+
*/
|
|
236
|
+
currentRunId = '';
|
|
237
|
+
/** 上一次运行的 runId (收尾不清空; 见 getLastRunId) */
|
|
238
|
+
lastRunId = '';
|
|
239
|
+
runSurface = 'cli';
|
|
240
|
+
/**
|
|
241
|
+
* 2026-09-16 (Milestone 1): 本次运行因**持久化失败**被硬停。
|
|
242
|
+
* 标记出来是为了让外层 loop 重试逻辑别重试 —— 盘写不进去, 重试 3 次也只是再失败 3 次
|
|
243
|
+
* (协议: 同一个错误不许无限重复; 该交人时交人)。
|
|
244
|
+
*/
|
|
245
|
+
runPersistenceBlocked = false;
|
|
246
|
+
/** 由 server / CLI / cron 注入运行表面 (影响落盘记录里的 surface 字段) */
|
|
247
|
+
setRunSurface(surface) {
|
|
248
|
+
this.runSurface = surface;
|
|
249
|
+
}
|
|
223
250
|
currentIntentHint = '';
|
|
224
251
|
/**
|
|
225
252
|
* 算 judgment 注入门: 失败静默, 不阻塞主对话
|
|
@@ -669,7 +696,41 @@ export class PiAgentSession {
|
|
|
669
696
|
content: input
|
|
670
697
|
});
|
|
671
698
|
if (!this.minimaxAvailable) {
|
|
699
|
+
// 2026-09-16 (2-C.2): fallback **也必须留下 Run 事实**。
|
|
700
|
+
// 旧行为: 直接返回兜底文案, 连 Run 都不建 —— 上层 (独立宿主/Supervisor) 只看到"执行完成但没有 Run",
|
|
701
|
+
// 于是把"什么都没跑"当成一次正常执行 (这正是"agent 跑了但没记录"的老毛病在 fallback 路径的翻版)。
|
|
672
702
|
const response = await this.handleFallback(input);
|
|
703
|
+
try {
|
|
704
|
+
let boundGoalId = this.currentGoalId;
|
|
705
|
+
if (!boundGoalId) {
|
|
706
|
+
const g = await createGoal({
|
|
707
|
+
objective: this.currentUserInput || input.slice(0, 200),
|
|
708
|
+
channelId: this.currentChannelId || undefined,
|
|
709
|
+
agentId: this.currentAgentId || undefined,
|
|
710
|
+
createdBy: this.runSurface,
|
|
711
|
+
});
|
|
712
|
+
boundGoalId = g.goalId;
|
|
713
|
+
}
|
|
714
|
+
this.currentGoalId = boundGoalId;
|
|
715
|
+
const rec = await startRun({
|
|
716
|
+
surface: this.runSurface,
|
|
717
|
+
goal: this.currentUserInput || input.slice(0, 200),
|
|
718
|
+
goalId: boundGoalId,
|
|
719
|
+
channelId: this.currentChannelId || undefined,
|
|
720
|
+
agentId: this.currentAgentId || undefined,
|
|
721
|
+
});
|
|
722
|
+
this.lastRunId = rec.runId;
|
|
723
|
+
await attachRun(boundGoalId, rec.runId).catch(() => null);
|
|
724
|
+
await recordStep(rec.runId, { tool: 'llm', ok: false, error: 'LLM 不可用 (provider 未初始化/无 apiKey) → 走了 fallback' });
|
|
725
|
+
await finishRun(rec.runId, {
|
|
726
|
+
status: 'needs_human',
|
|
727
|
+
error: 'LLM 不可用: provider 未初始化或无 apiKey (fallback 不是执行结果, 需要配置或人工处理)',
|
|
728
|
+
});
|
|
729
|
+
console.log(`[PiAgent] LLM 不可用 → 本次仍落 Run ${rec.runId} (needs_human), 不伪装成正常执行`);
|
|
730
|
+
}
|
|
731
|
+
catch (err) {
|
|
732
|
+
await recordDegradation({ kind: 'core', op: 'pi-sdk.fallbackRun', runId: this.lastRunId || '', message: String(err?.message || err).slice(0, 160) }).catch(() => { });
|
|
733
|
+
}
|
|
673
734
|
this.messageHistory.push({ role: 'assistant', content: response });
|
|
674
735
|
this.reportUsageToContextManager();
|
|
675
736
|
this.finishTrajectory(trajRec, response);
|
|
@@ -940,6 +1001,9 @@ export class PiAgentSession {
|
|
|
940
1001
|
try {
|
|
941
1002
|
const loopResult = await this.runReActLoop(onStream, signal);
|
|
942
1003
|
result = loopResult.reply;
|
|
1004
|
+
// 持久化失败不重试 (写不进去就是写不进去): 直接按本次结果收尾
|
|
1005
|
+
if (this.runPersistenceBlocked)
|
|
1006
|
+
break;
|
|
943
1007
|
if (!loopResult.aiFailed)
|
|
944
1008
|
break; // 正常完成, 退出 retry 循环
|
|
945
1009
|
lastAiFailureReason = loopResult.aiFailureReason || 'AI 调用失败';
|
|
@@ -1134,6 +1198,155 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1134
1198
|
this.clearJudgmentGate();
|
|
1135
1199
|
return result;
|
|
1136
1200
|
}
|
|
1201
|
+
/**
|
|
1202
|
+
* 2026-09-16 (Milestone 1-B): 唯一 Harness 门面 — 约束层的**唯一**入口。
|
|
1203
|
+
* pi-sdk 从此刻起不再分别调用 deny-pipeline / validator / react-harness / hooks / loop-review,
|
|
1204
|
+
* 只调 harness.*; 由 Harness 决定约束顺序与失败处置 (见 pi-harness.ts)。
|
|
1205
|
+
*/
|
|
1206
|
+
_harness = null;
|
|
1207
|
+
/** M2 绑定 GoalStore 后填真值; 在此之前为空 (事件里 goalId 字段已就位) */
|
|
1208
|
+
currentGoalId = '';
|
|
1209
|
+
/** 2026-09-16 (M2): 本次执行是"从 checkpoint 恢复"的 runId (非空 = 恢复模式, 不再新建 run) */
|
|
1210
|
+
resumeRunId = '';
|
|
1211
|
+
resumePlan = null;
|
|
1212
|
+
/** 2026-09-16 (M3): 外部等待中 (awaiting_external) —— 收到成功步骤后回 running */
|
|
1213
|
+
awaitingExternal = false;
|
|
1214
|
+
/** 2026-09-16 (M3): 熔断原因 (同一工具连续失败达上限 → needs_human) */
|
|
1215
|
+
breakerReason = '';
|
|
1216
|
+
/** 2026-09-16 (M3): 运行中核心写失败 (状态迁移等) —— 交给循环顶部硬闸, 不吞 */
|
|
1217
|
+
persistenceFailure = '';
|
|
1218
|
+
/**
|
|
1219
|
+
* 2026-09-16 (M2-B): Supervisor 开新 Run 继续同一 Goal 时注入"上一个 Run 已成功的非幂等动作"。
|
|
1220
|
+
* 与 resume 的重放守卫同源 (run-store 的 buildContinuationPlan), 保证跨 Run 也不重复副作用。
|
|
1221
|
+
*/
|
|
1222
|
+
continuationGuards = [];
|
|
1223
|
+
/** Supervisor / CLI 注入跨 Run 重放守卫 */
|
|
1224
|
+
setContinuationGuards(guards) {
|
|
1225
|
+
this.continuationGuards = Array.isArray(guards) ? guards : [];
|
|
1226
|
+
}
|
|
1227
|
+
/** CLI / Web 注入"当前目标" (有 goalId 就在该 Goal 下执行) */
|
|
1228
|
+
setGoalId(goalId) {
|
|
1229
|
+
this.currentGoalId = String(goalId || '');
|
|
1230
|
+
}
|
|
1231
|
+
/** 核心状态迁移的兜底: 失败 → 记降级 + 交给循环顶部的持久化硬闸 (不吞错) */
|
|
1232
|
+
async safeSetRunStatus(runId, to) {
|
|
1233
|
+
try {
|
|
1234
|
+
const r = await setRunStatus(runId, to);
|
|
1235
|
+
return !!r.ok;
|
|
1236
|
+
}
|
|
1237
|
+
catch (err) {
|
|
1238
|
+
this.persistenceFailure = `状态迁移失败 (${to}): ${String(err?.message || err).slice(0, 180)}`;
|
|
1239
|
+
await recordDegradation({ kind: 'core', op: 'pi-sdk.setRunStatus', runId, message: this.persistenceFailure }).catch(() => { });
|
|
1240
|
+
return false;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* 2026-09-16 (M3): 工具失败接线 —— 分类 → recovery 留痕 → 熔断 / 外部等待。
|
|
1245
|
+
* 这是"错误恢复闭环"的运行时接线点 (此前 recordRecovery / repeatedFailureCount 只是数据结构)。
|
|
1246
|
+
*/
|
|
1247
|
+
async wireToolFailure(tool, errorText, args) {
|
|
1248
|
+
if (!this.currentRunId)
|
|
1249
|
+
return;
|
|
1250
|
+
const cls = classifyRunError(errorText);
|
|
1251
|
+
const digest = argsDigestOf(args);
|
|
1252
|
+
let repeats = 1;
|
|
1253
|
+
try {
|
|
1254
|
+
const rec = await readRun(this.currentRunId);
|
|
1255
|
+
// recordStep 已经把**这次**失败记进去了, 所以"连续失败次数"直接就是当前值 (不再 +1)
|
|
1256
|
+
const seen = rec ? repeatedFailureCount(rec, tool, digest) : 0;
|
|
1257
|
+
repeats = Math.max(seen, 1);
|
|
1258
|
+
}
|
|
1259
|
+
catch { /* 读不到就按首次记 */ }
|
|
1260
|
+
const action = cls === 'transient' ? 'backoff'
|
|
1261
|
+
: cls === 'auth' ? 'escalate'
|
|
1262
|
+
: cls === 'external_no_reply' ? 'pause'
|
|
1263
|
+
: 'retry';
|
|
1264
|
+
try {
|
|
1265
|
+
await recordRecovery(this.currentRunId, {
|
|
1266
|
+
errorClass: cls,
|
|
1267
|
+
message: `${tool}: ${errorText}`.slice(0, 200),
|
|
1268
|
+
action,
|
|
1269
|
+
attempt: repeats,
|
|
1270
|
+
recovered: false,
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
catch (err) {
|
|
1274
|
+
await recordDegradation({ kind: 'core', op: 'pi-sdk.recordRecovery', runId: this.currentRunId, message: String(err?.message || err) }).catch(() => { });
|
|
1275
|
+
}
|
|
1276
|
+
// 外部无响应 → awaiting_external (不算失败; 等回话后回到 running)
|
|
1277
|
+
if (cls === 'external_no_reply') {
|
|
1278
|
+
this.awaitingExternal = true;
|
|
1279
|
+
await this.safeSetRunStatus(this.currentRunId, 'awaiting_external');
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
// 鉴权类: 不重试, 直接交人
|
|
1283
|
+
if (cls === 'auth') {
|
|
1284
|
+
this.breakerReason = `鉴权类错误不重试 (${tool}): ${errorText.slice(0, 120)}`;
|
|
1285
|
+
await this.safeSetRunStatus(this.currentRunId, 'needs_human');
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
// 重复失败熔断: 同一工具 + 同一组参数连续失败达 3 次
|
|
1289
|
+
if (repeats >= MAX_SAME_TOOL_FAILURES) {
|
|
1290
|
+
this.breakerReason = `同一工具 ${tool} 连续失败 ${repeats} 次 (${cls}) → 熔断, 不再重试`;
|
|
1291
|
+
await this.safeSetRunStatus(this.currentRunId, 'needs_human');
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
getRunId() {
|
|
1295
|
+
return this.currentRunId;
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* 2026-09-16 (M2-B): **上一次**运行的 runId —— prompt 收尾时 `currentRunId` 会被清空,
|
|
1299
|
+
* 而 Supervisor / 控制面要在运行结束后才知道"刚才跑的是哪条 run" (否则会拿旧 run 做决策)。
|
|
1300
|
+
*/
|
|
1301
|
+
getLastRunId() {
|
|
1302
|
+
return this.lastRunId || this.currentRunId;
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* 2026-09-16 (M2): 从 checkpoint 恢复一次运行 —— **不是**重发原 prompt。
|
|
1306
|
+
* 语义: prepareResume (校验状态 + 读 checkpoint + 记 recovery + 落 recovering) → 用恢复指令
|
|
1307
|
+
* 驱动同一个 runId 继续 (历史保留), 非幂等动作由重放守卫挡住。
|
|
1308
|
+
*/
|
|
1309
|
+
async resumeRun(runId) {
|
|
1310
|
+
const prep = await prepareResume(runId);
|
|
1311
|
+
if (!prep.ok || !prep.plan)
|
|
1312
|
+
return { ok: false, reason: prep.reason };
|
|
1313
|
+
this.resumeRunId = runId;
|
|
1314
|
+
this.resumePlan = prep.plan;
|
|
1315
|
+
this.currentGoalId = prep.plan.goalId || this.currentGoalId;
|
|
1316
|
+
try {
|
|
1317
|
+
const reply = await this.prompt(buildResumeInstruction(prep.plan), {});
|
|
1318
|
+
return { ok: true, reply };
|
|
1319
|
+
}
|
|
1320
|
+
finally {
|
|
1321
|
+
this.resumeRunId = '';
|
|
1322
|
+
this.resumePlan = null;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
piHarness() {
|
|
1326
|
+
if (!this._harness) {
|
|
1327
|
+
this._harness = new PiAgentHarness({
|
|
1328
|
+
reactHarness: this.reactHarness,
|
|
1329
|
+
denyPipeline: this._denyPipeline,
|
|
1330
|
+
hooks: this._hooks,
|
|
1331
|
+
// pre-tool-validator 4 步链 (modeGate/blacklist/shell-guard/schema), 经 human-value-pipeline 包装
|
|
1332
|
+
preToolUse: async (o) => onPreToolUse({ tool: o.tool, args: o.args, permissionMode: o.permissionMode }),
|
|
1333
|
+
// 事件写 Run: 观测级 (记账失败不改变已做出的决策)
|
|
1334
|
+
events: (e) => { if (this.currentRunId)
|
|
1335
|
+
void recordHarnessEvent(this.currentRunId, e); },
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
return this._harness;
|
|
1339
|
+
}
|
|
1340
|
+
/** 每个生命周期事件都带上的运行身份 (runId / goalId / agentId / channelId / surface) */
|
|
1341
|
+
harnessCtx() {
|
|
1342
|
+
return {
|
|
1343
|
+
runId: this.currentRunId || undefined,
|
|
1344
|
+
goalId: this.currentGoalId || undefined,
|
|
1345
|
+
agentId: this.currentAgentId || undefined,
|
|
1346
|
+
channelId: this.currentChannelId || undefined,
|
|
1347
|
+
surface: this.runSurface,
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1137
1350
|
async runReActLoop(onStream, signal) {
|
|
1138
1351
|
const llm = getMinimax();
|
|
1139
1352
|
let iteration = 0;
|
|
@@ -1150,7 +1363,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1150
1363
|
let aiFailed = false;
|
|
1151
1364
|
let aiFailureReason = '';
|
|
1152
1365
|
const MAX_CONSECUTIVE_ERRORS = 3;
|
|
1153
|
-
|
|
1366
|
+
// 同一工具连续失败 3 次, 强制让 LLM 给出最终答案 (模块级常量 MAX_SAME_TOOL_FAILURES 也用它做熔断)
|
|
1154
1367
|
// 2026-07-29: Hermes 风格硬限制 — 防死循环 (不再靠 soft hint)
|
|
1155
1368
|
const MAX_IDEMPOTENT_TOOL = 5; // 同工具成功调 5 次 → 注入 hint 强制 final gen
|
|
1156
1369
|
const MAX_TOOL_CALLS_PER_LOOP = 25; // 单轮循环总工具调用上限 → 注入 hint
|
|
@@ -1176,19 +1389,147 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1176
1389
|
}
|
|
1177
1390
|
// React Harness: 循环开始 (重置 turn 计数 + 触发 harness sessionStart)
|
|
1178
1391
|
// 失败静默 (fail-open), 不阻塞主循环
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1392
|
+
// 2026-09-16 (Milestone 1-B): 会话开启走唯一门面 (react-harness 8-gate 复位 + hooks onLoopStart)
|
|
1393
|
+
await this.piHarness().sessionStart(this.harnessCtx());
|
|
1394
|
+
// 2026-09-16: 持久化 run harness — 本次运行立即落盘 (~/.bolloon/runs/<id>.json)。
|
|
1395
|
+
// 之后每步工具调用都追加一条, 所以刷新页面/进程重载/崩溃都能看到"做到哪一步"。
|
|
1396
|
+
let runStopReason = '';
|
|
1397
|
+
// 2026-09-16 (Milestone 1): 持久化硬约束 —— 核心 run 状态写不进去时, 运行必须停。
|
|
1398
|
+
// 理由: "agent 实际跑了但没记录" 比 "agent 没跑" 更危险 (UI 显示旧状态/重启后无从知晓/结果可能被错标 done)。
|
|
1399
|
+
let runPersistenceFailure = '';
|
|
1400
|
+
/** 2026-09-16 (M3): 熔断/需要人处置 → 收尾落 needs_human (不是 done, 也不是普通 failed) */
|
|
1401
|
+
let runNeedsHuman = '';
|
|
1402
|
+
/** 2026-09-16 (M5): 外部 (CLI/Web) 把 run 改成 paused/aborted → 如实停, 不再覆盖它的状态 */
|
|
1403
|
+
let runExternallyPaused = false;
|
|
1404
|
+
let runExternallyAborted = false;
|
|
1405
|
+
this.runPersistenceBlocked = false;
|
|
1406
|
+
this.breakerReason = '';
|
|
1407
|
+
this.awaitingExternal = false;
|
|
1408
|
+
if (this.resumeRunId) {
|
|
1409
|
+
// ── 恢复模式: 复用原来的 runId, 不新建 run (历史保留) ──
|
|
1410
|
+
this.currentRunId = this.resumeRunId;
|
|
1411
|
+
this.lastRunId = this.resumeRunId;
|
|
1412
|
+
try {
|
|
1413
|
+
await markRunRunning(this.currentRunId);
|
|
1414
|
+
}
|
|
1415
|
+
catch (err) {
|
|
1416
|
+
runPersistenceFailure = `恢复时状态迁移失败 (recovering → running): ${String(err?.message || err).slice(0, 180)}`;
|
|
1417
|
+
}
|
|
1418
|
+
const doneN = this.resumePlan?.completedSteps.length ?? 0;
|
|
1419
|
+
const guards = this.resumePlan?.replayGuards.length ?? 0;
|
|
1420
|
+
onStream?.({ type: 'status', content: `♻️ 从 checkpoint 恢复运行 ${this.currentRunId} (已完成 ${doneN} 步, 非幂等重放守卫 ${guards} 条)`, tool: 'harness' });
|
|
1184
1421
|
}
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1422
|
+
else {
|
|
1423
|
+
// 2026-09-16 (M2): 目标绑定 —— 有 goalId 就在该 Goal 下执行; 没有就建 Goal 再建 Run。
|
|
1424
|
+
// 延续规则 (确定性, 不靠猜): 该 channel/agent 上已有 open/active Goal, 且它的上一次执行**没收尾**
|
|
1425
|
+
// (interrupted/stalled/needs_human/paused/awaiting_external/recovering) → 继续该 Goal; 否则新建。
|
|
1426
|
+
let boundGoalId = this.currentGoalId;
|
|
1427
|
+
if (!boundGoalId) {
|
|
1428
|
+
try {
|
|
1429
|
+
const active = await findActiveGoal({ channelId: this.currentChannelId || undefined, agentId: this.currentAgentId || undefined });
|
|
1430
|
+
if (active?.currentRunId) {
|
|
1431
|
+
const prev = await readRun(active.currentRunId);
|
|
1432
|
+
const unfinished = prev && ['interrupted', 'stalled', 'needs_human', 'paused', 'awaiting_external', 'recovering'].includes(prev.status);
|
|
1433
|
+
if (unfinished)
|
|
1434
|
+
boundGoalId = active.goalId;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
catch (err) {
|
|
1438
|
+
console.warn('[PiAgent] 查找进行中 Goal 失败 (按新建处理):', err?.message);
|
|
1439
|
+
}
|
|
1440
|
+
if (!boundGoalId) {
|
|
1441
|
+
try {
|
|
1442
|
+
const g = await createGoal({
|
|
1443
|
+
objective: this.currentUserInput || '(未记录目标)',
|
|
1444
|
+
channelId: this.currentChannelId || undefined,
|
|
1445
|
+
agentId: this.currentAgentId || undefined,
|
|
1446
|
+
createdBy: this.runSurface,
|
|
1447
|
+
});
|
|
1448
|
+
boundGoalId = g.goalId;
|
|
1449
|
+
}
|
|
1450
|
+
catch (err) {
|
|
1451
|
+
console.warn('[PiAgent] 创建 Goal 失败 (无目标也要有运行记录):', err?.message);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
this.currentGoalId = boundGoalId;
|
|
1455
|
+
}
|
|
1456
|
+
try {
|
|
1457
|
+
const rec = await startRun({
|
|
1458
|
+
surface: this.runSurface,
|
|
1459
|
+
goal: this.currentUserInput || '(未记录目标)',
|
|
1460
|
+
goalId: boundGoalId || undefined,
|
|
1461
|
+
channelId: this.currentChannelId || undefined,
|
|
1462
|
+
agentId: this.currentAgentId || undefined,
|
|
1463
|
+
});
|
|
1464
|
+
this.currentRunId = rec.runId;
|
|
1465
|
+
this.lastRunId = rec.runId;
|
|
1466
|
+
this.currentGoalId = rec.goalId || this.currentGoalId;
|
|
1467
|
+
if (this.currentGoalId) {
|
|
1468
|
+
// Run → Goal 反查链: runId → goalId → objective / success criteria
|
|
1469
|
+
await attachRun(this.currentGoalId, rec.runId).catch((err) => console.warn('[PiAgent] attachRun 失败:', err?.message));
|
|
1470
|
+
}
|
|
1471
|
+
onStream?.({ type: 'status', content: `🧷 运行已登记 (run=${rec.runId}${this.currentGoalId ? `, goal=${this.currentGoalId}` : ''}, 预算 ${rec.budget.maxSteps} 步 / ${Math.round(rec.budget.deadlineMs / 60000)} 分钟)`, tool: 'harness' });
|
|
1472
|
+
}
|
|
1473
|
+
catch (err) {
|
|
1474
|
+
// 核心写失败: 不再 warn 后继续 —— 没有运行记录就不执行 (strict 模式默认如此)
|
|
1475
|
+
runPersistenceFailure = `无法创建运行记录: ${String(err?.message || err).slice(0, 200)}`;
|
|
1476
|
+
this.runPersistenceBlocked = true;
|
|
1477
|
+
console.error('[PiAgent] run-store startRun 失败 (核心持久化) → 拒绝无记录执行:', runPersistenceFailure);
|
|
1478
|
+
onStream?.({ type: 'error', content: `⛔ ${runPersistenceFailure} — 已停止 (不在没有记录的情况下执行)`, tool: 'harness' });
|
|
1479
|
+
}
|
|
1188
1480
|
}
|
|
1189
|
-
catch { /* hook 失败静默 */ }
|
|
1190
1481
|
while (iteration < this.MAX_REACT_ITERATIONS) {
|
|
1191
1482
|
iteration++;
|
|
1483
|
+
// 2026-09-16 (Milestone 1): 持久化失败硬闸 —— 到这一层说明记录已经不可信, 继续跑就是"无约束执行"
|
|
1484
|
+
if (runPersistenceFailure || this.persistenceFailure) {
|
|
1485
|
+
runPersistenceFailure = runPersistenceFailure || this.persistenceFailure;
|
|
1486
|
+
this.runPersistenceBlocked = true;
|
|
1487
|
+
aiFailed = true;
|
|
1488
|
+
aiFailureReason = aiFailureReason || runPersistenceFailure;
|
|
1489
|
+
finalResponse = finalResponse || `❌ 运行已停止: ${runPersistenceFailure}\n\n(运行记录无法写入/校验, 按协议停在 needs_human, 不假装完成)`;
|
|
1490
|
+
break;
|
|
1491
|
+
}
|
|
1492
|
+
// 2026-09-16 (M3): 熔断硬闸 —— 同一工具连续失败达上限后, 不许再"自动重试成功"式地把运行放活
|
|
1493
|
+
if (this.breakerReason) {
|
|
1494
|
+
runNeedsHuman = this.breakerReason;
|
|
1495
|
+
aiFailed = true;
|
|
1496
|
+
aiFailureReason = aiFailureReason || this.breakerReason;
|
|
1497
|
+
finalResponse = finalResponse || `❌ 已熔断: ${this.breakerReason}\n\n(重复失败不再重试, 按协议停在 needs_human 交人处置)`;
|
|
1498
|
+
break;
|
|
1499
|
+
}
|
|
1500
|
+
// 2026-09-16: 预算闸门 (持久化 harness 的约束面) —— 到点必须**如实**终止, 不许静默算完成
|
|
1501
|
+
if (this.currentRunId) {
|
|
1502
|
+
try {
|
|
1503
|
+
const rec = await readRun(this.currentRunId);
|
|
1504
|
+
if (!rec)
|
|
1505
|
+
throw new Error(`运行记录读不到: ${this.currentRunId}`);
|
|
1506
|
+
// 2026-09-16 (M5): 外部控制面 (CLI /pause /abort, Web API) 改过状态 → 如实停在那儿。
|
|
1507
|
+
// 不覆盖成 done/failed: 人按下暂停就是暂停, 人按下中止就是中止。
|
|
1508
|
+
if (rec.status === 'paused' || rec.status === 'aborted') {
|
|
1509
|
+
runExternallyPaused = rec.status === 'paused';
|
|
1510
|
+
runExternallyAborted = rec.status === 'aborted';
|
|
1511
|
+
runStopReason = `外部请求: ${rec.status}`;
|
|
1512
|
+
onStream?.({ type: 'error', content: `⏹️ 运行被外部${rec.status === 'paused' ? '暂停' : '中止'} (run=${this.currentRunId})`, tool: 'harness' });
|
|
1513
|
+
finalResponse = finalResponse || `(运行已${rec.status === 'paused' ? '暂停' : '中止'})`;
|
|
1514
|
+
break;
|
|
1515
|
+
}
|
|
1516
|
+
const verdict = budgetVerdict(rec);
|
|
1517
|
+
if (verdict.exceeded) {
|
|
1518
|
+
runStopReason = verdict.reason || '运行预算用尽';
|
|
1519
|
+
onStream?.({ type: 'error', content: `⛔ 运行预算用尽: ${runStopReason} (已如实终止, 不假装完成)`, tool: 'harness' });
|
|
1520
|
+
finalResponse = finalResponse || `(运行预算用尽: ${runStopReason})`;
|
|
1521
|
+
break;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
catch (err) {
|
|
1525
|
+
// 预算闸门读不到状态 = 约束失效, 不能"当作没超预算"继续跑
|
|
1526
|
+
runPersistenceFailure = `预算闸门无法校验运行状态: ${String(err?.message || err).slice(0, 200)}`;
|
|
1527
|
+
this.runPersistenceBlocked = true;
|
|
1528
|
+
console.error('[PiAgent] run-store 预算检查失败 (核心持久化):', runPersistenceFailure);
|
|
1529
|
+
onStream?.({ type: 'error', content: `⛔ ${runPersistenceFailure} — 已停止`, tool: 'harness' });
|
|
1530
|
+
break;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1192
1533
|
// 停止条件 1: max turns (fail-safe 10000, 正常任务永远跑不到)
|
|
1193
1534
|
// 2026-07-01 (v0.2.4 子任务 1): 委托给 react-loop.decideMaxIterations 纯函数
|
|
1194
1535
|
const maxIterDecision = decideMaxIterations(iteration, this.MAX_REACT_ITERATIONS);
|
|
@@ -1359,7 +1700,11 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1359
1700
|
},
|
|
1360
1701
|
});
|
|
1361
1702
|
}
|
|
1703
|
+
// 2026-09-16 (Milestone 1-B): 模型调用前后走唯一门面 (扩展点 + 计数留痕; 默认不加新 hook 事件, 避免改变现有触发次数)
|
|
1704
|
+
this.piHarness().beforeModelCall(this.harnessCtx());
|
|
1705
|
+
const _modelCallT0 = Date.now();
|
|
1362
1706
|
const response = await this.callLlmWithRecovery(llm, messages, systemPrompt, signal, onStream, openaiFormattedTools);
|
|
1707
|
+
this.piHarness().afterModelCall(this.harnessCtx(), { ms: Date.now() - _modelCallT0 });
|
|
1363
1708
|
const reply = (response.reply || '').trim();
|
|
1364
1709
|
// 2026-06-30: OpenAI 协议 native tool_calls (LLM 真产了 tool_call 时, minimax/M3 会返回 id)
|
|
1365
1710
|
const nativeToolCalls = response.toolCalls;
|
|
@@ -1494,29 +1839,6 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1494
1839
|
args: toolCall.args || {},
|
|
1495
1840
|
});
|
|
1496
1841
|
}
|
|
1497
|
-
// 2026-07-29: Unified Deny-First Pipeline — 统合所有拒绝检查
|
|
1498
|
-
let denyResult = { denied: false, reason: '', source: '' };
|
|
1499
|
-
try {
|
|
1500
|
-
denyResult = await this._denyPipeline.check({
|
|
1501
|
-
toolName: toolCall.name,
|
|
1502
|
-
toolArgs: toolCall.args || {},
|
|
1503
|
-
permissionMode: this.currentPermissionMode,
|
|
1504
|
-
channelId: this.currentChannelId,
|
|
1505
|
-
agentId: this.currentAgentId,
|
|
1506
|
-
});
|
|
1507
|
-
}
|
|
1508
|
-
catch { /* pipeline 失败不阻塞工具调用 */ }
|
|
1509
|
-
if (denyResult.denied) {
|
|
1510
|
-
consecutiveErrors++;
|
|
1511
|
-
totalErrors++;
|
|
1512
|
-
const denyResultMsg = { success: false, error: `拒绝: [${denyResult.source}] ${denyResult.reason}` };
|
|
1513
|
-
this.messageHistory.push({ role: 'tool', content: JSON.stringify(denyResultMsg), toolResult: denyResultMsg });
|
|
1514
|
-
this.logToHarness(toolCall.name, toolCall.args, denyResultMsg);
|
|
1515
|
-
return;
|
|
1516
|
-
}
|
|
1517
|
-
if (denyResult.systemAddition) {
|
|
1518
|
-
this.contextHintAddition += '\n' + denyResult.systemAddition;
|
|
1519
|
-
}
|
|
1520
1842
|
const tool = this.tools.get(toolCall.name);
|
|
1521
1843
|
if (!tool) {
|
|
1522
1844
|
consecutiveErrors++;
|
|
@@ -1533,119 +1855,165 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1533
1855
|
console.warn(`[PiAgent] 未知工具: ${toolCall.name} (累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}),跳过并继续`);
|
|
1534
1856
|
return;
|
|
1535
1857
|
}
|
|
1536
|
-
//
|
|
1537
|
-
//
|
|
1538
|
-
//
|
|
1539
|
-
let
|
|
1858
|
+
// 2026-09-16 (Milestone 1-B): 工具调用前的**唯一**约束入口。
|
|
1859
|
+
// 顺序由 PiAgentHarness 决定: deny-pipeline → pre-tool-validator(4 步链) → react-harness(8-gate)。
|
|
1860
|
+
// 旧实现是这三处在不同位置各自调用 (且有两条路径 fail-open); 现在 pi-sdk 不再散调任何 gate。
|
|
1861
|
+
let toolDecision;
|
|
1540
1862
|
try {
|
|
1541
|
-
|
|
1863
|
+
toolDecision = await this.piHarness().beforeToolCall({
|
|
1542
1864
|
tool: toolCall.name,
|
|
1543
1865
|
args: toolCall.args || {},
|
|
1866
|
+
ctx: this.harnessCtx(),
|
|
1544
1867
|
permissionMode: this.currentPermissionMode,
|
|
1545
1868
|
});
|
|
1546
|
-
if (!pre.allowed) {
|
|
1547
|
-
const deniedResult = {
|
|
1548
|
-
success: false,
|
|
1549
|
-
error: `PreToolUse 拒绝: ${pre.reason || '未通过安全校验'}`,
|
|
1550
|
-
};
|
|
1551
|
-
this.messageHistory.push({ role: 'tool', content: JSON.stringify(deniedResult), toolResult: deniedResult });
|
|
1552
|
-
this.logToHarness(toolCall.name, toolCall.args, deniedResult);
|
|
1553
|
-
if (onStream) {
|
|
1554
|
-
onStream({ type: 'error', content: `🛡️ PreToolUse 拒绝 ${toolCall.name}: ${pre.reason || '安全校验失败'}`, tool: toolCall.name });
|
|
1555
|
-
onStream({ type: 'step_error', content: `PreToolUse 拒绝 ${toolCall.name}`, tool: toolCall.name, error: pre.reason || '安全校验失败' });
|
|
1556
|
-
}
|
|
1557
|
-
console.warn(`[PiAgent] PreToolUse denied ${toolCall.name}: ${pre.reason}`);
|
|
1558
|
-
// 拒绝也算错误, 让错误恢复机制触发
|
|
1559
|
-
consecutiveErrors++;
|
|
1560
|
-
totalErrors++;
|
|
1561
|
-
if (toolCall.name === lastFailedTool) {
|
|
1562
|
-
lastFailedToolCount++;
|
|
1563
|
-
}
|
|
1564
|
-
else {
|
|
1565
|
-
lastFailedTool = toolCall.name;
|
|
1566
|
-
lastFailedToolCount = 1;
|
|
1567
|
-
}
|
|
1568
|
-
if (lastFailedToolCount >= MAX_SAME_TOOL_FAILURES) {
|
|
1569
|
-
this.messageHistory.push({ role: 'system', content: `[注意] 工具 ${toolCall.name} 被系统拒绝 (连续 ${MAX_SAME_TOOL_FAILURES} 次). 请不要再次尝试, 直接用已有信息回答用户, 末尾加 <final gen>.` });
|
|
1570
|
-
lastFailedTool = '';
|
|
1571
|
-
lastFailedToolCount = 0;
|
|
1572
|
-
consecutiveErrors = 0;
|
|
1573
|
-
}
|
|
1574
|
-
else if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
1575
|
-
this.messageHistory.push({ role: 'system', content: `[注意] 连续 ${consecutiveErrors} 次工具调用被系统拒绝. 请换其他工具或直接回答用户, 末尾加 <final gen>.` });
|
|
1576
|
-
consecutiveErrors = 0;
|
|
1577
|
-
}
|
|
1578
|
-
return;
|
|
1579
|
-
}
|
|
1580
1869
|
}
|
|
1581
1870
|
catch (err) {
|
|
1582
|
-
|
|
1871
|
+
// 门面自身抛错 = 核心约束失效 → fail-closed (不执行工具, 也不静默放行)
|
|
1872
|
+
toolDecision = {
|
|
1873
|
+
allow: false,
|
|
1874
|
+
source: 'harness-error',
|
|
1875
|
+
kind: 'core_constraint',
|
|
1876
|
+
reason: `Harness 门面异常: ${String(err?.message || err).slice(0, 150)}`,
|
|
1877
|
+
};
|
|
1583
1878
|
}
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
const deniedResult = { success: false, error: `Harness gate 拒绝 (${pre.details.rejectedBy}): ${pre.reason || '未通过安全校验'}` };
|
|
1589
|
-
this.messageHistory.push({ role: 'tool', content: JSON.stringify(deniedResult), toolResult: deniedResult });
|
|
1590
|
-
this.logToHarness(toolCall.name, toolCall.args, deniedResult);
|
|
1591
|
-
if (onStream) {
|
|
1592
|
-
onStream({ type: 'error', content: `🛡️ Harness ${pre.details.rejectedBy} 拒绝 ${toolCall.name}: ${pre.reason || '安全校验失败'}`, tool: toolCall.name });
|
|
1593
|
-
onStream({ type: 'step_error', content: `Harness 拒绝 ${toolCall.name}`, tool: toolCall.name, error: pre.reason || '安全校验失败' });
|
|
1594
|
-
}
|
|
1595
|
-
console.warn(`[PiAgent] Harness denied ${toolCall.name} (${pre.details.rejectedBy}): ${pre.reason}`);
|
|
1879
|
+
if (!toolDecision.allow) {
|
|
1880
|
+
const src = toolDecision.source || 'unknown';
|
|
1881
|
+
if (src === 'deny-pipeline') {
|
|
1882
|
+
// 旧 deny-pipeline 分支: 不计连续失败计数, 文案 "拒绝: [source] reason"
|
|
1596
1883
|
consecutiveErrors++;
|
|
1597
1884
|
totalErrors++;
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
else {
|
|
1602
|
-
lastFailedTool = toolCall.name;
|
|
1603
|
-
lastFailedToolCount = 1;
|
|
1604
|
-
}
|
|
1605
|
-
if (lastFailedToolCount >= MAX_SAME_TOOL_FAILURES) {
|
|
1606
|
-
this.messageHistory.push({ role: 'system', content: `[注意] 工具 ${toolCall.name} 被 Harness 拒绝 (连续 ${MAX_SAME_TOOL_FAILURES} 次). 请不要再次尝试, 末尾加 <final gen>.` });
|
|
1607
|
-
lastFailedTool = '';
|
|
1608
|
-
lastFailedToolCount = 0;
|
|
1609
|
-
consecutiveErrors = 0;
|
|
1610
|
-
}
|
|
1611
|
-
else if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
1612
|
-
this.messageHistory.push({ role: 'system', content: `[注意] 连续 ${consecutiveErrors} 次工具调用被 Harness 拒绝. 请换其他工具或直接回答.` });
|
|
1613
|
-
consecutiveErrors = 0;
|
|
1614
|
-
}
|
|
1885
|
+
const denyResultMsg = { success: false, error: `拒绝: [${toolDecision.rejectedBy || 'deny-pipeline'}] ${toolDecision.reason}` };
|
|
1886
|
+
this.messageHistory.push({ role: 'tool', content: JSON.stringify(denyResultMsg), toolResult: denyResultMsg });
|
|
1887
|
+
this.logToHarness(toolCall.name, toolCall.args, denyResultMsg);
|
|
1615
1888
|
return;
|
|
1616
1889
|
}
|
|
1890
|
+
const isGateDeny = src === 'react-harness';
|
|
1891
|
+
const isHarnessError = src === 'harness-error';
|
|
1892
|
+
const deniedResult = {
|
|
1893
|
+
success: false,
|
|
1894
|
+
error: isGateDeny
|
|
1895
|
+
? `Harness gate 拒绝 (${toolDecision.rejectedBy}): ${toolDecision.reason || '未通过安全校验'}`
|
|
1896
|
+
: `PreToolUse 拒绝: ${toolDecision.reason || '未通过安全校验'}`,
|
|
1897
|
+
};
|
|
1898
|
+
this.messageHistory.push({ role: 'tool', content: JSON.stringify(deniedResult), toolResult: deniedResult });
|
|
1899
|
+
this.logToHarness(toolCall.name, toolCall.args, deniedResult);
|
|
1900
|
+
if (onStream) {
|
|
1901
|
+
const gateName = isGateDeny ? `Harness ${toolDecision.rejectedBy}` : (isHarnessError ? '核心约束层' : 'PreToolUse');
|
|
1902
|
+
onStream({ type: 'error', content: `🛡️ ${gateName} 拒绝 ${toolCall.name}: ${toolDecision.reason || '安全校验失败'}`, tool: toolCall.name });
|
|
1903
|
+
onStream({ type: 'step_error', content: `${gateName} 拒绝 ${toolCall.name}`, tool: toolCall.name, error: toolDecision.reason || '安全校验失败' });
|
|
1904
|
+
}
|
|
1905
|
+
console.warn(`[PiAgent] 工具被拒 ${toolCall.name} (${src}${toolDecision.rejectedBy ? ':' + toolDecision.rejectedBy : ''}): ${toolDecision.reason}`);
|
|
1906
|
+
consecutiveErrors++;
|
|
1907
|
+
totalErrors++;
|
|
1908
|
+
if (toolCall.name === lastFailedTool) {
|
|
1909
|
+
lastFailedToolCount++;
|
|
1910
|
+
}
|
|
1911
|
+
else {
|
|
1912
|
+
lastFailedTool = toolCall.name;
|
|
1913
|
+
lastFailedToolCount = 1;
|
|
1914
|
+
}
|
|
1915
|
+
// 达到同一工具连续失败上限 / 连续错误上限时的引导语 (按拒绝来源保持原有文案)
|
|
1916
|
+
const systemMaxMsg = isGateDeny
|
|
1917
|
+
? `[注意] 工具 ${toolCall.name} 被 Harness 拒绝 (连续 ${MAX_SAME_TOOL_FAILURES} 次). 请不要再次尝试, 末尾加 <final gen>.`
|
|
1918
|
+
: isHarnessError
|
|
1919
|
+
? `[注意] 工具 ${toolCall.name} 的约束校验层失效, 已按 fail-closed 阻止 (连续 ${MAX_SAME_TOOL_FAILURES} 次). 请换其他工具或直接回答用户, 末尾加 <final gen>.`
|
|
1920
|
+
: `[注意] 工具 ${toolCall.name} 被系统拒绝 (连续 ${MAX_SAME_TOOL_FAILURES} 次). 请不要再次尝试, 直接用已有信息回答用户, 末尾加 <final gen>.`;
|
|
1921
|
+
const systemConsecMsg = isGateDeny
|
|
1922
|
+
? `[注意] 连续 ${consecutiveErrors} 次工具调用被 Harness 拒绝. 请换其他工具或直接回答.`
|
|
1923
|
+
: isHarnessError
|
|
1924
|
+
? `[注意] 连续 ${consecutiveErrors} 次工具调用因约束层失效被阻止. 请换其他工具或直接回答用户, 末尾加 <final gen>.`
|
|
1925
|
+
: `[注意] 连续 ${consecutiveErrors} 次工具调用被系统拒绝. 请换其他工具或直接回答用户, 末尾加 <final gen>.`;
|
|
1926
|
+
if (lastFailedToolCount >= MAX_SAME_TOOL_FAILURES) {
|
|
1927
|
+
this.messageHistory.push({ role: 'system', content: systemMaxMsg });
|
|
1928
|
+
lastFailedTool = '';
|
|
1929
|
+
lastFailedToolCount = 0;
|
|
1930
|
+
consecutiveErrors = 0;
|
|
1931
|
+
}
|
|
1932
|
+
else if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
1933
|
+
this.messageHistory.push({ role: 'system', content: systemConsecMsg });
|
|
1934
|
+
consecutiveErrors = 0;
|
|
1935
|
+
}
|
|
1936
|
+
return;
|
|
1617
1937
|
}
|
|
1618
|
-
|
|
1619
|
-
|
|
1938
|
+
if (toolDecision.systemAddition) {
|
|
1939
|
+
this.contextHintAddition += '\n' + toolDecision.systemAddition;
|
|
1620
1940
|
}
|
|
1621
1941
|
try {
|
|
1622
1942
|
const toolStart = Date.now();
|
|
1623
|
-
|
|
1943
|
+
// 2026-09-16 (M2): 恢复重放守卫 —— 中断前**已成功执行过的非幂等动作**恢复后不再重做,
|
|
1944
|
+
// 直接复用当时的结果 (避免重复副作用: 重复写文件/重复提交/重复付款)。
|
|
1945
|
+
// (M2-B: 守卫也覆盖"上一个 Run 已做过的非幂等动作" —— 跨 Run 续跑同样不许重做)
|
|
1946
|
+
let replaySkip = null;
|
|
1947
|
+
const guards = [
|
|
1948
|
+
...(this.resumePlan?.replayGuards || []),
|
|
1949
|
+
...(this.continuationGuards || []),
|
|
1950
|
+
];
|
|
1951
|
+
if (guards.length) {
|
|
1952
|
+
const d = argsDigestOf(toolCall.args);
|
|
1953
|
+
const hit = guards.find((g) => g.tool === toolCall.name && (!g.argsDigest || !d || g.argsDigest === d));
|
|
1954
|
+
if (hit)
|
|
1955
|
+
replaySkip = hit.summary;
|
|
1956
|
+
}
|
|
1957
|
+
let result = replaySkip
|
|
1958
|
+
? { success: true, output: `[恢复保护] ${toolCall.name} 在中断前已成功执行过, 本次不重复执行 (避免重复副作用)。当时结果: ${replaySkip}`, _replaySkipped: true }
|
|
1959
|
+
: await tool.execute(toolCall.args);
|
|
1624
1960
|
const toolDurationMs = Date.now() - toolStart;
|
|
1961
|
+
if (replaySkip) {
|
|
1962
|
+
console.log(`[PiAgent] 恢复重放守卫: 跳过已完成的非幂等工具 ${toolCall.name}`);
|
|
1963
|
+
onStream?.({ type: 'status', content: `🛡️ 恢复保护: ${toolCall.name} 此前已成功执行, 本次不重复执行`, tool: toolCall.name });
|
|
1964
|
+
}
|
|
1625
1965
|
console.log(`[PiAgent] 工具 ${toolCall.name} 执行完成: success=${result.success} (${toolDurationMs}ms)`);
|
|
1966
|
+
// 2026-09-16 (M3): 失败接线 —— 分类 + recovery 留痕 + 熔断 / 外部等待
|
|
1967
|
+
if (!result.success && this.currentRunId && !replaySkip) {
|
|
1968
|
+
await this.wireToolFailure(toolCall.name, String(result.error || ''), toolCall.args);
|
|
1969
|
+
}
|
|
1970
|
+
else if (result.success && this.currentRunId && this.awaitingExternal) {
|
|
1971
|
+
// 外部回话了: awaiting_external → running (不是"恢复完成", 只是等待结束)
|
|
1972
|
+
this.awaitingExternal = false;
|
|
1973
|
+
await this.safeSetRunStatus(this.currentRunId, 'running');
|
|
1974
|
+
}
|
|
1975
|
+
// 2026-09-16: 持久化 run harness — 每步工具调用立即落盘 (崩在这里也能看到做到哪步)
|
|
1976
|
+
if (this.currentRunId) {
|
|
1977
|
+
try {
|
|
1978
|
+
await recordStep(this.currentRunId, {
|
|
1979
|
+
tool: toolCall.name,
|
|
1980
|
+
ok: !!result.success,
|
|
1981
|
+
ms: toolDurationMs,
|
|
1982
|
+
args: toolCall.args,
|
|
1983
|
+
summary: String(result.output || '').slice(0, 200),
|
|
1984
|
+
error: result.error ? String(result.error) : undefined,
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
catch (err) {
|
|
1988
|
+
// 核心写失败 → 本轮工具批跑完即停 (循环顶部硬闸), 不再 warn 后继续
|
|
1989
|
+
runPersistenceFailure = `工具步骤写盘失败 (${toolCall.name}): ${String(err?.message || err).slice(0, 200)}`;
|
|
1990
|
+
this.runPersistenceBlocked = true;
|
|
1991
|
+
console.error('[PiAgent] run-store recordStep 失败 (核心持久化):', runPersistenceFailure);
|
|
1992
|
+
onStream?.({ type: 'error', content: `⛔ ${runPersistenceFailure} — 本轮结束后停止`, tool: 'harness' });
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1626
1995
|
try {
|
|
1627
1996
|
await onPostToolUse({ tool: toolCall.name, args: toolCall.args || {}, result: { success: result.success, output: result.output?.substring(0, 500), error: result.error }, durationMs: toolDurationMs });
|
|
1628
1997
|
}
|
|
1629
1998
|
catch (postErr) {
|
|
1630
1999
|
console.warn('[PiAgent] onPostToolUse failed (non-fatal):', postErr);
|
|
1631
2000
|
}
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
2001
|
+
// 2026-09-16 (Milestone 1-B): 工具调用后的唯一入口 (router hint + 输出 gate 一起判定)
|
|
2002
|
+
const after = await this.piHarness().afterToolCall({
|
|
2003
|
+
tool: toolCall.name,
|
|
2004
|
+
output: String(result.output || ''),
|
|
2005
|
+
ctx: this.harnessCtx(),
|
|
2006
|
+
ok: !!result.success,
|
|
2007
|
+
});
|
|
2008
|
+
if (after.routeHint?.systemAddition) {
|
|
2009
|
+
this.messageHistory.push({ role: 'system', content: `[Harness Router Hint: ${after.routeHint.reason}]\n${after.routeHint.systemAddition}` });
|
|
1636
2010
|
}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
if (onStream) {
|
|
1641
|
-
onStream({ type: 'error', content: `🛡️ Harness output 拒绝 ${toolCall.name}: ${post.reason || '输出含敏感信息'}`, tool: toolCall.name });
|
|
1642
|
-
}
|
|
1643
|
-
console.warn(`[PiAgent] Harness output denied ${toolCall.name}: ${post.reason}`);
|
|
1644
|
-
result = { ...result, output: `[harness output gate: 输出含敏感内容, 已屏蔽. 原因: ${post.reason || 'unknown'}]`, _harnessDenied: true };
|
|
2011
|
+
if (after.outputBlocked) {
|
|
2012
|
+
if (onStream) {
|
|
2013
|
+
onStream({ type: 'error', content: `🛡️ Harness output 拒绝 ${toolCall.name}: ${after.outputBlocked.reason}`, tool: toolCall.name });
|
|
1645
2014
|
}
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
console.warn('[PiAgent] reactHarness.postToolCall failed (non-fatal, allowing):', err);
|
|
2015
|
+
console.warn(`[PiAgent] Harness output denied ${toolCall.name}: ${after.outputBlocked.reason}`);
|
|
2016
|
+
result = { ...result, output: `[harness output gate: 输出含敏感内容, 已屏蔽. 原因: ${after.outputBlocked.reason}]`, _harnessDenied: true };
|
|
1649
2017
|
}
|
|
1650
2018
|
this.messageHistory.push({ role: 'tool', content: JSON.stringify(result), toolResult: result, toolCallId: toolCall.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` });
|
|
1651
2019
|
this.logToHarness(toolCall.name, toolCall.args, result);
|
|
@@ -1808,13 +2176,16 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1808
2176
|
// 2026-08-08: final 前目标对齐 review — 不潦草收尾 (见 loop-review.ts)
|
|
1809
2177
|
// LLM 想 <final gen> 时, 先跑 1-2 次「目标对齐 + 需求深挖」review;
|
|
1810
2178
|
// 达成用户需求才放行真正结束. 达上限或无需深挖则以用户需求为准结束.
|
|
1811
|
-
|
|
2179
|
+
// 2026-09-16 (Milestone 1-B): 目标审查走唯一门面 (loop-review 是 Harness 的一个环节)
|
|
2180
|
+
const reviewDecision = this.piHarness().reviewFinal({
|
|
1812
2181
|
reviewsDone: loopReviewCount,
|
|
1813
2182
|
// 2026-08-10: 传用户原始输入 (不是派生 intentHint) — LLM 对照原文自查完成度,
|
|
1814
2183
|
// 未完成 → 自动继续调工具 (自动触发后续步骤)
|
|
1815
2184
|
userIntent: this.currentUserInput,
|
|
1816
2185
|
completedTools: Array.from(loopReviewCompletedTools),
|
|
1817
2186
|
actionLog: loopActionLog,
|
|
2187
|
+
runId: this.currentRunId || undefined,
|
|
2188
|
+
goalId: this.currentGoalId || undefined,
|
|
1818
2189
|
}, DEFAULT_MAX_REVIEWS);
|
|
1819
2190
|
if (reviewDecision.kind === 'continue-review') {
|
|
1820
2191
|
loopReviewCount++;
|
|
@@ -1874,11 +2245,82 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1874
2245
|
}
|
|
1875
2246
|
this.messageHistory.push({ role: 'assistant', content: finalResponse });
|
|
1876
2247
|
// React Harness: 循环结束
|
|
1877
|
-
|
|
1878
|
-
|
|
2248
|
+
// 2026-09-16 (Milestone 1-B): 会话收尾走唯一门面
|
|
2249
|
+
await this.piHarness().sessionEnd(this.harnessCtx());
|
|
2250
|
+
// 2026-09-16: 收尾落盘 — done / failed / aborted / needs_human 如实写回 (不留幽灵 running)
|
|
2251
|
+
if (this.currentRunId && !runExternallyPaused && !runExternallyAborted) {
|
|
2252
|
+
try {
|
|
2253
|
+
// 2026-09-16 (M4): 完成门 —— "模型说完成"不等于"系统确认完成"。
|
|
2254
|
+
// 证据 = 成功步骤的事实摘要; 末尾还有失败步骤没被后续成功覆盖 → 不许 done。
|
|
2255
|
+
const recBefore = await readRun(this.currentRunId).catch(() => null);
|
|
2256
|
+
const steps = recBefore?.steps || [];
|
|
2257
|
+
const evidence = steps.filter((s) => s.ok).map((s) => `${s.tool}: ${(s.summary || '').slice(0, 120)}`).slice(-10);
|
|
2258
|
+
const lastStep = steps[steps.length - 1];
|
|
2259
|
+
const trailingFailure = !!lastStep && !lastStep.ok;
|
|
2260
|
+
/** 有工具步骤却一条成功证据都没有 → 不算完成 (证据门槛, 不是文案) */
|
|
2261
|
+
const noEvidence = steps.length > 0 && evidence.length === 0;
|
|
2262
|
+
const errText = runPersistenceFailure || this.breakerReason || runNeedsHuman || runStopReason || aiFailureReason || '';
|
|
2263
|
+
const cls = errText ? classifyRunError(errText) : 'unknown';
|
|
2264
|
+
// 持久化失败 / 熔断 / 鉴权 → needs_human; 预算/中止 → aborted; 末尾仍失败或无证据 → failed (不许 done)
|
|
2265
|
+
const status = runPersistenceFailure
|
|
2266
|
+
? 'needs_human'
|
|
2267
|
+
: (this.breakerReason || runNeedsHuman)
|
|
2268
|
+
? 'needs_human'
|
|
2269
|
+
: runStopReason
|
|
2270
|
+
? 'aborted'
|
|
2271
|
+
: aiFailed
|
|
2272
|
+
? (cls === 'auth' ? 'needs_human' : 'failed')
|
|
2273
|
+
: (trailingFailure || noEvidence)
|
|
2274
|
+
? 'failed'
|
|
2275
|
+
: 'done';
|
|
2276
|
+
await finishRun(this.currentRunId, {
|
|
2277
|
+
status,
|
|
2278
|
+
summary: finalResponse ? String(finalResponse).slice(0, 400) : undefined,
|
|
2279
|
+
error: errText
|
|
2280
|
+
|| (trailingFailure ? `末尾步骤失败 (${lastStep.tool}): ${(lastStep.error || '').slice(0, 150)}` : undefined)
|
|
2281
|
+
|| (noEvidence ? '有工具步骤但没有任何成功证据: 不许判 done' : undefined),
|
|
2282
|
+
evidence,
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
catch (err) {
|
|
2286
|
+
// 终态也写不下去: 这是最坏情况 —— 除了留痕, 没有别的自愈手段, 所以必须显眼
|
|
2287
|
+
const message = `收尾落盘失败 (状态无法写回, 记录会停在 running): ${String(err?.message || err).slice(0, 200)}`;
|
|
2288
|
+
console.error('[PiAgent] run-store finishRun 失败 (核心持久化):', message);
|
|
2289
|
+
await recordDegradation({ kind: 'core', op: 'pi-sdk.finishRun', runId: this.currentRunId, message }).catch(() => { });
|
|
2290
|
+
onStream?.({ type: 'error', content: `⚠️ ${message}`, tool: 'harness' });
|
|
2291
|
+
}
|
|
2292
|
+
// 2026-09-16 (M4): Goal 侧完成门 —— Run 结束 ≠ Goal 完成; 只有判据全满足 + 有证据 + 无未解决项才算
|
|
2293
|
+
if (this.currentGoalId) {
|
|
2294
|
+
try {
|
|
2295
|
+
const full = await readRun(this.currentRunId).catch(() => null);
|
|
2296
|
+
const ev = (full?.evidence || []).slice(-10);
|
|
2297
|
+
if (ev.length)
|
|
2298
|
+
await addEvidence(this.currentGoalId, ev);
|
|
2299
|
+
const goal = await readGoal(this.currentGoalId);
|
|
2300
|
+
if (goal) {
|
|
2301
|
+
const verdict = evaluateGoalCompletion(goal);
|
|
2302
|
+
if (verdict.complete) {
|
|
2303
|
+
const done = await completeGoalIfEligible(this.currentGoalId);
|
|
2304
|
+
onStream?.({ type: 'status', content: `🎯 目标已达成: ${done.reason} (goal=${this.currentGoalId})`, tool: 'harness' });
|
|
2305
|
+
}
|
|
2306
|
+
else {
|
|
2307
|
+
// 不静默: 未达成的目标留在 active, 未解决项写清楚 (下次 prompt 会继续这个 Goal)
|
|
2308
|
+
const items = verdict.missing.length ? verdict.missing : [`run=${this.currentRunId} 结束但目标未达成: ${verdict.reason}`];
|
|
2309
|
+
await setUnresolved(this.currentGoalId, items);
|
|
2310
|
+
onStream?.({ type: 'status', content: `🎯 目标仍在进行 (未判完成): ${verdict.reason}`, tool: 'harness' });
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
catch (err) {
|
|
2315
|
+
await recordDegradation({ kind: 'observational', op: 'pi-sdk.goalCompletion', runId: this.currentRunId, message: String(err?.message || err).slice(0, 160) }).catch(() => { });
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
this.currentRunId = '';
|
|
1879
2319
|
}
|
|
1880
|
-
|
|
1881
|
-
|
|
2320
|
+
else if (this.currentRunId) {
|
|
2321
|
+
// 外部暂停/中止: 状态是人定的, 不覆盖 (paused 等 /resume; aborted 是终态)
|
|
2322
|
+
onStream?.({ type: 'status', content: `⏹️ 运行状态保持为 ${runExternallyPaused ? 'paused' : 'aborted'} (由外部控制面决定)`, tool: 'harness' });
|
|
2323
|
+
this.currentRunId = '';
|
|
1882
2324
|
}
|
|
1883
2325
|
// 2026-06-16: 暴露 aiFailed 标志 — promptStream 据此决定是否自动重试整个 loop
|
|
1884
2326
|
return { reply: finalResponse, aiFailed, aiFailureReason: aiFailureReason || undefined };
|