zames_pro 2.4.3 → 2.4.5

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 || ''));
@@ -101,6 +100,10 @@ export class DeepSeekBrowser {
101
100
  maxRateLimitRetries;
102
101
  _lastSentAt;
103
102
  _abort;
103
+ // Пользователь нажал Esc/Ctrl+C — «стоп» для ВСЕЙ текущей пачки задач
104
+ // (включая очередь). В отличие от _abort (сбрасывается на каждую
105
+ // отправку), этот флаг живёт до явного запуска новой задачи с промпта.
106
+ _stopped;
104
107
  context;
105
108
  page;
106
109
  _netCapture;
@@ -122,6 +125,7 @@ export class DeepSeekBrowser {
122
125
  this.maxRateLimitRetries = maxRateLimitRetries;
123
126
  this._lastSentAt = 0;
124
127
  this._abort = false;
128
+ this._stopped = false;
125
129
  this._netCapture = '';
126
130
  this._netCaptureAt = 0;
127
131
  this._netChatId = null;
@@ -300,11 +304,31 @@ export class DeepSeekBrowser {
300
304
  return out.replace(new RegExp(NL + '{3,}', 'g'), NL + NL).trim();
301
305
  }, ANSWER_SELECTORS);
302
306
  }
303
- // Полный видимый текст страницы — по нему ловим тосты/ошибки интерфейса
304
- // DeepSeek (в частности «Messages too frequent. Try again later.»).
307
+ // Читаем ТОЛЬКО видимые тосты/уведомления/ошибки, а не весь текст
308
+ // страницы. Иначе ловим «try again later» из служебных/скрытых блоков
309
+ // и уходим в ложное ожидание лимита на 5 минут.
305
310
  async _readPageText() {
306
311
  return await this.page
307
- .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
+ })
308
332
  .catch(() => '');
309
333
  }
