fix: Detect streaming state in reasoning content blocks (#21549)

This commit is contained in:
Aleksander Grygier 2026-04-07 12:04:41 +02:00 committed by GitHub
parent d1f82e382d
commit ecce0087da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 133 additions and 92 deletions

File diff suppressed because one or more lines are too long

View File

@ -18,7 +18,7 @@
<div style="display: contents"> <div style="display: contents">
<script> <script>
{ {
__sveltekit_1y361v9 = { __sveltekit_1wqaxod = {
base: new URL('.', location).pathname.slice(0, -1) base: new URL('.', location).pathname.slice(0, -1)
}; };

View File

@ -33,7 +33,7 @@
const showToolCallInProgress = $derived(config().showToolCallInProgress as boolean); const showToolCallInProgress = $derived(config().showToolCallInProgress as boolean);
const showThoughtInProgress = $derived(config().showThoughtInProgress as boolean); const showThoughtInProgress = $derived(config().showThoughtInProgress as boolean);
const sections = $derived(deriveAgenticSections(message, toolMessages, [])); const sections = $derived(deriveAgenticSections(message, toolMessages, [], isStreaming));
// Parse tool results with images // Parse tool results with images
const sectionsParsed = $derived( const sectionsParsed = $derived(

View File

@ -38,14 +38,19 @@ export type ToolResultLine = {
function deriveSingleTurnSections( function deriveSingleTurnSections(
message: DatabaseMessage, message: DatabaseMessage,
toolMessages: DatabaseMessage[] = [], toolMessages: DatabaseMessage[] = [],
streamingToolCalls: ApiChatCompletionToolCall[] = [] streamingToolCalls: ApiChatCompletionToolCall[] = [],
isStreaming: boolean = false
): AgenticSection[] { ): AgenticSection[] {
const sections: AgenticSection[] = []; const sections: AgenticSection[] = [];
// 1. Reasoning content (from dedicated field) // 1. Reasoning content (from dedicated field)
if (message.reasoningContent) { if (message.reasoningContent) {
const toolCalls = parseToolCalls(message.toolCalls);
const hasContentAfterReasoning =
!!message.content?.trim() || toolCalls.length > 0 || streamingToolCalls.length > 0;
const isPending = isStreaming && !hasContentAfterReasoning;
sections.push({ sections.push({
type: AgenticSectionType.REASONING, type: isPending ? AgenticSectionType.REASONING_PENDING : AgenticSectionType.REASONING,
content: message.reasoningContent content: message.reasoningContent
}); });
} }
@ -104,12 +109,13 @@ function deriveSingleTurnSections(
export function deriveAgenticSections( export function deriveAgenticSections(
message: DatabaseMessage, message: DatabaseMessage,
toolMessages: DatabaseMessage[] = [], toolMessages: DatabaseMessage[] = [],
streamingToolCalls: ApiChatCompletionToolCall[] = [] streamingToolCalls: ApiChatCompletionToolCall[] = [],
isStreaming: boolean = false
): AgenticSection[] { ): AgenticSection[] {
const hasAssistantContinuations = toolMessages.some((m) => m.role === MessageRole.ASSISTANT); const hasAssistantContinuations = toolMessages.some((m) => m.role === MessageRole.ASSISTANT);
if (!hasAssistantContinuations) { if (!hasAssistantContinuations) {
return deriveSingleTurnSections(message, toolMessages, streamingToolCalls); return deriveSingleTurnSections(message, toolMessages, streamingToolCalls, isStreaming);
} }
const sections: AgenticSection[] = []; const sections: AgenticSection[] = [];
@ -127,7 +133,12 @@ export function deriveAgenticSections(
const isLastTurn = i + 1 + turnToolMsgs.length >= toolMessages.length; const isLastTurn = i + 1 + turnToolMsgs.length >= toolMessages.length;
sections.push( sections.push(
...deriveSingleTurnSections(msg, turnToolMsgs, isLastTurn ? streamingToolCalls : []) ...deriveSingleTurnSections(
msg,
turnToolMsgs,
isLastTurn ? streamingToolCalls : [],
isLastTurn && isStreaming
)
); );
i += 1 + turnToolMsgs.length; i += 1 + turnToolMsgs.length;

View File

@ -162,6 +162,36 @@ describe('deriveAgenticSections', () => {
expect(sections[4].content).toBe('Here is the analysis.'); expect(sections[4].content).toBe('Here is the analysis.');
}); });
it('returns REASONING_PENDING when streaming with only reasoning content', () => {
const msg = makeAssistant({
reasoningContent: 'Let me think about this...'
});
const sections = deriveAgenticSections(msg, [], [], true);
expect(sections).toHaveLength(1);
expect(sections[0].type).toBe(AgenticSectionType.REASONING_PENDING);
expect(sections[0].content).toBe('Let me think about this...');
});
it('returns REASONING (not pending) when streaming but text content has appeared', () => {
const msg = makeAssistant({
content: 'The answer is',
reasoningContent: 'Let me think...'
});
const sections = deriveAgenticSections(msg, [], [], true);
expect(sections).toHaveLength(2);
expect(sections[0].type).toBe(AgenticSectionType.REASONING);
expect(sections[1].type).toBe(AgenticSectionType.TEXT);
});
it('returns REASONING (not pending) when not streaming', () => {
const msg = makeAssistant({
reasoningContent: 'Let me think...'
});
const sections = deriveAgenticSections(msg, [], [], false);
expect(sections).toHaveLength(1);
expect(sections[0].type).toBe(AgenticSectionType.REASONING);
});
it('multi-turn: streaming tool calls on last turn', () => { it('multi-turn: streaming tool calls on last turn', () => {
const assistant1 = makeAssistant({ const assistant1 = makeAssistant({
toolCalls: JSON.stringify([ toolCalls: JSON.stringify([