Skip to content
292 changes: 275 additions & 17 deletions src/chrome/src/agent/agent.js

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/chrome/src/agent/planner.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ Rules:
- respond when the user asks only for a natural-language answer or recoverable artifact from the existing conversation/working notes and no fresh page read or browser action is needed.
- plan_only when the user asks for a plan, outline, strategy, or discussion without authorizing action.
- clarify only when missing or conflicting user information prevents a useful plan; make localized.summary the concise question to ask.
- A request to answer, summarize, explain, analyze, or draft a response about currently visible/open page content is execute when producing the answer needs a fresh page or browser read, even if the final deliverable is only text and requires_state_change is false. Example: "How should I respond to this open email?" is execute because the email must be read now; it is not plan_only merely because the deliverable is advice or a draft.
- respond must not include steps that need page, browser, network, memory, or scheduling tools. If any such tool is needed to produce the requested answer, classify the request as execute instead.
- requires_state_change is true only when completing an execute request needs a mutation such as interacting with form/account state, modifying page data, downloading/uploading a file, a write-method network request, a Dev patch, or scheduling work. It is false for reads, analysis, summaries, navigation, scrolling, hovering, window/viewport changes, plan_only, and clarify.
- requires_submission is true only when completing an execute request requires an explicit form/dialog commit action such as Submit, Save, Send, Publish, Post, or Confirm. It is false for filling, editing, checking, or selecting without committing, including explicit do-not-submit tasks and autosave UIs, and false for non-execute requests.
- allows_planner_shaped_result is true only when the user explicitly requests planner-like final data (summary/steps JSON or Plan/Steps/Workflow markdown). Never changes request_kind.
Expand Down Expand Up @@ -114,6 +116,8 @@ Rules:
- respond means the user asks only for a natural-language answer or recoverable artifact from existing conversation/working-note context, with no fresh page read or browser action.
- plan_only means the user asks for a plan, outline, strategy, or discussion without authorizing action.
- clarify means missing or conflicting user information prevents a useful plan; localized.summary must be the concise question to ask.
- A request to answer, summarize, explain, analyze, or draft a response about currently visible/open page content is execute when producing the answer needs a fresh page or browser read, even if the final deliverable is only text and requires_state_change is false. Example: "How should I respond to this open email?" is execute because the email must be read now.
- respond must not include steps that need page, browser, network, memory, or scheduling tools. If any such tool is needed to produce the requested answer, classify the request as execute instead.
- requires_state_change is true only when an execute request needs a mutation such as interacting with form/account state, modifying page data, downloading/uploading a file, a write-method network request, a Dev patch, or scheduling work. It is false for reads, analysis, summaries, navigation, scrolling, hovering, window/viewport changes, plan_only, and clarify.
- requires_submission is true only when an execute request must explicitly commit a form/dialog with an action such as Submit, Save, Send, Publish, Post, or Confirm. It is false for filling, editing, checking, or selecting without committing, including explicit do-not-submit tasks and autosave UIs, and false for non-execute requests.
- allows_planner_shaped_result is true only when the user explicitly requests planner-like final data (summary/steps JSON or Plan/Steps/Workflow markdown). Never changes request_kind.
Expand Down
13 changes: 11 additions & 2 deletions src/chrome/src/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -1380,16 +1380,25 @@ function isClarificationRequiredRunUpdate(update) {
&& update?.data?.status === 'clarification_required';
}

function isPlannerRequestFailureUpdate(update) {
return update?.type === 'warning'
&& update?.data?.code === 'planner_request_failed';
}

function runUpdatesSucceeded(updates = []) {
return !updates.some(update => update?.type === 'error' || isClarificationRequiredRunUpdate(update));
return !updates.some(update => (
update?.type === 'error'
|| isClarificationRequiredRunUpdate(update)
|| isPlannerRequestFailureUpdate(update)
));
}

