zames_pro 2.5.0 → 2.5.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.
@@ -51,6 +51,13 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
51
51
  const MAX_MALFORMED_RETRIES = 3;
52
52
  let stallRetries = 0;
53
53
  const MAX_STALL_RETRIES = 5;
54
+ // Защита от «агент встал»: DeepSeek иногда присылает финальный текст,
55
+ // который лишь ОПИСЫВАЕТ следующий вызов инструмента (или рвёт ответ на
56
+ // полуслове), и агент молча завершает задачу, хотя работа не сделана.
57
+ // Если финальный ответ похож на «сейчас вызову …» — переспрашиваем, а не
58
+ // останавливаемся. Счётчик общий, чтобы не зациклиться на болтливой модели.
59
+ let looksDoneRetries = 0;
60
+ const MAX_LOOKSDONE_RETRIES = 3;
54
61
  for (let i = 0; i < maxIterations; i++) {
55
62
  onThinking();
56
63
  // Первое сообщение (task) — пользовательский ввод: без паузы.
@@ -108,10 +115,16 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
108
115
  continue;
109
116
  }
110
117
  const trimmed = (rawResponse || '').trim();
118
+ // Служебный ответ — это КОРОТКАЯ заглушка DeepSeek («Reading…») или
119
+ // короткое уведомление о лимите. Слова про rate limit в ДЛИННОМ
120
+ // ответе — это, как правило, сам агент цитирует код/логи (в транскрипте
121
+ // был ровно такой случай: ответ на 1365 символов про ask() и лимиты),
122
+ // и принимать его за «служебный» нельзя, иначе агент зря переспрашивает.
111
123
  const looksService = !trimmed ||
112
124
  trimmed.length < 2 ||
113
125
  /^(reading|thinking|searching|analyzing|generating|stop|остановить|читаю|думаю|поиск|анализ)[\s.…]*$/i.test(trimmed) ||
114
- /(messages? too frequent|too many requests|rate limit|слишком часто|try again later)/i.test(trimmed);
126
+ (trimmed.length <= 200 &&
127
+ /(messages? too frequent|too many requests|rate limit|слишком часто|try again later)/i.test(trimmed));
115
128
  if (looksService && stallRetries < MAX_STALL_RETRIES) {
116
129
  stallRetries++;
117
130
  transcript?.log('stall_retry', {
@@ -131,6 +144,32 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
131
144
  'Если задача выполнена — вызови инструмент respond с итоговым сообщением.';
132
145
  continue;
133
146
  }
147
+ // Ответ похож на «сейчас вызову инструмент», но вызова в нём нет.
148
+ // DeepSeek иногда так обрывает ход: пишет «Now update README…» или
149
+ // «Let me run the tests…» и замолкает. Если принять это за финал,
150
+ // агент встаёт, не сделав работу. Просим продолжить и на этот раз
151
+ // обязательно вызвать инструмент (или respond, если правда готово).
152
+ if (looksLikeUnfinishedWork(trimmed) && looksDoneRetries < MAX_LOOKSDONE_RETRIES) {
153
+ looksDoneRetries++;
154
+ transcript?.log('unfinished_retry', {
155
+ attempt: looksDoneRetries,
156
+ response: rawResponse,
157
+ });
158
+ if (debugLog) {
159
+ console.error('внимание: ответ похож на незавершённую работу, прошу продолжить (попытка ' +
160
+ looksDoneRetries +
161
+ '/' +
162
+ MAX_LOOKSDONE_RETRIES +
163
+ ')');
164
+ }
165
+ message =
166
+ 'Похоже, ты собирался вызвать инструмент, но не вызвал. ' +
167
+ 'Если задача ещё не выполнена — ответь РОВНО одним JSON-объектом ' +
168
+ 'вызова инструмента, без текста до и после. ' +
169
+ 'Если задача действительно выполнена — вызови respond с итоговым ' +
170
+ 'сообщением оператору.';
171
+ continue;
172
+ }
134
173
  onAssistantMessage(rawResponse);
135
174
  transcript?.log('assistant_final', { message: rawResponse });
136
175
  return rawResponse;
@@ -206,6 +245,27 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
206
245
  }
207
246
  return 'Достигнут лимит итераций.';
208
247
  }
