From d5284445802b5af6bff3240329f2558f9c35b5f5 Mon Sep 17 00:00:00 2001 From: Pascal Date: Fri, 15 May 2026 11:18:11 +0200 Subject: [PATCH] webui: preserve partial response on streaming error (#23090) --- .../server/webui/src/lib/constants/agentic.ts | 3 --- .../webui/src/lib/stores/agentic.svelte.ts | 12 +++------- .../webui/src/lib/stores/chat.svelte.ts | 23 +++++++++++-------- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/tools/server/webui/src/lib/constants/agentic.ts b/tools/server/webui/src/lib/constants/agentic.ts index 4fe6da61c3..c0575163ef 100644 --- a/tools/server/webui/src/lib/constants/agentic.ts +++ b/tools/server/webui/src/lib/constants/agentic.ts @@ -4,9 +4,6 @@ export const ATTACHMENT_SAVED_REGEX = /\[Attachment saved: ([^\]]+)\]/; export const NEWLINE_SEPARATOR = '\n'; -export const LLM_ERROR_BLOCK_START = '\n\n```\nUpstream LLM error:\n'; -export const LLM_ERROR_BLOCK_END = '\n```\n'; - export const DEFAULT_AGENTIC_CONFIG: AgenticConfig = { enabled: true, maxTurns: 100, diff --git a/tools/server/webui/src/lib/stores/agentic.svelte.ts b/tools/server/webui/src/lib/stores/agentic.svelte.ts index 3334f6c111..1f1f05c453 100644 --- a/tools/server/webui/src/lib/stores/agentic.svelte.ts +++ b/tools/server/webui/src/lib/stores/agentic.svelte.ts @@ -30,12 +30,7 @@ import { ToolSource, ToolPermissionDecision } from '$lib/enums'; import { SvelteMap } from 'svelte/reactivity'; import { ToolsService } from '$lib/services/tools.service'; import { isAbortError } from '$lib/utils'; -import { - DEFAULT_AGENTIC_CONFIG, - NEWLINE_SEPARATOR, - LLM_ERROR_BLOCK_START, - LLM_ERROR_BLOCK_END -} from '$lib/constants'; +import { DEFAULT_AGENTIC_CONFIG, NEWLINE_SEPARATOR } from '$lib/constants'; import { IMAGE_MIME_TO_EXTENSION, DATA_URI_BASE64_REGEX, @@ -640,10 +635,9 @@ class AgenticStore { return; } const normalizedError = error instanceof Error ? error : new Error('LLM stream error'); - // Save error as content in the current turn - onChunk?.(`${LLM_ERROR_BLOCK_START}${normalizedError.message}${LLM_ERROR_BLOCK_END}`); + // preserve partial output as is, the outer error dialog informs the user separately await onAssistantTurnComplete?.( - turnContent + `${LLM_ERROR_BLOCK_START}${normalizedError.message}${LLM_ERROR_BLOCK_END}`, + turnContent, turnReasoningContent || undefined, this.buildFinalTimings(capturedTimings, agenticTimings), undefined diff --git a/tools/server/webui/src/lib/stores/chat.svelte.ts b/tools/server/webui/src/lib/stores/chat.svelte.ts index 7c34579ca5..04a735eec9 100644 --- a/tools/server/webui/src/lib/stores/chat.svelte.ts +++ b/tools/server/webui/src/lib/stores/chat.svelte.ts @@ -814,7 +814,7 @@ class ChatStore { ); } }, - onError: (error: Error) => { + onError: async (error: Error) => { this.setStreamingActive(false); if (isAbortError(error)) { cleanupStreamingState(); @@ -826,13 +826,10 @@ class ChatStore { return; } console.error('Streaming error:', error); + // keep whatever was streamed so far, the message stays in memory and in DB + await this.savePartialResponseIfNeeded(convId); cleanupStreamingState(); this.clearPendingMessage(convId); - const idx = conversationsStore.findMessageIndex(assistantMessage.id); - if (idx !== -1) { - const failedMessage = conversationsStore.removeMessageAtIndex(idx); - if (failedMessage) DatabaseService.deleteMessage(failedMessage.id).catch(console.error); - } const contextInfo = ( error as Error & { contextInfo?: { n_prompt_tokens: number; n_ctx: number } } ).contextInfo; @@ -1389,9 +1386,17 @@ class ChatStore { } console.error('Continue generation error:', error); - conversationsStore.updateMessageAtIndex(idx, { content: originalContent }); - - await DatabaseService.updateMessage(msg.id, { content: originalContent }); + // keep whatever was appended so far, the message stays in memory and in DB + await DatabaseService.updateMessage(msg.id, { + content: originalContent + appendedContent, + reasoningContent: originalReasoning + appendedReasoning || undefined, + timestamp: Date.now() + }); + conversationsStore.updateMessageAtIndex(idx, { + content: originalContent + appendedContent, + reasoningContent: originalReasoning + appendedReasoning || undefined, + timestamp: Date.now() + }); this.setChatLoading(msg.convId, false); this.clearChatStreaming(msg.convId);