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.
- package/dist/agent-loop.js +61 -1
- package/dist/xml-toolcall.js +37 -22
- package/package.json +1 -1
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) — пользовательский ввод: без паузы.
|
|
@@ -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
|
-
|
|
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 = '';
|
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
|
}
|