feat: add finalizeIntermediaryMessage for StreamingProcessor (#5333) * feat: add finalizeIntermediaryMessage for streaming tool call chains Add a lighter finalization method on StreamingProcessor that runs before tool invocation. This ensures CHARACTER_MESSAGE_RENDERED is emitted for streamed text messages before the tool call chain continues, allowing extensions (TTS, image forwarding, etc.) to process intermediary messages. finalizeIntermediaryMessage performs essential processing: - Code block styling (addCopyToCodeBlocks) - Reasoning handler finalization - Logprobs saving - Image attachment processing - Reasoning signature storage - MESSAGE_RECEIVED and CHARACTER_MESSAGE_RENDERED events Without the heavier onFinishStreaming operations: - UI unlock (markUIGenStopped) - Auto-swipe - Sound playback - Chat saving - Swipe counter update * fix: return Error objects from invokeFunctionTool and create error invocations invokeFunctionTool previously called .toString() on caught errors, converting them to plain strings. This made the 'instanceof Error' check in invokeFunctionTools dead code - errors silently became successful invocations, showToolCallError never fired, and the result.errors array was always empty. Changes: - Return Error objects directly from invokeFunctionTool - Create error invocations with error: true flag when tools fail - Add error field to ToolInvocation typedef * fix: record failed stealth tools in stealthCalls and preserve signature/reasoning on error invocations When a stealth tool errors, its name was not added to stealthCalls, causing shouldStopGeneration to evaluate as false. This led to an incorrect recursive Generate('normal') call instead of stopping generation as the stealth tool contract requires ('no follow-up generation'). Also preserve toolCall.signature and reasoningText on error invocations to match the success path, preventing Gemini/OpenRouter multi-turn tool context from breaking when a tool call fails. * Clarify method comments * fix: initialize error property in ToolInvocation to false * Apply review suggestions * Make options object required * fix: remove unnecessary return statement in updateSwipeCounter method * refactor: split tool call error handling into separate PR (#5351) --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -3662,7 +3662,16 @@ class StreamingProcessor { | ||
| 3662 | 3662 | } |
| 3663 | 3663 | } |
| 3664 | 3664 | |
| 3665 | - async onFinishStreaming(messageId, text) { | |
| 3665 | + /** | |
| 3666 | + * Finalizes an intermediary message after generation is complete, or a tool call is performed. | |
| 3667 | + * Performs essential message processing (code blocks, reasoning, swipes, attachments, events) | |
| 3668 | + * without the heavier finish operations (UI unlock - optional, auto-swipe, sound, save chat). | |
| 3669 | + * @param {number} messageId - The message ID to finalize. | |
| 3670 | + * @param {string} text - The message text. | |
| 3671 | + * @param {Object} options - Additional options for finalization. | |
| 3672 | + * @param {boolean} options.unlockUI - Whether to unlock the generation UI. | |
| 3673 | + */ | |
| 3674 | + async finalizeIntermediaryMessage(messageId, text, { unlockUI = true }) { | |
| 3666 | 3675 | await this.onProgressStreaming(messageId, text, true); |
| 3667 | 3676 | const messageElement = chatElement.find(`.mes[mesid="${messageId}"]`); |
| 3668 | 3677 | const message = chat[messageId]; |
| @@ -3701,7 +3710,9 @@ class StreamingProcessor { | ||
| 3701 | 3710 | message.extra.reasoning_signature = this.reasoningSignature; |
| 3702 | 3711 | } |
| 3703 | 3712 | |
| 3713 | + if (unlockUI) { | |
| 3704 | 3714 | this.markUIGenStopped(); |
| 3715 | + } | |
| 3705 | 3716 | |
| 3706 | 3717 | if (this.type !== 'impersonate') { |
| 3707 | 3718 | await eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId, this.type); |
| @@ -3711,6 +3722,10 @@ class StreamingProcessor { | ||
| 3711 | 3722 | } |
| 3712 | 3723 | |
| 3713 | 3724 | updateSwipeCounter(messageId, { message, messageElement }); |
| 3725 | + } | |
| 3726 | + | |
| 3727 | + async onFinishStreaming(messageId, text) { | |
| 3728 | + await this.finalizeIntermediaryMessage(messageId, text, { unlockUI: true }); | |
| 3714 | 3729 | |
| 3715 | 3730 | const isAborted = this.abortController.signal.aborted; |
| 3716 | 3731 | if (!isAborted && power_user.auto_swipe && generatedTextFiltered(text)) { |
| @@ -5306,6 +5321,9 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 5306 | 5321 | const hasToolCalls = ToolManager.hasToolCalls(streamingProcessor.toolCalls); |
| 5307 | 5322 | const shouldDeleteMessage = type !== 'swipe' && ['', '...'].includes(lastMessage?.mes) && !lastMessage?.extra?.reasoning && ['', '...'].includes(streamingProcessor?.result); |
| 5308 | 5323 | hasToolCalls && shouldDeleteMessage && await deleteLastMessage(); |
| 5324 | + if (hasToolCalls && !shouldDeleteMessage) { | |
| 5325 | + await streamingProcessor.finalizeIntermediaryMessage(streamingProcessor.messageId, getMessage, { unlockUI: false }); | |
| 5326 | + } | |
| 5309 | 5327 | const invocationResult = await ToolManager.invokeFunctionTools(streamingProcessor.toolCalls, { |
| 5310 | 5328 | reasoningText: streamingProcessor.reasoningHandler.reasoning, |
| 5311 | 5329 | }); |