310
334
  async _readLastAnswerTextClean() {
@@ -318,10 +342,7 @@ export class DeepSeekBrowser {
318
342
  }
319
343
  // Найти кнопку Stop в интерфейсе DeepSeek. Полагаться только на класс
320
344
  // нельзя: во время генерации кнопка отправки (та же circle-кнопка)
321
- // меняет иконку на «квадрат» (stop), сохраняя классы. Поэтому:
322
- // 1) проверяем явные селекторы (aria-label/текст Stop);
323
- // 2) ищем primary circle-кнопку и смотрим на её иконку: rect/квадрат =
324
- // stop, path/стрелка = send.
345
+ // меняет иконку на «квадрат» (stop), сохраняя классы.
325
346
  async _stopButtonVisible() {
326
347
  const explicit = await this._findVisible(STOP_SELECTORS, 250);
327
348
  if (explicit)
@@ -354,7 +375,7 @@ export class DeepSeekBrowser {
354
375
  }
355
376
  async stopGeneration() {
356
377
  this._abort = true;
357
- // 1) Пробуем явные селекторы Stop.
378
+ this._stopped = true;
358
379
  const btn = await this._findVisible(STOP_SELECTORS, 500);
359
380
  if (btn) {
360
381
  try {
@@ -363,7 +384,6 @@ export class DeepSeekBrowser {
363
384
  }
364
385
  catch { }
365
386
  }
366
- // 2) Кликаем по primary circle-кнопке, если у неё иконка-квадрат.
367
387
  const clicked = await this.page
368
388
  .evaluate(() => {
369
389
  const btns = Array.from(document.querySelectorAll('div[role="button"], button'));
@@ -382,7 +402,6 @@ export class DeepSeekBrowser {
382
402
  .catch(() => false);
383
403
  if (clicked)
384
404
  return true;
385
- // 3) Крайний случай — нажать Esc в поле ввода (иногда останавливает).
386
405
  try {
387
406
  await this.page.keyboard.press('Escape');
388
407
  return true;
@@ -390,22 +409,21 @@ export class DeepSeekBrowser {
390
409
  catch { }
391
410
  return false;
392
411
  }
393
- async ask(prompt, { timeout = this.answerTimeoutMs } = {}) {
412
+ async ask(prompt, { timeout = this.answerTimeoutMs, agent = false, } = {}) {
394
413
  let lastErr = null;
395
414
  let attempt = 0;
396
415
  let rateLimitRetries = 0;
397
416
  while (attempt < this.askRetries) {
398
417
  attempt++;
399
418
  try {
400
- return await this._askOnce(prompt, { timeout });
419
+ return await this._askOnce(prompt, { timeout, agent });
401
420
  }
402
421
  catch (e) {
403
422
  lastErr = e;
404
- // Лимит частоты: DeepSeek не принял сообщение. Ждём долго —
405
- // лимиты сбрасываются за минуты, — и повторяем отправку сами.
406
- // Эти паузы НЕ расходуют обычные попытки ask().
423
+ // Лимит частоты: DeepSeek не принял сообщение. Ждём долго и
424
+ // повторяем отправку в ТОТ ЖЕ чат (без newChat — иначе теряется
425
+ // контекст). Паузы не расходуют обычные попытки ask().
407
426
  if (e instanceof RateLimitError) {
408
- attempt--;
409
427
  rateLimitRetries++;
410
428
  if (rateLimitRetries > this.maxRateLimitRetries) {
411
429
  console.error(theme.error(`✖ DeepSeek не принял сообщение после ${rateLimitRetries} пауз по ${Math.ceil(this.rateLimitWaitMs / 60000)} мин.`));
@@ -413,7 +431,6 @@ export class DeepSeekBrowser {
413
431
  }
414
432
  console.error(theme.warn(`⏳ DeepSeek: «слишком часто». Жду ${Math.ceil(this.rateLimitWaitMs / 60000)} мин (${rateLimitRetries}/${this.maxRateLimitRetries}) и повторю...`));
415
433
  await this.page.waitForTimeout(this.rateLimitWaitMs);
416
- await this.newChat().catch(() => { });
417
434
  continue;
418
435
  }
419
436
  console.error(`\n⚠ ask() попытка ${attempt}/${this.askRetries} провалилась: ${e.message}`);
@@ -478,26 +495,29 @@ export class DeepSeekBrowser {
478
495
  await this.page.keyboard.insertText(text);
479
496
  }
480
497
  }
481
- async _waitForSendSlot() {
498
+ // Пауза между отправками. Применяется ТОЛЬКО к сообщениям агента
499
+ // (tool-result, system-prompt), чтобы не упираться в лимит частоты.
500
+ // Пользовательский ввод отправляется без задержки.
501
+ async _waitForSendSlot(agent) {
502
+ if (!agent)
503
+ return;
482
504
  if (!this._lastSentAt)
483
505
  return;
484
506
  const gap = this.minSendIntervalMs - (Date.now() - this._lastSentAt);
485
507
  if (gap <= 0)
486
508
  return;
487
- console.error(theme.warn(`⏳ пауза ${Math.ceil(gap / 1000)}с перед отправкой (лимит частоты DeepSeek)...`));
509
+ console.error(theme.warn(`⏳ пауза ${Math.ceil(gap / 1000)}с перед отправкой...`));
488
510
  await this.page.waitForTimeout(gap);
489
511
  }
490
- async _askOnce(prompt, { timeout }) {
491
- // Сбрасываем флаг прерывания ТОЛЬКО в самом начале отправки. Раньше он
492
- // сбрасывался перед циклом ожидания — и Esc/Ctrl+C, нажатые во время
493
- // старта генерации, терялись, запрос не останавливался.
512
+ async _askOnce(prompt, { timeout, agent }) {
513
+ // Сбрасываем флаг прерывания ТОЛЬКО в самом начале отправки.
494
514
  this._abort = false;
495
515
  const input = await this._findVisible(INPUT_SELECTORS, 10_000);
496
516
  if (!input) {
497
517
  throw new Error('Не найдено поле ввода. Запустите /debug-dom и поправьте INPUT_SELECTORS.');
498
518
  }
499
519
  const beforeText = await this._readLastAnswerTextClean().catch(() => '');
500
- await this._waitForSendSlot();
520
+ await this._waitForSendSlot(agent);
501
521
  this._netCapture = '';
502
522
  this._netCaptureAt = 0;
503
523
  await this._setInputText(input, prompt);
@@ -522,15 +542,13 @@ export class DeepSeekBrowser {
522
542
  this._lastSentAt = Date.now();
523
543
  // Ждём старта: либо появился Stop, либо изменился текст ответа,
524
544
  // либо вырос общий объём текста на странице. Параллельно ловим
525
- // тост о превышении лимита частоты — это НЕ ответ модели, а отказ
526
- // приёма сообщения, и его надо обработать паузой и повтором.
545
+ // тост о превышении лимита частоты (только тосты, не весь body).
527
546
  const startDeadline = Date.now() + 15_000;
528
547
  const startBodyLen = await this.page
529
548
  .evaluate(() => document.body.innerText.length)
530
549
  .catch(() => 0);
531
550
  let started = false;
532
551
  while (Date.now() < startDeadline) {
533
- // Прервали (Esc/Ctrl+C) ещё до старта генерации — выходим сразу.
534
552
  if (this._abort)
535
553
  return '(прервано пользователем)';
536
554
  const pageText = await this._readPageText();
@@ -549,18 +567,13 @@ export class DeepSeekBrowser {
549
567
  await this.page.waitForTimeout(300);
550
568
  }
551
569
  if (!started) {
552
- // Перед ошибкой ещё раз проверим на лимит частоты: иногда тост
553
- // появляется с задержкой и не успевает попасть в ранние проверки.
554
- const pageText = await this._readPageText();
555
- if (isRateLimitText(pageText)) {
556
- throw new RateLimitError(pageText.slice(0, 300));
557
- }
570
+ // Больше НЕ проверяем лимит по всему тексту страницы — это давало
571
+ // ложные срабатывания и 5-минутные паузы. Просто сообщаем, что
572
+ // генерация не началась.
558
573
  throw new Error('Ответ не начал генерироваться за 15с. Возможно, сообщение не отправилось.');
559
574
  }
560
575
  // Ждём, пока ответ перестанет меняться. Условие "не генерируется"
561
- // проверяем через рост текста, а НЕ через _isGenerating(): кнопка Stop
562
- // у DeepSeek ненадёжна, и если она не находится, ответ бы никогда не
563
- // вернулся.
576
+ // проверяем через рост текста, а НЕ через _isGenerating().
564
577
  const deadline = Date.now() + timeout;
565
578
  let last = '';
566
579
  let stable = 0;
package/dist/index.js CHANGED
@@ -626,6 +626,9 @@ async function runTask(browser, tools, taskText, workdir, opts) {
626
626
  // В TTY-режиме UI — это LineEditor: он владеет вводом (очередь, Esc,
627
627
  // Ctrl+C) и рисует статус НАД постоянной строкой ввода. В не-TTY режиме
628
628
  // (пайпы) — обычный спиннер + watchInput.
629
+ // Новая задача с промпта — сбрасываем «стоп» от прошлого прерывания.
630
+ browser._stopped = false;
631
+ browser._abort = false;
629
632
  const ui = editor || mod.createSpinner();
630
633
  const stopWatching = editor
631
634
  ? () => { }
@@ -667,6 +670,12 @@ async function runTask(browser, tools, taskText, workdir, opts) {
667
670
  onChatReady,
668
671
  debugLog: debug,
669
672
  });
673
+ // Прервали (Esc/Ctrl+C) — не запускаем следующие задачи из очереди
674
+ // и очищаем её, чтобы «стоп» действительно останавливал всё.
675
+ if (browser._stopped) {
676
+ queue.length = 0;
677
+ break;
678
+ }
670
679
  if (!queue.length)
671
680
  break;
672
681
  const queued = queue.shift() ?? '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.4.3",
3
+ "version": "2.4.5",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {