From 991926b6856180681b63e7c87ef2647128f6267a Mon Sep 17 00:00:00 2001 From: ai-sdk-factory <308175966+ai-sdk-factory@users.noreply.github.com> Date: Wed, 5 Aug 2026 22:01:57 +0000 Subject: [PATCH] reproduction (reproduced) Co-authored-by: Gregor Martynus <39992+gr2m@users.noreply.github.com> --- .ai-sdk-factory/reproduction-replay.json | 13 ++ examples/ai-functions/package.json | 11 ++ ...sume-stream-duplicates-previous-message.ts | 128 ++++++++++++++++++ 3 files changed, 152 insertions(+) create mode 100644 .ai-sdk-factory/reproduction-replay.json create mode 100644 examples/ai-functions/package.json create mode 100644 examples/ai-functions/src/reproduction/issue-18457-resume-stream-duplicates-previous-message.ts diff --git a/.ai-sdk-factory/reproduction-replay.json b/.ai-sdk-factory/reproduction-replay.json new file mode 100644 index 000000000000..45688ca8e6e1 --- /dev/null +++ b/.ai-sdk-factory/reproduction-replay.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "issueId": 18457, + "command": "pnpm -C examples/ai-functions exec tsx src/reproduction/issue-18457-resume-stream-duplicates-previous-message.ts", + "artifactPaths": [ + "examples/ai-functions/package.json", + "examples/ai-functions/src/reproduction/issue-18457-resume-stream-duplicates-previous-message.ts" + ], + "expectedFailure": { + "exitCode": 1, + "signal": "ISSUE_18457_REPRODUCED: resumeStream duplicated the previous assistant message" + } +} diff --git a/examples/ai-functions/package.json b/examples/ai-functions/package.json new file mode 100644 index 000000000000..084fcda5c199 --- /dev/null +++ b/examples/ai-functions/package.json @@ -0,0 +1,11 @@ +{ + "name": "@example/ai-functions", + "version": "0.0.0", + "private": true, + "dependencies": { + "ai": "link:../../packages/ai" + }, + "devDependencies": { + "tsx": "link:../ai-core/node_modules/tsx" + } +} diff --git a/examples/ai-functions/src/reproduction/issue-18457-resume-stream-duplicates-previous-message.ts b/examples/ai-functions/src/reproduction/issue-18457-resume-stream-duplicates-previous-message.ts new file mode 100644 index 000000000000..219080a7da97 --- /dev/null +++ b/examples/ai-functions/src/reproduction/issue-18457-resume-stream-duplicates-previous-message.ts @@ -0,0 +1,128 @@ +import { + AbstractChat, + type ChatState, + type ChatStatus, + type ChatTransport, + type UIMessage, + type UIMessageChunk, +} from 'ai'; + +class State implements ChatState { + status: ChatStatus = 'ready'; + error: Error | undefined; + + constructor(public messages: UIMessage[]) {} + + pushMessage = (message: UIMessage) => { + this.messages = [...this.messages, message]; + }; + + popMessage = () => { + this.messages = this.messages.slice(0, -1); + }; + + replaceMessage = (index: number, message: UIMessage) => { + this.messages = this.messages.map((current, currentIndex) => + currentIndex === index ? message : current, + ); + }; + + snapshot = (value: T): T => structuredClone(value); +} + +class Chat extends AbstractChat {} + +function chunkStream(chunks: UIMessageChunk[]) { + let index = 0; + + return new ReadableStream({ + pull(controller) { + if (index >= chunks.length) { + controller.close(); + } else { + controller.enqueue(chunks[index++]); + } + }, + }); +} + +function text(message: UIMessage | undefined) { + return ( + message?.parts + .filter(part => part.type === 'text') + .map(part => part.text) + .join(' | ') ?? '' + ); +} + +async function main() { + const previousAnswer = 'The capital of France is Paris.'; + const resumedAnswer = 'It has been the capital since 987 AD.'; + const state = new State([ + { + id: 'u1', + role: 'user', + parts: [{ type: 'text', text: 'What is the capital of France?' }], + }, + { + id: 'a1', + role: 'assistant', + parts: [{ type: 'text', text: previousAnswer }], + }, + ]); + + const transport: ChatTransport = { + sendMessages: async () => chunkStream([]), + reconnectToStream: async () => + chunkStream([ + { type: 'start', messageId: 'server-msg-2' }, + { type: 'text-start', id: 't1' }, + { type: 'text-delta', id: 't1', delta: resumedAnswer }, + { type: 'text-end', id: 't1' }, + { type: 'finish' }, + ]), + }; + + const chat = new Chat({ + id: 'c1', + state, + transport, + generateId: () => 'client-generated', + }); + + await chat.resumeStream(); + + const previousAnswerOccurrences = state.messages.filter(message => + text(message).includes(previousAnswer), + ).length; + const resumedMessage = state.messages.find( + message => message.id === 'server-msg-2', + ); + const resumedText = text(resumedMessage); + + if (previousAnswerOccurrences !== 1 || resumedText.includes(previousAnswer)) { + console.error( + 'ISSUE_18457_REPRODUCED: resumeStream duplicated the previous assistant message', + ); + console.error(JSON.stringify(state.messages, null, 2)); + process.exitCode = 1; + return; + } + + if ( + state.messages.length !== 3 || + resumedText !== resumedAnswer || + text(state.messages[1]) !== previousAnswer + ) { + throw new Error( + `Unexpected resumeStream result: ${JSON.stringify(state.messages)}`, + ); + } + + console.log('Issue #18457 did not reproduce.'); +} + +main().catch(error => { + console.error(error); + process.exitCode = 1; +});