zames_pro 1.1.1 → 1.2.1
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/README.md +5 -0
- package/package.json +1 -1
- package/src/agent-loop.js +99 -12
- package/src/browser.js +22 -0
- package/src/config.js +1 -0
- package/src/index.js +403 -109
- package/src/input.js +447 -0
- package/src/sessions.js +110 -0
- package/src/spinner.js +60 -2
- package/src/system-prompt.js +39 -5
- package/src/theme.js +1 -0
- package/src/xml-toolcall.js +69 -0
package/README.md
CHANGED
|
@@ -29,6 +29,11 @@ zames
|
|
|
29
29
|
|
|
30
30
|
The agent works inside the directory it was started in and cannot leave it (sandbox).
|
|
31
31
|
|
|
32
|
+
While the agent is working you can keep typing: press Enter to queue a message
|
|
33
|
+
(it is sent right after the current task, in the same chat), or Esc / Ctrl+C to
|
|
34
|
+
abort the current generation. This mirrors typing during generation on the
|
|
35
|
+
DeepSeek website.
|
|
36
|
+
|
|
32
37
|
### Options
|
|
33
38
|
|
|
34
39
|
```
|
package/package.json
CHANGED
package/src/agent-loop.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { buildSystemPrompt } from './system-prompt.js'
|
|
2
2
|
import { getGitContext, formatGitContext } from './gitTools.js'
|
|
3
|
+
import { parseXmlToolCalls } from './xml-toolcall.js'
|
|
3
4
|
|
|
4
5
|
export async function runAgentLoop({
|
|
5
6
|
browser,
|
|
@@ -47,6 +48,12 @@ export async function runAgentLoop({
|
|
|
47
48
|
let message = task
|
|
48
49
|
transcript?.log('task', { task })
|
|
49
50
|
|
|
51
|
+
// Счётчик «ответ похож на tool-call, но не распознан». Чтобы модель,
|
|
52
|
+
// написавшая битый JSON/XML, не останавливала агента молча, мы просим
|
|
53
|
+
// её переотправить вызов. Ограничиваем число таких попыток.
|
|
54
|
+
let malformedRetries = 0
|
|
55
|
+
const MAX_MALFORMED_RETRIES = 3
|
|
56
|
+
|
|
50
57
|
for (let i = 0; i < maxIterations; i++) {
|
|
51
58
|
onThinking()
|
|
52
59
|
const rawResponse = await browser.ask(message)
|
|
@@ -69,6 +76,28 @@ export async function runAgentLoop({
|
|
|
69
76
|
}
|
|
70
77
|
|
|
71
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
|
+
|
|
72
101
|
onAssistantMessage(rawResponse)
|
|
73
102
|
transcript?.log('assistant_final', { message: rawResponse })
|
|
74
103
|
return rawResponse
|
|
@@ -138,6 +167,27 @@ export async function runAgentLoop({
|
|
|
138
167
|
|
|
139
168
|
// ============ JSON parsing ============
|
|
140
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
|
+
|
|
141
191
|
function tryParse(str) {
|
|
142
192
|
try {
|
|
143
193
|
const obj = JSON.parse(str)
|
|
@@ -393,17 +443,34 @@ function parseToolCallPermissive(text) {
|
|
|
393
443
|
// а не первый попавшийся '}'. Иначе вложенные объекты/массивы или
|
|
394
444
|
// фигурные скобки внутри строк ломают разбор.
|
|
395
445
|
const endIdx = findMatching(text, openIdx, '{', '}')
|
|
396
|
-
if (endIdx === -1) return null
|
|
397
446
|
|
|
398
|
-
const argsStr = text.slice(openIdx, endIdx + 1)
|
|
399
447
|
if (tool === 'Edit') {
|
|
400
|
-
|
|
401
|
-
|
|
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 }
|
|
402
473
|
}
|
|
403
|
-
const args = parseArgsPermissive(argsStr)
|
|
404
|
-
if (args) return { tool, args }
|
|
405
|
-
const greedy = parseArgsGreedy(argsStr)
|
|
406
|
-
if (greedy) return { tool, args: greedy }
|
|
407
474
|
return null
|
|
408
475
|
}
|
|
409
476
|
|
|
@@ -487,7 +554,7 @@ function findMatching(text, openIdx, openCh, closeCh) {
|
|
|
487
554
|
return -1
|
|
488
555
|
}
|
|
489
556
|
|
|
490
|
-
function parseToolCall(text) {
|
|
557
|
+
export function parseToolCall(text) {
|
|
491
558
|
if (!text || typeof text !== 'string') return null
|
|
492
559
|
|
|
493
560
|
let cleaned = text.trim()
|
|
@@ -515,9 +582,20 @@ function parseToolCall(text) {
|
|
|
515
582
|
const repaired = raw.replace(/\\(?!["\\/bfnrtu])/g, '\\\\')
|
|
516
583
|
const second = tryParse(repaired)
|
|
517
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
|
|
518
594
|
}
|
|
519
595
|
|
|
520
|
-
const permissive =
|
|
596
|
+
const permissive =
|
|
597
|
+
parseToolCallPermissive(cleaned) ||
|
|
598
|
+
parseToolCallPermissive(repairRawControlChars(cleaned))
|
|
521
599
|
if (permissive) return { ...permissive, _permissive: true }
|
|
522
600
|
|
|
523
601
|
// Fallback: модель могла обернуть JSON в прозу и/или добавить мусорные
|
|
@@ -526,11 +604,20 @@ function parseToolCall(text) {
|
|
|
526
604
|
const toolIdx = cleaned.search(/["']?tool["']?\s:/)
|
|
527
605
|
if (toolIdx > 0) {
|
|
528
606
|
let tail = cleaned.slice(toolIdx)
|
|
529
|
-
|
|
530
|
-
|
|
607
|
+
// Регексп обязан матчить многосимвольные теги (<|DSML|invoke ...>),
|
|
608
|
+
// поэтому <[^>]*>, а не <[^>]> — иначе DSML-хвост не срезается,
|
|
609
|
+
// permissive-разбор падает и агент молча останавливается.
|
|
610
|
+
tail = tail.replace(/<[^>]*>.*$/s, '').trim()
|
|
611
|
+
const tailPermissive =
|
|
612
|
+
parseToolCallPermissive('{"' + tail) ||
|
|
613
|
+
parseToolCallPermissive('{"' + repairRawControlChars(tail))
|
|
531
614
|
if (tailPermissive) return { ...tailPermissive, _permissive: true }
|
|
532
615
|
}
|
|
533
616
|
|
|
617
|
+
// Последний fallback: модель ответила XML/DSML-блоком вместо JSON.
|
|
618
|
+
const xmlCalls = parseXmlToolCalls(cleaned)
|
|
619
|
+
if (xmlCalls) return Array.isArray(xmlCalls) ? xmlCalls : [xmlCalls]
|
|
620
|
+
|
|
534
621
|
return null
|
|
535
622
|
}
|
|
536
623
|
|
package/src/browser.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'path'
|
|
|
3
3
|
import os from 'os'
|
|
4
4
|
import fs from 'fs/promises'
|
|
5
5
|
import { execSync } from 'child_process'
|
|
6
|
+
import { theme } from './theme.js'
|
|
6
7
|
|
|
7
8
|
const USER_DATA_DIR = path.join(os.homedir(), '.zames', 'profile')
|
|
8
9
|
const CHAT_URL = 'https://chat.deepseek.com/'
|
|
@@ -78,6 +79,7 @@ export class DeepSeekBrowser {
|
|
|
78
79
|
askRetries = 3,
|
|
79
80
|
stabilityChecks = 3,
|
|
80
81
|
stabilityDelayMs = 1000,
|
|
82
|
+
minSendIntervalMs = 15000,
|
|
81
83
|
} = {}) {
|
|
82
84
|
this.headless = headless
|
|
83
85
|
this.debug = debug
|
|
@@ -86,6 +88,10 @@ export class DeepSeekBrowser {
|
|
|
86
88
|
this.askRetries = askRetries
|
|
87
89
|
this.stabilityChecks = stabilityChecks
|
|
88
90
|
this.stabilityDelayMs = stabilityDelayMs
|
|
91
|
+
// Минимальный интервал между отправками в чат: DeepSeek ограничивает
|
|
92
|
+
// частоту («Messages too frequent. Try again later.»).
|
|
93
|
+
this.minSendIntervalMs = minSendIntervalMs
|
|
94
|
+
this._lastSentAt = 0
|
|
89
95
|
this.context = null
|
|
90
96
|
this.page = null
|
|
91
97
|
}
|
|
@@ -418,6 +424,20 @@ export class DeepSeekBrowser {
|
|
|
418
424
|
}
|
|
419
425
|
}
|
|
420
426
|
|
|
427
|
+
// Ждём, пока с прошлой отправки пройдёт minSendIntervalMs. Защита от
|
|
428
|
+
// «Messages too frequent. Try again later.» на [chat.deepseek.com](https://chat.deepseek.com/).
|
|
429
|
+
async _waitForSendSlot() {
|
|
430
|
+
if (!this._lastSentAt) return // первая отправка — пауза не нужна
|
|
431
|
+
const gap = this.minSendIntervalMs - (Date.now() - this._lastSentAt)
|
|
432
|
+
if (gap <= 0) return
|
|
433
|
+
console.error(
|
|
434
|
+
theme.warn(
|
|
435
|
+
`⏳ пауза ${Math.ceil(gap / 1000)}с перед отправкой (лимит частоты DeepSeek)...`,
|
|
436
|
+
),
|
|
437
|
+
)
|
|
438
|
+
await this.page.waitForTimeout(gap)
|
|
439
|
+
}
|
|
440
|
+
|
|
421
441
|
async _askOnce(prompt, { timeout }) {
|
|
422
442
|
const input = await this._findVisible(INPUT_SELECTORS, 10_000)
|
|
423
443
|
if (!input) {
|
|
@@ -428,6 +448,7 @@ export class DeepSeekBrowser {
|
|
|
428
448
|
|
|
429
449
|
const beforeText = await this._readLastAnswerTextClean().catch(() => '')
|
|
430
450
|
|
|
451
|
+
await this._waitForSendSlot()
|
|
431
452
|
await this._setInputText(input, prompt)
|
|
432
453
|
await this.page.waitForTimeout(200)
|
|
433
454
|
|
|
@@ -440,6 +461,7 @@ export class DeepSeekBrowser {
|
|
|
440
461
|
} catch {
|
|
441
462
|
await this.page.keyboard.press('Enter')
|
|
442
463
|
}
|
|
464
|
+
this._lastSentAt = Date.now()
|
|
443
465
|
|
|
444
466
|
const startDeadline = Date.now() + 15_000
|
|
445
467
|
let started = false
|