zames_pro 2.29.2 → 2.29.4
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/browser.js +99 -8
- package/dist/net-capture.js +26 -0
- package/package.json +1 -1
package/dist/browser.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { chromium, } from 'playwright';
|
|
2
|
-
import { extractAnswer, extractTokenUsage, dumpNetBody } from './net-capture.js';
|
|
2
|
+
import { extractAnswer, extractTokenUsage, dumpNetBody, isFinishedWithoutAnswer, } from './net-capture.js';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import os from 'os';
|
|
5
5
|
import fs from 'fs/promises';
|
|
@@ -317,6 +317,10 @@ export class DeepSeekBrowser {
|
|
|
317
317
|
page;
|
|
318
318
|
_netCapture;
|
|
319
319
|
_netCaptureAt;
|
|
320
|
+
// True when the LAST response body was a FINISHED turn with NO answer text
|
|
321
|
+
// (only reasoning). The DOM keeps showing reasoning/old text, so the loop
|
|
322
|
+
// must not wait it out — it clicks Continue / resends. Reset before a send.
|
|
323
|
+
_netNoAnswer;
|
|
320
324
|
_netChatId;
|
|
321
325
|
// The latest CONTEXT size (in tokens) DeepSeek reported in the current
|
|
322
326
|
// chat. It is the `accumulated_token_usage` counter taken from the SSE
|
|
@@ -336,6 +340,13 @@ export class DeepSeekBrowser {
|
|
|
336
340
|
_lastHistoryError;
|
|
337
341
|
_netSniff;
|
|
338
342
|
_netSniffLimit;
|
|
343
|
+
// The RAW SSE body of the LAST answer response. Unlike `_netCapture`, this
|
|
344
|
+
// is kept even when the body carries no RESPONSE text (a truncated turn:
|
|
345
|
+
// generation_err / INCOMPLETE, or FINISHED-with-only-reasoning). The
|
|
346
|
+
// incomplete/no-answer detectors MUST run on the raw body — guarding them on
|
|
347
|
+
// `_netCapture` (which is '' in exactly those cases) made them dead code and
|
|
348
|
+
// the agent hung until the timeout.
|
|
349
|
+
_netBody;
|
|
339
350
|
_netHookInstalled;
|
|
340
351
|
// Fired right when a message is actually typed/sent (AFTER the send-pause
|
|
341
352
|
// and attachments). Used to start the "agent is working" spinner only when
|
|
@@ -380,6 +391,7 @@ export class DeepSeekBrowser {
|
|
|
380
391
|
this._stopped = false;
|
|
381
392
|
this._netCapture = '';
|
|
382
393
|
this._netCaptureAt = 0;
|
|
394
|
+
this._netNoAnswer = false;
|
|
383
395
|
this._netChatId = null;
|
|
384
396
|
this._lastTokenUsage = null;
|
|
385
397
|
this._toggles = { deepThinking, webSearch };
|
|
@@ -388,6 +400,7 @@ export class DeepSeekBrowser {
|
|
|
388
400
|
this._lastHistoryError = '';
|
|
389
401
|
this._netSniff = [];
|
|
390
402
|
this._netSniffLimit = 5;
|
|
403
|
+
this._netBody = '';
|
|
391
404
|
this._netHookInstalled = false;
|
|
392
405
|
this.onSendStart = null;
|
|
393
406
|
this.onSendPause = null;
|
|
@@ -543,11 +556,38 @@ export class DeepSeekBrowser {
|
|
|
543
556
|
const extracted = extractAnswer(body);
|
|
544
557
|
if (extracted) {
|
|
545
558
|
this._netCapture = extracted;
|
|
559
|
+
this._netNoAnswer = false;
|
|
546
560
|
const cid = url.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/);
|
|
547
561
|
if (cid)
|
|
548
562
|
this._netChatId = cid[0];
|
|
549
563
|
this._netCaptureAt = Date.now();
|
|
550
564
|
}
|
|
565
|
+
// Store the RAW body for the CURRENT answer (completion/continue) so the
|
|
566
|
+
// truncated/no-answer detectors can inspect it even when there is no
|
|
567
|
+
// RESPONSE text (extractAnswer() returns '' for those bodies).
|
|
568
|
+
if (/chat\/(completion|continue)/i.test(url)) {
|
|
569
|
+
this._netBody = body;
|
|
570
|
+
const cid = url.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/);
|
|
571
|
+
if (cid)
|
|
572
|
+
this._netChatId = cid[0];
|
|
573
|
+
if (!extracted) {
|
|
574
|
+
if (isFinishedWithoutAnswer(body)) {
|
|
575
|
+
// A FINISHED turn with NO RESPONSE fragment (only reasoning): the
|
|
576
|
+
// UI shows Stopped + Continue. Record it with a fresh timestamp
|
|
577
|
+
// so the ask() loops do not wait out the timeout — they click
|
|
578
|
+
// Continue / resend.
|
|
579
|
+
this._netNoAnswer = true;
|
|
580
|
+
this._netCapture = '';
|
|
581
|
+
}
|
|
582
|
+
// A truncated turn (generation_err / INCOMPLETE) also has no
|
|
583
|
+
// RESPONSE; make the body visible to the incomplete check by
|
|
584
|
+
// refreshing the timestamp.
|
|
585
|
+
if (isGenerationIncompleteText(body)) {
|
|
586
|
+
this._netCapture = '';
|
|
587
|
+
}
|
|
588
|
+
this._netCaptureAt = Date.now();
|
|
589
|
+
}
|
|
590
|
+
}
|
|
551
591
|
}
|
|
552
592
|
catch { }
|
|
553
593
|
}
|
|
@@ -1633,6 +1673,8 @@ export class DeepSeekBrowser {
|
|
|
1633
1673
|
}
|
|
1634
1674
|
this._netCapture = '';
|
|
1635
1675
|
this._netCaptureAt = 0;
|
|
1676
|
+
this._netNoAnswer = false;
|
|
1677
|
+
this._netBody = '';
|
|
1636
1678
|
// Align the DeepSeek chat toggles (deep thinking / web search) with the
|
|
1637
1679
|
// configured state BEFORE typing. Doing it here (after the send-pause) it
|
|
1638
1680
|
// does not flip toggles for a generation that is not going to happen.
|
|
@@ -1692,10 +1734,26 @@ export class DeepSeekBrowser {
|
|
|
1692
1734
|
// The turn was truncated by the server: the SSE body carries
|
|
1693
1735
|
// finish_reason=generation_err / quasi_status=INCOMPLETE and the UI
|
|
1694
1736
|
// shows "Continue". Retry instead of waiting out the whole timeout.
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1737
|
+
// NOTE: the check runs on the RAW `_netBody`, not on `_netCapture`: a
|
|
1738
|
+
// truncated body has NO RESPONSE fragment, so extractAnswer() returned
|
|
1739
|
+
// '' and `_netCapture` was empty — guarding on it made this branch dead
|
|
1740
|
+
// code and the agent hung on "Stopped" until the timeout.
|
|
1741
|
+
if (this._netCaptureAt >= this._lastSentAt &&
|
|
1742
|
+
isGenerationIncompleteText(this._netBody)) {
|
|
1743
|
+
throw new GenerationIncompleteError(this._netBody.slice(-300));
|
|
1744
|
+
}
|
|
1745
|
+
// A FINISHED turn with NO answer (only reasoning): the UI shows a
|
|
1746
|
+
// Continue button. Click it (or, if autoContinue is off, keep waiting)
|
|
1747
|
+
// instead of spinning to the deadline on an empty answer.
|
|
1748
|
+
if (this._netNoAnswer && this._netCaptureAt >= this._lastSentAt) {
|
|
1749
|
+
this._askDebug('START-loop net-no-answer');
|
|
1750
|
+
if (this.autoContinue && (await this._clickContinueIfVisible())) {
|
|
1751
|
+
this._askDebug('CLICKED Continue (net-no-answer, start-loop)');
|
|
1752
|
+
this._netNoAnswer = false;
|
|
1753
|
+
this._netCaptureAt = 0;
|
|
1754
|
+
await this.page.waitForTimeout(500);
|
|
1755
|
+
continue;
|
|
1756
|
+
}
|
|
1699
1757
|
}
|
|
1700
1758
|
// Reasoning-mode pause during the THINK phase: click Continue so the
|
|
1701
1759
|
// generation resumes (the button appears before any RESPONSE text, so
|
|
@@ -1773,6 +1831,14 @@ export class DeepSeekBrowser {
|
|
|
1773
1831
|
// previous tool call.
|
|
1774
1832
|
const cur = await this._readLastAnswerTextCleanDom().catch(() => '');
|
|
1775
1833
|
const fresh = !!this._netCapture && this._netCaptureAt >= this._lastSentAt;
|
|
1834
|
+
// The turn was truncated / produced no answer (generation_err,
|
|
1835
|
+
// INCOMPLETE, or FINISHED-with-only-reasoning): the message WAS sent,
|
|
1836
|
+
// the server just cut the turn. Resend (ask() handles it) instead of
|
|
1837
|
+
// treating this as "send failed" and hammering Enter.
|
|
1838
|
+
if (this._netCaptureAt >= this._lastSentAt &&
|
|
1839
|
+
(isGenerationIncompleteText(this._netBody) || this._netNoAnswer)) {
|
|
1840
|
+
throw new GenerationIncompleteError(this._netBody.slice(-300));
|
|
1841
|
+
}
|
|
1776
1842
|
if (cur &&
|
|
1777
1843
|
cur.trim() &&
|
|
1778
1844
|
normText(cur) !== normText(beforeText) &&
|
|
@@ -1843,10 +1909,25 @@ export class DeepSeekBrowser {
|
|
|
1843
1909
|
// keeps the partial answer, so without this check the loop waits out the
|
|
1844
1910
|
// whole timeout and throws ds.send_no_new_answer — the operator sees the
|
|
1845
1911
|
// agent "stop" with a Continue button in the chat. Retry the send.
|
|
1846
|
-
|
|
1912
|
+
// Runs on the RAW `_netBody` (a truncated body has no RESPONSE, so
|
|
1913
|
+
// `_netCapture` is empty and the old guard was dead code).
|
|
1914
|
+
if (this._netCaptureAt >= this._lastSentAt &&
|
|
1915
|
+
isGenerationIncompleteText(this._netBody)) {
|
|
1916
|
+
throw new GenerationIncompleteError(this._netBody.slice(-300));
|
|
1917
|
+
}
|
|
1918
|
+
// A FINISHED turn with NO answer (only reasoning): the UI shows a
|
|
1919
|
+
// Continue button. Click it and keep waiting — otherwise the loop spins
|
|
1920
|
+
// to the deadline on an empty answer and the operator sees "Stopped".
|
|
1921
|
+
if (this._netNoAnswer &&
|
|
1847
1922
|
this._netCaptureAt >= this._lastSentAt &&
|
|
1848
|
-
|
|
1849
|
-
|
|
1923
|
+
this.autoContinue) {
|
|
1924
|
+
if (await this._clickContinueIfVisible()) {
|
|
1925
|
+
this._askDebug('CLICKED Continue (net-no-answer, finish-loop)');
|
|
1926
|
+
this._netNoAnswer = false;
|
|
1927
|
+
this._netCaptureAt = 0;
|
|
1928
|
+
await this.page.waitForTimeout(500);
|
|
1929
|
+
continue;
|
|
1930
|
+
}
|
|
1850
1931
|
}
|
|
1851
1932
|
// Reasoning-mode pause: DeepSeek caps the THINK phase and shows a
|
|
1852
1933
|
// Continue button; the model does not resume by itself. Click it so the
|
|
@@ -1917,6 +1998,16 @@ export class DeepSeekBrowser {
|
|
|
1917
1998
|
netFresh);
|
|
1918
1999
|
await this.page.waitForTimeout(Math.max(0, this.stabilityDelayMs));
|
|
1919
2000
|
}
|
|
2001
|
+
// A FINISHED turn with NO answer text (only reasoning) must NOT be
|
|
2002
|
+
// returned as a final answer, and must not hang until the timeout either.
|
|
2003
|
+
// Treat it like a truncated turn: ask() resends the prompt (the same thing
|
|
2004
|
+
// the Continue button does). Only if a real capture arrived in the
|
|
2005
|
+
// meantime do we fall through to return it.
|
|
2006
|
+
if (this._netNoAnswer &&
|
|
2007
|
+
this._netCaptureAt >= this._lastSentAt &&
|
|
2008
|
+
!this._netCapture) {
|
|
2009
|
+
throw new GenerationIncompleteError('finished without answer');
|
|
2010
|
+
}
|
|
1920
2011
|
if (last && (normText(last) !== normText(beforeText) || this._netCapture)) {
|
|
1921
2012
|
// If Continue is STILL on screen, the turn is paused, not finished:
|
|
1922
2013
|
// click once more and report what we have rather than silently stopping.
|
package/dist/net-capture.js
CHANGED
|
@@ -159,6 +159,32 @@ export function extractAnswer(body) {
|
|
|
159
159
|
return sse;
|
|
160
160
|
return extractFromJson(body);
|
|
161
161
|
}
|
|
162
|
+
// True when the SSE body shows a turn that ENDED (`quasi_status: FINISHED`)
|
|
163
|
+
// but produced NO answer text — the reasoning (THINK fragment) was generated
|
|
164
|
+
// and then the model stopped WITHOUT writing a RESPONSE fragment.
|
|
165
|
+
//
|
|
166
|
+
// This is the "Stopped + Continue button" case: DeepSeek finishes the turn
|
|
167
|
+
// with only reasoning, the UI shows a Continue button, and extractAnswer()
|
|
168
|
+
// returns '' (it only collects RESPONSE fragments). The old code then saw an
|
|
169
|
+
// empty capture, ignored it, and the finish loop waited out the whole timeout
|
|
170
|
+
// and threw ds.send_no_new_answer — to the operator the agent "hung on Stopped".
|
|
171
|
+
// Unlike generation_err, `quasi_status` here is FINISHED, so
|
|
172
|
+
// isGenerationIncompleteText() does NOT catch it.
|
|
173
|
+
//
|
|
174
|
+
export function isFinishedWithoutAnswer(body) {
|
|
175
|
+
const b = String(body || '');
|
|
176
|
+
if (!b)
|
|
177
|
+
return false;
|
|
178
|
+
const finished = /"quasi_status","v":"FINISHED"/i.test(b);
|
|
179
|
+
if (!finished)
|
|
180
|
+
return false;
|
|
181
|
+
// A RESPONSE fragment with actual content means the model DID answer.
|
|
182
|
+
if (/"type":"RESPONSE","content":"[^"]/.test(b))
|
|
183
|
+
return false;
|
|
184
|
+
// The turn carried reasoning (THINK) or was truncated mid-way; treat the
|
|
185
|
+
// absence of response content as "no answer produced".
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
162
188
|
// The CONTEXT size (in tokens) DeepSeek reports for the current answer.
|
|
163
189
|
//
|
|
164
190
|
// chat.deepseek.com does not expose prompt_tokens/completion_tokens the way
|