Skip to content

Commit 1efae04

Browse files
authored
Merge pull request #209 from esokullu/agent/planner-provider-retry
Add recovery actions for planner provider failures
2 parents 69b1aee + 39d34bf commit 1efae04

9 files changed

Lines changed: 508 additions & 27 deletions

File tree

src/chrome/src/agent/agent.js

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,19 @@ function isBrowserNewTabUrl(url) {
126126
return BROWSER_NEW_TAB_URL_PREFIXES.some(prefix => value.startsWith(prefix));
127127
}
128128

129+
function plannerRequestFailureKind(detail) {
130+
const text = String(detail || '');
131+
const statusMatch = text.match(/\b(?:error|http|status)\s*[:#-]?\s*(\d{3})\b/i)
132+
|| text.match(/\b(401|403|408|425|429|5\d\d)\b/);
133+
const status = Number(statusMatch?.[1] || 0);
134+
if (status === 401 || status === 403) return 'auth';
135+
if ([408, 425, 429].includes(status) || status >= 500) return 'transient';
136+
if (/\b(?:timed?\s*out|timeout|network|fetch failed|connection (?:closed|reset|refused)|econn\w+|temporar(?:y|ily)|unavailable)\b/i.test(text)) {
137+
return 'transient';
138+
}
139+
return 'provider';
140+
}
141+
129142
/**
130143
* The WebBrain Agent — orchestrates multi-step LLM + tool-use loops.
131144
*/
@@ -7362,20 +7375,31 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
73627375
};
73637376
}
73647377

7365-
_plannerRequestFailure(error, onUpdate) {
7378+
_plannerRequestFailure(error, onUpdate, provider = null) {
73667379
const detail = sanitizePlannerText(
73677380
error?.message || String(error || 'Unknown planner request error.'),
73687381
500,
73697382
{ collapseWhitespace: true },
73707383
);
7371-
const message = `Planner request failed before a valid response was available: ${detail}`;
7372-
onUpdate('warning', { message });
7384+
const message = `Planner request failed before a valid response was available: ${detail} No tools ran.`;
7385+
const failureKind = plannerRequestFailureKind(detail);
7386+
onUpdate('warning', {
7387+
code: 'planner_request_failed',
7388+
message,
7389+
failureKind,
7390+
provider: sanitizePlannerText(
7391+
provider?.config?.label || provider?.name || provider?.config?.providerName || '',
7392+
80,
7393+
{ collapseWhitespace: true },
7394+
),
7395+
});
73737396
return {
73747397
proceed: false,
73757398
message,
73767399
reason: 'planner_error',
73777400
requestKind: 'respond',
73787401
requiresStateChange: false,
7402+
failureKind,
73797403
};
73807404
}
73817405

@@ -7564,7 +7588,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
75647588
if (this._isCostAllowanceError(e)) {
75657589
return { proceed: false, message: e.message, reason: 'cost_limit' };
75667590
}
7567-
return this._plannerRequestFailure(e, onUpdate);
7591+
return this._plannerRequestFailure(e, onUpdate, provider);
75687592
}
75697593
}
75707594

@@ -7786,7 +7810,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
77867810
if (this._isCostAllowanceError(e)) {
77877811
return { proceed: false, message: e.message, reason: 'cost_limit' };
77887812
}
7789-
return this._plannerRequestFailure(e, onUpdate);
7813+
return this._plannerRequestFailure(e, onUpdate, provider);
77907814
}
77917815
}
77927816

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/ui/sidepanel.js

Lines changed: 102 additions & 4 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,15 @@ 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+
const targetAssistantEl = assistantEl || currentAssistantEl;
3744+
renderPlannerRequestFailure(
3745+
targetAssistantEl,
3746+
returnedPlannerFailure.data,
3747+
retryPayloadForRunAssistant(targetAssistantEl),
3748+
);
3749+
}
37413750
const returnedErrorUpdate = Array.isArray(res?.updates)
37423751
? res.updates.find(update => update?.type === 'error')
37433752
: null;
@@ -5165,7 +5174,7 @@ function bindErrorRetryButton(btn) {
51655174
}
51665175

51675176
function rebindRetryButtons() {
5168-
document.querySelectorAll('.error-retry-btn').forEach(bindErrorRetryButton);
5177+
document.querySelectorAll('.error-retry-btn, .planner-request-failure-retry-btn').forEach(bindErrorRetryButton);
51695178
}
51705179

