@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.
@@ -0,0 +1,118 @@
1
+ /**
2
+ * error-classifier.ts — 工具错误分类 + 反射引擎
3
+ *
4
+ * 分层:
5
+ * 1. classifyError: 原始 error string → ErrorClass
6
+ * 2. buildObservation: 工具结果 → 结构化 Observation
7
+ * 3. buildReflection: 错误历史 → 替代策略建议
8
+ * 4. suggestEscalation: 阶梯升档: 重试→换参数→换工具→简化→放弃
9
+ */
10
+ const ERROR_SIGNATURES = [
11
+ { pattern: /unknown tool|未知工具|tool.*not found|is not a function/i, cls: 'tool_not_found', label: '工具不存在' },
12
+ { pattern: /PreToolUse 拒绝|Harness.*拒绝|permission|not allowed|denied/i, cls: 'permission_denied', label: '权限拒绝' },
13
+ { pattern: /ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENOTFOUND|network|connect.*fail|fetch.*fail/i, cls: 'network_error', label: '网络错误' },
14
+ { pattern: /timeout|timed out/i, cls: 'timeout', label: '执行超时' },
15
+ { pattern: /ENOENT|not found|no such file|does not exist|invalid path|bad argument|ERR_INVALID/i, cls: 'bad_input', label: '参数错误' },
16
+ { pattern: /401|403|quota|rate limit|API key|unauthorized|authentication/i, cls: 'api_error', label: 'API 认证错误' },
17
+ ];
18
+ export function classifyError(errorMsg) {
19
+ if (!errorMsg)
20
+ return { cls: 'unknown', label: '未知错误', severity: 'medium', recoverable: true };
21
+ for (const sig of ERROR_SIGNATURES) {
22
+ if (sig.pattern.test(errorMsg)) {
23
+ const severity = sig.cls === 'api_error' ? 'high'
24
+ : sig.cls === 'internal_error' ? 'high'
25
+ : sig.cls === 'permission_denied' ? 'high'
26
+ : sig.cls === 'tool_not_found' ? 'low'
27
+ : sig.cls === 'bad_input' ? 'low'
28
+ : 'medium';
29
+ const recoverable = sig.cls !== 'api_error' && sig.cls !== 'permission_denied';
30
+ return { cls: sig.cls, label: sig.label, severity, recoverable };
31
+ }
32
+ }
33
+ return { cls: 'unknown', label: '未知错误', severity: 'medium', recoverable: true };
34
+ }
35
+ export function buildObservation(tool, args, result) {
36
+ const obs = { tool, args, success: result.success, summary: '' };
37
+ if (result.success) {
38
+ const output = result.output || '(无输出)';
39
+ obs.output = output;
40
+ obs.summary = `✅ ${tool} 成功 (${output.length}B)`;
41
+ }
42
+ else {
43
+ const errMsg = result.error || '未知失败';
44
+ const cls = classifyError(errMsg);
45
+ obs.errorClass = cls.cls;
46
+ obs.errorLabel = cls.label;
47
+ obs.summary = `❌ ${tool} 失败: ${cls.label} — ${errMsg.slice(0, 120)}`;
48
+ }
49
+ return obs;
50
+ }
51
+ const ERROR_TO_STRATEGIES = {
52
+ tool_not_found: [
53
+ { action: 'change_tool', reason: '工具名不存在', detail: '用 list_tools 查可用工具, 使用正确的工具名' },
54
+ { action: 'retry', reason: '可能是别名没匹配上', detail: '尝试用标准工具名重新调用, 如 shell_exec 而非 bash' },
55
+ ],
56
+ permission_denied: [
57
+ { action: 'change_tool', reason: '当前工具被权限系统阻止', detail: '尝试用其他方式完成目标, 如 read_file 替代 shell_exec cat' },
58
+ { action: 'simplify_goal', reason: '权限持续拒绝', detail: '缩小操作范围, 选择不需要高权限的操作' },
59
+ ],
60
+ network_error: [
61
+ { action: 'retry', reason: '网络偶发故障', detail: '等待 2-3 秒后重试, LLM 会自动降速' },
62
+ { action: 'change_tool', reason: '网络不可用', detail: '尝试本地操作替代网络请求' },
63
+ ],
64
+ timeout: [
65
+ { action: 'retry', reason: '可能暂时性负载高', detail: '简化参数后重试, 或分多次执行' },
66
+ { action: 'change_tool', reason: '工具执行时间过长', detail: '换一个更轻量的工具' },
67
+ ],
68
+ bad_input: [
69
+ { action: 'change_params', reason: '参数格式不对', detail: '检查参数格式: 路径用绝对路径, 空格用引号包裹' },
70
+ { action: 'retry', reason: '参数调整后重试', detail: '用正确的参数重新调用' },
71
+ ],
72
+ api_error: [
73
+ { action: 'simplify_goal', reason: 'API 认证失败', detail: '检查 API 配置或使用已有的本地能力完成' },
74
+ { action: 'abandon', reason: 'API 不可恢复', detail: 'API key 或配额问题无法自动解决, 告知用户' },
75
+ ],
76
+ internal_error: [
77
+ { action: 'change_tool', reason: '工具内部异常', detail: '换另一种方式处理' },
78
+ { action: 'simplify_goal', reason: '工具无法正常工作', detail: '尝试用更简单的方式完成任务' },
79
+ ],
80
+ unknown: [
81
+ { action: 'retry', reason: '错误类型不确定', detail: '换个方式或参数再试一次' },
82
+ { action: 'change_tool', reason: '原方法不可行', detail: '尝试用其他工具组合达成目标' },
83
+ ],
84
+ };
85
+ export function buildReflection(toolName, errorMsg, errorCount, sameToolFailCount) {
86
+ if (!errorMsg)
87
+ return ERROR_TO_STRATEGIES.unknown.slice(0, 1);
88
+ const cls = classifyError(errorMsg);
89
+ // 阶梯升档: 根据连续失败次数选择更激进的策略
90
+ if (sameToolFailCount >= 3) {
91
+ return [
92
+ { action: 'abandon', reason: `工具 ${toolName} 连续失败 ${sameToolFailCount} 次`, detail: '放弃这个工具, 用其他方法或直接给用户已知信息' },
93
+ { action: 'simplify_goal', reason: '工具不可用', detail: '简化任务, 给出已成功执行的部分结果' },
94
+ ];
95
+ }
96
+ if (errorCount >= 5) {
97
+ return [
98
+ { action: 'simplify_goal', reason: `累计 ${errorCount} 次错误`, detail: '放弃复杂操作, 用已有知识回答用户' },
99
+ ];
100
+ }
101
+ return ERROR_TO_STRATEGIES[cls.cls] || ERROR_TO_STRATEGIES.unknown;
102
+ }
103
+ // ==================== 格式化为 system prompt 注入 ====================
104
+ /**
105
+ * 把 Observation + Reflection 格式化成一条 system 消息,
106
+ * 注入到 messageHistory 中让 LLM 下一轮能看到.
107
+ */
108
+ export function formatObservationWithReflection(obs, reflection) {
109
+ const lines = [];
110
+ lines.push(`[工具结果] ${obs.summary}`);
111
+ if (!obs.success && reflection.length > 0) {
112
+ lines.push(`[Reflection] 推荐策略:`);
113
+ for (const r of reflection.slice(0, 2)) {
114
+ lines.push(` - ${r.action}: ${r.reason}. ${r.detail}`);
115
+ }
116
+ }
117
+ return lines.join('\n');
118
+ }
@@ -53,7 +53,202 @@ function defaultResolveAlias(name, tools) {
53
53
  return lower;
54
54
  return null;
55
55
  }
