@bolloon/bolloon-agent 0.1.16 → 0.1.17

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.
@@ -861,7 +861,10 @@ ${this.getToolDefinitions()}
861
861
  let lastQualityScore = 0;
862
862
  let refineAttempts = 0;
863
863
  let consecutiveErrors = 0;
864
+ let lastFailedTool = ''; // 跟踪最近一次失败的 tool name
865
+ let lastFailedToolCount = 0; // 最近失败工具的连续失败次数
864
866
  const MAX_CONSECUTIVE_ERRORS = 3;
867
+ const MAX_SAME_TOOL_FAILURES = 3; // 同一工具连续失败 3 次, 强制让 LLM 给出最终答案
865
868
  // 发送循环开始的事件
866
869
  if (onStream) {
867
870
  onStream({ type: 'status', content: '🔄 开始 ReAct 循环...', tool: 'system' });
@@ -987,16 +990,35 @@ ${toolDefs}
987
990
  }
988
991
  else {
989
992
  consecutiveErrors++;
990
- console.warn(`[PiAgent] 工具执行失败 (${consecutiveErrors}/${MAX_CONSECUTIVE_ERRORS}): ${result.error}`);
991
- // 连续错误达到上限,尝试换一种方式
993
+ // 跟踪同一工具连续失败次数
994
+ if (toolCall.name === lastFailedTool) {
995
+ lastFailedToolCount++;
996
+ }
997
+ else {
998
+ lastFailedTool = toolCall.name;
999
+ lastFailedToolCount = 1;
1000
+ }
1001
+ console.warn(`[PiAgent] 工具 ${toolCall.name} 执行失败 (${lastFailedToolCount}/${MAX_SAME_TOOL_FAILURES}): ${result.error}`);
1002
+ // 同一工具连续失败达到上限, 不再重试, 强制 LLM 给出最终答案
1003
+ if (lastFailedToolCount >= MAX_SAME_TOOL_FAILURES) {
1004
+ console.log(`[PiAgent] 工具 ${toolCall.name} 连续 ${MAX_SAME_TOOL_FAILURES} 次失败, 放弃并要求直接回答`);
1005
+ this.messageHistory.push({
1006
+ role: 'system',
1007
+ content: `[注意] 工具 ${toolCall.name} 在这个上下文中不可用 (连续 ${MAX_SAME_TOOL_FAILURES} 次失败: ${result.error}). 请不要再次调用它, 直接用你已知的信息回答用户, 并在回答开头标记 <final gen>.`
1008
+ });
1009
+ lastFailedTool = '';
1010
+ lastFailedToolCount = 0;
1011
+ consecutiveErrors = 0;
1012
+ continue; // 让 LLM 看到系统提示后再决定
1013
+ }
1014
+ // 连续错误达到上限(混合不同工具), 尝试换一种方式
992
1015
  if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
993
1016
  console.log(`[PiAgent] 连续 ${MAX_CONSECUTIVE_ERRORS} 次错误,尝试换一种方式处理`);
994
- // 添加错误上下文,让 LLM 换一种方式
995
1017
  this.messageHistory.push({
996
1018
  role: 'system',
997
- content: `[注意] 前面的工具调用连续失败。请尝试其他工具或换一种方式完成用户请求。`
1019
+ content: `[注意] 前面的工具调用连续失败。请尝试其他工具或换一种方式完成用户请求, 或用 <final gen> 给出最终回答.`
998
1020
  });
999
- consecutiveErrors = 0; // 重置以继续尝试
1021
+ consecutiveErrors = 0;
1000
1022
  }
1001
1023
  }
1002
1024
  }
@@ -1045,9 +1067,14 @@ ${toolDefs}
1045
1067
  }
1046
1068
  }
