11import { getDeviceId } from './deviceId.js'
22
33// ─────────────────────────────────────────────────────────────
4- // AI SDK Data Stream Protocol Parser
5- // @see https://sdk.vercel.ai /docs/ai-sdk-ui/stream-protocol
4+ // AI SDK UI Message Stream Parser
5+ // @see https://ai- sdk.dev /docs/ai-sdk-ui/streaming-data
66// ─────────────────────────────────────────────────────────────
77
88/**
9- * AI SDK Data Stream Protocol 타입 ID
10- * 포맷: `{type_id}:{json }\n`
9+ * UI Message Stream 라인을 파싱하여 이벤트로 변환
10+ * 포맷: `data: {... }\n\n` (SSE)
1111 */
12- const DATA_STREAM_TYPES = {
13- TEXT : '0' , // 텍스트 청크
14- DATA : '2' , // 커스텀 데이터 파트
15- ERROR : 'e' , // 에러
16- FINISH : 'd' // 완료
17- } as const
12+ function parseUIMessageStreamLine ( line : string ) : StreamEvent | null {
13+ // SSE 포맷: "data: {...}" 또는 "data: [DONE]"
14+ if ( ! line . startsWith ( 'data: ' ) ) return null
1815
19- /**
20- * Data Stream 라인을 파싱하여 이벤트로 변환
21- */
22- function parseDataStreamLine ( line : string ) : StreamEvent | null {
23- if ( ! line || line . length < 2 ) return null
24-
25- const typeId = line [ 0 ]
26- if ( line [ 1 ] !== ':' ) return null
27-
28- const jsonStr = line . slice ( 2 )
16+ const jsonStr = line . slice ( 6 ) // "data: " 제거
17+ if ( jsonStr === '[DONE]' ) return null
2918
3019 try {
31- switch ( typeId ) {
32- case DATA_STREAM_TYPES . TEXT : {
33- const text = JSON . parse ( jsonStr ) as string
34- return { type : 'content' , content : text }
35- }
36-
37- case DATA_STREAM_TYPES . DATA : {
38- // Data parts는 배열로 옴: [{ type: 'data-xxx', ... }]
39- const parts = JSON . parse ( jsonStr ) as Array < { type : string ; [ key : string ] : unknown } >
40- for ( const part of parts ) {
41- return convertDataPart ( part )
42- }
43- return null
44- }
45-
46- case DATA_STREAM_TYPES . ERROR : {
47- const error = JSON . parse ( jsonStr ) as string
48- return { type : 'error' , error }
49- }
50-
51- case DATA_STREAM_TYPES . FINISH : {
52- // finish 이벤트는 무시 (done 이벤트가 별도로 옴)
53- return null
54- }
55-
56- default :
57- return null
58- }
20+ const event = JSON . parse ( jsonStr ) as { type : string ; [ key : string ] : unknown }
21+ return convertUIMessageEvent ( event )
5922 } catch {
6023 return null
6124 }
6225}
6326
6427/**
65- * AI SDK 커스텀 데이터 파트를 CLI StreamEvent로 변환
28+ * UI Message Stream 이벤트를 CLI StreamEvent로 변환
6629 */
67- function convertDataPart ( part : { type : string ; [ key : string ] : unknown } ) : StreamEvent | null {
68- switch ( part . type ) {
30+ function convertUIMessageEvent ( event : {
31+ type : string
32+ [ key : string ] : unknown
33+ } ) : StreamEvent | null {
34+ switch ( event . type ) {
35+ case 'text-delta' :
36+ return { type : 'content' , content : event . delta as string }
37+
6938 case 'data-session' :
70- return { type : 'session' , sessionId : part . sessionId as string }
39+ return { type : 'session' , sessionId : ( event . data as { sessionId : string } ) . sessionId }
7140
7241 case 'data-sources' :
73- return { type : 'sources' , sources : part . sources as Source [ ] }
42+ return { type : 'sources' , sources : ( event . data as { sources : Source [ ] } ) . sources }
7443
7544 case 'data-progress' :
76- return { type : 'progress' , items : part . items as ProgressItem [ ] }
45+ return { type : 'progress' , items : ( event . data as { items : ProgressItem [ ] } ) . items }
7746
7847 case 'data-clarification' :
79- return { type : 'clarification' , suggestedQuestions : part . suggestedQuestions as string [ ] }
48+ return {
49+ type : 'clarification' ,
50+ suggestedQuestions : ( event . data as { suggestedQuestions : string [ ] } ) . suggestedQuestions
51+ }
8052
8153 case 'data-followup' :
82- return { type : 'followup' , suggestedQuestions : part . suggestedQuestions as string [ ] }
54+ return {
55+ type : 'followup' ,
56+ suggestedQuestions : ( event . data as { suggestedQuestions : string [ ] } ) . suggestedQuestions
57+ }
8358
8459 case 'data-escalation' :
8560 return {
8661 type : 'escalation' ,
87- reason : part . reason as string ,
88- uncertainty : part . uncertainty as number
62+ reason : ( event . data as { reason : string } ) . reason ,
63+ uncertainty : ( event . data as { uncertainty : number } ) . uncertainty
8964 }
9065
91- case 'data-done' :
92- return {
93- type : 'done' ,
94- metadata : part . metadata as {
66+ case 'data-done' : {
67+ const data = event . data as {
68+ metadata : {
9569 searchQuery : string
9670 searchResults : number
9771 processingTime : number
@@ -104,10 +78,13 @@ function convertDataPart(part: { type: string; [key: string]: unknown }): Stream
10478 confidence ?: 'high' | 'medium' | 'low'
10579 }
10680 }
81+ return { type : 'done' , metadata : data . metadata }
82+ }
10783
108- case 'data- error' :
109- return { type : 'error' , error : part . error as string }
84+ case 'error' :
85+ return { type : 'error' , error : event . errorText as string }
11086
87+ // start, text-start, text-end, finish 등은 무시
11188 default :
11289 return null
11390 }
@@ -434,9 +411,9 @@ export class PersonaApiClient {
434411 buffer = lines . pop ( ) || ''
435412
436413 for ( const line of lines ) {
437- // AI SDK Data Stream Protocol 파싱
438- // 포맷: `{type_id}:{json}\n` (예: `0:"hello"`, `2:[ {...}]` )
439- const event = parseDataStreamLine ( line )
414+ // AI SDK UI Message Stream 파싱
415+ // 포맷: `data: {...}\n\n` (SSE )
416+ const event = parseUIMessageStreamLine ( line )
440417 if ( event ) {
441418 yield event
442419 }
0 commit comments