@personaai/react 0.3.4 → 0.4.0

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.
package/dist/index.d.cts CHANGED
@@ -415,6 +415,62 @@ declare function useMcpConnections(options?: UseMcpConnectionsOptions): {
415
415
  refetch: () => Promise<PersonaMcpConnection[]>;
416
416
  };
417
417
 
418
- declare const VERSION = "0.3.4";
418
+ /**
419
+ * SSE transport for chat streams, with a React Native fallback.
420
+ *
421
+ * On the web, `fetch` gives a streaming `response.body` and we read it with a
422
+ * ReadableStream reader. React Native's `fetch` is the whatwg-fetch polyfill
423
+ * over XMLHttpRequest: `response.body` is `undefined`, and the promise only
424
+ * settles once the entire response has arrived. That means the check cannot be
425
+ * "call fetch, then see whether body exists" - by that point the whole stream
426
+ * has already been buffered and the chance to stream is gone. The transport has
427
+ * to be chosen *before* the request is made.
428
+ *
429
+ * XMLHttpRequest itself does stream everywhere React Native runs: it appends to
430
+ * `responseText` and fires `readyState === 3` (LOADING) on every chunk, which is
431
+ * all an SSE consumer needs. So the RN path issues the request over raw XHR and
432
+ * hands back the same reader interface the fetch path does, leaving callers
433
+ * unaware of which one they got.
434
+ */
435
+ /** Minimal reader interface, mirroring the shape of a ReadableStream reader. */
436
+ interface SSEReader {
437
+ /** Resolves with the next decoded chunk, or `{ done: true }` at end of stream. */
438
+ read(): Promise<{
439
+ done: boolean;
440
+ value?: string;
441
+ }>;
442
+ /** Aborts the underlying request. */
443
+ cancel(): void;
444
+ }
445
+ interface SSEStream {
446
+ status: number;
447
+ ok: boolean;
448
+ getHeader(name: string): string | null;
449
+ /** Present only when `ok` is false, so callers can surface the server's message. */
450
+ errorText?: string;
451
+ reader: SSEReader;
452
+ }
453
+ interface OpenSSEOptions {
454
+ url: string;
455
+ headers: Record<string, string>;
456
+ body: string;
457
+ signal?: AbortSignal;
458
+ }
459
+ /**
460
+ * Whether `fetch` on this platform yields a streaming body.
461
+ *
462
+ * `navigator.product === 'ReactNative'` is the long-standing marker React
463
+ * Native sets, and is checked rather than feature-detecting `ReadableStream`:
464
+ * newer RN versions do expose a global `ReadableStream` while still leaving
465
+ * `response.body` undefined, so the presence of the type says nothing about
466
+ * whether fetch will populate it.
467
+ */
468
+ declare function supportsStreamingFetch(): boolean;
469
+ /**
470
+ * Opens an SSE stream, using whichever transport this platform can stream over.
471
+ */
472
+ declare function openSSEStream(opts: OpenSSEOptions): Promise<SSEStream>;
473
+
474
+ declare const VERSION = "0.4.0";
419
475
 
420
- export { type PersonaAgentSummary, type PersonaClarificationQuestion, type PersonaFileItem, type PersonaHealthInfo, type PersonaHitlActionRequest, type PersonaInterrupt, type PersonaMcpConnection, type PersonaMemoryAgentGroup, type PersonaMemoryFile, type PersonaMemoryList, type PersonaMessage, type PersonaPresentedFile, PersonaProvider, type PersonaProviderProps, type PersonaResumeValue, type PersonaRole, type PersonaStreamingEvent, type PersonaSubagentActivityEntry, type PersonaThread, type PersonaTodo, type PersonaToolCall, type PersonaWorkspaceFile, type SendMessageOverride, type UseChatOptions, type UseMcpConnectionsOptions, VERSION, useAgents, useChat, useConnection, useFiles, useMcpConnections, useMemory, usePersonaContext, useThreads };
476
+ export { type OpenSSEOptions, type PersonaAgentSummary, type PersonaClarificationQuestion, type PersonaFileItem, type PersonaHealthInfo, type PersonaHitlActionRequest, type PersonaInterrupt, type PersonaMcpConnection, type PersonaMemoryAgentGroup, type PersonaMemoryFile, type PersonaMemoryList, type PersonaMessage, type PersonaPresentedFile, PersonaProvider, type PersonaProviderProps, type PersonaResumeValue, type PersonaRole, type PersonaStreamingEvent, type PersonaSubagentActivityEntry, type PersonaThread, type PersonaTodo, type PersonaToolCall, type PersonaWorkspaceFile, type SSEReader, type SSEStream, type SendMessageOverride, type UseChatOptions, type UseMcpConnectionsOptions, VERSION, openSSEStream, supportsStreamingFetch, useAgents, useChat, useConnection, useFiles, useMcpConnections, useMemory, usePersonaContext, useThreads };
package/dist/index.d.ts CHANGED
@@ -415,6 +415,62 @@ declare function useMcpConnections(options?: UseMcpConnectionsOptions): {
415
415
  refetch: () => Promise<PersonaMcpConnection[]>;
416
416
  };
