show chat run errors as agent messages (#887)

This commit is contained in:
ekko
2026-05-21 09:05:17 +08:00
committed by GitHub
parent 40109e9c42
commit 3612a76735
6 changed files with 157 additions and 46 deletions
+34 -1
View File
@@ -467,6 +467,18 @@ export function disconnectChatRun(): void {
}
}
function removeSocketListener(socket: Socket, event: string, handler: (...args: any[]) => void): void {
const candidate = socket as Socket & {
off?: (event: string, handler: (...args: any[]) => void) => Socket
removeListener?: (event: string, handler: (...args: any[]) => void) => Socket
}
if (typeof candidate.off === 'function') {
candidate.off(event, handler)
return
}
candidate.removeListener?.(event, handler)
}
/**
* Start a chat run via Socket.IO and stream events back.
* Returns an AbortController-compatible handle for cancellation.
@@ -500,6 +512,23 @@ export function startRunViaSocket(
let closed = false
const socket = connectChatRun()
const handleSocketError = (err: Error) => {
if (closed) return
closed = true
sessionEventHandlers.delete(sid)
onError(err)
}
socket.once('connect_error', handleSocketError)
const handleSocketDisconnect = (reason: string) => {
if (closed || reason === 'io client disconnect') return
handleSocketError(new Error(`Socket disconnected: ${reason}`))
}
socket.once('disconnect', handleSocketDisconnect)
const removeTerminalSocketListeners = () => {
removeSocketListener(socket, 'connect_error', handleSocketError)
removeSocketListener(socket, 'disconnect', handleSocketDisconnect)
}
if (sessionEventHandlers.has(sid)) {
socket.emit('run', body)
@@ -548,6 +577,7 @@ export function startRunViaSocket(
onEvent(evt)
if ((evt as any).queue_remaining > 0) return
closed = true
removeTerminalSocketListeners()
onDone()
},
onRunFailed: (evt: RunEvent) => {
@@ -555,7 +585,8 @@ export function startRunViaSocket(
onEvent(evt)
if ((evt as any).queue_remaining > 0) return
closed = true
onError(new Error(evt.error || 'Run failed'))
removeTerminalSocketListeners()
onDone()
},
onCompressionStarted: (evt: RunEvent) => {
if (closed) return
@@ -574,6 +605,7 @@ export function startRunViaSocket(
onEvent(evt)
if ((evt as any).queue_length > 0) return
closed = true
removeTerminalSocketListeners()
onDone()
},
onUsageUpdated: (evt: RunEvent) => {
@@ -585,6 +617,7 @@ export function startRunViaSocket(
onEvent(evt)
if ((evt as any).terminal === false) return
closed = true
removeTerminalSocketListeners()
sessionEventHandlers.delete(sid)
onDone()
},
+1 -1
View File
@@ -34,7 +34,7 @@ export interface ChatMessage {
tool_call_id?: string | null
tool_calls?: any[] | null
tool_name?: string | null
finish_reason?: string | null
finish_reason?: 'streaming' | 'tool_calls' | 'error' | string | null
reasoning?: string | null
reasoning_details?: string | null
reasoning_content?: string | null
@@ -34,6 +34,7 @@ const { t } = useI18n();
const toast = useMessage();
const isSystem = computed(() => props.message.role === "system");
const isAgentError = computed(() => props.message.role === "assistant" && props.message.systemType === "error");
const effectiveHeadingIdPrefix = computed(() => props.headingIdPrefix || `msg-${props.message.id}`);
const isCommandMessage = computed(() => props.message.role === "command" || props.message.systemType === "command");
@@ -790,6 +791,7 @@ onBeforeUnmount(() => {
class="message-bubble"
:class="{
system: isSystem,
'agent-error': isAgentError,
command: isCommandMessage,
'command-error': isCommandError,
'speech-playing': isPlayingThisMessage && !isPausedThisMessage,
@@ -1043,6 +1045,12 @@ onBeforeUnmount(() => {
background-color: $msg-assistant-bg;
border-radius: 10px;
}
.message-bubble.agent-error {
color: $error;
background-color: rgba(var(--error-rgb), 0.06);
border: 1px solid rgba(var(--error-rgb), 0.2);
}
}
&.tool {
@@ -1120,6 +1128,20 @@ onBeforeUnmount(() => {
background-color: rgba(var(--warning-rgb), 0.06);
}
&.agent-error {
color: $error;
background-color: rgba(var(--error-rgb), 0.06);
border: 1px solid rgba(var(--error-rgb), 0.2);
:deep(.markdown-body),
:deep(.markdown-body p),
:deep(.markdown-body li),
:deep(.markdown-body strong),
:deep(.markdown-body code) {
color: $error;
}
}
&.speech-playing {
box-shadow:
0 0 0 2px #ff6b6b,
@@ -41,6 +41,12 @@ const isAgent = computed(() => {
return props.agents.some(a => a.agentId === props.message.senderId || a.name === props.message.senderName)
})
const isAgentError = computed(() => {
if (props.message.role !== 'assistant') return false
if (props.message.finish_reason === 'error') return true
return /^Error:\s*/i.test(props.message.content || '')
})
const isSelf = computed(() => {
return !!props.currentUserId && props.message.senderId === props.currentUserId
})
@@ -443,6 +449,7 @@ onBeforeUnmount(() => {
class="msg-content"
:class="{
'agent-content': isAgent,
'agent-error': isAgentError,
'speech-playing': isPlayingThisMessage && !isPausedThisMessage,
}"
>
@@ -548,6 +555,20 @@ onBeforeUnmount(() => {
background-color: rgba(var(--accent-primary-rgb), 0.06);
}
&.agent .msg-content.agent-error {
color: $error;
background-color: rgba(var(--error-rgb), 0.06);
border: 1px solid rgba(var(--error-rgb), 0.2);
:deep(.markdown-body),
:deep(.markdown-body p),
:deep(.markdown-body li),
:deep(.markdown-body strong),
:deep(.markdown-body code) {
color: $error;
}
}
&.self .msg-content {
background-color: rgba(var(--accent-primary-rgb), 0.1);
}
@@ -834,6 +855,20 @@ onBeforeUnmount(() => {
animation: rainbow-glow 4s linear infinite;
}
&.agent-error {
color: $error;
background-color: rgba(var(--error-rgb), 0.06);
border: 1px solid rgba(var(--error-rgb), 0.2);
:deep(.markdown-body),
:deep(.markdown-body p),
:deep(.markdown-body li),
:deep(.markdown-body strong),
:deep(.markdown-body code) {
color: $error;
}
}
:deep(.mention-highlight) {
color: #409eff;
font-weight: 600;
+31 -38
View File
@@ -567,6 +567,10 @@ export const useChatStore = defineStore('chat', () => {
setPendingApproval({ ...e, session_id: sessionId } as RunEvent)
} else if (e.event === 'approval.resolved') {
clearPendingApproval({ ...e, session_id: sessionId } as RunEvent)
} else if (e.event === 'run.failed') {
addAgentErrorMessage(sessionId, e.error)
serverWorking.value.delete(sessionId)
queueLengths.value.delete(sessionId)
} else if (e.event === 'tool.started') {
const msgs = getSessionMsgs(sessionId)
const toolCallId = e.tool_call_id as string | undefined
@@ -692,6 +696,29 @@ export const useChatStore = defineStore('chat', () => {
}
}
function addAgentErrorMessage(sessionId: string, error?: string | null) {
const content = error ? `Error: ${error}` : 'Run failed'
const msgs = getSessionMsgs(sessionId)
const last = msgs[msgs.length - 1]
if (last?.isStreaming) {
updateMessage(sessionId, last.id, {
role: 'assistant',
content,
isStreaming: false,
systemType: 'error',
})
return
}
if (last?.role === 'assistant' && last.systemType === 'error' && last.content === content) return
addMessage(sessionId, {
id: uid(),
role: 'assistant',
content,
timestamp: Date.now(),
systemType: 'error',
})
}
function handleSessionCommandEvent(evt: RunEvent) {
const sid = evt.session_id
if (!sid) return
@@ -1319,22 +1346,8 @@ export const useChatStore = defineStore('chat', () => {
}
case 'run.failed': {
addAgentErrorMessage(sid, evt.error)
const msgs = getSessionMsgs(sid)
const lastErr = msgs[msgs.length - 1]
if (lastErr?.isStreaming) {
updateMessage(sid, lastErr.id, {
isStreaming: false,
content: evt.error ? `Error: ${evt.error}` : 'Run failed',
role: 'system',
})
} else {
addMessage(sid, {
id: uid(),
role: 'system',
content: evt.error ? `Error: ${evt.error}` : 'Run failed',
timestamp: Date.now(),
})
}
msgs.forEach((m, i) => {
if (m.role === 'tool' && m.toolStatus === 'running') {
msgs[i] = { ...m, toolStatus: 'error' }
@@ -1371,20 +1384,14 @@ export const useChatStore = defineStore('chat', () => {
// onError
(err) => {
console.warn('Socket.IO run stream error:', err.message)
addAgentErrorMessage(sid, err.message)
const msgs = getSessionMsgs(sid)
const last = msgs[msgs.length - 1]
if (last?.isStreaming) {
updateMessage(sid, last.id, { isStreaming: false })
}
msgs.forEach((m, i) => {
if (m.role === 'tool' && m.toolStatus === 'running') {
msgs[i] = { ...m, toolStatus: 'done' }
msgs[i] = { ...m, toolStatus: 'error' }
}
})
cleanup()
if (sid === activeSessionId.value) {
void refreshActiveSession()
}
},
undefined,
)
@@ -1756,22 +1763,8 @@ export const useChatStore = defineStore('chat', () => {
} else {
queueLengths.value.delete(sid)
}
addAgentErrorMessage(sid, evt.error)
const msgs = getSessionMsgs(sid)
const lastErr = msgs[msgs.length - 1]
if (lastErr?.isStreaming) {
updateMessage(sid, lastErr.id, {
isStreaming: false,
content: evt.error ? `Error: ${evt.error}` : 'Run failed',
role: 'system',
})
} else {
addMessage(sid, {
id: uid(),
role: 'system',
content: evt.error ? `Error: ${evt.error}` : 'Run failed',
timestamp: Date.now(),
})
}
msgs.forEach((m, i) => {
if (m.role === 'tool' && m.toolStatus === 'running') {
msgs[i] = { ...m, toolStatus: 'error' }