Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion src/chrome/src/agent/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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 {
Expand Down Expand Up @@ -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',
]);

Expand Down Expand Up @@ -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.
Expand Down
24 changes: 23 additions & 1 deletion src/firefox/src/agent/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
]);

Expand Down Expand Up @@ -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.
*
Expand All @@ -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 {
Expand Down Expand Up @@ -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).
Expand Down
46 changes: 45 additions & 1 deletion test/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -12785,13 +12785,57 @@ 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}`);
}
assert.equal(compactNamesActual.includes('execute_js'), false, `[${label}] compact mode must omit execute_js`);
}
});

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],
Expand Down Expand Up @@ -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],
Expand Down
Loading