417
417
 
418
- declare const VERSION = "0.3.4";
418
+ /**
419
+ * SSE transport for chat streams, with a React Native fallback.
420
+ *
421
+ * On the web, `fetch` gives a streaming `response.body` and we read it with a
422
+ * ReadableStream reader. React Native's `fetch` is the whatwg-fetch polyfill
423
+ * over XMLHttpRequest: `response.body` is `undefined`, and the promise only
424
+ * settles once the entire response has arrived. That means the check cannot be
425
+ * "call fetch, then see whether body exists" - by that point the whole stream
426
+ * has already been buffered and the chance to stream is gone. The transport has
427
+ * to be chosen *before* the request is made.
428
+ *
429
+ * XMLHttpRequest itself does stream everywhere React Native runs: it appends to
430
+ * `responseText` and fires `readyState === 3` (LOADING) on every chunk, which is
431
+ * all an SSE consumer needs. So the RN path issues the request over raw XHR and
432
+ * hands back the same reader interface the fetch path does, leaving callers
433
+ * unaware of which one they got.
434
+ */
435
+ /** Minimal reader interface, mirroring the shape of a ReadableStream reader. */
436
+ interface SSEReader {
437
+ /** Resolves with the next decoded chunk, or `{ done: true }` at end of stream. */
438
+ read(): Promise<{
439
+ done: boolean;
440
+ value?: string;
441
+ }>;
442
+ /** Aborts the underlying request. */
443
+ cancel(): void;
444
+ }
445
+ interface SSEStream {
446
+ status: number;
447
+ ok: boolean;
448
+ getHeader(name: string): string | null;
449
+ /** Present only when `ok` is false, so callers can surface the server's message. */
450
+ errorText?: string;
451
+ reader: SSEReader;
452
+ }
453
+ interface OpenSSEOptions {
454
+ url: string;
455
+ headers: Record<string, string>;
456
+ body: string;
457
+ signal?: AbortSignal;
458
+ }
459
+ /**
460
+ * Whether `fetch` on this platform yields a streaming body.
461
+ *
462
+ * `navigator.product === 'ReactNative'` is the long-standing marker React
463
+ * Native sets, and is checked rather than feature-detecting `ReadableStream`:
464
+ * newer RN versions do expose a global `ReadableStream` while still leaving
465
+ * `response.body` undefined, so the presence of the type says nothing about
466
+ * whether fetch will populate it.
467
+ */
468
+ declare function supportsStreamingFetch(): boolean;
469
+ /**
470
+ * Opens an SSE stream, using whichever transport this platform can stream over.
471
+ */
472
+ declare function openSSEStream(opts: OpenSSEOptions): Promise<SSEStream>;
473
+
474
+ declare const VERSION = "0.4.0";
419
475
 
