@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.
- package/dist/agents/pi-sdk.js +48 -9
- package/dist/web/icons/apple-touch-icon.png +0 -0
- package/dist/web/icons/favicon-16x16.png +0 -0
- package/dist/web/icons/favicon-192x192.png +0 -0
- package/dist/web/icons/favicon-32x32.png +0 -0
- package/dist/web/icons/favicon-48x48.png +0 -0
- package/dist/web/icons/favicon-512x512.png +0 -0
- package/dist/web/icons/favicon.icns +0 -0
- package/dist/web/icons/favicon.ico +0 -0
- package/dist/web/icons/icon.png +0 -0
- package/dist/web/icons/image.png +0 -0
- package/dist/web/manifest.json +21 -0
- package/dist/web/server.js +7 -0
- package/package.json +2 -2
- package/scripts/build-web.ts +4 -0
- package/src/agents/pi-sdk.ts +44 -9
- package/src/web/server.ts +10 -0
package/dist/agents/pi-sdk.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
1361
|
-
|
|
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
|
|
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
|
+
}
|
package/dist/web/server.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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",
|
package/scripts/build-web.ts
CHANGED
|
@@ -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
|
}
|
package/src/agents/pi-sdk.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
1658
|
-
|
|
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;
|