zames_pro 2.4.4 → 2.4.6

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.
@@ -6,9 +6,7 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
6
6
  await browser.newChat();
7
7
  transcript?.log('new_chat');
8
8
  }
9
- // Сообщаем вызывающему актуальный chat id. После newChat() URL ещё без id
10
- // (он появляется только после первой отправки), поэтому зовём колбэк и
11
- // здесь, и после первой реальной отправки ниже.
9
+ // Сообщаем вызывающему актуальный chat id.
12
10
  let lastReportedChatId = null;
13
11
  const reportChat = async () => {
14
12
  let id = null;
@@ -43,29 +41,26 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
43
41
  gitContext: gitText,
44
42
  });
45
43
  onThinking();
46
- await browser.ask(systemPrompt, { timeout: 60_000 });
44
+ // system-prompt — отправка агента: с паузой (agent: true).
45
+ await browser.ask(systemPrompt, { timeout: 60_000, agent: true });
47
46
  await reportChat();
48
47
  }
49
48
  let message = task;
50
49
  transcript?.log('task', { task });
51
- // Счётчик «ответ похож на tool-call, но не распознан». Чтобы модель,
52
- // написавшая битый JSON/XML, не останавливала агента молча, мы просим
53
- // её переотправить вызов. Ограничиваем число таких попыток.
54
50
  let malformedRetries = 0;
55
51
  const MAX_MALFORMED_RETRIES = 3;
56
- // Счётчик «пустой/служебный ответ без вызова инструмента». Модель иногда
57
- // отвечает статусом интерфейса (Reading…), тостом лимита или пустой
58
- // строкой. Это НЕ финальный ответ — просим её продолжить, а не молча
59
- // останавливаемся (раньше агент на этом вставал).
60
52
  let stallRetries = 0;
61
53
  const MAX_STALL_RETRIES = 5;