248
+ // Текст, который обещает вызов инструмента в будущем времени, но самого
249
+ // вызова не содержит. DeepSeek регулярно так «зависает»: пишет
250
+ // «Now update README to mention …», «Let me run the tests», «Сейчас проверю»
251
+ // и останавливается. Такие ответы нельзя принимать за финальные — иначе
252
+ // агент встаёт, не выполнив работу. Держим эвристику узкой (будущее время /
253
+ // намерение), чтобы не ловить обычные отчёты о выполненной работе.
254
+ function looksLikeUnfinishedWork(text) {
255
+ const t = (text || '').trim();
256
+ if (!t)
257
+ return false;
258
+ // Длинные ответы (отчёты) не трогаем — там может быть что угодно.
259
+ if (t.length > 600)
260
+ return false;
261
+ // Уже есть финальный маркер — считаем ответ завершённым.
262
+ if (/\b(done|finished|completed|готово|выполнено|завершено)\b/i.test(t)) {
263
+ return false;
264
+ }
265
+ const en = /\b(now|next|then|let me|let's|i will|i'll|i am going to|i'm going to|going to|about to|will now|time to)\b[^.!?\n]{0,120}\b(update|write|edit|read|run|check|add|fix|create|remove|delete|apply|test|commit|push|install|open|search|look|verify|change|modify|implement|review)\b/i;
266
+ const ru = /(^|[^а-яё])(проверю|обновлю|исправлю|добавлю|запущу|выполню|посмотрю|прочитаю|изменю|попробую|сделаю)([^а-яё]|$)/i;
267
+ return en.test(t) || ru.test(t);
268
+ }
209
269
  // ============ JSON parsing ============
210
270
  function repairRawControlChars(str) {
211
271
  let out = '';
@@ -13,11 +13,37 @@ function readAttr(attrs, name) {
13
13
  const m = attrs.match(re);
14
14
  return m ? m[2] : null;
15
15
  }
16
+ // Достаёт параметры из тела <invoke>. Тег открытия и закрытия может нести
17
+ // произвольный префикс (DSML и т.п.), а некоторые модели путают пары тегов
18
+ // (<parameter …> … </|DSML| parameter>). Регулярка ниже матчит любую пару
19
+ // <…parameter…> … </…parameter…>, поэтому такие «смешанные» теги тоже
20
+ // разбираются. Значения дедуплицируются: если модель повторила parameter
21
+ // несколько раз, берём ПЕРВОЕ значение, а не последнее (повторы обычно
22
+ // дублируют корректный параметр).
23
+ function parseParameters(body) {
24
+ const args = {};
25
+ const paramRe = /<[^>]*?parameter([^>]*)>([\s\S]*?)<\/[^>]*?parameter[^>]*>/gi;
26
+ let p;
27
+ while ((p = paramRe.exec(body)) !== null) {
28
+ const pname = readAttr(p[1] || '', 'name');
29
+ if (!pname)
30
+ continue;
31
+ const isString = /string[ ]*=[ ]*"?true"?/i.test(p[1] || '');
32
+ let value = unescapeXml(p[2]);
33
+ if (!isString) {
34
+ try {
35
+ value = JSON.parse(value);
36
+ }
37
+ catch { }
38
+ }
39
+ if (!(pname in args))
40
+ args[pname] = value;
41
+ }
42
+ return args;
43
+ }
16
44
  export function parseXmlToolCalls(text) {
17
45
  if (!text || typeof text !== 'string')
18
46
  return null;
19
- if (!/<[^>]*invoke/i.test(text))
20
- return null;
21
47
  const invokeRe = /<[^>]*?invoke([^>]*)>/gi;
22
48
  const calls = [];
23
49
  let m;
@@ -30,23 +56,7 @@ export function parseXmlToolCalls(text) {
30
56
  const closeRe = /<\/[^>]*?invoke[^>]*>/i;
31
57
  const closeMatch = closeRe.exec(rest);
32
58
  const body = closeMatch ? rest.slice(0, closeMatch.index) : rest;
33
- const args = {};
34
- const paramRe = /<[^>]*?parameter([^>]*)>([\s\S]*?)<\/[^>]*?parameter[^>]*>/gi;
35
- let p;
36
- while ((p = paramRe.exec(body)) !== null) {
37
- const pname = readAttr(p[1] || '', 'name');
38
- if (!pname)
39
- continue;
40
- const isString = /string[ ]*=[ ]*"?true"?/i.test(p[1] || '');
41
- let value = unescapeXml(p[2]);
42
- if (!isString) {
43
- try {
44
- value = JSON.parse(value);
45
- }
46
- catch { }
47
- }
48
- args[pname] = value;
49
- }
59
+ const args = parseParameters(body);
50
60
  // Некоторые модели кладут весь JSON-объект аргументов в один
51
61
  // parameter с именем args. Разворачиваем, чтобы не было
52
62
  // {args: {args: {...}}}.
@@ -61,7 +71,12 @@ export function parseXmlToolCalls(text) {
61
71
  }
62
72
  calls.push({ tool: name, args: finalArgs });
63
73
  }
64
- if (!calls.length)
65
- return null;
66
- return calls.length === 1 ? calls[0] : calls;
74
+ if (calls.length)
75
+ return calls.length === 1 ? calls[0] : calls;
76
+ // Модель иногда опускает тег <invoke> и оставляет только DSML-обёртку
77
+ // (<|DSML| calls> … <|DSML| parameter name=…>…). Имя инструмента в таком
78
+ // ответе отсутствует, поэтому восстановить вызов нельзя — но и молча
79
+ // принимать его за финальный ответ нельзя: agent-loop попросит повторить
80
+ // (looksLikeToolCall срабатывает по слову parameter/DSML).
81
+ return null;
67
82
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.5.0",
3
+ "version": "2.5.1",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {