zames_pro 2.10.5 → 2.10.8
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 +34 -4
- package/dist/browser.js +69 -13
- package/package.json +1 -1
package/dist/agent-loop.js
CHANGED
|
@@ -117,6 +117,14 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
117
117
|
// instead get an EMPTY answer or the EXACT same answer as the previous turn
|
|
118
118
|
// (a sign the new message was not sent), we nudge instead of stopping.
|
|
119
119
|
let lastRaw = '';
|
|
120
|
+
// True when the PREVIOUS turn actually executed a tool. This is what
|
|
121
|
+
// makes a repeated answer "stale": the tool already ran, so running it
|
|
122
|
+
// again would duplicate side effects. If the previous answer looked like a
|
|
123
|
+
// call but was not recognized (e.g. a DSML/XML form the strict parser
|
|
124
|
+
// missed), the tool did NOT run and the echo is the only copy of the
|
|
125
|
+
// call — it must be executed, not discarded as stale.
|
|
126
|
+
// const ranToolLastTurn follows the previous turn's actual effect.
|
|
127
|
+
let lastTurnRanTool = false;
|
|
120
128
|
let justRanTool = false;
|
|
121
129
|
let watchdogRetries = 0;
|
|
122
130
|
const MAX_WATCHDOG_RETRIES = 3;
|
|
@@ -131,6 +139,11 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
131
139
|
// Subsequent ones (tool-result and resend requests) are agent sends:
|
|
132
140
|
// throttled so we don't hit the rate limit.
|
|
133
141
|
const isFirst = i === 0;
|
|
142
|
+
// Did the PREVIOUS turn actually execute a tool? Used by the stale
|
|
143
|
+
// check below. We take a snapshot and immediately clear it: it will be
|
|
144
|
+
// set again at the end of this iteration only if a tool really runs.
|
|
145
|
+
const ranToolPrevTurn = lastTurnRanTool;
|
|
146
|
+
lastTurnRanTool = false;
|
|
134
147
|
// Safety net: browser.ask() has its own timeout, but a stuck send used to
|
|
135
148
|
// block the whole loop and look like a silent stop. We race it against a
|
|
136
149
|
// hard deadline and treat a timeout as a nudge (re-ask), never as a
|
|
@@ -194,18 +207,31 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
194
207
|
// previous answer with different markdown emphasis (`**done**` vs `done`),
|
|
195
208
|
// which defeated an exact match and made the loop run the SAME tool again —
|
|
196
209
|
// the "stopped after a tool call" signature with a duplicate call.
|
|
197
|
-
|
|
210
|
+
//
|
|
211
|
+
// AN ECHO IS NOT ALWAYS STALE: the previous answer may have been a
|
|
212
|
+
// DSML/XML call that the strict parser missed and the loop never actually
|
|
213
|
+
// ran. In that case the echo is the ONLY copy of the call we can get,
|
|
214
|
+
// and discarding it as "stale" burns the watchdog budget and stalls the
|
|
215
|
+
// agent. So we first check whether the echo itself parses as a tool
|
|
216
|
+
// call (including the XML/DSML form); if it does, we do NOT treat it
|
|
217
|
+
// as stale — the call gets run below like any other.
|
|
218
|
+
// Stale means: the PREVIOUS turn actually ran a tool (lastTurnRanTool)
|
|
219
|
+
// and this answer is a repeat of the previous one. If the previous
|
|
220
|
+
// answer never ran a tool (unrecognized DSML call), the repeat is NOT
|
|
221
|
+
// stale — it is the only copy of the call and must be executed.
|
|
222
|
+
const staleCandidate = ranToolPrevTurn &&
|
|
198
223
|
lastRaw.trim() !== '' &&
|
|
199
224
|
(rawResponse.trim() === lastRaw.trim() ||
|
|
200
225
|
normForStale(rawResponse) === normForStale(lastRaw));
|
|
226
|
+
const wdStale = staleCandidate;
|
|
201
227
|
const wdNoCall = justRanTool &&
|
|
202
228
|
!wdEmpty &&
|
|
203
229
|
!wdStale &&
|
|
204
230
|
parseToolCall(rawResponse) === null &&
|
|
205
231
|
!responseLooksLikeToolCall(rawResponse);
|
|
206
|
-
// A stale answer is discarded
|
|
207
|
-
//
|
|
208
|
-
//
|
|
232
|
+
// A stale answer is discarded only when it does NOT parse as a tool
|
|
233
|
+
// call (see wdStale above); an echo of a call that never ran is a real
|
|
234
|
+
// call and must be executed, not dropped.
|
|
209
235
|
if (!isFirst && (wdEmpty || wdStale || wdNoCall) && watchdogRetries < MAX_WATCHDOG_RETRIES) {
|
|
210
236
|
watchdogRetries++;
|
|
211
237
|
transcript?.log('watchdog_nudge', {
|
|
@@ -464,6 +490,10 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
464
490
|
// A tool just ran — the next answer is expected to be a fresh tool call.
|
|
465
491
|
// Reset the watchdog so the next empty/repeated answer is nudged.
|
|
466
492
|
justRanTool = true;
|
|
493
|
+
// The tool really executed on this turn. The NEXT iteration will treat
|
|
494
|
+
// a repeat of this answer as stale only because of this flag (see NEXT,
|
|
495
|
+
// not the current, justRanTool check).
|
|
496
|
+
lastTurnRanTool = true;
|
|
467
497
|
watchdogRetries = 0;
|
|
468
498
|
// A fresh tool call just ran: reset the per-tool-result nudge budget so
|
|
469
499
|
// a long chain of tools is not cut off by an earlier bad turn.
|
package/dist/browser.js
CHANGED
|
@@ -460,7 +460,11 @@ export class DeepSeekBrowser {
|
|
|
460
460
|
throw e;
|
|
461
461
|
}
|
|
462
462
|
console.error(theme.warn(`⏳ DeepSeek: «слишком часто». Жду ${Math.ceil(this.rateLimitWaitMs / 60000)} мин (${rateLimitRetries}/${this.maxRateLimitRetries}) и повторю...`));
|
|
463
|
-
|
|
463
|
+
// Interruptible: Esc/Ctrl+C must cancel this long wait too,
|
|
464
|
+
// otherwise "stop" stays frozen for up to 5 minutes.
|
|
465
|
+
const aborted = await this._sleepInterruptible(this.rateLimitWaitMs);
|
|
466
|
+
if (aborted)
|
|
467
|
+
return '(прервано пользователем)';
|
|
464
468
|
continue;
|
|
465
469
|
}
|
|
466
470
|
console.error(`\n⚠ ask() попытка ${attempt}/${this.askRetries} провалилась: ${e.message}`);
|
|
@@ -605,6 +609,12 @@ export class DeepSeekBrowser {
|
|
|
605
609
|
// Pause between sends. Applied ONLY to agent messages
|
|
606
610
|
// (tool-result, system-prompt) so we don't hit the rate limit.
|
|
607
611
|
// User input is sent without delay.
|
|
612
|
+
//
|
|
613
|
+
// The wait is INTERRUPTIBLE: Esc/Ctrl+C sets _abort, and we check it every
|
|
614
|
+
// 100ms instead of one long page.waitForTimeout(gap). Before, Esc pressed
|
|
615
|
+
// during this pause did nothing to the pause itself — the send was still
|
|
616
|
+
// delayed by the remaining seconds, and the stop request only took effect
|
|
617
|
+
// after the pause. This is the main "Esc does not cancel the pause" bug.
|
|
608
618
|
async _waitForSendSlot(agent) {
|
|
609
619
|
if (!agent)
|
|
610
620
|
return;
|
|
@@ -614,7 +624,21 @@ export class DeepSeekBrowser {
|
|
|
614
624
|
if (gap <= 0)
|
|
615
625
|
return;
|
|
616
626
|
console.error(theme.warn(`⏳ пауза ${Math.ceil(gap / 1000)}с перед отправкой`));
|
|
617
|
-
await this.
|
|
627
|
+
await this._sleepInterruptible(gap);
|
|
628
|
+
}
|
|
629
|
+
// Sleep in small slices so Esc/Ctrl+C can cancel the wait promptly.
|
|
630
|
+
// Returns true if the sleep was cut short by _abort.
|
|
631
|
+
async _sleepInterruptible(ms) {
|
|
632
|
+
const step = 100;
|
|
633
|
+
let left = ms;
|
|
634
|
+
while (left > 0) {
|
|
635
|
+
if (this._abort)
|
|
636
|
+
return true;
|
|
637
|
+
const chunk = Math.min(step, left);
|
|
638
|
+
await this.page.waitForTimeout(chunk);
|
|
639
|
+
left -= chunk;
|
|
640
|
+
}
|
|
641
|
+
return this._abort;
|
|
618
642
|
}
|
|
619
643
|
// DEBUG: append ask() phases to ~/.zames/ask-debug.log so a stall can be
|
|
620
644
|
// diagnosed from the field (what the DOM/network looked like at each step).
|
|
@@ -630,7 +654,12 @@ export class DeepSeekBrowser {
|
|
|
630
654
|
}
|
|
631
655
|
async _askOnce(prompt, { timeout, agent, attachments = [], }) {
|
|
632
656
|
// We reset the abort flag ONLY at the very start of the send.
|
|
657
|
+
// _stopped (Esc for the WHOLE batch) is NOT reset here: it may have been
|
|
658
|
+
// set while a previous send was waiting in the pause. If it is set, the
|
|
659
|
+
// user asked to stop and we must not start a new generation at all.
|
|
633
660
|
this._abort = false;
|
|
661
|
+
if (this._stopped)
|
|
662
|
+
return '(прервано пользователем)';
|
|
634
663
|
const input = await this._findVisible(INPUT_SELECTORS, 10_000);
|
|
635
664
|
if (!input) {
|
|
636
665
|
throw new Error('Не найдено поле ввода. Запустите /debug-dom и поправьте INPUT_SELECTORS.');
|
|
@@ -638,6 +667,9 @@ export class DeepSeekBrowser {
|
|
|
638
667
|
const beforeText = await this._readLastAnswerTextClean().catch(() => '');
|
|
639
668
|
this._askDebug('SEND agent=' + agent + ' len=' + prompt.length + ' beforeLen=' + beforeText.length + ' beforeHead=' + JSON.stringify(beforeText.slice(0, 60)));
|
|
640
669
|
await this._waitForSendSlot(agent);
|
|
670
|
+
// Esc/Ctrl+C pressed during the pause — do not send anything.
|
|
671
|
+
if (this._abort)
|
|
672
|
+
return '(прервано пользователем)';
|
|
641
673
|
this._netCapture = '';
|
|
642
674
|
this._netCaptureAt = 0;
|
|
643
675
|
// Attach files/images FIRST (before the text): the DeepSeek upload widget
|
|
@@ -669,7 +701,7 @@ export class DeepSeekBrowser {
|
|
|
669
701
|
// Wait for the start: either Stop appeared, or the answer text changed,
|
|
670
702
|
// or the total amount of text on the page grew. In parallel we catch
|
|
671
703
|
// the rate-limit toast (only toasts, not the whole body).
|
|
672
|
-
const startDeadline = Date.now() +
|
|
704
|
+
const startDeadline = Date.now() + 30_000;
|
|
673
705
|
const startBodyLen = await this.page
|
|
674
706
|
.evaluate(() => document.body.innerText.length)
|
|
675
707
|
.catch(() => 0);
|
|
@@ -749,10 +781,36 @@ export class DeepSeekBrowser {
|
|
|
749
781
|
if (fresh) {
|
|
750
782
|
return this._netCapture;
|
|
751
783
|
}
|
|
752
|
-
//
|
|
753
|
-
//
|
|
754
|
-
//
|
|
755
|
-
|
|
784
|
+
// The send did not start generation within 15s. The most common cause is
|
|
785
|
+
// that the message did not actually go out (Enter lost, button not
|
|
786
|
+
// clicked). Instead of throwing (which made ask() retry for minutes and
|
|
787
|
+
// looked like a stall), press Enter once more and give it another
|
|
788
|
+
// window. Only if that also fails do we throw.
|
|
789
|
+
this._askDebug('START-failed, resending');
|
|
790
|
+
await this.page.keyboard.press('Enter');
|
|
791
|
+
this._lastSentAt = Date.now();
|
|
792
|
+
const retryDeadline = Date.now() + 20_000;
|
|
793
|
+
while (Date.now() < retryDeadline) {
|
|
794
|
+
if (this._abort)
|
|
795
|
+
return '(прервано пользователем)';
|
|
796
|
+
const cur2 = await this._readLastAnswerTextClean().catch(() => '');
|
|
797
|
+
const net2 = !!this._netCapture && this._netCaptureAt >= this._lastSentAt;
|
|
798
|
+
const grew2 = (await this.page
|
|
799
|
+
.evaluate(() => document.body.innerText.length)
|
|
800
|
+
.catch(() => 0)) > startBodyLen;
|
|
801
|
+
if (net2 ||
|
|
802
|
+
grew2 ||
|
|
803
|
+
(!!cur2 && normText(cur2) !== normText(beforeText))) {
|
|
804
|
+
started = true;
|
|
805
|
+
this._askDebug('STARTED-after-resend');
|
|
806
|
+
break;
|
|
807
|
+
}
|
|
808
|
+
await this.page.waitForTimeout(300);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
if (!started) {
|
|
812
|
+
throw new Error('Ответ не начал генерироваться за 35с даже после повторной отправки. ' +
|
|
813
|
+
'Проверьте чат DeepSeek вручную.');
|
|
756
814
|
}
|
|
757
815
|
// Wait until the answer stops changing. We check the "not generating"
|
|
758
816
|
// condition via text growth, NOT via _isGenerating().
|
|
@@ -791,7 +849,7 @@ export class DeepSeekBrowser {
|
|
|
791
849
|
// tool call" hang. If generation has clearly ENDED (no Stop button) and
|
|
792
850
|
// the text is stable, accept it (even when it repeats the previous one).
|
|
793
851
|
const sameAsBefore = !!cur && !isNew && normText(cur) === normText(beforeText);
|
|
794
|
-
if (
|
|
852
|
+
if (isNew && cur === last) {
|
|
795
853
|
stable++;
|
|
796
854
|
if (stable >= 2) {
|
|
797
855
|
if (isNew || !(await this._isGenerating())) {
|
|
@@ -803,7 +861,7 @@ export class DeepSeekBrowser {
|
|
|
803
861
|
else {
|
|
804
862
|
stable = 0;
|
|
805
863
|
}
|
|
806
|
-
if (isNew
|
|
864
|
+
if (isNew)
|
|
807
865
|
last = cur;
|
|
808
866
|
this._askDebug('FIN-loop isNew=' + isNew + ' sameAsBefore=' + sameAsBefore + ' stable=' + stable + ' curLen=' + cur.length + ' lastLen=' + last.length + ' netFresh=' + netFresh);
|
|
809
867
|
await this.page.waitForTimeout(800);
|
|
@@ -812,10 +870,8 @@ export class DeepSeekBrowser {
|
|
|
812
870
|
this._askDebug('RETURN last len=' + last.length);
|
|
813
871
|
return last;
|
|
814
872
|
}
|
|
815
|
-
//
|
|
816
|
-
|
|
817
|
-
return last;
|
|
818
|
-
}
|
|
873
|
+
// No fallback on a text equal to beforeText: returning it would re-run
|
|
874
|
+
// the previous tool call. Only a fresh network capture is accepted below.
|
|
819
875
|
if (this._netCapture && this._netCaptureAt >= this._lastSentAt) {
|
|
820
876
|
return this._netCapture;
|
|
821
877
|
}
|