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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {
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
- const e = parseEditArgs(argsStr)
401
- if (e) return { tool, args: e }
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 = parseToolCallPermissive(cleaned)
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
- tail = tail.replace(/<[^>]>.*$/s, '').trim()
530
- const tailPermissive = parseToolCallPermissive('{"' + tail)
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
package/src/config.js CHANGED
@@ -43,6 +43,7 @@ const DEFAULTS = {
43
43
  askRetries: 3,
44
44
  stabilityChecks: 3,
45
45
  stabilityDelayMs: 1000,
46
+ minSendIntervalMs: 15000,
46
47
  },
47
48
  }
48
49