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 +8 -1
- package/dist/agent-loop.js +88 -2
- package/dist/tools.js +1 -1
- package/dist/xml-toolcall.js +37 -22
- package/package.json +3 -1
- package/scripts/postinstall.mjs +67 -0
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
|
package/dist/agent-loop.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
{
|
package/dist/xml-toolcall.js
CHANGED
|
@@ -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 (
|
|
65
|
-
return
|
|
66
|
-
|
|
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.
|
|
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)
|