zames_pro 2.80.5 → 2.81.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 +56 -0
- package/README.md +79 -21
- package/dist/browser-login.js +177 -0
- package/dist/browser-pure.js +112 -8
- package/dist/browser.js +274 -16
- package/dist/commands.js +24 -8
- package/dist/config.js +106 -2
- package/dist/glm-browser.js +1766 -0
- package/dist/glm-chats.js +172 -0
- package/dist/glm-flags.js +75 -0
- package/dist/glm-net-capture.js +338 -0
- package/dist/glm-ui.js +171 -0
- package/dist/i18n/common.js +18 -2
- package/dist/i18n/config.js +41 -1
- package/dist/i18n/doctor.js +11 -6
- package/dist/i18n/glm.js +86 -0
- package/dist/i18n/help.js +14 -10
- package/dist/i18n/qwen.js +47 -15
- package/dist/i18n/self-review.js +2 -2
- package/dist/i18n.js +2 -0
- package/dist/index.js +325 -114
- package/dist/net-capture.js +60 -7
- package/dist/provider-handoff.js +11 -0
- package/dist/providers.js +65 -0
- package/dist/qwen-browser.js +274 -30
- package/dist/send-runtime.js +112 -0
- package/dist/send-telemetry.js +99 -0
- package/dist/send-throttle.js +108 -0
- package/dist/system-prompt.js +23 -2
- package/dist/transcript.js +34 -0
- package/dist/tui/edit.js +9 -4
- package/package.json +3 -2
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Send TELEMETRY — one JSONL line per agent send, per provider.
|
|
2
|
+
//
|
|
3
|
+
// WHY: the send interval is currently a hand-tuned constant (DeepSeek 15 s,
|
|
4
|
+
// Qwen 4 s, GLM 0) and the real limits are only visible in the OUTCOME of a
|
|
5
|
+
// send: a rate limit, a busy server, a truncated turn. Recording every send
|
|
6
|
+
// with its pause and its outcome turns "what is the limit?" into data instead
|
|
7
|
+
// of a guess, and feeds the adaptive throttle (src/send-throttle.ts).
|
|
8
|
+
//
|
|
9
|
+
// NOTHING about the prompt or the answer is recorded: only timing, the mode
|
|
10
|
+
// flags, and the error code/headers a server returned. The file is rotated so
|
|
11
|
+
// an unattended run cannot grow it without bound.
|
|
12
|
+
//
|
|
13
|
+
// The module is PURE apart from the append/rotate helpers, which take the
|
|
14
|
+
// directory as an argument (unit-tested with a temp dir).
|
|
15
|
+
import fs from 'fs';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
/** One line of the telemetry file (PURE). */
|
|
18
|
+
export function formatSendEvent(e) {
|
|
19
|
+
return JSON.stringify(e);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* PURE: parse the raw JSONL text into events, skipping a broken line (a
|
|
23
|
+
* half-written last line is expected after a crash and must not poison the
|
|
24
|
+
* statistics).
|
|
25
|
+
*/
|
|
26
|
+
export function parseSendEvents(raw) {
|
|
27
|
+
const out = [];
|
|
28
|
+
for (const line of String(raw || '').split(String.fromCharCode(10))) {
|
|
29
|
+
const t = line.trim();
|
|
30
|
+
if (!t)
|
|
31
|
+
continue;
|
|
32
|
+
try {
|
|
33
|
+
const obj = JSON.parse(t);
|
|
34
|
+
if (obj && typeof obj === 'object' && typeof obj.outcome === 'string') {
|
|
35
|
+
out.push(obj);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch { }
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
/** PURE: aggregate the last events for the /limits view. */
|
|
43
|
+
export function summarizeSends(events, now = Date.now()) {
|
|
44
|
+
const within = (ms) => events.filter((e) => {
|
|
45
|
+
const t = Date.parse(e.at);
|
|
46
|
+
return Number.isFinite(t) && now - t <= ms;
|
|
47
|
+
}).length;
|
|
48
|
+
const blocks = events
|
|
49
|
+
.filter((e) => e.outcome === 'rate_limit' && typeof e.blockMs === 'number')
|
|
50
|
+
.map((e) => e.blockMs);
|
|
51
|
+
const avgBlockMs = blocks.length
|
|
52
|
+
? Math.round(blocks.reduce((a, b) => a + b, 0) / blocks.length)
|
|
53
|
+
: 0;
|
|
54
|
+
return {
|
|
55
|
+
last10m: within(10 * 60_000),
|
|
56
|
+
last30m: within(30 * 60_000),
|
|
57
|
+
last60m: within(60 * 60_000),
|
|
58
|
+
rateLimits: events.filter((e) => e.outcome === 'rate_limit').length,
|
|
59
|
+
avgBlockMs,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** `~/.zames/logs/sends-<provider>.jsonl`. */
|
|
63
|
+
export function sendLogPath(logsDir, provider) {
|
|
64
|
+
return path.join(logsDir, 'sends-' + provider + '.jsonl');
|
|
65
|
+
}
|
|
66
|
+
/** Keep at most `keep` sends-*.jsonl files (rotate the oldest away). */
|
|
67
|
+
export function rotateSendLogs(logsDir, keep = 5) {
|
|
68
|
+
try {
|
|
69
|
+
const files = fs
|
|
70
|
+
.readdirSync(logsDir)
|
|
71
|
+
.filter((f) => /^sends-.*\.jsonl$/.test(f))
|
|
72
|
+
.map((f) => ({ f, t: fs.statSync(path.join(logsDir, f)).mtimeMs }))
|
|
73
|
+
.sort((a, b) => b.t - a.t);
|
|
74
|
+
for (const old of files.slice(keep)) {
|
|
75
|
+
try {
|
|
76
|
+
fs.unlinkSync(path.join(logsDir, old.f));
|
|
77
|
+
}
|
|
78
|
+
catch { }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch { }
|
|
82
|
+
}
|
|
83
|
+
/** Append one event. Best-effort: a logging failure never breaks a send. */
|
|
84
|
+
export function appendSendEvent(logsDir, e) {
|
|
85
|
+
try {
|
|
86
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
87
|
+
fs.appendFileSync(sendLogPath(logsDir, e.provider), formatSendEvent(e) + String.fromCharCode(10), 'utf-8');
|
|
88
|
+
}
|
|
89
|
+
catch { }
|
|
90
|
+
}
|
|
91
|
+
/** Read the recent events for one provider (best-effort, [] on failure). */
|
|
92
|
+
export function readSendEvents(logsDir, provider) {
|
|
93
|
+
try {
|
|
94
|
+
return parseSendEvents(fs.readFileSync(sendLogPath(logsDir, provider), 'utf-8'));
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Adaptive SEND THROTTLE — how long to wait before the next agent send.
|
|
2
|
+
//
|
|
3
|
+
// WHY: a fixed interval is wrong in both directions. Too small and the site
|
|
4
|
+
// answers with a rate limit whose PENALTY grows with the overrun (DeepSeek: a
|
|
5
|
+
// zero-gap burst ran ~30 min, then a ~30 min block). Too large and every turn
|
|
6
|
+
// pays a pause that is usually unnecessary. The throttle learns from the
|
|
7
|
+
// telemetry: it stays below a soft budget, backs off on a real limit and
|
|
8
|
+
// recovers gradually after a run of successes.
|
|
9
|
+
//
|
|
10
|
+
// The module is PURE: it takes the current state and one event and returns the
|
|
11
|
+
// next state + the wait. The browser owns the state, so switching providers or
|
|
12
|
+
// finishing a task does not reset what was learned.
|
|
13
|
+
export const DEFAULT_THROTTLE = {
|
|
14
|
+
windowMs: 10 * 60_000,
|
|
15
|
+
// Off until the operator has data: an infinite budget never fires.
|
|
16
|
+
budget: Number.POSITIVE_INFINITY,
|
|
17
|
+
recoverAfter: 3,
|
|
18
|
+
maxMs: 60_000,
|
|
19
|
+
limitFloorMs: 2000,
|
|
20
|
+
};
|
|
21
|
+
export function initialThrottleState(cfg) {
|
|
22
|
+
return {
|
|
23
|
+
intervalMs: Math.max(0, cfg.baseMs),
|
|
24
|
+
okStreak: 0,
|
|
25
|
+
blocked: false,
|
|
26
|
+
blockedAt: 0,
|
|
27
|
+
forcedWaitMs: 0,
|
|
28
|
+
recent: [],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Drop sends older than the window (keeps the state small). */
|
|
32
|
+
function prune(recent, now, windowMs) {
|
|
33
|
+
return recent.filter((t) => now - t <= windowMs);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* PURE: the wait before the next send, given the state and `now`. A blocked
|
|
37
|
+
* throttle waits out the forced pause; otherwise the learned interval applies
|
|
38
|
+
* since the last send.
|
|
39
|
+
*/
|
|
40
|
+
export function nextWaitMs(state, lastSentAt, now) {
|
|
41
|
+
if (state.forcedWaitMs > 0)
|
|
42
|
+
return state.forcedWaitMs;
|
|
43
|
+
if (!lastSentAt)
|
|
44
|
+
return 0;
|
|
45
|
+
const gap = state.intervalMs - (now - lastSentAt);
|
|
46
|
+
return gap > 0 ? gap : 0;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* PURE: fold one telemetry event into the state.
|
|
50
|
+
*
|
|
51
|
+
* Rules (the operator's strategy):
|
|
52
|
+
* - a soft budget: too many sends in the window GROW the interval smoothly,
|
|
53
|
+
* ahead of a ban;
|
|
54
|
+
* - a rate limit: wait the server's own Retry-After when present, else the
|
|
55
|
+
* configured pause; after it lifts the interval doubles (from a floor);
|
|
56
|
+
* - after `recoverAfter` successes in a row the interval halves, down to the
|
|
57
|
+
* base.
|
|
58
|
+
*/
|
|
59
|
+
export function applySendEvent(state, event, cfg, now = Date.now()) {
|
|
60
|
+
const next = { ...state, recent: [...state.recent] };
|
|
61
|
+
const at = Date.parse(event.at);
|
|
62
|
+
const stamp = Number.isFinite(at) ? at : now;
|
|
63
|
+
if (event.outcome === 'ok') {
|
|
64
|
+
next.recent.push(stamp);
|
|
65
|
+
next.recent = prune(next.recent, now, cfg.windowMs);
|
|
66
|
+
next.okStreak = state.okStreak + 1;
|
|
67
|
+
if (next.okStreak >= cfg.recoverAfter) {
|
|
68
|
+
next.intervalMs = Math.max(cfg.baseMs, Math.floor(next.intervalMs / 2));
|
|
69
|
+
next.okStreak = 0;
|
|
70
|
+
}
|
|
71
|
+
next.blocked = false;
|
|
72
|
+
next.blockedAt = 0;
|
|
73
|
+
next.forcedWaitMs = 0;
|
|
74
|
+
}
|
|
75
|
+
else if (event.outcome === 'rate_limit') {
|
|
76
|
+
next.blocked = true;
|
|
77
|
+
next.blockedAt = state.blocked ? state.blockedAt || stamp : stamp;
|
|
78
|
+
next.okStreak = 0;
|
|
79
|
+
next.forcedWaitMs =
|
|
80
|
+
typeof event.retryAfterMs === 'number' && event.retryAfterMs > 0
|
|
81
|
+
? event.retryAfterMs
|
|
82
|
+
: cfg.rateLimitWaitMs;
|
|
83
|
+
const floor = Math.max(cfg.baseMs, cfg.limitFloorMs);
|
|
84
|
+
next.intervalMs = Math.min(cfg.maxMs, Math.max(next.intervalMs, floor) * 2);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
// busy / incomplete / guest_limit: not a rate signal, keep the interval.
|
|
88
|
+
next.recent.push(stamp);
|
|
89
|
+
next.recent = prune(next.recent, now, cfg.windowMs);
|
|
90
|
+
}
|
|
91
|
+
// Soft budget: a smooth growth once the window is over budget.
|
|
92
|
+
if (Number.isFinite(cfg.budget) && next.recent.length > cfg.budget) {
|
|
93
|
+
const over = next.recent.length - cfg.budget;
|
|
94
|
+
next.intervalMs = Math.min(cfg.maxMs, Math.max(next.intervalMs, cfg.baseMs) + over * 1000);
|
|
95
|
+
}
|
|
96
|
+
return next;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* PURE: a rate limit was observed and the block ended — record its duration on
|
|
100
|
+
* the event so the telemetry can average it.
|
|
101
|
+
*/
|
|
102
|
+
export function withBlockDuration(event, blockedAt, liftedAt) {
|
|
103
|
+
if (event.outcome !== 'rate_limit')
|
|
104
|
+
return event;
|
|
105
|
+
if (!blockedAt)
|
|
106
|
+
return event;
|
|
107
|
+
return { ...event, blockMs: Math.max(0, liftedAt - blockedAt) };
|
|
108
|
+
}
|
package/dist/system-prompt.js
CHANGED
|
@@ -195,7 +195,7 @@ export function renderToolSection(tools) {
|
|
|
195
195
|
.map((t2) => `### ${t2.name}\n${t2.description}\nParameters: ${JSON.stringify(t2.parameters)}`)
|
|
196
196
|
.join('\n\n');
|
|
197
197
|
}
|
|
198
|
-
export function buildSystemPrompt({ workdir, tools, gitContext = null, locale = 'ru', attachments = [], context = null, selfImprovement = false, }) {
|
|
198
|
+
export function buildSystemPrompt({ workdir, tools, gitContext = null, locale = 'ru', attachments = [], context = null, selfImprovement = false, provider, }) {
|
|
199
199
|
const t = translate(locale);
|
|
200
200
|
const toolDescriptions = renderToolSection(tools);
|
|
201
201
|
// Emit the subagent guidance ONLY when the Task tool is actually in the set
|
|
@@ -208,6 +208,27 @@ export function buildSystemPrompt({ workdir, tools, gitContext = null, locale =
|
|
|
208
208
|
const askUserSection = tools.some((x) => x.name === 'AskUser')
|
|
209
209
|
? ASKUSER_SECTION
|
|
210
210
|
: '';
|
|
211
|
+
// GLM runs on chat.z.ai, whose OWN agent mode used to execute tool calls in
|
|
212
|
+
// a cloud sandbox instead of the operator's machine. The provider neutralises
|
|
213
|
+
// that mode (see glm-browser.ts), but the model must also be TOLD where it
|
|
214
|
+
// runs: otherwise it asks for a sandbox ("Limit Sandbox Concurrency") or
|
|
215
|
+
// claims it ran something it did not.
|
|
216
|
+
const runtimeSection = provider === 'glm'
|
|
217
|
+
? '\n## You run on the OPERATOR\'S MACHINE (GLM)\n\n' +
|
|
218
|
+
'The tools above execute on the operator\'s computer, in the working ' +
|
|
219
|
+
'directory shown at the top. They are the ONLY way to touch files or ' +
|
|
220
|
+
'run commands.\n\n' +
|
|
221
|
+
'- There is NO cloud sandbox for you. Do NOT request or use a z.ai ' +
|
|
222
|
+
'sandbox/VM; a "Limit Sandbox Concurrency" page is a leftover of the ' +
|
|
223
|
+
"site's own agent mode and is NOT part of this task.\n" +
|
|
224
|
+
'- Never claim a command ran unless you emitted its tool call and ' +
|
|
225
|
+
'received a tool result. If you have not called the tool, say so.\n' +
|
|
226
|
+
'- Paths like `/home/z/...` or `/mnt/data` are NOT the operator\'s ' +
|
|
227
|
+
'machine.\n' +
|
|
228
|
+
'- Do NOT use web search / WebSearch unless the task explicitly asks ' +
|
|
229
|
+
'for current web information: it is slow, it burns the account quota ' +
|
|
230
|
+
'and it is not needed to read or edit files.\n'
|
|
231
|
+
: '';
|
|
211
232
|
const gitSection = gitContext
|
|
212
233
|
? `\n## Git context\n\n${gitContext}\n`
|
|
213
234
|
: '\n## Git context\n\nNot a git repository (or git is not installed).\n';
|
|
@@ -269,7 +290,7 @@ task is done (or when you must ask the operator), not between tool calls.
|
|
|
269
290
|
You have access to the following tools:
|
|
270
291
|
|
|
271
292
|
${toolDescriptions}
|
|
272
|
-
${gitSection}${attachSection}${contextSection}${selfSection}
|
|
293
|
+
${runtimeSection}${gitSection}${attachSection}${contextSection}${selfSection}
|
|
273
294
|
## Choosing the right tool
|
|
274
295
|
|
|
275
296
|
Prefer the most specific tool; a wrong choice wastes a turn:
|
package/dist/transcript.js
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
// A transcript file is appended per RUN and never deleted, so ~/.zames/logs
|
|
4
|
+
// grew without bound (177 files / 138 MB observed). On WSL the directory sits
|
|
5
|
+
// inside ext4.vhdx: the space of deleted files is not returned to the Windows
|
|
6
|
+
// host, so a big log dir is also a host-disk problem. Rotate on start-up: keep
|
|
7
|
+
// the newest MAX_TRANSCRIPT_FILES and drop the rest.
|
|
8
|
+
const MAX_TRANSCRIPT_FILES = 200;
|
|
9
|
+
// The constructor is SYNCHRONOUS (it opens the stream right away), so the
|
|
10
|
+
// rotation that must happen before the create has to be sync as well.
|
|
11
|
+
export function rotateTranscripts(dir, keep = MAX_TRANSCRIPT_FILES) {
|
|
12
|
+
let names;
|
|
13
|
+
try {
|
|
14
|
+
names = fs.readdirSync(dir).filter((n) => n.endsWith('.jsonl'));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// A missing/unreadable dir must not break the run: the transcript is
|
|
18
|
+
// created right after and mkdirSync has its own error handling.
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
if (names.length <= keep)
|
|
22
|
+
return 0;
|
|
23
|
+
names.sort();
|
|
24
|
+
let removed = 0;
|
|
25
|
+
for (const n of names.slice(0, names.length - keep)) {
|
|
26
|
+
try {
|
|
27
|
+
fs.rmSync(path.join(dir, n), { force: true });
|
|
28
|
+
removed++;
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
}
|
|
32
|
+
return removed;
|
|
33
|
+
}
|
|
3
34
|
function transcriptErrorLine(e) {
|
|
4
35
|
return `transcript: failed to create file: ${e.message}`;
|
|
5
36
|
}
|
|
@@ -27,6 +58,9 @@ export class Transcript {
|
|
|
27
58
|
const safeName = String(sessionName)
|
|
28
59
|
.replace(/[\\/:*?"<>|\u0000-\u001f]/g, '_')
|
|
29
60
|
.trim() || 'session';
|
|
61
|
+
// Rotation BEFORE the create: the new file then has the newest stamp,
|
|
62
|
+
// so the current run's transcript is never a deletion candidate.
|
|
63
|
+
rotateTranscripts(dir);
|
|
30
64
|
this.file = path.join(dir, `${safeName}-${stamp}.jsonl`);
|
|
31
65
|
this.stream = fs.createWriteStream(this.file, { flags: 'a' });
|
|
32
66
|
// Write errors (disk full, file deleted, etc.) arrive as an 'error'
|
package/dist/tui/edit.js
CHANGED
|
@@ -74,9 +74,12 @@ export function del(s) {
|
|
|
74
74
|
export function deleteWordLeft(s) {
|
|
75
75
|
const chars = charsOf(s);
|
|
76
76
|
let i = s.cursor;
|
|
77
|
-
|
|
77
|
+
// A NEWLINE is a hard boundary: at the start of a line the key does
|
|
78
|
+
// nothing instead of eating the break and the previous line's word
|
|
79
|
+
// (deleteWordRight and wordLeft already stop at a newline).
|
|
80
|
+
while (i > 0 && chars[i - 1] !== NL && /\s/.test(chars[i - 1]))
|
|
78
81
|
i--;
|
|
79
|
-
while (i > 0 && !/\s/.test(chars[i - 1]))
|
|
82
|
+
while (i > 0 && chars[i - 1] !== NL && !/\s/.test(chars[i - 1]))
|
|
80
83
|
i--;
|
|
81
84
|
chars.splice(i, s.cursor - i);
|
|
82
85
|
return { ...s, buf: chars.join(''), cursor: i };
|
|
@@ -137,9 +140,11 @@ export function moveEnd(s) {
|
|
|
137
140
|
export function wordLeft(s) {
|
|
138
141
|
const chars = charsOf(s);
|
|
139
142
|
let i = s.cursor;
|
|
140
|
-
|
|
143
|
+
// Stop at a newline (mirrors deleteWordLeft/deleteWordRight): the cursor
|
|
144
|
+
// must not jump to the previous LINE, only to the previous word.
|
|
145
|
+
while (i > 0 && chars[i - 1] !== NL && /\s/.test(chars[i - 1]))
|
|
141
146
|
i--;
|
|
142
|
-
while (i > 0 && !/\s/.test(chars[i - 1]))
|
|
147
|
+
while (i > 0 && chars[i - 1] !== NL && !/\s/.test(chars[i - 1]))
|
|
143
148
|
i--;
|
|
144
149
|
return i === s.cursor ? s : { ...s, cursor: i };
|
|
145
150
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.81.0",
|
|
4
4
|
"description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen and other web LLMs — no API key required. Reads and edits files, runs commands, commits to git, like Claude Code / Codex CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
"render-how": "tsx scripts/render-how-it-works.mts",
|
|
42
42
|
"build:readme": "tsx scripts/build-readme.mts",
|
|
43
43
|
"postinstall": "node scripts/postinstall.mjs",
|
|
44
|
-
"prepare": "husky"
|
|
44
|
+
"prepare": "husky",
|
|
45
|
+
"glm-smoke": "tsx scripts/glm-smoke.mts"
|
|
45
46
|
},
|
|
46
47
|
"keywords": [
|
|
47
48
|
"agent",
|