@kortyx/react 0.4.0 → 0.4.3

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
@@ -41,7 +41,11 @@ function buildAssistantMessage(args) {
41
41
  role: "assistant",
42
42
  content: plainTextContent,
43
43
  debug: args.debug,
44
- ...args.trace ? args.trace : {}
44
+ ...args.trace ? args.trace : {},
45
+ ...args.checkpoint ? {
46
+ checkpointId: args.checkpoint.id,
47
+ checkpointTurnIndex: args.checkpoint.turnIndex
48
+ } : {}
45
49
  };
46
50
  return args.pieces.length > 0 ? { ...base, contentPieces: args.pieces } : base;
47
51
  }
@@ -51,7 +55,9 @@ var DEFAULT_STORAGE_KEYS = {
51
55
  messages: "chat.messages.v1",
52
56
  includeHistory: "chat.includeHistory.v1",
53
57
  sessionId: "chat.sessionId",
54
- workflowId: "chat.workflowId"
58
+ workflowId: "chat.workflowId",
59
+ branches: "chat.branches.v1",
60
+ responseVariants: "chat.responseVariants.v1"
55
61
  };
56
62
  var getLocalStorage = () => {
57
63
  const candidate = globalThis;
@@ -78,13 +84,40 @@ function createBrowserChatStorage(options) {
78
84
  const workflowId = storage.getItem(keys.workflowId) ?? "";
79
85
  const includeHistoryRaw = storage.getItem(keys.includeHistory);
80
86
  const rawMessages = storage.getItem(keys.messages);
87
+ const rawBranches = storage.getItem(keys.branches);
88
+ const rawResponseVariants = storage.getItem(keys.responseVariants);
81
89
  const parsedMessages = rawMessages ? JSON.parse(rawMessages) : [];
82
90
  const messages = Array.isArray(parsedMessages) ? parsedMessages.map((item) => parseMessage(item)).filter((message) => message !== null) : [];
91
+ const parsedBranches = rawBranches ? JSON.parse(rawBranches) : void 0;
92
+ const branches = parsedBranches && typeof parsedBranches === "object" ? Object.fromEntries(
93
+ Object.entries(
94
+ parsedBranches
95
+ ).flatMap(([sessionId2, branch]) => {
96
+ if (typeof branch.sessionId !== "string") return [];
97
+ const rawBranchMessages = Array.isArray(branch.messages) ? branch.messages : [];
98
+ const branchMessages = rawBranchMessages.map((item) => parseMessage(item)).filter((message) => message !== null).slice(-maxMessages);
99
+ return [
100
+ [
101
+ sessionId2,
102
+ {
103
+ sessionId: branch.sessionId,
104
+ ...typeof branch.parentSessionId === "string" ? { parentSessionId: branch.parentSessionId } : {},
105
+ ...typeof branch.forkedFrom === "string" ? { forkedFrom: branch.forkedFrom } : {},
106
+ messages: branchMessages,
107
+ ...Array.isArray(branch.checkpoints) ? { checkpoints: branch.checkpoints } : {}
108
+ }
109
+ ]
110
+ ];
111
+ })
112
+ ) : void 0;
113
+ const parsedResponseVariants = rawResponseVariants ? JSON.parse(rawResponseVariants) : void 0;
83
114
  return {
84
115
  sessionId,
85
116
  workflowId,
86
117
  includeHistory: includeHistoryRaw !== "0",
87
- messages
118
+ messages,
119
+ ...branches ? { branches } : {},
120
+ ...Array.isArray(parsedResponseVariants) ? { responseVariants: parsedResponseVariants } : {}
88
121
  };
89
122
  } catch {
90
123
  return {};
@@ -103,6 +136,28 @@ function createBrowserChatStorage(options) {
103
136
  storage.setItem(keys.includeHistory, state.includeHistory ? "1" : "0");
104
137
  const trimmedMessages = state.messages.slice(-maxMessages).map((message) => serializeMessage(message));
105
138
  storage.setItem(keys.messages, JSON.stringify(trimmedMessages));
139
+ if (state.branches) {
140
+ const serializedBranches = Object.fromEntries(
141
+ Object.entries(state.branches).map(([sessionId, branch]) => [
142
+ sessionId,
143
+ {
144
+ ...branch,
145
+ messages: branch.messages.slice(-maxMessages).map((message) => serializeMessage(message))
146
+ }
147
+ ])
148
+ );
149
+ storage.setItem(keys.branches, JSON.stringify(serializedBranches));
150
+ } else {
151
+ storage.removeItem(keys.branches);
152
+ }
153
+ if (state.responseVariants) {
154
+ storage.setItem(
155
+ keys.responseVariants,
156
+ JSON.stringify(state.responseVariants)
157
+ );
158
+ } else {
159
+ storage.removeItem(keys.responseVariants);
160
+ }
106
161
  } catch {
107
162
  }
108
163
  },
@@ -111,6 +166,8 @@ function createBrowserChatStorage(options) {
111
166
  const storage = getLocalStorage();
112
167
  if (!storage) return;
113
168
  storage.removeItem(keys.messages);
169
+ storage.removeItem(keys.branches);
170
+ storage.removeItem(keys.responseVariants);
114
171
  } catch {
115
172
  }
116
173
  }
@@ -132,7 +189,7 @@ function createChatTransport(args) {
132
189
  };
133
190
  }