51715180
function createActiveChatPayloadState(retryPayload, requestId = '') {
@@ -5204,6 +5213,81 @@ function retryPayloadForRunAssistant(assistantEl) {
52045213
};
52055214
}
52065215

5216+
function plannerRequestFailureUpdate(updates = []) {
5217+
if (!Array.isArray(updates)) return null;
5218+
return updates.find(update => (
5219+
update?.type === 'warning'
5220+
&& update?.data?.code === 'planner_request_failed'
5221+
)) || null;
5222+
}
5223+
5224+
function bindPlannerProviderSettingsButton(btn) {
5225+
if (!btn || btn.dataset.bound) return;
5226+
btn.dataset.bound = 'true';
5227+
btn.addEventListener('click', async (event) => {
5228+
event.preventDefault();
5229+
event.stopPropagation();
5230+
btn.disabled = true;
5231+
try {
5232+
await openProvidersSettingsPage();
5233+
} finally {
5234+
btn.disabled = false;
5235+
}
5236+
});
5237+
}
5238+
5239+
function rebindPlannerRequestFailureControls() {
5240+
document.querySelectorAll('.planner-request-failure-provider-btn')
5241+
.forEach(bindPlannerProviderSettingsButton);
5242+
}
5243+
5244+
function renderPlannerRequestFailure(assistantEl, data, retryPayload = null) {
5245+
if (!assistantEl || data?.code !== 'planner_request_failed') return false;
5246+
const textEl = assistantEl.querySelector('.message-text');
5247+
if (!textEl) return false;
5248+
if (textEl.querySelector('.planner-request-failure-actions')) return true;
5249+
5250+
clearAssistantTextStreamState(assistantEl);
5251+
assistantEl.classList.add('planner-request-failure');
5252+
textEl.classList.add('planner-request-failure-content');
5253+
textEl.replaceChildren();
5254+
5255+
const message = document.createElement('div');
5256+
message.className = 'planner-request-failure-message';
5257+
message.textContent = data.message || 'Planner request failed before a valid response was available.';
5258+
5259+
const actions = document.createElement('div');
5260+
actions.className = 'planner-request-failure-actions';
5261+
5262+
const retryBtn = document.createElement('button');
5263+
retryBtn.type = 'button';
5264+
retryBtn.className = 'planner-request-failure-action planner-request-failure-retry-btn';
5265+
retryBtn.textContent = t('sp.retry');
5266+
retryBtn.title = t('sp.retry');
5267+
retryBtn.setAttribute('aria-label', t('sp.retry'));
5268+
const retryReady = configureRetryButton(retryBtn, retryPayload);
5269+
5270+
const providerBtn = document.createElement('button');
5271+
providerBtn.type = 'button';
5272+
providerBtn.className = 'planner-request-failure-action planner-request-failure-provider-btn';
5273+
providerBtn.textContent = t('st.tab.providers');
5274+
providerBtn.title = `${t('sp.btn.settings')}: ${t('st.tab.providers')}`;
5275+
providerBtn.setAttribute('aria-label', providerBtn.title);
5276+
bindPlannerProviderSettingsButton(providerBtn);
5277+
5278+
const orderedActions = data.failureKind === 'auth'
5279+
? [providerBtn, retryReady ? retryBtn : null]
5280+
: [retryReady ? retryBtn : null, providerBtn];
5281+
const visibleActions = orderedActions.filter(Boolean);
5282+
visibleActions[0]?.classList.add('primary');
5283+
visibleActions.forEach(btn => actions.appendChild(btn));
5284+
5285+
textEl.append(message, actions);
5286+
addMessageCopyButton(assistantEl);
5287+
scrollToBottom();
5288+
return true;
5289+
}
5290+
52075291
function clearActiveChatPayloadForTab(tabId) {
52085292
if (tabId != null) activeChatPayloadsByTab.delete(tabId);
52095293
}
@@ -5262,6 +5346,7 @@ function rebindRestoredMessageControls() {
52625346
rebindCopyButtons();
52635347
rebindScreenshotSaveButtons();
52645348
rebindRetryButtons();
5349+
rebindPlannerRequestFailureControls();
52655350
rebindContinueButtons();
52665351
rebindClarifyCards();
52675352
rebindPlanReviewCards();
@@ -6989,6 +7074,14 @@ async function sendMessage(extraChatParams = {}) {
69897074
accepted = true;
69907075
completedSuccessfully = res?.successfulDone === true || updatesContainSuccessfulDone(res?.updates);
69917076
promptEligibleCompletion = completedSuccessfully || isSuccessfulAskCompletion(modeForSend, res);
7077+
const returnedPlannerFailure = plannerRequestFailureUpdate(res?.updates);
7078+
if (returnedPlannerFailure
7079+
&& renderToCurrentTab
7080+
&& currentTabId === tabId
7081+
&& !isTabAbortRequested(tabId)
7082+
&& !clearedConversationRunRequestIds.has(requestId)) {
7083+
renderPlannerRequestFailure(assistantEl, returnedPlannerFailure.data, retryPayload);
7084+
}
69927085
const returnedErrorUpdate = Array.isArray(res?.updates)
69937086
? res.updates.find(u => u?.type === 'error')
69947087
: null;
@@ -7539,7 +7632,12 @@ function handleAgentUpdateMessage(msg) {
75397632

75407633
case 'warning':
75417634
hideActivity();
7542-
if (data?.code === 'ask_stream_fallback') {
7635+
if (data?.code === 'planner_request_failed') {
7636+
const targetAssistantEl = eventAssistantEl || currentAssistantEl;
7637+
const retryPayload = activeRetryPayloadForRequest(eventTabId, msg.requestId)
7638+
|| retryPayloadForRunAssistant(targetAssistantEl);
7639+
renderPlannerRequestFailure(targetAssistantEl, data, retryPayload);
7640+
} else if (data?.code === 'ask_stream_fallback') {
75437641
showComposerToast(t('sp.streaming.fallback'), { duration: 6000 });
75447642
}
75457643
break;

src/chrome/styles/sidepanel.css

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2861,6 +2861,72 @@ body {
28612861
background: var(--accent-hover);
28622862
}
28632863

2864+
/* Planner transport/auth failure — actionable assistant card */
2865+
.message.assistant.planner-request-failure .message-content {
2866+
background: rgba(244, 67, 54, 0.08);
2867+
border-color: rgba(244, 67, 54, 0.3);
2868+
}
2869+
2870+
.planner-request-failure-content {
2871+
display: flex;
2872+
flex-direction: column;
2873+
gap: 10px;
2874+
}
2875+
2876+
.planner-request-failure-message {
2877+
color: var(--error-soft);
2878+
}
2879+
2880+
.planner-request-failure-actions {
2881+
display: flex;
2882+
flex-wrap: wrap;
2883+
gap: 7px;
2884+
}
2885+
2886+
.planner-request-failure-action {
2887+
min-height: 30px;
2888+
padding: 5px 11px;
2889+
border: 1px solid rgba(244, 67, 54, 0.35);
2890+
border-radius: 6px;
2891+
background: rgba(244, 67, 54, 0.06);
2892+
color: var(--error-soft);
2893+
font: inherit;
2894+
font-size: 12px;
2895+
font-weight: 600;
2896+
cursor: pointer;
2897+
transition: background 0.15s, border-color 0.15s, color 0.15s;
2898+
}
2899+
2900+
.planner-request-failure-action.primary {
2901+
border-color: var(--accent);
2902+
background: var(--accent);
2903+
color: #fff;
2904+
}
2905+
2906+
.planner-request-failure-action:hover:not(:disabled),
2907+
.planner-request-failure-action:focus-visible {
2908+
border-color: rgba(244, 67, 54, 0.6);
2909+
background: rgba(244, 67, 54, 0.16);
2910+
color: var(--text-primary);
2911+
}
2912+
2913+
.planner-request-failure-action.primary:hover:not(:disabled),
2914+
.planner-request-failure-action.primary:focus-visible {
2915+
border-color: var(--accent-hover);
2916+
background: var(--accent-hover);
2917+
color: #fff;
2918+
}
2919+
2920+
.planner-request-failure-action:focus-visible {
2921+
outline: 2px solid var(--accent);
2922+
outline-offset: 2px;
2923+
}
2924+
2925+
.planner-request-failure-action:disabled {
2926+
cursor: wait;
2927+
opacity: 0.65;
2928+
}
2929+
28642930
/* Error messages */
28652931
.message.error .message-content {
28662932
background: rgba(244, 67, 54, 0.1);

0 commit comments

Comments
 (0)