@bolloon/bolloon-agent 0.3.16 → 0.3.18

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.
@@ -41,7 +41,8 @@ import { onPostToolUse } from '../bootstrap/lifecycle-hooks.js';
41
41
  import { budgetReduce, snip, microcompact } from '../context-compaction/index.js';
42
42
  // React Harness: 8-gate + 4-guard (防越权 / 防 prompt 注入)
43
43
  import { ReactHarness } from '../security/react-harness.js';
44
- import { parseToolCall as parseToolCallImpl, isFinalResponse as isFinalResponseImpl, extractFinalAnswer as extractFinalAnswerImpl } from './parse-tool-call.js';
44
+ import { parseToolCall as parseToolCallImpl, parseAllToolCalls, isFinalResponse as isFinalResponseImpl, extractFinalAnswer as extractFinalAnswerImpl } from './parse-tool-call.js';
45
+ import { buildObservation, buildReflection, formatObservationWithReflection } from './error-classifier.js';
45
46
  import { sessionStore as defaultSessionStore } from './session-store.js';
46
47
  import { ToolRegistry } from './tool-registry.js';
47
48
  import { decideMaxIterations, decideContextOverflow, shouldCompactBeforeIteration } from './react-loop.js';
@@ -1127,303 +1128,235 @@ ${toolDefs}
1127
1128
  // Bug 5 (2026-07-17): 优先用 LLM 的 native tool_calls (response.toolCalls), 再回退到文本解析
1128
1129
  // deepseek-v4-flash 用 OpenAI 协议 tools 时, 会真返回结构化 tool_calls 数组
1129
1130
  // 之前 nativeToolCalls 被读了不用, 只查 reply 文本, 导致 LLM 明明选了工具但代码找不到
1130
- let toolCall = null;
1131
+ // 2026-07-28: 修复多工具调用 — 收集 ALL tool calls, 顺序执行后一次性返回
1132
+ let toolCalls = [];
1133
+ // 路径 A: native OpenAI 协议 tool_calls (可能多个)
1131
1134
  if (nativeToolCalls && nativeToolCalls.length > 0) {
1132
- const nc = nativeToolCalls[0];
1133
- // OpenAI 协议: { id, type: 'function', function: { name, arguments: JSON string } }
1134
- // 转换成 internal { name, args, id }
1135
- try {
1136
- const args = typeof nc.function?.arguments === 'string'
1137
- ? JSON.parse(nc.function.arguments)
1138
- : (nc.function?.arguments || {});
1139
- toolCall = {
1140
- name: nc.function?.name,
1141
- args,
1142
- id: nc.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
1143
- };
1144
- console.log(`[PiAgent] native tool_call: ${toolCall.name} (id=${toolCall.id})`);
1145
- }
1146
- catch (err) {
1147
- console.warn(`[PiAgent] 解析 native tool_call 失败, 回退到文本解析: ${err.message?.slice(0, 100)}`);
1148
- toolCall = null;
1135
+ for (const nc of nativeToolCalls) {
1136
+ try {
1137
+ const args = typeof nc.function?.arguments === 'string'
1138
+ ? JSON.parse(nc.function.arguments)
1139
+ : (nc.function?.arguments || {});
1140
+ toolCalls.push({
1141
+ name: nc.function?.name,
1142
+ args,
1143
+ id: nc.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
1144
+ });
1145
+ }
1146
+ catch (err) {
1147
+ console.warn(`[PiAgent] 解析 native tool_call 失败: ${err.message?.slice(0, 100)}`);
1148
+ }
1149
1149
  }
1150
1150
  }
1151
- if (!toolCall) {
1152
- toolCall = this.parseToolCall(reply);
1153
- }
1154
- // 2026-06-30 修: 给 toolCall 分配稳定 id, 让后续 tool result 能引用同一个 id
1155
- // OpenAI 协议要求 messages 里 tool result 必须有对应的 tool_call_id, 否则 400
1156
- if (toolCall && !toolCall.id) {
1157
- toolCall.id = `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1151
+ // 路径 B: 文本解析 (parseAllToolCalls 收集全部)
1152
+ if (toolCalls.length === 0) {
1153
+ const knownTools = new Set(Array.from(this.tools.keys()));
1154
+ toolCalls = parseAllToolCalls(reply, { tools: knownTools });
1155
+ }
1156
+ // 回退路径 C: 原生 parseToolCall (单个)
1157
+ if (toolCalls.length === 0) {
1158
+ const single = this.parseToolCall(reply);
1159
+ if (single)
1160
+ toolCalls.push(single);
1161
+ }
1162
+ // 给每个 toolCall 分配稳定 id
1163
+ for (const tc of toolCalls) {
1164
+ if (!tc.id) {
1165
+ tc.id = `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1166
+ }
1158
1167
  }
