zames_pro 2.89.0 → 2.90.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/CHANGELOG.md +47 -0
- package/README.md +32 -585
- package/dist/agent-loop.js +47 -4
- package/dist/ask-retry.js +49 -0
- package/dist/ask-state.js +80 -0
- package/dist/browser-base.js +69 -0
- package/dist/browser.js +170 -41
- package/dist/command-registry.js +58 -0
- package/dist/commands.js +25 -2
- package/dist/completion-check.js +77 -0
- package/dist/config-schema.js +8 -0
- package/dist/config.js +14 -0
- package/dist/dev-commands.js +27 -0
- package/dist/dev-watch.js +96 -0
- package/dist/first-run.js +80 -0
- package/dist/glm-browser.js +133 -32
- package/dist/i18n/common.js +6 -3
- package/dist/i18n/config.js +16 -0
- package/dist/i18n/wizard.js +25 -0
- package/dist/i18n.js +6 -1
- package/dist/index.js +1289 -919
- package/dist/info-commands.js +61 -0
- package/dist/input/layout.js +38 -1
- package/dist/input.js +2 -2
- package/dist/password-store.js +48 -0
- package/dist/prompt-budget.js +23 -0
- package/dist/provider-errors.js +53 -0
- package/dist/provider-fallback.js +46 -0
- package/dist/qwen-browser.js +155 -39
- package/dist/run-task.js +2 -1
- package/dist/scheduler-commands.js +41 -0
- package/dist/send-slot.js +36 -0
- package/dist/session-commands.js +42 -0
- package/dist/spinner.js +3 -3
- package/dist/swallow.js +45 -0
- package/dist/system-prompt.js +2 -2
- package/dist/tree-commands.js +36 -0
- package/dist/tui/FullScreenApp.js +13 -4
- package/dist/tui/InputLine.js +20 -3
- package/dist/tui/LogView.js +11 -5
- package/dist/tui/StatusBar.js +4 -1
- package/dist/tui/TuiApp.js +11 -2
- package/dist/tui/layout.js +14 -1
- package/dist/tui/store.js +2 -1
- package/dist/tui/ui-colors.js +2 -0
- package/dist/width.js +10 -5
- package/llms-full.txt +16 -292
- package/package.json +2 -2
package/dist/agent-loop.js
CHANGED
|
@@ -3,12 +3,16 @@ import { isContextLimitText } from './browser-pure.js';
|
|
|
3
3
|
import { providerLabel as providerHumanLabel } from './providers.js';
|
|
4
4
|
import { loadProjectContext } from './context.js';
|
|
5
5
|
import { getGitContext, formatGitContext } from './gitTools.js';
|
|
6
|
-
import { translate } from './i18n.js';
|
|
6
|
+
import { translate, DEFAULT_LOCALE } from './i18n.js';
|
|
7
7
|
import { substituteAttachmentMarkers } from './commands.js';
|
|
8
|
+
import { needsCompletionNudge, COMPLETION_NUDGE, } from './completion-check.js';
|
|
8
9
|
import { loadHooks, runPreToolUse, runPostToolUse, runLifecycleHooks, } from './hooks.js';
|
|
9
10
|
import { loadPermissions, decidePermission, } from './permissions.js';
|
|
10
11
|
import { shouldRunDiagnostics, runDiagnostics, } from './diagnostics.js';
|
|
11
|
-
|
|
12
|
+
// The tools that WRITE/EDIT a file (and take a `path`). The completion nudge
|
|
13
|
+
// only cares about these — `Bash`/`Git*` in MUTATING_TOOLS are not edits.
|
|
14
|
+
const EDIT_TOOLS = new Set(['Write', 'Edit', 'MultiEdit', 'ApplyPatch']);
|
|
15
|
+
export async function runAgentLoop({ browser, tools, task, workdir, maxIterations = 0, freshChat = false, sendSystemPrompt = false, transcript = null, attachments = [], onThinking = () => { }, onSendPause = () => { }, onSendState = () => { }, onNotice = () => { }, onAssistantThought = () => { }, onToolCall = () => { }, onToolResult = () => { }, onAssistantMessage = () => { }, onChatReady = () => { }, onWarning = () => { }, debugLog = false, locale = DEFAULT_LOCALE, askDeadlineMs = 240_000, maxAfterToolRetries = 6, onAutoCompact = null, onContextLimit = null, onProviderExhausted = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, hooks = undefined, permissions = undefined, diagnostics = null, onAskPermission = undefined, selfImprovement = false, onSubagent = null, onAskUser = null, }) {
|
|
12
16
|
// Resolve the hook config ONCE per task: a read per tool call would be
|
|
13
17
|
// wasteful, and a mid-task edit of hooks.json is not something to chase.
|
|
14
18
|
// `undefined` means "read .zames/hooks.json"; an explicit null disables
|
|
@@ -58,6 +62,10 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
58
62
|
;
|
|
59
63
|
browser.onContextLimit = onContextLimit;
|
|
60
64
|
}
|
|
65
|
+
if ('onProviderExhausted' in browser) {
|
|
66
|
+
;
|
|
67
|
+
browser.onProviderExhausted = onProviderExhausted;
|
|
68
|
+
}
|
|
61
69
|
const safeAssistantThought = safe(onAssistantThought);
|
|
62
70
|
const safeToolCall = safe(onToolCall);
|
|
63
71
|
const safeToolResult = safe(onToolResult);
|
|
@@ -79,8 +87,18 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
79
87
|
}
|
|
80
88
|
};
|
|
81
89
|
if (freshChat) {
|
|
82
|
-
|
|
83
|
-
|
|
90
|
+
// A RESUMED chat must win over "fresh": the caller marks a chat fresh only
|
|
91
|
+
// when it has no live id (a page restart dropped it). Sending into newChat()
|
|
92
|
+
// there opened a SECOND chat and the operator saw the dialogue vanish —
|
|
93
|
+
// the very regression this branch prevents. A provider that cannot navigate
|
|
94
|
+
// simply ignores the id.
|
|
95
|
+
const resumeId = browser.resumeChatId || null;
|
|
96
|
+
const reopened = resumeId && browser.openChat
|
|
97
|
+
? await browser.openChat(resumeId).catch(() => false)
|
|
98
|
+
: false;
|
|
99
|
+
if (!reopened)
|
|
100
|
+
await browser.newChat();
|
|
101
|
+
transcript?.log(reopened ? 'reopen_chat' : 'new_chat', resumeId ? { id: resumeId } : undefined);
|
|
84
102
|
// SessionStart hook: a fresh chat is the start of a session, so a
|
|
85
103
|
// hook can inject context or set up state. Best-effort, output is logged.
|
|
86
104
|
try {
|
|
@@ -268,6 +286,11 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
268
286
|
// paragraph that is neither a tool call nor a real respond) from a genuine
|
|
269
287
|
// short answer. We key on the STRUCTURE (work already started), not words.
|
|
270
288
|
let toolsRanInTask = 0;
|
|
289
|
+
// Ordered history of the tool calls in this task — feeds the post-edit
|
|
290
|
+
// "did you verify the artifact?" nudge (src/completion-check.ts).
|
|
291
|
+
const runSteps = [];
|
|
292
|
+
// The nudge is sent at most ONCE per task: it is a hint, not a guard.
|
|
293
|
+
let completionNudgeSent = false;
|
|
271
294
|
const MAX_AFTER_TOOL_RETRIES = Math.max(0, Math.floor(maxAfterToolRetries));
|
|
272
295
|
// One-time hint when the context nears full while auto-compact is OFF:
|
|
273
296
|
// the operator is told about /compact instead of silently degrading. Set
|
|
@@ -745,6 +768,16 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
745
768
|
' chat manually.');
|
|
746
769
|
return 'The model produced no final message (empty respond).';
|
|
747
770
|
}
|
|
771
|
+
// A runnable code file was written/edited and NOTHING ran since: the
|
|
772
|
+
// model may be finishing on an INCOMPLETE artifact (a real case: a page
|
|
773
|
+
// with no game loop, reported DONE). Ask ONCE to verify it; the model
|
|
774
|
+
// can still finish with a confirmed respond.
|
|
775
|
+
if (!completionNudgeSent && needsCompletionNudge(runSteps)) {
|
|
776
|
+
completionNudgeSent = true;
|
|
777
|
+
transcript?.log('completion_nudge', { steps: runSteps.length });
|
|
778
|
+
message = COMPLETION_NUDGE;
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
748
781
|
safeAssistantMessage(msg);
|
|
749
782
|
transcript?.log('assistant_final', { message: msg });
|
|
750
783
|
return msg;
|
|
@@ -1159,6 +1192,16 @@ ${diag}`;
|
|
|
1159
1192
|
// Reset the watchdog so the next empty/repeated answer is nudged.
|
|
1160
1193
|
justRanTool = true;
|
|
1161
1194
|
toolsRanInTask++;
|
|
1195
|
+
// Remember the calls (and whether they mutated a code file) for the
|
|
1196
|
+
// post-edit completion nudge below.
|
|
1197
|
+
for (const r of results) {
|
|
1198
|
+
const callArgs = callsToRun.find((c) => c.tool === r.tool)?.args;
|
|
1199
|
+
runSteps.push({
|
|
1200
|
+
tool: r.tool,
|
|
1201
|
+
mutating: EDIT_TOOLS.has(r.tool),
|
|
1202
|
+
path: typeof callArgs?.path === 'string' ? callArgs.path : undefined,
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1162
1205
|
// The tool really executed on this turn. The NEXT iteration will treat
|
|
1163
1206
|
// a repeat of this answer as stale only because of this flag (see NEXT,
|
|
1164
1207
|
// not the current, justRanTool check).
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// N1 stage 4: the retry POLICY of a provider's `ask()` as a PURE function.
|
|
2
|
+
//
|
|
3
|
+
// The three providers ran the same loop — call `_askOnce`, classify the thrown
|
|
4
|
+
// error into a family (rate limit / server busy / incomplete / context limit /
|
|
5
|
+
// provider-specific fatal), retry that family within its own budget, otherwise
|
|
6
|
+
// fall through to a generic attempt counter — and each copy had its own order
|
|
7
|
+
// of checks and its own wording. The loop itself (sends, throttling, aborts)
|
|
8
|
+
// stays in the browser class; only the DECISION is here, so every branch is
|
|
9
|
+
// unit-tested without a browser.
|
|
10
|
+
/**
|
|
11
|
+
* Decide what to do after a failed attempt.
|
|
12
|
+
*
|
|
13
|
+
* `classify` maps the thrown error to a family; it lives in the provider
|
|
14
|
+
* because the backend-specific markers (guest limit, age gate, captcha) are
|
|
15
|
+
* provider knowledge. A `null` classification falls through to the generic
|
|
16
|
+
* attempt budget.
|
|
17
|
+
*/
|
|
18
|
+
export function decideRetry(family, state, budgets, waits) {
|
|
19
|
+
switch (family) {
|
|
20
|
+
case 'fatal':
|
|
21
|
+
return { kind: 'give-up', family };
|
|
22
|
+
case 'context-limit':
|
|
23
|
+
if (state.contextLimitRetries >= budgets.maxContextLimitRetries) {
|
|
24
|
+
return { kind: 'give-up', family };
|
|
25
|
+
}
|
|
26
|
+
return { kind: 'compact', family };
|
|
27
|
+
case 'rate-limit':
|
|
28
|
+
if (state.rateLimitRetries >= budgets.maxRateLimitRetries) {
|
|
29
|
+
return { kind: 'give-up', family };
|
|
30
|
+
}
|
|
31
|
+
return { kind: 'wait', family, waitMs: waits.rateLimitMs };
|
|
32
|
+
case 'server-busy':
|
|
33
|
+
if (state.serverBusyRetries >= budgets.maxServerBusyRetries) {
|
|
34
|
+
return { kind: 'give-up', family };
|
|
35
|
+
}
|
|
36
|
+
return { kind: 'wait', family, waitMs: waits.serverBusyMs };
|
|
37
|
+
case 'incomplete':
|
|
38
|
+
if (state.incompleteRetries >= budgets.maxIncompleteRetries) {
|
|
39
|
+
return { kind: 'give-up', family };
|
|
40
|
+
}
|
|
41
|
+
return { kind: 'wait', family, waitMs: waits.incompleteMs };
|
|
42
|
+
default:
|
|
43
|
+
// A generic failure is retried while the TOTAL attempt budget lasts.
|
|
44
|
+
if (state.attempt >= budgets.askRetries - 1) {
|
|
45
|
+
return { kind: 'give-up', family: 'generic' };
|
|
46
|
+
}
|
|
47
|
+
return { kind: 'generic', waitMs: 2000 * (state.attempt + 1) };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// N3: the PHASE DECISION of DeepSeek's `_askOnce()` as a pure state machine.
|
|
2
|
+
//
|
|
3
|
+
// `_askOnce` (src/browser.ts) is the most fragile function of the project: a
|
|
4
|
+
// start-wait loop, a finish loop, the Continue button, the stability window
|
|
5
|
+
// and four detectors (rate limit / context limit / truncated / finished
|
|
6
|
+
// without answer). Until now it was covered only by live smoke runs, and a
|
|
7
|
+
// wrong ordering of the checks kept reappearing (the "Stopped" hang, the
|
|
8
|
+
// "agent stopped with a Continue button", the 15-minute spinner on a dead
|
|
9
|
+
// turn). The Playwright part keeps COLLECTING an observation; the TRANSITION
|
|
10
|
+
// below is pure and unit-tested against the real cases from DESIGN-NOTES.
|
|
11
|
+
/** Grace ticks before a FINISHED-with-answer turn is returned (default 2). */
|
|
12
|
+
export const NET_FINISHED_TICKS = 2;
|
|
13
|
+
/** Ticks a dead (FINISHED, no answer, no Continue) turn waits before giving up. */
|
|
14
|
+
export const DEAD_TURN_TICKS = 6;
|
|
15
|
+
/**
|
|
16
|
+
* One transition of the ask() state machine.
|
|
17
|
+
*
|
|
18
|
+
* ORDER MATTERS and is the whole point of this function:
|
|
19
|
+
* 1. an abort wins over everything (the operator stopped);
|
|
20
|
+
* 2. the CONTEXT limit is checked BEFORE the rate limit — both look like a
|
|
21
|
+
* toast, and retrying an exhausted chat is useless;
|
|
22
|
+
* 3. rate limit / server busy are retryable errors;
|
|
23
|
+
* 4. a truncated turn PREFERS the Continue button (it RESUMES the turn) and
|
|
24
|
+
* only falls back to a resend when there is no button;
|
|
25
|
+
* 5. a PAUSED generation (`continueVisible`) is never "settled": the
|
|
26
|
+
* stability early-return must be checked AFTER it, otherwise a partial
|
|
27
|
+
* answer is returned with Continue still on screen.
|
|
28
|
+
*/
|
|
29
|
+
export function nextPhase(phase, o, opts) {
|
|
30
|
+
if (o.aborted)
|
|
31
|
+
return { kind: 'return', text: 'dom' };
|
|
32
|
+
if (o.contextLimit)
|
|
33
|
+
return { kind: 'throw', error: 'context-limit' };
|
|
34
|
+
if (o.rateLimited)
|
|
35
|
+
return { kind: 'throw', error: 'rate-limit' };
|
|
36
|
+
if (o.serverBusy)
|
|
37
|
+
return { kind: 'throw', error: 'server-busy' };
|
|
38
|
+
// A truncated / answer-less turn: click Continue when possible (it resumes
|
|
39
|
+
// the SAME turn); otherwise resend.
|
|
40
|
+
if (o.truncated || o.noAnswer) {
|
|
41
|
+
if (opts.autoContinue && o.continueVisible) {
|
|
42
|
+
return { kind: 'click-continue' };
|
|
43
|
+
}
|
|
44
|
+
return { kind: 'throw', error: 'incomplete' };
|
|
45
|
+
}
|
|
46
|
+
if (phase === 'start') {
|
|
47
|
+
if (o.isNew)
|
|
48
|
+
return { kind: 'started' };
|
|
49
|
+
if (o.hasText && o.notGenerating && !o.continueVisible) {
|
|
50
|
+
return { kind: 'return', text: 'dom' };
|
|
51
|
+
}
|
|
52
|
+
// A reasoning pause during the THINK phase: click Continue so the answer
|
|
53
|
+
// starts flowing instead of waiting for the start deadline.
|
|
54
|
+
if (opts.autoContinue && opts.deepThinking && o.continueVisible) {
|
|
55
|
+
return { kind: 'click-continue' };
|
|
56
|
+
}
|
|
57
|
+
return { kind: 'wait' };
|
|
58
|
+
}
|
|
59
|
+
// --- finish loop ---------------------------------------------------------
|
|
60
|
+
// The SSE body is ground truth: FINISHED with an answer, after a short grace
|
|
61
|
+
// window, is complete.
|
|
62
|
+
if (o.finishedWithAnswer && opts.netFinishedTicks >= NET_FINISHED_TICKS) {
|
|
63
|
+
return { kind: 'return', text: 'net' };
|
|
64
|
+
}
|
|
65
|
+
// A dead turn (the server ended it with nothing and there is no Continue to
|
|
66
|
+
// resume): give up quickly so ask() resends instead of a long spinner.
|
|
67
|
+
if (opts.deadTicks >= DEAD_TURN_TICKS && !o.continueVisible) {
|
|
68
|
+
return { kind: 'throw', error: 'incomplete' };
|
|
69
|
+
}
|
|
70
|
+
// A PAUSED generation looks stable: check it BEFORE the stability return.
|
|
71
|
+
if (opts.autoContinue && opts.deepThinking && o.continueVisible) {
|
|
72
|
+
return { kind: 'click-continue' };
|
|
73
|
+
}
|
|
74
|
+
if (o.isNew && opts.stableReached) {
|
|
75
|
+
if (o.notGenerating)
|
|
76
|
+
return { kind: 'return', text: 'dom' };
|
|
77
|
+
return { kind: 'wait' };
|
|
78
|
+
}
|
|
79
|
+
return { kind: 'wait' };
|
|
80
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// N1 stage 1: the helpers that were byte-identical in DeepSeekBrowser,
|
|
2
|
+
// QwenBrowser and GlmBrowser. Composition, not inheritance: every provider
|
|
3
|
+
// class owns a page and calls these with it, so the provider-specific logic
|
|
4
|
+
// stays where it is and only the shared mechanics live here.
|
|
5
|
+
/** Best-effort: a page error while marking the tab title must never break a
|
|
6
|
+
* send loop, so every failure is swallowed. */
|
|
7
|
+
export async function setBusyTitle(page, busy, setInDom) {
|
|
8
|
+
try {
|
|
9
|
+
await page.evaluate(setInDom, busy);
|
|
10
|
+
}
|
|
11
|
+
catch { }
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The first selector that has a VISIBLE element. `timeout` is a per-selector
|
|
15
|
+
* budget, not a total one (the providers rely on that: a login form is tried
|
|
16
|
+
* with a long budget while optional probes use a short one).
|
|
17
|
+
*
|
|
18
|
+
* Two probe styles exist in the providers (`waitFor({state:'visible'})` in
|
|
19
|
+
* DeepSeek/Qwen and `count()` + `isVisible()` in GLM). `style: 'wait'` keeps
|
|
20
|
+
* the first (it also covers a late-mounting element); `style: 'check'` keeps
|
|
21
|
+
* the second (no waiting state, a cheap poll).
|
|
22
|
+
*/
|
|
23
|
+
export async function findVisible(page, selectors, timeout = 1000, style = 'wait') {
|
|
24
|
+
for (const sel of selectors) {
|
|
25
|
+
const loc = page.locator(sel).last();
|
|
26
|
+
if (style === 'wait') {
|
|
27
|
+
try {
|
|
28
|
+
await loc.waitFor({ state: 'visible', timeout });
|
|
29
|
+
return loc;
|
|
30
|
+
}
|
|
31
|
+
catch { }
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
if ((await loc.count()) === 0)
|
|
36
|
+
continue;
|
|
37
|
+
if (await loc.isVisible({ timeout }).catch(() => false))
|
|
38
|
+
return loc;
|
|
39
|
+
}
|
|
40
|
+
catch { }
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Sleep in 100 ms chunks so Esc and the ask() watchdog stay responsive.
|
|
46
|
+
* Returns true when the wait was INTERRUPTED (abort / cancelled ask), so the
|
|
47
|
+
* caller can stop early. `onTick` fires about once a second with the time
|
|
48
|
+
* left — that is what animates the throttle countdown.
|
|
49
|
+
*/
|
|
50
|
+
export async function sleepInterruptible(page, ms, isInterrupted, onTick) {
|
|
51
|
+
const step = 100;
|
|
52
|
+
let left = ms;
|
|
53
|
+
let sinceTick = 0;
|
|
54
|
+
while (left > 0) {
|
|
55
|
+
if (isInterrupted())
|
|
56
|
+
return true;
|
|
57
|
+
const chunk = Math.min(step, left);
|
|
58
|
+
await page.waitForTimeout(chunk);
|
|
59
|
+
left -= chunk;
|
|
60
|
+
if (onTick) {
|
|
61
|
+
sinceTick += chunk;
|
|
62
|
+
if (sinceTick >= 1000) {
|
|
63
|
+
sinceTick = 0;
|
|
64
|
+
onTick(left);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return isInterrupted();
|
|
69
|
+
}
|