1047
1069
  if (!finalResponse) {
1048
- finalResponse = '任务处理超时,请尝试更具体的请求。';
1070
+ // 走到这里通常是 LLM 一直在调同一个不存在的工具, 没输出 <final gen>
1071
+ // 把已知的失败信息也带回去, 让用户知道发生了什么
1072
+ const reason = lastFailedTool
1073
+ ? `(工具 ${lastFailedTool} 连续 ${MAX_SAME_TOOL_FAILURES} 次失败, 已放弃)`
1074
+ : `(共 ${iteration - 1} 轮无最终输出)`;
1075
+ finalResponse = `抱歉,任务未能完成 ${reason}。请换个方式提问,或明确告诉 agent 不要调用工具。`;
1049
1076
  if (onStream) {
1050
- onStream({ type: 'error', content: '⚠️ 任务处理超时', tool: 'system' });
1077
+ onStream({ type: 'error', content: `⚠️ 任务未完成: ${reason}`, tool: 'system' });
1051
1078
  }
1052
1079
  }
1053
1080
  // 通知前端循环完成
@@ -1357,9 +1384,21 @@ ${this.extractOperationsFromRef(operationsRef)}
1357
1384
  try {
1358
1385
  const response = await llm.chat(`根据以下对话内容,为这个对话生成一个简短的名称(不超过20个字):\n\n${conversation}\n\n直接输出名称,不要其他解释。`, '命名建议');
1359
1386
  const name = response.reply.trim();
1360
- if (name && name.length <= 20 && name !== '智能体') {
1361
- return `Agent | ${name}`;
1387
+ // 拒绝错误回退串 (LLM 不可用时返回的占位文本)
1388
+ if (!name)
1389
+ return null;
1390
+ if (/^(抱歉|对不起|sorry|error|错误|失败|暂不可用|服务不可用)/i.test(name)) {
1391
+ console.log(`[suggestRename] 拒绝错误回退: "${name}"`);
1392
+ return null;
1362
1393
  }
1394
+ if (name.length > 20)
1395
+ return null;
1396
+ if (name === '智能体')
1397
+ return null;
1398
+ // 拒绝纯符号/标点
1399
+ if (!/[一-鿿\w]/.test(name))
1400
+ return null;
1401
+ return `Agent | ${name}`;
1363
1402
  }
1364
1403
  catch {
1365
1404
  // ignore
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "Bolloon Agent",
3
+ "short_name": "Bolloon",
4
+ "description": "AI Agent with Claude API integration",
5
+ "start_url": "/",
6
+ "display": "standalone",
7
+ "background_color": "#1a1a2e",
8
+ "theme_color": "#1a1a2e",
9
+ "icons": [
10
+ {
11
+ "src": "/icons/favicon-192x192.png",
12
+ "sizes": "192x192",
13
+ "type": "image/png"
14
+ },
15
+ {
16
+ "src": "/icons/favicon-512x512.png",
17
+ "sizes": "512x512",
18
+ "type": "image/png"
19
+ }
20
+ ]
21
+ }
@@ -58,12 +58,19 @@ async function saveChannels(channels) {
58
58
  return rest;
59
59
  });
60
60
  const jsonStr = JSON.stringify(sanitized, null, 2);
61
+ // 写盘保护: 内容和上次完全一致就跳过, 避免 SSE ping / 重新 init 触发的无意义写盘
62
+ if (jsonStr === lastChannelsJson) {
63
+ return; // 静默跳过, 不打日志
64
+ }
65
+ lastChannelsJson = jsonStr;
61
66
  console.log('[saveChannels] 保存频道数据, 数量:', sanitized.length);
62
67
  console.log('[saveChannels] JSON 长度:', jsonStr.length);
63
68
  await fs.writeFile(CHANNELS_PATH, jsonStr);
64
69
  // 写盘即令缓存失效: 用 lastChannelsWriteAt 标记, getChannelsWithDID 会检查
65
70
  lastChannelsWriteAt = Date.now();
66
71
  }
72
+ // 写盘去重: 上次写盘内容, 用于跳过幂等调用
73
+ let lastChannelsJson = '';
67
74
  // 模块级: 最近一次 channels.json 写盘时间. saveChannels 在模块顶层,
68
75
  // getChannelsWithDID 在 createWebServer 内部, 跨作用域用模块变量桥接.
69
76
  let lastChannelsWriteAt = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli.js",
@@ -32,7 +32,7 @@
32
32
  "src/constraint-runtime"
33
33
  ],
