zames_pro 2.42.0 → 2.44.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 +14 -4
- package/dist/attachments.js +11 -4
- package/dist/browser.js +23 -6
- package/dist/commands.js +13 -0
- package/dist/context.js +77 -0
- package/dist/index.js +19 -9
- package/dist/system-prompt.js +4 -3
- package/package.json +1 -1
package/dist/agent-loop.js
CHANGED
|
@@ -773,10 +773,20 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
773
773
|
export function truncateToolResult(text, limit) {
|
|
774
774
|
if (text.length <= limit)
|
|
775
775
|
return text;
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
776
|
+
// Prefer a LINE boundary near the limit so the model does not read a line
|
|
777
|
+
// cut in half (a half line is easy to misread as the real content). We look
|
|
778
|
+
// for the last newline within the last 20% of the window; if none, fall back
|
|
779
|
+
// to the hard char cut.
|
|
780
|
+
let cut = limit;
|
|
781
|
+
const NL = String.fromCharCode(10);
|
|
782
|
+
const searchFrom = Math.floor(limit * 0.8);
|
|
783
|
+
const nl = text.lastIndexOf(NL, limit);
|
|
784
|
+
// Cut AFTER the newline (include it) so the kept part is a whole number of
|
|
785
|
+
// lines — a cut that EXCLUDED the newline left a half line before the marker.
|
|
786
|
+
if (nl >= searchFrom)
|
|
787
|
+
cut = nl + 1;
|
|
788
|
+
const omitted = text.length - cut;
|
|
789
|
+
return text.slice(0, cut) + NL + `[...truncated ${omitted} chars]`;
|
|
780
790
|
}
|
|
781
791
|
// The answer looks like a tool call, but parseToolCall() did not recognize it.
|
|
782
792
|
// Used as a safeguard against "the agent called a tool and stopped": in that
|
package/dist/attachments.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
+
import { createHash } from 'crypto';
|
|
4
5
|
import { execFileSync } from 'child_process';
|
|
5
6
|
import { extractPathToken } from './path-token.js';
|
|
6
7
|
export { extractPathToken };
|
|
@@ -15,6 +16,11 @@ export const IMAGE_EXTS = [
|
|
|
15
16
|
'.ico',
|
|
16
17
|
];
|
|
17
18
|
export const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
|
|
19
|
+
// Stable content hash for de-duplicating identical attachments (a pasted image
|
|
20
|
+
// is saved to a NEW temp file every time, so its path is not a stable identity).
|
|
21
|
+
export function contentHash(data) {
|
|
22
|
+
return createHash('sha1').update(data).digest('hex');
|
|
23
|
+
}
|
|
18
24
|
function sanitize(name) {
|
|
19
25
|
return (String(name || '')
|
|
20
26
|
.replace(/[\u0000-\u001f\u007f]/g, '')
|
|
@@ -139,10 +145,10 @@ export class AttachmentStore {
|
|
|
139
145
|
this.fileCount = 0;
|
|
140
146
|
}
|
|
141
147
|
add(item) {
|
|
142
|
-
// De-duplicate by real path
|
|
143
|
-
//
|
|
144
|
-
// The operator saw
|
|
145
|
-
const existing = this.items.find((a) => a.path === item.path);
|
|
148
|
+
// De-duplicate by real path (files/paths) OR by content hash (images and
|
|
149
|
+
// clipboard data, which are re-saved to a fresh temp file on every paste).
|
|
150
|
+
// The operator saw the SAME file become [file#1] and [file#2].
|
|
151
|
+
const existing = this.items.find((a) => a.path === item.path || (item.hash && a.hash && a.hash === item.hash));
|
|
146
152
|
if (existing)
|
|
147
153
|
return existing;
|
|
148
154
|
const image = isImageName(item.name) || String(item.mime).startsWith('image/');
|
|
@@ -156,6 +162,7 @@ export class AttachmentStore {
|
|
|
156
162
|
image,
|
|
157
163
|
marker,
|
|
158
164
|
size: Number(item.size) || 0,
|
|
165
|
+
hash: item.hash,
|
|
159
166
|
};
|
|
160
167
|
this.items.push(att);
|
|
161
168
|
return att;
|
package/dist/browser.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { chromium, } from 'playwright';
|
|
2
2
|
import { extractAnswer, extractTokenUsage, dumpNetBody, isFinishedWithoutAnswer, } from './net-capture.js';
|
|
3
|
+
import { rewriteFailedAttachments } from './commands.js';
|
|
3
4
|
import path from 'path';
|
|
4
5
|
import os from 'os';
|
|
5
6
|
import fs from 'fs/promises';
|
|
@@ -1859,7 +1860,12 @@ export class DeepSeekBrowser {
|
|
|
1859
1860
|
// Attach files/images to the chat via the hidden <input type=file> of the
|
|
1860
1861
|
// DeepSeek upload widget. The element is not visible, so we set the files
|
|
1861
1862
|
// programmatically (Playwright setInputFiles works on hidden inputs too).
|
|
1863
|
+
// Returns the files that could NOT be attached (read error, no upload
|
|
1864
|
+
// widget, or a failed upload), so the caller can rewrite their path in the
|
|
1865
|
+
// message to [attach-failed: name]. The model must not believe a file was
|
|
1866
|
+
// attached when it was not.
|
|
1862
1867
|
async _attachFiles(files) {
|
|
1868
|
+
const failed = [];
|
|
1863
1869
|
const bufPayload = [];
|
|
1864
1870
|
for (const f of files) {
|
|
1865
1871
|
try {
|
|
@@ -1867,6 +1873,7 @@ export class DeepSeekBrowser {
|
|
|
1867
1873
|
bufPayload.push({ name: f.name, mimeType: f.mime, buffer });
|
|
1868
1874
|
}
|
|
1869
1875
|
catch (e) {
|
|
1876
|
+
failed.push({ path: f.path, name: f.name });
|
|
1870
1877
|
this._notice(theme.warn(this._t('ds.attach_read_failed', {
|
|
1871
1878
|
name: f.name,
|
|
1872
1879
|
error: e.message,
|
|
@@ -1874,7 +1881,7 @@ export class DeepSeekBrowser {
|
|
|
1874
1881
|
}
|
|
1875
1882
|
}
|
|
1876
1883
|
if (!bufPayload.length)
|
|
1877
|
-
return;
|
|
1884
|
+
return failed;
|
|
1878
1885
|
// Preferred path: click the attach button and let the file chooser event
|
|
1879
1886
|
// carry the files. This is how a real user attaches a file and it reliably
|
|
1880
1887
|
// triggers DeepSeek's upload handler.
|
|
@@ -1900,7 +1907,7 @@ export class DeepSeekBrowser {
|
|
|
1900
1907
|
await chooser.setFiles(bufPayload);
|
|
1901
1908
|
// Wait for the upload preview instead of a fixed 2.5s sleep.
|
|
1902
1909
|
await this._awaitUploadPreview(files.map((f) => f.name), 2500);
|
|
1903
|
-
return;
|
|
1910
|
+
return failed;
|
|
1904
1911
|
}
|
|
1905
1912
|
catch { }
|
|
1906
1913
|
}
|
|
@@ -1908,17 +1915,22 @@ export class DeepSeekBrowser {
|
|
|
1908
1915
|
const input = this.page.locator('input[type="file"]').first();
|
|
1909
1916
|
if ((await input.count()) === 0) {
|
|
1910
1917
|
this._notice(theme.warn(this._t('ds.attach_no_input')));
|
|
1911
|
-
|
|
1918
|
+
for (const f of files)
|
|
1919
|
+
failed.push({ path: f.path, name: f.name });
|
|
1920
|
+
return failed;
|
|
1912
1921
|
}
|
|
1913
1922
|
try {
|
|
1914
1923
|
await input.setInputFiles(bufPayload, { timeout: 15_000 });
|
|
1915
1924
|
}
|
|
1916
1925
|
catch (e) {
|
|
1917
1926
|
this._notice(theme.warn(this._t('ds.attach_failed', { error: e.message })));
|
|
1918
|
-
|
|
1927
|
+
for (const f of files)
|
|
1928
|
+
failed.push({ path: f.path, name: f.name });
|
|
1929
|
+
return failed;
|
|
1919
1930
|
}
|
|
1920
1931
|
// Wait for the upload to finish (the attach preview to appear).
|
|
1921
1932
|
await this._awaitUploadPreview(files.map((f) => f.name), 2000);
|
|
1933
|
+
return failed;
|
|
1922
1934
|
}
|
|
1923
1935
|
// Wait for the upload preview (the attached-file chip above the
|
|
1924
1936
|
// input) to appear, instead of a fixed 2.5s/2s sleep. The signal is the
|
|
@@ -2103,10 +2115,15 @@ export class DeepSeekBrowser {
|
|
|
2103
2115
|
await this._applyToggles();
|
|
2104
2116
|
// Attach files/images FIRST (before the text): the DeepSeek upload widget
|
|
2105
2117
|
// shows them above the input, and only then the message can be sent.
|
|
2118
|
+
// A file that FAILED to attach has its real path in the prompt text (see
|
|
2119
|
+
// substituteAttachmentMarkers); rewrite it to [attach-failed: name] so the
|
|
2120
|
+
// model does not believe the file is available.
|
|
2121
|
+
let finalPrompt = prompt;
|
|
2106
2122
|
if (attachments.length) {
|
|
2107
|
-
await this._attachFiles(attachments);
|
|
2123
|
+
const failed = await this._attachFiles(attachments);
|
|
2124
|
+
finalPrompt = rewriteFailedAttachments(finalPrompt, failed);
|
|
2108
2125
|
}
|
|
2109
|
-
await this._setInputText(input,
|
|
2126
|
+
await this._setInputText(input, finalPrompt);
|
|
2110
2127
|
// Send immediately: the input is already verified by _setInputText, and a
|
|
2111
2128
|
// clickable send button normally appears right away. The old code slept a
|
|
2112
2129
|
// fixed 50ms here, which only added latency (the button does not need it).
|
package/dist/commands.js
CHANGED
|
@@ -532,6 +532,19 @@ export function substituteAttachmentMarkers(text, attachments) {
|
|
|
532
532
|
out = out.replace(/\[file#(\d+)\]/g, (whole, n) => filePath.get(Number(n)) ?? whole);
|
|
533
533
|
return out;
|
|
534
534
|
}
|
|
535
|
+
// Rewrite the paths of files that FAILED to attach into [attach-failed: name]
|
|
536
|
+
// in the task text, so the model does not believe an unavailable file was
|
|
537
|
+
// attached. The prompt carries real paths (substituteAttachmentMarkers), so a
|
|
538
|
+
// failed file's path is replaced in place. Pure, so it is unit-tested.
|
|
539
|
+
export function rewriteFailedAttachments(text, failed) {
|
|
540
|
+
let out = String(text ?? '');
|
|
541
|
+
for (const f of failed || []) {
|
|
542
|
+
if (!f || !f.path)
|
|
543
|
+
continue;
|
|
544
|
+
out = out.split(f.path).join('[attach-failed: ' + f.name + ']');
|
|
545
|
+
}
|
|
546
|
+
return out;
|
|
547
|
+
}
|
|
535
548
|
// True when a `/config ...` line can be handled LIVE (while the agent is busy):
|
|
536
549
|
// the text subcommands only touch config values, never the chat. The bare
|
|
537
550
|
// `/config` and `/config menu` (which pause the editor and read keys) are NOT
|
package/dist/context.js
CHANGED
|
@@ -1,6 +1,65 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
+
// Built-in skills, always available unless a project/global skill with the
|
|
5
|
+
// same name overrides them (real files win). They give the agent proven,
|
|
6
|
+
// reusable workflows in the spirit of Claude Code / Codex. English only (the
|
|
7
|
+
// body goes into the agent-facing task text).
|
|
8
|
+
const BUILTIN_SKILLS = [
|
|
9
|
+
{
|
|
10
|
+
name: 'commit',
|
|
11
|
+
description: 'Prepare a clean git commit from the current changes (stage, message).',
|
|
12
|
+
body: 'Prepare a commit from the current working-tree changes. Steps:\n' +
|
|
13
|
+
'1. Run `git status` and `git diff` (and `git diff --staged`) to see exactly what changed.\n' +
|
|
14
|
+
'2. Stage ONLY the files that belong to the intended change — never `git add -A` blindly.\n' +
|
|
15
|
+
'3. Write a short imperative subject line (<=72 chars) and an optional body explaining WHY.\n' +
|
|
16
|
+
'4. Commit with `git commit`. Do NOT push.\n' +
|
|
17
|
+
'If the changes are unrelated or include junk, report that and ask before committing.',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'review',
|
|
21
|
+
description: 'Review the current diff for bugs, risks and style issues (no code changes).',
|
|
22
|
+
body: 'Review the current uncommitted diff. Do NOT change any code — only report.\n' +
|
|
23
|
+
'1. Get the diff with `git diff` (and `git diff --staged`).\n' +
|
|
24
|
+
'2. Look for real problems: correctness bugs, unhandled edge cases, races, ' +
|
|
25
|
+
'silent failures, security issues, broken error handling.\n' +
|
|
26
|
+
'3. For each finding give file:line, what is wrong and WHY it matters.\n' +
|
|
27
|
+
'4. End with a short verdict: safe to commit / needs fixes, and list the must-fix items first.',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'test',
|
|
31
|
+
description: 'Run the project tests and fix failures until green.',
|
|
32
|
+
body: 'Run the project test suite and make it green. Steps:\n' +
|
|
33
|
+
'1. Find the test command (package.json scripts, Makefile, README).\n' +
|
|
34
|
+
'2. Run it. If it passes, report the result and stop.\n' +
|
|
35
|
+
'3. On failure, read the failing test and the code it exercises. Fix the ROOT cause ' +
|
|
36
|
+
'(do not weaken or delete the test to make it pass, unless the test itself is wrong).\n' +
|
|
37
|
+
'4. Re-run until it passes. Report what changed and the final result.',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'debug',
|
|
41
|
+
description: 'Systematically diagnose and fix a bug.',
|
|
42
|
+
body: 'Diagnose a bug systematically. Steps:\n' +
|
|
43
|
+
'1. Reproduce it: find the smallest command/steps that show the failure.\n' +
|
|
44
|
+
'2. Form a hypothesis about the cause and find evidence in the code (read the actual code path).\n' +
|
|
45
|
+
'3. Narrow it down (logs, a focused test, a minimal repro) BEFORE editing.\n' +
|
|
46
|
+
'4. Fix the ROOT cause, not the symptom. Add a regression test that fails before and passes after.\n' +
|
|
47
|
+
'5. Verify the fix and report the cause, the change and the test.',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'explain',
|
|
51
|
+
description: 'Explain how a file/module/feature works.',
|
|
52
|
+
body: 'Explain how the requested code works, for a developer new to it.\n' +
|
|
53
|
+
'1. Read the relevant files (start from the entry point and follow the calls).\n' +
|
|
54
|
+
'2. Give a concise overview: what it does, its inputs/outputs, the main flow.\n' +
|
|
55
|
+
'3. Call out non-obvious details: side effects, ordering, error handling, ' +
|
|
56
|
+
'couplings, and any WHY-comments that matter.\n' +
|
|
57
|
+
'4. Do NOT change code. Answer in the operator language.',
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
export function builtinSkills() {
|
|
61
|
+
return BUILTIN_SKILLS;
|
|
62
|
+
}
|
|
4
63
|
const MAX_FILE_BASE = 60000;
|
|
5
64
|
const MAX_TOTAL = 240000;
|
|
6
65
|
const CRLF_RE = /\r\n/g;
|
|
@@ -190,6 +249,24 @@ export async function loadSkills(workdir) {
|
|
|
190
249
|
}
|
|
191
250
|
}
|
|
192
251
|
found.sort((a, b) => a.name.localeCompare(b.name));
|
|
252
|
+
// Built-in skills come LAST (lowest priority): any project/global skill
|
|
253
|
+
// with the same name already claimed `seen` and wins. Only the not-overridden
|
|
254
|
+
// built-ins are appended, with an empty path and the body inline.
|
|
255
|
+
for (const b of BUILTIN_SKILLS) {
|
|
256
|
+
const key = b.name.toLowerCase();
|
|
257
|
+
if (seen.has(key))
|
|
258
|
+
continue;
|
|
259
|
+
seen.add(key);
|
|
260
|
+
found.push({
|
|
261
|
+
name: b.name,
|
|
262
|
+
description: b.description,
|
|
263
|
+
path: '',
|
|
264
|
+
dir: '',
|
|
265
|
+
source: 'global',
|
|
266
|
+
userInvokable: true,
|
|
267
|
+
builtinBody: b.body,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
193
270
|
return found;
|
|
194
271
|
}
|
|
195
272
|
const PROJECT_COMMAND_DIRS = [
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { createTools } from './tools.js';
|
|
|
9
9
|
import { runAgentLoop } from './agent-loop.js';
|
|
10
10
|
import { createSpinner } from './spinner.js';
|
|
11
11
|
import { LineEditor, expandPastes, pasteReplacement, formatCompactTokens, } from './input.js';
|
|
12
|
-
import { parseImagePaste, extForMime, saveToTemp, guessMime, isImageName, formatSize, looksLikeFilePath, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, hasClipboardTool, } from './attachments.js';
|
|
12
|
+
import { parseImagePaste, extForMime, saveToTemp, guessMime, isImageName, formatSize, looksLikeFilePath, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, hasClipboardTool, contentHash, } from './attachments.js';
|
|
13
13
|
import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, } from './config.js';
|
|
14
14
|
import { runConfigMenu } from './config-menu.js';
|
|
15
15
|
import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n.js';
|
|
@@ -454,14 +454,20 @@ async function expandSlashTarget(workdir, name, rest) {
|
|
|
454
454
|
const skill = skills.find((s) => s.name.toLowerCase() === name.toLowerCase() && s.userInvokable);
|
|
455
455
|
if (skill) {
|
|
456
456
|
let body = '';
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
body = skillBody(raw);
|
|
457
|
+
if (skill.builtinBody !== undefined) {
|
|
458
|
+
body = skill.builtinBody;
|
|
460
459
|
}
|
|
461
|
-
|
|
462
|
-
|
|
460
|
+
else {
|
|
461
|
+
try {
|
|
462
|
+
const raw = await fs.readFile(skill.path, 'utf-8');
|
|
463
|
+
body = skillBody(raw);
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
body = '';
|
|
467
|
+
}
|
|
463
468
|
}
|
|
464
|
-
const
|
|
469
|
+
const from = skill.path ? ' (from ' + skill.path + ')' : ' (built-in)';
|
|
470
|
+
const header = 'Follow the skill "' + skill.name + '"' + from + '.';
|
|
465
471
|
const extra = rest
|
|
466
472
|
? '\n\nAdditional instructions from the operator: ' + rest
|
|
467
473
|
: '';
|
|
@@ -1500,6 +1506,7 @@ async function main() {
|
|
|
1500
1506
|
name,
|
|
1501
1507
|
mime: image.mime,
|
|
1502
1508
|
size: image.data.length,
|
|
1509
|
+
hash: contentHash(image.data),
|
|
1503
1510
|
});
|
|
1504
1511
|
ed.printAbove(theme.system(t('msg.attached_image', {
|
|
1505
1512
|
marker: att.marker,
|
|
@@ -1573,6 +1580,7 @@ async function main() {
|
|
|
1573
1580
|
name,
|
|
1574
1581
|
mime,
|
|
1575
1582
|
size: res.data.length,
|
|
1583
|
+
hash: contentHash(res.data),
|
|
1576
1584
|
});
|
|
1577
1585
|
ed.printAbove(theme.system(t('msg.attached_image', {
|
|
1578
1586
|
marker: att.marker,
|
|
@@ -2537,11 +2545,13 @@ async function main() {
|
|
|
2537
2545
|
else {
|
|
2538
2546
|
console.log(theme.system(t('skills.title', { n: String(skills.length) })));
|
|
2539
2547
|
for (const s of skills) {
|
|
2548
|
+
const src = s.path ? s.source : 'built-in';
|
|
2540
2549
|
console.log(' ' +
|
|
2541
2550
|
theme.user('/' + s.name) +
|
|
2542
|
-
theme.dim(' [' +
|
|
2551
|
+
theme.dim(' [' + src + '] ') +
|
|
2543
2552
|
(s.description || theme.dim(t('common.none'))));
|
|
2544
|
-
|
|
2553
|
+
if (s.path)
|
|
2554
|
+
console.log(theme.dim(' ' + s.path));
|
|
2545
2555
|
}
|
|
2546
2556
|
}
|
|
2547
2557
|
continue;
|
package/dist/system-prompt.js
CHANGED
|
@@ -37,17 +37,18 @@ export function renderContextSection(context) {
|
|
|
37
37
|
const tools = s.allowedTools && s.allowedTools.length
|
|
38
38
|
? ' [tools: ' + s.allowedTools.join(', ') + ']'
|
|
39
39
|
: '';
|
|
40
|
+
// A built-in skill (no file on disk) says so instead of an empty path.
|
|
41
|
+
const loc = s.path ? '\n file: ' + s.path : '\n (built-in)';
|
|
40
42
|
return ('- ' +
|
|
41
43
|
s.name +
|
|
42
44
|
tools +
|
|
43
45
|
': ' +
|
|
44
46
|
(s.description || '(no description)') +
|
|
45
|
-
|
|
46
|
-
s.path);
|
|
47
|
+
loc);
|
|
47
48
|
});
|
|
48
49
|
out +=
|
|
49
50
|
'\n## Skills\n\n' +
|
|
50
|
-
'Specialized workflows
|
|
51
|
+
'Specialized workflows. A skill with a `file:` is a SKILL.md on disk (read it when it applies); a `(built-in)` skill has no file — the operator can invoke it with /<name> and its instructions arrive as the task. Skills are progressive disclosure: only read the body when the skill applies.\n\n' +
|
|
51
52
|
lines.join('\n') +
|
|
52
53
|
'\n';
|
|
53
54
|
}
|