Skip to content

Commit c45d100

Browse files
fix (core): send buffered text in smooth stream when stream parts change (#5531)
Co-authored-by: Carl Brugger <cebrugg@gmail.com>
1 parent 8362d21 commit c45d100

3 files changed

Lines changed: 167 additions & 6 deletions

File tree

.changeset/smooth-mirrors-kneel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'ai': patch
3+
---
4+
5+
fix (core): send buffered text in smooth stream when stream parts change

packages/ai/core/generate-text/smooth-stream.test.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,166 @@ describe('smoothStream', () => {
173173
},
174174
]);
175175
});
176+
177+
it('should send remaining text buffer before tool call starts', async () => {
178+
const stream = convertArrayToReadableStream([
179+
{ type: 'text-delta', textDelta: 'I will check the' },
180+
{ type: 'text-delta', textDelta: ' weather in Lon' },
181+
{ type: 'text-delta', textDelta: 'don.' },
182+
{ type: 'tool-call', name: 'weather', args: { city: 'London' } },
183+
{ type: 'step-finish' },
184+
{ type: 'finish' },
185+
]).pipeThrough(
186+
smoothStream({
187+
delayInMs: 10,
188+
_internal: { delay },
189+
})({ tools: {} }),
190+
);
191+
192+
await consumeStream(stream);
193+
194+
expect(events).toMatchInlineSnapshot(`
195+
[
196+
"delay 10",
197+
{
198+
"textDelta": "I ",
199+
"type": "text-delta",
200+
},
201+
"delay 10",
202+
{
203+
"textDelta": "will ",
204+
"type": "text-delta",
205+
},
206+
"delay 10",
207+
{
208+
"textDelta": "check ",
209+
"type": "text-delta",
210+
},
211+
"delay 10",
212+
{
213+
"textDelta": "the ",
214+
"type": "text-delta",
215+
},
216+
"delay 10",
217+
{
218+
"textDelta": "weather ",
219+
"type": "text-delta",
220+
},
221+
"delay 10",
222+
{
223+
"textDelta": "in ",
224+
"type": "text-delta",
225+
},
226+
{
227+
"textDelta": "London.",
228+
"type": "text-delta",
229+
},
230+
{
231+
"args": {
232+
"city": "London",
233+
},
234+
"name": "weather",
235+
"type": "tool-call",
236+
},
237+
{
238+
"type": "step-finish",
239+
},
240+
{
241+
"type": "finish",
242+
},
243+
]
244+
`);
245+
});
246+
247+
it('should send remaining text buffer before tool call starts and tool call streaming is enabled', async () => {
248+
const stream = convertArrayToReadableStream([
249+
{ type: 'text-delta', textDelta: 'I will check the' },
250+
{ type: 'text-delta', textDelta: ' weather in Lon' },
251+
{ type: 'text-delta', textDelta: 'don.' },
252+
{
253+
type: 'tool-call-streaming-start',
254+
name: 'weather',
255+
args: { city: 'London' },
256+
},
257+
{ type: 'tool-call-delta', name: 'weather', args: { city: 'London' } },
258+
{ type: 'tool-call', name: 'weather', args: { city: 'London' } },
259+
{ type: 'step-finish' },
260+
{ type: 'finish' },
261+
]).pipeThrough(
262+
smoothStream({
263+
delayInMs: 10,
264+
_internal: { delay },
265+
})({ tools: {} }),
266+
);
267+
268+
await consumeStream(stream);
269+
270+
expect(events).toMatchInlineSnapshot(`
271+
[
272+
"delay 10",
273+
{
274+
"textDelta": "I ",
275+
"type": "text-delta",
276+
},
277+
"delay 10",
278+
{
279+
"textDelta": "will ",
280+
"type": "text-delta",
281+
},
282+
"delay 10",
283+
{
284+
"textDelta": "check ",
285+
"type": "text-delta",
286+
},
287+
"delay 10",
288+
{
289+
"textDelta": "the ",
290+
"type": "text-delta",
291+
},
292+
"delay 10",
293+
{
294+
"textDelta": "weather ",
295+
"type": "text-delta",
296+
},
297+
"delay 10",
298+
{
299+
"textDelta": "in ",
300+
"type": "text-delta",
301+
},
302+
{
303+
"textDelta": "London.",
304+
"type": "text-delta",
305+
},
306+
{
307+
"args": {
308+
"city": "London",
309+
},
310+
"name": "weather",
311+
"type": "tool-call-streaming-start",
312+
},
313+
{
314+
"args": {
315+
"city": "London",
316+
},
317+
"name": "weather",
318+
"type": "tool-call-delta",
319+
},
320+
{
321+
"args": {
322+
"city": "London",
323+
},
324+
"name": "weather",
325+
"type": "tool-call",
326+
},
327+
{
328+
"type": "step-finish",
329+
},
330+
{
331+
"type": "finish",
332+
},
333+
]
334+
`);
335+
});
176336
});
177337

178338
describe('line chunking', () => {

packages/ai/core/generate-text/smooth-stream.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,10 @@ export function smoothStream<TOOLS extends ToolSet>({
4444

4545
return () => {
4646
let buffer = '';
47+
4748
return new TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>>({
4849
async transform(chunk, controller) {
49-
if (chunk.type === 'step-finish') {
50+
if (chunk.type !== 'text-delta') {
5051
if (buffer.length > 0) {
5152
controller.enqueue({ type: 'text-delta', textDelta: buffer });
5253
buffer = '';
@@ -56,11 +57,6 @@ export function smoothStream<TOOLS extends ToolSet>({
5657
return;
5758
}
5859

59-
if (chunk.type !== 'text-delta') {
60-
controller.enqueue(chunk);
61-
return;
62-
}
63-
6460
buffer += chunk.textDelta;
6561

6662
let match;

0 commit comments

Comments
 (0)