zames_pro 1.2.1 → 2.1.0
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/agent-loop.js +671 -0
- package/dist/browser.js +656 -0
- package/dist/config.js +73 -0
- package/dist/confirm.js +81 -0
- package/dist/diff.js +37 -0
- package/dist/gitTools.js +218 -0
- package/dist/index.js +1378 -0
- package/dist/input.js +523 -0
- package/dist/markdown.js +47 -0
- package/dist/net-capture.js +100 -0
- package/dist/self-review.js +280 -0
- package/dist/sessions.js +122 -0
- package/dist/spinner.js +137 -0
- package/{src → dist}/system-prompt.js +23 -14
- package/{src → dist}/theme.js +16 -19
- package/dist/tools.js +211 -0
- package/dist/transcript.js +50 -0
- package/dist/types.js +3 -0
- package/dist/undo.js +99 -0
- package/dist/web.js +219 -0
- package/dist/xml-toolcall.js +67 -0
- package/package.json +17 -5
- package/src/agent-loop.js +0 -634
- package/src/browser.js +0 -627
- package/src/config.js +0 -81
- package/src/confirm.js +0 -85
- package/src/diff.js +0 -32
- package/src/gitTools.js +0 -256
- package/src/index.js +0 -1515
- package/src/input.js +0 -447
- package/src/markdown.js +0 -49
- package/src/self-review.js +0 -319
- package/src/sessions.js +0 -110
- package/src/spinner.js +0 -144
- package/src/tools.js +0 -206
- package/src/transcript.js +0 -46
- package/src/undo.js +0 -101
- package/src/web.js +0 -266
- package/src/xml-toolcall.js +0 -69
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
import { buildSystemPrompt } from './system-prompt.js';
|
|
2
|
+
import { getGitContext, formatGitContext } from './gitTools.js';
|
|
3
|
+
import { parseXmlToolCalls } from './xml-toolcall.js';
|
|
4
|
+
export async function runAgentLoop({ browser, tools, task, workdir, maxIterations = 40, freshChat = false, sendSystemPrompt = false, transcript = null, onThinking = () => { }, onAssistantThought = () => { }, onToolCall = () => { }, onToolResult = () => { }, onAssistantMessage = () => { }, onChatReady = () => { }, debugLog = false, }) {
|
|
5
|
+
if (freshChat) {
|
|
6
|
+
await browser.newChat();
|
|
7
|
+
transcript?.log('new_chat');
|
|
8
|
+
}
|
|
9
|
+
// Сообщаем вызывающему актуальный chat id. После newChat() URL ещё без id
|
|
10
|
+
// (он появляется только после первой отправки), поэтому зовём колбэк и
|
|
11
|
+
// здесь, и после первой реальной отправки ниже.
|
|
12
|
+
let lastReportedChatId = null;
|
|
13
|
+
const reportChat = async () => {
|
|
14
|
+
let id = null;
|
|
15
|
+
try {
|
|
16
|
+
id = await browser.getCurrentChatId();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
id = null;
|
|
20
|
+
}
|
|
21
|
+
if (id && id !== lastReportedChatId) {
|
|
22
|
+
lastReportedChatId = id;
|
|
23
|
+
onChatReady(id);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
await reportChat();
|
|
27
|
+
if (sendSystemPrompt) {
|
|
28
|
+
let gitText = null;
|
|
29
|
+
try {
|
|
30
|
+
const ctx = await getGitContext(workdir);
|
|
31
|
+
gitText = formatGitContext(ctx);
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
gitText = `(git context error: ${e.message})`;
|
|
35
|
+
}
|
|
36
|
+
const systemPrompt = buildSystemPrompt({
|
|
37
|
+
workdir,
|
|
38
|
+
tools,
|
|
39
|
+
gitContext: gitText,
|
|
40
|
+
});
|
|
41
|
+
transcript?.log('system_prompt', {
|
|
42
|
+
length: systemPrompt.length,
|
|
43
|
+
gitContext: gitText,
|
|
44
|
+
});
|
|
45
|
+
onThinking();
|
|
46
|
+
await browser.ask(systemPrompt, { timeout: 60_000 });
|
|
47
|
+
await reportChat();
|
|
48
|
+
}
|
|
49
|
+
let message = task;
|
|
50
|
+
transcript?.log('task', { task });
|
|
51
|
+
// Счётчик «ответ похож на tool-call, но не распознан». Чтобы модель,
|
|
52
|
+
// написавшая битый JSON/XML, не останавливала агента молча, мы просим
|
|
53
|
+
// её переотправить вызов. Ограничиваем число таких попыток.
|
|
54
|
+
let malformedRetries = 0;
|
|
55
|
+
const MAX_MALFORMED_RETRIES = 3;
|
|
56
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
57
|
+
onThinking();
|
|
58
|
+
const rawResponse = await browser.ask(message);
|
|
59
|
+
await reportChat();
|
|
60
|
+
transcript?.log('assistant_raw', { response: rawResponse });
|
|
61
|
+
const parsed = parseToolCall(rawResponse);
|
|
62
|
+
if (parsed) {
|
|
63
|
+
const thought = extractPreToolText(rawResponse);
|
|
64
|
+
if (thought)
|
|
65
|
+
onAssistantThought(thought);
|
|
66
|
+
}
|
|
67
|
+
const parsedCalls = Array.isArray(parsed) ? parsed : parsed ? [parsed] : [];
|
|
68
|
+
if (parsedCalls.some((p) => p && p._permissive)) {
|
|
69
|
+
// Пишем в транскрипт (для отладки), но не сыпем в консоль.
|
|
70
|
+
transcript?.log('permissive_parse', { response: rawResponse });
|
|
71
|
+
if (debugLog) {
|
|
72
|
+
console.error('внимание: tool-call распознан нестрогим парсером');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (!parsed) {
|
|
76
|
+
// Ответ не распознан как tool-call. Если он ПОХОЖ на попытку вызова
|
|
77
|
+
// (есть tool/invoke/parameter, но JSON/XML битый) — это почти всегда
|
|
78
|
+
// ошибка формата. Не считаем её финальным ответом (иначе агент молча
|
|
79
|
+
// остановится), а просим модель переотправить вызов корректно.
|
|
80
|
+
const looksLikeToolCall = /("tool"\s*:|\btool_calls?\b|\binvoke\b|\bparameter\b|DSML|function_call)/i.test(rawResponse) || /\{\s*"?(tool|name)"?\s*:/.test(rawResponse);
|
|
81
|
+
if (looksLikeToolCall && malformedRetries < MAX_MALFORMED_RETRIES) {
|
|
82
|
+
malformedRetries++;
|
|
83
|
+
transcript?.log("malformed_toolcall", { attempt: malformedRetries, response: rawResponse });
|
|
84
|
+
if (debugLog) {
|
|
85
|
+
console.error("внимание: ответ похож на tool-call, но не распознан (попытка " + malformedRetries + "/" + MAX_MALFORMED_RETRIES + ")");
|
|
86
|
+
}
|
|
87
|
+
message =
|
|
88
|
+
'Твой предыдущий ответ не распознан как вызов инструмента. ' +
|
|
89
|
+
'Ответь РОВНО одним JSON-объектом вызова инструмента, без текста до и после. ' +
|
|
90
|
+
'НЕ используй XML/DSML-теги — только JSON. ' +
|
|
91
|
+
'Например: {"tool": "Read", "args": {"path": "src/index.js"}}';
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
onAssistantMessage(rawResponse);
|
|
95
|
+
transcript?.log('assistant_final', { message: rawResponse });
|
|
96
|
+
return rawResponse;
|
|
97
|
+
}
|
|
98
|
+
const calls = Array.isArray(parsed) ? parsed : [parsed];
|
|
99
|
+
const respondCall = calls.find((c) => c.tool === 'respond');
|
|
100
|
+
if (respondCall) {
|
|
101
|
+
const msg = typeof respondCall.args.message === 'string'
|
|
102
|
+
? respondCall.args.message
|
|
103
|
+
: String(respondCall.args.message ?? '');
|
|
104
|
+
onAssistantMessage(msg);
|
|
105
|
+
transcript?.log('assistant_final', { message: msg });
|
|
106
|
+
return msg;
|
|
107
|
+
}
|
|
108
|
+
const results = [];
|
|
109
|
+
for (const call of calls) {
|
|
110
|
+
const tool = tools.find((t) => t.name === call.tool);
|
|
111
|
+
if (!tool) {
|
|
112
|
+
const err = `Неизвестный инструмент: ${call.tool}`;
|
|
113
|
+
onToolResult(err);
|
|
114
|
+
transcript?.log('tool_error', { tool: call.tool, error: err });
|
|
115
|
+
results.push({ tool: call.tool, result: err });
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
onToolCall(call.tool, call.args);
|
|
119
|
+
transcript?.log('tool_call', { tool: call.tool, args: call.args });
|
|
120
|
+
let result;
|
|
121
|
+
try {
|
|
122
|
+
result = await tool.fn(call.args);
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
result = `Ошибка: ${e.message}`;
|
|
126
|
+
}
|
|
127
|
+
onToolResult(result);
|
|
128
|
+
transcript?.log('tool_result', {
|
|
129
|
+
tool: call.tool,
|
|
130
|
+
result: String(result),
|
|
131
|
+
});
|
|
132
|
+
results.push({ tool: call.tool, result });
|
|
133
|
+
}
|
|
134
|
+
if (results.length === 1) {
|
|
135
|
+
const r = results[0];
|
|
136
|
+
const resultStr = typeof r.result === 'string' ? r.result : JSON.stringify(r.result);
|
|
137
|
+
message = `Tool result for ${r.tool}:\n${resultStr.slice(0, 12_000)}`;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
message = results
|
|
141
|
+
.map((r) => {
|
|
142
|
+
const resultStr = typeof r.result === 'string' ? r.result : JSON.stringify(r.result);
|
|
143
|
+
return `Tool result for ${r.tool}:\n${resultStr.slice(0, 8000)}`;
|
|
144
|
+
})
|
|
145
|
+
.join('\n\n');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return 'Достигнут лимит итераций.';
|
|
149
|
+
}
|
|
150
|
+
// ============ JSON parsing ============
|
|
151
|
+
function repairRawControlChars(str) {
|
|
152
|
+
let out = '';
|
|
153
|
+
let inString = false;
|
|
154
|
+
let escape = false;
|
|
155
|
+
for (let i = 0; i < str.length; i++) {
|
|
156
|
+
const c = str[i];
|
|
157
|
+
if (escape) {
|
|
158
|
+
out += c;
|
|
159
|
+
escape = false;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (c === '\\') {
|
|
163
|
+
out += c;
|
|
164
|
+
escape = true;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (c === '"') {
|
|
168
|
+
inString = !inString;
|
|
169
|
+
out += c;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (inString) {
|
|
173
|
+
if (c === '\n') {
|
|
174
|
+
out += '\\n';
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (c === '\r') {
|
|
178
|
+
out += '\\r';
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (c === '\t') {
|
|
182
|
+
out += '\\t';
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const code = c.charCodeAt(0);
|
|
186
|
+
if (code < 0x20) {
|
|
187
|
+
out += '\\u' + code.toString(16).padStart(4, '0');
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
out += c;
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
function tryParse(str) {
|
|
196
|
+
try {
|
|
197
|
+
const obj = JSON.parse(str);
|
|
198
|
+
if (obj &&
|
|
199
|
+
typeof obj.tool === 'string' &&
|
|
200
|
+
obj.args &&
|
|
201
|
+
typeof obj.args === 'object' &&
|
|
202
|
+
!Array.isArray(obj.args)) {
|
|
203
|
+
return obj;
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function tryParseArray(str) {
|
|
212
|
+
try {
|
|
213
|
+
const arr = JSON.parse(str);
|
|
214
|
+
if (Array.isArray(arr) &&
|
|
215
|
+
arr.length > 0 &&
|
|
216
|
+
arr.every((o) => o &&
|
|
217
|
+
typeof o.tool === 'string' &&
|
|
218
|
+
o.args &&
|
|
219
|
+
typeof o.args === 'object' &&
|
|
220
|
+
!Array.isArray(o.args))) {
|
|
221
|
+
return arr;
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// Жадный разбор args для грязного JSON (незаэкранированные кавычки в строке).
|
|
230
|
+
function parseArgsGreedy(str) {
|
|
231
|
+
const result = {};
|
|
232
|
+
let i = str.indexOf('{') + 1;
|
|
233
|
+
if (i === 0)
|
|
234
|
+
return null;
|
|
235
|
+
const skipWs = () => {
|
|
236
|
+
while (i < str.length && /[\s,]/.test(str[i]))
|
|
237
|
+
i++;
|
|
238
|
+
};
|
|
239
|
+
while (i < str.length) {
|
|
240
|
+
skipWs();
|
|
241
|
+
if (i >= str.length || str[i] === '}')
|
|
242
|
+
break;
|
|
243
|
+
if (str[i] !== '"')
|
|
244
|
+
return null;
|
|
245
|
+
const keyEnd = str.indexOf('"', i + 1);
|
|
246
|
+
if (keyEnd === -1)
|
|
247
|
+
return null;
|
|
248
|
+
const key = str.slice(i + 1, keyEnd);
|
|
249
|
+
i = keyEnd + 1;
|
|
250
|
+
while (i < str.length && /\s/.test(str[i]))
|
|
251
|
+
i++;
|
|
252
|
+
if (str[i] !== ':')
|
|
253
|
+
return null;
|
|
254
|
+
i++;
|
|
255
|
+
while (i < str.length && /\s/.test(str[i]))
|
|
256
|
+
i++;
|
|
257
|
+
if (str[i] === '"') {
|
|
258
|
+
let lastEnd = -1;
|
|
259
|
+
let k = i + 1;
|
|
260
|
+
while (k < str.length) {
|
|
261
|
+
if (str.charCodeAt(k) === 92) {
|
|
262
|
+
k += 2;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (str[k] === '"') {
|
|
266
|
+
let t = k + 1;
|
|
267
|
+
while (t < str.length && /\s/.test(str[t]))
|
|
268
|
+
t++;
|
|
269
|
+
if (t >= str.length || str[t] === ',' || str[t] === '}')
|
|
270
|
+
lastEnd = k;
|
|
271
|
+
}
|
|
272
|
+
k++;
|
|
273
|
+
}
|
|
274
|
+
if (lastEnd === -1)
|
|
275
|
+
return null;
|
|
276
|
+
result[key] = unescapeValue(str.slice(i + 1, lastEnd));
|
|
277
|
+
i = lastEnd + 1;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (str[i] === '{' || str[i] === '[') {
|
|
281
|
+
const open = str[i];
|
|
282
|
+
const close = open === '{' ? '}' : ']';
|
|
283
|
+
const end = findMatching(str, i, open, close);
|
|
284
|
+
if (end === -1)
|
|
285
|
+
return null;
|
|
286
|
+
let parsed = null;
|
|
287
|
+
try {
|
|
288
|
+
parsed = JSON.parse(str.slice(i, end + 1));
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
if (open === '{')
|
|
292
|
+
parsed = parseArgsPermissive(str.slice(i, end + 1));
|
|
293
|
+
}
|
|
294
|
+
if (parsed === null)
|
|
295
|
+
return null;
|
|
296
|
+
result[key] = parsed;
|
|
297
|
+
i = end + 1;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
let j = i;
|
|
301
|
+
while (j < str.length && !/[,}]/.test(str[j]))
|
|
302
|
+
j++;
|
|
303
|
+
const raw = str.slice(i, j).trim();
|
|
304
|
+
if (raw === 'true')
|
|
305
|
+
result[key] = true;
|
|
306
|
+
else if (raw === 'false')
|
|
307
|
+
result[key] = false;
|
|
308
|
+
else if (raw === 'null')
|
|
309
|
+
result[key] = null;
|
|
310
|
+
else {
|
|
311
|
+
const n = Number(raw);
|
|
312
|
+
result[key] = Number.isNaN(n) ? raw : n;
|
|
313
|
+
}
|
|
314
|
+
i = j;
|
|
315
|
+
}
|
|
316
|
+
return result;
|
|
317
|
+
}
|
|
318
|
+
function parseArgsPermissive(str) {
|
|
319
|
+
const result = {};
|
|
320
|
+
let i = 1;
|
|
321
|
+
while (i < str.length) {
|
|
322
|
+
while (i < str.length && /[\s,]/.test(str[i]))
|
|
323
|
+
i++;
|
|
324
|
+
if (i >= str.length || str[i] === '}')
|
|
325
|
+
break;
|
|
326
|
+
if (str[i] !== '"')
|
|
327
|
+
return null;
|
|
328
|
+
let keyEnd = str.indexOf('"', i + 1);
|
|
329
|
+
if (keyEnd === -1)
|
|
330
|
+
return null;
|
|
331
|
+
const key = str.slice(i + 1, keyEnd);
|
|
332
|
+
i = keyEnd + 1;
|
|
333
|
+
while (i < str.length && /\s/.test(str[i]))
|
|
334
|
+
i++;
|
|
335
|
+
if (str[i] !== ':')
|
|
336
|
+
return null;
|
|
337
|
+
i++;
|
|
338
|
+
while (i < str.length && /\s/.test(str[i]))
|
|
339
|
+
i++;
|
|
340
|
+
if (str[i] === '"') {
|
|
341
|
+
let j = i + 1;
|
|
342
|
+
let value = '';
|
|
343
|
+
while (j < str.length) {
|
|
344
|
+
const c = str[j];
|
|
345
|
+
if (c === '\\' && j + 1 < str.length) {
|
|
346
|
+
value += c + str[j + 1];
|
|
347
|
+
j += 2;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (c === '"') {
|
|
351
|
+
let k = j + 1;
|
|
352
|
+
while (k < str.length && /\s/.test(str[k]))
|
|
353
|
+
k++;
|
|
354
|
+
if (k >= str.length ||
|
|
355
|
+
str[k] === ',' ||
|
|
356
|
+
str[k] === '}' ||
|
|
357
|
+
str[k] === ']') {
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
value += '"';
|
|
361
|
+
j++;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
value += c;
|
|
365
|
+
j++;
|
|
366
|
+
}
|
|
367
|
+
if (j >= str.length)
|
|
368
|
+
return null;
|
|
369
|
+
result[key] = unescapeValue(value);
|
|
370
|
+
i = j + 1;
|
|
371
|
+
}
|
|
372
|
+
else if (str[i] === '{' || str[i] === '[') {
|
|
373
|
+
// Вложенный объект/массив: находим сбалансированный фрагмент и
|
|
374
|
+
// пытаемся распарсить его как JSON (или как вложенный permissive).
|
|
375
|
+
const open = str[i];
|
|
376
|
+
const close = open === '{' ? '}' : ']';
|
|
377
|
+
const end = findMatching(str, i, open, close);
|
|
378
|
+
if (end === -1)
|
|
379
|
+
return null;
|
|
380
|
+
const frag = str.slice(i, end + 1);
|
|
381
|
+
let parsedFrag = null;
|
|
382
|
+
try {
|
|
383
|
+
parsedFrag = JSON.parse(frag);
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
if (open === '{')
|
|
387
|
+
parsedFrag = parseArgsPermissive(frag);
|
|
388
|
+
}
|
|
389
|
+
if (parsedFrag === null)
|
|
390
|
+
return null;
|
|
391
|
+
result[key] = parsedFrag;
|
|
392
|
+
i = end + 1;
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
let j = i;
|
|
396
|
+
while (j < str.length && !/[,}]/.test(str[j]))
|
|
397
|
+
j++;
|
|
398
|
+
const raw = str.slice(i, j).trim();
|
|
399
|
+
if (raw === 'true')
|
|
400
|
+
result[key] = true;
|
|
401
|
+
else if (raw === 'false')
|
|
402
|
+
result[key] = false;
|
|
403
|
+
else if (raw === 'null')
|
|
404
|
+
result[key] = null;
|
|
405
|
+
else {
|
|
406
|
+
const n = Number(raw);
|
|
407
|
+
result[key] = Number.isNaN(n) ? raw : n;
|
|
408
|
+
}
|
|
409
|
+
i = j;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return result;
|
|
413
|
+
}
|
|
414
|
+
function unescapeValue(s) {
|
|
415
|
+
let out = '';
|
|
416
|
+
let i = 0;
|
|
417
|
+
while (i < s.length) {
|
|
418
|
+
const c = s[i];
|
|
419
|
+
if (c === '\\' && i + 1 < s.length) {
|
|
420
|
+
const n = s[i + 1];
|
|
421
|
+
if (n === 'n') {
|
|
422
|
+
out += '\n';
|
|
423
|
+
i += 2;
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (n === 't') {
|
|
427
|
+
out += '\t';
|
|
428
|
+
i += 2;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (n === 'r') {
|
|
432
|
+
out += '\r';
|
|
433
|
+
i += 2;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (n === '"') {
|
|
437
|
+
out += '"';
|
|
438
|
+
i += 2;
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (n === '\\') {
|
|
442
|
+
out += '\\';
|
|
443
|
+
i += 2;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
if (n === '/') {
|
|
447
|
+
out += '/';
|
|
448
|
+
i += 2;
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (n === 'u' && i + 5 < s.length) {
|
|
452
|
+
const hex = s.slice(i + 2, i + 6);
|
|
453
|
+
if (/^[0-9a-fA-F]{4}$/.test(hex)) {
|
|
454
|
+
out += String.fromCharCode(parseInt(hex, 16));
|
|
455
|
+
i += 6;
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
out += c + n;
|
|
460
|
+
i += 2;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
out += c;
|
|
464
|
+
i++;
|
|
465
|
+
}
|
|
466
|
+
return out;
|
|
467
|
+
}
|
|
468
|
+
function parseToolCallPermissive(text) {
|
|
469
|
+
const toolMatch = text.match(/"tool"\s*:\s*"([A-Za-z_][A-Za-z0-9_]*)"/);
|
|
470
|
+
if (!toolMatch)
|
|
471
|
+
return null;
|
|
472
|
+
const tool = toolMatch[1];
|
|
473
|
+
const argsIdx = text.indexOf('"args"');
|
|
474
|
+
if (argsIdx === -1)
|
|
475
|
+
return null;
|
|
476
|
+
const openIdx = text.indexOf('{', argsIdx);
|
|
477
|
+
if (openIdx === -1)
|
|
478
|
+
return null;
|
|
479
|
+
// Берём сбалансированный по скобкам фрагмент args (с учётом строк),
|
|
480
|
+
// а не первый попавшийся '}'. Иначе вложенные объекты/массивы или
|
|
481
|
+
// фигурные скобки внутри строк ломают разбор.
|
|
482
|
+
const endIdx = findMatching(text, openIdx, '{', '}');
|
|
483
|
+
if (tool === 'Edit') {
|
|
484
|
+
// Edit-аргументы часто содержат сырые кавычки и переводы строк, из-за
|
|
485
|
+
// чего балансировка скобок сбоит. parseEditArgs рассчитан ровно на
|
|
486
|
+
// такой случай, поэтому пробуем его и на «хвосте» до конца текста,
|
|
487
|
+
// а не только на сбалансированном фрагменте.
|
|
488
|
+
const balanced = endIdx === -1 ? null : text.slice(openIdx, endIdx + 1);
|
|
489
|
+
const tailToEnd = text.slice(openIdx);
|
|
490
|
+
for (const frag of [balanced, tailToEnd]) {
|
|
491
|
+
if (!frag)
|
|
492
|
+
continue;
|
|
493
|
+
const e = parseEditArgs(frag);
|
|
494
|
+
if (e)
|
|
495
|
+
return { tool, args: e };
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
// Балансировка скобок сбоит, если в строковых значениях есть сырые
|
|
499
|
+
// кавычки/скобки (частый случай для Bash/Write с кодом внутри). Тогда
|
|
500
|
+
// пробуем разобрать args из «хвоста» до конца текста и жадным парсером.
|
|
501
|
+
const frags = [];
|
|
502
|
+
if (endIdx !== -1)
|
|
503
|
+
frags.push(text.slice(openIdx, endIdx + 1));
|
|
504
|
+
frags.push(text.slice(openIdx));
|
|
505
|
+
for (const argsStr of frags) {
|
|
506
|
+
const args = parseArgsPermissive(argsStr);
|
|
507
|
+
if (args)
|
|
508
|
+
return { tool, args };
|
|
509
|
+
const greedy = parseArgsGreedy(argsStr);
|
|
510
|
+
if (greedy)
|
|
511
|
+
return { tool, args: greedy };
|
|
512
|
+
}
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
// Специализированный разбор args для Edit: ровно три поля path/old_string/
|
|
516
|
+
// new_string, значения могут содержать сырые кавычки и переводы строк.
|
|
517
|
+
function parseEditArgs(str) {
|
|
518
|
+
const keyRe = (name) => new RegExp('"' + name + '"\\s*:\\s*"');
|
|
519
|
+
const readValue = (name, nextNames) => {
|
|
520
|
+
const m = keyRe(name).exec(str);
|
|
521
|
+
if (!m)
|
|
522
|
+
return null;
|
|
523
|
+
const start = m.index + m[0].length;
|
|
524
|
+
let end = str.length;
|
|
525
|
+
for (const n of nextNames) {
|
|
526
|
+
const mm = keyRe(n).exec(str.slice(start));
|
|
527
|
+
if (mm) {
|
|
528
|
+
const absIdx = start + mm.index;
|
|
529
|
+
if (absIdx < end)
|
|
530
|
+
end = absIdx;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
let val = str.slice(start, end);
|
|
534
|
+
val = val.replace(/"\s*[,}]?\s*$/, '');
|
|
535
|
+
return val;
|
|
536
|
+
};
|
|
537
|
+
const path = readValue('path', ['old_string', 'new_string']);
|
|
538
|
+
const oldStr = readValue('old_string', ['new_string']);
|
|
539
|
+
const newStr = readValue('new_string', []);
|
|
540
|
+
if (path === null || oldStr === null || newStr === null)
|
|
541
|
+
return null;
|
|
542
|
+
return { path: path, old_string: oldStr, new_string: newStr };
|
|
543
|
+
}
|
|
544
|
+
function extractJsonObjects(text) {
|
|
545
|
+
const objects = [];
|
|
546
|
+
let i = 0;
|
|
547
|
+
while (i < text.length) {
|
|
548
|
+
if (text[i] === '{') {
|
|
549
|
+
const end = findMatching(text, i, '{', '}');
|
|
550
|
+
if (end !== -1) {
|
|
551
|
+
objects.push(text.slice(i, end + 1));
|
|
552
|
+
i = end + 1;
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (text[i] === '[') {
|
|
557
|
+
const end = findMatching(text, i, '[', ']');
|
|
558
|
+
if (end !== -1) {
|
|
559
|
+
objects.push(text.slice(i, end + 1));
|
|
560
|
+
i = end + 1;
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
i++;
|
|
565
|
+
}
|
|
566
|
+
return objects;
|
|
567
|
+
}
|
|
568
|
+
function findMatching(text, openIdx, openCh, closeCh) {
|
|
569
|
+
let depth = 0;
|
|
570
|
+
let inString = false;
|
|
571
|
+
let escape = false;
|
|
572
|
+
for (let i = openIdx; i < text.length; i++) {
|
|
573
|
+
const c = text[i];
|
|
574
|
+
if (escape) {
|
|
575
|
+
escape = false;
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
if (c === '\\') {
|
|
579
|
+
escape = true;
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
if (c === '"') {
|
|
583
|
+
inString = !inString;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
if (inString)
|
|
587
|
+
continue;
|
|
588
|
+
if (c === openCh)
|
|
589
|
+
depth++;
|
|
590
|
+
else if (c === closeCh) {
|
|
591
|
+
depth--;
|
|
592
|
+
if (depth === 0)
|
|
593
|
+
return i;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return -1;
|
|
597
|
+
}
|
|
598
|
+
export function parseToolCall(text) {
|
|
599
|
+
if (!text || typeof text !== 'string')
|
|
600
|
+
return null;
|
|
601
|
+
let cleaned = text.trim();
|
|
602
|
+
cleaned = cleaned
|
|
603
|
+
.replace(/^```(?:json)?\s*/i, '')
|
|
604
|
+
.replace(/```$/i, '')
|
|
605
|
+
.trim();
|
|
606
|
+
const candidates = extractJsonObjects(cleaned);
|
|
607
|
+
for (let i = candidates.length - 1; i >= 0; i--) {
|
|
608
|
+
const raw = candidates[i];
|
|
609
|
+
const arrFirst = tryParseArray(raw);
|
|
610
|
+
if (arrFirst)
|
|
611
|
+
return arrFirst;
|
|
612
|
+
const arrRepaired = tryParseArray(raw.replace(/\\(?!["\\/bfnrtu])/g, '\\\\'));
|
|
613
|
+
if (arrRepaired)
|
|
614
|
+
return arrRepaired;
|
|
615
|
+
const first = tryParse(raw);
|
|
616
|
+
if (first)
|
|
617
|
+
return first;
|
|
618
|
+
const repaired = raw.replace(/\\(?!["\\/bfnrtu])/g, '\\\\');
|
|
619
|
+
const second = tryParse(repaired);
|
|
620
|
+
if (second)
|
|
621
|
+
return second;
|
|
622
|
+
// Сырые переводы строк/табы внутри строковых значений (old_string,
|
|
623
|
+
// new_string, content) делают JSON невалидным. Экранируем их и
|
|
624
|
+
// пробуем снова, иначе многострочный вызов не распознаётся.
|
|
625
|
+
const ctrl = repairRawControlChars(raw);
|
|
626
|
+
const third = tryParse(ctrl);
|
|
627
|
+
if (third)
|
|
628
|
+
return third;
|
|
629
|
+
const ctrlArr = tryParseArray(ctrl);
|
|
630
|
+
if (ctrlArr)
|
|
631
|
+
return ctrlArr;
|
|
632
|
+
}
|
|
633
|
+
const permissive = parseToolCallPermissive(cleaned) ||
|
|
634
|
+
parseToolCallPermissive(repairRawControlChars(cleaned));
|
|
635
|
+
if (permissive)
|
|
636
|
+
return { ...permissive, _permissive: true };
|
|
637
|
+
// Fallback: модель могла обернуть JSON в прозу и/или добавить мусорные
|
|
638
|
+
// теги после него. Отрезаем всё до первого '"tool"' и всё, что похоже
|
|
639
|
+
// на XML/DSML-хвост, затем пробуем permissive-разбор ещё раз.
|
|
640
|
+
const toolIdx = cleaned.search(/["']?tool["']?\s:/);
|
|
641
|
+
if (toolIdx > 0) {
|
|
642
|
+
let tail = cleaned.slice(toolIdx);
|
|
643
|
+
// Регексп обязан матчить многосимвольные теги (<|DSML|invoke ...>),
|
|
644
|
+
// поэтому <[^>]*>, а не <[^>]> — иначе DSML-хвост не срезается,
|
|
645
|
+
// permissive-разбор падает и агент молча останавливается.
|
|
646
|
+
tail = tail.replace(/<[^>]*>.*$/s, '').trim();
|
|
647
|
+
const tailPermissive = parseToolCallPermissive('{"' + tail) ||
|
|
648
|
+
parseToolCallPermissive('{"' + repairRawControlChars(tail));
|
|
649
|
+
if (tailPermissive)
|
|
650
|
+
return { ...tailPermissive, _permissive: true };
|
|
651
|
+
}
|
|
652
|
+
// Последний fallback: модель ответила XML/DSML-блоком вместо JSON.
|
|
653
|
+
const xmlCalls = parseXmlToolCalls(cleaned);
|
|
654
|
+
if (xmlCalls)
|
|
655
|
+
return Array.isArray(xmlCalls) ? xmlCalls : [xmlCalls];
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
function extractPreToolText(text) {
|
|
659
|
+
if (!text)
|
|
660
|
+
return '';
|
|
661
|
+
const patterns = ['{"tool"', '[{"tool"', '{"tool":', '[{"tool":'];
|
|
662
|
+
let earliest = -1;
|
|
663
|
+
for (const p of patterns) {
|
|
664
|
+
const idx = text.indexOf(p);
|
|
665
|
+
if (idx >= 0 && (earliest === -1 || idx < earliest))
|
|
666
|
+
earliest = idx;
|
|
667
|
+
}
|
|
668
|
+
if (earliest === -1)
|
|
669
|
+
return '';
|
|
670
|
+
return text.slice(0, earliest).trim();
|
|
671
|
+
}
|