@@ -33,11 +33,9 @@ function sse(data: object): string {
3333 */
3434export function encodeStreamPart ( part : TextStreamPart < ToolSet > ) : string {
3535 switch ( part . type ) {
36- // ── Text ──────────────────────────────────────────────────
3736 case 'text-delta' :
3837 return sse ( { type : 'text-delta' , id : '0' , delta : part . text } ) ;
3938
40- // ── Tool calling ─────────────────────────────────────────
4139 case 'tool-input-start' :
4240 return sse ( {
4341 type : 'tool-input-start' ,
@@ -67,24 +65,13 @@ export function encodeStreamPart(part: TextStreamPart<ToolSet>): string {
6765 output : part . output ,
6866 } ) ;
6967
70- // ── Finish / Step ────────────────────────────────────────
71- case 'finish-step' :
72- return sse ( { type : 'finish-step' } ) ;
73-
74- case 'finish' :
75- return sse ( {
76- type : 'finish' ,
77- finishReason : part . finishReason ,
78- } ) ;
79-
80- // ── Error ────────────────────────────────────────────────
8168 case 'error' :
8269 return sse ( {
8370 type : 'error' ,
8471 errorText : String ( part . error ) ,
8572 } ) ;
8673
87- // ── Unhandled types (silently skip) ──────────────────────
74+ // finish-step and finish are handled by the generator, not here
8875 default :
8976 return '' ;
9077 }
@@ -94,8 +81,8 @@ export function encodeStreamPart(part: TextStreamPart<ToolSet>): string {
9481 * Transform an `AsyncIterable<TextStreamPart>` into an `AsyncIterable<string>`
9582 * where each yielded string is an SSE-formatted UI Message Stream chunk.
9683 *
97- * Emits the required `start`, `start-step`, `text-start` preamble and
98- * ` text-end`, ` finish-step`, ` finish`, ` [DONE]` postamble automatically.
84+ * Lifecycle order required by the client:
85+ * start → start-step → text-start → text-delta* → text- end → finish-step → finish → [DONE]
9986 */
10087export async function * encodeVercelDataStream (
10188 events : AsyncIterable < TextStreamPart < ToolSet > > ,
@@ -105,24 +92,37 @@ export async function* encodeVercelDataStream(
10592 yield sse ( { type : 'start-step' } ) ;
10693 yield sse ( { type : 'text-start' , id : '0' } ) ;
10794
95+ let textOpen = true ;
10896 let finishReason = 'stop' ;
10997
11098 for await ( const part of events ) {
99+ // Capture finish reason
111100 if ( part . type === 'finish' ) {
112101 finishReason = part . finishReason ?? 'stop' ;
113102 }
103+
104+ // Before finish-step/finish, close the text part first
105+ if ( part . type === 'finish-step' || part . type === 'finish' ) {
106+ if ( textOpen ) {
107+ yield sse ( { type : 'text-end' , id : '0' } ) ;
108+ textOpen = false ;
109+ }
110+ // Don't emit these via encodeStreamPart — we handle them in postamble
111+ continue ;
112+ }
113+
114114 const frame = encodeStreamPart ( part ) ;
115115 if ( frame ) {
116116 yield frame ;
117117 }
118118 }
119119
120- // Postamble — text-end + finish-step + finish are already emitted by
121- // encodeStreamPart when the corresponding parts arrive from the LLM.
122- // However, we always need text-end and the [DONE] sentinel.
123- yield sse ( { type : 'text-end' , id : '0' } ) ;
124- // finish-step and finish may have already been emitted; emit them
125- // again only as safeguards — the client handles duplicates gracefully.
120+ // Close text if still open (safety)
121+ if ( textOpen ) {
122+ yield sse ( { type : ' text-end' , id : '0' } ) ;
123+ }
124+
125+ // Postamble
126126 yield sse ( { type : 'finish-step' } ) ;
127127 yield sse ( { type : 'finish' , finishReason } ) ;
128128 yield 'data: [DONE]\n\n' ;
0 commit comments