zames_pro 2.4.7 → 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.
package/README.md CHANGED
@@ -9,14 +9,21 @@ directory, reads and edits files, runs commands, and commits to git.
9
9
  ## Requirements
10
10
 
11
11
  - Node.js >= 18
12
- - Google Chrome or Chromium (used through Playwright)
13
12
  - A DeepSeek account (you log in manually in the browser window that opens on first launch)
14
13
 
15
14
  ## Installation
16
15
 
17
16
  ```bash
18
17
  npm install -g zames_pro
18
+ ```
19
+
20
+ Chromium for Playwright is downloaded automatically on install. On Linux/WSL
21
+ the required system libraries are installed too when passwordless `sudo` is
22
+ available; otherwise run once by hand:
23
+
24
+ ```bash
19
25
  npx playwright install chromium
26
+ sudo npx playwright install-deps chromium
20
27
  ```
21
28
 
22
29
  ## Usage
@@ -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) — пользовательский ввод: без паузы.
@@ -79,7 +86,14 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
79
86
  }
80
87
  }
81
88
  if (!parsed) {
82
- const looksLikeToolCall = /("tool"\s*:|\btool_calls?\b|\binvoke\b|\bparameter\b|DSML|function_call)/i.test(rawResponse) || /\{\s*"?(tool|name)"?\s*:/.test(rawResponse);
89
+ // Ответ похож на (возможно, обрезанный) вызов инструмента. Ловим не
90
+ // только явный JSON, но и XML/DSML-формы, «грязные» варианты и
91
+ // незакрытые фрагменты: если такой ответ молча принять за финальный,
92
+ // агент встанет, хотя модель пыталась позвать инструмент.
93
+ const looksLikeToolCall = /("tool"\s*:|\btool_calls?\b|\binvoke\b|\bparameter\b|DSML|function_call)/i.test(rawResponse) ||
94
+ /\{\s*"?(tool|name|args)"?\s*:/.test(rawResponse) ||
95
+ /<\s*\|?\s*(DSML|invoke|parameter)/i.test(rawResponse) ||
96
+ /^\s*\[?\s*\{[^}]*$/.test(rawResponse.trim());
83
97
  if (looksLikeToolCall && malformedRetries < MAX_MALFORMED_RETRIES) {
84
98
  malformedRetries++;
85
99
  transcript?.log('malformed_toolcall', {
@@ -101,10 +115,16 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
101
115
  continue;
102
116
  }
103
117
  const trimmed = (rawResponse || '').trim();
118
+ // Служебный ответ — это КОРОТКАЯ заглушка DeepSeek («Reading…») или
119
+ // короткое уведомление о лимите. Слова про rate limit в ДЛИННОМ
120
+ // ответе — это, как правило, сам агент цитирует код/логи (в транскрипте
121
+ // был ровно такой случай: ответ на 1365 символов про ask() и лимиты),
122
+ // и принимать его за «служебный» нельзя, иначе агент зря переспрашивает.
104
123
  const looksService = !trimmed ||
105
124
  trimmed.length < 2 ||
106
125
  /^(reading|thinking|searching|analyzing|generating|stop|остановить|читаю|думаю|поиск|анализ)[\s.…]*$/i.test(trimmed) ||
107
- /(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));
108
128
  if (looksService && stallRetries < MAX_STALL_RETRIES) {
109
129
  stallRetries++;
110
130
  transcript?.log('stall_retry', {
@@ -124,6 +144,32 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
124
144
  'Если задача выполнена — вызови инструмент respond с итоговым сообщением.';
125
145
  continue;
126
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
+ }
127
173
  onAssistantMessage(rawResponse);
128
174
  transcript?.log('assistant_final', { message: rawResponse });
129
175
  return rawResponse;
@@ -134,6 +180,25 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
134
180
  const msg = typeof respondCall.args.message === 'string'
135
181
  ? respondCall.args.message
136
182
  : String(respondCall.args.message ?? '');
183
+ // Пустой respond — не финал: модель позвала respond, но не написала
184
+ // итог. Если так завершить, оператор не увидит ничего, а задача
185
+ // «зависнет». Просим продолжить (в пределах stallRetries).
186
+ if (!msg.trim() && stallRetries < MAX_STALL_RETRIES) {
187
+ stallRetries++;
188
+ transcript?.log('empty_respond', { attempt: stallRetries });
189
+ if (debugLog) {
190
+ console.error('внимание: пустой respond, прошу продолжить (попытка ' +
191
+ stallRetries +
192
+ '/' +
193
+ MAX_STALL_RETRIES +
194
+ ')');
195
+ }
196
+ message =
197
+ 'Ты вызвал respond с пустым message. Если задача выполнена — ' +
198
+ 'вызови respond с итоговым сообщением оператору. Если нет — ' +
199
+ 'продолжи работу вызовом инструмента.';
200
+ continue;
201
+ }
137
202
  onAssistantMessage(msg);
138
203
  transcript?.log('assistant_final', { message: msg });
139
204
  return msg;
@@ -180,6 +245,27 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
180
245
  }
181
246
  return 'Достигнут лимит итераций.';
182
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
+ }
183
269
  // ============ JSON parsing ============
184
270
  function repairRawControlChars(str) {
185
271
  let out = '';
package/dist/tools.js CHANGED
@@ -115,7 +115,7 @@ export function createTools(workdir, { undo } = {}) {
115
115
  await undo.backup(file);
116
116
  await fs.mkdir(path.dirname(file), { recursive: true });
117
117
  await fs.writeFile(file, text, 'utf-8');
118
- return `Файл записан: {p}`;
118
+ return `Файл записан: ${p}`;
119
119
  },
120
120
  },
121
121
  {
@@ -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.4.7",
3
+ "version": "2.5.1",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "files": [
10
10
  "dist",
11
+ "scripts",
11
12
  "README.md",
12
13
  "LICENSE",
13
14
  "logo.png"
@@ -20,6 +21,7 @@
20
21
  "prepublishOnly": "npm run build",
21
22
  "test": "tsx --test test/*.test.ts",
22
23
  "test:watch": "tsx --test --watch test/*.test.ts",
24
+ "postinstall": "node scripts/postinstall.mjs",
23
25
  "prepare": "husky"
24
26
  },
25
27
  "keywords": [
@@ -0,0 +1,67 @@
1
+ // Automatically install the Chromium build and system dependencies
2
+ // that Playwright needs.
3
+ //
4
+ // Runs when the zames package is installed so the user does not have to run
5
+ // "npx playwright install chromium" / "install-deps chromium" by hand.
6
+ //
7
+ // On Linux (including WSL) the browser binary also needs system libraries.
8
+ // They are installed via "install-deps", which requires root (sudo).
9
+ // If we are not root and sudo is not passwordless, we skip it and tell the
10
+ // user the exact command to run.
11
+ //
12
+ // Install failure is NOT fatal: without network access (or in CI) the
13
+ // package install must still succeed.
14
+
15
+ import { spawnSync } from 'node:child_process'
16
+
17
+ const isWin = process.platform === 'win32'
18
+
19
+ // npm puts node_modules/.bin on PATH for lifecycle scripts, so the local
20
+ // playwright CLI is resolvable. On Windows npm runs scripts through cmd.exe,
21
+ // so shell: true is needed for the .cmd shim to be found.
22
+ function run(cmd, args) {
23
+ return spawnSync(cmd, args, {
24
+ stdio: 'inherit',
25
+ shell: isWin,
26
+ })
27
+ }
28
+
29
+ function canSudo() {
30
+ // Already root? No sudo needed.
31
+ if (typeof process.getuid === 'function' && process.getuid() === 0) return true
32
+ // Passwordless sudo (common in WSL / CI images).
33
+ const r = run('sudo', ['-n', 'true'])
34
+ return !r.error && r.status === 0
35
+ }
36
+
37
+ // 1) Browser binary for this Playwright version.
38
+ const browser = run('playwright', ['install', 'chromium'])
39
+ if (browser.error || browser.status !== 0) {
40
+ console.warn(
41
+ '\nzames: could not install Chromium for Playwright automatically.\n' +
42
+ ' Run it manually: npx playwright install chromium\n',
43
+ )
44
+ process.exit(0)
45
+ }
46
+
47
+ // 2) System libraries (Linux / WSL only). Without them Chromium fails
48
+ // to launch with "error while loading shared libraries".
49
+ if (!isWin) {
50
+ if (canSudo()) {
51
+ const deps = run('playwright', ['install-deps', 'chromium'])
52
+ if (deps.error || deps.status !== 0) {
53
+ console.warn(
54
+ '\nzames: could not install Chromium system dependencies automatically.\n' +
55
+ ' Run it manually: sudo npx playwright install-deps chromium\n',
56
+ )
57
+ }
58
+ } else {
59
+ console.warn(
60
+ '\nzames: on Linux/WSL Chromium needs system libraries (sudo required).\n' +
61
+ ' Run it once manually: sudo npx playwright install-deps chromium\n',
62
+ )
63
+ }
64
+ }
65
+
66
+ // Never fail the package install because of a browser download.
67
+ process.exit(0)