420
- export { type PersonaAgentSummary, type PersonaClarificationQuestion, type PersonaFileItem, type PersonaHealthInfo, type PersonaHitlActionRequest, type PersonaInterrupt, type PersonaMcpConnection, type PersonaMemoryAgentGroup, type PersonaMemoryFile, type PersonaMemoryList, type PersonaMessage, type PersonaPresentedFile, PersonaProvider, type PersonaProviderProps, type PersonaResumeValue, type PersonaRole, type PersonaStreamingEvent, type PersonaSubagentActivityEntry, type PersonaThread, type PersonaTodo, type PersonaToolCall, type PersonaWorkspaceFile, type SendMessageOverride, type UseChatOptions, type UseMcpConnectionsOptions, VERSION, useAgents, useChat, useConnection, useFiles, useMcpConnections, useMemory, usePersonaContext, useThreads };
476
+ export { type OpenSSEOptions, type PersonaAgentSummary, type PersonaClarificationQuestion, type PersonaFileItem, type PersonaHealthInfo, type PersonaHitlActionRequest, type PersonaInterrupt, type PersonaMcpConnection, type PersonaMemoryAgentGroup, type PersonaMemoryFile, type PersonaMemoryList, type PersonaMessage, type PersonaPresentedFile, PersonaProvider, type PersonaProviderProps, type PersonaResumeValue, type PersonaRole, type PersonaStreamingEvent, type PersonaSubagentActivityEntry, type PersonaThread, type PersonaTodo, type PersonaToolCall, type PersonaWorkspaceFile, type SSEReader, type SSEStream, type SendMessageOverride, type UseChatOptions, type UseMcpConnectionsOptions, VERSION, openSSEStream, supportsStreamingFetch, useAgents, useChat, useConnection, useFiles, useMcpConnections, useMemory, usePersonaContext, useThreads };
package/dist/index.js CHANGED
@@ -42,6 +42,181 @@ function usePersonaContext() {
42
42
 
43
43
  // src/hooks/useChat.ts
44
44
  import { useCallback, useEffect, useRef, useState } from "react";
45
+
46
+ // src/streaming.ts
47
+ function supportsStreamingFetch() {
48
+ if (typeof navigator !== "undefined" && navigator.product === "ReactNative") {
49
+ return false;
50
+ }
51
+ return typeof fetch !== "undefined" && typeof ReadableStream !== "undefined";
52
+ }
53
+ function fetchReader(response, controller) {
54
+ const reader = response.body.getReader();
55
+ const decoder = new TextDecoder("utf-8");
56
+ return {
57
+ async read() {
58
+ const { done, value } = await reader.read();
59
+ if (done) return { done: true };
60
+ return { done: false, value: decoder.decode(value, { stream: true }) };
61
+ },
62
+ cancel() {
63
+ controller.abort();
64
+ void reader.cancel().catch(() => {
65
+ });
66
+ }
67
+ };
68
+ }
69
+ function xhrStream(opts) {
70
+ return new Promise((resolveStream, rejectStream) => {
71
+ const xhr = new XMLHttpRequest();
72
+ xhr.open("POST", opts.url);
73
+ for (const [key, val] of Object.entries(opts.headers)) xhr.setRequestHeader(key, val);
74
+ let consumed = 0;
75
+ let finished = false;
76
+ let failure = null;
77
+ const queue = [];
78
+ let waiting = null;
79
+ let waitingReject = null;
80
+ let headersResolved = false;
81
+ const pump = () => {
82
+ const text = xhr.responseText;
83
+ if (text.length <= consumed) return;
84
+ const chunk = text.slice(consumed);
85
+ consumed = text.length;
86
+ if (waiting) {
87
+ const resolve = waiting;
88
+ waiting = null;
89
+ waitingReject = null;
90
+ resolve({ done: false, value: chunk });
91
+ } else {
92
+ queue.push(chunk);
93
+ }
94
+ };
95
+ const finish = (err) => {
96
+ if (finished) return;
97
+ finished = true;
98
+ failure = err ?? null;
99
+ if (waiting) {
100
+ const resolve = waiting;
101
+ const reject = waitingReject;
102
+ waiting = null;
103
+ waitingReject = null;
104
+ if (err && reject) reject(err);
105
+ else resolve({ done: true });
106
+ }
107
+ };
108
+ const reader = {
109
+ read() {
110
+ if (queue.length > 0) {
111
+ return Promise.resolve({ done: false, value: queue.shift() });
112
+ }
113
+ if (finished) {
114
+ return failure ? Promise.reject(failure) : Promise.resolve({ done: true });
115
+ }
116
+ return new Promise((resolve, reject) => {
117
+ waiting = resolve;
118
+ waitingReject = reject;
119
+ });
120
+ },
121
+ cancel() {
122
+ finish();
123
+ xhr.abort();
124
+ }
125
+ };
126
+ const resolveHeaders = () => {
127
+ if (headersResolved) return;
128
+ headersResolved = true;
129
+ const ok = xhr.status >= 200 && xhr.status < 300;
130
+ resolveStream({
131
+ status: xhr.status,
132
+ ok,
133
+ getHeader: (name) => xhr.getResponseHeader(name),
134
+ reader
135
+ });
136
+ };
137
+ xhr.onreadystatechange = () => {
138
+ if (xhr.readyState === 2) {
139
+ resolveHeaders();
140
+ } else if (xhr.readyState === 3) {
141
+ resolveHeaders();
142
+ pump();
143
+ } else if (xhr.readyState === 4) {
144
+ resolveHeaders();
145
+ pump();
146
+ finish();
147
+ }
148
+ };
149
+ xhr.onerror = () => {
150
+ const err = new Error("Network request failed");
151
+ if (!headersResolved) rejectStream(err);
152
+ finish(err);
153
+ };
154
+ xhr.ontimeout = () => {
155
+ const err = new Error("Network request timed out");
156
+ if (!headersResolved) rejectStream(err);
157
+ finish(err);
158
+ };
159
+ if (opts.signal) {
160
+ if (opts.signal.aborted) {
161
+ xhr.abort();
162
+ finish();
163
+ } else {
164
+ opts.signal.addEventListener("abort", () => {
165
+ finish();
166
+ xhr.abort();
167
+ });
168
+ }
169
+ }
170
+ xhr.send(opts.body);
171
+ });
172
+ }
173
+ async function openSSEStream(opts) {
174
+ if (!supportsStreamingFetch()) {
175
+ return xhrStream(opts);
176
+ }
177
+ const controller = new AbortController();
178
+ if (opts.signal) {
179
+ if (opts.signal.aborted) controller.abort();
180
+ else opts.signal.addEventListener("abort", () => controller.abort());
181
+ }
182
+ const response = await fetch(opts.url, {
183
+ method: "POST",
184
+ headers: opts.headers,
185
+ body: opts.body,
186
+ signal: controller.signal
187
+ });
188
+ if (!response.ok) {
189
+ const errorText = await response.text().catch(() => "Stream failed");
190
+ return {
191
+ status: response.status,
192
+ ok: false,
193
+ errorText,
194
+ getHeader: (name) => response.headers.get(name),
195
+ reader: { read: async () => ({ done: true }), cancel: () => controller.abort() }
196
+ };
197
+ }
198
+ if (!response.body) {
199
+ const text = await response.text();
200
+ let handed = false;
201
+ return {
202
+ status: response.status,
203
+ ok: true,
204
+ getHeader: (name) => response.headers.get(name),
205
+ reader: {
206
+ read: async () => handed ? { done: true } : (handed = true, { done: false, value: text }),
207
+ cancel: () => controller.abort()
208
+ }
209
+ };
210
+ }
211
+ return {
212
+ status: response.status,
213
+ ok: true,
214
+ getHeader: (name) => response.headers.get(name),
215
+ reader: fetchReader(response, controller)
216
+ };
217
+ }
218
+
219
+ // src/hooks/useChat.ts
45
220
  function isErrorToolContent(content) {
46
221
  if (typeof content !== "string" || !content.trim().startsWith("{")) return false;
47
222
  try {
@@ -50,6 +225,20 @@ function isErrorToolContent(content) {
50
225
  return false;
51
226
  }
52
227
  }
228
+ function persistedTraceToActivityEntries(items) {
229
+ const entries = [];
230
+ for (const item of items) {
231
+ if (item.type === "text") {
232
+ if (item.text) entries.push({ kind: "text", delta: item.text });
233
+ } else {
234
+ entries.push({ kind: "tool_start", toolName: item.name, args: item.argsText });
235
+ if (item.status === "completed") {
236
+ entries.push({ kind: "tool_result", toolName: item.name, result: item.resultText });
237
+ }
238
+ }
239
+ }
240
+ return entries;
241
+ }
53
242
  function parsePresentedFile(content) {
54
243
  try {
55
244
  const parsed = JSON.parse(content);
@@ -94,7 +283,7 @@ function normalizePendingInterrupt(pending) {
94
283
  return null;
95
284
  }
96
285
  function useChat(options = {}) {
97
- const { defaultAgentId, fetchWithAuth } = usePersonaContext();
286
+ const { defaultAgentId, fetchWithAuth, baseUrl, getAuthToken } = usePersonaContext();
98
287
  const agentId = options.agentId || defaultAgentId;
99
288
  const threadId = options.threadId;
100
289
  const [messages, setMessages] = useState(options.initialMessages || []);
@@ -134,12 +323,16 @@ function useChat(options = {}) {
134
323
  const body = await res.json();
135
324
  const data = body?.data ?? body;
136
325
  const raw = data?.messages ?? [];
326
+ const subagentTraces = data?.subagentTraces ?? {};
137
327
  const loaded = raw.map((m, i) => ({
138
328
  id: m.id || `history-${id}-${i}`,
139
329
  role: m.role,
140
330
  content: m.content,
141
331
  createdAt: /* @__PURE__ */ new Date(),
142
- toolCalls: m.toolCalls
332
+ toolCalls: m.toolCalls?.map((tc) => {
333
+ const trace = subagentTraces[tc.toolCallId];
334
+ return Array.isArray(trace) && trace.length > 0 ? { ...tc, subagentActivity: persistedTraceToActivityEntries(trace) } : tc;
335
+ })
143
336
  }));
144
337
  setMessages(loaded);
145
338
  setInterrupt(normalizePendingInterrupt(data?.pendingInterrupt));
@@ -203,10 +396,12 @@ function useChat(options = {}) {
203
396
  content: m.content
204
397
  }));
205
398
  const resolvedThreadId = await (overrideOptions?.threadId ?? options.threadId);
206
- const response = await fetchWithAuth("/chat", {
207
- method: "POST",
399
+ const token = getAuthToken ? await getAuthToken() : null;
400
+ const stream = await openSSEStream({
401
+ url: `${baseUrl}/chat`,
208
402
  headers: {
209
- "Content-Type": "application/json"
403
+ "Content-Type": "application/json",
404
+ ...token ? { Authorization: `Bearer ${token}` } : {}
210
405
  },
211
406
  body: JSON.stringify({
212
407
  agentId: targetAgentId,
@@ -216,15 +411,10 @@ function useChat(options = {}) {
216
411
  }),
217
412
  signal: controller.signal
218
413
  });
219
- if (!response.ok) {
220
- const errText = await response.text().catch(() => "Stream failed");
221
- throw new Error(`Chat error (${response.status}): ${errText}`);
222
- }
223
- if (!response.body) {
224
- throw new Error("No response body received for streaming.");
414
+ if (!stream.ok) {
415
+ throw new Error(`Chat error (${stream.status}): ${stream.errorText ?? "Stream failed"}`);
225
416
  }
226
- const reader = response.body.getReader();
227
- const decoder = new TextDecoder("utf-8");
417
+ const reader = stream.reader;
228
418
  let buffer = "";
229
419
  let accumulatedText = "";
230
420
  let accumulatedReasoning = "";
@@ -239,7 +429,7 @@ function useChat(options = {}) {
239
429
  while (true) {
240
430
  const { done, value } = await reader.read();
241
431
  if (done) break;
242
- buffer += decoder.decode(value, { stream: true });
432
+ buffer += value ?? "";
243
433
  const lines = buffer.split("\n");
244
434
  buffer = lines.pop() ?? "";
245
435
  for (const line of lines) {
@@ -352,7 +542,7 @@ function useChat(options = {}) {
352
542
  abortControllerRef.current = null;
353
543
  }
354
544
  },
355
- [agentId, fetchWithAuth, input, isStreaming, messages, options]
545
+ [agentId, baseUrl, getAuthToken, input, isStreaming, messages, options]
356
546
  );
357
547
  const handleInputChange = useCallback((e) => {
358
548
  setInput(e.target.value);
@@ -838,10 +1028,12 @@ function useMcpConnections(options = {}) {
838
1028
  }
839
1029
 
840
1030
  // src/index.ts
841
- var VERSION = "0.3.4";
1031
+ var VERSION = "0.4.0";
842
1032
  export {
843
1033
  PersonaProvider,
844
1034
  VERSION,
1035
+ openSSEStream,
1036
+ supportsStreamingFetch,
845
1037
  useAgents,
846
1038
  useChat,
847
1039
  useConnection,