show chat run errors as agent messages (#887)
This commit is contained in:
@@ -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()
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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' }
|
||||
|
||||
@@ -264,6 +264,13 @@ class AgentClient {
|
||||
onStatus?: (status: 'compressing' | 'replying' | 'ready') => void,
|
||||
): Promise<void> {
|
||||
logger.debug(`[AgentClients] ${this.name} mentioned by ${msg.senderName}: "${msg.content.slice(0, 50)}"`)
|
||||
const runMessageId = groupMessageId(roomId, this.profile, this.name)
|
||||
let partIndex = 0
|
||||
let streamMessageId = groupMessagePartId(runMessageId, partIndex)
|
||||
let currentContent = ''
|
||||
let totalContent = ''
|
||||
let reasoningContent = ''
|
||||
let streamStarted = false
|
||||
try {
|
||||
// Notify room that agent is typing
|
||||
this.startTyping(roomId)
|
||||
@@ -335,12 +342,6 @@ class AgentClient {
|
||||
const bridge = new AgentBridgeClient()
|
||||
const sessionSeed = String(this.storage?.getRoom?.(roomId)?.sessionSeed || '0')
|
||||
const sessionId = groupBridgeSessionId(roomId, this.profile, this.name, sessionSeed)
|
||||
const runMessageId = groupMessageId(roomId, this.profile, this.name)
|
||||
let partIndex = 0
|
||||
let streamMessageId = groupMessagePartId(runMessageId, partIndex)
|
||||
let currentContent = ''
|
||||
let totalContent = ''
|
||||
let reasoningContent = ''
|
||||
const flushedAssistantParts = new Set<string>()
|
||||
let lastChunk: AgentBridgeOutput | null = null
|
||||
const started = await bridge.chat(
|
||||
@@ -355,6 +356,7 @@ class AgentClient {
|
||||
)
|
||||
|
||||
this.emitMessageStreamStart(roomId, streamMessageId)
|
||||
streamStarted = true
|
||||
for await (const chunk of bridge.streamOutput(started.run_id, { timeoutMs: 120000 })) {
|
||||
lastChunk = chunk
|
||||
reasoningContent += await this.recordBridgeEvents(roomId, chunk, () => streamMessageId, async () => {
|
||||
@@ -373,6 +375,7 @@ class AgentClient {
|
||||
partIndex += 1
|
||||
streamMessageId = groupMessagePartId(runMessageId, partIndex)
|
||||
this.emitMessageStreamStart(roomId, streamMessageId)
|
||||
streamStarted = true
|
||||
return toolBaseId
|
||||
})
|
||||
if (chunk.delta) {
|
||||
@@ -384,6 +387,7 @@ class AgentClient {
|
||||
|
||||
if (lastChunk?.status === 'error') {
|
||||
logger.error(`[AgentClients] ${this.name}: bridge response failed: ${lastChunk.error || 'unknown error'}`)
|
||||
await this.sendAgentErrorMessage(roomId, streamMessageId, lastChunk.error || 'Run failed', msg, reasoningContent)
|
||||
this.emitMessageStreamEnd(roomId, streamMessageId)
|
||||
this.stopTyping(roomId)
|
||||
onStatus?.('ready')
|
||||
@@ -414,11 +418,35 @@ class AgentClient {
|
||||
onStatus?.('ready')
|
||||
} catch (err: any) {
|
||||
logger.error(`[AgentClients] ${this.name}: error handling message: ${err.message}`)
|
||||
try {
|
||||
await this.sendAgentErrorMessage(roomId, streamMessageId, err, msg, reasoningContent)
|
||||
if (streamStarted) this.emitMessageStreamEnd(roomId, streamMessageId)
|
||||
} catch (sendErr: any) {
|
||||
logger.warn(`[AgentClients] ${this.name}: failed to send error message: ${sendErr.message}`)
|
||||
}
|
||||
this.stopTyping(roomId)
|
||||
onStatus?.('ready')
|
||||
}
|
||||
}
|
||||
|
||||
private async sendAgentErrorMessage(
|
||||
roomId: string,
|
||||
messageId: string,
|
||||
error: unknown,
|
||||
sourceMsg: MentionMessage,
|
||||
reasoningContent = '',
|
||||
): Promise<void> {
|
||||
const detail = error instanceof Error ? error.message : String(error || 'Run failed')
|
||||
const content = detail.startsWith('Error:') ? detail : `Error: ${detail}`
|
||||
await this.sendMessage(roomId, content, messageId, {
|
||||
role: 'assistant',
|
||||
mentionDepth: nextMentionDepth(sourceMsg),
|
||||
finish_reason: 'error',
|
||||
reasoning: reasoningContent || null,
|
||||
reasoning_content: reasoningContent || null,
|
||||
})
|
||||
}
|
||||
|
||||
private async recordBridgeEvents(
|
||||
roomId: string,
|
||||
chunk: AgentBridgeOutput,
|
||||
|
||||
Reference in New Issue
Block a user