@kortyx/react 0.4.1 → 0.5.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/CHANGELOG.md +37 -0
- package/LICENSE +51 -43
- package/dist/index.d.ts +166 -1
- package/dist/index.js +732 -39
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
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
|
}
|
|
@@ -118,6 +175,7 @@ function createBrowserChatStorage(options) {
|
|
|
118
175
|
}
|
|
119
176
|
|
|
120
177
|
// src/chat-transport.ts
|
|
178
|
+
var import_errors = require("@kortyx/core/errors");
|
|
121
179
|
var import_browser = require("@kortyx/stream/browser");
|
|
122
180
|
function createChatTransport(args) {
|
|
123
181
|
return {
|
|
@@ -132,7 +190,7 @@ function createChatTransport(args) {
|
|
|
132
190
|
};
|
|
133
191
|
}
|
|
134
192
|
function createRouteChatTransport(args) {
|
|
135
|
-
|
|
193
|
+
const transport = createChatTransport({
|
|
136
194
|
stream: (context) => {
|
|
137
195
|
const body = args.createBody?.(context) ?? {
|
|
138
196
|
sessionId: context.sessionId,
|
|
@@ -153,6 +211,47 @@ function createRouteChatTransport(args) {
|
|
|
153
211
|
});
|
|
154
212
|
}
|
|
155
213
|
});
|
|
214
|
+
if (!args.checkpointEndpoint) return transport;
|
|
215
|
+
const fetchJson = async (body) => {
|
|
216
|
+
const fetchImpl = args.fetchImpl ?? fetch;
|
|
217
|
+
const response = await fetchImpl(args.checkpointEndpoint, {
|
|
218
|
+
method: "POST",
|
|
219
|
+
headers: {
|
|
220
|
+
"content-type": "application/json",
|
|
221
|
+
...args.headers ?? {}
|
|
222
|
+
},
|
|
223
|
+
body: JSON.stringify(body)
|
|
224
|
+
});
|
|
225
|
+
if (!response.ok) {
|
|
226
|
+
let failure;
|
|
227
|
+
let message = `Checkpoint request failed with status ${response.status}`;
|
|
228
|
+
try {
|
|
229
|
+
const parsed = await response.json();
|
|
230
|
+
failure = parsed.failure;
|
|
231
|
+
if (typeof parsed.error === "string") message = parsed.error;
|
|
232
|
+
} catch {
|
|
233
|
+
}
|
|
234
|
+
throw (0, import_errors.isFailureDescriptor)(failure) ? (0, import_errors.errorFromFailure)(failure) : new import_errors.KortyxError("CHECKPOINT_REQUEST_FAILED", message, {
|
|
235
|
+
category: "transport",
|
|
236
|
+
status: response.status,
|
|
237
|
+
safeMessage: message
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return await response.json();
|
|
241
|
+
};
|
|
242
|
+
return {
|
|
243
|
+
...transport,
|
|
244
|
+
listCheckpoints: (sessionId) => fetchJson({ action: "list", sessionId }),
|
|
245
|
+
rollbackTo: (checkpointId) => fetchJson({
|
|
246
|
+
action: "rollback",
|
|
247
|
+
checkpointId
|
|
248
|
+
}),
|
|
249
|
+
fork: (checkpointId, options) => fetchJson({
|
|
250
|
+
action: "fork",
|
|
251
|
+
checkpointId,
|
|
252
|
+
...options?.newSessionId ? { newSessionId: options.newSessionId } : {}
|
|
253
|
+
})
|
|
254
|
+
};
|
|
156
255
|
}
|
|
157
256
|
|
|
158
257
|
// src/create-live-chat-pieces.ts
|
|
@@ -262,7 +361,8 @@ function createLiveChatPieces(args) {
|
|
|
262
361
|
pushPiece({
|
|
263
362
|
id: args.createId(),
|
|
264
363
|
type: "error",
|
|
265
|
-
content: chunk.message ?? "An error occurred"
|
|
364
|
+
content: chunk.message ?? "An error occurred",
|
|
365
|
+
...chunk.failure ? { failure: chunk.failure } : {}
|
|
266
366
|
});
|
|
267
367
|
return true;
|
|
268
368
|
}
|
|
@@ -325,6 +425,7 @@ function toHumanInputPiece(args) {
|
|
|
325
425
|
}
|
|
326
426
|
|
|
327
427
|
// src/use-chat.ts
|
|
428
|
+
var import_errors2 = require("@kortyx/core/errors");
|
|
328
429
|
var import_react3 = require("react");
|
|
329
430
|
|
|
330
431
|
// src/use-chat-stream-debug.ts
|
|
@@ -437,6 +538,17 @@ function useStructuredStreams(options) {
|
|
|
437
538
|
const get = (0, import_react2.useCallback)((streamId) => {
|
|
438
539
|
return itemsRef.current.find((item) => item.streamId === streamId);
|
|
439
540
|
}, []);
|
|
541
|
+
const deleteStream = (0, import_react2.useCallback)(
|
|
542
|
+
(streamId) => {
|
|
543
|
+
const deleted = accumulatorRef.current.delete(streamId);
|
|
544
|
+
if (!deleted) return false;
|
|
545
|
+
setNextItems(
|
|
546
|
+
itemsRef.current.filter((item) => item.streamId !== streamId)
|
|
547
|
+
);
|
|
548
|
+
return true;
|
|
549
|
+
},
|
|
550
|
+
[setNextItems]
|
|
551
|
+
);
|
|
440
552
|
const byStreamId = (0, import_react2.useMemo)(
|
|
441
553
|
() => Object.fromEntries(
|
|
442
554
|
items.map((item) => [item.streamId, item.state])
|
|
@@ -448,7 +560,8 @@ function useStructuredStreams(options) {
|
|
|
448
560
|
byStreamId,
|
|
449
561
|
applyStreamChunk,
|
|
450
562
|
clear,
|
|
451
|
-
get
|
|
563
|
+
get,
|
|
564
|
+
delete: deleteStream
|
|
452
565
|
};
|
|
453
566
|
}
|
|
454
567
|
|
|
@@ -481,6 +594,62 @@ function findActiveTextInterrupt(args) {
|
|
|
481
594
|
(piece) => piece.type === "interrupt" && piece.kind === "text"
|
|
482
595
|
);
|
|
483
596
|
}
|
|
597
|
+
function trimMessagesToCheckpoint(args) {
|
|
598
|
+
const hasCheckpointMetadata = args.messages.some(
|
|
599
|
+
(message) => typeof message.checkpointTurnIndex === "number"
|
|
600
|
+
);
|
|
601
|
+
if (!hasCheckpointMetadata && args.turnIndex > 0) return args.messages;
|
|
602
|
+
const keepThroughIndex = args.messages.reduce((lastIndex, message, index) => {
|
|
603
|
+
return typeof message.checkpointTurnIndex === "number" && message.checkpointTurnIndex <= args.turnIndex ? index : lastIndex;
|
|
604
|
+
}, -1);
|
|
605
|
+
return keepThroughIndex >= 0 ? args.messages.slice(0, keepThroughIndex + 1) : [];
|
|
606
|
+
}
|
|
607
|
+
function applyForkedPendingRequests(args) {
|
|
608
|
+
if (args.pendingRequests.length === 0) return args.messages;
|
|
609
|
+
const interruptPositions = args.messages.flatMap(
|
|
610
|
+
(message, messageIndex) => (message.contentPieces ?? []).flatMap(
|
|
611
|
+
(piece, pieceIndex) => piece.type === "interrupt" ? [{ messageIndex, pieceIndex }] : []
|
|
612
|
+
)
|
|
613
|
+
);
|
|
614
|
+
const positionsToPatch = interruptPositions.slice(
|
|
615
|
+
-args.pendingRequests.length
|
|
616
|
+
);
|
|
617
|
+
if (positionsToPatch.length === 0) return args.messages;
|
|
618
|
+
return args.messages.map((message, messageIndex) => {
|
|
619
|
+
if (!message.contentPieces) return message;
|
|
620
|
+
let changed = false;
|
|
621
|
+
const contentPieces = message.contentPieces.map((piece, pieceIndex) => {
|
|
622
|
+
if (piece.type !== "interrupt") return piece;
|
|
623
|
+
const patchIndex = positionsToPatch.findIndex(
|
|
624
|
+
(position) => position.messageIndex === messageIndex && position.pieceIndex === pieceIndex
|
|
625
|
+
);
|
|
626
|
+
const pending = args.pendingRequests[patchIndex];
|
|
627
|
+
if (!pending) return piece;
|
|
628
|
+
changed = true;
|
|
629
|
+
return {
|
|
630
|
+
...piece,
|
|
631
|
+
resumeToken: pending.token,
|
|
632
|
+
requestId: pending.requestId,
|
|
633
|
+
...pending.schema?.kind ? { kind: pending.schema.kind } : {},
|
|
634
|
+
...typeof pending.schema?.multiple === "boolean" ? { multiple: pending.schema.multiple } : {},
|
|
635
|
+
...typeof pending.schema?.question === "string" ? { question: pending.schema.question } : {},
|
|
636
|
+
...typeof pending.schema?.schemaId === "string" ? { schemaId: pending.schema.schemaId } : {},
|
|
637
|
+
...typeof pending.schema?.schemaVersion === "string" ? { schemaVersion: pending.schema.schemaVersion } : {},
|
|
638
|
+
...typeof pending.schema?.id === "string" ? { interruptId: pending.schema.id } : {},
|
|
639
|
+
...pending.schema?.meta ? { meta: pending.schema.meta } : {},
|
|
640
|
+
...pending.options ? { options: pending.options } : {}
|
|
641
|
+
};
|
|
642
|
+
});
|
|
643
|
+
return changed ? { ...message, contentPieces } : message;
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
function latestAssistantMessageId(messages) {
|
|
647
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
648
|
+
const message = messages[index];
|
|
649
|
+
if (message?.role === "assistant") return message.id;
|
|
650
|
+
}
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
484
653
|
function useChat(options) {
|
|
485
654
|
const resolvedStorage = options.storage ?? defaultStorage;
|
|
486
655
|
const transportRef = useLatestRef(options.transport);
|
|
@@ -495,13 +664,20 @@ function useChat(options) {
|
|
|
495
664
|
const [error, setError] = (0, import_react3.useState)(null);
|
|
496
665
|
const [streamContentPieces, setStreamContentPieces] = (0, import_react3.useState)([]);
|
|
497
666
|
const [lastAssistantId, setLastAssistantId] = (0, import_react3.useState)(null);
|
|
667
|
+
const [checkpoints, setCheckpoints] = (0, import_react3.useState)([]);
|
|
498
668
|
const [includeHistory, setIncludeHistory] = (0, import_react3.useState)(true);
|
|
499
669
|
const [workflowId, setWorkflowId] = (0, import_react3.useState)("");
|
|
500
670
|
const [sessionId, setSessionId] = (0, import_react3.useState)(null);
|
|
671
|
+
const [branches, setBranches] = (0, import_react3.useState)({});
|
|
672
|
+
const [responseVariants, setResponseVariants] = (0, import_react3.useState)([]);
|
|
501
673
|
const [didHydrateStorage, setDidHydrateStorage] = (0, import_react3.useState)(false);
|
|
502
674
|
const abortControllerRef = (0, import_react3.useRef)(null);
|
|
503
675
|
const { streamDebug, clearStreamDebug, createRecorder } = useChatStreamDebug();
|
|
504
|
-
const {
|
|
676
|
+
const {
|
|
677
|
+
applyStreamChunk,
|
|
678
|
+
clear: clearStructuredStreams,
|
|
679
|
+
delete: deleteStructuredStream
|
|
680
|
+
} = useStructuredStreams({
|
|
505
681
|
createId
|
|
506
682
|
});
|
|
507
683
|
(0, import_react3.useEffect)(() => {
|
|
@@ -521,12 +697,28 @@ function useChat(options) {
|
|
|
521
697
|
if (Array.isArray(storedState.messages) && storedState.messages.length > 0) {
|
|
522
698
|
setMessages(storedState.messages);
|
|
523
699
|
}
|
|
700
|
+
if (storedState.branches && typeof storedState.branches === "object") {
|
|
701
|
+
setBranches(
|
|
702
|
+
storedState.branches
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
if (Array.isArray(storedState.responseVariants)) {
|
|
706
|
+
setResponseVariants(
|
|
707
|
+
storedState.responseVariants
|
|
708
|
+
);
|
|
709
|
+
}
|
|
524
710
|
setDidHydrateStorage(true);
|
|
525
711
|
});
|
|
526
712
|
return () => {
|
|
527
713
|
cancelled = true;
|
|
528
714
|
};
|
|
529
715
|
}, [resolvedStorage]);
|
|
716
|
+
(0, import_react3.useEffect)(() => {
|
|
717
|
+
const nextLastAssistantId = latestAssistantMessageId(messages);
|
|
718
|
+
if (lastAssistantId !== nextLastAssistantId) {
|
|
719
|
+
setLastAssistantId(nextLastAssistantId);
|
|
720
|
+
}
|
|
721
|
+
}, [lastAssistantId, messages]);
|
|
530
722
|
(0, import_react3.useEffect)(() => {
|
|
531
723
|
if (!didHydrateStorage) return;
|
|
532
724
|
const storage = storageRef.current;
|
|
@@ -534,21 +726,64 @@ function useChat(options) {
|
|
|
534
726
|
sessionId,
|
|
535
727
|
workflowId,
|
|
536
728
|
includeHistory,
|
|
537
|
-
messages
|
|
729
|
+
messages,
|
|
730
|
+
branches,
|
|
731
|
+
responseVariants
|
|
538
732
|
});
|
|
539
733
|
}, [
|
|
734
|
+
branches,
|
|
540
735
|
didHydrateStorage,
|
|
541
736
|
includeHistory,
|
|
542
737
|
messages,
|
|
738
|
+
responseVariants,
|
|
543
739
|
sessionId,
|
|
544
740
|
storageRef,
|
|
545
741
|
workflowId
|
|
546
742
|
]);
|
|
743
|
+
(0, import_react3.useEffect)(() => {
|
|
744
|
+
if (!didHydrateStorage || !sessionId) return;
|
|
745
|
+
setBranches((current) => ({
|
|
746
|
+
...current,
|
|
747
|
+
[sessionId]: {
|
|
748
|
+
...current[sessionId] ?? {},
|
|
749
|
+
sessionId,
|
|
750
|
+
messages,
|
|
751
|
+
checkpoints
|
|
752
|
+
}
|
|
753
|
+
}));
|
|
754
|
+
}, [checkpoints, didHydrateStorage, messages, sessionId]);
|
|
755
|
+
(0, import_react3.useEffect)(() => {
|
|
756
|
+
if (!didHydrateStorage || !sessionId) return;
|
|
757
|
+
setResponseVariants(
|
|
758
|
+
(current) => current.map((group) => {
|
|
759
|
+
const selectedVariant = group.variants.find(
|
|
760
|
+
(variant) => variant.id === group.selectedVariantId && variant.sessionId === sessionId
|
|
761
|
+
);
|
|
762
|
+
if (!selectedVariant) return group;
|
|
763
|
+
if (selectedVariant.messages === messages && selectedVariant.checkpoints === checkpoints) {
|
|
764
|
+
return group;
|
|
765
|
+
}
|
|
766
|
+
return {
|
|
767
|
+
...group,
|
|
768
|
+
variants: group.variants.map(
|
|
769
|
+
(variant) => variant.id === selectedVariant.id ? {
|
|
770
|
+
...variant,
|
|
771
|
+
messages,
|
|
772
|
+
checkpoints
|
|
773
|
+
} : variant
|
|
774
|
+
)
|
|
775
|
+
};
|
|
776
|
+
})
|
|
777
|
+
);
|
|
778
|
+
}, [checkpoints, didHydrateStorage, messages, sessionId]);
|
|
547
779
|
const clearActiveStream = () => {
|
|
548
780
|
abortControllerRef.current?.abort();
|
|
549
781
|
abortControllerRef.current = null;
|
|
550
782
|
setIsStreaming(false);
|
|
551
783
|
setMessages([]);
|
|
784
|
+
setCheckpoints([]);
|
|
785
|
+
setBranches({});
|
|
786
|
+
setResponseVariants([]);
|
|
552
787
|
setStreamContentPieces([]);
|
|
553
788
|
clearStreamDebug();
|
|
554
789
|
clearStructuredStreams();
|
|
@@ -560,6 +795,7 @@ function useChat(options) {
|
|
|
560
795
|
};
|
|
561
796
|
const resetSession = () => {
|
|
562
797
|
setSessionId(null);
|
|
798
|
+
setCheckpoints([]);
|
|
563
799
|
};
|
|
564
800
|
const resetChat = () => {
|
|
565
801
|
clearActiveStream();
|
|
@@ -577,7 +813,12 @@ function useChat(options) {
|
|
|
577
813
|
const persistSessionId = (sid) => {
|
|
578
814
|
setSessionId(sid);
|
|
579
815
|
};
|
|
580
|
-
const resolveSessionId = () => {
|
|
816
|
+
const resolveSessionId = (preferredSessionId) => {
|
|
817
|
+
if (preferredSessionId) {
|
|
818
|
+
if (sessionId !== preferredSessionId)
|
|
819
|
+
persistSessionId(preferredSessionId);
|
|
820
|
+
return preferredSessionId;
|
|
821
|
+
}
|
|
581
822
|
const sid = sessionId ?? createId();
|
|
582
823
|
if (!sessionId) persistSessionId(sid);
|
|
583
824
|
return sid;
|
|
@@ -587,13 +828,14 @@ function useChat(options) {
|
|
|
587
828
|
content: message.content
|
|
588
829
|
});
|
|
589
830
|
const prepareMessagesToSend = async (args) => {
|
|
831
|
+
const baseMessages = args.baseMessages ?? messages;
|
|
590
832
|
const prepared = options.prepareContextMessages ? await options.prepareContextMessages({
|
|
591
|
-
messages,
|
|
833
|
+
messages: baseMessages,
|
|
592
834
|
sessionId: args.sid,
|
|
593
835
|
workflowId,
|
|
594
836
|
reason: args.reason,
|
|
595
837
|
context: requestContext
|
|
596
|
-
}) : includeHistory ?
|
|
838
|
+
}) : includeHistory ? baseMessages.map(toOutgoingHistoryMessage) : [];
|
|
597
839
|
return [...prepared, args.outgoingMessage];
|
|
598
840
|
};
|
|
599
841
|
const streamAssistantResponse = async (args) => {
|
|
@@ -602,6 +844,7 @@ function useChat(options) {
|
|
|
602
844
|
const debug = createRecorder(`${args.debugLabel} sessionId=${args.sid}`);
|
|
603
845
|
let currentTrace;
|
|
604
846
|
let completedTrace;
|
|
847
|
+
let currentCheckpoint;
|
|
605
848
|
const pieces = createLiveChatPieces({
|
|
606
849
|
createId,
|
|
607
850
|
onChange: setStreamContentPieces,
|
|
@@ -637,8 +880,35 @@ function useChat(options) {
|
|
|
637
880
|
}
|
|
638
881
|
return;
|
|
639
882
|
}
|
|
883
|
+
if (chunk.type === "checkpoint") {
|
|
884
|
+
currentCheckpoint = {
|
|
885
|
+
id: chunk.id,
|
|
886
|
+
turnIndex: chunk.turnIndex
|
|
887
|
+
};
|
|
888
|
+
setCheckpoints((prev) => {
|
|
889
|
+
const next = {
|
|
890
|
+
id: chunk.id,
|
|
891
|
+
sessionId: chunk.sessionId,
|
|
892
|
+
turnIndex: chunk.turnIndex,
|
|
893
|
+
createdAt: Date.now(),
|
|
894
|
+
nodes: [],
|
|
895
|
+
workflow: workflowId,
|
|
896
|
+
...chunk.label ? { label: chunk.label } : {}
|
|
897
|
+
};
|
|
898
|
+
return [...prev.filter((item) => item.id !== chunk.id), next].sort(
|
|
899
|
+
(a, b) => a.turnIndex - b.turnIndex
|
|
900
|
+
);
|
|
901
|
+
});
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
if (chunk.type === "structured-data-invalidated") {
|
|
905
|
+
deleteStructuredStream(chunk.streamId);
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
640
908
|
if (chunk.type === "error") {
|
|
641
|
-
setError(
|
|
909
|
+
setError(
|
|
910
|
+
(0, import_errors2.isFailureDescriptor)(chunk.failure) ? (0, import_errors2.errorFromFailure)(chunk.failure) : new Error(chunk.message)
|
|
911
|
+
);
|
|
642
912
|
}
|
|
643
913
|
if (chunk.type === "done") {
|
|
644
914
|
completedTrace = currentTrace;
|
|
@@ -649,36 +919,54 @@ function useChat(options) {
|
|
|
649
919
|
});
|
|
650
920
|
}
|
|
651
921
|
});
|
|
652
|
-
if (args.signal.aborted) return;
|
|
922
|
+
if (args.signal.aborted) return void 0;
|
|
653
923
|
const assistant = buildAssistantMessage({
|
|
654
924
|
createId,
|
|
655
925
|
pieces: pieces.getPieces(),
|
|
656
926
|
debug: debug.getAll(),
|
|
657
|
-
trace: completedTrace ?? currentTrace
|
|
927
|
+
trace: completedTrace ?? currentTrace,
|
|
928
|
+
checkpoint: currentCheckpoint
|
|
658
929
|
});
|
|
659
|
-
setMessages(
|
|
930
|
+
setMessages(
|
|
931
|
+
(prev) => args.messagesBeforeAssistant ? [...args.messagesBeforeAssistant, assistant] : [...prev, assistant]
|
|
932
|
+
);
|
|
660
933
|
setLastAssistantId(assistant.id);
|
|
661
934
|
clearStructuredStreams();
|
|
662
935
|
setStreamContentPieces([]);
|
|
663
936
|
clearStreamDebug();
|
|
937
|
+
return assistant;
|
|
664
938
|
};
|
|
665
939
|
const isAbortError = (value, signal) => {
|
|
666
940
|
if (signal.aborted) return true;
|
|
667
941
|
if (!(value instanceof Error)) return false;
|
|
668
942
|
return value.name === "AbortError";
|
|
669
943
|
};
|
|
670
|
-
const
|
|
944
|
+
const respondToHumanInputFromBase = async ({
|
|
671
945
|
resumeToken,
|
|
672
946
|
requestId,
|
|
673
947
|
selected,
|
|
674
|
-
text
|
|
948
|
+
text,
|
|
949
|
+
baseMessages,
|
|
950
|
+
sid
|
|
675
951
|
}) => {
|
|
676
|
-
if (isStreaming) return;
|
|
952
|
+
if (isStreaming) return void 0;
|
|
677
953
|
setError(null);
|
|
678
954
|
const label = typeof text === "string" && text.length > 0 ? text : selected.length > 0 ? selected.length === 1 ? selected[0] : selected.join(", ") : "(selection)";
|
|
679
955
|
const userId = createId();
|
|
680
|
-
const userMsg = {
|
|
681
|
-
|
|
956
|
+
const userMsg = {
|
|
957
|
+
id: userId,
|
|
958
|
+
role: "user",
|
|
959
|
+
content: label,
|
|
960
|
+
source: {
|
|
961
|
+
type: "interrupt-response",
|
|
962
|
+
resumeToken,
|
|
963
|
+
requestId,
|
|
964
|
+
selected,
|
|
965
|
+
...text !== void 0 ? { text } : {}
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
const nextMessages = [...baseMessages ?? messages, userMsg];
|
|
969
|
+
setMessages(nextMessages);
|
|
682
970
|
setIsStreaming(true);
|
|
683
971
|
setStreamContentPieces([]);
|
|
684
972
|
clearStreamDebug();
|
|
@@ -694,18 +982,25 @@ function useChat(options) {
|
|
|
694
982
|
content: label,
|
|
695
983
|
metadata: resumePayload
|
|
696
984
|
};
|
|
697
|
-
const
|
|
985
|
+
const resolvedSessionId = resolveSessionId(sid);
|
|
698
986
|
const messagesToSend = await prepareMessagesToSend({
|
|
699
|
-
sid,
|
|
987
|
+
sid: resolvedSessionId,
|
|
700
988
|
outgoingMessage: outgoing,
|
|
701
|
-
reason: "resume"
|
|
989
|
+
reason: "resume",
|
|
990
|
+
baseMessages
|
|
702
991
|
});
|
|
703
|
-
await streamAssistantResponse({
|
|
704
|
-
sid,
|
|
992
|
+
const assistantMessage = await streamAssistantResponse({
|
|
993
|
+
sid: resolvedSessionId,
|
|
705
994
|
messagesToSend,
|
|
995
|
+
messagesBeforeAssistant: nextMessages,
|
|
706
996
|
debugLabel: "runChat (resume)",
|
|
707
997
|
signal: abortController.signal
|
|
708
998
|
});
|
|
999
|
+
return {
|
|
1000
|
+
userMessage: userMsg,
|
|
1001
|
+
...assistantMessage ? { assistantMessage } : {},
|
|
1002
|
+
messages: assistantMessage ? [...nextMessages, assistantMessage] : nextMessages
|
|
1003
|
+
};
|
|
709
1004
|
} catch (err) {
|
|
710
1005
|
if (isAbortError(err, abortController.signal)) return;
|
|
711
1006
|
const nextError = err instanceof Error ? err : new Error(String(err));
|
|
@@ -718,36 +1013,45 @@ function useChat(options) {
|
|
|
718
1013
|
setIsStreaming(false);
|
|
719
1014
|
}
|
|
720
1015
|
};
|
|
1016
|
+
const respondToHumanInput = async (args) => {
|
|
1017
|
+
await respondToHumanInputFromBase(args);
|
|
1018
|
+
};
|
|
721
1019
|
const respondToInterrupt = async (piece, response) => {
|
|
722
1020
|
const text = response?.text;
|
|
723
1021
|
const selected = response?.selected ?? (typeof text === "string" && text.length > 0 ? [text] : []);
|
|
724
|
-
|
|
1022
|
+
await respondToHumanInput({
|
|
725
1023
|
resumeToken: piece.resumeToken,
|
|
726
1024
|
requestId: piece.requestId,
|
|
727
1025
|
selected,
|
|
728
1026
|
...text !== void 0 ? { text } : {}
|
|
729
1027
|
});
|
|
730
1028
|
};
|
|
731
|
-
const
|
|
1029
|
+
const sendFromBase = async (text, baseMessages, sid) => {
|
|
732
1030
|
const content = text.trim();
|
|
733
|
-
if (!content || isStreaming) return;
|
|
1031
|
+
if (!content || isStreaming) return void 0;
|
|
734
1032
|
setError(null);
|
|
735
1033
|
const activeTextInterrupt = findActiveTextInterrupt({
|
|
736
|
-
messages,
|
|
1034
|
+
messages: baseMessages ?? messages,
|
|
737
1035
|
streamContentPieces
|
|
738
1036
|
});
|
|
739
1037
|
if (activeTextInterrupt) {
|
|
740
|
-
|
|
1038
|
+
return respondToHumanInputFromBase({
|
|
741
1039
|
resumeToken: activeTextInterrupt.resumeToken,
|
|
742
1040
|
requestId: activeTextInterrupt.requestId,
|
|
743
1041
|
selected: [content],
|
|
744
|
-
text: content
|
|
1042
|
+
text: content,
|
|
1043
|
+
baseMessages,
|
|
1044
|
+
sid
|
|
745
1045
|
});
|
|
746
|
-
return;
|
|
747
1046
|
}
|
|
748
1047
|
const userId = createId();
|
|
749
|
-
const userMsg = {
|
|
750
|
-
|
|
1048
|
+
const userMsg = {
|
|
1049
|
+
id: userId,
|
|
1050
|
+
role: "user",
|
|
1051
|
+
content
|
|
1052
|
+
};
|
|
1053
|
+
const nextMessages = [...baseMessages ?? messages, userMsg];
|
|
1054
|
+
setMessages(nextMessages);
|
|
751
1055
|
setIsStreaming(true);
|
|
752
1056
|
setStreamContentPieces([]);
|
|
753
1057
|
clearStreamDebug();
|
|
@@ -755,19 +1059,26 @@ function useChat(options) {
|
|
|
755
1059
|
const abortController = new AbortController();
|
|
756
1060
|
abortControllerRef.current = abortController;
|
|
757
1061
|
try {
|
|
758
|
-
const
|
|
1062
|
+
const resolvedSessionId = resolveSessionId(sid);
|
|
759
1063
|
const messagesToSend = await prepareMessagesToSend({
|
|
760
|
-
sid,
|
|
1064
|
+
sid: resolvedSessionId,
|
|
761
1065
|
outgoingMessage: { role: "user", content },
|
|
762
|
-
reason: "send"
|
|
1066
|
+
reason: "send",
|
|
1067
|
+
baseMessages
|
|
763
1068
|
});
|
|
764
|
-
await streamAssistantResponse({
|
|
765
|
-
sid,
|
|
1069
|
+
const assistantMessage = await streamAssistantResponse({
|
|
1070
|
+
sid: resolvedSessionId,
|
|
766
1071
|
messagesToSend,
|
|
1072
|
+
messagesBeforeAssistant: nextMessages,
|
|
767
1073
|
debugLabel: "runChat (send)",
|
|
768
1074
|
openDebugOnInterrupt: true,
|
|
769
1075
|
signal: abortController.signal
|
|
770
1076
|
});
|
|
1077
|
+
return {
|
|
1078
|
+
userMessage: userMsg,
|
|
1079
|
+
...assistantMessage ? { assistantMessage } : {},
|
|
1080
|
+
messages: assistantMessage ? [...nextMessages, assistantMessage] : nextMessages
|
|
1081
|
+
};
|
|
771
1082
|
} catch (err) {
|
|
772
1083
|
if (isAbortError(err, abortController.signal)) return;
|
|
773
1084
|
const nextError = err instanceof Error ? err : new Error(String(err));
|
|
@@ -780,6 +1091,375 @@ function useChat(options) {
|
|
|
780
1091
|
setIsStreaming(false);
|
|
781
1092
|
}
|
|
782
1093
|
};
|
|
1094
|
+
const send = async (text) => {
|
|
1095
|
+
await sendFromBase(text);
|
|
1096
|
+
};
|
|
1097
|
+
const requireCheckpointTransport = () => {
|
|
1098
|
+
const transport = transportRef.current;
|
|
1099
|
+
if (!transport.listCheckpoints || !transport.rollbackTo || !transport.fork) {
|
|
1100
|
+
throw new Error(
|
|
1101
|
+
"Checkpoint helpers require a transport with checkpoint methods."
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
return transport;
|
|
1105
|
+
};
|
|
1106
|
+
const refreshCheckpoints = async (sid) => {
|
|
1107
|
+
const transport = requireCheckpointTransport();
|
|
1108
|
+
const next = await transport.listCheckpoints(sid);
|
|
1109
|
+
setCheckpoints(next);
|
|
1110
|
+
return next;
|
|
1111
|
+
};
|
|
1112
|
+
const applyStructuredInvalidations = (streamIds) => {
|
|
1113
|
+
for (const streamId of streamIds) {
|
|
1114
|
+
deleteStructuredStream(streamId);
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
const rollbackTo = async (id) => {
|
|
1118
|
+
if (isStreaming) {
|
|
1119
|
+
throw new Error("Cannot rollback while a stream is active.");
|
|
1120
|
+
}
|
|
1121
|
+
const transport = requireCheckpointTransport();
|
|
1122
|
+
const result = await transport.rollbackTo(id);
|
|
1123
|
+
applyStructuredInvalidations(result.invalidatedStructuredStreamIds);
|
|
1124
|
+
const refreshedCheckpoints = await refreshCheckpoints(result.sessionId);
|
|
1125
|
+
const target = refreshedCheckpoints.find(
|
|
1126
|
+
(checkpoint) => checkpoint.id === result.head
|
|
1127
|
+
);
|
|
1128
|
+
if (target) {
|
|
1129
|
+
setMessages((current) => {
|
|
1130
|
+
return trimMessagesToCheckpoint({
|
|
1131
|
+
messages: current,
|
|
1132
|
+
turnIndex: target.turnIndex
|
|
1133
|
+
});
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
return result;
|
|
1137
|
+
};
|
|
1138
|
+
const fork = async (id, options2) => {
|
|
1139
|
+
if (isStreaming) {
|
|
1140
|
+
throw new Error("Cannot fork while a stream is active.");
|
|
1141
|
+
}
|
|
1142
|
+
const transport = requireCheckpointTransport();
|
|
1143
|
+
const sid = resolveSessionId();
|
|
1144
|
+
let availableCheckpoints = checkpoints.length > 0 ? checkpoints : await refreshCheckpoints(sid);
|
|
1145
|
+
let target = availableCheckpoints.find(
|
|
1146
|
+
(checkpoint) => checkpoint.id === id
|
|
1147
|
+
);
|
|
1148
|
+
if (!target && checkpoints.length > 0) {
|
|
1149
|
+
availableCheckpoints = await refreshCheckpoints(sid);
|
|
1150
|
+
target = availableCheckpoints.find((checkpoint) => checkpoint.id === id);
|
|
1151
|
+
}
|
|
1152
|
+
const result = await transport.fork(id, options2);
|
|
1153
|
+
setSessionId(result.sessionId);
|
|
1154
|
+
const childCheckpoints = await refreshCheckpoints(result.sessionId);
|
|
1155
|
+
const childHead = childCheckpoints.at(-1);
|
|
1156
|
+
const forkTurnIndex = target?.turnIndex ?? childHead?.turnIndex;
|
|
1157
|
+
if (typeof forkTurnIndex === "number") {
|
|
1158
|
+
setMessages(
|
|
1159
|
+
(current) => applyForkedPendingRequests({
|
|
1160
|
+
messages: trimMessagesToCheckpoint({
|
|
1161
|
+
messages: current,
|
|
1162
|
+
turnIndex: forkTurnIndex
|
|
1163
|
+
}),
|
|
1164
|
+
pendingRequests: result.checkpoint?.activePendingRequests ?? []
|
|
1165
|
+
})
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
setStreamContentPieces([]);
|
|
1169
|
+
clearStructuredStreams();
|
|
1170
|
+
return result;
|
|
1171
|
+
};
|
|
1172
|
+
const checkpointForMessage = (messageId) => {
|
|
1173
|
+
return messages.find((message) => message.id === messageId)?.checkpointId ?? null;
|
|
1174
|
+
};
|
|
1175
|
+
const variantForMessage = (messageId) => {
|
|
1176
|
+
return responseVariants.find(
|
|
1177
|
+
(group) => group.sourceAssistantMessageId === messageId || group.variants.some(
|
|
1178
|
+
(variant) => variant.assistantMessageId === messageId
|
|
1179
|
+
)
|
|
1180
|
+
) ?? null;
|
|
1181
|
+
};
|
|
1182
|
+
const findResumeOverrideForFork = (baseMessages, forkedBaseMessages, userMessage) => {
|
|
1183
|
+
if (userMessage.source?.type !== "interrupt-response") return void 0;
|
|
1184
|
+
const userSource = userMessage.source;
|
|
1185
|
+
const interruptPositions = baseMessages.flatMap(
|
|
1186
|
+
(message, messageIndex) => (message.contentPieces ?? []).flatMap(
|
|
1187
|
+
(piece, pieceIndex) => piece.type === "interrupt" ? [{ messageIndex, pieceIndex, piece }] : []
|
|
1188
|
+
)
|
|
1189
|
+
);
|
|
1190
|
+
const matchedPosition = [...interruptPositions].reverse().find(
|
|
1191
|
+
({ piece }) => piece.resumeToken === userSource.resumeToken || piece.requestId === userSource.requestId
|
|
1192
|
+
);
|
|
1193
|
+
const forkedPiece = typeof matchedPosition?.messageIndex === "number" ? forkedBaseMessages[matchedPosition.messageIndex]?.contentPieces?.[matchedPosition.pieceIndex] : void 0;
|
|
1194
|
+
if (forkedPiece?.type === "interrupt") {
|
|
1195
|
+
return {
|
|
1196
|
+
resumeToken: forkedPiece.resumeToken,
|
|
1197
|
+
requestId: forkedPiece.requestId
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
const latestForkedInterrupt = forkedBaseMessages.flatMap((message) => message.contentPieces ?? []).filter((piece) => piece.type === "interrupt").at(-1);
|
|
1201
|
+
return latestForkedInterrupt ? {
|
|
1202
|
+
resumeToken: latestForkedInterrupt.resumeToken,
|
|
1203
|
+
requestId: latestForkedInterrupt.requestId
|
|
1204
|
+
} : void 0;
|
|
1205
|
+
};
|
|
1206
|
+
const resolveRegenerateTarget = async (assistantMessageId) => {
|
|
1207
|
+
const assistantIndex = messages.findIndex(
|
|
1208
|
+
(message) => message.id === assistantMessageId
|
|
1209
|
+
);
|
|
1210
|
+
if (assistantIndex < 0) {
|
|
1211
|
+
throw new Error(`Assistant message "${assistantMessageId}" not found.`);
|
|
1212
|
+
}
|
|
1213
|
+
const assistant = messages[assistantIndex];
|
|
1214
|
+
if (!assistant) {
|
|
1215
|
+
throw new Error(`Assistant message "${assistantMessageId}" not found.`);
|
|
1216
|
+
}
|
|
1217
|
+
if (assistant.role !== "assistant") {
|
|
1218
|
+
throw new Error("Regenerate expects an assistant message id.");
|
|
1219
|
+
}
|
|
1220
|
+
let userIndex = -1;
|
|
1221
|
+
for (let index = assistantIndex - 1; index >= 0; index--) {
|
|
1222
|
+
if (messages[index]?.role === "user") {
|
|
1223
|
+
userIndex = index;
|
|
1224
|
+
break;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
if (userIndex < 0) {
|
|
1228
|
+
throw new Error("No preceding user message found for regenerate.");
|
|
1229
|
+
}
|
|
1230
|
+
const previousUser = messages[userIndex];
|
|
1231
|
+
const sid = resolveSessionId();
|
|
1232
|
+
let availableCheckpoints = checkpoints.length > 0 ? checkpoints : await refreshCheckpoints(sid);
|
|
1233
|
+
const assistantTurn = assistant.checkpointTurnIndex;
|
|
1234
|
+
const targetTurn = typeof assistantTurn === "number" ? assistantTurn - 1 : void 0;
|
|
1235
|
+
let target = targetTurn !== void 0 ? availableCheckpoints.find(
|
|
1236
|
+
(checkpoint) => checkpoint.turnIndex === targetTurn
|
|
1237
|
+
) : availableCheckpoints.filter(
|
|
1238
|
+
(checkpoint) => checkpoint.turnIndex < Number.MAX_SAFE_INTEGER
|
|
1239
|
+
).at(-2);
|
|
1240
|
+
if (!target && checkpoints.length > 0) {
|
|
1241
|
+
availableCheckpoints = await refreshCheckpoints(sid);
|
|
1242
|
+
target = targetTurn !== void 0 ? availableCheckpoints.find(
|
|
1243
|
+
(checkpoint) => checkpoint.turnIndex === targetTurn
|
|
1244
|
+
) : availableCheckpoints.filter(
|
|
1245
|
+
(checkpoint) => checkpoint.turnIndex < Number.MAX_SAFE_INTEGER
|
|
1246
|
+
).at(-2);
|
|
1247
|
+
}
|
|
1248
|
+
if (!target) {
|
|
1249
|
+
throw new Error("No rollback checkpoint found for regenerate.");
|
|
1250
|
+
}
|
|
1251
|
+
const baseMessages = trimMessagesToCheckpoint({
|
|
1252
|
+
messages,
|
|
1253
|
+
turnIndex: target.turnIndex
|
|
1254
|
+
});
|
|
1255
|
+
return { target, assistant, previousUser, baseMessages };
|
|
1256
|
+
};
|
|
1257
|
+
const replayUserMessage = async (userMessage, baseMessages, editedContent, options2) => {
|
|
1258
|
+
const content = editedContent ?? userMessage.content;
|
|
1259
|
+
if (userMessage.source?.type === "interrupt-response") {
|
|
1260
|
+
return respondToHumanInputFromBase({
|
|
1261
|
+
resumeToken: options2?.resumeOverride?.resumeToken ?? userMessage.source.resumeToken,
|
|
1262
|
+
requestId: options2?.resumeOverride?.requestId ?? userMessage.source.requestId,
|
|
1263
|
+
selected: editedContent !== void 0 ? [editedContent] : userMessage.source.selected,
|
|
1264
|
+
baseMessages,
|
|
1265
|
+
sid: options2?.sid,
|
|
1266
|
+
...editedContent !== void 0 ? { text: editedContent } : userMessage.source.text !== void 0 ? { text: userMessage.source.text } : {}
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
return sendFromBase(content, baseMessages, options2?.sid);
|
|
1270
|
+
};
|
|
1271
|
+
const buildVariant = (args) => ({
|
|
1272
|
+
id: args.id,
|
|
1273
|
+
sessionId: args.sessionId,
|
|
1274
|
+
assistantMessageId: args.assistantMessage.id,
|
|
1275
|
+
createdAt: Date.now(),
|
|
1276
|
+
messages: args.messages,
|
|
1277
|
+
checkpoints: args.checkpoints,
|
|
1278
|
+
...args.assistantMessage.checkpointId ? { checkpointId: args.assistantMessage.checkpointId } : {},
|
|
1279
|
+
...typeof args.assistantMessage.checkpointTurnIndex === "number" ? { checkpointTurnIndex: args.assistantMessage.checkpointTurnIndex } : {},
|
|
1280
|
+
...args.label ? { label: args.label } : {}
|
|
1281
|
+
});
|
|
1282
|
+
const updateVariantSnapshot = (args) => ({
|
|
1283
|
+
...args.variant,
|
|
1284
|
+
sessionId: args.sessionId,
|
|
1285
|
+
assistantMessageId: args.assistantMessage.id,
|
|
1286
|
+
messages: args.messages,
|
|
1287
|
+
checkpoints: args.checkpoints,
|
|
1288
|
+
...args.assistantMessage.checkpointId ? { checkpointId: args.assistantMessage.checkpointId } : {},
|
|
1289
|
+
...typeof args.assistantMessage.checkpointTurnIndex === "number" ? { checkpointTurnIndex: args.assistantMessage.checkpointTurnIndex } : {}
|
|
1290
|
+
});
|
|
1291
|
+
const replaceResponseVariantGroup = (nextGroup) => {
|
|
1292
|
+
setResponseVariants((current) => [
|
|
1293
|
+
...current.filter((group) => group.id !== nextGroup.id),
|
|
1294
|
+
nextGroup
|
|
1295
|
+
]);
|
|
1296
|
+
};
|
|
1297
|
+
const regenerateVariant = async (assistantMessageId) => {
|
|
1298
|
+
if (isStreaming) {
|
|
1299
|
+
throw new Error("Cannot regenerate a variant while a stream is active.");
|
|
1300
|
+
}
|
|
1301
|
+
const { target, assistant, previousUser, baseMessages } = await resolveRegenerateTarget(assistantMessageId);
|
|
1302
|
+
const transport = requireCheckpointTransport();
|
|
1303
|
+
const parentSessionId = resolveSessionId();
|
|
1304
|
+
const existingGroup = variantForMessage(assistantMessageId);
|
|
1305
|
+
const sourceAssistantMessageId = existingGroup?.sourceAssistantMessageId ?? assistantMessageId;
|
|
1306
|
+
const existingVariants = existingGroup ? existingGroup.variants.map(
|
|
1307
|
+
(variant) => variant.id === existingGroup.selectedVariantId || variant.assistantMessageId === assistantMessageId ? updateVariantSnapshot({
|
|
1308
|
+
variant,
|
|
1309
|
+
sessionId: parentSessionId,
|
|
1310
|
+
assistantMessage: assistant,
|
|
1311
|
+
messages,
|
|
1312
|
+
checkpoints
|
|
1313
|
+
}) : variant
|
|
1314
|
+
) : void 0;
|
|
1315
|
+
const originalVariant = existingVariants?.[0] ?? buildVariant({
|
|
1316
|
+
id: `${sourceAssistantMessageId}:original`,
|
|
1317
|
+
sessionId: parentSessionId,
|
|
1318
|
+
assistantMessage: assistant,
|
|
1319
|
+
messages,
|
|
1320
|
+
checkpoints,
|
|
1321
|
+
label: "Original"
|
|
1322
|
+
});
|
|
1323
|
+
const forkResult = await transport.fork(target.id);
|
|
1324
|
+
const forkedSessionId = forkResult.sessionId;
|
|
1325
|
+
setSessionId(forkedSessionId);
|
|
1326
|
+
const childCheckpointsBeforeReplay = await refreshCheckpoints(forkedSessionId);
|
|
1327
|
+
const forkedBaseMessages = applyForkedPendingRequests({
|
|
1328
|
+
messages: baseMessages,
|
|
1329
|
+
pendingRequests: forkResult.checkpoint?.activePendingRequests ?? []
|
|
1330
|
+
});
|
|
1331
|
+
const resumeOverride = findResumeOverrideForFork(
|
|
1332
|
+
baseMessages,
|
|
1333
|
+
forkedBaseMessages,
|
|
1334
|
+
previousUser
|
|
1335
|
+
);
|
|
1336
|
+
setMessages(forkedBaseMessages);
|
|
1337
|
+
setStreamContentPieces([]);
|
|
1338
|
+
clearStructuredStreams();
|
|
1339
|
+
const replay = await replayUserMessage(
|
|
1340
|
+
previousUser,
|
|
1341
|
+
forkedBaseMessages,
|
|
1342
|
+
void 0,
|
|
1343
|
+
{
|
|
1344
|
+
sid: forkedSessionId,
|
|
1345
|
+
resumeOverride
|
|
1346
|
+
}
|
|
1347
|
+
);
|
|
1348
|
+
const childCheckpointsAfterReplay = await refreshCheckpoints(forkedSessionId);
|
|
1349
|
+
const variantMessages = replay?.messages ?? forkedBaseMessages;
|
|
1350
|
+
const variantAssistant = replay?.assistantMessage;
|
|
1351
|
+
if (!variantAssistant) {
|
|
1352
|
+
throw new Error("Regenerate variant did not produce an assistant reply.");
|
|
1353
|
+
}
|
|
1354
|
+
const nextVariant = buildVariant({
|
|
1355
|
+
id: createId(),
|
|
1356
|
+
sessionId: forkedSessionId,
|
|
1357
|
+
assistantMessage: variantAssistant,
|
|
1358
|
+
messages: variantMessages,
|
|
1359
|
+
checkpoints: childCheckpointsAfterReplay.length > 0 ? childCheckpointsAfterReplay : childCheckpointsBeforeReplay,
|
|
1360
|
+
label: `Variant ${(existingGroup?.variants.length ?? 1) + 1}`
|
|
1361
|
+
});
|
|
1362
|
+
const nextVariants = existingVariants ? [...existingVariants, nextVariant] : [originalVariant, nextVariant];
|
|
1363
|
+
const nextGroup = {
|
|
1364
|
+
id: existingGroup?.id ?? createId(),
|
|
1365
|
+
sourceAssistantMessageId,
|
|
1366
|
+
checkpointId: existingGroup?.checkpointId ?? target.id,
|
|
1367
|
+
checkpointTurnIndex: existingGroup?.checkpointTurnIndex ?? target.turnIndex,
|
|
1368
|
+
selectedVariantId: nextVariant.id,
|
|
1369
|
+
variants: nextVariants
|
|
1370
|
+
};
|
|
1371
|
+
replaceResponseVariantGroup(nextGroup);
|
|
1372
|
+
setBranches((current) => ({
|
|
1373
|
+
...current,
|
|
1374
|
+
[parentSessionId]: {
|
|
1375
|
+
...current[parentSessionId] ?? {},
|
|
1376
|
+
sessionId: parentSessionId,
|
|
1377
|
+
messages,
|
|
1378
|
+
checkpoints
|
|
1379
|
+
},
|
|
1380
|
+
[forkedSessionId]: {
|
|
1381
|
+
...current[forkedSessionId] ?? {},
|
|
1382
|
+
sessionId: forkedSessionId,
|
|
1383
|
+
parentSessionId: forkResult.parentSessionId,
|
|
1384
|
+
forkedFrom: forkResult.forkedFrom,
|
|
1385
|
+
messages: variantMessages,
|
|
1386
|
+
checkpoints: nextVariant.checkpoints
|
|
1387
|
+
}
|
|
1388
|
+
}));
|
|
1389
|
+
return nextGroup;
|
|
1390
|
+
};
|
|
1391
|
+
const selectVariant = async (assistantMessageId, variantId) => {
|
|
1392
|
+
const group = variantForMessage(assistantMessageId);
|
|
1393
|
+
if (!group) {
|
|
1394
|
+
throw new Error(
|
|
1395
|
+
`No response variants found for "${assistantMessageId}".`
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
const selectedVariant = group.variants.find(
|
|
1399
|
+
(variant) => variant.id === variantId
|
|
1400
|
+
);
|
|
1401
|
+
if (!selectedVariant) {
|
|
1402
|
+
throw new Error(`Response variant "${variantId}" not found.`);
|
|
1403
|
+
}
|
|
1404
|
+
setSessionId(selectedVariant.sessionId);
|
|
1405
|
+
setMessages(selectedVariant.messages);
|
|
1406
|
+
setCheckpoints(selectedVariant.checkpoints);
|
|
1407
|
+
setStreamContentPieces([]);
|
|
1408
|
+
clearStructuredStreams();
|
|
1409
|
+
setBranches((current) => ({
|
|
1410
|
+
...current,
|
|
1411
|
+
[selectedVariant.sessionId]: {
|
|
1412
|
+
...current[selectedVariant.sessionId] ?? {},
|
|
1413
|
+
sessionId: selectedVariant.sessionId,
|
|
1414
|
+
messages: selectedVariant.messages,
|
|
1415
|
+
checkpoints: selectedVariant.checkpoints
|
|
1416
|
+
}
|
|
1417
|
+
}));
|
|
1418
|
+
const nextGroup = {
|
|
1419
|
+
...group,
|
|
1420
|
+
selectedVariantId: selectedVariant.id
|
|
1421
|
+
};
|
|
1422
|
+
replaceResponseVariantGroup(nextGroup);
|
|
1423
|
+
};
|
|
1424
|
+
const regenerate = async (assistantMessageId) => {
|
|
1425
|
+
const { target, previousUser, baseMessages } = await resolveRegenerateTarget(assistantMessageId);
|
|
1426
|
+
await rollbackTo(target.id);
|
|
1427
|
+
setMessages(baseMessages);
|
|
1428
|
+
await replayUserMessage(previousUser, baseMessages);
|
|
1429
|
+
};
|
|
1430
|
+
const regenerateFromCheckpoint = async (checkpointId) => {
|
|
1431
|
+
const sid = resolveSessionId();
|
|
1432
|
+
let availableCheckpoints = checkpoints.length > 0 ? checkpoints : await refreshCheckpoints(sid);
|
|
1433
|
+
let target = availableCheckpoints.find(
|
|
1434
|
+
(checkpoint) => checkpoint.id === checkpointId
|
|
1435
|
+
);
|
|
1436
|
+
if (!target && checkpoints.length > 0) {
|
|
1437
|
+
availableCheckpoints = await refreshCheckpoints(sid);
|
|
1438
|
+
target = availableCheckpoints.find(
|
|
1439
|
+
(checkpoint) => checkpoint.id === checkpointId
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
if (!target) {
|
|
1443
|
+
throw new Error(`Checkpoint "${checkpointId}" not found.`);
|
|
1444
|
+
}
|
|
1445
|
+
const baseMessages = trimMessagesToCheckpoint({
|
|
1446
|
+
messages,
|
|
1447
|
+
turnIndex: target.turnIndex
|
|
1448
|
+
});
|
|
1449
|
+
const previousUser = messages.slice(baseMessages.length).find((message) => message.role === "user");
|
|
1450
|
+
if (!previousUser) {
|
|
1451
|
+
throw new Error("No user message found after checkpoint for regenerate.");
|
|
1452
|
+
}
|
|
1453
|
+
await rollbackTo(target.id);
|
|
1454
|
+
setMessages(baseMessages);
|
|
1455
|
+
await replayUserMessage(previousUser, baseMessages);
|
|
1456
|
+
};
|
|
1457
|
+
const retryWithEdit = async (assistantMessageId, newUserContent) => {
|
|
1458
|
+
const { target, previousUser, baseMessages } = await resolveRegenerateTarget(assistantMessageId);
|
|
1459
|
+
await rollbackTo(target.id);
|
|
1460
|
+
setMessages(baseMessages);
|
|
1461
|
+
await replayUserMessage(previousUser, baseMessages, newUserContent);
|
|
1462
|
+
};
|
|
783
1463
|
return {
|
|
784
1464
|
messages,
|
|
785
1465
|
isStreaming,
|
|
@@ -788,12 +1468,25 @@ function useChat(options) {
|
|
|
788
1468
|
streamContentPieces,
|
|
789
1469
|
streamDebug,
|
|
790
1470
|
lastAssistantId,
|
|
1471
|
+
checkpoints,
|
|
1472
|
+
activeSessionId: sessionId,
|
|
1473
|
+
branches,
|
|
1474
|
+
responseVariants,
|
|
791
1475
|
workflowId,
|
|
792
1476
|
setWorkflowId,
|
|
793
1477
|
send,
|
|
794
1478
|
respondToHumanInput,
|
|
795
1479
|
respondToInterrupt,
|
|
796
1480
|
setMessages,
|
|
1481
|
+
checkpointForMessage,
|
|
1482
|
+
rollbackTo,
|
|
1483
|
+
fork,
|
|
1484
|
+
regenerate,
|
|
1485
|
+
regenerateVariant,
|
|
1486
|
+
selectVariant,
|
|
1487
|
+
variantForMessage,
|
|
1488
|
+
regenerateFromCheckpoint,
|
|
1489
|
+
retryWithEdit,
|
|
797
1490
|
clearMessages,
|
|
798
1491
|
resetSession,
|
|
799
1492
|
resetChat,
|