1159
- if (toolCall) {
1168
+ if (toolCalls.length > 0) {
1169
+ // 把原始 LLM 回复 push 进 history (仅一次)
1160
1170
  this.messageHistory.push({
1161
1171
  role: 'assistant',
1162
1172
  content: reply,
1163
- toolCall
1173
+ toolCalls: toolCalls.length > 1 ? toolCalls : [toolCalls[0]],
1164
1174
  });
1165
- // 通知前端:检测到工具调用
1166
- if (onStream) {
1167
- onStream({ type: 'tool', content: `🔧 调用工具: ${toolCall.name}`, tool: toolCall.name });
1168
- if (toolCall.args && Object.keys(toolCall.args).length > 0) {
1169
- onStream({ type: 'status', content: `📋 参数: ${JSON.stringify(toolCall.args)}`, tool: toolCall.name });
1170
- }
1171
- // 2026-06-15: step-timeline 状态机 开新节点
1172
- onStream({
1173
- type: 'step_start',
1174
- content: `调用 ${toolCall.name}`,
1175
- tool: toolCall.name,
1176
- args: toolCall.args || {},
1177
- });
1178
- }
1179
- const tool = this.tools.get(toolCall.name);
1180
- if (!tool) {
1181
- consecutiveErrors++;
1182
- // 2026-06-16 新增: 未知工具也要累计 (LLM 幻觉高频场景)
1183
- totalErrors++;
1184
- const errorResult = { success: false, error: `未知工具: ${toolCall.name}` };
1185
- this.messageHistory.push({ role: 'tool', content: JSON.stringify(errorResult), toolResult: errorResult });
1186
- this.logToHarness(toolCall.name, toolCall.args, errorResult);
1187
- console.warn(`[PiAgent] 未知工具: ${toolCall.name} (累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}),跳过并继续`);
1188
- continue;
1189
- }
1190
- // Bootstrap PreToolUse hook: 调工具前校验 (危险命令拦截)
1191
- // 失败静默 — hook 自身挂掉 = 放行
1192
- // P2: 透传 permissionMode (从 BootstrapOptions / env BOLLOON_PERM_MODE 解析)
1193
- let toolToExecute = tool;
1194
- try {
1195
- const pre = await onPreToolUse({
1196
- tool: toolCall.name,
1197
- args: toolCall.args || {},
1198
- permissionMode: this.currentPermissionMode,
1199
- });
1200
- if (!pre.allowed) {
1201
- const deniedResult = {
1202
- success: false,
1203
- error: `PreToolUse 拒绝: ${pre.reason || '未通过安全校验'}`,
1204
- };
1205
- this.messageHistory.push({
1206
- role: 'tool',
1207
- content: JSON.stringify(deniedResult),
1208
- toolResult: deniedResult,
1209
- });
1210
- this.logToHarness(toolCall.name, toolCall.args, deniedResult);
1211
- if (onStream) {
1212
- onStream({
1213
- type: 'error',
1214
- content: `🛡️ PreToolUse 拒绝 ${toolCall.name}: ${pre.reason || '安全校验失败'}`,
1215
- tool: toolCall.name,
1216
- });
1217
- // 2026-06-15: step-timeline — 拦在 PreToolUse, 标 step_error
1218
- onStream({
1219
- type: 'step_error',
1220
- content: `PreToolUse 拒绝 ${toolCall.name}`,
1221
- tool: toolCall.name,
1222
- error: pre.reason || '安全校验失败',
1223
- });
1175
+ // 顺序执行每个工具
1176
+ for (let ti = 0; ti < toolCalls.length; ti++) {
1177
+ const toolCall = toolCalls[ti];
1178
+ const isMulti = toolCalls.length > 1;
1179
+ // 通知前端
1180
+ if (onStream) {
1181
+ onStream({ type: 'tool', content: `🔧 调用工具 (${ti + 1}/${toolCalls.length}): ${toolCall.name}`, tool: toolCall.name });
1182
+ if (toolCall.args && Object.keys(toolCall.args).length > 0) {
1183
+ onStream({ type: 'status', content: `📋 参数: ${JSON.stringify(toolCall.args)}`, tool: toolCall.name });
1224
1184
  }
1225
- console.warn(`[PiAgent] PreToolUse denied ${toolCall.name}: ${pre.reason}`);
1226
- // 不调 tool.execute, 也不计 consecutiveErrors (这是用户级拒绝, 不是工具错)
1227
- continue;
1228
- }
1229
- }
1230
- catch (err) {
1231
- console.warn('[PiAgent] onPreToolUse failed (non-fatal, allowing):', err);
1232
- }
1233
- // React Harness: 8-gate + builtin-guards 校验 (在 PreToolUse 之后, 串接双层)
1234
- // 失败静默, 拒绝时不调 tool.execute
1235
- try {
1236
- const pre = await this.reactHarness.preToolCall(toolCall.name, toolCall.args || {}, this.currentChannelId || undefined);
1237
- if (!pre.allowed) {
1238
- const deniedResult = {
1239
- success: false,
1240
- error: `Harness gate 拒绝 (${pre.details.rejectedBy}): ${pre.reason || '未通过安全校验'}`,
1241
- };
1242
- this.messageHistory.push({
1243
- role: 'tool',
1244
- content: JSON.stringify(deniedResult),
1245
- toolResult: deniedResult,
1185
+ onStream({
1186
+ type: 'step_start',
1187
+ content: `调用 ${toolCall.name}${isMulti ? ` (${ti + 1}/${toolCalls.length})` : ''}`,
1188
+ tool: toolCall.name,
1189
+ args: toolCall.args || {},
1246
1190
  });
1247
- this.logToHarness(toolCall.name, toolCall.args, deniedResult);
1248
- if (onStream) {
1249
- onStream({
1250
- type: 'error',
1251
- content: `🛡️ Harness ${pre.details.rejectedBy} 拒绝 ${toolCall.name}: ${pre.reason || '安全校验失败'}`,
1252
- tool: toolCall.name,
1253
- });
1254
- // 2026-06-15: step-timeline — Harness gate 拒绝, 标 step_error
1255
- onStream({
1256
- type: 'step_error',
1257
- content: `Harness 拒绝 ${toolCall.name}`,
1258
- tool: toolCall.name,
1259
- error: pre.reason || '安全校验失败',
1260
- });
1261
- }
1262
- console.warn(`[PiAgent] Harness denied ${toolCall.name} (${pre.details.rejectedBy}): ${pre.reason}`);
1191
+ }
1192
+ const tool = this.tools.get(toolCall.name);
1193
+ if (!tool) {
1194
+ consecutiveErrors++;
1195
+ totalErrors++;
1196
+ const errorResult = { success: false, error: `未知工具: ${toolCall.name}` };
1197
+ this.messageHistory.push({ role: 'tool', content: JSON.stringify(errorResult), toolResult: errorResult });
1198
+ this.logToHarness(toolCall.name, toolCall.args, errorResult);
1199
+ // 2026-07-28: 注入 Reflection 帮助 LLM 理解错误
1200
+ const obs = buildObservation(toolCall.name, toolCall.args, errorResult);
1201
+ const ref = buildReflection(toolCall.name, errorResult.error, totalErrors, lastFailedToolCount);
1202
+ this.messageHistory.push({ role: 'system', content: formatObservationWithReflection(obs, ref) });
1203
+ if (onStream)
1204
+ onStream({ type: 'status', content: `💡 Reflection: ${obs.summary}`, tool: 'system' });
1205
+ console.warn(`[PiAgent] 未知工具: ${toolCall.name} (累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}),跳过并继续`);
1263
1206
  continue;
1264
1207
  }
1265
- }
1266
- catch (err) {
1267
- console.warn('[PiAgent] reactHarness.preToolCall failed (non-fatal, allowing):', err);
1268
- }
1269
- try {
1270
- const toolStart = Date.now();
1271
- let result = await tool.execute(toolCall.args);
1272
- const toolDurationMs = Date.now() - toolStart;
1273
- console.log(`[PiAgent] 工具 ${toolCall.name} 执行完成: success=${result.success} (${toolDurationMs}ms)`);
1274
- // PostToolUse 审计 hook: 写 audit log, 默认 continue
1208
+ // Bootstrap PreToolUse hook: 调工具前校验 (危险命令拦截)
1209
+ // 失败静默 — hook 自身挂掉 = 放行
1210
+ // P2: 透传 permissionMode ( BootstrapOptions / env BOLLOON_PERM_MODE 解析)
1211
+ let toolToExecute = tool;
1275
1212
  try {
1276
- await onPostToolUse({
1213
+ const pre = await onPreToolUse({
1277
1214
  tool: toolCall.name,
1278
1215
  args: toolCall.args || {},
1279
- result: {
1280
- success: result.success,
1281
- output: result.output?.substring(0, 500),
1282
- error: result.error,
1283
- },
1284
- durationMs: toolDurationMs,
1216
+ permissionMode: this.currentPermissionMode,
1285
1217
  });
1218
+ if (!pre.allowed) {
1219
+ const deniedResult = {
1220
+ success: false,
1221
+ error: `PreToolUse 拒绝: ${pre.reason || '未通过安全校验'}`,
1222
+ };
1223
+ this.messageHistory.push({ role: 'tool', content: JSON.stringify(deniedResult), toolResult: deniedResult });
1224
+ this.logToHarness(toolCall.name, toolCall.args, deniedResult);
1225
+ if (onStream) {
1226
+ onStream({ type: 'error', content: `🛡️ PreToolUse 拒绝 ${toolCall.name}: ${pre.reason || '安全校验失败'}`, tool: toolCall.name });
1227
+ onStream({ type: 'step_error', content: `PreToolUse 拒绝 ${toolCall.name}`, tool: toolCall.name, error: pre.reason || '安全校验失败' });
1228
+ }
1229
+ console.warn(`[PiAgent] PreToolUse denied ${toolCall.name}: ${pre.reason}`);
1230
+ continue;
1231
+ }
1286
1232
  }
1287
- catch (postErr) {
1288
- console.warn('[PiAgent] onPostToolUse failed (non-fatal):', postErr);
1289
- }
1290
- // Context router: 拿最近一次 preToolCall 算的 hint, 拼到 tool result messageHistory
1291
- // (LLM 下次看到 tool result 时, 能"记得"这次调用的安全约束)
1292
- const routeHint = this.reactHarness.getLastRouteHint();
1293
- if (routeHint && routeHint.systemAddition) {
1294
- this.messageHistory.push({
1295
- role: 'system',
1296
- content: `[Harness Router Hint: ${routeHint.reason}]\n${routeHint.systemAddition}`,
1297
- });
1298
- this.reactHarness.clearRouteHint();
1233
+ catch (err) {
1234
+ console.warn('[PiAgent] onPreToolUse failed (non-fatal, allowing):', err);
1299
1235
  }
1300
- // React Harness: post-tool call (output 审计: secret leak 等)
1301
- // 拒绝时 result.output 含敏感 → 替换为 generic message, 不污染 messageHistory
1236
+ // React Harness: 8-gate + builtin-guards 校验 (串接双层)
1302
1237
  try {
1303
- const post = await this.reactHarness.postToolCall(toolCall.name, String(result.output || ''), this.currentChannelId || undefined);
1304
- if (!post.allowed) {
1238
+ const pre = await this.reactHarness.preToolCall(toolCall.name, toolCall.args || {}, this.currentChannelId || undefined);
1239
+ if (!pre.allowed) {
1240
+ const deniedResult = { success: false, error: `Harness gate 拒绝 (${pre.details.rejectedBy}): ${pre.reason || '未通过安全校验'}` };
1241
+ this.messageHistory.push({ role: 'tool', content: JSON.stringify(deniedResult), toolResult: deniedResult });
1242
+ this.logToHarness(toolCall.name, toolCall.args, deniedResult);
1305
1243
  if (onStream) {
1306
- onStream({
1307
- type: 'error',
1308
- content: `🛡️ Harness output 拒绝 ${toolCall.name}: ${post.reason || '输出含敏感信息'}`,
1309
- tool: toolCall.name,
1310
- });
1244
+ onStream({ type: 'error', content: `🛡️ Harness ${pre.details.rejectedBy} 拒绝 ${toolCall.name}: ${pre.reason || '安全校验失败'}`, tool: toolCall.name });
1245
+ onStream({ type: 'step_error', content: `Harness 拒绝 ${toolCall.name}`, tool: toolCall.name, error: pre.reason || '安全校验失败' });
1311
1246
  }
1312
- console.warn(`[PiAgent] Harness output denied ${toolCall.name}: ${post.reason}`);
1313
- // 替换 result: success 仍保留 (tool 本身没错), 但 output 改成 generic
1314
- // 这样 LLM 下轮看 output 不会拿到秘密, 但 success 标志让它知道 "工具执行了"
1315
- result = {
1316
- ...result,
1317
- output: `[harness output gate: output 含敏感内容, 已屏蔽. 原因: ${post.reason || 'unknown'}]`,
1318
- _harnessDenied: true,
1319
- };
1247
+ console.warn(`[PiAgent] Harness denied ${toolCall.name} (${pre.details.rejectedBy}): ${pre.reason}`);
1248
+ continue;
1320
1249
  }
1321
1250
  }
1322
1251
  catch (err) {
1323
- console.warn('[PiAgent] reactHarness.postToolCall failed (non-fatal, allowing):', err);
1252
+ console.warn('[PiAgent] reactHarness.preToolCall failed (non-fatal, allowing):', err);
1324
1253
  }
1325
- this.messageHistory.push({ role: 'tool', content: JSON.stringify(result), toolResult: result, toolCallId: toolCall.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` });
1326
- this.logToHarness(toolCall.name, toolCall.args, result);
1327
- // 通知前端工具执行结果
1328
- if (onStream) {
1329
- if (result.success) {
1330
- onStream({ type: 'status', content: `✅ ${toolCall.name} 执行成功`, tool: toolCall.name });
1331
- if (result.output) {
1332
- const outputPreview = result.output.substring(0, 200);
1333
- onStream({ type: 'tool', content: `📤 结果: ${outputPreview}${result.output.length > 200 ? '...' : ''}`, tool: toolCall.name });
1334
- }
1335
- // 2026-06-15: step-timeline 状态机 — 关闭当前节点 (成功)
1336
- onStream({
1337
- type: 'step_done',
1338
- content: `${toolCall.name} 执行成功`,
1339
- tool: toolCall.name,
1340
- success: true,
1341
- output: result.output,
1342
- });
1343
- }
1344
- else {
1345
- onStream({ type: 'error', content: `❌ ${toolCall.name} 执行失败: ${result.error}`, tool: toolCall.name });
1346
- // 2026-06-15: step-timeline 状态机 — 关闭当前节点 (失败)
1347
- onStream({
1348
- type: 'step_error',
1349
- content: `${toolCall.name} 执行失败`,
1350
- tool: toolCall.name,
1351
- error: result.error,
1352
- });
1254
+ try {
1255
+ const toolStart = Date.now();
1256
+ let result = await tool.execute(toolCall.args);
1257
+ const toolDurationMs = Date.now() - toolStart;
1258
+ console.log(`[PiAgent] 工具 ${toolCall.name} 执行完成: success=${result.success} (${toolDurationMs}ms)`);
1259
+ try {
1260
+ await onPostToolUse({ tool: toolCall.name, args: toolCall.args || {}, result: { success: result.success, output: result.output?.substring(0, 500), error: result.error }, durationMs: toolDurationMs });
1353
1261
  }
1354
- }
1355
- if (result.success) {
1356
- consecutiveErrors = 0; // 重置连续错误计数
1357
- // 2026-06-19: 记录成功结果, 用于 LLM 失败退出时汇总给用户
1358
- if (result.output) {
1359
- this.successfulToolResults.push({
1360
- tool: toolCall.name,
1361
- outputPreview: result.output.substring(0, 200) + (result.output.length > 200 ? '...' : '')
1362
- });
1262
+ catch (postErr) {
1263
+ console.warn('[PiAgent] onPostToolUse failed (non-fatal):', postErr);
1363
1264
  }
1364
- else {
1365
- this.successfulToolResults.push({ tool: toolCall.name, outputPreview: '(无输出)' });
1265
+ const routeHint = this.reactHarness.getLastRouteHint();
1266
+ if (routeHint && routeHint.systemAddition) {
1267
+ this.messageHistory.push({ role: 'system', content: `[Harness Router Hint: ${routeHint.reason}]\n${routeHint.systemAddition}` });
1268
+ this.reactHarness.clearRouteHint();
1366
1269
  }
1367
- // 检查工具执行质量
1368
- lastQualityScore = this.estimateToolResultQuality(result);
1369
- if (lastQualityScore < this.QUALITY_THRESHOLD && refineAttempts < this.MAX_REFINE_ATTEMPTS) {
1370
- refineAttempts++;
1371
- console.log(`[PiAgent] 工具结果质量低,自动重试 (${refineAttempts}/${this.MAX_REFINE_ATTEMPTS})`);
1270
+ try {
1271
+ const post = await this.reactHarness.postToolCall(toolCall.name, String(result.output || ''), this.currentChannelId || undefined);
1272
+ if (!post.allowed) {
1273
+ if (onStream) {
1274
+ onStream({ type: 'error', content: `🛡️ Harness output 拒绝 ${toolCall.name}: ${post.reason || '输出含敏感信息'}`, tool: toolCall.name });
1275
+ }
1276
+ console.warn(`[PiAgent] Harness output denied ${toolCall.name}: ${post.reason}`);
1277
+ result = { ...result, output: `[harness output gate: 输出含敏感内容, 已屏蔽. 原因: ${post.reason || 'unknown'}]`, _harnessDenied: true };
1278
+ }
1372
1279
  }
1373
- else {
1374
- console.log(`[PiAgent] 工具执行成功,质量评分: ${(lastQualityScore * 10).toFixed(1)}/10`);
1280
+ catch (err) {
1281
+ console.warn('[PiAgent] reactHarness.postToolCall failed (non-fatal, allowing):', err);
1375
1282
  }
1376
- // 工具执行成功后,继续循环获取下一个 LLM 响应
1283
+ this.messageHistory.push({ role: 'tool', content: JSON.stringify(result), toolResult: result, toolCallId: toolCall.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` });
1284
+ this.logToHarness(toolCall.name, toolCall.args, result);
1377
1285
  if (onStream) {
1378
- onStream({ type: 'status', content: `🔄 工具执行完成,继续循环...`, tool: 'loop' });
1379
- }
1380
- // break,继续下一次循环
1381
- }
1382
- else {
1383
- consecutiveErrors++;
1384
- // 2026-06-16 新增: 累计错误 (跨工具, 兜底防 LLM 轮换工具名死循环)
1385
- totalErrors++;
1386
- // 跟踪同一工具连续失败次数
1387
- if (toolCall.name === lastFailedTool) {
1388
- lastFailedToolCount++;
1389
- }
1390
- else {
1391
- lastFailedTool = toolCall.name;
1392
- lastFailedToolCount = 1;
1286
+ if (result.success) {
1287
+ onStream({ type: 'status', content: `✅ ${toolCall.name} 执行成功`, tool: toolCall.name });
1288
+ if (result.output) {
1289
+ onStream({ type: 'tool', content: `📤 结果: ${result.output.substring(0, 200)}${result.output.length > 200 ? '...' : ''}`, tool: toolCall.name });
1290
+ }
1291
+ onStream({ type: 'step_done', content: `${toolCall.name} 执行成功`, tool: toolCall.name, success: true, output: result.output });
1292
+ }
1293
+ else {
1294
+ onStream({ type: 'error', content: `❌ ${toolCall.name} 执行失败: ${result.error}`, tool: toolCall.name });
1295
+ onStream({ type: 'step_error', content: `${toolCall.name} 执行失败`, tool: toolCall.name, error: result.error });
1296
+ }
1393
1297
  }
1394
- console.warn(`[PiAgent] 工具 ${toolCall.name} 执行失败 (${lastFailedToolCount}/${MAX_SAME_TOOL_FAILURES}, 累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}): ${result.error}`);
1395
- // 同一工具连续失败达到上限, 不再重试, 强制 LLM 给出最终答案
1396
- if (lastFailedToolCount >= MAX_SAME_TOOL_FAILURES) {
1397
- console.log(`[PiAgent] 工具 ${toolCall.name} 连续 ${MAX_SAME_TOOL_FAILURES} 次失败, 放弃并要求直接回答`);
1398
- this.messageHistory.push({
1399
- role: 'system',
1400
- content: `[注意] 工具 ${toolCall.name} 在这个上下文中不可用 (连续 ${MAX_SAME_TOOL_FAILURES} 次失败: ${result.error}). 请不要再次调用它, 直接用你已知的信息回答用户, 并在回答开头标记 <final gen>.`
1401
- });
1402
- lastFailedTool = '';
1403
- lastFailedToolCount = 0;
1298
+ if (result.success) {
1404
1299
  consecutiveErrors = 0;
1405
- continue; // 让 LLM 看到系统提示后再决定
1300
+ if (result.output) {
1301
+ this.successfulToolResults.push({ tool: toolCall.name, outputPreview: result.output.substring(0, 200) + (result.output.length > 200 ? '...' : '') });
1302
+ }
1303
+ else {
1304
+ this.successfulToolResults.push({ tool: toolCall.name, outputPreview: '(无输出)' });
1305
+ }
1306
+ lastQualityScore = this.estimateToolResultQuality(result);
1307
+ if (lastQualityScore < this.QUALITY_THRESHOLD && refineAttempts < this.MAX_REFINE_ATTEMPTS) {
1308
+ refineAttempts++;
1309
+ }
1310
+ if (onStream) {
1311
+ onStream({ type: 'status', content: `🔄 工具执行完成,继续循环...`, tool: 'loop' });
1312
+ }
1406
1313
  }
1407
- // 连续错误达到上限(混合不同工具), 尝试换一种方式
1408
- if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
1409
- console.log(`[PiAgent] 连续 ${MAX_CONSECUTIVE_ERRORS} 次错误,尝试换一种方式处理`);
1410
- this.messageHistory.push({
1411
- role: 'system',
1412
- content: `[注意] 前面的工具调用连续失败。请尝试其他工具或换一种方式完成用户请求, 或用 <final gen> 给出最终回答.`
1413
- });
1414
- consecutiveErrors = 0;
1314
+ else {
1315
+ consecutiveErrors++;
1316
+ totalErrors++;
1317
+ if (toolCall.name === lastFailedTool) {
1318
+ lastFailedToolCount++;
1319
+ }
1320
+ else {
1321
+ lastFailedTool = toolCall.name;
1322
+ lastFailedToolCount = 1;
1323
+ }
1324
+ console.warn(`[PiAgent] 工具 ${toolCall.name} 执行失败 (${lastFailedToolCount}/${MAX_SAME_TOOL_FAILURES}, 累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}): ${result.error}`);
1325
+ // 2026-07-28: 注入 Observation + Reflection 替代旧 hardcode 提示
1326
+ const obs = buildObservation(toolCall.name, toolCall.args, { success: false, error: result.error });
1327
+ const ref = buildReflection(toolCall.name, result.error, totalErrors, lastFailedToolCount);
1328
+ this.messageHistory.push({ role: 'system', content: formatObservationWithReflection(obs, ref) });
1329
+ if (onStream)
1330
+ onStream({ type: 'status', content: `💡 Reflection: ${obs.summary} → ${ref[0]?.action || '放弃'}`, tool: 'system' });
1331
+ if (lastFailedToolCount >= MAX_SAME_TOOL_FAILURES) {
1332
+ this.messageHistory.push({ role: 'system', content: `[注意] 工具 ${toolCall.name} 在这个上下文中不可用 (连续 ${MAX_SAME_TOOL_FAILURES} 次失败: ${result.error}). 请不要再次调用它, 直接用你已知的信息回答用户, 并在回答开头标记 <final gen>.` });
1333
+ lastFailedTool = '';
1334
+ lastFailedToolCount = 0;
1335
+ consecutiveErrors = 0;
1336
+ continue;
1337
+ }
1338
+ if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
1339
+ this.messageHistory.push({ role: 'system', content: `[注意] 前面的工具调用连续失败。请尝试其他工具或换一种方式完成用户请求, 或用 <final gen> 给出最终回答.` });
1340
+ consecutiveErrors = 0;
1341
+ }
1415
1342
  }
1416
1343
  }
1417
- }
1418
- catch (execError) {
1419
- consecutiveErrors++;
1420
- // 2026-06-16 新增: 异常分支也要累计
1421
- totalErrors++;
1422
- const errorResult = { success: false, error: String(execError) };
1423
- this.messageHistory.push({ role: 'tool', content: JSON.stringify(errorResult), toolResult: errorResult });
1424
- this.logToHarness(toolCall.name, toolCall.args, errorResult);
1425
- console.error(`[PiAgent] 工具执行异常 (累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}): ${execError}`);
1426
- }
1344
+ catch (execError) {
1345
+ consecutiveErrors++;
1346
+ totalErrors++;
1347
+ const errorResult = { success: false, error: String(execError) };
1348
+ this.messageHistory.push({ role: 'tool', content: JSON.stringify(errorResult), toolResult: errorResult });
1349
+ this.logToHarness(toolCall.name, toolCall.args, errorResult);
1350
+ const obs = buildObservation(toolCall.name, toolCall.args, errorResult);
1351
+ const ref = buildReflection(toolCall.name, errorResult.error, totalErrors, lastFailedToolCount);
1352
+ this.messageHistory.push({ role: 'system', content: formatObservationWithReflection(obs, ref) });
1353
+ if (onStream)
1354
+ onStream({ type: 'status', content: `💡 Reflection: ${obs.summary}`, tool: 'system' });
1355
+ console.error(`[PiAgent] 工具执行异常 (累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}): ${execError}`);
1356
+ }
1357
+ } // end for (ti)
1358
+ // 所有工具执行完毕后, continue while 循环, 让 LLM 看到结果
1359
+ continue;
1427
1360
  }
1428
1361
  else {
1429
1362
  // LLM 返回的不是 tool call 格式