62
54
  for (let i = 0; i < maxIterations; i++) {
63
55
  onThinking();
64
- const rawResponse = await browser.ask(message);
56
+ // Первое сообщение (task) — пользовательский ввод: без паузы.
57
+ // Последующие (tool-result и просьбы переотправить) — агентские:
58
+ // с паузой, чтобы не упираться в лимит частоты.
59
+ const isFirst = i === 0;
60
+ const rawResponse = await browser.ask(message, { agent: !isFirst });
65
61
  await reportChat();
66
62
  transcript?.log('assistant_raw', { response: rawResponse });
67
- // Пользователь прервал генерацию (Esc/Ctrl+C): не считаем это ответом
68
- // модели и не запускаем инструменты — корректно завершаем задачу.
63
+ // Пользователь прервал генерацию (Esc/Ctrl+C).
69
64
  if (/^\s*\(прервано пользователем\)\s*$/.test(rawResponse)) {
70
65
  transcript?.log('user_aborted');
71
66
  return rawResponse;
@@ -78,17 +73,12 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
78
73
  }
79
74
  const parsedCalls = Array.isArray(parsed) ? parsed : parsed ? [parsed] : [];
80
75
  if (parsedCalls.some((p) => p && p._permissive)) {
81
- // Пишем в транскрипт (для отладки), но не сыпем в консоль.
82
76
  transcript?.log('permissive_parse', { response: rawResponse });
83
77
  if (debugLog) {
84
78
  console.error('внимание: tool-call распознан нестрогим парсером');
85
79
  }
86
80
  }
87
81
  if (!parsed) {
88
- // Ответ не распознан как tool-call. Если он ПОХОЖ на попытку вызова
89
- // (есть tool/invoke/parameter, но JSON/XML битый) — это почти всегда
90
- // ошибка формата. Не считаем её финальным ответом (иначе агент молча
91
- // остановится), а просим модель переотправить вызов корректно.
92
82
  const looksLikeToolCall = /("tool"\s*:|\btool_calls?\b|\binvoke\b|\bparameter\b|DSML|function_call)/i.test(rawResponse) || /\{\s*"?(tool|name)"?\s*:/.test(rawResponse);
93
83
  if (looksLikeToolCall && malformedRetries < MAX_MALFORMED_RETRIES) {
94
84
  malformedRetries++;
@@ -110,9 +100,6 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
110
100
  'Например: {"tool": "Read", "args": {"path": "src/index.js"}}';
111
101
  continue;
112
102
  }
113
- // Пустой или служебный ответ (статус интерфейса, тост лимита, пробелы)
114
- // не считаем финальным: просим модель продолжить. Иначе агент
115
- // останавливается, хотя должен был вызвать инструмент.
116
103
  const trimmed = (rawResponse || '').trim();
117
104
  const looksService = !trimmed ||
118
105
  trimmed.length < 2 ||
@@ -272,7 +259,6 @@ function tryParseArray(str) {
272
259
  return null;
273
260
  }
274
261
  }
275
- // Жадный разбор args для грязного JSON (незаэкранированные кавычки в строке).
276
262
  function parseArgsGreedy(str) {
277
263
  const result = {};
278
264
  let i = str.indexOf('{') + 1;
@@ -416,8 +402,6 @@ function parseArgsPermissive(str) {
416
402
  i = j + 1;
417
403
  }
418
404
  else if (str[i] === '{' || str[i] === '[') {
419
- // Вложенный объект/массив: находим сбалансированный фрагмент и
420
- // пытаемся распарсить его как JSON (или как вложенный permissive).
421
405
  const open = str[i];
422
406
  const close = open === '{' ? '}' : ']';
423
407
  const end = findMatching(str, i, open, close);
@@ -522,15 +506,8 @@ function parseToolCallPermissive(text) {
522
506
  const openIdx = text.indexOf('{', argsIdx);
523
507
  if (openIdx === -1)
524
508
  return null;
525
- // Берём сбалансированный по скобкам фрагмент args (с учётом строк),
526
- // а не первый попавшийся '}'. Иначе вложенные объекты/массивы или
527
- // фигурные скобки внутри строк ломают разбор.
528
509
  const endIdx = findMatching(text, openIdx, '{', '}');
529
510
  if (tool === 'Edit') {
530
- // Edit-аргументы часто содержат сырые кавычки и переводы строк, из-за
531
- // чего балансировка скобок сбоит. parseEditArgs рассчитан ровно на
532
- // такой случай, поэтому пробуем его и на «хвосте» до конца текста,
533
- // а не только на сбалансированном фрагменте.
534
511
  const balanced = endIdx === -1 ? null : text.slice(openIdx, endIdx + 1);
535
512
  const tailToEnd = text.slice(openIdx);
536
513
  for (const frag of [balanced, tailToEnd]) {
@@ -541,9 +518,6 @@ function parseToolCallPermissive(text) {
541
518
  return { tool, args: e };
542
519
  }
543
520
  }
544
- // Балансировка скобок сбоит, если в строковых значениях есть сырые
545
- // кавычки/скобки (частый случай для Bash/Write с кодом внутри). Тогда
546
- // пробуем разобрать args из «хвоста» до конца текста и жадным парсером.
547
521
  const frags = [];
548
522
  if (endIdx !== -1)
549
523
  frags.push(text.slice(openIdx, endIdx + 1));
@@ -558,8 +532,6 @@ function parseToolCallPermissive(text) {
558
532
  }
559
533
  return null;
560
534
  }
561
- // Специализированный разбор args для Edit: ровно три поля path/old_string/
562
- // new_string, значения могут содержать сырые кавычки и переводы строк.
563
535
  function parseEditArgs(str) {
564
536
  const keyRe = (name) => new RegExp('"' + name + '"\\s*:\\s*"');
565
537
  const readValue = (name, nextNames) => {
@@ -665,9 +637,6 @@ export function parseToolCall(text) {
665
637
  const second = tryParse(repaired);
666
638
  if (second)
667
639
  return second;
668
- // Сырые переводы строк/табы внутри строковых значений (old_string,
669
- // new_string, content) делают JSON невалидным. Экранируем их и
670
- // пробуем снова, иначе многострочный вызов не распознаётся.
671
640
  const ctrl = repairRawControlChars(raw);
672
641
  const third = tryParse(ctrl);
673
642
  if (third)
@@ -680,22 +649,15 @@ export function parseToolCall(text) {
680
649
  parseToolCallPermissive(repairRawControlChars(cleaned));
681
650
  if (permissive)
682
651
  return { ...permissive, _permissive: true };
683
- // Fallback: модель могла обернуть JSON в прозу и/или добавить мусорные
684
- // теги после него. Отрезаем всё до первого '"tool"' и всё, что похоже
685
- // на XML/DSML-хвост, затем пробуем permissive-разбор ещё раз.
686
652
  const toolIdx = cleaned.search(/["']?tool["']?\s:/);
687
653
  if (toolIdx > 0) {
688
654
  let tail = cleaned.slice(toolIdx);
689
- // Регексп обязан матчить многосимвольные теги (<|DSML|invoke ...>),
690
- // поэтому <[^>]*>, а не <[^>]> — иначе DSML-хвост не срезается,
691
- // permissive-разбор падает и агент молча останавливается.
692
655
  tail = tail.replace(/<[^>]*>.*$/s, '').trim();
693
656
  const tailPermissive = parseToolCallPermissive('{"' + tail) ||
694
657
  parseToolCallPermissive('{"' + repairRawControlChars(tail));
695
658
  if (tailPermissive)
696
659
  return { ...tailPermissive, _permissive: true };
697
660
  }
698
- // Последний fallback: модель ответила XML/DSML-блоком вместо JSON.
699
661
  const xmlCalls = parseXmlToolCalls(cleaned);
700
662
  if (xmlCalls)
701
663
  return Array.isArray(xmlCalls) ? xmlCalls : [xmlCalls];
package/dist/browser.js CHANGED
@@ -39,8 +39,7 @@ const STOP_SELECTORS = [
39
39
  // Служебные статусы интерфейса DeepSeek, которые НЕ являются ответом модели.
40
40
  // Иначе агент принимает статус (Reading...) за ответ и ломает разбор.
41
41
  const STATUS_RE = /^(reading|thinking|searching|analyzing|generating|stop|остановить|читаю|думаю|поиск|анализ)[\s.…]*$/i;
42
- // Ответ DeepSeek при превышении лимита частоты. В этом случае сообщение НЕ
43
- // отправлено — нужно подождать и повторить, а не считать это ответом модели.
42
+ // Ответ DeepSeek при превышении лимита частоты.
44
43
  const RATE_LIMIT_RE = /(messages? too frequent|too many requests|rate limit|слишком часто|повторите позже|try again later)/i;
45
44
  export function isRateLimitText(text) {
46
45
  return RATE_LIMIT_RE.test(String(text || ''));
@@ -113,7 +112,7 @@ export class DeepSeekBrowser {
113
112
  _netSniff;
114
113
  _netSniffLimit;
115
114
  _netHookInstalled;
116
- constructor({ headless = false, debug = false, channel = 'chrome', answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 3, stabilityDelayMs = 1000, minSendIntervalMs = 2000, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, } = {}) {
115
+ constructor({ headless = false, debug = false, channel = 'chrome', answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 3, stabilityDelayMs = 1000, minSendIntervalMs = 15000, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, } = {}) {
117
116
  this.headless = headless;
118
117
  this.debug = debug;
119
118
  this.channel = channel;
@@ -305,11 +304,31 @@ export class DeepSeekBrowser {
305
304
  return out.replace(new RegExp(NL + '{3,}', 'g'), NL + NL).trim();
306
305
  }, ANSWER_SELECTORS);
307
306
  }
308
- // Полный видимый текст страницы — по нему ловим тосты/ошибки интерфейса
309
- // DeepSeek (в частности «Messages too frequent. Try again later.»).
307
+ // Читаем ТОЛЬКО видимые тосты/уведомления/ошибки, а не весь текст
308
+ // страницы. Иначе ловим «try again later» из служебных/скрытых блоков
309
+ // и уходим в ложное ожидание лимита на 5 минут.
310
310
  async _readPageText() {
311
311
  return await this.page
312
- .evaluate(() => document.body.innerText || '')
312
+ .evaluate(() => {
313
+ const sels = [
314
+ '[role="alert"]',
315
+ '[class*="toast" i]',
316
+ '[class*="notification" i]',
317
+ '[class*="alert" i]',
318
+ '[class*="error" i]',
319
+ ];
320
+ let out = '';
321
+ for (const s of sels) {
322
+ for (const el of Array.from(document.querySelectorAll(s))) {
323
+ const e = el;
324
+ const st = getComputedStyle(e);
325
+ if (st.display === 'none' || st.visibility === 'hidden')
326
+ continue;
327
+ out += ' ' + (e.innerText || '');
328
+ }
329
+ }
330
+ return out;
331
+ })
313
332
  .catch(() => '');
314
333
  }
315
334
  async _readLastAnswerTextClean() {
@@ -323,10 +342,7 @@ export class DeepSeekBrowser {
323
342
  }
324
343
  // Найти кнопку Stop в интерфейсе DeepSeek. Полагаться только на класс
325
344
  // нельзя: во время генерации кнопка отправки (та же circle-кнопка)
326
- // меняет иконку на «квадрат» (stop), сохраняя классы. Поэтому:
327
- // 1) проверяем явные селекторы (aria-label/текст Stop);
328
- // 2) ищем primary circle-кнопку и смотрим на её иконку: rect/квадрат =
329
- // stop, path/стрелка = send.
345
+ // меняет иконку на «квадрат» (stop), сохраняя классы.
330
346
  async _stopButtonVisible() {
331
347
  const explicit = await this._findVisible(STOP_SELECTORS, 250);
332
348
  if (explicit)
@@ -360,7 +376,6 @@ export class DeepSeekBrowser {
360
376
  async stopGeneration() {
361
377
  this._abort = true;
362
378
  this._stopped = true;
363
- // 1) Пробуем явные селекторы Stop.
364
379
  const btn = await this._findVisible(STOP_SELECTORS, 500);
365
380
  if (btn) {
366
381
  try {
@@ -369,7 +384,6 @@ export class DeepSeekBrowser {
369
384
  }
370
385
  catch { }
371
386
  }
372
- // 2) Кликаем по primary circle-кнопке, если у неё иконка-квадрат.
373
387
  const clicked = await this.page
374
388
  .evaluate(() => {
375
389
  const btns = Array.from(document.querySelectorAll('div[role="button"], button'));
@@ -388,7 +402,6 @@ export class DeepSeekBrowser {
388
402
  .catch(() => false);
389
403
  if (clicked)
390
404
  return true;
391
- // 3) Крайний случай — нажать Esc в поле ввода (иногда останавливает).
392
405
  try {
393
406
  await this.page.keyboard.press('Escape');
394
407
  return true;
@@ -396,22 +409,21 @@ export class DeepSeekBrowser {
396
409
  catch { }
397
410
  return false;
398
411
  }
399
- async ask(prompt, { timeout = this.answerTimeoutMs } = {}) {
412
+ async ask(prompt, { timeout = this.answerTimeoutMs, agent = false, } = {}) {
400
413
  let lastErr = null;
401
414
  let attempt = 0;
402
415
  let rateLimitRetries = 0;
403
416
  while (attempt < this.askRetries) {
404
417
  attempt++;
405
418
  try {
406
- return await this._askOnce(prompt, { timeout });
419
+ return await this._askOnce(prompt, { timeout, agent });
407
420
  }
408
421
  catch (e) {
409
422
  lastErr = e;
410
- // Лимит частоты: DeepSeek не принял сообщение. Ждём долго —
411
- // лимиты сбрасываются за минуты, — и повторяем отправку сами.
412
- // Эти паузы НЕ расходуют обычные попытки ask().
423
+ // Лимит частоты: DeepSeek не принял сообщение. Ждём долго и
424
+ // повторяем отправку в ТОТ ЖЕ чат (без newChat — иначе теряется
425
+ // контекст). Паузы не расходуют обычные попытки ask().
413
426
  if (e instanceof RateLimitError) {
414
- attempt--;
415
427
  rateLimitRetries++;
416
428
  if (rateLimitRetries > this.maxRateLimitRetries) {
417
429
  console.error(theme.error(`✖ DeepSeek не принял сообщение после ${rateLimitRetries} пауз по ${Math.ceil(this.rateLimitWaitMs / 60000)} мин.`));
@@ -419,7 +431,6 @@ export class DeepSeekBrowser {
419
431
  }
420
432
  console.error(theme.warn(`⏳ DeepSeek: «слишком часто». Жду ${Math.ceil(this.rateLimitWaitMs / 60000)} мин (${rateLimitRetries}/${this.maxRateLimitRetries}) и повторю...`));
421
433
  await this.page.waitForTimeout(this.rateLimitWaitMs);
422
- await this.newChat().catch(() => { });
423
434
  continue;
424
435
  }
425
436
  console.error(`\n⚠ ask() попытка ${attempt}/${this.askRetries} провалилась: ${e.message}`);
@@ -484,11 +495,12 @@ export class DeepSeekBrowser {
484
495
  await this.page.keyboard.insertText(text);
485
496
  }
486
497
  }
487
- // Небольшая пауза между отправками, чтобы случайно не отправить два
488
- // сообщения подряд. Основная защита от лимита частоты — обработка
489
- // RateLimitError в ask() (ждём rateLimitWaitMs и повторяем), поэтому
490
- // здесь интервал маленький и не мешает живому вводу.
491
- async _waitForSendSlot() {
498
+ // Пауза между отправками. Применяется ТОЛЬКО к сообщениям агента
499
+ // (tool-result, system-prompt), чтобы не упираться в лимит частоты.
500
+ // Пользовательский ввод отправляется без задержки.
501
+ async _waitForSendSlot(agent) {
502
+ if (!agent)
503
+ return;
492
504
  if (!this._lastSentAt)
493
505
  return;
494
506
  const gap = this.minSendIntervalMs - (Date.now() - this._lastSentAt);
@@ -497,17 +509,15 @@ export class DeepSeekBrowser {
497
509
  console.error(theme.warn(`⏳ пауза ${Math.ceil(gap / 1000)}с перед отправкой...`));
498
510
  await this.page.waitForTimeout(gap);
499
511
  }
500
- async _askOnce(prompt, { timeout }) {
501
- // Сбрасываем флаг прерывания ТОЛЬКО в самом начале отправки. Раньше он
502
- // сбрасывался перед циклом ожидания — и Esc/Ctrl+C, нажатые во время
503
- // старта генерации, терялись, запрос не останавливался.
512
+ async _askOnce(prompt, { timeout, agent }) {
513
+ // Сбрасываем флаг прерывания ТОЛЬКО в самом начале отправки.
504
514
  this._abort = false;
505
515
  const input = await this._findVisible(INPUT_SELECTORS, 10_000);
506
516
  if (!input) {
507
517
  throw new Error('Не найдено поле ввода. Запустите /debug-dom и поправьте INPUT_SELECTORS.');
508
518
  }
509
519
  const beforeText = await this._readLastAnswerTextClean().catch(() => '');
510
- await this._waitForSendSlot();
520
+ await this._waitForSendSlot(agent);
511
521
  this._netCapture = '';
512
522
  this._netCaptureAt = 0;
513
523
  await this._setInputText(input, prompt);
@@ -532,15 +542,13 @@ export class DeepSeekBrowser {
532
542
  this._lastSentAt = Date.now();
533
543
  // Ждём старта: либо появился Stop, либо изменился текст ответа,
534
544
  // либо вырос общий объём текста на странице. Параллельно ловим
535
- // тост о превышении лимита частоты — это НЕ ответ модели, а отказ
536
- // приёма сообщения, и его надо обработать паузой и повтором.
545
+ // тост о превышении лимита частоты (только тосты, не весь body).
537
546
  const startDeadline = Date.now() + 15_000;
538
547
  const startBodyLen = await this.page
539
548
  .evaluate(() => document.body.innerText.length)
540
549
  .catch(() => 0);
541
550
  let started = false;
542
551
  while (Date.now() < startDeadline) {
543
- // Прервали (Esc/Ctrl+C) ещё до старта генерации — выходим сразу.
544
552
  if (this._abort)
545
553
  return '(прервано пользователем)';
546
554
  const pageText = await this._readPageText();
@@ -559,25 +567,31 @@ export class DeepSeekBrowser {
559
567
  await this.page.waitForTimeout(300);
560
568
  }
561
569
  if (!started) {
562
- // Перед ошибкой ещё раз проверим на лимит частоты: иногда тост
563
- // появляется с задержкой и не успевает попасть в ранние проверки.
564
- const pageText = await this._readPageText();
565
- if (isRateLimitText(pageText)) {
566
- throw new RateLimitError(pageText.slice(0, 300));
567
- }
570
+ // Больше НЕ проверяем лимит по всему тексту страницы — это давало
571
+ // ложные срабатывания и 5-минутные паузы. Просто сообщаем, что
572
+ // генерация не началась.
568
573
  throw new Error('Ответ не начал генерироваться за 15с. Возможно, сообщение не отправилось.');
569
574
  }
570
575
  // Ждём, пока ответ перестанет меняться. Условие "не генерируется"
571
- // проверяем через рост текста, а НЕ через _isGenerating(): кнопка Stop
572
- // у DeepSeek ненадёжна, и если она не находится, ответ бы никогда не
573
- // вернулся.
576
+ // проверяем через рост текста, а НЕ через _isGenerating().
577
+ // Параллельно ловим тост лимита частоты, если он всплывёт во время
578
+ // генерации (читаем только тосты, ложных срабатываний нет).
574
579
  const deadline = Date.now() + timeout;
575
580
  let last = '';
576
581
  let stable = 0;
582
+ let tick = 0;
577
583
  while (Date.now() < deadline) {
578
584
  if (this._abort) {
579
585
  return last || '(прервано пользователем)';
580
586
  }
587
+ // Тост лимита проверяем не каждый тик, а раз в ~5 тиков, чтобы не
588
+ // дёргать DOM лишний раз.
589
+ if (tick++ % 5 === 0) {
590
+ const pageText = await this._readPageText();
591
+ if (isRateLimitText(pageText)) {
592
+ throw new RateLimitError(pageText.slice(0, 300));
593
+ }
594
+ }
581
595
  const cur = await this._readLastAnswerTextClean().catch(() => '');
582
596
  if (cur && cur === last) {
583
597
  stable++;
package/dist/config.js CHANGED
@@ -37,7 +37,7 @@ export const DEFAULTS = {
37
37
  askRetries: 3,
38
38
  stabilityChecks: 3,
39
39
  stabilityDelayMs: 1000,
40
- minSendIntervalMs: 2000,
40
+ minSendIntervalMs: 15000,
41
41
  rateLimitWaitMs: 300000,
42
42
  maxRateLimitRetries: 6,
43
43
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.4.4",
3
+ "version": "2.4.6",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {