fix: separate reasoning blocks with newline on markdown boundaries (#569)
This commit is contained in:
@@ -109,6 +109,68 @@ describe('createDisplayPipeline', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('inserts newline before reasoning chunk starting with bold header', async () => {
|
||||
const events = await collect([
|
||||
{ type: 'reasoning', content: 'First section ends here.', runId: 'run-1' },
|
||||
{ type: 'reasoning', content: '**Second Section**\nMore text.', runId: 'run-1' },
|
||||
{ type: 'assistant', content: 'reply', runId: 'run-1' },
|
||||
{ type: 'result', success: true, result: 'reply', runIds: ['run-1'] },
|
||||
]);
|
||||
|
||||
const reasoning = events.find(e => e.type === 'reasoning');
|
||||
expect(reasoning).toBeDefined();
|
||||
if (reasoning?.type === 'reasoning') {
|
||||
expect(reasoning.content).toBe('First section ends here.\n**Second Section**\nMore text.');
|
||||
}
|
||||
});
|
||||
|
||||
it('inserts newline before reasoning chunk starting with markdown heading', async () => {
|
||||
const events = await collect([
|
||||
{ type: 'reasoning', content: 'End of thought.', runId: 'run-1' },
|
||||
{ type: 'reasoning', content: '## Next Topic\nDetails here.', runId: 'run-1' },
|
||||
{ type: 'assistant', content: 'reply', runId: 'run-1' },
|
||||
{ type: 'result', success: true, result: 'reply', runIds: ['run-1'] },
|
||||
]);
|
||||
|
||||
const reasoning = events.find(e => e.type === 'reasoning');
|
||||
expect(reasoning).toBeDefined();
|
||||
if (reasoning?.type === 'reasoning') {
|
||||
expect(reasoning.content).toBe('End of thought.\n## Next Topic\nDetails here.');
|
||||
}
|
||||
});
|
||||
|
||||
it('does not insert separator for token-level streaming chunks', async () => {
|
||||
const events = await collect([
|
||||
{ type: 'reasoning', content: "I'm", runId: 'run-1' },
|
||||
{ type: 'reasoning', content: ' thinking', runId: 'run-1' },
|
||||
{ type: 'reasoning', content: ' about this', runId: 'run-1' },
|
||||
{ type: 'assistant', content: 'reply', runId: 'run-1' },
|
||||
{ type: 'result', success: true, result: 'reply', runIds: ['run-1'] },
|
||||
]);
|
||||
|
||||
const reasoning = events.find(e => e.type === 'reasoning');
|
||||
expect(reasoning).toBeDefined();
|
||||
if (reasoning?.type === 'reasoning') {
|
||||
expect(reasoning.content).toBe("I'm thinking about this");
|
||||
}
|
||||
});
|
||||
|
||||
it('skips separator when buffer already ends with newline', async () => {
|
||||
const events = await collect([
|
||||
{ type: 'reasoning', content: 'First block.\n', runId: 'run-1' },
|
||||
{ type: 'reasoning', content: '**Second block**', runId: 'run-1' },
|
||||
{ type: 'assistant', content: 'reply', runId: 'run-1' },
|
||||
{ type: 'result', success: true, result: 'reply', runIds: ['run-1'] },
|
||||
]);
|
||||
|
||||
const reasoning = events.find(e => e.type === 'reasoning');
|
||||
expect(reasoning).toBeDefined();
|
||||
if (reasoning?.type === 'reasoning') {
|
||||
// No double newline -- buffer already ended with \n
|
||||
expect(reasoning.content).toBe('First block.\n**Second block**');
|
||||
}
|
||||
});
|
||||
|
||||
it('prefers streamed text over result field on divergence', async () => {
|
||||
const events = await collect([
|
||||
{ type: 'assistant', content: 'streamed reply', runId: 'run-1' },
|
||||
|
||||
@@ -237,7 +237,17 @@ export async function* createDisplayPipeline(
|
||||
// ── Dispatch by type ──
|
||||
switch (msg.type) {
|
||||
case 'reasoning': {
|
||||
reasoningBuffer += msg.content || '';
|
||||
const chunk = msg.content || '';
|
||||
// When a new chunk starts with a markdown block indicator (bold header,
|
||||
// heading, list item), insert a newline to prevent it running into the
|
||||
// previous text. This separates complete reasoning blocks (common with
|
||||
// OpenAI models that emit whole sections) without affecting token-level
|
||||
// streaming where tokens don't start with these patterns.
|
||||
if (chunk && reasoningBuffer && !reasoningBuffer.endsWith('\n')
|
||||
&& /^(\*\*|#{1,6}\s|[-*]\s|\d+\.\s)/.test(chunk)) {
|
||||
reasoningBuffer += '\n';
|
||||
}
|
||||
reasoningBuffer += chunk;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user