@@ -15,6 +15,8 @@ import type {
1515 AppGoal ,
1616 AppMessage ,
1717 AppMessageContent ,
18+ AppNotice ,
19+ AppNoticeDetail ,
1820 AppWarning ,
1921 AppQuestionRequest ,
2022 AppSession ,
@@ -229,6 +231,64 @@ function appendToolOutputToMessages(messages: AppMessage[], toolCallId: string,
229231// Reducer
230232// ---------------------------------------------------------------------------
231233
234+ /** Agent error code → semantic title key under `warnings.agentError`. Codes
235+ * come from the protocol error domain (agent-core-v2 `ProtocolErrors`);
236+ * anything unmapped falls back to the generic `title`. */
237+ const AGENT_ERROR_TITLE_KEYS : Readonly < Record < string , string > > = {
238+ 'provider.connection_error' : 'connection' ,
239+ 'provider.auth_error' : 'auth' ,
240+ 'provider.rate_limit' : 'rateLimit' ,
241+ 'provider.overloaded' : 'overloaded' ,
242+ 'provider.filtered' : 'filtered' ,
243+ 'provider.api_error' : 'api' ,
244+ 'context.overflow' : 'contextOverflow' ,
245+ } ;
246+
247+ interface AgentErrorRaw {
248+ code ?: string ;
249+ message ?: string ;
250+ name ?: string ;
251+ details ?: Record < string , unknown > ;
252+ }
253+
254+ /**
255+ * Build the structured error notice for a failed agent turn (typically a
256+ * model-provider failure). The wire payload already carries the coded error —
257+ * surface it in full so a rate-limit / auth / endpoint failure is diagnosable
258+ * from the toast: semantic title, the provider's raw message as the body, and
259+ * a diagnostics list (error code, HTTP status, request id, SDK error name,
260+ * plus any extra detail fields such as finishReason).
261+ */
262+ function buildAgentErrorNotice ( raw : AgentErrorRaw ) : AppNotice {
263+ const t = i18n . global . t ;
264+ const details : AppNoticeDetail [ ] = [ ] ;
265+ const push = ( label : string , value : unknown ) : void => {
266+ if ( typeof value === 'number' || typeof value === 'boolean' ) {
267+ details . push ( { label, value : String ( value ) } ) ;
268+ } else if ( typeof value === 'string' && value . length > 0 ) {
269+ details . push ( { label, value } ) ;
270+ }
271+ } ;
272+ push ( t ( 'warnings.details.code' ) , raw . code ) ;
273+ const rawDetails = raw . details ?? { } ;
274+ push ( t ( 'warnings.details.status' ) , rawDetails [ 'statusCode' ] ) ;
275+ push ( t ( 'warnings.details.requestId' ) , rawDetails [ 'requestId' ] ) ;
276+ push ( t ( 'warnings.details.errorName' ) , raw . name ) ;
277+ // Keep any remaining detail fields (finishReason, rawFinishReason, …) so no
278+ // diagnostics the daemon sent are hidden.
279+ for ( const [ key , value ] of Object . entries ( rawDetails ) ) {
280+ if ( key === 'statusCode' || key === 'requestId' ) continue ;
281+ push ( key , value ) ;
282+ }
283+ const titleKey = ( raw . code !== undefined ? AGENT_ERROR_TITLE_KEYS [ raw . code ] : undefined ) ?? 'title' ;
284+ return {
285+ severity : 'error' ,
286+ title : t ( `warnings.agentError.${ titleKey } ` ) ,
287+ message : raw . message ,
288+ details : details . length > 0 ? details : undefined ,
289+ } ;
290+ }
291+
232292/**
233293 * Apply a single AppEvent to the state, returning a new state object.
234294 * The event carries `_wireSeq` and `_wireSessionId` as hidden extras when
@@ -674,18 +734,20 @@ export function reduceAppEvent(
674734 _agentWarning ?: boolean ;
675735 code ?: string ;
676736 message ?: string ;
737+ name ?: string ;
738+ details ?: Record < string , unknown > ;
677739 type ?: string ;
678740 } | null ;
679741 if ( raw && raw . _noop === true ) {
680742 // No-op streaming/tool event — seq already advanced, nothing else to do
681- } else if ( raw && ( raw . _agentError || raw . _agentWarning ) ) {
682- // Surface the agent's real error/warning message (e.g. a 403 from the
683- // model provider) instead of a useless "Unhandled event".
684- const label = raw . _agentError
685- ? i18n . global . t ( 'warnings.errorLabel' )
686- : i18n . global . t ( 'warnings.noteLabel' ) ;
687- const msg = raw . message ?? raw . code ?? 'agent error ' ;
688- next . warnings = [ ...next . warnings , `${ label } : ${ msg } ` ] ;
743+ } else if ( raw && raw . _agentError ) {
744+ // Surface the agent's real error (e.g. a 429 from the model provider)
745+ // as a structured notice: semantic title + raw provider message +
746+ // diagnostics (code / HTTP status / request id) for troubleshooting.
747+ next . warnings = [ ... next . warnings , buildAgentErrorNotice ( raw ) ] ;
748+ } else if ( raw && raw . _agentWarning ) {
749+ const msg = raw . message ?? raw . code ?? 'agent warning ' ;
750+ next . warnings = [ ...next . warnings , `${ i18n . global . t ( 'warnings.noteLabel' ) } : ${ msg } ` ] ;
689751 } else {
690752 // Truly unknown — push a warning
691753 const wireType = raw ?. type ?? '(unknown)' ;
0 commit comments