134
191
  function createRouteChatTransport(args) {
135
- return createChatTransport({
192
+ const transport = createChatTransport({
136
193
  stream: (context) => {
137
194
  const body = args.createBody?.(context) ?? {
138
195
  sessionId: context.sessionId,
@@ -153,6 +210,41 @@ function createRouteChatTransport(args) {
153
210
  });
154
211
  }
155
212
  });
213
+ if (!args.checkpointEndpoint) return transport;
214
+ const fetchJson = async (body) => {
215
+ const fetchImpl = args.fetchImpl ?? fetch;
216
+ const response = await fetchImpl(args.checkpointEndpoint, {
217
+ method: "POST",
218
+ headers: {
219
+ "content-type": "application/json",
220
+ ...args.headers ?? {}
221
+ },
222
+ body: JSON.stringify(body)
223
+ });
224
+ if (!response.ok) {
225
+ let message = `Checkpoint request failed with status ${response.status}`;
226
+ try {
227
+ const parsed = await response.json();
228
+ if (typeof parsed.error === "string") message = parsed.error;
229
+ } catch {
230
+ }
231
+ throw new Error(message);
232
+ }
233
+ return await response.json();
234
+ };
235
+ return {
236
+ ...transport,
237
+ listCheckpoints: (sessionId) => fetchJson({ action: "list", sessionId }),
238
+ rollbackTo: (checkpointId) => fetchJson({
239
+ action: "rollback",
240
+ checkpointId
241
+ }),
242
+ fork: (checkpointId, options) => fetchJson({
243
+ action: "fork",
244
+ checkpointId,
245
+ ...options?.newSessionId ? { newSessionId: options.newSessionId } : {}
246
+ })
247
+ };
156
248
  }
157
249
 
158
250
  // src/create-live-chat-pieces.ts
@@ -437,6 +529,17 @@ function useStructuredStreams(options) {
437
529
  const get = (0, import_react2.useCallback)((streamId) => {
438
530
  return itemsRef.current.find((item) => item.streamId === streamId);
439
531
  }, []);
532
+ const deleteStream = (0, import_react2.useCallback)(
533
+ (streamId) => {
534
+ const deleted = accumulatorRef.current.delete(streamId);
535
+ if (!deleted) return false;
536
+ setNextItems(
537
+ itemsRef.current.filter((item) => item.streamId !== streamId)
538
+ );
539
+ return true;
540
+ },
541
+ [setNextItems]
542
+ );
440
543
  const byStreamId = (0, import_react2.useMemo)(
441
544
  () => Object.fromEntries(
442
545
  items.map((item) => [item.streamId, item.state])
@@ -448,7 +551,8 @@ function useStructuredStreams(options) {
448
551
  byStreamId,
449
552
  applyStreamChunk,
450
553
  clear,
451
- get
554
+ get,
555
+ delete: deleteStream
452
556
  };
453
557
  }
454
558
 
@@ -469,18 +573,73 @@ function useLatestRef(value) {
469
573
  return ref;
470
574
  }
471
575
  function findActiveTextInterrupt(args) {
472
- const liveInterrupt = args.streamContentPieces.find(
576
+ const liveInterrupt = [...args.streamContentPieces].reverse().find(
473
577
  (piece) => piece.type === "interrupt" && piece.kind === "text"
474
578
  );
475
579
  if (liveInterrupt) return liveInterrupt;
476
- for (const message of [...args.messages].reverse()) {
477
- if (message.role !== "assistant" || !message.contentPieces) continue;
478
- const messageInterrupt = [...message.contentPieces].reverse().find(
479
- (piece) => piece.type === "interrupt" && piece.kind === "text"
480
- );
481
- if (messageInterrupt) return messageInterrupt;
580
+ const latestMessage = args.messages.at(-1);
581
+ if (latestMessage?.role !== "assistant" || !latestMessage.contentPieces) {
582
+ return void 0;
482
583
  }
483
- return void 0;
584
+ return [...latestMessage.contentPieces].reverse().find(
585
+ (piece) => piece.type === "interrupt" && piece.kind === "text"
586
+ );
587
+ }
588
+ function trimMessagesToCheckpoint(args) {
589
+ const hasCheckpointMetadata = args.messages.some(
590
+ (message) => typeof message.checkpointTurnIndex === "number"
591
+ );
592
+ if (!hasCheckpointMetadata && args.turnIndex > 0) return args.messages;
593
+ const keepThroughIndex = args.messages.reduce((lastIndex, message, index) => {
594
+ return typeof message.checkpointTurnIndex === "number" && message.checkpointTurnIndex <= args.turnIndex ? index : lastIndex;
595
+ }, -1);
596
+ return keepThroughIndex >= 0 ? args.messages.slice(0, keepThroughIndex + 1) : [];
597
+ }
598
+ function applyForkedPendingRequests(args) {
599
+ if (args.pendingRequests.length === 0) return args.messages;
600
+ const interruptPositions = args.messages.flatMap(
601
+ (message, messageIndex) => (message.contentPieces ?? []).flatMap(
602
+ (piece, pieceIndex) => piece.type === "interrupt" ? [{ messageIndex, pieceIndex }] : []
603
+ )
604
+ );
605
+ const positionsToPatch = interruptPositions.slice(
606
+ -args.pendingRequests.length
607
+ );
608
+ if (positionsToPatch.length === 0) return args.messages;
609
+ return args.messages.map((message, messageIndex) => {
610
+ if (!message.contentPieces) return message;
611
+ let changed = false;
612
+ const contentPieces = message.contentPieces.map((piece, pieceIndex) => {
613
+ if (piece.type !== "interrupt") return piece;
614
+ const patchIndex = positionsToPatch.findIndex(
615
+ (position) => position.messageIndex === messageIndex && position.pieceIndex === pieceIndex
616
+ );
617
+ const pending = args.pendingRequests[patchIndex];
618
+ if (!pending) return piece;
619
+ changed = true;
620
+ return {
621
+ ...piece,
622
+ resumeToken: pending.token,
623
+ requestId: pending.requestId,
624
+ ...pending.schema?.kind ? { kind: pending.schema.kind } : {},
625
+ ...typeof pending.schema?.multiple === "boolean" ? { multiple: pending.schema.multiple } : {},
626
+ ...typeof pending.schema?.question === "string" ? { question: pending.schema.question } : {},
627
+ ...typeof pending.schema?.schemaId === "string" ? { schemaId: pending.schema.schemaId } : {},
628
+ ...typeof pending.schema?.schemaVersion === "string" ? { schemaVersion: pending.schema.schemaVersion } : {},
629
+ ...typeof pending.schema?.id === "string" ? { interruptId: pending.schema.id } : {},
630
+ ...pending.schema?.meta ? { meta: pending.schema.meta } : {},
631
+ ...pending.options ? { options: pending.options } : {}
632
+ };
633
+ });
634
+ return changed ? { ...message, contentPieces } : message;
635
+ });
636
+ }
637
+ function latestAssistantMessageId(messages) {
638
+ for (let index = messages.length - 1; index >= 0; index--) {
639
+ const message = messages[index];
640
+ if (message?.role === "assistant") return message.id;
641
+ }
642
+ return null;
484
643
  }
485
644
  function useChat(options) {
486
645
  const resolvedStorage = options.storage ?? defaultStorage;
@@ -496,13 +655,20 @@ function useChat(options) {
496
655
  const [error, setError] = (0, import_react3.useState)(null);
497
656
  const [streamContentPieces, setStreamContentPieces] = (0, import_react3.useState)([]);
498
657
  const [lastAssistantId, setLastAssistantId] = (0, import_react3.useState)(null);
658
+ const [checkpoints, setCheckpoints] = (0, import_react3.useState)([]);
499
659
  const [includeHistory, setIncludeHistory] = (0, import_react3.useState)(true);
500
660
  const [workflowId, setWorkflowId] = (0, import_react3.useState)("");
501
661
  const [sessionId, setSessionId] = (0, import_react3.useState)(null);
662
+ const [branches, setBranches] = (0, import_react3.useState)({});
663
+ const [responseVariants, setResponseVariants] = (0, import_react3.useState)([]);
502
664
  const [didHydrateStorage, setDidHydrateStorage] = (0, import_react3.useState)(false);
503
665
  const abortControllerRef = (0, import_react3.useRef)(null);
504
666
  const { streamDebug, clearStreamDebug, createRecorder } = useChatStreamDebug();
505
- const { applyStreamChunk, clear: clearStructuredStreams } = useStructuredStreams({
667
+ const {
668
+ applyStreamChunk,
669
+ clear: clearStructuredStreams,
670
+ delete: deleteStructuredStream
671
+ } = useStructuredStreams({
506
672
  createId
507
673
  });
508
674
  (0, import_react3.useEffect)(() => {
@@ -522,12 +688,28 @@ function useChat(options) {
522
688
  if (Array.isArray(storedState.messages) && storedState.messages.length > 0) {
523
689
  setMessages(storedState.messages);
524
690
  }
691
+ if (storedState.branches && typeof storedState.branches === "object") {
692
+ setBranches(
693
+ storedState.branches
694
+ );
695
+ }
696
+ if (Array.isArray(storedState.responseVariants)) {
697
+ setResponseVariants(
698
+ storedState.responseVariants
699
+ );
700
+ }
525
701
  setDidHydrateStorage(true);
526
702
  });
527
703
  return () => {
528
704
  cancelled = true;
529
705
  };
530
706
  }, [resolvedStorage]);
707
+ (0, import_react3.useEffect)(() => {
708
+ const nextLastAssistantId = latestAssistantMessageId(messages);
709
+ if (lastAssistantId !== nextLastAssistantId) {
710
+ setLastAssistantId(nextLastAssistantId);
711
+ }
712
+ }, [lastAssistantId, messages]);
531
713
  (0, import_react3.useEffect)(() => {
532
714
  if (!didHydrateStorage) return;
533
715
  const storage = storageRef.current;
@@ -535,21 +717,64 @@ function useChat(options) {
535
717
  sessionId,
536
718
  workflowId,
537
719
  includeHistory,
538
- messages
720
+ messages,
721
+ branches,
722
+ responseVariants
539
723
  });
540
724
  }, [
725
+ branches,
541
726
  didHydrateStorage,
542
727
  includeHistory,
543
728
  messages,
729
+ responseVariants,
544
730
  sessionId,
545
731
  storageRef,
546
732
  workflowId
547
733
  ]);
734
+ (0, import_react3.useEffect)(() => {
735
+ if (!didHydrateStorage || !sessionId) return;
736
+ setBranches((current) => ({
737
+ ...current,
738
+ [sessionId]: {
739
+ ...current[sessionId] ?? {},
740
+ sessionId,
741
+ messages,
742
+ checkpoints
743
+ }
744
+ }));
745
+ }, [checkpoints, didHydrateStorage, messages, sessionId]);
746
+ (0, import_react3.useEffect)(() => {
747
+ if (!didHydrateStorage || !sessionId) return;
748
+ setResponseVariants(
749
+ (current) => current.map((group) => {
750
+ const selectedVariant = group.variants.find(
751
+ (variant) => variant.id === group.selectedVariantId && variant.sessionId === sessionId
752
+ );
753
+ if (!selectedVariant) return group;
754
+ if (selectedVariant.messages === messages && selectedVariant.checkpoints === checkpoints) {
755
+ return group;
756
+ }
757
+ return {
758
+ ...group,
759
+ variants: group.variants.map(
760
+ (variant) => variant.id === selectedVariant.id ? {
761
+ ...variant,
762
+ messages,
763
+ checkpoints
764
+ } : variant
765
+ )
766
+ };
767
+ })
768
+ );
769
+ }, [checkpoints, didHydrateStorage, messages, sessionId]);
548
770
  const clearActiveStream = () => {
549
771
  abortControllerRef.current?.abort();
550
772
  abortControllerRef.current = null;
551
773
  setIsStreaming(false);
552
774
  setMessages([]);
775
+ setCheckpoints([]);
776
+ setBranches({});
777
+ setResponseVariants([]);
553
778
  setStreamContentPieces([]);
554
779
  clearStreamDebug();
555
780
  clearStructuredStreams();
@@ -561,6 +786,7 @@ function useChat(options) {
561
786
  };
562
787
  const resetSession = () => {
563
788
  setSessionId(null);
789
+ setCheckpoints([]);
564
790
  };
565
791
  const resetChat = () => {
566
792
  clearActiveStream();
@@ -578,7 +804,12 @@ function useChat(options) {
578
804
  const persistSessionId = (sid) => {
579
805
  setSessionId(sid);
580
806
  };
581
- const resolveSessionId = () => {
807
+ const resolveSessionId = (preferredSessionId) => {
808
+ if (preferredSessionId) {
809
+ if (sessionId !== preferredSessionId)
810
+ persistSessionId(preferredSessionId);
811
+ return preferredSessionId;
812
+ }
582
813
  const sid = sessionId ?? createId();
583
814
  if (!sessionId) persistSessionId(sid);
584
815
  return sid;
@@ -588,13 +819,14 @@ function useChat(options) {
588
819
  content: message.content
589
820
  });
590
821
  const prepareMessagesToSend = async (args) => {
822
+ const baseMessages = args.baseMessages ?? messages;
591
823
  const prepared = options.prepareContextMessages ? await options.prepareContextMessages({
592
- messages,
824
+ messages: baseMessages,
593
825
  sessionId: args.sid,
594
826
  workflowId,
595
827
  reason: args.reason,
596
828
  context: requestContext
597
- }) : includeHistory ? messages.map(toOutgoingHistoryMessage) : [];
829
+ }) : includeHistory ? baseMessages.map(toOutgoingHistoryMessage) : [];
598
830
  return [...prepared, args.outgoingMessage];
599
831
  };
600
832
  const streamAssistantResponse = async (args) => {
@@ -603,6 +835,7 @@ function useChat(options) {
603
835
  const debug = createRecorder(`${args.debugLabel} sessionId=${args.sid}`);
604
836
  let currentTrace;
605
837
  let completedTrace;
838
+ let currentCheckpoint;
606
839
  const pieces = createLiveChatPieces({
607
840
  createId,
608
841
  onChange: setStreamContentPieces,
@@ -638,6 +871,31 @@ function useChat(options) {
638
871
  }
639
872
  return;
640
873
  }
874
+ if (chunk.type === "checkpoint") {
875
+ currentCheckpoint = {
876
+ id: chunk.id,
877
+ turnIndex: chunk.turnIndex
878
+ };
879
+ setCheckpoints((prev) => {
880
+ const next = {
881
+ id: chunk.id,
882
+ sessionId: chunk.sessionId,
883
+ turnIndex: chunk.turnIndex,
884
+ createdAt: Date.now(),
885
+ nodes: [],
886
+ workflow: workflowId,
887
+ ...chunk.label ? { label: chunk.label } : {}
888
+ };
889
+ return [...prev.filter((item) => item.id !== chunk.id), next].sort(
890
+ (a, b) => a.turnIndex - b.turnIndex
891
+ );
892
+ });
893
+ return;
894
+ }
895
+ if (chunk.type === "structured-data-invalidated") {
896
+ deleteStructuredStream(chunk.streamId);
897
+ return;
898
+ }
641
899
  if (chunk.type === "error") {
642
900
  setError(new Error(chunk.message));
643
901
  }
@@ -650,36 +908,54 @@ function useChat(options) {
650
908
  });
651
909
  }
652
910
  });
653
- if (args.signal.aborted) return;
911
+ if (args.signal.aborted) return void 0;
654
912
  const assistant = buildAssistantMessage({
655
913
  createId,
656
914
  pieces: pieces.getPieces(),
657
915
  debug: debug.getAll(),
658
- trace: completedTrace ?? currentTrace
916
+ trace: completedTrace ?? currentTrace,
917
+ checkpoint: currentCheckpoint
659
918
  });
660
- setMessages((prev) => [...prev, assistant]);
919
+ setMessages(
920
+ (prev) => args.messagesBeforeAssistant ? [...args.messagesBeforeAssistant, assistant] : [...prev, assistant]
921
+ );
661
922
  setLastAssistantId(assistant.id);
662
923
  clearStructuredStreams();
663
924
  setStreamContentPieces([]);
664
925
  clearStreamDebug();
926
+ return assistant;
665
927
  };
666
928
  const isAbortError = (value, signal) => {
667
929
  if (signal.aborted) return true;
668
930
  if (!(value instanceof Error)) return false;
669
931
  return value.name === "AbortError";
670
932
  };
671
- const respondToHumanInput = async ({
933
+ const respondToHumanInputFromBase = async ({
672
934
  resumeToken,
673
935
  requestId,
674
936
  selected,
675
- text
937
+ text,
938
+ baseMessages,
939
+ sid
676
940
  }) => {
677
- if (isStreaming) return;
941
+ if (isStreaming) return void 0;
678
942
  setError(null);
679
943
  const label = typeof text === "string" && text.length > 0 ? text : selected.length > 0 ? selected.length === 1 ? selected[0] : selected.join(", ") : "(selection)";
680
944
  const userId = createId();
681
- const userMsg = { id: userId, role: "user", content: label };
682
- setMessages((prev) => [...prev, userMsg]);
945
+ const userMsg = {
946
+ id: userId,
947
+ role: "user",
948
+ content: label,
949
+ source: {
950
+ type: "interrupt-response",
951
+ resumeToken,
952
+ requestId,
953
+ selected,
954
+ ...text !== void 0 ? { text } : {}
955
+ }
956
+ };
957
+ const nextMessages = [...baseMessages ?? messages, userMsg];
958
+ setMessages(nextMessages);
683
959
  setIsStreaming(true);
684
960
  setStreamContentPieces([]);
685
961
  clearStreamDebug();
@@ -695,18 +971,25 @@ function useChat(options) {
695
971
  content: label,
696
972
  metadata: resumePayload
697
973
  };
698
- const sid = resolveSessionId();
974
+ const resolvedSessionId = resolveSessionId(sid);
699
975
  const messagesToSend = await prepareMessagesToSend({
700
- sid,
976
+ sid: resolvedSessionId,
701
977
  outgoingMessage: outgoing,
702
- reason: "resume"
978
+ reason: "resume",
979
+ baseMessages
703
980
  });
704
- await streamAssistantResponse({
705
- sid,
981
+ const assistantMessage = await streamAssistantResponse({
982
+ sid: resolvedSessionId,
706
983
  messagesToSend,
984
+ messagesBeforeAssistant: nextMessages,
707
985
  debugLabel: "runChat (resume)",
708
986
  signal: abortController.signal
709
987
  });
988
+ return {
989
+ userMessage: userMsg,
990
+ ...assistantMessage ? { assistantMessage } : {},
991
+ messages: assistantMessage ? [...nextMessages, assistantMessage] : nextMessages
992
+ };
710
993
  } catch (err) {
711
994
  if (isAbortError(err, abortController.signal)) return;
712
995
  const nextError = err instanceof Error ? err : new Error(String(err));
@@ -719,36 +1002,45 @@ function useChat(options) {
719
1002
  setIsStreaming(false);
720
1003
  }
721
1004
  };
1005
+ const respondToHumanInput = async (args) => {
1006
+ await respondToHumanInputFromBase(args);
1007
+ };
722
1008
  const respondToInterrupt = async (piece, response) => {
723
1009
  const text = response?.text;
724
1010
  const selected = response?.selected ?? (typeof text === "string" && text.length > 0 ? [text] : []);
725
- return respondToHumanInput({
1011
+ await respondToHumanInput({
726
1012
  resumeToken: piece.resumeToken,
727
1013
  requestId: piece.requestId,
728
1014
  selected,
729
1015
  ...text !== void 0 ? { text } : {}
730
1016
  });
731
1017
  };
732
- const send = async (text) => {
1018
+ const sendFromBase = async (text, baseMessages, sid) => {
733
1019
  const content = text.trim();
734
- if (!content || isStreaming) return;
1020
+ if (!content || isStreaming) return void 0;
735
1021
  setError(null);
736
1022
  const activeTextInterrupt = findActiveTextInterrupt({
737
- messages,
1023
+ messages: baseMessages ?? messages,
738
1024
  streamContentPieces
739
1025
  });
740
1026
  if (activeTextInterrupt) {
741
- await respondToHumanInput({
1027
+ return respondToHumanInputFromBase({
742
1028
  resumeToken: activeTextInterrupt.resumeToken,
743
1029
  requestId: activeTextInterrupt.requestId,
744
1030
  selected: [content],
745
- text: content
1031
+ text: content,
1032
+ baseMessages,
1033
+ sid
746
1034
  });
747
- return;
748
1035
  }
749
1036
  const userId = createId();
750
- const userMsg = { id: userId, role: "user", content };
751
- setMessages((prev) => [...prev, userMsg]);
1037
+ const userMsg = {
1038
+ id: userId,
1039
+ role: "user",
1040
+ content
1041
+ };
1042
+ const nextMessages = [...baseMessages ?? messages, userMsg];
1043
+ setMessages(nextMessages);
752
1044
  setIsStreaming(true);
753
1045
  setStreamContentPieces([]);
754
1046
  clearStreamDebug();
@@ -756,19 +1048,26 @@ function useChat(options) {
756
1048
  const abortController = new AbortController();
757
1049
  abortControllerRef.current = abortController;
758
1050
  try {
759
- const sid = resolveSessionId();
1051
+ const resolvedSessionId = resolveSessionId(sid);
760
1052
  const messagesToSend = await prepareMessagesToSend({
761
- sid,
1053
+ sid: resolvedSessionId,
762
1054
  outgoingMessage: { role: "user", content },
763
- reason: "send"
1055
+ reason: "send",
1056
+ baseMessages
764
1057
  });
765
- await streamAssistantResponse({
766
- sid,
1058
+ const assistantMessage = await streamAssistantResponse({
1059
+ sid: resolvedSessionId,
767
1060
  messagesToSend,
1061
+ messagesBeforeAssistant: nextMessages,
768
1062
  debugLabel: "runChat (send)",
769
1063
  openDebugOnInterrupt: true,
770
1064
  signal: abortController.signal
771
1065
  });
1066
+ return {
1067
+ userMessage: userMsg,
1068
+ ...assistantMessage ? { assistantMessage } : {},
1069
+ messages: assistantMessage ? [...nextMessages, assistantMessage] : nextMessages
1070
+ };
772
1071
  } catch (err) {
773
1072
  if (isAbortError(err, abortController.signal)) return;
774
1073
  const nextError = err instanceof Error ? err : new Error(String(err));
@@ -781,6 +1080,375 @@ function useChat(options) {
781
1080
  setIsStreaming(false);
782
1081
  }
783
1082
  };
1083
+ const send = async (text) => {
1084
+ await sendFromBase(text);
1085
+ };
1086
+ const requireCheckpointTransport = () => {
1087
+ const transport = transportRef.current;
1088
+ if (!transport.listCheckpoints || !transport.rollbackTo || !transport.fork) {
1089
+ throw new Error(
1090
+ "Checkpoint helpers require a transport with checkpoint methods."
1091
+ );
1092
+ }
1093
+ return transport;
1094
+ };
1095
+ const refreshCheckpoints = async (sid) => {
1096
+ const transport = requireCheckpointTransport();
1097
+ const next = await transport.listCheckpoints(sid);
1098
+ setCheckpoints(next);
1099
+ return next;
1100
+ };
1101
+ const applyStructuredInvalidations = (streamIds) => {
1102
+ for (const streamId of streamIds) {
1103
+ deleteStructuredStream(streamId);
1104
+ }
1105
+ };
1106
+ const rollbackTo = async (id) => {
1107
+ if (isStreaming) {
1108
+ throw new Error("Cannot rollback while a stream is active.");
1109
+ }
1110
+ const transport = requireCheckpointTransport();
1111
+ const result = await transport.rollbackTo(id);
1112
+ applyStructuredInvalidations(result.invalidatedStructuredStreamIds);
1113
+ const refreshedCheckpoints = await refreshCheckpoints(result.sessionId);
1114
+ const target = refreshedCheckpoints.find(
1115
+ (checkpoint) => checkpoint.id === result.head
1116
+ );
1117
+ if (target) {
1118
+ setMessages((current) => {
1119
+ return trimMessagesToCheckpoint({
1120
+ messages: current,
1121
+ turnIndex: target.turnIndex
1122
+ });
1123
+ });
1124
+ }
1125
+ return result;
1126
+ };
1127
+ const fork = async (id, options2) => {
1128
+ if (isStreaming) {
1129
+ throw new Error("Cannot fork while a stream is active.");
1130
+ }
1131
+ const transport = requireCheckpointTransport();
1132
+ const sid = resolveSessionId();
1133
+ let availableCheckpoints = checkpoints.length > 0 ? checkpoints : await refreshCheckpoints(sid);
1134
+ let target = availableCheckpoints.find(
1135
+ (checkpoint) => checkpoint.id === id
1136
+ );
1137
+ if (!target && checkpoints.length > 0) {
1138
+ availableCheckpoints = await refreshCheckpoints(sid);
1139
+ target = availableCheckpoints.find((checkpoint) => checkpoint.id === id);
1140
+ }
1141
+ const result = await transport.fork(id, options2);
1142
+ setSessionId(result.sessionId);
1143
+ const childCheckpoints = await refreshCheckpoints(result.sessionId);
1144
+ const childHead = childCheckpoints.at(-1);
1145
+ const forkTurnIndex = target?.turnIndex ?? childHead?.turnIndex;
1146
+ if (typeof forkTurnIndex === "number") {
1147
+ setMessages(
1148
+ (current) => applyForkedPendingRequests({
1149
+ messages: trimMessagesToCheckpoint({
1150
+ messages: current,
1151
+ turnIndex: forkTurnIndex
1152
+ }),
1153
+ pendingRequests: result.checkpoint?.activePendingRequests ?? []
1154
+ })
1155
+ );
1156
+ }
1157
+ setStreamContentPieces([]);
1158
+ clearStructuredStreams();
1159
+ return result;
1160
+ };
1161
+ const checkpointForMessage = (messageId) => {
1162
+ return messages.find((message) => message.id === messageId)?.checkpointId ?? null;
1163
+ };
1164
+ const variantForMessage = (messageId) => {
1165
+ return responseVariants.find(
1166
+ (group) => group.sourceAssistantMessageId === messageId || group.variants.some(
1167
+ (variant) => variant.assistantMessageId === messageId
1168
+ )
1169
+ ) ?? null;
1170
+ };
1171
+ const findResumeOverrideForFork = (baseMessages, forkedBaseMessages, userMessage) => {
1172
+ if (userMessage.source?.type !== "interrupt-response") return void 0;
1173
+ const userSource = userMessage.source;
1174
+ const interruptPositions = baseMessages.flatMap(
1175
+ (message, messageIndex) => (message.contentPieces ?? []).flatMap(
1176
+ (piece, pieceIndex) => piece.type === "interrupt" ? [{ messageIndex, pieceIndex, piece }] : []
1177
+ )
1178
+ );
1179
+ const matchedPosition = [...interruptPositions].reverse().find(
1180
+ ({ piece }) => piece.resumeToken === userSource.resumeToken || piece.requestId === userSource.requestId
1181
+ );
1182
+ const forkedPiece = typeof matchedPosition?.messageIndex === "number" ? forkedBaseMessages[matchedPosition.messageIndex]?.contentPieces?.[matchedPosition.pieceIndex] : void 0;
1183
+ if (forkedPiece?.type === "interrupt") {
1184
+ return {
1185
+ resumeToken: forkedPiece.resumeToken,
1186
+ requestId: forkedPiece.requestId
1187
+ };
1188
+ }
1189
+ const latestForkedInterrupt = forkedBaseMessages.flatMap((message) => message.contentPieces ?? []).filter((piece) => piece.type === "interrupt").at(-1);
1190
+ return latestForkedInterrupt ? {
1191
+ resumeToken: latestForkedInterrupt.resumeToken,
1192
+ requestId: latestForkedInterrupt.requestId
1193
+ } : void 0;
1194
+ };
1195
+ const resolveRegenerateTarget = async (assistantMessageId) => {
1196
+ const assistantIndex = messages.findIndex(
1197
+ (message) => message.id === assistantMessageId
1198
+ );
1199
+ if (assistantIndex < 0) {
1200
+ throw new Error(`Assistant message "${assistantMessageId}" not found.`);
1201
+ }
1202
+ const assistant = messages[assistantIndex];
1203
+ if (!assistant) {
1204
+ throw new Error(`Assistant message "${assistantMessageId}" not found.`);
1205
+ }
1206
+ if (assistant.role !== "assistant") {
1207
+ throw new Error("Regenerate expects an assistant message id.");
1208
+ }
1209
+ let userIndex = -1;
1210
+ for (let index = assistantIndex - 1; index >= 0; index--) {
1211
+ if (messages[index]?.role === "user") {
1212
+ userIndex = index;
1213
+ break;
1214
+ }
1215
+ }
1216
+ if (userIndex < 0) {
1217
+ throw new Error("No preceding user message found for regenerate.");
1218
+ }
1219
+ const previousUser = messages[userIndex];
1220
+ const sid = resolveSessionId();
1221
+ let availableCheckpoints = checkpoints.length > 0 ? checkpoints : await refreshCheckpoints(sid);
1222
+ const assistantTurn = assistant.checkpointTurnIndex;
1223
+ const targetTurn = typeof assistantTurn === "number" ? assistantTurn - 1 : void 0;
1224
+ let target = targetTurn !== void 0 ? availableCheckpoints.find(
1225
+ (checkpoint) => checkpoint.turnIndex === targetTurn
1226
+ ) : availableCheckpoints.filter(
1227
+ (checkpoint) => checkpoint.turnIndex < Number.MAX_SAFE_INTEGER
1228
+ ).at(-2);
1229
+ if (!target && checkpoints.length > 0) {
1230
+ availableCheckpoints = await refreshCheckpoints(sid);
1231
+ target = targetTurn !== void 0 ? availableCheckpoints.find(
1232
+ (checkpoint) => checkpoint.turnIndex === targetTurn
1233
+ ) : availableCheckpoints.filter(
1234
+ (checkpoint) => checkpoint.turnIndex < Number.MAX_SAFE_INTEGER
1235
+ ).at(-2);
1236
+ }
1237
+ if (!target) {
1238
+ throw new Error("No rollback checkpoint found for regenerate.");
1239
+ }
1240
+ const baseMessages = trimMessagesToCheckpoint({
1241
+ messages,
1242
+ turnIndex: target.turnIndex
1243
+ });
1244
+ return { target, assistant, previousUser, baseMessages };
1245
+ };
1246
+ const replayUserMessage = async (userMessage, baseMessages, editedContent, options2) => {
1247
+ const content = editedContent ?? userMessage.content;
1248
+ if (userMessage.source?.type === "interrupt-response") {
1249
+ return respondToHumanInputFromBase({
1250
+ resumeToken: options2?.resumeOverride?.resumeToken ?? userMessage.source.resumeToken,
1251
+ requestId: options2?.resumeOverride?.requestId ?? userMessage.source.requestId,
1252
+ selected: editedContent !== void 0 ? [editedContent] : userMessage.source.selected,
1253
+ baseMessages,
1254
+ sid: options2?.sid,
1255
+ ...editedContent !== void 0 ? { text: editedContent } : userMessage.source.text !== void 0 ? { text: userMessage.source.text } : {}
1256
+ });
1257
+ }
1258
+ return sendFromBase(content, baseMessages, options2?.sid);
1259
+ };
1260
+ const buildVariant = (args) => ({
1261
+ id: args.id,
1262
+ sessionId: args.sessionId,
1263
+ assistantMessageId: args.assistantMessage.id,
1264
+ createdAt: Date.now(),
1265
+ messages: args.messages,
1266
+ checkpoints: args.checkpoints,
1267
+ ...args.assistantMessage.checkpointId ? { checkpointId: args.assistantMessage.checkpointId } : {},
1268
+ ...typeof args.assistantMessage.checkpointTurnIndex === "number" ? { checkpointTurnIndex: args.assistantMessage.checkpointTurnIndex } : {},
1269
+ ...args.label ? { label: args.label } : {}
1270
+ });
1271
+ const updateVariantSnapshot = (args) => ({
1272
+ ...args.variant,
1273
+ sessionId: args.sessionId,
1274
+ assistantMessageId: args.assistantMessage.id,
1275
+ messages: args.messages,
1276
+ checkpoints: args.checkpoints,
1277
+ ...args.assistantMessage.checkpointId ? { checkpointId: args.assistantMessage.checkpointId } : {},
1278
+ ...typeof args.assistantMessage.checkpointTurnIndex === "number" ? { checkpointTurnIndex: args.assistantMessage.checkpointTurnIndex } : {}
1279
+ });
1280
+ const replaceResponseVariantGroup = (nextGroup) => {
1281
+ setResponseVariants((current) => [
1282
+ ...current.filter((group) => group.id !== nextGroup.id),
1283
+ nextGroup
1284
+ ]);
1285
+ };
1286
+ const regenerateVariant = async (assistantMessageId) => {
1287
+ if (isStreaming) {
1288
+ throw new Error("Cannot regenerate a variant while a stream is active.");
1289
+ }
1290
+ const { target, assistant, previousUser, baseMessages } = await resolveRegenerateTarget(assistantMessageId);
1291
+ const transport = requireCheckpointTransport();
1292
+ const parentSessionId = resolveSessionId();
1293
+ const existingGroup = variantForMessage(assistantMessageId);
1294
+ const sourceAssistantMessageId = existingGroup?.sourceAssistantMessageId ?? assistantMessageId;
1295
+ const existingVariants = existingGroup ? existingGroup.variants.map(
1296
+ (variant) => variant.id === existingGroup.selectedVariantId || variant.assistantMessageId === assistantMessageId ? updateVariantSnapshot({
1297
+ variant,
1298
+ sessionId: parentSessionId,
1299
+ assistantMessage: assistant,
1300
+ messages,
1301
+ checkpoints
1302
+ }) : variant
1303
+ ) : void 0;
1304
+ const originalVariant = existingVariants?.[0] ?? buildVariant({
1305
+ id: `${sourceAssistantMessageId}:original`,
1306
+ sessionId: parentSessionId,
1307
+ assistantMessage: assistant,
1308
+ messages,
1309
+ checkpoints,
1310
+ label: "Original"
1311
+ });
1312
+ const forkResult = await transport.fork(target.id);
1313
+ const forkedSessionId = forkResult.sessionId;
1314
+ setSessionId(forkedSessionId);
1315
+ const childCheckpointsBeforeReplay = await refreshCheckpoints(forkedSessionId);
1316
+ const forkedBaseMessages = applyForkedPendingRequests({
1317
+ messages: baseMessages,
1318
+ pendingRequests: forkResult.checkpoint?.activePendingRequests ?? []
1319
+ });
1320
+ const resumeOverride = findResumeOverrideForFork(
1321
+ baseMessages,
1322
+ forkedBaseMessages,
1323
+ previousUser
1324
+ );
1325
+ setMessages(forkedBaseMessages);
1326
+ setStreamContentPieces([]);
1327
+ clearStructuredStreams();
1328
+ const replay = await replayUserMessage(
1329
+ previousUser,
1330
+ forkedBaseMessages,
1331
+ void 0,
1332
+ {
1333
+ sid: forkedSessionId,
1334
+ resumeOverride
1335
+ }
1336
+ );
1337
+ const childCheckpointsAfterReplay = await refreshCheckpoints(forkedSessionId);
1338
+ const variantMessages = replay?.messages ?? forkedBaseMessages;
1339
+ const variantAssistant = replay?.assistantMessage;
1340
+ if (!variantAssistant) {
1341
+ throw new Error("Regenerate variant did not produce an assistant reply.");
1342
+ }
1343
+ const nextVariant = buildVariant({
1344
+ id: createId(),
1345
+ sessionId: forkedSessionId,
1346
+ assistantMessage: variantAssistant,
1347
+ messages: variantMessages,
1348
+ checkpoints: childCheckpointsAfterReplay.length > 0 ? childCheckpointsAfterReplay : childCheckpointsBeforeReplay,
1349
+ label: `Variant ${(existingGroup?.variants.length ?? 1) + 1}`
1350
+ });
1351
+ const nextVariants = existingVariants ? [...existingVariants, nextVariant] : [originalVariant, nextVariant];
1352
+ const nextGroup = {
1353
+ id: existingGroup?.id ?? createId(),
1354
+ sourceAssistantMessageId,
1355
+ checkpointId: existingGroup?.checkpointId ?? target.id,
1356
+ checkpointTurnIndex: existingGroup?.checkpointTurnIndex ?? target.turnIndex,
1357
+ selectedVariantId: nextVariant.id,
1358
+ variants: nextVariants
1359
+ };
1360
+ replaceResponseVariantGroup(nextGroup);
1361
+ setBranches((current) => ({
1362
+ ...current,
1363
+ [parentSessionId]: {
1364
+ ...current[parentSessionId] ?? {},
1365
+ sessionId: parentSessionId,
1366
+ messages,
1367
+ checkpoints
1368
+ },
1369
+ [forkedSessionId]: {
1370
+ ...current[forkedSessionId] ?? {},
1371
+ sessionId: forkedSessionId,
1372
+ parentSessionId: forkResult.parentSessionId,
1373
+ forkedFrom: forkResult.forkedFrom,
1374
+ messages: variantMessages,
1375
+ checkpoints: nextVariant.checkpoints
1376
+ }
1377
+ }));
1378
+ return nextGroup;
1379
+ };
1380
+ const selectVariant = async (assistantMessageId, variantId) => {
1381
+ const group = variantForMessage(assistantMessageId);
1382
+ if (!group) {
1383
+ throw new Error(
1384
+ `No response variants found for "${assistantMessageId}".`
1385
+ );
1386
+ }
1387
+ const selectedVariant = group.variants.find(
1388
+ (variant) => variant.id === variantId
1389
+ );
1390
+ if (!selectedVariant) {
1391
+ throw new Error(`Response variant "${variantId}" not found.`);
1392
+ }
1393
+ setSessionId(selectedVariant.sessionId);
1394
+ setMessages(selectedVariant.messages);
1395
+ setCheckpoints(selectedVariant.checkpoints);
1396
+ setStreamContentPieces([]);
1397
+ clearStructuredStreams();
1398
+ setBranches((current) => ({
1399
+ ...current,
1400
+ [selectedVariant.sessionId]: {
1401
+ ...current[selectedVariant.sessionId] ?? {},
1402
+ sessionId: selectedVariant.sessionId,
1403
+ messages: selectedVariant.messages,
1404
+ checkpoints: selectedVariant.checkpoints
1405
+ }
1406
+ }));
1407
+ const nextGroup = {
1408
+ ...group,
1409
+ selectedVariantId: selectedVariant.id
1410
+ };
1411
+ replaceResponseVariantGroup(nextGroup);
1412
+ };
1413
+ const regenerate = async (assistantMessageId) => {
1414
+ const { target, previousUser, baseMessages } = await resolveRegenerateTarget(assistantMessageId);
1415
+ await rollbackTo(target.id);
1416
+ setMessages(baseMessages);
1417
+ await replayUserMessage(previousUser, baseMessages);
1418
+ };
1419
+ const regenerateFromCheckpoint = async (checkpointId) => {
1420
+ const sid = resolveSessionId();
1421
+ let availableCheckpoints = checkpoints.length > 0 ? checkpoints : await refreshCheckpoints(sid);
1422
+ let target = availableCheckpoints.find(
1423
+ (checkpoint) => checkpoint.id === checkpointId
1424
+ );
1425
+ if (!target && checkpoints.length > 0) {
1426
+ availableCheckpoints = await refreshCheckpoints(sid);
1427
+ target = availableCheckpoints.find(
1428
+ (checkpoint) => checkpoint.id === checkpointId
1429
+ );
1430
+ }
1431
+ if (!target) {
1432
+ throw new Error(`Checkpoint "${checkpointId}" not found.`);
1433
+ }
1434
+ const baseMessages = trimMessagesToCheckpoint({
1435
+ messages,
1436
+ turnIndex: target.turnIndex
1437
+ });
1438
+ const previousUser = messages.slice(baseMessages.length).find((message) => message.role === "user");
1439
+ if (!previousUser) {
1440
+ throw new Error("No user message found after checkpoint for regenerate.");
1441
+ }
1442
+ await rollbackTo(target.id);
1443
+ setMessages(baseMessages);
1444
+ await replayUserMessage(previousUser, baseMessages);
1445
+ };
1446
+ const retryWithEdit = async (assistantMessageId, newUserContent) => {
1447
+ const { target, previousUser, baseMessages } = await resolveRegenerateTarget(assistantMessageId);
1448
+ await rollbackTo(target.id);
1449
+ setMessages(baseMessages);
1450
+ await replayUserMessage(previousUser, baseMessages, newUserContent);
1451
+ };
784
1452
  return {
785
1453
  messages,
786
1454
  isStreaming,
@@ -789,12 +1457,25 @@ function useChat(options) {
789
1457
  streamContentPieces,
790
1458
  streamDebug,
791
1459
  lastAssistantId,
1460
+ checkpoints,
1461
+ activeSessionId: sessionId,
1462
+ branches,
1463
+ responseVariants,
792
1464
  workflowId,
793
1465
  setWorkflowId,
794
1466
  send,
795
1467
  respondToHumanInput,
796
1468
  respondToInterrupt,
797
1469
  setMessages,
1470
+ checkpointForMessage,
1471
+ rollbackTo,
1472
+ fork,
1473
+ regenerate,
1474
+ regenerateVariant,
1475
+ selectVariant,
1476
+ variantForMessage,
1477
+ regenerateFromCheckpoint,
1478
+ retryWithEdit,
798
1479
  clearMessages,
799
1480
  resetSession,
800
1481
  resetChat,