I want to help you diagnose an issue that comes up when you stream multiple things one after the other. Here we are streaming one paragraph of a story at a time. And before each paragraph, we're writing paragraph 1, paragraph 2, and paragraph 3.
In the code, we're inside a UIMessageStream. We have a writeTextPart function that essentially just writes a single text part.
const writeTextPart = (writer: UIMessageStreamWriter,text: string,) => {const textPartId = crypto.randomUUID();writer.write({type: 'text-start',id: textPartId,});
If you don't know what this code does, then there's other reference material that explains it.
In our main stream execution, we:
We do this three times until we have three paragraphs. Here's the code for the first paragraph:
writeTextPart(writer, 'Paragraph 1: ');const firstParagraphResult = streamText({model: google('gemini-2.0-flash-lite'),messages: [...modelMessages,{role: 'user',content:'Given the conversation history above, write the first paragraph of a story. Make it short.'
The issue we're getting is a really weird one. At the top of our UI, we end up with two separate messages where one message only has paragraph one, but the second message also has paragraph one. Check the video above for a visual.
The issue is that there are multiple start and finish chunks being streamed in:
I thought this was sort of okay to have multiple finish and start parts like this. But I chatted to the lead maintainer of the AI SDK, Lars, and he said, "Oh no, we do not support that."
You should be very, very careful about your start and finish part, otherwise you're going to get really weird errors like double messages in the frontend.
The way this code should work is we should only have:
start part at the very start of the streamfinish part right at the endWe can fix this by uncommenting the commented TODO in our code and manually writing the start part:
// TODO: Try uncommenting this and see what happens// writer.write({// type: 'start',// });
Just this change, by the way, is enough to fix the weird error that we were getting. The issue was that this text part was being streamed before the stream had started, and so it was being counted as a separate message.
Whereas if we try it again now, then we only get a single message because it's been started.
We can clean this up a lot more even by going into the toUIMessageStream function and saying whether we want it to send a start part or send a finish part.
For the first paragraph, we don't need it to send a start part because we've already started it, and we don't need to send a finish part either because there's a lot more to go.
Same is true in the second paragraph too. We can uncomment both of these parameters:
And then in the third paragraph down here, we only need to say sendStart: false because we do want it to add a finish part:
Now when we run this, we can see we get a type start here and there's no type finish until all the way at the end. And so this behavior, even though it's pretty annoying to have to manually do it yourself, more closely matches what the AI SDK actually expects.
So if you ever have any bugs where you have multiple different messages being streamed in, then it's probably something to do with your start and finish parts.
Nice work, and I will see you in the next one.
writer.merge(firstParagraphResult.toUIMessageStream({// TODO: Try uncommenting these and see what happens// sendStart: false,// sendFinish: false,}),);
writer.merge(secondParagraphResult.toUIMessageStream({// TODO: Try uncommenting these and see what happens// sendStart: false,// sendFinish: false,}),);
writer.merge(thirdParagraphResult.toUIMessageStream({// TODO: Try uncommenting these and see what happens// sendStart: false,}),);