From 05acd261925f41f0599602ef069355a2ed0afa84 Mon Sep 17 00:00:00 2001 From: Emre Sokullu Date: Sun, 9 Aug 2026 00:08:42 +0300 Subject: [PATCH] Normalize empty tool arguments --- src/chrome/src/agent/agent.js | 10 ++++- src/firefox/src/agent/agent.js | 10 ++++- test/run.js | 75 +++++++++++++++++++++++++++++++++- 3 files changed, 92 insertions(+), 3 deletions(-) diff --git a/src/chrome/src/agent/agent.js b/src/chrome/src/agent/agent.js index bd11c4f18..d992ebf29 100644 --- a/src/chrome/src/agent/agent.js +++ b/src/chrome/src/agent/agent.js @@ -17512,7 +17512,9 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d return await downloadResourceFromPage(tabId, args); } if (name === 'download_files' || name === 'download_file') { - if (args.url && !args.urls) args.urls = [args.url]; + if (args.url && (!Array.isArray(args.urls) || args.urls.length === 0)) { + args.urls = [args.url]; + } return await downloadFiles(args); } @@ -18828,6 +18830,12 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d if (name === 'upload_file') { args = args || {}; + // Some providers materialize omitted optional schema properties as empty + // strings. Treat those placeholders as absent so a valid downloadId does + // not conflict with a nonexistent user attachment or local path. + for (const key of ['selector', 'attachmentId', 'filePath']) { + if (typeof args[key] === 'string' && !args[key].trim()) delete args[key]; + } const compactUpload = ( executionContext?.promptTier || this._resolvePromptTier() ) === 'compact'; diff --git a/src/firefox/src/agent/agent.js b/src/firefox/src/agent/agent.js index 9e483aca4..e393f01ec 100644 --- a/src/firefox/src/agent/agent.js +++ b/src/firefox/src/agent/agent.js @@ -15331,13 +15331,21 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d return await downloadResourceFromPage(tabId, args); } if (name === 'download_files') { - if (args.url && !args.urls) args.urls = [args.url]; + if (args.url && (!Array.isArray(args.urls) || args.urls.length === 0)) { + args.urls = [args.url]; + } return await downloadFiles(args); } if (name === 'upload_file') { const UPLOAD_MAX_BYTES = 25 * 1024 * 1024; try { args = args || {}; + // Some providers materialize omitted optional schema properties as empty + // strings. Treat those placeholders as absent so a valid downloadId does + // not conflict with a nonexistent user attachment or local path. + for (const key of ['selector', 'attachmentId', 'filePath']) { + if (typeof args[key] === 'string' && !args[key].trim()) delete args[key]; + } const compactUpload = ( executionContext?.promptTier || this._resolvePromptTier() ) === 'compact'; diff --git a/test/run.js b/test/run.js index 98cb105a6..9cdcdbf65 100644 --- a/test/run.js +++ b/test/run.js @@ -54546,6 +54546,72 @@ test('download_files digest echoes safe downloadIds but never the filename (chro } }); +test('download_files falls back to singular url when providers emit an empty urls placeholder', async () => { + const originalChrome = globalThis.chrome; + const originalBrowser = globalThis.browser; + const url = 'https://example.com/webbrain-firefox.zip'; + try { + let chromeDownloadOptions = null; + globalThis.chrome = { + runtime: { lastError: null }, + downloads: { + download(options, cb) { + chromeDownloadOptions = options; + cb(468); + }, + search(_query, cb) { + cb([{ + id: 468, + filename: '/Users/test/Downloads/webbrain-firefox.zip', + state: 'complete', + bytesReceived: 10, + totalBytes: 10, + }]); + }, + }, + }; + const chromeArgs = { url, urls: [], filename: 'webbrain-firefox.zip' }; + const chromeResult = await new AgentCh({}).executeTool(42, 'download_files', chromeArgs); + assert.equal(chromeResult.success, true); + assert.deepEqual(chromeArgs.urls, [url]); + assert.equal(chromeDownloadOptions.url, url); + + let firefoxDownloadOptions = null; + globalThis.browser = { + storage: { + local: { + async get() { return { downloadDirectory: '' }; }, + }, + }, + downloads: { + async download(options) { + firefoxDownloadOptions = options; + return 469; + }, + async search() { + return [{ + id: 469, + filename: '/Users/test/Downloads/webbrain-firefox.zip', + state: 'complete', + bytesReceived: 10, + totalBytes: 10, + }]; + }, + }, + }; + const firefoxArgs = { url, urls: [], filename: 'webbrain-firefox.zip' }; + const firefoxResult = await new AgentFx({}).executeTool(42, 'download_files', firefoxArgs); + assert.equal(firefoxResult.success, true); + assert.deepEqual(firefoxArgs.urls, [url]); + assert.equal(firefoxDownloadOptions.url, url); + } finally { + if (originalChrome === undefined) delete globalThis.chrome; + else globalThis.chrome = originalChrome; + if (originalBrowser === undefined) delete globalThis.browser; + else globalThis.browser = originalBrowser; + } +}); + test('download_files treats interrupted browser downloads as failed (chrome & firefox)', async () => { const originalChrome = globalThis.chrome; const originalBrowser = globalThis.browser; @@ -55202,7 +55268,9 @@ test('upload_file prefers a valid downloadId and falls back to filePath for an i cdpClientCh.getFileInputFiles = async () => []; const consumed = await agent.executeTool(42, 'upload_file', { selector: 'input[type=file]', + attachmentId: '', downloadId: 9123, + filePath: '', }); assert.equal(consumed.success, true); assert.equal(consumed.attachmentState, 'page_consumed'); @@ -55680,7 +55748,12 @@ test('upload_file (firefox) re-fetches downloadId with manual redirect handling }; const agent = new AgentFx({}); - const args = { selector: 'input[type=file]', downloadId: 8123 }; + const args = { + selector: 'input[type=file]', + attachmentId: '', + downloadId: 8123, + filePath: '', + }; const result = await agent.executeTool(42, 'upload_file', args); assert.equal(result.success, true);