Skip to content

Commit e3c729a

Browse files
authored
Merge pull request #2620 from esokullu/main
Enhance planner recovery actions and trace configuration
2 parents 25e2f69 + 2cc29e1 commit e3c729a

17 files changed

Lines changed: 1737 additions & 57 deletions

File tree

src/chrome/src/agent/agent.js

Lines changed: 275 additions & 17 deletions
Large diffs are not rendered by default.

src/chrome/src/agent/planner.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ Rules:
5454
- 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.
5555
- plan_only when the user asks for a plan, outline, strategy, or discussion without authorizing action.
5656
- clarify only when missing or conflicting user information prevents a useful plan; make localized.summary the concise question to ask.
57+
- 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.
58+
- 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.
5759
- 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.
5860
- 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.
5961
- 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.
@@ -114,6 +116,8 @@ Rules:
114116
- 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.
115117
- plan_only means the user asks for a plan, outline, strategy, or discussion without authorizing action.
116118
- clarify means missing or conflicting user information prevents a useful plan; localized.summary must be the concise question to ask.
119+
- 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.
120+
- 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.
117121
- 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.
118122
- 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.
119123
- 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.

src/chrome/src/background.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1380,16 +1380,25 @@ function isClarificationRequiredRunUpdate(update) {
13801380
&& update?.data?.status === 'clarification_required';
13811381
}
13821382

1383+
function isPlannerRequestFailureUpdate(update) {
1384+
return update?.type === 'warning'
1385+
&& update?.data?.code === 'planner_request_failed';
1386+
}
1387+
13831388
function runUpdatesSucceeded(updates = []) {
1384-
return !updates.some(update => update?.type === 'error' || isClarificationRequiredRunUpdate(update));
1389+
return !updates.some(update => (
1390+
update?.type === 'error'
1391+
|| isClarificationRequiredRunUpdate(update)
1392+
|| isPlannerRequestFailureUpdate(update)
1393+
));
13851394
}
13861395

13871396
function terminalRunUiStatus(content, updates = [], error = null) {
13881397
if (error) return 'failed';
13891398
const text = String(content || '');
13901399
if (/stopped by user|aborted by user/i.test(text)) return 'stopped';
13911400
if (/before executing requested tool calls/i.test(text)) return 'cancelled';
1392-
if (updates.some(update => update?.type === 'error')) return 'failed';
1401+
if (updates.some(update => update?.type === 'error' || isPlannerRequestFailureUpdate(update))) return 'failed';
13931402
if (updates.some(isClarificationRequiredRunUpdate)) return 'clarification_required';
13941403
return 'completed';
13951404
}

src/chrome/src/providers/openai.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
shouldUseOpenAIResponsesApi,
66
supportsOpenAIAskStreaming,
77
} from './provider-compatibility.js';
8+
import { normalizeRuntimeTraceConfig } from '../trace/runtime-config.js';
89