34
34
  "dependencies": {
35
- "@bolloon/bolloon-agent": "^0.1.16",
35
+ "@bolloon/bolloon-agent": "^0.1.17",
36
36
  "@bolloon/constraint-runtime": "0.1.0",
37
37
  "@chainsafe/libp2p-noise": "^17.0.0",
38
38
  "@chainsafe/libp2p-yamux": "^8.0.1",
@@ -57,6 +57,10 @@ async function main() {
57
57
  await fs.copyFile(path.join(ROOT, 'src/web/api-config.html'), path.join(DIST_WEB, 'api-config.html'));
58
58
  await fs.copyFile(path.join(ROOT, 'src/web/style.css'), path.join(DIST_WEB, 'style.css'));
59
59
  await fs.copyFile(path.join(ROOT, 'src/web/client.js'), path.join(DIST_WEB, 'client.js'));
60
+ // 复制 PWA manifest (index.html 里有 <link rel="manifest">, 否则浏览器会 404)
61
+ await fs.copyFile(path.join(ROOT, 'src/web/manifest.json'), path.join(DIST_WEB, 'manifest.json'));
62
+ // 复制 icons 目录 (manifest.json 里引用了 favicon 等)
63
+ await fs.cp(path.join(ROOT, 'src/web/icons'), path.join(DIST_WEB, 'icons'), { recursive: true });
60
64
 
61
65
  console.log('[build-web] 完成!');
62
66
  }
@@ -1115,7 +1115,10 @@ ${this.getToolDefinitions()}
1115
1115
  let lastQualityScore = 0;
1116
1116
  let refineAttempts = 0;
1117
1117
  let consecutiveErrors = 0;
1118
+ let lastFailedTool = ''; // 跟踪最近一次失败的 tool name
1119
+ let lastFailedToolCount = 0; // 最近失败工具的连续失败次数
1118
1120
  const MAX_CONSECUTIVE_ERRORS = 3;
1121
+ const MAX_SAME_TOOL_FAILURES = 3; // 同一工具连续失败 3 次, 强制让 LLM 给出最终答案
1119
1122
 
1120
1123
  // 发送循环开始的事件
1121
1124
  if (onStream) {
@@ -1260,17 +1263,36 @@ ${toolDefs}
1260
1263
  // 不 break,继续下一次循环
1261
1264
  } else {
1262
1265
  consecutiveErrors++;
1263
- console.warn(`[PiAgent] 工具执行失败 (${consecutiveErrors}/${MAX_CONSECUTIVE_ERRORS}): ${result.error}`);
1266
+ // 跟踪同一工具连续失败次数
1267
+ if (toolCall.name === lastFailedTool) {
1268
+ lastFailedToolCount++;
1269
+ } else {
1270
+ lastFailedTool = toolCall.name;
1271
+ lastFailedToolCount = 1;
1272
+ }
1273
+ console.warn(`[PiAgent] 工具 ${toolCall.name} 执行失败 (${lastFailedToolCount}/${MAX_SAME_TOOL_FAILURES}): ${result.error}`);
1274
+
1275
+ // 同一工具连续失败达到上限, 不再重试, 强制 LLM 给出最终答案
1276
+ if (lastFailedToolCount >= MAX_SAME_TOOL_FAILURES) {
1277
+ console.log(`[PiAgent] 工具 ${toolCall.name} 连续 ${MAX_SAME_TOOL_FAILURES} 次失败, 放弃并要求直接回答`);
1278
+ this.messageHistory.push({
1279
+ role: 'system',
1280
+ content: `[注意] 工具 ${toolCall.name} 在这个上下文中不可用 (连续 ${MAX_SAME_TOOL_FAILURES} 次失败: ${result.error}). 请不要再次调用它, 直接用你已知的信息回答用户, 并在回答开头标记 <final gen>.`
1281
+ });
1282
+ lastFailedTool = '';
1283
+ lastFailedToolCount = 0;
1284
+ consecutiveErrors = 0;
1285
+ continue; // 让 LLM 看到系统提示后再决定
1286
+ }
1264
1287
 
1265
- // 连续错误达到上限,尝试换一种方式
1288
+ // 连续错误达到上限(混合不同工具), 尝试换一种方式
1266
1289
  if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
1267
1290
  console.log(`[PiAgent] 连续 ${MAX_CONSECUTIVE_ERRORS} 次错误,尝试换一种方式处理`);
1268
- // 添加错误上下文,让 LLM 换一种方式
1269
1291
  this.messageHistory.push({
1270
1292
  role: 'system',
1271
- content: `[注意] 前面的工具调用连续失败。请尝试其他工具或换一种方式完成用户请求。`
1293
+ content: `[注意] 前面的工具调用连续失败。请尝试其他工具或换一种方式完成用户请求, 或用 <final gen> 给出最终回答.`
1272
1294
  });
1273
- consecutiveErrors = 0; // 重置以继续尝试
1295
+ consecutiveErrors = 0;
1274
1296
  }
1275
1297
  }
