@deltakit/react 0.1.4 → 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.
package/dist/index.cjs CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ fromAgnoAgents: () => import_core2.fromAgnoAgents,
23
24
  fromOpenAiAgents: () => import_core2.fromOpenAiAgents,
24
25
  parseSSEStream: () => import_core2.parseSSEStream,
25
26
  useAutoScroll: () => useAutoScroll,
@@ -108,8 +109,9 @@ function useAutoScroll(dependencies, options) {
108
109
  }
109
110
 
110
111
  // src/use-stream-chat.ts
111
- var import_core = require("@deltakit/core");
112
112
  var import_react2 = require("react");
113
+
114
+ // src/chat-controller.ts
113
115
  var counter = 0;
114
116
  function generateId() {
115
117
  return `msg_${Date.now()}_${++counter}`;
@@ -117,30 +119,403 @@ function generateId() {
117
119
  function createMessage(role, parts) {
118
120
  return { id: generateId(), role, parts };
119
121
  }
122
+ function createChatTransportContext(options) {
123
+ const helpers = {
124
+ appendPart: options.appendPart,
125
+ appendText: options.appendText,
126
+ setMessages: options.setMessages
127
+ };
128
+ return {
129
+ emit: (event) => {
130
+ options.eventHandler(event, helpers);
131
+ },
132
+ ensureAssistantMessage: () => {
133
+ options.setMessages((prev) => {
134
+ const last = prev[prev.length - 1];
135
+ if (last?.role === "assistant") {
136
+ return prev;
137
+ }
138
+ return [...prev, createMessage("assistant", [])];
139
+ });
140
+ },
141
+ fail: (error) => {
142
+ options.setError(error);
143
+ options.onError?.(error);
144
+ options.setIsLoading(false);
145
+ options.setRunId(null);
146
+ },
147
+ finish: () => {
148
+ options.setIsLoading(false);
149
+ options.setRunId(null);
150
+ const finalMessages = options.getMessages();
151
+ const lastMessage = finalMessages[finalMessages.length - 1];
152
+ if (lastMessage?.role === "assistant") {
153
+ options.onMessage?.(lastMessage);
154
+ }
155
+ options.onFinish?.(finalMessages);
156
+ },
157
+ getMessages: options.getMessages,
158
+ setRunId: (runId) => {
159
+ options.setRunId(runId);
160
+ }
161
+ };
162
+ }
163
+
164
+ // src/transports.ts
165
+ var import_core = require("@deltakit/core");
166
+ function toError(error) {
167
+ return error instanceof Error ? error : new Error(String(error));
168
+ }
169
+ function isAbortError(error) {
170
+ return error instanceof DOMException && error.name === "AbortError";
171
+ }
172
+ function resolveRunId(response) {
173
+ if (!response || typeof response !== "object") {
174
+ throw new Error("Background SSE start response did not contain a run id");
175
+ }
176
+ const maybeRunId = "runId" in response ? response.runId : "job_id" in response ? response.job_id : null;
177
+ if (typeof maybeRunId !== "string" || maybeRunId.length === 0) {
178
+ throw new Error("Background SSE start response did not contain a run id");
179
+ }
180
+ return maybeRunId;
181
+ }
182
+ function resolveUrl(url, runId) {
183
+ return typeof url === "function" ? url(runId) : url.replace(":runId", runId);
184
+ }
185
+ async function streamFetchSSE(response, context, signal) {
186
+ if (!response.ok) {
187
+ throw new Error(
188
+ `SSE request failed: ${response.status} ${response.statusText}`
189
+ );
190
+ }
191
+ if (!response.body) {
192
+ throw new Error("Response body is null \u2014 SSE streaming not supported");
193
+ }
194
+ for await (const event of (0, import_core.parseSSEStream)(response.body, signal)) {
195
+ context.emit(event);
196
+ }
197
+ }
198
+ function createDirectSSETransport(config) {
199
+ return {
200
+ start: ({ context, message }) => {
201
+ const controller = new AbortController();
202
+ const run = {
203
+ close: () => {
204
+ controller.abort();
205
+ },
206
+ stop: () => {
207
+ controller.abort();
208
+ }
209
+ };
210
+ const fetchImpl = config.fetch ?? fetch;
211
+ void (async () => {
212
+ try {
213
+ const response = await fetchImpl(config.api, {
214
+ body: JSON.stringify({ message, ...config.body }),
215
+ headers: {
216
+ "Content-Type": "application/json",
217
+ ...config.headers
218
+ },
219
+ method: config.method ?? "POST",
220
+ signal: controller.signal
221
+ });
222
+ await streamFetchSSE(response, context, controller.signal);
223
+ context.finish();
224
+ } catch (error) {
225
+ if (!isAbortError(error)) {
226
+ context.fail(toError(error));
227
+ }
228
+ }
229
+ })();
230
+ return run;
231
+ }
232
+ };
233
+ }
234
+ function createBackgroundSSETransport(config) {
235
+ const fetchImpl = config.fetch ?? fetch;
236
+ const connect = (runId, context) => {
237
+ const controller = new AbortController();
238
+ void (async () => {
239
+ try {
240
+ context.ensureAssistantMessage();
241
+ const response = await fetchImpl(resolveUrl(config.eventsApi, runId), {
242
+ headers: config.eventHeaders,
243
+ method: "GET",
244
+ signal: controller.signal
245
+ });
246
+ await streamFetchSSE(response, context, controller.signal);
247
+ context.finish();
248
+ } catch (error) {
249
+ if (!isAbortError(error)) {
250
+ context.fail(toError(error));
251
+ }
252
+ }
253
+ })();
254
+ return {
255
+ close: () => {
256
+ controller.abort();
257
+ },
258
+ stop: () => {
259
+ controller.abort();
260
+ if (config.cancelApi) {
261
+ void fetchImpl(resolveUrl(config.cancelApi, runId), {
262
+ method: "POST"
263
+ });
264
+ }
265
+ },
266
+ runId
267
+ };
268
+ };
269
+ return {
270
+ resume: ({ context, runId }) => {
271
+ context.setRunId(runId);
272
+ return connect(runId, context);
273
+ },
274
+ start: ({ context, message }) => {
275
+ const startController = new AbortController();
276
+ let activeRun;
277
+ void (async () => {
278
+ try {
279
+ const response = await fetchImpl(config.startApi, {
280
+ body: JSON.stringify({ message, ...config.startBody }),
281
+ headers: {
282
+ "Content-Type": "application/json",
283
+ ...config.startHeaders
284
+ },
285
+ method: config.startMethod ?? "POST",
286
+ signal: startController.signal
287
+ });
288
+ if (!response.ok) {
289
+ throw new Error(
290
+ `Background SSE start failed: ${response.status} ${response.statusText}`
291
+ );
292
+ }
293
+ const data = await response.json();
294
+ const runId = (config.resolveRunId ?? resolveRunId)(data);
295
+ context.setRunId(runId);
296
+ activeRun = connect(runId, context);
297
+ } catch (error) {
298
+ if (!isAbortError(error)) {
299
+ context.fail(toError(error));
300
+ }
301
+ }
302
+ })();
303
+ return {
304
+ close: () => {
305
+ startController.abort();
306
+ void activeRun?.close?.();
307
+ },
308
+ stop: () => {
309
+ startController.abort();
310
+ activeRun?.stop?.();
311
+ },
312
+ runId: null
313
+ };
314
+ }
315
+ };
316
+ }
317
+ function defaultParseWebSocketMessage(data) {
318
+ if (typeof data !== "string") {
319
+ return null;
320
+ }
321
+ const parsed = JSON.parse(data);
322
+ return parsed;
323
+ }
324
+ function createWebSocketTransport(config) {
325
+ const parseMessage = config.parseMessage ?? defaultParseWebSocketMessage;
326
+ const serializeMessage = config.serializeMessage ?? JSON.stringify;
327
+ let resolvedRunId = null;
328
+ const applyIncomingEvents = (parsed, context) => {
329
+ const events = Array.isArray(parsed) ? parsed : parsed ? [parsed] : [];
330
+ for (const item of events) {
331
+ const nextRunId = config.resolveRunId?.(item) ?? null;
332
+ if (nextRunId) {
333
+ resolvedRunId = nextRunId;
334
+ context.setRunId(nextRunId);
335
+ }
336
+ context.emit(item);
337
+ if (item.type === "done") {
338
+ context.finish();
339
+ } else if (item.type === "error") {
340
+ context.fail(
341
+ new Error(
342
+ "message" in item && typeof item.message === "string" ? item.message : "Stream error"
343
+ )
344
+ );
345
+ }
346
+ }
347
+ };
348
+ return {
349
+ resume: ({ context, runId }) => {
350
+ context.setRunId(runId);
351
+ context.ensureAssistantMessage();
352
+ const socket = new WebSocket(
353
+ typeof config.url === "function" ? config.url(runId) : config.url,
354
+ config.protocols
355
+ );
356
+ let manuallyClosed = false;
357
+ let sentResumePayload = false;
358
+ const sendResumePayload = () => {
359
+ if (sentResumePayload || socket.readyState !== WebSocket.OPEN) {
360
+ return;
361
+ }
362
+ sentResumePayload = true;
363
+ const payload = config.buildResumePayload?.(runId) ?? { [config.runIdKey ?? "runId"]: runId };
364
+ socket.send(serializeMessage(payload));
365
+ };
366
+ socket.onopen = sendResumePayload;
367
+ socket.onmessage = (event) => {
368
+ try {
369
+ applyIncomingEvents(parseMessage(event.data), context);
370
+ } catch (error) {
371
+ context.fail(toError(error));
372
+ }
373
+ };
374
+ socket.onerror = () => {
375
+ context.fail(new Error("WebSocket connection failed"));
376
+ };
377
+ socket.onclose = () => {
378
+ if (!manuallyClosed) {
379
+ context.finish();
380
+ }
381
+ };
382
+ queueMicrotask(sendResumePayload);
383
+ return {
384
+ close: () => {
385
+ manuallyClosed = true;
386
+ socket.close();
387
+ },
388
+ stop: () => {
389
+ manuallyClosed = true;
390
+ const stopRunId = resolvedRunId ?? runId;
391
+ if (stopRunId && config.cancelUrl) {
392
+ void fetch(resolveUrl(config.cancelUrl, stopRunId), {
393
+ method: "POST"
394
+ });
395
+ }
396
+ socket.close();
397
+ },
398
+ runId
399
+ };
400
+ },
401
+ start: ({ context, message }) => {
402
+ const runId = config.runId ?? config.getResumeKey?.() ?? null;
403
+ const socket = new WebSocket(
404
+ typeof config.url === "function" ? config.url(runId) : config.url,
405
+ config.protocols
406
+ );
407
+ let manuallyClosed = false;
408
+ let sentStartPayload = false;
409
+ const sendStartPayload = () => {
410
+ if (sentStartPayload || socket.readyState !== WebSocket.OPEN) {
411
+ return;
412
+ }
413
+ sentStartPayload = true;
414
+ context.ensureAssistantMessage();
415
+ const payload = {
416
+ message,
417
+ ...config.body
418
+ };
419
+ if (runId) {
420
+ payload[config.runIdKey ?? "runId"] = runId;
421
+ }
422
+ socket.send(serializeMessage(payload));
423
+ };
424
+ socket.onopen = sendStartPayload;
425
+ socket.onmessage = (event) => {
426
+ try {
427
+ applyIncomingEvents(parseMessage(event.data), context);
428
+ } catch (error) {
429
+ context.fail(toError(error));
430
+ }
431
+ };
432
+ socket.onerror = () => {
433
+ context.fail(new Error("WebSocket connection failed"));
434
+ };
435
+ socket.onclose = () => {
436
+ if (!manuallyClosed) {
437
+ context.finish();
438
+ }
439
+ };
440
+ queueMicrotask(sendStartPayload);
441
+ return {
442
+ close: () => {
443
+ manuallyClosed = true;
444
+ socket.close();
445
+ },
446
+ stop: () => {
447
+ manuallyClosed = true;
448
+ const stopRunId = resolvedRunId ?? runId;
449
+ if (stopRunId && config.cancelUrl) {
450
+ void fetch(resolveUrl(config.cancelUrl, stopRunId), {
451
+ method: "POST"
452
+ });
453
+ }
454
+ socket.close();
455
+ },
456
+ runId
457
+ };
458
+ }
459
+ };
460
+ }
461
+ function resolveTransport(options) {
462
+ if (typeof options.transport === "object" && options.transport) {
463
+ return options.transport;
464
+ }
465
+ const transportKind = options.transport ?? "sse";
466
+ if (transportKind === "background-sse") {
467
+ const config = options.transportOptions?.backgroundSSE;
468
+ if (!config) {
469
+ throw new Error(
470
+ '`transportOptions.backgroundSSE` is required when transport is "background-sse"'
471
+ );
472
+ }
473
+ return createBackgroundSSETransport(config);
474
+ }
475
+ if (transportKind === "websocket") {
476
+ const config = options.transportOptions?.websocket;
477
+ if (!config) {
478
+ throw new Error(
479
+ '`transportOptions.websocket` is required when transport is "websocket"'
480
+ );
481
+ }
482
+ return createWebSocketTransport(config);
483
+ }
484
+ const sseConfig = options.transportOptions?.sse ?? {
485
+ api: options.api,
486
+ body: options.body,
487
+ headers: options.headers
488
+ };
489
+ if (!sseConfig.api) {
490
+ throw new Error(
491
+ "`api` or `transportOptions.sse.api` is required when using the default SSE transport"
492
+ );
493
+ }
494
+ return createDirectSSETransport({
495
+ ...sseConfig,
496
+ api: sseConfig.api
497
+ });
498
+ }
499
+
500
+ // src/use-stream-chat.ts
120
501
  function defaultOnEvent(event, helpers) {
121
502
  if (event.type === "text_delta") {
122
503
  helpers.appendText(event.delta);
123
504
  }
124
505
  }
125
506
  function useStreamChat(options) {
126
- const {
127
- api,
128
- headers,
129
- body,
130
- initialMessages,
131
- onEvent,
132
- onMessage,
133
- onError,
134
- onFinish
135
- } = options;
136
- const [messages, setMessages] = (0, import_react2.useState)(
137
- initialMessages ?? []
138
- );
507
+ const { initialMessages, onEvent, onMessage, onError, onFinish } = options;
508
+ const [messages, setMessages] = (0, import_react2.useState)(initialMessages ?? []);
139
509
  const [isLoading, setIsLoading] = (0, import_react2.useState)(false);
140
510
  const [error, setError] = (0, import_react2.useState)(null);
141
- const abortRef = (0, import_react2.useRef)(null);
511
+ const [runId, setRunId] = (0, import_react2.useState)(null);
512
+ const runRef = (0, import_react2.useRef)(null);
513
+ const resumedRunIdRef = (0, import_react2.useRef)(null);
514
+ const manuallyStoppedRef = (0, import_react2.useRef)(false);
142
515
  const messagesRef = (0, import_react2.useRef)(messages);
143
516
  messagesRef.current = messages;
517
+ const transportOptionsRef = (0, import_react2.useRef)(options.transportOptions);
518
+ transportOptionsRef.current = options.transportOptions;
144
519
  const appendText = (0, import_react2.useCallback)((delta) => {
145
520
  setMessages((prev) => {
146
521
  const last = prev[prev.length - 1];
@@ -156,29 +531,69 @@ function useStreamChat(options) {
156
531
  } else {
157
532
  parts.push({ type: "text", text: delta });
158
533
  }
159
- const updated = { ...last, parts };
160
- return [...prev.slice(0, -1), updated];
534
+ return [...prev.slice(0, -1), { ...last, parts }];
161
535
  });
162
536
  }, []);
163
537
  const appendPart = (0, import_react2.useCallback)((part) => {
164
538
  setMessages((prev) => {
165
539
  const last = prev[prev.length - 1];
166
540
  if (!last || last.role !== "assistant") return prev;
167
- const updated = {
168
- ...last,
169
- parts: [...last.parts, part]
170
- };
171
- return [...prev.slice(0, -1), updated];
541
+ return [
542
+ ...prev.slice(0, -1),
543
+ {
544
+ ...last,
545
+ parts: [...last.parts, part]
546
+ }
547
+ ];
172
548
  });
173
549
  }, []);
550
+ const transportRef = (0, import_react2.useRef)(null);
551
+ if (!transportRef.current) {
552
+ transportRef.current = resolveTransport(options);
553
+ }
554
+ const transport = transportRef.current;
555
+ const eventHandler = onEvent ?? defaultOnEvent;
556
+ const eventHandlerRef = (0, import_react2.useRef)(eventHandler);
557
+ eventHandlerRef.current = eventHandler;
558
+ const onErrorRef = (0, import_react2.useRef)(onError);
559
+ onErrorRef.current = onError;
560
+ const onFinishRef = (0, import_react2.useRef)(onFinish);
561
+ onFinishRef.current = onFinish;
562
+ const onMessageRef = (0, import_react2.useRef)(onMessage);
563
+ onMessageRef.current = onMessage;
564
+ const transportContext = (0, import_react2.useMemo)(
565
+ () => createChatTransportContext({
566
+ appendPart,
567
+ appendText,
568
+ eventHandler: (event, helpers) => eventHandlerRef.current(event, helpers),
569
+ getMessages: () => messagesRef.current,
570
+ onError: (...args) => onErrorRef.current?.(...args),
571
+ onFinish: (...args) => onFinishRef.current?.(...args),
572
+ onMessage: (...args) => onMessageRef.current?.(...args),
573
+ setError,
574
+ setIsLoading,
575
+ setMessages,
576
+ setRunId: (next) => {
577
+ setRunId(next);
578
+ transportOptionsRef.current?.backgroundSSE?.onRunIdChange?.(next);
579
+ transportOptionsRef.current?.websocket?.onRunIdChange?.(next);
580
+ }
581
+ }),
582
+ [appendPart, appendText]
583
+ );
174
584
  const stop = (0, import_react2.useCallback)(() => {
175
- abortRef.current?.abort();
176
- abortRef.current = null;
585
+ const activeRun = runRef.current;
586
+ if (!activeRun?.stop) {
587
+ return;
588
+ }
589
+ manuallyStoppedRef.current = true;
590
+ void activeRun.stop();
591
+ runRef.current = null;
177
592
  setIsLoading(false);
178
593
  }, []);
179
594
  const sendMessage = (0, import_react2.useCallback)(
180
595
  (text) => {
181
- if (abortRef.current) {
596
+ if (runRef.current || isLoading) {
182
597
  return;
183
598
  }
184
599
  const userMessage = createMessage("user", [
@@ -193,89 +608,69 @@ function useStreamChat(options) {
193
608
  onMessage?.(userMessage);
194
609
  setError(null);
195
610
  setIsLoading(true);
196
- const controller = new AbortController();
197
- abortRef.current = controller;
198
- const eventHandler = onEvent ?? defaultOnEvent;
199
- const helpers = {
200
- appendText,
201
- appendPart,
202
- setMessages
203
- };
204
- (async () => {
205
- try {
206
- const response = await fetch(api, {
207
- method: "POST",
208
- headers: {
209
- "Content-Type": "application/json",
210
- ...headers
211
- },
212
- body: JSON.stringify({ message: text, ...body }),
213
- signal: controller.signal
214
- });
215
- if (!response.ok) {
216
- throw new Error(
217
- `SSE request failed: ${response.status} ${response.statusText}`
218
- );
219
- }
220
- if (!response.body) {
221
- throw new Error(
222
- "Response body is null \u2014 SSE streaming not supported"
223
- );
224
- }
225
- for await (const event of (0, import_core.parseSSEStream)(
226
- response.body,
227
- controller.signal
228
- )) {
229
- eventHandler(event, helpers);
230
- }
231
- const finalMessages = messagesRef.current;
232
- const lastMessage = finalMessages[finalMessages.length - 1];
233
- if (lastMessage?.role === "assistant") {
234
- onMessage?.(lastMessage);
235
- }
236
- onFinish?.(finalMessages);
237
- } catch (err) {
238
- if (err instanceof DOMException && err.name === "AbortError") {
239
- return;
240
- }
241
- const error2 = err instanceof Error ? err : new Error(String(err));
242
- setError(error2);
243
- onError?.(error2);
244
- } finally {
245
- abortRef.current = null;
246
- setIsLoading(false);
247
- }
248
- })();
611
+ resumedRunIdRef.current = null;
612
+ manuallyStoppedRef.current = false;
613
+ const run = transport.start({ context: transportContext, message: text });
614
+ runRef.current = run ?? null;
615
+ if (run?.runId) {
616
+ setRunId(run.runId);
617
+ }
249
618
  },
250
- [
251
- api,
252
- headers,
253
- body,
254
- onEvent,
255
- onMessage,
256
- onError,
257
- onFinish,
258
- appendText,
259
- appendPart
260
- ]
619
+ [isLoading, onMessage, transport, transportContext]
261
620
  );
621
+ const candidateRunId = options.transportOptions?.backgroundSSE?.runId ?? options.transportOptions?.backgroundSSE?.getResumeKey?.() ?? options.transportOptions?.websocket?.runId ?? options.transportOptions?.websocket?.getResumeKey?.() ?? null;
622
+ (0, import_react2.useEffect)(() => {
623
+ if (runRef.current) {
624
+ return;
625
+ }
626
+ if (!candidateRunId) {
627
+ return;
628
+ }
629
+ if (manuallyStoppedRef.current) {
630
+ return;
631
+ }
632
+ if (resumedRunIdRef.current === candidateRunId) {
633
+ return;
634
+ }
635
+ if (!transport.resume) {
636
+ return;
637
+ }
638
+ resumedRunIdRef.current = candidateRunId;
639
+ setError(null);
640
+ setIsLoading(true);
641
+ const run = transport.resume({
642
+ context: transportContext,
643
+ runId: candidateRunId
644
+ });
645
+ runRef.current = run ?? null;
646
+ setRunId(candidateRunId);
647
+ }, [candidateRunId, transport, transportContext]);
262
648
  (0, import_react2.useEffect)(() => {
263
649
  return () => {
264
- abortRef.current?.abort();
265
- abortRef.current = null;
650
+ void runRef.current?.close?.();
651
+ runRef.current = null;
266
652
  };
267
653
  }, []);
654
+ const prevIsLoadingRef = (0, import_react2.useRef)(isLoading);
655
+ (0, import_react2.useEffect)(() => {
656
+ if (prevIsLoadingRef.current && !isLoading) {
657
+ runRef.current = null;
658
+ }
659
+ prevIsLoadingRef.current = isLoading;
660
+ }, [isLoading]);
268
661
  return {
269
- messages,
270
- isLoading,
271
662
  error,
663
+ isLoading,
664
+ messages,
665
+ runId,
272
666
  sendMessage,
273
- stop,
274
- setMessages
667
+ setMessages,
668
+ stop
275
669
  };
276
670
  }
277
671
  // Annotate the CommonJS export names for ESM import in node:
278
672
  0 && (module.exports = {
673
+ fromAgnoAgents,
279
674
  fromOpenAiAgents,
280
675
  parseSSEStream,
281
676
  useAutoScroll,