910
const OPENAI_RESPONSES_MIN_MAX_OUTPUT_TOKENS = 16;
1011
const KIMI_CURRENT_TOOL_REASONING_MODELS = new Set([
@@ -235,11 +236,16 @@ export class OpenAICompatibleProvider extends BaseLLMProvider {
235236
const sessionId = String(options.webbrainSessionId || '').trim();
236237
if (sessionId) body.session_id = sessionId.slice(0, 200);
237238
const generationName = String(options.webbrainGenerationName || '').trim().toLowerCase();
238-
if (generationName) {
239+
const runtimeConfig = normalizeRuntimeTraceConfig(options.webbrainRuntimeConfig);
240+
if (generationName || runtimeConfig) {
239241
const trace = body.trace && typeof body.trace === 'object' && !Array.isArray(body.trace)
240242
? body.trace
241243
: {};
242-
body.trace = { ...trace, generation_name: generationName.slice(0, 64) };
244+
body.trace = {
245+
...trace,
246+
...(generationName ? { generation_name: generationName.slice(0, 64) } : {}),
247+
...(runtimeConfig ? { runtime_config: runtimeConfig } : {}),
248+
};
243249
}
244250
}
245251

src/chrome/src/trace/recorder.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { normalizeRuntimeTraceConfig } from './runtime-config.js';
2+
13
/**
24
* Trace recorder — writes per-run traces (LLM requests/responses, tool calls,
35
* screenshots) into IndexedDB for later inspection and cross-model comparison.
@@ -130,6 +132,7 @@ export async function startRun(meta = {}) {
130132
providerId: meta.providerId || '',
131133
providerClass: meta.providerClass || '',
132134
webbrainVersion: meta.webbrainVersion || '',
135+
runtimeConfig: normalizeRuntimeTraceConfig(meta.runtimeConfig),
133136
userMessage: meta.userMessage || '',
134137
tabUrl: meta.tabUrl || '',
135138
tabTitle: meta.tabTitle || '',
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
const STRING_ENUMS = Object.freeze({
2+
browser_target: new Set(['chrome', 'firefox']),
3+
mode: new Set(['ask', 'act', 'dev']),
4+
prompt_tier: new Set(['compact', 'mid', 'full']),
5+
plan_before_act_mode: new Set(['off', 'try', 'strict']),
6+
auto_screenshot: new Set(['off', 'navigation', 'state_change', 'every_step']),
7+
image_detail: new Set(['auto', 'low', 'high']),
8+
});
9+
10+
const BOOLEAN_FIELDS = Object.freeze([
11+
'screenshot_redaction',
12+
'strict_secret_mode',
13+
'use_site_adapters',
14+
'web_mcp_enabled',
15+
'api_mutations_allowed',
16+
'user_memory_enabled',
17+
'selection_grounded',
18+
]);
19+
20+
const INTEGER_RANGES = Object.freeze({
21+
max_agent_steps: [0, 10_000],
22+
max_image_dimension: [256, 16_384],
23+
max_screenshots_per_turn: [0, 1_000],
24+
});
25+
26+
function safeVersion(value) {
27+
const version = String(value || '').trim();
28+
return /^[0-9A-Za-z][0-9A-Za-z.+_-]{0,63}$/.test(version) ? version : '';
29+
}
30+
31+
/**
32+
* Runtime trace metadata crosses both a provider boundary and an export
33+
* boundary. Keep it to a small, versioned allowlist of booleans, bounded
34+
* integers, and enums so a caller can never smuggle credentials or profile
35+
* contents into a trace by passing an arbitrary settings object.
36+
*/
37+
export function normalizeRuntimeTraceConfig(value) {
38+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
39+
40+
const normalized = { schema_version: 1 };
41+
const extensionVersion = safeVersion(value.extension_version);
42+
if (extensionVersion) normalized.extension_version = extensionVersion;
43+
44+
for (const [field, allowed] of Object.entries(STRING_ENUMS)) {
45+
const candidate = String(value[field] || '').trim().toLowerCase();
46+
if (allowed.has(candidate)) normalized[field] = candidate;
47+
}
48+
for (const field of BOOLEAN_FIELDS) {
49+
if (typeof value[field] === 'boolean') normalized[field] = value[field];
50+
}
51+
for (const [field, [min, max]] of Object.entries(INTEGER_RANGES)) {
52+
const candidate = Number(value[field]);
53+
if (Number.isInteger(candidate) && candidate >= min && candidate <= max) {
54+
normalized[field] = candidate;
55+
}
56+
}
57+
58+
return normalized;
59+
}

src/chrome/src/ui/sidepanel.js

Lines changed: 116 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1703,10 +1703,10 @@ function releaseRetryAttachmentPayload(retryId) {
17031703

17041704
function releaseRetryAttachmentsInTree(root) {
17051705
if (!root) return;
1706-
if (root.matches?.('.error-retry-btn[data-retry-id], .cost-allowance-retry-btn[data-retry-id]')) {
1706+
if (root.matches?.('.error-retry-btn[data-retry-id], .cost-allowance-retry-btn[data-retry-id], .planner-request-failure-retry-btn[data-retry-id]')) {
17071707
releaseRetryAttachmentPayload(root.dataset.retryId);
17081708
}
1709-
root.querySelectorAll?.('.error-retry-btn[data-retry-id], .cost-allowance-retry-btn[data-retry-id]').forEach((btn) => {
1709+
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) => {
17101710
releaseRetryAttachmentPayload(btn.dataset.retryId);
17111711
});
17121712
}
@@ -3738,6 +3738,14 @@ async function adoptRestoredRunState(tabId, state) {
37383738
probeFirst: true,
37393739
requireDurableSubmittedTurn: runUi.kind !== 'continue',
37403740
});
3741+
const returnedPlannerFailure = plannerRequestFailureUpdate(res?.updates);
3742+
if (returnedPlannerFailure && sameTabId(currentTabId, tabId) && !isTabAbortRequested(tabId)) {
3743+
renderPlannerRequestFailure(
3744+
assistantEl,
3745+
returnedPlannerFailure.data,
3746+
retryPayloadForRunAssistant(assistantEl),
3747+
);
3748+
}
37413749
const returnedErrorUpdate = Array.isArray(res?.updates)
37423750
? res.updates.find(update => update?.type === 'error')
37433751
: null;
@@ -5165,7 +5173,7 @@ function bindErrorRetryButton(btn) {
51655173
}
51665174

51675175
function rebindRetryButtons() {
5168-
document.querySelectorAll('.error-retry-btn').forEach(bindErrorRetryButton);
5176+
document.querySelectorAll('.error-retry-btn, .planner-request-failure-retry-btn').forEach(bindErrorRetryButton);
51695177
}
51705178

51715179
function createActiveChatPayloadState(retryPayload, requestId = '') {
@@ -5204,6 +5212,94 @@ function retryPayloadForRunAssistant(assistantEl) {
52045212
};
52055213
}
52065214

5215+
function plannerRequestFailureUpdate(updates = []) {
5216+
if (!Array.isArray(updates)) return null;
5217+
return updates.find(update => (
5218+
update?.type === 'warning'
5219+
&& update?.data?.code === 'planner_request_failed'
5220+
)) || null;
5221+
}
5222+
5223+
function bindPlannerProviderSettingsButton(btn) {
5224+
if (!btn || btn.dataset.bound) return;
5225+
btn.dataset.bound = 'true';
5226+
btn.addEventListener('click', async (event) => {
5227+
event.preventDefault();
5228+
event.stopPropagation();
5229+
btn.disabled = true;
5230+
try {
5231+
await openProvidersSettingsPage();
5232+
} finally {
5233+
btn.disabled = false;
5234+
}
5235+
});
5236+
}
5237+
5238+
function rebindPlannerRequestFailureControls() {
5239+
document.querySelectorAll('.planner-request-failure-provider-btn')
5240+
.forEach(bindPlannerProviderSettingsButton);
5241+
}
5242+
5243+
function renderPlannerRequestFailure(assistantEl, data, retryPayload = null) {
5244+
if (!assistantEl || data?.code !== 'planner_request_failed') return false;
5245+
const textEl = assistantEl.querySelector('.message-text');
5246+
if (!textEl) return false;
5247+
if (textEl.querySelector('.planner-request-failure-actions')) return true;
5248+
5249+
clearAssistantTextStreamState(assistantEl);
5250+
assistantEl.classList.add('planner-request-failure');
5251+
textEl.classList.add('planner-request-failure-content');
5252+
textEl.replaceChildren();
5253+
// Set the role on the empty container first: screen readers announce
5254+
// content inserted into an existing live region, not a region that arrives
5255+
// already populated. A restored card stays silent, which is what we want.
5256+
textEl.setAttribute('role', 'alert');
5257+
5258+
const message = document.createElement('div');
5259+
message.className = 'planner-request-failure-message';
5260+
message.textContent = data.message || 'Planner request failed before a valid response was available.';
5261+
// Name the provider that failed — with several configured, "open Providers"
5262+
// is only actionable if the user knows which one to look at. The label is a
5263+
// proper noun, so it needs no translation.
5264+
if (data.provider) {
5265+
const providerName = document.createElement('div');
5266+
providerName.className = 'planner-request-failure-provider';
5267+
providerName.textContent = data.provider;
5268+
message.appendChild(providerName);
5269+
}
5270+
5271+
const actions = document.createElement('div');
5272+
actions.className = 'planner-request-failure-actions';
5273+
5274+
const retryBtn = document.createElement('button');
5275+
retryBtn.type = 'button';
5276+
retryBtn.className = 'planner-request-failure-action planner-request-failure-retry-btn';
5277+
retryBtn.textContent = t('sp.retry');
5278+
retryBtn.title = t('sp.retry');
5279+
retryBtn.setAttribute('aria-label', t('sp.retry'));
5280+
const retryReady = configureRetryButton(retryBtn, retryPayload);
5281+
5282+
const providerBtn = document.createElement('button');
5283+
providerBtn.type = 'button';
5284+
providerBtn.className = 'planner-request-failure-action planner-request-failure-provider-btn';
5285+
providerBtn.textContent = t('st.tab.providers');
5286+
providerBtn.title = `${t('sp.btn.settings')}: ${t('st.tab.providers')}`;
5287+
providerBtn.setAttribute('aria-label', providerBtn.title);
5288+
bindPlannerProviderSettingsButton(providerBtn);
5289+
5290+
const orderedActions = data.failureKind === 'auth'
5291+
? [providerBtn, retryReady ? retryBtn : null]
5292+
: [retryReady ? retryBtn : null, providerBtn];
5293+
const visibleActions = orderedActions.filter(Boolean);
5294+
visibleActions[0]?.classList.add('primary');
5295+
visibleActions.forEach(btn => actions.appendChild(btn));
5296+
5297+
textEl.append(message, actions);
5298+
addMessageCopyButton(assistantEl);
5299+
scrollToBottom();
5300+
return true;
5301+
}
5302+
52075303
function clearActiveChatPayloadForTab(tabId) {
52085304
if (tabId != null) activeChatPayloadsByTab.delete(tabId);
52095305
}
@@ -5262,6 +5358,7 @@ function rebindRestoredMessageControls() {
52625358
rebindCopyButtons();
52635359
rebindScreenshotSaveButtons();
52645360
rebindRetryButtons();
5361+
rebindPlannerRequestFailureControls();
52655362
rebindContinueButtons();
52665363
rebindClarifyCards();
52675364
rebindPlanReviewCards();
@@ -6989,6 +7086,14 @@ async function sendMessage(extraChatParams = {}) {
69897086
accepted = true;
69907087
completedSuccessfully = res?.successfulDone === true || updatesContainSuccessfulDone(res?.updates);
69917088
promptEligibleCompletion = completedSuccessfully || isSuccessfulAskCompletion(modeForSend, res);
7089+
const returnedPlannerFailure = plannerRequestFailureUpdate(res?.updates);
7090+
if (returnedPlannerFailure
7091+
&& renderToCurrentTab
7092+
&& currentTabId === tabId
7093+
&& !isTabAbortRequested(tabId)
7094+
&& !clearedConversationRunRequestIds.has(requestId)) {
7095+
renderPlannerRequestFailure(assistantEl, returnedPlannerFailure.data, retryPayload);
7096+
}
69927097
const returnedErrorUpdate = Array.isArray(res?.updates)
69937098
? res.updates.find(u => u?.type === 'error')
69947099
: null;
@@ -7539,7 +7644,12 @@ function handleAgentUpdateMessage(msg) {
75397644

75407645
case 'warning':
75417646
hideActivity();
7542-
if (data?.code === 'ask_stream_fallback') {
7647+
if (data?.code === 'planner_request_failed') {
7648+
const targetAssistantEl = eventAssistantEl || currentAssistantEl;
7649+
const retryPayload = activeRetryPayloadForRequest(eventTabId, msg.requestId)
7650+
|| retryPayloadForRunAssistant(targetAssistantEl);
7651+
renderPlannerRequestFailure(targetAssistantEl, data, retryPayload);
7652+
} else if (data?.code === 'ask_stream_fallback') {
75437653
showComposerToast(t('sp.streaming.fallback'), { duration: 6000 });
75447654
}
75457655
break;
@@ -8753,7 +8863,8 @@ function configureRetryButton(btn, retryPayload) {
87538863
}
87548864

87558865
function addErrorRetryButton(msgEl, retryPayload) {
8756-
if (!msgEl || !retryPayload?.text || msgEl.querySelector('.error-retry-btn, .cost-allowance-retry-btn')) return;
8866+
if (!msgEl || !retryPayload?.text
8867+
|| msgEl.querySelector('.error-retry-btn, .cost-allowance-retry-btn, .planner-request-failure-retry-btn')) return;
87578868
msgEl.classList.add('retryable');
87588869
const btn = document.createElement('button');
87598870
btn.type = 'button';

0 commit comments

Comments
 (0)