zames_pro 2.8.0 → 2.9.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 +29 -0
- package/dist/attachments.js +65 -0
- package/dist/index.js +21 -1
- package/package.json +1 -1
package/dist/agent-loop.js
CHANGED
|
@@ -80,6 +80,14 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
80
80
|
// chatty model.
|
|
81
81
|
let looksDoneRetries = 0;
|
|
82
82
|
const MAX_LOOKSDONE_RETRIES = 3;
|
|
83
|
+
// Watchdog against the agent emitting a tool call and then going silent.
|
|
84
|
+
// After a tool result the expected next answer is a fresh tool call; if we
|
|
85
|
+
// instead get an EMPTY answer or the EXACT same answer as the previous turn
|
|
86
|
+
// (a sign the new message was not sent), we nudge instead of stopping.
|
|
87
|
+
let lastRaw = '';
|
|
88
|
+
let justRanTool = false;
|
|
89
|
+
let watchdogRetries = 0;
|
|
90
|
+
const MAX_WATCHDOG_RETRIES = 3;
|
|
83
91
|
for (let i = 0; i < maxIterations; i++) {
|
|
84
92
|
onThinking();
|
|
85
93
|
// The first message (task) is user input: no throttle.
|
|
@@ -97,6 +105,23 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
97
105
|
transcript?.log('user_aborted');
|
|
98
106
|
return rawResponse;
|
|
99
107
|
}
|
|
108
|
+
// Watchdog: after a tool result we expect a FRESH tool call. If the answer
|
|
109
|
+
// is empty or identical to the previous turn (the new message was not
|
|
110
|
+
// sent), nudge instead of stopping.
|
|
111
|
+
const wdEmpty = !String(rawResponse || '').trim();
|
|
112
|
+
const wdStale = justRanTool && lastRaw.trim() !== '' && rawResponse.trim() === lastRaw.trim();
|
|
113
|
+
if (!isFirst && (wdEmpty || wdStale) && watchdogRetries < MAX_WATCHDOG_RETRIES) {
|
|
114
|
+
watchdogRetries++;
|
|
115
|
+
transcript?.log('watchdog_nudge', {
|
|
116
|
+
attempt: watchdogRetries,
|
|
117
|
+
empty: wdEmpty,
|
|
118
|
+
stale: wdStale,
|
|
119
|
+
response: String(rawResponse || '').slice(0, 200),
|
|
120
|
+
});
|
|
121
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
lastRaw = rawResponse;
|
|
100
125
|
const parsed = parseToolCall(rawResponse);
|
|
101
126
|
if (parsed) {
|
|
102
127
|
const thought = extractPreToolText(rawResponse);
|
|
@@ -259,6 +284,10 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
259
284
|
});
|
|
260
285
|
results.push({ tool: call.tool, result });
|
|
261
286
|
}
|
|
287
|
+
// A tool just ran — the next answer is expected to be a fresh tool call.
|
|
288
|
+
// Reset the watchdog so the next empty/repeated answer is nudged.
|
|
289
|
+
justRanTool = true;
|
|
290
|
+
watchdogRetries = 0;
|
|
262
291
|
if (results.length === 1) {
|
|
263
292
|
const r = results[0];
|
|
264
293
|
const resultStr = typeof r.result === 'string' ? r.result : JSON.stringify(r.result);
|
package/dist/attachments.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
3
4
|
import { execFileSync } from 'child_process';
|
|
4
5
|
export const IMAGE_EXTS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg', '.ico'];
|
|
5
6
|
export const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
|
|
@@ -185,11 +186,42 @@ export async function saveToTemp(tmpDir, name, data) {
|
|
|
185
186
|
// the current platform. Returns the raw bytes plus a short note about which
|
|
186
187
|
// tool was used (or the reason nothing was found) — the caller shows that note
|
|
187
188
|
// to the user so a failed paste is never silent.
|
|
189
|
+
// True when running inside WSL. There the X11 clipboard is a SEPARATE
|
|
190
|
+
// clipboard from the Windows one: the user copies an image in Windows, so we
|
|
191
|
+
// must read it through powershell.exe, not xclip.
|
|
192
|
+
export function isWsl() {
|
|
193
|
+
if (process.platform !== 'linux')
|
|
194
|
+
return false;
|
|
195
|
+
if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP)
|
|
196
|
+
return true;
|
|
197
|
+
try {
|
|
198
|
+
const rel = os.release().toLowerCase();
|
|
199
|
+
if (rel.includes('microsoft') || rel.includes('wsl'))
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
catch { }
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
// PowerShell one-liner: write the clipboard image as raw PNG bytes to stdout.
|
|
206
|
+
const PS_GET_IMAGE = 'Add-Type -AssemblyName System.Windows.Forms,System.Drawing; ' +
|
|
207
|
+
'$img=[System.Windows.Forms.Clipboard]::GetImage(); ' +
|
|
208
|
+
'if($img){$ms=New-Object System.IO.MemoryStream; ' +
|
|
209
|
+
'$img.Save($ms,[System.Drawing.Imaging.ImageFormat]::Png); ' +
|
|
210
|
+
'[Console]::OpenStandardOutput().Write($ms.ToArray(),0,$ms.Length)}';
|
|
211
|
+
// PowerShell one-liner: write the clipboard file paths (CF_HDROP) to stdout.
|
|
212
|
+
const PS_GET_FILE_DROP = 'Add-Type -AssemblyName System.Windows.Forms; ' +
|
|
213
|
+
'$d=[System.Windows.Forms.Clipboard]::GetDataObject(); ' +
|
|
214
|
+
'if($d -and $d.GetDataPresent([System.Windows.Forms.DataFormats]::FileDrop)){' +
|
|
215
|
+
'$f=$d.GetData([System.Windows.Forms.DataFormats]::FileDrop); $f -join [Environment]::NewLine}';
|
|
188
216
|
export function readClipboardImageDetailed() {
|
|
189
217
|
if (process.platform === 'linux' || process.platform === 'freebsd' || process.platform === 'openbsd') {
|
|
190
218
|
const wayland = process.env.WAYLAND_DISPLAY;
|
|
191
219
|
const x11 = process.env.DISPLAY;
|
|
192
220
|
const attempts = [];
|
|
221
|
+
// On WSL the Windows clipboard is what the user copies into — read it first.
|
|
222
|
+
if (isWsl()) {
|
|
223
|
+
attempts.push({ bin: 'powershell.exe', args: ['-NoProfile', '-NonInteractive', '-Command', PS_GET_IMAGE], via: 'powershell.exe' });
|
|
224
|
+
}
|
|
193
225
|
if (wayland || !x11)
|
|
194
226
|
attempts.push({ bin: 'wl-paste', args: ['--type', 'image/png', '--no-newline'], via: 'wl-paste' });
|
|
195
227
|
if (x11 || !wayland) {
|
|
@@ -265,3 +297,36 @@ export async function findFileByName(workdir, name) {
|
|
|
265
297
|
catch { }
|
|
266
298
|
return null;
|
|
267
299
|
}
|
|
300
|
+
// Read file paths from the Windows clipboard (copied files, CF_HDROP) and
|
|
301
|
+
// translate them to WSL paths. On WSL the user often copies a FILE, not an
|
|
302
|
+
// image. Returns [] on non-WSL / when nothing is there.
|
|
303
|
+
export function readWindowsClipboardFiles() {
|
|
304
|
+
if (!isWsl())
|
|
305
|
+
return [];
|
|
306
|
+
const out = tryCommand('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', PS_GET_FILE_DROP]);
|
|
307
|
+
if (!out)
|
|
308
|
+
return [];
|
|
309
|
+
const text = out.toString('utf-8');
|
|
310
|
+
return text
|
|
311
|
+
.split(/\r?\n/)
|
|
312
|
+
.map((s) => s.trim())
|
|
313
|
+
.filter(Boolean)
|
|
314
|
+
.map(winPathToWsl)
|
|
315
|
+
.filter((p) => !!p);
|
|
316
|
+
}
|
|
317
|
+
// Convert a Windows path (C:\\Users\\me\\a.png) into a WSL path
|
|
318
|
+
// (/mnt/c/Users/me/a.png). Leaves already-POSIX paths untouched.
|
|
319
|
+
export function winPathToWsl(winPath) {
|
|
320
|
+
let p = String(winPath || '').trim();
|
|
321
|
+
if (p.startsWith('"') && p.endsWith('"'))
|
|
322
|
+
p = p.slice(1, -1);
|
|
323
|
+
if (!p)
|
|
324
|
+
return null;
|
|
325
|
+
if (p.startsWith('/'))
|
|
326
|
+
return p;
|
|
327
|
+
if (p.charAt(1) !== ':' || (p.charAt(2) !== '/' && p.charAt(2) !== '\\'))
|
|
328
|
+
return null;
|
|
329
|
+
const drive = p.charAt(0).toLowerCase();
|
|
330
|
+
const rest = p.slice(3).split('\\').join('/');
|
|
331
|
+
return '/mnt/' + drive + '/' + rest;
|
|
332
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { createTools } from './tools.js';
|
|
|
8
8
|
import { runAgentLoop } from './agent-loop.js';
|
|
9
9
|
import { createSpinner } from './spinner.js';
|
|
10
10
|
import { LineEditor, expandPastes, pasteReplacement } from './input.js';
|
|
11
|
-
import { parseImagePaste, extForMime, saveToTemp, guessMime, isImageName, formatSize, looksLikeFilePath, readClipboardImageDetailed, sniffMime, } from './attachments.js';
|
|
11
|
+
import { parseImagePaste, extForMime, saveToTemp, guessMime, isImageName, formatSize, looksLikeFilePath, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, } from './attachments.js';
|
|
12
12
|
import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, } from './config.js';
|
|
13
13
|
import { runConfigMenu } from './config-menu.js';
|
|
14
14
|
import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n.js';
|
|
@@ -997,6 +997,26 @@ async function main() {
|
|
|
997
997
|
ed.onClipboard = async () => {
|
|
998
998
|
const res = readClipboardImageDetailed();
|
|
999
999
|
if (!res.data || !res.data.length) {
|
|
1000
|
+
// On WSL the user may have copied a FILE in Windows (not an image):
|
|
1001
|
+
// the Windows clipboard holds its path - attach it directly.
|
|
1002
|
+
for (const fp of readWindowsClipboardFiles()) {
|
|
1003
|
+
const data = await fs.readFile(fp).catch(() => null);
|
|
1004
|
+
if (!data)
|
|
1005
|
+
continue;
|
|
1006
|
+
const nm = path.basename(fp);
|
|
1007
|
+
const att = ed.attachments.add({
|
|
1008
|
+
path: fp,
|
|
1009
|
+
name: nm,
|
|
1010
|
+
mime: guessMime(nm),
|
|
1011
|
+
size: data.length,
|
|
1012
|
+
});
|
|
1013
|
+
ed.printAbove(theme.system(t('msg.attached_file', {
|
|
1014
|
+
marker: att.marker,
|
|
1015
|
+
name: nm,
|
|
1016
|
+
size: formatSize(data.length),
|
|
1017
|
+
}) + theme.dim(' (windows-clipboard)')));
|
|
1018
|
+
return att;
|
|
1019
|
+
}
|
|
1000
1020
|
if (!clipboardWarned) {
|
|
1001
1021
|
clipboardWarned = true;
|
|
1002
1022
|
ed.printAbove(theme.warn(t('msg.clip_empty', { via: res.via }) +
|