56
- /** 拆分 args.command: "git status" → { command: "git", args: "status" } */
56
+ /**
57
+ * parseAllToolCalls — 从 LLM 输出中提取**所有**工具调用.
58
+ *
59
+ * 与 parseToolCall 不同, 这个函数不短路: 从多种格式中收集全部匹配.
60
+ * 支持 JSON 数组、多行 XML invoke、连续 tool_call 标签等.
61
+ *
62
+ * 用法:
63
+ * import { parseAllToolCalls } from './parse-tool-call';
64
+ * const calls = parseAllToolCalls(reply, { tools: knownTools });
65
+ * for (const tc of calls) tool.execute(tc.args);
66
+ */
67
+ export function parseAllToolCalls(content, ctx) {
68
+ if (!content)
69
+ return [];
70
+ const stripped = content.replace(/<think[\s\S]*?<\/think/g, '');
71
+ const results = [];
72
+ const seen = new Set(); // 去重: name+JSON.stringify(args)
73
+ // 1. JSON 数组: [{"name":"x","arguments":{...}}, ...]
74
+ const jsonArrayMatch = stripped.match(/\[\s*\{[\s\S]*?\}\s*\]/);
75
+ if (jsonArrayMatch) {
76
+ try {
77
+ const arr = JSON.parse(jsonArrayMatch[0]);
78
+ if (Array.isArray(arr)) {
79
+ for (const item of arr) {
80
+ if (item && item.name) {
81
+ const args = typeof item.arguments === 'object' && item.arguments
82
+ ? Object.fromEntries(Object.entries(item.arguments).map(([k, v]) => [k, String(v)]))
83
+ : {};
84
+ const name = ctx.tools.has(item.name) ? item.name : resolve(ctx, item.name);
85
+ if (!name)
86
+ continue;
87
+ autoSplitCommand(args);
88
+ const key = name + JSON.stringify(args);
89
+ if (!seen.has(key)) {
90
+ seen.add(key);
91
+ results.push({ name, args });
92
+ }
93
+ }
94
+ }
95
+ if (results.length > 0)
96
+ return results;
97
+ }
98
+ }
99
+ catch { /* not valid JSON array, fall through */ }
100
+ }
101
+ // 2. JSON 行: 每行一个 tool_call (minimax / deepseek 多行 JSON)
102
+ // 也匹配连续 `{"name":"...","arguments":{...}}{"name":"...",...}` (无分隔符)
103
+ const jsonLineRe = /\{[^{}]*"name"\s*:\s*"(\w+)"[^{}]*"arguments"\s*:\s*(\{[^{}]*\})[^{}]*\}/g;
104
+ let jm;
105
+ while ((jm = jsonLineRe.exec(stripped)) !== null) {
106
+ try {
107
+ const name = jm[1];
108
+ const argsRaw = JSON.parse(jm[2]);
109
+ const args = typeof argsRaw === 'object' && argsRaw
110
+ ? Object.fromEntries(Object.entries(argsRaw).map(([k, v]) => [k, String(v)]))
111
+ : {};
112
+ const resolved = ctx.tools.has(name) ? name : resolve(ctx, name);
113
+ if (!resolved)
114
+ continue;
115
+ autoSplitCommand(args);
116
+ const key = resolved + JSON.stringify(args);
117
+ if (!seen.has(key)) {
118
+ seen.add(key);
119
+ results.push({ name: resolved, args });
120
+ }
121
+ }
122
+ catch { /* skip malformed */ }
123
+ }
124
+ // 3. 多行 XML: 连续 <invoke name="X">...</invoke>
125
+ const xmlRe = /<invoke\s+name=["'](\w+)["']>([\s\S]*?)<\/invoke>/g;
126
+ let xm;
127
+ while ((xm = xmlRe.exec(stripped)) !== null) {
128
+ const name = xm[1];
129
+ const rawArgs = xm[2] || '';
130
+ const args = parseXmlArgs(rawArgs);
131
+ const resolved = ctx.tools.has(name) ? name : resolve(ctx, name);
132
+ if (!resolved)
133
+ continue;
134
+ autoSplitCommand(args);
135
+ const key = resolved + JSON.stringify(args);
136
+ if (!seen.has(key)) {
137
+ seen.add(key);
138
+ results.push({ name: resolved, args });
139
+ }
140
+ }
141
+ // 4. <function_calls> 包裹
142
+ const fcRe = /<function_calls>[\s\S]*?<invoke\s+name=["'](\w+)["']>([\s\S]*?)<\/invoke>[\s\S]*?<\/function_calls>/g;
143
+ let fm;
144
+ while ((fm = fcRe.exec(stripped)) !== null) {
145
+ const name = fm[1];
146
+ const rawArgs = fm[2] || '';
147
+ const args = parseXmlArgs(rawArgs);
148
+ const resolved = ctx.tools.has(name) ? name : resolve(ctx, name);
149
+ if (!resolved)
150
+ continue;
151
+ autoSplitCommand(args);
152
+ const key = resolved + JSON.stringify(args);
153
+ if (!seen.has(key)) {
154
+ seen.add(key);
155
+ results.push({ name: resolved, args });
156
+ }
157
+ }
158
+ // 5. <function_calls><tool name="X">...</tool></function_calls>
159
+ const toolTagRe = /<function_calls>[\s\S]*?<tool\s+name=["'](\w+)["']>([\s\S]*?)<\/tool>[\s\S]*?<\/function_calls>/g;
160
+ let tm;
161
+ while ((tm = toolTagRe.exec(stripped)) !== null) {
162
+ const name = tm[1];
163
+ const rawArgs = tm[2] || '';
164
+ const args = parseXmlArgs(rawArgs);
165
+ const resolved = ctx.tools.has(name) ? name : resolve(ctx, name);
166
+ if (!resolved)
167
+ continue;
168
+ autoSplitCommand(args);
169
+ const key = resolved + JSON.stringify(args);
170
+ if (!seen.has(key)) {
171
+ seen.add(key);
172
+ results.push({ name: resolved, args });
173
+ }
174
+ }
175
+ // 6. [TOOL_CALL] 标签 — 可能有多段
176
+ const tcTagRe = /\[TOOL_CALL\]([\s\S]*?)\[\/TOOL_CALL\]/g;
177
+ let tcm;
178
+ while ((tcm = tcTagRe.exec(stripped)) !== null) {
179
+ try {
180
+ const inner = tcm[1].trim();
181
+ const parsed = JSON.parse(inner);
182
+ if (parsed && parsed.name) {
183
+ const rawArgs = (typeof parsed.args === 'object' && parsed.args) ? parsed.args
184
+ : (typeof parsed.arguments === 'object' && parsed.arguments) ? parsed.arguments
185
+ : {};
186
+ const args = Object.fromEntries(Object.entries(rawArgs).map(([k, v]) => [k, String(v)]));
187
+ const name = ctx.tools.has(parsed.name) ? parsed.name : resolve(ctx, parsed.name);
188
+ if (!name)
189
+ continue;
190
+ autoSplitCommand(args);
191
+ const key = name + JSON.stringify(args);
192
+ if (!seen.has(key)) {
193
+ seen.add(key);
194
+ results.push({ name, args });
195
+ }
196
+ }
197
+ }
198
+ catch { /* skip */ }
199
+ }
200
+ // 7. tool_call 标签 — <tool_call>...</tool_call>
201
+ const toolCallTagRe = /<tool_call>([\s\S]*?)<\/tool_call>/g;
202
+ let tlcm;
203
+ while ((tlcm = toolCallTagRe.exec(stripped)) !== null) {
204
+ try {
205
+ const inner = tlcm[1].trim();
206
+ const parsed = JSON.parse(inner);
207
+ if (parsed && parsed.name) {
208
+ const rawArgs = (typeof parsed.args === 'object' && parsed.args) ? parsed.args
209
+ : (typeof parsed.arguments === 'object' && parsed.arguments) ? parsed.arguments
210
+ : {};
211
+ const args = Object.fromEntries(Object.entries(rawArgs).map(([k, v]) => [k, String(v)]));
212
+ const name = ctx.tools.has(parsed.name) ? parsed.name : resolve(ctx, parsed.name);
213
+ if (!name)
214
+ continue;
215
+ autoSplitCommand(args);
216
+ const key = name + JSON.stringify(args);
217
+ if (!seen.has(key)) {
218
+ seen.add(key);
219
+ results.push({ name, args });
220
+ }
221
+ }
222
+ }
223
+ catch { /* skip */ }
224
+ }
225
+ return results;
226
+ }
227
+ /** 从 XML 子标签中提取 args */
228
+ function parseXmlArgs(rawArgs) {
229
+ const args = {};
230
+ // <parameter name="X">value</parameter>
231
+ const paramRe = /<parameter\s+name=["'](\w+)["']>([\s\S]*?)<\/parameter>/g;
232
+ let pm;
233
+ while ((pm = paramRe.exec(rawArgs)) !== null) {
234
+ args[pm[1]] = pm[2].trim();
235
+ }
236
+ if (Object.keys(args).length > 0)
237
+ return args;
238
+ // <param name="X">value</param>
239
+ const pRe = /<param\s+name=["'](\w+)["']>([\s\S]*?)<\/param>/g;
240
+ while ((pm = pRe.exec(rawArgs)) !== null) {
241
+ args[pm[1]] = pm[2].trim().replace(/^["']|["']$/g, '');
242
+ }
243
+ if (Object.keys(args).length > 0)
244
+ return args;
245
+ // <tag>value</tag>
246
+ const xmlArgPattern = /<(\w+)>([\s\S]*?)<\/\1>/g;
247
+ while ((pm = xmlArgPattern.exec(rawArgs)) !== null) {
248
+ args[pm[1]] = pm[2].trim();
249
+ }
250
+ return args;
251
+ }
57
252
  function autoSplitCommand(args) {
58
253
  if (typeof args.command === 'string' && args.command.includes(' ') && !args.args) {
59
254
  const parts = args.command.split(/\s+/);
@@ -1551,6 +1551,19 @@ export function registerWalletTools(ctx) {
1551
1551
  }
1552
1552
  }
1553
1553
  });
1554
+ // 2026-07-28: 注册 LSP 工具 (代码智能)
1555
+ // registerBuiltinTools 不是 async, 用同步 import 兜底
1556
+ try {
1557
+ // 动态 import + 立即执行, 兼容 ESM
1558
+ import('../lsp/lsp-tools.js').then(({ registerLspTools }) => {
1559
+ registerLspTools(ctx);
1560
+ }).catch((e) => {
1561
+ console.warn('[registerTools] LSP 工具注册失败 (非致命):', e);
1562
+ });
1563
+ }
1564
+ catch (lspErr) {
1565
+ console.warn('[registerTools] LSP 工具注册失败 (非致命):', lspErr);
1566
+ }
1554
1567
  }
1555
1568
  /**
1556
1569
  * 监听 P2P 消息 (远端 + 本地 inbox bus), 把消息存到 _inboxMessages 供 check_inbox 读.