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