1276
1298
  } catch (execError) {
@@ -1323,9 +1345,14 @@ ${toolDefs}
1323
1345
  }
1324
1346
 
1325
1347
  if (!finalResponse) {
1326
- finalResponse = '任务处理超时,请尝试更具体的请求。';
1348
+ // 走到这里通常是 LLM 一直在调同一个不存在的工具, 没输出 <final gen>
1349
+ // 把已知的失败信息也带回去, 让用户知道发生了什么
1350
+ const reason = lastFailedTool
1351
+ ? `(工具 ${lastFailedTool} 连续 ${MAX_SAME_TOOL_FAILURES} 次失败, 已放弃)`
1352
+ : `(共 ${iteration - 1} 轮无最终输出)`;
1353
+ finalResponse = `抱歉,任务未能完成 ${reason}。请换个方式提问,或明确告诉 agent 不要调用工具。`;
1327
1354
  if (onStream) {
1328
- onStream({ type: 'error', content: '⚠️ 任务处理超时', tool: 'system' });
1355
+ onStream({ type: 'error', content: `⚠️ 任务未完成: ${reason}`, tool: 'system' });
1329
1356
  }
1330
1357
  }
1331
1358
 
@@ -1654,9 +1681,17 @@ ${this.extractOperationsFromRef(operationsRef)}
1654
1681
  );
1655
1682
 
1656
1683
  const name = response.reply.trim();
1657
- if (name && name.length <= 20 && name !== '智能体') {
1658
- return `Agent | ${name}`;
1684
+ // 拒绝错误回退串 (LLM 不可用时返回的占位文本)
1685
+ if (!name) return null;
1686
+ if (/^(抱歉|对不起|sorry|error|错误|失败|暂不可用|服务不可用)/i.test(name)) {
1687
+ console.log(`[suggestRename] 拒绝错误回退: "${name}"`);
1688
+ return null;
1659
1689
  }
1690
+ if (name.length > 20) return null;
1691
+ if (name === '智能体') return null;
1692
+ // 拒绝纯符号/标点
1693
+ if (!/[一-鿿\w]/.test(name)) return null;
1694
+ return `Agent | ${name}`;
1660
1695
  } catch {
1661
1696
  // ignore
1662
1697
  }
package/src/web/server.ts CHANGED
@@ -117,6 +117,13 @@ async function saveChannels(channels: Channel[]): Promise<void> {
117
117
  return rest as Channel;
118
118
  });
119
119
  const jsonStr = JSON.stringify(sanitized, null, 2);
120
+
121
+ // 写盘保护: 内容和上次完全一致就跳过, 避免 SSE ping / 重新 init 触发的无意义写盘
122
+ if (jsonStr === lastChannelsJson) {
123
+ return; // 静默跳过, 不打日志
124
+ }
125
+ lastChannelsJson = jsonStr;
126
+
120
127
  console.log('[saveChannels] 保存频道数据, 数量:', sanitized.length);
121
128
  console.log('[saveChannels] JSON 长度:', jsonStr.length);
122
129
  await fs.writeFile(CHANNELS_PATH, jsonStr);
@@ -124,6 +131,9 @@ async function saveChannels(channels: Channel[]): Promise<void> {
124
131
  lastChannelsWriteAt = Date.now();
125
132
  }
126
133
 
134
+ // 写盘去重: 上次写盘内容, 用于跳过幂等调用
135
+ let lastChannelsJson = '';
136
+
127
137
  // 模块级: 最近一次 channels.json 写盘时间. saveChannels 在模块顶层,
128
138
  // getChannelsWithDID 在 createWebServer 内部, 跨作用域用模块变量桥接.
129
139
  let lastChannelsWriteAt = 0;