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