@bytexbyte/nxtlinq-ai-agent-core-development 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"nxtlinq-api.d.ts","sourceRoot":"","sources":["../../src/api/nxtlinq-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAI5D,OAAO,EAAe,KAAK,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAE5D,YAAY,EAAE,iBAAiB,EAAE,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,OAAO,CAAC;AAEzD,MAAM,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE/C,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,EAAE,SAAS,GAAG,MAAM,GAAG,aAAa,CAAC,CAAC;AAknBpF,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,aAAa,GAClB,UAAU,CAYZ"}
1
+ {"version":3,"file":"nxtlinq-api.d.ts","sourceRoot":"","sources":["../../src/api/nxtlinq-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAI5D,OAAO,EAAe,KAAK,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAE5D,YAAY,EAAE,iBAAiB,EAAE,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,OAAO,CAAC;AAEzD,MAAM,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE/C,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,EAAE,SAAS,GAAG,MAAM,GAAG,aAAa,CAAC,CAAC;AA0nBpF,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,aAAa,GAClB,UAAU,CAYZ"}
@@ -1,7 +1,7 @@
1
1
  import { STORAGE_KEYS } from '../constants/storageKeys';
2
2
  import { createDefaultHttpPort } from '../ports/HttpPort';
3
3
  import { getAiAgentApiHost, getAitServiceApiHost } from './hosts';
4
- import { parseSSEText } from './parse-sse';
4
+ import { parseSSEResponse, parseSSEText } from './parse-sse';
5
5
  import { postTextTts } from './tts';
6
6
  export { postTextTts, buildTextTtsDataUri } from './tts';
7
7
  /** RN/Hermes 的 URLSearchParams 常缺少 set/append,改用手動組 query。 */
@@ -128,7 +128,7 @@ const createAgentApi = (helpers) => ({
128
128
  : {}),
129
129
  };
130
130
  const useSSE = Boolean(params.onPiiProgress || params.onStreamDelta || model.endsWith('-stream'));
131
- const response = await helpers.fetchFn(`${getAiAgentApiHost()}/api/${model}`, {
131
+ const fetchInit = {
132
132
  method: 'POST',
133
133
  headers: {
134
134
  'Content-Type': 'application/json',
@@ -138,7 +138,10 @@ const createAgentApi = (helpers) => ({
138
138
  ...(params.onPiiProgress ? { 'X-Stream-PII': '1' } : {}),
139
139
  },
140
140
  body: JSON.stringify(requestBody),
141
- });
141
+ // React Native: enable incremental body reads for `*-stream` SSE routes.
142
+ ...(useSSE ? { reactNative: { textStreaming: true } } : {}),
143
+ };
144
+ const response = await helpers.fetchFn(`${getAiAgentApiHost()}/api/${model}`, fetchInit);
142
145
  if (!response.ok) {
143
146
  throw new Error('Failed to send message');
144
147
  }
@@ -148,13 +151,14 @@ const createAgentApi = (helpers) => ({
148
151
  onPiiProgress: params.onPiiProgress,
149
152
  onStreamDelta: params.onStreamDelta,
150
153
  };
151
- // Always buffer first: RN often exposes a `body` stream that closes
152
- // immediately without data, which breaks incremental parsing.
154
+ const bodyStream = response.body;
155
+ if (bodyStream?.getReader) {
156
+ return await parseSSEResponse(bodyStream, sseHandlers);
157
+ }
153
158
  const rawText = await response.text();
154
159
  if (rawText.trimStart().startsWith('event:') || rawText.includes('\nevent:')) {
155
160
  return parseSSEText(rawText, sseHandlers);
156
161
  }
157
- // Gateway returned JSON despite SSE content-type.
158
162
  return rawText ? JSON.parse(rawText) : {};
159
163
  }
160
164
  return await response.json();
@@ -1,9 +1,25 @@
1
1
  export type SSEHandlers = {
2
2
  onPiiProgress?: (step: 'scan_start' | 'scan_complete' | 'send_start' | 'done', data?: unknown) => void;
3
+ /** Token chunk from a `text_delta` event. */
3
4
  onStreamDelta?: (text: string) => void;
5
+ /** Accumulated assistant text after each `text_delta` (convenience for chat UIs). */
6
+ onStreamText?: (fullText: string) => void;
4
7
  };
5
- /** Parse a complete SSE payload (React Native fetch often lacks a usable `response.body`). */
8
+ type SSEParseState = {
9
+ streamText: string;
10
+ };
11
+ /**
12
+ * Parse complete SSE blocks from `buffer`, dispatch events, return trailing incomplete bytes.
13
+ */
14
+ export declare function consumeSSEBuffer(buffer: string, handlers: SSEHandlers, finalDataRef: {
15
+ value: unknown | null;
16
+ }, streamState?: SSEParseState): string;
17
+ /** Parse a complete SSE payload (e.g. React Native fallback after `response.text()`). */
6
18
  export declare function parseSSEText(text: string, handlers: SSEHandlers): unknown;
7
- /** Parse Agent SSE: PII steps, optional LLM `text_delta`, final `done`. */
19
+ /**
20
+ * Incrementally parse Agent SSE from a ReadableStream (`text_delta`, PII steps, final `done`).
21
+ * Dispatches handlers as chunks arrive (required for RN chat UIs).
22
+ */
8
23
  export declare function parseSSEResponse(body: ReadableStream<Uint8Array>, handlers: SSEHandlers): Promise<unknown>;
24
+ export {};
9
25
  //# sourceMappingURL=parse-sse.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"parse-sse.d.ts","sourceRoot":"","sources":["../../src/api/parse-sse.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG;IACxB,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,GAAG,YAAY,GAAG,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACvG,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC,CAAC;AA8EF,8FAA8F;AAC9F,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,GAAG,OAAO,CAEzE;AAED,2EAA2E;AAC3E,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,EAChC,QAAQ,EAAE,WAAW,GACpB,OAAO,CAAC,OAAO,CAAC,CAalB"}
1
+ {"version":3,"file":"parse-sse.d.ts","sourceRoot":"","sources":["../../src/api/parse-sse.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG;IACxB,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,GAAG,YAAY,GAAG,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACvG,6CAA6C;IAC7C,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,qFAAqF;IACrF,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CAC3C,CAAC;AAEF,KAAK,aAAa,GAAG;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAkEF;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,WAAW,EACrB,YAAY,EAAE;IAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAA;CAAE,EACvC,WAAW,GAAE,aAAkC,GAC9C,MAAM,CAUR;AA6BD,yFAAyF;AACzF,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,GAAG,OAAO,CAEzE;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,EAChC,QAAQ,EAAE,WAAW,GACpB,OAAO,CAAC,OAAO,CAAC,CAmBlB"}
@@ -1,9 +1,4 @@
1
- /** Normalize CRLF and split into SSE message blocks. */
2
- function splitSSEMessages(raw) {
3
- const normalized = raw.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
4
- return normalized.split('\n\n').filter((block) => block.trim().length > 0);
5
- }
6
- function dispatchSSEMessage(msg, handlers, finalDataRef) {
1
+ function dispatchSSEMessage(msg, handlers, finalDataRef, streamState) {
7
2
  if (!msg.trim())
8
3
  return;
9
4
  let eventType = '';
@@ -46,7 +41,11 @@ function dispatchSSEMessage(msg, handlers, finalDataRef) {
46
41
  /* ignore */
47
42
  }
48
43
  const chunk = typeof parsed.text === 'string' ? parsed.text : '';
49
- handlers.onStreamDelta?.(chunk);
44
+ if (chunk) {
45
+ handlers.onStreamDelta?.(chunk);
46
+ streamState.streamText += chunk;
47
+ handlers.onStreamText?.(streamState.streamText);
48
+ }
50
49
  }
51
50
  else if (eventType === 'error') {
52
51
  let parsed = {};
@@ -67,22 +66,47 @@ function dispatchSSEMessage(msg, handlers, finalDataRef) {
67
66
  }
68
67
  }
69
68
  }
70
- function parseSSEPayload(raw, handlers) {
71
- const finalDataRef = { value: null };
72
- for (const msg of splitSSEMessages(raw)) {
73
- dispatchSSEMessage(msg, handlers, finalDataRef);
69
+ /**
70
+ * Parse complete SSE blocks from `buffer`, dispatch events, return trailing incomplete bytes.
71
+ */
72
+ export function consumeSSEBuffer(buffer, handlers, finalDataRef, streamState = { streamText: '' }) {
73
+ const normalized = buffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
74
+ const parts = normalized.split('\n\n');
75
+ const remainder = parts.pop() ?? '';
76
+ for (const block of parts) {
77
+ dispatchSSEMessage(block, handlers, finalDataRef, streamState);
74
78
  }
79
+ return remainder;
80
+ }
81
+ function flushSSEBuffer(remainder, handlers, finalDataRef, streamState) {
82
+ if (!remainder.trim())
83
+ return;
84
+ dispatchSSEMessage(remainder, handlers, finalDataRef, streamState);
85
+ }
86
+ function finalizeSSEParse(finalDataRef) {
75
87
  if (finalDataRef.value !== null && finalDataRef.value !== undefined) {
76
88
  return finalDataRef.value;
77
89
  }
78
90
  throw new Error('SSE stream ended without done event');
79
91
  }
80
- /** Parse a complete SSE payload (React Native fetch often lacks a usable `response.body`). */
92
+ function parseSSEPayload(raw, handlers) {
93
+ const finalDataRef = { value: null };
94
+ const streamState = { streamText: '' };
95
+ const remainder = consumeSSEBuffer(raw, handlers, finalDataRef, streamState);
96
+ flushSSEBuffer(remainder, handlers, finalDataRef, streamState);
97
+ return finalizeSSEParse(finalDataRef);
98
+ }
99
+ /** Parse a complete SSE payload (e.g. React Native fallback after `response.text()`). */
81
100
  export function parseSSEText(text, handlers) {
82
101
  return parseSSEPayload(text, handlers);
83
102
  }
84
- /** Parse Agent SSE: PII steps, optional LLM `text_delta`, final `done`. */
103
+ /**
104
+ * Incrementally parse Agent SSE from a ReadableStream (`text_delta`, PII steps, final `done`).
105
+ * Dispatches handlers as chunks arrive (required for RN chat UIs).
106
+ */
85
107
  export async function parseSSEResponse(body, handlers) {
108
+ const finalDataRef = { value: null };
109
+ const streamState = { streamText: '' };
86
110
  const reader = body.getReader();
87
111
  const decoder = new TextDecoder();
88
112
  let buffer = '';
@@ -91,7 +115,10 @@ export async function parseSSEResponse(body, handlers) {
91
115
  if (done)
92
116
  break;
93
117
  buffer += decoder.decode(value, { stream: true });
118
+ buffer = consumeSSEBuffer(buffer, handlers, finalDataRef, streamState);
94
119
  }
95
120
  buffer += decoder.decode();
96
- return parseSSEPayload(buffer, handlers);
121
+ buffer = consumeSSEBuffer(buffer, handlers, finalDataRef, streamState);
122
+ flushSSEBuffer(buffer, handlers, finalDataRef, streamState);
123
+ return finalizeSSEParse(finalDataRef);
97
124
  }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { setApiHosts, getAiAgentApiHost, getAitServiceApiHost } from './api/hosts';
2
2
  export { createNxtlinqApiWithDeps, type CoreAITApi, type ApiClientDeps, } from './api/nxtlinq-api';
3
- export { parseSSEResponse, parseSSEText } from './api/parse-sse';
3
+ export { consumeSSEBuffer, parseSSEResponse, parseSSEText, type SSEHandlers, } from './api/parse-sse';
4
4
  export { postTextTts, buildTextTtsDataUri, type PostTextTtsParams, type PostTextTtsResult, } from './api/tts';
5
5
  export * from './ports';
6
6
  export { STORAGE_KEYS } from './constants/storageKeys';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnF,OAAO,EACL,wBAAwB,EACxB,KAAK,UAAU,EACf,KAAK,aAAa,GACnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,GACvB,MAAM,WAAW,CAAC;AAGnB,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAGvD,mBAAmB,iBAAiB,CAAC;AACrC,YAAY,EAAE,WAAW,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAGnF,OAAO,EACL,+BAA+B,EAC/B,yBAAyB,EACzB,0BAA0B,EAC1B,oBAAoB,EACpB,KAAK,oBAAoB,GAC1B,MAAM,0BAA0B,CAAC;AAGlC,cAAc,eAAe,CAAC;AAC9B,OAAO,EACL,sBAAsB,EACtB,wBAAwB,EACxB,uBAAuB,GACxB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,kBAAkB,EAClB,6BAA6B,GAC9B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,yBAAyB,EACzB,KAAK,qBAAqB,GAC3B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAC;AAC5E,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAC1E,YAAY,EAAE,wBAAwB,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAC3G,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EACL,4BAA4B,EAC5B,mCAAmC,EACnC,yBAAyB,EACzB,KAAK,oBAAoB,GAC1B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,2BAA2B,EAC3B,mCAAmC,EACnC,gCAAgC,EAChC,KAAK,qBAAqB,GAC3B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AAGtE,OAAO,EAAE,YAAY,EAAE,KAAK,oBAAoB,EAAE,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACxG,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnF,OAAO,EACL,wBAAwB,EACxB,KAAK,UAAU,EACf,KAAK,aAAa,GACnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,KAAK,WAAW,GACjB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,GACvB,MAAM,WAAW,CAAC;AAGnB,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAGvD,mBAAmB,iBAAiB,CAAC;AACrC,YAAY,EAAE,WAAW,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAGnF,OAAO,EACL,+BAA+B,EAC/B,yBAAyB,EACzB,0BAA0B,EAC1B,oBAAoB,EACpB,KAAK,oBAAoB,GAC1B,MAAM,0BAA0B,CAAC;AAGlC,cAAc,eAAe,CAAC;AAC9B,OAAO,EACL,sBAAsB,EACtB,wBAAwB,EACxB,uBAAuB,GACxB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,kBAAkB,EAClB,6BAA6B,GAC9B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,yBAAyB,EACzB,KAAK,qBAAqB,GAC3B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAC;AAC5E,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAC1E,YAAY,EAAE,wBAAwB,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAC3G,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EACL,4BAA4B,EAC5B,mCAAmC,EACnC,yBAAyB,EACzB,KAAK,oBAAoB,GAC1B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,2BAA2B,EAC3B,mCAAmC,EACnC,gCAAgC,EAChC,KAAK,qBAAqB,GAC3B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AAGtE,OAAO,EAAE,YAAY,EAAE,KAAK,oBAAoB,EAAE,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACxG,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // API
2
2
  export { setApiHosts, getAiAgentApiHost, getAitServiceApiHost } from './api/hosts';
3
3
  export { createNxtlinqApiWithDeps, } from './api/nxtlinq-api';
4
- export { parseSSEResponse, parseSSEText } from './api/parse-sse';
4
+ export { consumeSSEBuffer, parseSSEResponse, parseSSEText, } from './api/parse-sse';
5
5
  export { postTextTts, buildTextTtsDataUri, } from './api/tts';
6
6
  // Ports
7
7
  export * from './ports';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytexbyte/nxtlinq-ai-agent-core-development",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Platform-agnostic nxtlinq AI Agent core — API client, types, and orchestration ports",
5
5
  "main": "dist/index.js",
6
6
  "react-native": "src/index.ts",
@@ -220,7 +220,7 @@ const createAgentApi = (helpers: ReturnType<typeof createApiHelpers>) => ({
220
220
  params.onPiiProgress || params.onStreamDelta || model.endsWith('-stream')
221
221
  );
222
222
 
223
- const response = await helpers.fetchFn(`${getAiAgentApiHost()}/api/${model}`, {
223
+ const fetchInit: RequestInit = {
224
224
  method: 'POST',
225
225
  headers: {
226
226
  'Content-Type': 'application/json',
@@ -230,7 +230,14 @@ const createAgentApi = (helpers: ReturnType<typeof createApiHelpers>) => ({
230
230
  ...(params.onPiiProgress ? { 'X-Stream-PII': '1' } : {}),
231
231
  },
232
232
  body: JSON.stringify(requestBody),
233
- });
233
+ // React Native: enable incremental body reads for `*-stream` SSE routes.
234
+ ...(useSSE ? { reactNative: { textStreaming: true } } : {}),
235
+ };
236
+
237
+ const response = await helpers.fetchFn(
238
+ `${getAiAgentApiHost()}/api/${model}`,
239
+ fetchInit as RequestInit,
240
+ );
234
241
 
235
242
  if (!response.ok) {
236
243
  throw new Error('Failed to send message');
@@ -243,13 +250,14 @@ const createAgentApi = (helpers: ReturnType<typeof createApiHelpers>) => ({
243
250
  onPiiProgress: params.onPiiProgress,
244
251
  onStreamDelta: params.onStreamDelta,
245
252
  };
246
- // Always buffer first: RN often exposes a `body` stream that closes
247
- // immediately without data, which breaks incremental parsing.
253
+ const bodyStream = response.body;
254
+ if (bodyStream?.getReader) {
255
+ return await parseSSEResponse(bodyStream, sseHandlers);
256
+ }
248
257
  const rawText = await response.text();
249
258
  if (rawText.trimStart().startsWith('event:') || rawText.includes('\nevent:')) {
250
259
  return parseSSEText(rawText, sseHandlers);
251
260
  }
252
- // Gateway returned JSON despite SSE content-type.
253
261
  return rawText ? JSON.parse(rawText) : {};
254
262
  }
255
263
 
@@ -1,18 +1,20 @@
1
1
  export type SSEHandlers = {
2
2
  onPiiProgress?: (step: 'scan_start' | 'scan_complete' | 'send_start' | 'done', data?: unknown) => void;
3
+ /** Token chunk from a `text_delta` event. */
3
4
  onStreamDelta?: (text: string) => void;
5
+ /** Accumulated assistant text after each `text_delta` (convenience for chat UIs). */
6
+ onStreamText?: (fullText: string) => void;
4
7
  };
5
8
 
6
- /** Normalize CRLF and split into SSE message blocks. */
7
- function splitSSEMessages(raw: string): string[] {
8
- const normalized = raw.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
9
- return normalized.split('\n\n').filter((block) => block.trim().length > 0);
10
- }
9
+ type SSEParseState = {
10
+ streamText: string;
11
+ };
11
12
 
12
13
  function dispatchSSEMessage(
13
14
  msg: string,
14
15
  handlers: SSEHandlers,
15
16
  finalDataRef: { value: unknown | null },
17
+ streamState: SSEParseState,
16
18
  ): void {
17
19
  if (!msg.trim()) return;
18
20
  let eventType = '';
@@ -50,7 +52,11 @@ function dispatchSSEMessage(
50
52
  /* ignore */
51
53
  }
52
54
  const chunk = typeof parsed.text === 'string' ? parsed.text : '';
53
- handlers.onStreamDelta?.(chunk);
55
+ if (chunk) {
56
+ handlers.onStreamDelta?.(chunk);
57
+ streamState.streamText += chunk;
58
+ handlers.onStreamText?.(streamState.streamText);
59
+ }
54
60
  } else if (eventType === 'error') {
55
61
  let parsed: { error?: string } = {};
56
62
  try {
@@ -68,27 +74,68 @@ function dispatchSSEMessage(
68
74
  }
69
75
  }
70
76
 
71
- function parseSSEPayload(raw: string, handlers: SSEHandlers): unknown {
72
- const finalDataRef = { value: null as unknown | null };
73
- for (const msg of splitSSEMessages(raw)) {
74
- dispatchSSEMessage(msg, handlers, finalDataRef);
77
+ /**
78
+ * Parse complete SSE blocks from `buffer`, dispatch events, return trailing incomplete bytes.
79
+ */
80
+ export function consumeSSEBuffer(
81
+ buffer: string,
82
+ handlers: SSEHandlers,
83
+ finalDataRef: { value: unknown | null },
84
+ streamState: SSEParseState = { streamText: '' },
85
+ ): string {
86
+ const normalized = buffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
87
+ const parts = normalized.split('\n\n');
88
+ const remainder = parts.pop() ?? '';
89
+
90
+ for (const block of parts) {
91
+ dispatchSSEMessage(block, handlers, finalDataRef, streamState);
75
92
  }
93
+
94
+ return remainder;
95
+ }
96
+
97
+ function flushSSEBuffer(
98
+ remainder: string,
99
+ handlers: SSEHandlers,
100
+ finalDataRef: { value: unknown | null },
101
+ streamState: SSEParseState,
102
+ ): void {
103
+ if (!remainder.trim()) return;
104
+ dispatchSSEMessage(remainder, handlers, finalDataRef, streamState);
105
+ }
106
+
107
+ function finalizeSSEParse(
108
+ finalDataRef: { value: unknown | null },
109
+ ): unknown {
76
110
  if (finalDataRef.value !== null && finalDataRef.value !== undefined) {
77
111
  return finalDataRef.value;
78
112
  }
79
113
  throw new Error('SSE stream ended without done event');
80
114
  }
81
115
 
82
- /** Parse a complete SSE payload (React Native fetch often lacks a usable `response.body`). */
116
+ function parseSSEPayload(raw: string, handlers: SSEHandlers): unknown {
117
+ const finalDataRef = { value: null as unknown | null };
118
+ const streamState: SSEParseState = { streamText: '' };
119
+ const remainder = consumeSSEBuffer(raw, handlers, finalDataRef, streamState);
120
+ flushSSEBuffer(remainder, handlers, finalDataRef, streamState);
121
+ return finalizeSSEParse(finalDataRef);
122
+ }
123
+
124
+ /** Parse a complete SSE payload (e.g. React Native fallback after `response.text()`). */
83
125
  export function parseSSEText(text: string, handlers: SSEHandlers): unknown {
84
126
  return parseSSEPayload(text, handlers);
85
127
  }
86
128
 
87
- /** Parse Agent SSE: PII steps, optional LLM `text_delta`, final `done`. */
129
+ /**
130
+ * Incrementally parse Agent SSE from a ReadableStream (`text_delta`, PII steps, final `done`).
131
+ * Dispatches handlers as chunks arrive (required for RN chat UIs).
132
+ */
88
133
  export async function parseSSEResponse(
89
134
  body: ReadableStream<Uint8Array>,
90
135
  handlers: SSEHandlers,
91
136
  ): Promise<unknown> {
137
+ const finalDataRef = { value: null as unknown | null };
138
+ const streamState: SSEParseState = { streamText: '' };
92
139
  const reader = body.getReader();
93
140
  const decoder = new TextDecoder();
94
141
  let buffer = '';
@@ -97,8 +144,12 @@ export async function parseSSEResponse(
97
144
  const { done, value } = await reader.read();
98
145
  if (done) break;
99
146
  buffer += decoder.decode(value, { stream: true });
147
+ buffer = consumeSSEBuffer(buffer, handlers, finalDataRef, streamState);
100
148
  }
101
149
 
102
150
  buffer += decoder.decode();
103
- return parseSSEPayload(buffer, handlers);
151
+ buffer = consumeSSEBuffer(buffer, handlers, finalDataRef, streamState);
152
+ flushSSEBuffer(buffer, handlers, finalDataRef, streamState);
153
+
154
+ return finalizeSSEParse(finalDataRef);
104
155
  }
package/src/index.ts CHANGED
@@ -5,7 +5,12 @@ export {
5
5
  type CoreAITApi,
6
6
  type ApiClientDeps,
7
7
  } from './api/nxtlinq-api';
8
- export { parseSSEResponse, parseSSEText } from './api/parse-sse';
8
+ export {
9
+ consumeSSEBuffer,
10
+ parseSSEResponse,
11
+ parseSSEText,
12
+ type SSEHandlers,
13
+ } from './api/parse-sse';
9
14
  export {
10
15
  postTextTts,
11
16
  buildTextTtsDataUri,