zames_pro 2.9.3 → 2.10.0
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 +182 -23
- package/dist/browser.js +17 -4
- package/dist/i18n.js +9 -0
- package/dist/system-prompt.js +14 -1
- package/package.json +1 -1
package/dist/agent-loop.js
CHANGED
|
@@ -72,6 +72,16 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
72
72
|
const MAX_MALFORMED_RETRIES = 3;
|
|
73
73
|
let stallRetries = 0;
|
|
74
74
|
const MAX_STALL_RETRIES = 5;
|
|
75
|
+
// SINGLE shared budget for "the answer is not a recognized tool call".
|
|
76
|
+
// Before, every guard had its own counter (3+5+3+5+6 = 22 re-asks), so a
|
|
77
|
+
// stuck answer hung the loop for ~20 iterations and then returned an
|
|
78
|
+
// "iteration limit" stub — the exact "agent stopped" symptom. All the
|
|
79
|
+
// guards below now also bump this counter, and once it is exhausted the
|
|
80
|
+
// loop asks for respond exactly once and then finishes with the model's
|
|
81
|
+
// own text (never a stub).
|
|
82
|
+
let unparsedRetries = 0;
|
|
83
|
+
const MAX_UNPARSED_RETRIES = 4;
|
|
84
|
+
let finalRespondAsked = false;
|
|
75
85
|
// Guard against "the agent stalled": DeepSeek sometimes sends a final text
|
|
76
86
|
// that merely DESCRIBES the next tool call (or cuts the answer off
|
|
77
87
|
// mid-word), and the agent silently finishes the task even though the work
|
|
@@ -80,8 +90,6 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
80
90
|
// chatty model.
|
|
81
91
|
let looksDoneRetries = 0;
|
|
82
92
|
const MAX_LOOKSDONE_RETRIES = 3;
|
|
83
|
-
let plainTextRetries = 0;
|
|
84
|
-
const MAX_PLAINTEXT_RETRIES = 5;
|
|
85
93
|
// Watchdog against the agent emitting a tool call and then going silent.
|
|
86
94
|
// After a tool result the expected next answer is a fresh tool call; if we
|
|
87
95
|
// instead get an EMPTY answer or the EXACT same answer as the previous turn
|
|
@@ -90,16 +98,62 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
90
98
|
let justRanTool = false;
|
|
91
99
|
let watchdogRetries = 0;
|
|
92
100
|
const MAX_WATCHDOG_RETRIES = 3;
|
|
101
|
+
// Retry budget for a browser.ask() TIMEOUT (not for content): the send did
|
|
102
|
+
// not come back in time. This is separate from unparsedRetries because a
|
|
103
|
+
// timeout is an infrastructure failure, not a model protocol violation.
|
|
104
|
+
let afterToolRetries = 0;
|
|
105
|
+
const MAX_AFTER_TOOL_RETRIES = 6;
|
|
93
106
|
for (let i = 0; i < maxIterations; i++) {
|
|
94
107
|
onThinking();
|
|
95
108
|
// The first message (task) is user input: no throttle.
|
|
96
109
|
// Subsequent ones (tool-result and resend requests) are agent sends:
|
|
97
110
|
// throttled so we don't hit the rate limit.
|
|
98
111
|
const isFirst = i === 0;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
112
|
+
// Safety net: browser.ask() has its own timeout, but a stuck send used to
|
|
113
|
+
// block the whole loop and look like a silent stop. We race it against a
|
|
114
|
+
// hard deadline and treat a timeout as a nudge (re-ask), never as a
|
|
115
|
+
// final answer. The deadline is generous enough for real long answers.
|
|
116
|
+
const askDeadlineMs = 240_000;
|
|
117
|
+
let rawResponse;
|
|
118
|
+
let askTimer = null;
|
|
119
|
+
try {
|
|
120
|
+
rawResponse = await Promise.race([
|
|
121
|
+
browser.ask(message, {
|
|
122
|
+
agent: !isFirst,
|
|
123
|
+
attachments: isFirst ? attachments : [],
|
|
124
|
+
}),
|
|
125
|
+
new Promise((_, reject) => {
|
|
126
|
+
askTimer = setTimeout(() => reject(new Error('ask() watchdog timeout')), askDeadlineMs);
|
|
127
|
+
if (askTimer && typeof askTimer.unref === 'function') {
|
|
128
|
+
askTimer.unref();
|
|
129
|
+
}
|
|
130
|
+
}),
|
|
131
|
+
]);
|
|
132
|
+
if (askTimer)
|
|
133
|
+
clearTimeout(askTimer);
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
if (askTimer)
|
|
137
|
+
clearTimeout(askTimer);
|
|
138
|
+
transcript?.log('ask_timeout', {
|
|
139
|
+
attempt: afterToolRetries,
|
|
140
|
+
error: e.message,
|
|
141
|
+
});
|
|
142
|
+
onWarning('browser.ask() не вернул ответ за ' +
|
|
143
|
+
Math.round(askDeadlineMs / 1000) +
|
|
144
|
+
'с — повторяю запрос.');
|
|
145
|
+
if (afterToolRetries < MAX_AFTER_TOOL_RETRIES) {
|
|
146
|
+
afterToolRetries++;
|
|
147
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
transcript?.log('ask_timeout_exhausted', {
|
|
151
|
+
message: 'ask() не вернул ответ и лимит повторов исчерпан',
|
|
152
|
+
});
|
|
153
|
+
onWarning('browser.ask() перестал отвечать; лимит повторов исчерпан, ' +
|
|
154
|
+
'останавливаюсь. Ответа модели нет — проверьте чат DeepSeek вручную.');
|
|
155
|
+
return 'ask() watchdog: ответ модели не получен';
|
|
156
|
+
}
|
|
103
157
|
await reportChat();
|
|
104
158
|
transcript?.log('assistant_raw', { response: rawResponse });
|
|
105
159
|
// The user aborted generation (Esc/Ctrl+C).
|
|
@@ -107,24 +161,47 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
107
161
|
transcript?.log('user_aborted');
|
|
108
162
|
return rawResponse;
|
|
109
163
|
}
|
|
110
|
-
// Watchdog: after a tool result we expect a FRESH tool call.
|
|
111
|
-
//
|
|
112
|
-
// sent),
|
|
164
|
+
// Watchdog: after a tool result we expect a FRESH tool call. DeepSeek
|
|
165
|
+
// regularly stops right here; the answer may be (a) empty, (b) an exact
|
|
166
|
+
// copy of the previous turn (the new message was not sent), or (c) a
|
|
167
|
+
// non-empty fragment that parses to nothing and is not a tool call (a
|
|
168
|
+
// cut-off "Stale. Let me ..." or a truncated JSON). All three mean the
|
|
169
|
+
// turn is unfinished: nudge instead of stopping.
|
|
113
170
|
const wdEmpty = !String(rawResponse || '').trim();
|
|
114
|
-
const wdStale = justRanTool &&
|
|
115
|
-
|
|
171
|
+
const wdStale = justRanTool &&
|
|
172
|
+
lastRaw.trim() !== '' &&
|
|
173
|
+
rawResponse.trim() === lastRaw.trim();
|
|
174
|
+
const wdNoCall = justRanTool &&
|
|
175
|
+
!wdEmpty &&
|
|
176
|
+
!wdStale &&
|
|
177
|
+
parseToolCall(rawResponse) === null &&
|
|
178
|
+
!responseLooksLikeToolCall(rawResponse);
|
|
179
|
+
if (!isFirst && (wdEmpty || wdStale || wdNoCall) && watchdogRetries < MAX_WATCHDOG_RETRIES) {
|
|
116
180
|
watchdogRetries++;
|
|
117
181
|
transcript?.log('watchdog_nudge', {
|
|
118
182
|
attempt: watchdogRetries,
|
|
119
183
|
empty: wdEmpty,
|
|
120
184
|
stale: wdStale,
|
|
185
|
+
no_call: wdNoCall,
|
|
121
186
|
response: String(rawResponse || '').slice(0, 200),
|
|
122
187
|
});
|
|
188
|
+
message =
|
|
189
|
+
'Ты остановился после результата инструмента. Продолжи работу: ' +
|
|
190
|
+
'ответь РОВНО одним JSON-объектом вызова инструмента, без текста до и после, ' +
|
|
191
|
+
'например: {"tool": "Bash", "args": {"command": "..."}}. ' +
|
|
192
|
+
'Если задача действительно выполнена — вызови respond с итоговым сообщением.';
|
|
123
193
|
await new Promise((r) => setTimeout(r, 1500));
|
|
124
194
|
continue;
|
|
125
195
|
}
|
|
126
196
|
lastRaw = rawResponse;
|
|
127
197
|
const parsed = parseToolCall(rawResponse);
|
|
198
|
+
// SILENT CONTRACT: pre-tool text around a valid call is NEVER shown to the
|
|
199
|
+
// operator. It is only handed to onAssistantThought, which is a no-op by
|
|
200
|
+
// default and is not wired to the UI in src/index.ts (so ui.assistant /
|
|
201
|
+
// the terminal never receives "Let me check..." / "Сейчас посмотрю").
|
|
202
|
+
// We cannot strip this text from DeepSeek's own chat output with code —
|
|
203
|
+
// that text is generated by the model. We can only (a) forbid it via the
|
|
204
|
+
// system-prompt ("ONLY TOOL CALLS") and (b) not print it here.
|
|
128
205
|
if (parsed) {
|
|
129
206
|
const thought = extractPreToolText(rawResponse);
|
|
130
207
|
if (thought)
|
|
@@ -145,6 +222,7 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
145
222
|
const looksLikeToolCall = responseLooksLikeToolCall(rawResponse);
|
|
146
223
|
if (looksLikeToolCall && malformedRetries < MAX_MALFORMED_RETRIES) {
|
|
147
224
|
malformedRetries++;
|
|
225
|
+
unparsedRetries++;
|
|
148
226
|
transcript?.log('malformed_toolcall', {
|
|
149
227
|
attempt: malformedRetries,
|
|
150
228
|
response: rawResponse,
|
|
@@ -176,6 +254,7 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
176
254
|
/(messages? too frequent|too many requests|rate limit|server (is )?busy|service (is )?unavailable|слишком часто|try again later)/i.test(trimmed));
|
|
177
255
|
if (looksService && stallRetries < MAX_STALL_RETRIES) {
|
|
178
256
|
stallRetries++;
|
|
257
|
+
unparsedRetries++;
|
|
179
258
|
transcript?.log('stall_retry', {
|
|
180
259
|
attempt: stallRetries,
|
|
181
260
|
response: rawResponse,
|
|
@@ -200,6 +279,7 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
200
279
|
// continue and to actually call a tool this time (or respond if truly done).
|
|
201
280
|
if (looksLikeUnfinishedWork(trimmed) && looksDoneRetries < MAX_LOOKSDONE_RETRIES) {
|
|
202
281
|
looksDoneRetries++;
|
|
282
|
+
unparsedRetries++;
|
|
203
283
|
transcript?.log('unfinished_retry', {
|
|
204
284
|
attempt: looksDoneRetries,
|
|
205
285
|
response: rawResponse,
|
|
@@ -220,36 +300,75 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
220
300
|
continue;
|
|
221
301
|
}
|
|
222
302
|
// STRICT: only tool calls and respond reach the operator. Plain text is
|
|
223
|
-
// a protocol violation
|
|
224
|
-
|
|
225
|
-
|
|
303
|
+
// a protocol violation. The retries are bounded by the SINGLE
|
|
304
|
+
// unparsedRetries budget, so a chatty model cannot hang the loop for
|
|
305
|
+
// 20+ iterations (the old plainTextRetries + afterToolRetries combo).
|
|
306
|
+
if (unparsedRetries < MAX_UNPARSED_RETRIES) {
|
|
307
|
+
unparsedRetries++;
|
|
226
308
|
transcript?.log('plaintext_retry', {
|
|
227
|
-
attempt:
|
|
309
|
+
attempt: unparsedRetries,
|
|
228
310
|
response: rawResponse.slice(0, 500),
|
|
229
311
|
});
|
|
230
312
|
message =
|
|
231
|
-
|
|
232
|
-
'
|
|
233
|
-
'
|
|
313
|
+
(justRanTool
|
|
314
|
+
? 'Ты остановился после вызова инструмента и написал обычный текст. '
|
|
315
|
+
: 'Ты написал обычный текст без вызова инструмента. ') +
|
|
316
|
+
'Задача ещё не завершена. Ответь РОВНО одним JSON-объектом вызова ' +
|
|
317
|
+
'инструмента, без текста до и после, например: ' +
|
|
318
|
+
'{\"tool\": \"Bash\", \"args\": {\"command\": \"...\"}}. ' +
|
|
319
|
+
'Если задача действительно выполнена — вызови respond с итоговым сообщением.';
|
|
234
320
|
continue;
|
|
235
321
|
}
|
|
236
|
-
if
|
|
322
|
+
// Budget exhausted. Ask for respond EXACTLY once more; if the model
|
|
323
|
+
// still does not call it, surface its own text as the final answer
|
|
324
|
+
// (with a single warning) instead of looping to the iteration limit.
|
|
325
|
+
if (!finalRespondAsked) {
|
|
326
|
+
finalRespondAsked = true;
|
|
327
|
+
transcript?.log('final_respond_request', {
|
|
328
|
+
response: rawResponse.slice(0, 500),
|
|
329
|
+
});
|
|
330
|
+
message =
|
|
331
|
+
'Последний шаг: вызови инструмент respond с итоговым сообщением ' +
|
|
332
|
+
'оператору. Не пиши обычный текст — только вызов respond, например: ' +
|
|
333
|
+
'{\"tool\": \"respond\", \"args\": {\"message\": \"...\"}}';
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const suspiciousFinal = responseLooksLikeToolCall(rawResponse) ||
|
|
337
|
+
!(rawResponse || '').trim();
|
|
338
|
+
if (suspiciousFinal) {
|
|
339
|
+
// The answer LOOKS like a call (or is empty) but could not be parsed
|
|
340
|
+
// even after all retries: warn the operator.
|
|
237
341
|
transcript?.log('suspicious_final', { response: rawResponse });
|
|
342
|
+
onWarning(translate(locale)('msg.suspicious_stop'));
|
|
238
343
|
}
|
|
344
|
+
// A meaningful plain-text answer (e.g. a final report the model forgot to
|
|
345
|
+
// wrap in respond) is surfaced as-is WITHOUT a warning: after the bounded
|
|
346
|
+
// re-asks it is the best available result, and warning here only
|
|
347
|
+
// confused the operator in earlier sessions.
|
|
239
348
|
transcript?.log('plaintext_final', { message: rawResponse });
|
|
240
|
-
onWarning(translate(locale)('msg.suspicious_stop'));
|
|
241
349
|
return rawResponse;
|
|
242
350
|
}
|
|
243
351
|
const calls = Array.isArray(parsed) ? parsed : [parsed];
|
|
352
|
+
// A respond mixed with real tool calls must NOT short-circuit the tools.
|
|
353
|
+
// DeepSeek sometimes returns [{"tool":"Edit",...},{"tool":"respond",...}]
|
|
354
|
+
// in ONE answer; handling respond first would silently DROP the other
|
|
355
|
+
// call and the agent would look "stopped after a tool call". We only
|
|
356
|
+
// finish on respond when it is the SOLE call in the answer.
|
|
357
|
+
const realCalls = calls.filter((c) => c.tool !== 'respond');
|
|
244
358
|
const respondCall = calls.find((c) => c.tool === 'respond');
|
|
245
|
-
if (respondCall) {
|
|
359
|
+
if (respondCall && realCalls.length > 0) {
|
|
360
|
+
transcript?.log('respond_mixed_with_tools', {
|
|
361
|
+
tools: realCalls.map((c) => c.tool),
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
if (respondCall && realCalls.length === 0) {
|
|
246
365
|
const msg = typeof respondCall.args.message === 'string'
|
|
247
366
|
? respondCall.args.message
|
|
248
367
|
: String(respondCall.args.message ?? '');
|
|
249
368
|
// An empty respond is not final: the model called respond but wrote no
|
|
250
369
|
// summary. Finishing like this would show the operator nothing and the
|
|
251
370
|
// task would "hang". We ask it to continue (within stallRetries).
|
|
252
|
-
if (!msg
|
|
371
|
+
if (!isMeaningfulRespond(msg) && stallRetries < MAX_STALL_RETRIES) {
|
|
253
372
|
stallRetries++;
|
|
254
373
|
transcript?.log('empty_respond', { attempt: stallRetries });
|
|
255
374
|
if (debugLog) {
|
|
@@ -265,12 +384,27 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
265
384
|
'продолжи работу вызовом инструмента.';
|
|
266
385
|
continue;
|
|
267
386
|
}
|
|
387
|
+
// An EMPTY respond must NEVER be a silent final: the operator would see
|
|
388
|
+
// nothing and the task would look "stopped after a tool call". If the
|
|
389
|
+
// retry budget is exhausted, warn the operator and keep the run open
|
|
390
|
+
// instead of returning an empty string. Only a NON-empty respond ends
|
|
391
|
+
// the task normally.
|
|
392
|
+
if (!isMeaningfulRespond(msg)) {
|
|
393
|
+
transcript?.log('empty_respond_exhausted', { response: rawResponse });
|
|
394
|
+
onWarning('Модель вызвала respond без текста, и лимит повторов исчерпан. ' +
|
|
395
|
+
'Проверьте чат DeepSeek вручную.');
|
|
396
|
+
return 'Модель не сформировала итоговое сообщение (пустой respond).';
|
|
397
|
+
}
|
|
268
398
|
onAssistantMessage(msg);
|
|
269
399
|
transcript?.log('assistant_final', { message: msg });
|
|
270
400
|
return msg;
|
|
271
401
|
}
|
|
272
402
|
const results = [];
|
|
273
|
-
|
|
403
|
+
// When respond came together with real tools, skip respond here: its
|
|
404
|
+
// message must NOT be delivered before the tools' results are known.
|
|
405
|
+
// The model will get the tool results and can call respond again.
|
|
406
|
+
const callsToRun = realCalls.length > 0 ? realCalls : calls;
|
|
407
|
+
for (const call of callsToRun) {
|
|
274
408
|
const tool = tools.find((t) => t.name === call.tool);
|
|
275
409
|
if (!tool) {
|
|
276
410
|
const err = `Неизвестный инструмент: ${call.tool}`;
|
|
@@ -299,6 +433,13 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
299
433
|
// Reset the watchdog so the next empty/repeated answer is nudged.
|
|
300
434
|
justRanTool = true;
|
|
301
435
|
watchdogRetries = 0;
|
|
436
|
+
// A fresh tool call just ran: reset the per-tool-result nudge budget so
|
|
437
|
+
// a long chain of tools is not cut off by an earlier bad turn.
|
|
438
|
+
afterToolRetries = 0;
|
|
439
|
+
// Real progress was made, so the "unparsed answer" budget is replenished:
|
|
440
|
+
// a long chain of tools must not run out of it because of earlier hiccups.
|
|
441
|
+
unparsedRetries = 0;
|
|
442
|
+
finalRespondAsked = false;
|
|
302
443
|
if (results.length === 1) {
|
|
303
444
|
const r = results[0];
|
|
304
445
|
const resultStr = typeof r.result === 'string' ? r.result : JSON.stringify(r.result);
|
|
@@ -340,6 +481,24 @@ export function responseLooksLikeToolCall(rawResponse) {
|
|
|
340
481
|
/<\s*\|?\s*(DSML|invoke|parameter)/i.test(raw) ||
|
|
341
482
|
/^\s*\[?\s*\{[^}]*$/.test(raw.trim()));
|
|
342
483
|
}
|
|
484
|
+
// A respond message is only a real FINAL answer when it carries some meaning.
|
|
485
|
+
// DeepSeek sometimes finishes with a placeholder — "...", "-", "ok", "done",
|
|
486
|
+
// "готово" — which looks like a stop with no report. Such a respond must not
|
|
487
|
+
// end the task silently: it is treated like an empty one (re-ask).
|
|
488
|
+
function isMeaningfulRespond(msg) {
|
|
489
|
+
const t = (msg || '').trim();
|
|
490
|
+
if (!t)
|
|
491
|
+
return false;
|
|
492
|
+
// Only punctuation/dots/ellipses: "...", "---", "…", "?" — not a report.
|
|
493
|
+
if (/^[.…–—_*?!/\s-]+$/.test(t))
|
|
494
|
+
return false;
|
|
495
|
+
// A bare acknowledgement with no content at all. NOTE: "ok"/"done"/
|
|
496
|
+
// "готово" are NOT in this list: a short "готово" is a legitimate final
|
|
497
|
+
// answer for a small task, and dropping it re-opened the loop.
|
|
498
|
+
if (/^(na|null|undefined)[.!]*$/i.test(t))
|
|
499
|
+
return false;
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
343
502
|
// Text that promises a tool call in the future tense but contains no call
|
|
344
503
|
// itself. DeepSeek regularly "hangs" like this: it writes
|
|
345
504
|
// "Now update README to mention …", "Let me run the tests", "I'll check now"
|
package/dist/browser.js
CHANGED
|
@@ -668,7 +668,13 @@ export class DeepSeekBrowser {
|
|
|
668
668
|
// make us think the new answer had started when in fact nothing was sent
|
|
669
669
|
// — and then the agent silently "stopped".
|
|
670
670
|
const changed = cur && normText(cur) !== normText(beforeText);
|
|
671
|
-
|
|
671
|
+
// Network capture with a fresh timestamp is the STRONGEST proof that a
|
|
672
|
+
// new answer started: it is the raw SSE body for the CURRENT send. Right
|
|
673
|
+
// after a tool result the DOM may still show the previous answer, so
|
|
674
|
+
// 'changed' can stay false for a while — without this check ask() used
|
|
675
|
+
// to hang until the full timeout and the agent appeared to "stop".
|
|
676
|
+
const netStarted = !!this._netCapture && this._netCaptureAt >= this._lastSentAt;
|
|
677
|
+
if (changed || netStarted || bodyLen > startBodyLen) {
|
|
672
678
|
started = true;
|
|
673
679
|
break;
|
|
674
680
|
}
|
|
@@ -703,12 +709,15 @@ export class DeepSeekBrowser {
|
|
|
703
709
|
}
|
|
704
710
|
}
|
|
705
711
|
}
|
|
712
|
+
const netFresh = !!this._netCapture && this._netCaptureAt >= this._lastSentAt;
|
|
706
713
|
const cur = await this._readLastAnswerTextClean().catch(() => '');
|
|
707
714
|
// Ignore an "answer" that is identical to what was on the page BEFORE we
|
|
708
715
|
// sent the message: that is the previous answer, not a new one. Returning
|
|
709
716
|
// it would make the agent re-process the old tool call (or silently
|
|
710
|
-
// stop). We keep waiting instead.
|
|
711
|
-
|
|
717
|
+
// stop). We keep waiting instead. A fresh network capture is exempt: it
|
|
718
|
+
// belongs to the CURRENT send even if the DOM still shows the old text.
|
|
719
|
+
const isNew = !!cur &&
|
|
720
|
+
(netFresh || normText(cur) !== normText(beforeText));
|
|
712
721
|
if (isNew && cur === last) {
|
|
713
722
|
stable++;
|
|
714
723
|
if (stable >= 2)
|
|
@@ -721,8 +730,12 @@ export class DeepSeekBrowser {
|
|
|
721
730
|
last = cur;
|
|
722
731
|
await this.page.waitForTimeout(800);
|
|
723
732
|
}
|
|
724
|
-
if (last && normText(last) !== normText(beforeText))
|
|
733
|
+
if (last && (normText(last) !== normText(beforeText) || this._netCapture)) {
|
|
725
734
|
return last;
|
|
735
|
+
}
|
|
736
|
+
if (this._netCapture && this._netCaptureAt >= this._lastSentAt) {
|
|
737
|
+
return this._netCapture;
|
|
738
|
+
}
|
|
726
739
|
throw new Error('Новый ответ не получен (на странице остался прежний текст). ' +
|
|
727
740
|
'Возможно, сообщение не отправилось.');
|
|
728
741
|
}
|
package/dist/i18n.js
CHANGED
|
@@ -283,6 +283,15 @@ const CATALOG = {
|
|
|
283
283
|
en: 'IMPORTANT: reply to the operator in English. All text in the respond tool message field, and any explanations, must be in English.',
|
|
284
284
|
},
|
|
285
285
|
'prompt.tools_header': { ru: 'You have access to the following tools:', en: 'You have access to the following tools:' },
|
|
286
|
+
// The hard "ONLY TOOL CALLS" block. All prose around a tool call is a
|
|
287
|
+
// protocol violation: the operator never sees it (only tool calls and the
|
|
288
|
+
// final respond reach the terminal), so it is pure pollution. We cannot
|
|
289
|
+
// stop DeepSeek from generating it INSIDE its chat with code (that is the
|
|
290
|
+
// model's output); we can only forbid it by prompt and hide it here.
|
|
291
|
+
'prompt.only_tool_calls': {
|
|
292
|
+
ru: '## ТОЛЬКО ВЫЗОВЫ ИНСТРУМЕНТОВ (жёсткое правило)\n\nОбщайся с оператором ТОЛЬКО через вызовы инструментов. Любой обычный текст — объяснения, планы, рассуждения, комментарии, извинения, приветствия, заголовки, списки, markdown, эмодзи — ЗАПРЕЩЁН. Он не читается и считается ошибкой.\n\nТВОЙ ЕДИНСТВЕННЫЙ ВЫВОД — вызов инструмента. В каждом ответе ровно один JSON-объект вызова (или массив независимых вызовов), без единого слова до и после.\n\nНЕЛЬЗЯ писать: «сейчас сделаю», «давай посмотрим», «проверю», планы, объяснения, итоги между шагами.\n\nМОЖНО только вызов инструмента и, в самом конце, когда задача выполнена, respond с итогом.\n\nЕдинственное место, где допускается текст, — поле message внутри respond, и только в самом конце.',
|
|
293
|
+
en: '## ONLY TOOL CALLS (hard rule)\n\nTalk to the operator ONLY through tool calls. Any plain text — explanations, plans, reasoning, comments, apologies, greetings, headings, lists, markdown, emoji — is FORBIDDEN. It is not read and counts as an error.\n\nYOUR ONLY OUTPUT is a tool call. Each turn contains exactly one JSON tool-call object (or an array of independent calls), with not a single word before or after.\n\nYou MUST NOT write: "I will now...", "let us look...", "let me check", plans, explanations, progress notes between steps.\n\nALLOWED: only a tool call and, at the very end, when the task is done, respond with the summary.\n\nThe only place where text is allowed is the message field inside respond, and only at the very end.',
|
|
294
|
+
},
|
|
286
295
|
};
|
|
287
296
|
export function translate(locale) {
|
|
288
297
|
const loc = isLocale(locale) ? locale : DEFAULT_LOCALE;
|
package/dist/system-prompt.js
CHANGED
|
@@ -45,7 +45,20 @@ stop. If you are still working, emit a tool call instead.
|
|
|
45
45
|
|
|
46
46
|
So the pattern is: tool call, tool call, tool call, ..., then a single final
|
|
47
47
|
respond. A bare text message without a tool call ends the task and the
|
|
48
|
-
operator will not read it, so never use plain text
|
|
48
|
+
operator will not read it, so never use plain text.
|
|
49
|
+
|
|
50
|
+
${t('prompt.only_tool_calls')}
|
|
51
|
+
|
|
52
|
+
NO PROSE AROUND TOOL CALLS. Each turn must contain ONLY the JSON of the tool
|
|
53
|
+
call(s) — not a single word before or after, not even a short lead-in like
|
|
54
|
+
"Let me check..." or "Now I'll fix it.". The JSON must be the entire response.
|
|
55
|
+
|
|
56
|
+
WRONG: "Let me read the file first." then a Read call.
|
|
57
|
+
WRONG: a Read call then "I'll analyze the result next."
|
|
58
|
+
RIGHT: only the JSON of the tool call, nothing else.
|
|
59
|
+
|
|
60
|
+
If you feel the urge to explain, do not: put it in the final respond when the
|
|
61
|
+
task is done (or when you must ask the operator), not between tool calls.
|
|
49
62
|
|
|
50
63
|
You have access to the following tools:
|
|
51
64
|
|