diff --git a/src/chrome/src/agent/tools.js b/src/chrome/src/agent/tools.js index 89f88ec8e..9ebc3d767 100644 --- a/src/chrome/src/agent/tools.js +++ b/src/chrome/src/agent/tools.js @@ -1266,6 +1266,25 @@ const WATCH_BEEP_TOOL = { }, }; +function compactUploadFileTool(tool) { + return { + ...tool, + function: { + ...tool.function, + description: 'Attach a user-provided file directly to an existing file input without clicking the page upload control. Use attachmentId from the current user-attachment notice, or an absolute filePath the user supplied. This proves only local page attachment, not remote upload or submission. Use the exact selector for the intended input; never guess a generic input[type="file"] selector when multiple inputs exist. If the widget creates its input lazily, make one guarded click on its add-files control, then retry upload_file with the exact selector.', + parameters: { + type: 'object', + properties: { + selector: { type: 'string', description: 'Exact CSS selector for the intended file input.' }, + attachmentId: { type: 'string', description: 'Opaque id from the current user-attachment notice. Never guess an id.' }, + filePath: { type: 'string', description: 'Absolute local path explicitly supplied by the user. Optional when attachmentId is given.' }, + }, + required: ['selector'], + }, + }, + }; +} + /** * Get tools filtered by mode. * @@ -1285,7 +1304,9 @@ export function getToolsForMode(mode, opts = {}) { } else if (devCompactBlocked) { base = []; } else if (tier === 'compact') { - base = AGENT_TOOLS.filter(t => COMPACT_TOOL_NAMES.has(t.function.name)); + base = AGENT_TOOLS + .filter(t => COMPACT_TOOL_NAMES.has(t.function.name)) + .map(t => (t.function.name === 'upload_file' ? compactUploadFileTool(t) : t)); } else if (tier === 'mid') { base = AGENT_TOOLS.filter(t => MID_TOOL_NAMES.has(t.function.name)); } else { @@ -1702,6 +1723,7 @@ export const COMPACT_TOOL_NAMES = new Set([ 'click', 'type_text', 'press_keys', 'navigate', 'new_tab', 'wait_for_element', 'fetch_url', + 'upload_file', 'scratchpad_write', 'progress_update', 'progress_read', 'clarify', 'done', ]); @@ -1747,6 +1769,7 @@ TOOLS — use ONLY these: - new_tab({url}): Open a URL in a background tab for user reference. It does not activate or retarget the current run, so never use it as a site-permission workaround. - wait_for_element({selector}): Wait for an element to appear. - fetch_url({url}): Fetch a URL for its content. +- upload_file({selector, attachmentId|filePath}): Attach a user-provided file directly to an existing file input; do not click the page upload control first. Use the exact selector and never guess generic input[type="file"] when multiple inputs exist. If the widget creates its input lazily, make one guarded initializer click, re-read the page, then retry with the exact selector. Verify the page shows the attachment before submitting. - scratchpad_write({text}): Save notes that persist across steps. - progress_update({items}) / progress_read({status}): Structured progress ledger for the active repeated item/action task. On GitHub stargazers, only "Follow USER" buttons are follow targets when following is allowed by the task; "Unfollow USER" means skip/already followed unless the ledger shows acted. - done({summary, outcome}): Signal success, partial progress, or a failed blocker. diff --git a/src/firefox/src/agent/tools.js b/src/firefox/src/agent/tools.js index 966bb4942..8d4eeffaa 100644 --- a/src/firefox/src/agent/tools.js +++ b/src/firefox/src/agent/tools.js @@ -993,6 +993,7 @@ export const COMPACT_TOOL_NAMES = new Set([ 'click', 'type_text', 'press_keys', 'navigate', 'new_tab', 'wait_for_element', 'fetch_url', + 'upload_file', 'scratchpad_write', 'progress_update', 'progress_read', 'clarify', 'done', ]); @@ -1114,6 +1115,24 @@ const WATCH_BEEP_TOOL = { }, }; +function compactUploadFileTool(tool) { + return { + ...tool, + function: { + ...tool.function, + description: 'Attach a user-provided file directly to an existing file input without clicking the page upload control. Use attachmentId from the current user-attachment notice, or omit it to open WebBrain\'s file picker. This proves only local page attachment, not remote upload or submission. Use the exact selector for the intended input; never guess a generic input[type="file"] selector when multiple inputs exist. If the widget creates its input lazily, make one guarded click on its add-files control, then retry upload_file with the exact selector.', + parameters: { + type: 'object', + properties: { + selector: { type: 'string', description: 'Exact CSS selector for the intended file input.' }, + attachmentId: { type: 'string', description: 'Opaque id from the current user-attachment notice. Omit only to ask the user through WebBrain\'s file picker; never guess an id.' }, + }, + required: ['selector'], + }, + }, + }; +} + /** * Get tools filtered by mode. * @@ -1132,7 +1151,9 @@ export function getToolsForMode(mode, opts = {}) { } else if (devCompactBlocked) { base = []; } else if (tier === 'compact') { - base = AGENT_TOOLS.filter(t => COMPACT_TOOL_NAMES.has(t.function.name)); + base = AGENT_TOOLS + .filter(t => COMPACT_TOOL_NAMES.has(t.function.name)) + .map(t => (t.function.name === 'upload_file' ? compactUploadFileTool(t) : t)); } else if (tier === 'mid') { base = AGENT_TOOLS.filter(t => MID_TOOL_NAMES.has(t.function.name)); } else { @@ -1230,6 +1251,7 @@ TOOLS - use only these: - new_tab({url}): Open a URL in a background tab for user reference. It does not activate or retarget the current run, so never use it as a site-permission workaround. - wait_for_element({selector}): Wait for an element to appear. - fetch_url({url}): Fetch other URLs for reading only; do not use it to re-read the active tab. +- upload_file({selector, attachmentId?}): Attach a user-provided file directly to an existing file input; do not click the page upload control first. Use the current attachmentId or omit it for WebBrain's picker. Use the exact selector and never guess generic input[type="file"] when multiple inputs exist. If the widget creates its input lazily, make one guarded initializer click, re-read the page, then retry with the exact selector. Verify the page shows the attachment before submitting. - scratchpad_write({text}): Save notes that persist across steps. - progress_update({items}) / progress_read({status}): Structured progress ledger for the active repeated item/action task. On GitHub stargazers, only "Follow USER" buttons are follow targets when following is allowed by the task; "Unfollow USER" means skip/already followed unless the ledger shows acted. - clarify({question, options?}): Ask the user only when materially blocked or ambiguous. Unanswered clarifies auto-select options[0] after timeout (source=timeout is not user approval for high-risk steps; source=auto Instant is intentional auto-approve). diff --git a/test/run.js b/test/run.js index 8c24ea4a2..00c1c93e6 100644 --- a/test/run.js +++ b/test/run.js @@ -12785,6 +12785,7 @@ test('getToolsForMode: compact mode restricts act tools in both browsers', () => [...compactNames].sort(), ); assert.ok(compactNamesActual.includes('done'), `[${label}] compact mode must keep done`); + assert.ok(compactNamesActual.includes('upload_file'), `[${label}] compact mode must expose upload_file`); for (const excluded of ['resize_window', 'download_social_media', 'solve_captcha']) { assert.equal(compactNamesActual.includes(excluded), false, `[${label}] compact mode must omit ${excluded}`); } @@ -12792,6 +12793,49 @@ test('getToolsForMode: compact mode restricts act tools in both browsers', () => } }); +test('compact Act exposes a direct-upload-only file workflow in both browsers', () => { + for (const [label, getTools, prompt] of [ + ['chrome', getToolsForModeCh, SYSTEM_PROMPT_ACT_COMPACT_CH], + ['firefox', getToolsForModeFx, SYSTEM_PROMPT_ACT_COMPACT_FX], + ]) { + const askNames = getTools('ask').map(tool => tool.function.name); + const compactTools = getTools('act', { tier: 'compact' }); + const compactNames = compactTools.map(tool => tool.function.name); + const upload = compactTools.find(tool => tool.function.name === 'upload_file'); + const fullUpload = getTools('act').find(tool => tool.function.name === 'upload_file'); + + assert.ok(upload, `[${label}] compact Act must expose upload_file`); + assert.equal(askNames.includes('upload_file'), false, `[${label}] Ask must remain read-only`); + for (const unavailable of [ + 'download_files', + 'download_resource_from_page', + 'list_downloads', + 'read_downloaded_file', + ]) { + assert.equal(compactNames.includes(unavailable), false, `[${label}] compact Act exposed ${unavailable}`); + } + assert.match(upload.function.description, /directly to an existing file input/i); + assert.match(upload.function.description, /without clicking the page upload control/i); + assert.match(upload.function.description, /exact selector/i); + assert.match(upload.function.description, /one guarded click/i); + assert.doesNotMatch(upload.function.description, /download_files|list_downloads|downloadId/i); + assert.ok(upload.function.parameters.properties.attachmentId, `[${label}] compact upload must accept attachmentId`); + assert.equal(upload.function.parameters.properties.downloadId, undefined, `[${label}] compact upload must hide downloadId`); + assert.ok(fullUpload.function.parameters.properties.downloadId, `[${label}] full upload must retain downloadId`); + assert.match(prompt, /upload_file[\s\S]{0,180}do not click the page upload control/i); + assert.match(prompt, /upload_file[\s\S]{0,360}exact selector/i); + assert.match(prompt, /upload_file[\s\S]{0,420}one guarded initializer click/i); + assert.doesNotMatch(prompt, /download_files|list_downloads|downloadId/i); + + if (label === 'chrome') { + assert.ok(upload.function.parameters.properties.filePath, 'chrome: compact upload must accept a user-supplied filePath'); + } else { + assert.equal(upload.function.parameters.properties.filePath, undefined, 'firefox: compact upload must not invent filePath support'); + assert.match(upload.function.description, /WebBrain's file picker/i); + } + } +}); + test('getToolsForMode: mode/tier redesign exposes the intended normal and Dev tools', () => { for (const [label, getTools] of [ ['chrome', getToolsForModeCh], @@ -50221,7 +50265,7 @@ test('user attachment upload guidance follows the active tier tool catalog', () ]); for (const [mode, tier, shouldAdvertiseUpload] of [ - ['act', 'compact', false], + ['act', 'compact', true], ['act', 'mid', true], ['act', 'full', true], ['ask', 'full', false],