function terminalRunUiStatus(content, updates = [], error = null) {
if (error) return 'failed';
const text = String(content || '');
if (/stopped by user|aborted by user/i.test(text)) return 'stopped';
if (/before executing requested tool calls/i.test(text)) return 'cancelled';
if (updates.some(update => update?.type === 'error')) return 'failed';
if (updates.some(update => update?.type === 'error' || isPlannerRequestFailureUpdate(update))) return 'failed';
if (updates.some(isClarificationRequiredRunUpdate)) return 'clarification_required';
return 'completed';
}
Expand Down
10 changes: 8 additions & 2 deletions src/chrome/src/providers/openai.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
shouldUseOpenAIResponsesApi,
supportsOpenAIAskStreaming,
} from './provider-compatibility.js';
import { normalizeRuntimeTraceConfig } from '../trace/runtime-config.js';

const OPENAI_RESPONSES_MIN_MAX_OUTPUT_TOKENS = 16;
const KIMI_CURRENT_TOOL_REASONING_MODELS = new Set([
Expand Down Expand Up @@ -235,11 +236,16 @@ export class OpenAICompatibleProvider extends BaseLLMProvider {
const sessionId = String(options.webbrainSessionId || '').trim();
if (sessionId) body.session_id = sessionId.slice(0, 200);
const generationName = String(options.webbrainGenerationName || '').trim().toLowerCase();
if (generationName) {
const runtimeConfig = normalizeRuntimeTraceConfig(options.webbrainRuntimeConfig);
if (generationName || runtimeConfig) {
const trace = body.trace && typeof body.trace === 'object' && !Array.isArray(body.trace)
? body.trace
: {};
body.trace = { ...trace, generation_name: generationName.slice(0, 64) };
body.trace = {
...trace,
...(generationName ? { generation_name: generationName.slice(0, 64) } : {}),
...(runtimeConfig ? { runtime_config: runtimeConfig } : {}),
};
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/chrome/src/trace/recorder.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { normalizeRuntimeTraceConfig } from './runtime-config.js';

/**
* Trace recorder — writes per-run traces (LLM requests/responses, tool calls,
* screenshots) into IndexedDB for later inspection and cross-model comparison.
Expand Down Expand Up @@ -130,6 +132,7 @@ export async function startRun(meta = {}) {
providerId: meta.providerId || '',
providerClass: meta.providerClass || '',
webbrainVersion: meta.webbrainVersion || '',
runtimeConfig: normalizeRuntimeTraceConfig(meta.runtimeConfig),
userMessage: meta.userMessage || '',
tabUrl: meta.tabUrl || '',
tabTitle: meta.tabTitle || '',
Expand Down
59 changes: 59 additions & 0 deletions src/chrome/src/trace/runtime-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const STRING_ENUMS = Object.freeze({
browser_target: new Set(['chrome', 'firefox']),
mode: new Set(['ask', 'act', 'dev']),
prompt_tier: new Set(['compact', 'mid', 'full']),
plan_before_act_mode: new Set(['off', 'try', 'strict']),
auto_screenshot: new Set(['off', 'navigation', 'state_change', 'every_step']),
image_detail: new Set(['auto', 'low', 'high']),
});

const BOOLEAN_FIELDS = Object.freeze([
'screenshot_redaction',
'strict_secret_mode',
'use_site_adapters',
'web_mcp_enabled',
'api_mutations_allowed',
'user_memory_enabled',
'selection_grounded',
]);

const INTEGER_RANGES = Object.freeze({
max_agent_steps: [0, 10_000],
max_image_dimension: [256, 16_384],
max_screenshots_per_turn: [0, 1_000],
});

function safeVersion(value) {
const version = String(value || '').trim();
return /^[0-9A-Za-z][0-9A-Za-z.+_-]{0,63}$/.test(version) ? version : '';
}
Comment on lines +26 to +29

/**
* Runtime trace metadata crosses both a provider boundary and an export
* boundary. Keep it to a small, versioned allowlist of booleans, bounded
* integers, and enums so a caller can never smuggle credentials or profile
* contents into a trace by passing an arbitrary settings object.
*/
export function normalizeRuntimeTraceConfig(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;

const normalized = { schema_version: 1 };
const extensionVersion = safeVersion(value.extension_version);
if (extensionVersion) normalized.extension_version = extensionVersion;

for (const [field, allowed] of Object.entries(STRING_ENUMS)) {
const candidate = String(value[field] || '').trim().toLowerCase();
if (allowed.has(candidate)) normalized[field] = candidate;
}
for (const field of BOOLEAN_FIELDS) {
if (typeof value[field] === 'boolean') normalized[field] = value[field];
}
for (const [field, [min, max]] of Object.entries(INTEGER_RANGES)) {
const candidate = Number(value[field]);
if (Number.isInteger(candidate) && candidate >= min && candidate <= max) {
normalized[field] = candidate;
}
}

return normalized;
}
121 changes: 116 additions & 5 deletions src/chrome/src/ui/sidepanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -1703,10 +1703,10 @@ function releaseRetryAttachmentPayload(retryId) {

function releaseRetryAttachmentsInTree(root) {
if (!root) return;
if (root.matches?.('.error-retry-btn[data-retry-id], .cost-allowance-retry-btn[data-retry-id]')) {
if (root.matches?.('.error-retry-btn[data-retry-id], .cost-allowance-retry-btn[data-retry-id], .planner-request-failure-retry-btn[data-retry-id]')) {
releaseRetryAttachmentPayload(root.dataset.retryId);
}
root.querySelectorAll?.('.error-retry-btn[data-retry-id], .cost-allowance-retry-btn[data-retry-id]').forEach((btn) => {
root.querySelectorAll?.('.error-retry-btn[data-retry-id], .cost-allowance-retry-btn[data-retry-id], .planner-request-failure-retry-btn[data-retry-id]').forEach((btn) => {
releaseRetryAttachmentPayload(btn.dataset.retryId);
});
}
Expand Down Expand Up @@ -3738,6 +3738,14 @@ async function adoptRestoredRunState(tabId, state) {
probeFirst: true,
requireDurableSubmittedTurn: runUi.kind !== 'continue',
});
const returnedPlannerFailure = plannerRequestFailureUpdate(res?.updates);
if (returnedPlannerFailure && sameTabId(currentTabId, tabId) && !isTabAbortRequested(tabId)) {
renderPlannerRequestFailure(
assistantEl,
returnedPlannerFailure.data,
retryPayloadForRunAssistant(assistantEl),
);
}
const returnedErrorUpdate = Array.isArray(res?.updates)
? res.updates.find(update => update?.type === 'error')
: null;
Expand Down Expand Up @@ -5165,7 +5173,7 @@ function bindErrorRetryButton(btn) {
}

function rebindRetryButtons() {
document.querySelectorAll('.error-retry-btn').forEach(bindErrorRetryButton);
document.querySelectorAll('.error-retry-btn, .planner-request-failure-retry-btn').forEach(bindErrorRetryButton);
}

function createActiveChatPayloadState(retryPayload, requestId = '') {
Expand Down Expand Up @@ -5204,6 +5212,94 @@ function retryPayloadForRunAssistant(assistantEl) {
};
}

function plannerRequestFailureUpdate(updates = []) {
if (!Array.isArray(updates)) return null;
return updates.find(update => (
update?.type === 'warning'
&& update?.data?.code === 'planner_request_failed'
)) || null;
}

function bindPlannerProviderSettingsButton(btn) {
if (!btn || btn.dataset.bound) return;
btn.dataset.bound = 'true';
btn.addEventListener('click', async (event) => {
event.preventDefault();
event.stopPropagation();
btn.disabled = true;
try {
await openProvidersSettingsPage();
} finally {
btn.disabled = false;
}
});
}

function rebindPlannerRequestFailureControls() {
document.querySelectorAll('.planner-request-failure-provider-btn')
.forEach(bindPlannerProviderSettingsButton);
}

function renderPlannerRequestFailure(assistantEl, data, retryPayload = null) {
if (!assistantEl || data?.code !== 'planner_request_failed') return false;
const textEl = assistantEl.querySelector('.message-text');
if (!textEl) return false;
if (textEl.querySelector('.planner-request-failure-actions')) return true;

clearAssistantTextStreamState(assistantEl);
assistantEl.classList.add('planner-request-failure');
textEl.classList.add('planner-request-failure-content');
textEl.replaceChildren();
// Set the role on the empty container first: screen readers announce
// content inserted into an existing live region, not a region that arrives
// already populated. A restored card stays silent, which is what we want.
textEl.setAttribute('role', 'alert');

const message = document.createElement('div');
message.className = 'planner-request-failure-message';
message.textContent = data.message || 'Planner request failed before a valid response was available.';
// Name the provider that failed — with several configured, "open Providers"
// is only actionable if the user knows which one to look at. The label is a
// proper noun, so it needs no translation.
if (data.provider) {
const providerName = document.createElement('div');
providerName.className = 'planner-request-failure-provider';
providerName.textContent = data.provider;
message.appendChild(providerName);
}

const actions = document.createElement('div');
actions.className = 'planner-request-failure-actions';

const retryBtn = document.createElement('button');
retryBtn.type = 'button';
retryBtn.className = 'planner-request-failure-action planner-request-failure-retry-btn';
retryBtn.textContent = t('sp.retry');
retryBtn.title = t('sp.retry');
retryBtn.setAttribute('aria-label', t('sp.retry'));
const retryReady = configureRetryButton(retryBtn, retryPayload);

const providerBtn = document.createElement('button');
providerBtn.type = 'button';
providerBtn.className = 'planner-request-failure-action planner-request-failure-provider-btn';
providerBtn.textContent = t('st.tab.providers');
providerBtn.title = `${t('sp.btn.settings')}: ${t('st.tab.providers')}`;
providerBtn.setAttribute('aria-label', providerBtn.title);
bindPlannerProviderSettingsButton(providerBtn);

const orderedActions = data.failureKind === 'auth'
? [providerBtn, retryReady ? retryBtn : null]
: [retryReady ? retryBtn : null, providerBtn];
Comment on lines +5290 to +5292
const visibleActions = orderedActions.filter(Boolean);
visibleActions[0]?.classList.add('primary');
visibleActions.forEach(btn => actions.appendChild(btn));

textEl.append(message, actions);
addMessageCopyButton(assistantEl);
scrollToBottom();
return true;
}

function clearActiveChatPayloadForTab(tabId) {
if (tabId != null) activeChatPayloadsByTab.delete(tabId);
}
Expand Down Expand Up @@ -5262,6 +5358,7 @@ function rebindRestoredMessageControls() {
rebindCopyButtons();
rebindScreenshotSaveButtons();
rebindRetryButtons();
rebindPlannerRequestFailureControls();
rebindContinueButtons();
rebindClarifyCards();
rebindPlanReviewCards();
Expand Down Expand Up @@ -6989,6 +7086,14 @@ async function sendMessage(extraChatParams = {}) {
accepted = true;
completedSuccessfully = res?.successfulDone === true || updatesContainSuccessfulDone(res?.updates);
promptEligibleCompletion = completedSuccessfully || isSuccessfulAskCompletion(modeForSend, res);
const returnedPlannerFailure = plannerRequestFailureUpdate(res?.updates);
if (returnedPlannerFailure
&& renderToCurrentTab
&& currentTabId === tabId
&& !isTabAbortRequested(tabId)
&& !clearedConversationRunRequestIds.has(requestId)) {
renderPlannerRequestFailure(assistantEl, returnedPlannerFailure.data, retryPayload);
}
const returnedErrorUpdate = Array.isArray(res?.updates)
? res.updates.find(u => u?.type === 'error')
: null;
Expand Down Expand Up @@ -7539,7 +7644,12 @@ function handleAgentUpdateMessage(msg) {

case 'warning':
hideActivity();
if (data?.code === 'ask_stream_fallback') {
if (data?.code === 'planner_request_failed') {
const targetAssistantEl = eventAssistantEl || currentAssistantEl;
const retryPayload = activeRetryPayloadForRequest(eventTabId, msg.requestId)
|| retryPayloadForRunAssistant(targetAssistantEl);
renderPlannerRequestFailure(targetAssistantEl, data, retryPayload);
} else if (data?.code === 'ask_stream_fallback') {
showComposerToast(t('sp.streaming.fallback'), { duration: 6000 });
}
break;
Expand Down Expand Up @@ -8753,7 +8863,8 @@ function configureRetryButton(btn, retryPayload) {
}

function addErrorRetryButton(msgEl, retryPayload) {
if (!msgEl || !retryPayload?.text || msgEl.querySelector('.error-retry-btn, .cost-allowance-retry-btn')) return;
if (!msgEl || !retryPayload?.text
|| msgEl.querySelector('.error-retry-btn, .cost-allowance-retry-btn, .planner-request-failure-retry-btn')) return;
msgEl.classList.add('retryable');
const btn = document.createElement('button');
btn.type = 'button';
Expand Down
Loading
Loading