feat: add api-error event support with folding for retry messages

Introduces support for displaying API error system messages in the web UI with
automatic folding of consecutive retry messages to show only the latest state.
This commit is contained in:
weishu
2026-01-05 13:00:25 +08:00
parent b665e34d3c
commit f7d161e6cf
5 changed files with 62 additions and 3 deletions
+16
View File
@@ -216,6 +216,22 @@ export function normalizeAgentRecord(
meta
}
}
if (data.type === 'system' && data.subtype === 'api_error') {
return {
id: messageId,
localId,
createdAt,
role: 'event',
content: {
type: 'api-error',
retryAttempt: asNumber(data.retryAttempt) ?? 0,
maxRetries: asNumber(data.maxRetries) ?? 0,
error: data.error
},
isSidechain: false,
meta
}
}
return null
}
+13
View File
@@ -13,6 +13,19 @@ export type EventPresentation = {
}
export function getEventPresentation(event: AgentEvent): EventPresentation {
if (event.type === 'api-error') {
const { retryAttempt, maxRetries } = event as { retryAttempt: number; maxRetries: number }
if (maxRetries > 0 && retryAttempt >= maxRetries) {
return { icon: '⚠️', text: 'API error: Max retries reached' }
}
if (maxRetries > 0) {
return { icon: '⏳', text: `API error: Retrying (${retryAttempt}/${maxRetries})` }
}
if (retryAttempt > 0) {
return { icon: '⏳', text: 'API error: Retrying...' }
}
return { icon: '⚠️', text: 'API error' }
}
if (event.type === 'switch') {
const mode = event.mode === 'local' ? 'local' : 'remote'
return { icon: '🔄', text: `Switched to ${mode}` }
+2 -2
View File
@@ -1,7 +1,7 @@
import type { AgentState } from '@/types/api'
import type { ChatBlock, NormalizedMessage, UsageData } from '@/chat/types'
import { traceMessages, type TracedMessage } from '@/chat/tracer'
import { dedupeAgentEvents } from '@/chat/reducerEvents'
import { dedupeAgentEvents, foldApiErrorEvents } from '@/chat/reducerEvents'
import { collectTitleChanges, collectToolIdsFromMessages, ensureToolBlock, getPermissions } from '@/chat/reducerTools'
import { reduceTimeline } from '@/chat/reducerTimeline'
@@ -94,5 +94,5 @@ export function reduceChatBlocks(
}
}
return { blocks: dedupeAgentEvents(rootResult.blocks), hasReadyEvent, latestUsage }
return { blocks: dedupeAgentEvents(foldApiErrorEvents(rootResult.blocks)), hasReadyEvent, latestUsage }
}
+30 -1
View File
@@ -1,4 +1,4 @@
import type { AgentEvent, ChatBlock, NormalizedMessage } from '@/chat/types'
import type { AgentEvent, AgentEventBlock, ChatBlock, NormalizedMessage } from '@/chat/types'
function parseClaudeUsageLimit(text: string): number | null {
const match = text.match(/^Claude AI usage limit reached\|(\d+)$/)
@@ -83,3 +83,32 @@ export function dedupeAgentEvents(blocks: ChatBlock[]): ChatBlock[] {
return result
}
/**
* Fold consecutive api-error events, keeping only the latest state.
*/
export function foldApiErrorEvents(blocks: ChatBlock[]): ChatBlock[] {
const result: ChatBlock[] = []
for (const block of blocks) {
if (block.kind !== 'agent-event') {
result.push(block)
continue
}
const event = block.event as { type: string }
if (event.type !== 'api-error') {
result.push(block)
continue
}
const prev = result[result.length - 1] as AgentEventBlock | undefined
if (prev?.kind === 'agent-event' && (prev.event as { type: string }).type === 'api-error') {
result[result.length - 1] = block
} else {
result.push(block)
}
}
return result
}
+1
View File
@@ -14,6 +14,7 @@ export type AgentEvent =
| { type: 'title-changed'; title: string }
| { type: 'limit-reached'; endsAt: number }
| { type: 'ready' }
| { type: 'api-error'; retryAttempt: number; maxRetries: number; error: unknown }
| ({ type: string } & Record<string, unknown>)
export type ToolResultPermission = {