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