zames_pro 2.23.0 → 2.25.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 +33 -1
- package/dist/browser.js +14 -18
- package/dist/index.js +8 -0
- package/dist/input.js +17 -4
- package/dist/spinner.js +8 -3
- package/dist/tools.js +9 -3
- package/package.json +1 -1
package/dist/agent-loop.js
CHANGED
|
@@ -35,6 +35,16 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
35
35
|
const safeToolResult = safe(onToolResult);
|
|
36
36
|
const safeAssistantMessage = safe(onAssistantMessage);
|
|
37
37
|
const safeWarning = safe(onWarning);
|
|
38
|
+
// One AbortController per task. A tool gets `toolAbort.signal`; the loop
|
|
39
|
+
// aborts it as soon as the operator presses Esc/Ctrl+C (browser._abort /
|
|
40
|
+
// _stopped). This is what actually kills a running `npm test` instead of
|
|
41
|
+
// just abandoning it.
|
|
42
|
+
const toolAbort = new AbortController();
|
|
43
|
+
const syncToolAbort = () => {
|
|
44
|
+
if ((browser._abort || browser._stopped) && !toolAbort.signal.aborted) {
|
|
45
|
+
toolAbort.abort();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
38
48
|
if (freshChat) {
|
|
39
49
|
await browser.newChat();
|
|
40
50
|
transcript?.log('new_chat');
|
|
@@ -546,21 +556,43 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
546
556
|
results.push({ tool: call.tool, result: err });
|
|
547
557
|
continue;
|
|
548
558
|
}
|
|
559
|
+
// The operator may have pressed Esc while we were parsing/among the
|
|
560
|
+
// previous calls — make sure the signal reflects it before we start.
|
|
561
|
+
syncToolAbort();
|
|
549
562
|
safeToolCall(call.tool, call.args);
|
|
550
563
|
transcript?.log('tool_call', { tool: call.tool, args: call.args });
|
|
551
564
|
let result;
|
|
565
|
+
// While the tool runs, poll for an Esc/Ctrl+C: the abort flag is a plain
|
|
566
|
+
// boolean set by stopGeneration(), so the only way to turn it into a
|
|
567
|
+
// child-process kill is to check it periodically. 100ms is cheap and
|
|
568
|
+
// makes Esc feel immediate.
|
|
569
|
+
const poll = setInterval(syncToolAbort, 100);
|
|
570
|
+
if (typeof poll.unref === 'function')
|
|
571
|
+
poll.unref();
|
|
552
572
|
try {
|
|
553
|
-
result = await tool.fn(call.args);
|
|
573
|
+
result = await tool.fn(call.args, { signal: toolAbort.signal });
|
|
554
574
|
}
|
|
555
575
|
catch (e) {
|
|
556
576
|
result = `Error: ${e.message}`;
|
|
557
577
|
}
|
|
578
|
+
finally {
|
|
579
|
+
clearInterval(poll);
|
|
580
|
+
}
|
|
558
581
|
safeToolResult(result);
|
|
559
582
|
transcript?.log('tool_result', {
|
|
560
583
|
tool: call.tool,
|
|
561
584
|
result: String(result),
|
|
562
585
|
});
|
|
563
586
|
results.push({ tool: call.tool, result });
|
|
587
|
+
// The operator pressed Esc/Ctrl+C while the tool was running. The tool
|
|
588
|
+
// itself has finished (we cannot kill an arbitrary child process from
|
|
589
|
+
// here), but we must NOT feed its result back to the model and must NOT
|
|
590
|
+
// run the remaining calls in this batch — that would keep the agent
|
|
591
|
+
// going after an explicit stop.
|
|
592
|
+
if (browser._abort || browser._stopped) {
|
|
593
|
+
transcript?.log('user_aborted');
|
|
594
|
+
return '(прервано пользователем)';
|
|
595
|
+
}
|
|
564
596
|
}
|
|
565
597
|
// A tool just ran — the next answer is expected to be a fresh tool call.
|
|
566
598
|
// Reset the watchdog so the next empty/repeated answer is nudged.
|
package/dist/browser.js
CHANGED
|
@@ -3,7 +3,6 @@ import { extractAnswer, extractTokenUsage, dumpNetBody } from './net-capture.js'
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import os from 'os';
|
|
5
5
|
import fs from 'fs/promises';
|
|
6
|
-
import { execSync } from 'child_process';
|
|
7
6
|
import { theme } from './theme.js';
|
|
8
7
|
import { translate, DEFAULT_LOCALE } from './i18n.js';
|
|
9
8
|
// Muted terminal input for passwords. We do NOT use readline here: readline
|
|
@@ -212,15 +211,6 @@ export class ServerBusyError extends Error {
|
|
|
212
211
|
}
|
|
213
212
|
}
|
|
214
213
|
// ---------- profile cleanup ----------
|
|
215
|
-
async function killStaleChrome() {
|
|
216
|
-
if (process.platform !== 'win32')
|
|
217
|
-
return;
|
|
218
|
-
try {
|
|
219
|
-
execSync(`powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"Name='chrome.exe'\\" | Where-Object { $_.CommandLine -like '*\\.zames\\profile*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"`, { stdio: 'ignore', timeout: 5000 });
|
|
220
|
-
await new Promise((r) => setTimeout(r, 800));
|
|
221
|
-
}
|
|
222
|
-
catch { }
|
|
223
|
-
}
|
|
224
214
|
async function cleanSingletonFiles() {
|
|
225
215
|
const names = ['SingletonLock', 'SingletonCookie', 'SingletonSocket'];
|
|
226
216
|
for (const name of names) {
|
|
@@ -361,7 +351,11 @@ export class DeepSeekBrowser {
|
|
|
361
351
|
return translate(this.locale)(key, params);
|
|
362
352
|
}
|
|
363
353
|
async launch() {
|
|
364
|
-
|
|
354
|
+
// NOTE: we deliberately do NOT kill stray chrome processes here. Per the
|
|
355
|
+
// project rules, an agent must never kill a browser it did not start —
|
|
356
|
+
// another zames instance may be using the same profile. A locked profile
|
|
357
|
+
// is handled by removing the Singleton files (below) and, if it is still
|
|
358
|
+
// busy, by a clear error in _launchOnce().
|
|
365
359
|
if (await profileLooksLocked()) {
|
|
366
360
|
if (this.debug)
|
|
367
361
|
console.error('profile: удаляю Singleton-файлы');
|
|
@@ -418,7 +412,10 @@ export class DeepSeekBrowser {
|
|
|
418
412
|
if (/Target page, context or browser has been closed|profile.*in use/i.test(e.message)) {
|
|
419
413
|
if (this.debug)
|
|
420
414
|
console.error('profile: занят, перезапускаю после очистки');
|
|
421
|
-
|
|
415
|
+
// Do NOT kill a foreign chrome (see AGENTS.md): only clear the
|
|
416
|
+
// Singleton files and retry. If the profile is genuinely in use by a
|
|
417
|
+
// live browser, nukeProfile() below fails safely and the error
|
|
418
|
+
// surfaces instead of silently killing someone else's session.
|
|
422
419
|
await cleanSingletonFiles();
|
|
423
420
|
try {
|
|
424
421
|
this.context = await chromium.launchPersistentContext(USER_DATA_DIR, options);
|
|
@@ -509,11 +506,11 @@ export class DeepSeekBrowser {
|
|
|
509
506
|
await this.context.close();
|
|
510
507
|
}
|
|
511
508
|
catch { }
|
|
512
|
-
// A restart must REUSE the existing profile. `launch()` would
|
|
513
|
-
//
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
-
//
|
|
509
|
+
// A restart must REUSE the existing profile. `launch()` would clean
|
|
510
|
+
// Singleton files, which can race with a sibling agent on the same
|
|
511
|
+
// profile; here we just relaunch the persistent context in place. A fresh
|
|
512
|
+
// window appears only because the old context was actually closed — no
|
|
513
|
+
// second window is created while one is alive.
|
|
517
514
|
await this._launchOnce();
|
|
518
515
|
await this._fixHeadlessUserAgent();
|
|
519
516
|
}
|
|
@@ -2033,6 +2030,5 @@ export class DeepSeekBrowser {
|
|
|
2033
2030
|
await this.context.close();
|
|
2034
2031
|
}
|
|
2035
2032
|
catch { }
|
|
2036
|
-
await killStaleChrome();
|
|
2037
2033
|
}
|
|
2038
2034
|
}
|
package/dist/index.js
CHANGED
|
@@ -871,6 +871,12 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
871
871
|
browser._stopped = false;
|
|
872
872
|
browser._abort = false;
|
|
873
873
|
const ui = editor || mod.createSpinner(currentLocale);
|
|
874
|
+
// Mark the editor busy for the WHOLE task, not only for /init and /self-fix.
|
|
875
|
+
// Esc / Ctrl+C abort the current generation only while busy; without this a
|
|
876
|
+
// long-running tool (Bash, npm, MCP) could not be interrupted — Esc did
|
|
877
|
+
// nothing. Cleared in the finally below.
|
|
878
|
+
if (editor)
|
|
879
|
+
editor.busy = true;
|
|
874
880
|
const stopWatching = editor
|
|
875
881
|
? () => { }
|
|
876
882
|
: watchInput({
|
|
@@ -971,6 +977,8 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
971
977
|
finally {
|
|
972
978
|
stopWatching();
|
|
973
979
|
ui.stop();
|
|
980
|
+
if (editor)
|
|
981
|
+
editor.busy = false;
|
|
974
982
|
// Detach the send hooks so a later browser.ask() outside this task cannot
|
|
975
983
|
// start a stale spinner.
|
|
976
984
|
browser.onSendStart = null;
|
package/dist/input.js
CHANGED
|
@@ -221,9 +221,9 @@ export function formatTokenStatus(tokens, limit = CONTEXT_LIMIT) {
|
|
|
221
221
|
}
|
|
222
222
|
const pct = Math.max(0, (n / limit) * 100);
|
|
223
223
|
const pctStr = pct >= 10 ? String(Math.round(pct)) : pct.toFixed(1);
|
|
224
|
-
// "ctx" marks this as the USED context size, so the operator does not have
|
|
224
|
+
// "ctx:" marks this as the USED context size, so the operator does not have
|
|
225
225
|
// to guess what "302k · 30%" means.
|
|
226
|
-
return 'ctx ' + compact + ' · ' + pctStr + '%';
|
|
226
|
+
return 'ctx: ' + compact + ' · ' + pctStr + '%';
|
|
227
227
|
}
|
|
228
228
|
export class LineEditor {
|
|
229
229
|
promptStr;
|
|
@@ -242,6 +242,9 @@ export class LineEditor {
|
|
|
242
242
|
// True while the dot-animation timer is running. Lets sendPause() update
|
|
243
243
|
// only the base text (remaining seconds) without restarting the timer.
|
|
244
244
|
_animating;
|
|
245
|
+
// Whether the animated status shows the trailing "Esc — стоп" hint. Off
|
|
246
|
+
// while a tool runs (its process cannot be cancelled).
|
|
247
|
+
_showHint;
|
|
245
248
|
_dotPhase;
|
|
246
249
|
_thinkBase;
|
|
247
250
|
// The last rendered status row(s) (status + right-aligned context). Cached so
|
|
@@ -305,6 +308,7 @@ export class LineEditor {
|
|
|
305
308
|
this._inPaste = false;
|
|
306
309
|
this._dotTimer = null;
|
|
307
310
|
this._animating = false;
|
|
311
|
+
this._showHint = true;
|
|
308
312
|
this._dotPhase = 0;
|
|
309
313
|
this._thinkBase = '';
|
|
310
314
|
this._lastStatusBlock = '';
|
|
@@ -699,6 +703,8 @@ export class LineEditor {
|
|
|
699
703
|
}
|
|
700
704
|
// Status-line hint ("Esc — stop"), localized.
|
|
701
705
|
_hint() {
|
|
706
|
+
if (!this._showHint)
|
|
707
|
+
return '';
|
|
702
708
|
return theme.dim(' · ' + translate(this.locale)('spinner.hint'));
|
|
703
709
|
}
|
|
704
710
|
_startThinking() {
|
|
@@ -712,8 +718,13 @@ export class LineEditor {
|
|
|
712
718
|
// Shared by the thinking spinner and the send-pause indicator, so the pause
|
|
713
719
|
// is animated too (previously the pause was a static console line and the
|
|
714
720
|
// dots stayed frozen).
|
|
715
|
-
|
|
721
|
+
//
|
|
722
|
+
// `showHint` (default true) controls the trailing "· Esc — стоп". While a
|
|
723
|
+
// TOOL runs we hide it: the tool's child process cannot be killed, so
|
|
724
|
+
// promising "Esc to stop" would be misleading.
|
|
725
|
+
_startAnimated(baseText, showHint = true) {
|
|
716
726
|
this._thinkBase = theme.brown(stripEllipsis(baseText));
|
|
727
|
+
this._showHint = showHint;
|
|
717
728
|
this._dotPhase = 0;
|
|
718
729
|
this.setStatus(this._thinkBase + this._dots(0) + this._hint());
|
|
719
730
|
this._stopDots();
|
|
@@ -764,7 +775,9 @@ export class LineEditor {
|
|
|
764
775
|
// A tool may run for a long time (Bash, npm test, MCP). Without an active
|
|
765
776
|
// animation the operator sees a frozen screen and cannot tell work is in
|
|
766
777
|
// progress. Start the animated status AFTER printAbove (which stops the
|
|
767
|
-
// dots) and keep it running until toolResult()/assistant()/stop().
|
|
778
|
+
// dots) and keep it running until toolResult()/assistant()/stop(). Esc now
|
|
779
|
+
// really aborts the tool (the Bash child process is killed via the
|
|
780
|
+
// AbortSignal), so the "Esc — стоп" hint is shown.
|
|
768
781
|
this._startAnimated(translate(this.locale)('spinner.running_tool', { name }));
|
|
769
782
|
}
|
|
770
783
|
toolResult(result) {
|
package/dist/spinner.js
CHANGED
|
@@ -52,12 +52,17 @@ export function createSpinner(locale = 'ru') {
|
|
|
52
52
|
// "running" dot sequence. Shared by the thinking spinner and the send-pause
|
|
53
53
|
// indicator, so the pause is animated too (before, it was a static line and
|
|
54
54
|
// the dots did not move — the operator saw a frozen spinner).
|
|
55
|
-
|
|
55
|
+
//
|
|
56
|
+
// `showHint` (default true) controls the trailing "· Esc — стоп". While a
|
|
57
|
+
// tool runs we hide it: its process cannot be cancelled, so promising a stop
|
|
58
|
+
// would be misleading.
|
|
59
|
+
const startAnimated = (baseText, showHint = true) => {
|
|
56
60
|
animBase = theme.brown(stripEllipsis(baseText));
|
|
61
|
+
const hint = showHint ? HINT : '';
|
|
57
62
|
// The dots are the same color as the base and of fixed width — otherwise
|
|
58
63
|
// the hint on the right "jumps" when the animation phase changes.
|
|
59
64
|
const dots = (n) => theme.brown(DOTS[n] + DOTS_PAD.slice(DOTS[n].length));
|
|
60
|
-
start(animBase + dots(0) +
|
|
65
|
+
start(animBase + dots(0) + hint);
|
|
61
66
|
dotPhase = 0;
|
|
62
67
|
if (dotTimer)
|
|
63
68
|
clearInterval(dotTimer);
|
|
@@ -65,7 +70,7 @@ export function createSpinner(locale = 'ru') {
|
|
|
65
70
|
if (!spinner)
|
|
66
71
|
return;
|
|
67
72
|
dotPhase = (dotPhase + 1) % DOTS.length;
|
|
68
|
-
spinner.text = animBase + dots(dotPhase) +
|
|
73
|
+
spinner.text = animBase + dots(dotPhase) + hint;
|
|
69
74
|
}, 400);
|
|
70
75
|
animating = true;
|
|
71
76
|
if (dotTimer.unref)
|
package/dist/tools.js
CHANGED
|
@@ -37,13 +37,16 @@ export function createTools(workdir, { undo } = {}) {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
};
|
|
40
|
-
const runShell = (command, timeout = 30_000) => new Promise((resolve) => {
|
|
40
|
+
const runShell = (command, timeout = 30_000, signal) => new Promise((resolve) => {
|
|
41
41
|
const options = {
|
|
42
42
|
cwd: workdir,
|
|
43
43
|
timeout,
|
|
44
44
|
maxBuffer: 1024 * 1024 * 8,
|
|
45
45
|
windowsHide: true,
|
|
46
46
|
env: { ...process.env },
|
|
47
|
+
// Abort the child process when the operator pressed Esc/Ctrl+C.
|
|
48
|
+
// Without it a long `npm test` kept running even after the stop.
|
|
49
|
+
signal,
|
|
47
50
|
};
|
|
48
51
|
if (process.platform === 'win32') {
|
|
49
52
|
options.shell = process.env.ComSpec || 'C:\\Windows\\System32\\cmd.exe';
|
|
@@ -63,6 +66,9 @@ export function createTools(workdir, { undo } = {}) {
|
|
|
63
66
|
if (err.killed) {
|
|
64
67
|
parts.push(`⏱ Timeout after ${timeout}ms — process killed.`);
|
|
65
68
|
}
|
|
69
|
+
else if (err.code === 'ABORT_ERR') {
|
|
70
|
+
parts.push('(aborted by the operator)');
|
|
71
|
+
}
|
|
66
72
|
else if (err.code !== undefined && err.code !== null) {
|
|
67
73
|
parts.push(`Exit code: ${err.code}`);
|
|
68
74
|
}
|
|
@@ -195,9 +201,9 @@ export function createTools(workdir, { undo } = {}) {
|
|
|
195
201
|
'Do not use for commands that require interactive input. ' +
|
|
196
202
|
'For git use the Git* tools. For the web use WebFetch / WebSearch.',
|
|
197
203
|
parameters: { command: 'string', timeout: 'number?' },
|
|
198
|
-
fn: async ({ command, timeout }) => {
|
|
204
|
+
fn: async ({ command, timeout }, ctx) => {
|
|
199
205
|
assertCommandInsideRoot(req(command, 'command'));
|
|
200
|
-
return runShell(req(command, 'command'), timeout);
|
|
206
|
+
return runShell(req(command, 'command'), timeout, ctx?.signal);
|
|
201
207
|
},
|
|
202
208
|
},
|
|
203
209
|
{
|