@assistant-ui/react-langchain 0.0.31 → 0.0.33

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.
Files changed (60) hide show
  1. package/dist/attachSubagentTranscripts.d.ts +19 -12
  2. package/dist/attachSubagentTranscripts.d.ts.map +1 -1
  3. package/dist/attachSubagentTranscripts.js +8 -4
  4. package/dist/attachSubagentTranscripts.js.map +1 -1
  5. package/dist/convertMessages.d.ts +8 -11
  6. package/dist/convertMessages.d.ts.map +1 -1
  7. package/dist/convertMessages.js +53 -15
  8. package/dist/convertMessages.js.map +1 -1
  9. package/dist/converter.d.ts +137 -138
  10. package/dist/converter.d.ts.map +1 -1
  11. package/dist/converter.js +2 -1
  12. package/dist/converter.js.map +1 -1
  13. package/dist/findForkCheckpointInHistory.d.ts +17 -19
  14. package/dist/findForkCheckpointInHistory.d.ts.map +1 -1
  15. package/dist/hooks.d.ts +19 -22
  16. package/dist/hooks.d.ts.map +1 -1
  17. package/dist/index.d.ts +7 -8
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/resolveForkCheckpoint.d.ts +5 -7
  20. package/dist/resolveForkCheckpoint.d.ts.map +1 -1
  21. package/dist/runtimeExtras.d.ts +2 -5
  22. package/dist/runtimeExtras.d.ts.map +1 -1
  23. package/dist/sdkIdentity.d.ts +2 -5
  24. package/dist/sdkIdentity.d.ts.map +1 -1
  25. package/dist/sdkIdentity.js +1 -1
  26. package/dist/streamingTiming.d.ts +4 -7
  27. package/dist/streamingTiming.d.ts.map +1 -1
  28. package/dist/subagentMessagesProjection.d.ts +17 -0
  29. package/dist/subagentMessagesProjection.d.ts.map +1 -0
  30. package/dist/subagentMessagesProjection.js +40 -0
  31. package/dist/subagentMessagesProjection.js.map +1 -0
  32. package/dist/types.d.ts +106 -109
  33. package/dist/types.d.ts.map +1 -1
  34. package/dist/uiMessages.d.ts +37 -10
  35. package/dist/uiMessages.d.ts.map +1 -1
  36. package/dist/uiMessages.js +55 -5
  37. package/dist/uiMessages.js.map +1 -1
  38. package/dist/useStreamRuntime.d.ts +9 -12
  39. package/dist/useStreamRuntime.d.ts.map +1 -1
  40. package/dist/useStreamRuntime.js +102 -28
  41. package/dist/useStreamRuntime.js.map +1 -1
  42. package/dist/useSubagentTranscripts.d.ts +5 -8
  43. package/dist/useSubagentTranscripts.d.ts.map +1 -1
  44. package/dist/useSubagentTranscripts.js +129 -34
  45. package/dist/useSubagentTranscripts.js.map +1 -1
  46. package/package.json +16 -11
  47. package/src/attachSubagentTranscripts.test.ts +13 -5
  48. package/src/attachSubagentTranscripts.ts +26 -7
  49. package/src/convertMessages.test.ts +69 -0
  50. package/src/convertMessages.ts +46 -8
  51. package/src/converter.ts +6 -0
  52. package/src/subagentMessagesProjection.test.ts +175 -0
  53. package/src/subagentMessagesProjection.ts +50 -0
  54. package/src/uiMessages.test.ts +149 -0
  55. package/src/uiMessages.ts +79 -4
  56. package/src/useStreamRuntime.test.tsx +212 -0
  57. package/src/useStreamRuntime.ts +202 -50
  58. package/src/useStreamRuntime.voice.test.tsx +712 -0
  59. package/src/useSubagentTranscripts.test.tsx +795 -52
  60. package/src/useSubagentTranscripts.ts +269 -50
@@ -1015,6 +1015,8 @@ describe("useStreamRuntime subagent transcripts", () => {
1015
1015
  status: "running",
1016
1016
  parentId: null,
1017
1017
  depth: 1,
1018
+ startedAt: new Date(1_000),
1019
+ completedAt: null,
1018
1020
  },
1019
1021
  ],
1020
1022
  ]);
@@ -1069,4 +1071,214 @@ describe("useStreamRuntime subagent transcripts", () => {
1069
1071
 
1070
1072
  expect(nestedTranscript()).toBe(rendered);
1071
1073
  });
1074
+
1075
+ it("keeps messages and transcripts across equal copies of the UI state", async () => {
1076
+ const stream = createMockStream([
1077
+ message("human-1", "human", "delegate"),
1078
+ {
1079
+ id: "root-ai",
1080
+ _getType: () => "ai",
1081
+ content: "",
1082
+ tool_calls: [{ id: "task-one", name: "task", args: {} }],
1083
+ },
1084
+ ]);
1085
+ const transcript = [message("nested-ai", "ai", "nested answer")];
1086
+ stream.subagents = new Map([
1087
+ [
1088
+ "task-one",
1089
+ {
1090
+ id: "task-one",
1091
+ namespace: ["tools:task-one"],
1092
+ status: "running",
1093
+ parentId: null,
1094
+ depth: 1,
1095
+ startedAt: new Date(1_000),
1096
+ completedAt: null,
1097
+ },
1098
+ ],
1099
+ ]);
1100
+ stream[streamController]!.registry.acquire.mockReturnValue({
1101
+ store: { getSnapshot: () => transcript, subscribe: () => () => {} },
1102
+ release: vi.fn(),
1103
+ });
1104
+ const uiState = (points: number[]) => [
1105
+ {
1106
+ type: "ui",
1107
+ id: "ui-root",
1108
+ name: "chart",
1109
+ props: { points },
1110
+ metadata: { message_id: "root-ai" },
1111
+ },
1112
+ {
1113
+ type: "ui",
1114
+ id: "ui-nested",
1115
+ name: "chart",
1116
+ props: { points },
1117
+ metadata: { message_id: "nested-ai" },
1118
+ },
1119
+ ];
1120
+ stream.values = { ui: uiState([1, 2]) };
1121
+ const { auiResult, rerender } = renderAui(stream);
1122
+ const nestedTranscript = () => {
1123
+ const { messages } = auiResult.current.thread.getState();
1124
+ for (const threadMessage of messages) {
1125
+ for (const part of threadMessage.content) {
1126
+ if (part.type === "tool-call" && part.toolCallId === "task-one")
1127
+ return part.messages;
1128
+ }
1129
+ }
1130
+ return undefined;
1131
+ };
1132
+
1133
+ await waitFor(() =>
1134
+ expect(nestedTranscript()?.[0]?.content).toMatchObject([
1135
+ { type: "text", text: "nested answer" },
1136
+ { type: "data", name: "chart", data: { points: [1, 2] } },
1137
+ ]),
1138
+ );
1139
+ const [human, ai] = auiResult.current.thread.getState().messages;
1140
+ expect(ai?.content).toMatchObject([
1141
+ { type: "tool-call", toolCallId: "task-one" },
1142
+ { type: "data", name: "chart", data: { points: [1, 2] } },
1143
+ ]);
1144
+ const rendered = nestedTranscript();
1145
+
1146
+ for (let i = 0; i < 3; i++) {
1147
+ stream.values = { ui: uiState([1, 2]) };
1148
+ await act(async () => {
1149
+ rerender();
1150
+ });
1151
+ }
1152
+
1153
+ const messages = auiResult.current.thread.getState().messages;
1154
+ expect(messages[0]).toBe(human);
1155
+ expect(messages[1]).toBe(ai);
1156
+ expect(nestedTranscript()).toBe(rendered);
1157
+
1158
+ stream.values = { ui: uiState([1, 2, 3]) };
1159
+ await act(async () => {
1160
+ rerender();
1161
+ });
1162
+
1163
+ expect(
1164
+ auiResult.current.thread.getState().messages[1]?.content,
1165
+ ).toMatchObject([
1166
+ { type: "tool-call", toolCallId: "task-one" },
1167
+ { type: "data", name: "chart", data: { points: [1, 2, 3] } },
1168
+ ]);
1169
+ expect(nestedTranscript()?.[0]?.content).toMatchObject([
1170
+ { type: "text", text: "nested answer" },
1171
+ { type: "data", name: "chart", data: { points: [1, 2, 3] } },
1172
+ ]);
1173
+ });
1174
+
1175
+ it("keeps messages and transcripts when custom events carry no UI update", async () => {
1176
+ const stream = createMockStream([
1177
+ message("human-1", "human", "delegate"),
1178
+ {
1179
+ id: "root-ai",
1180
+ _getType: () => "ai",
1181
+ content: "",
1182
+ tool_calls: [{ id: "task-one", name: "task", args: {} }],
1183
+ },
1184
+ ]);
1185
+ const transcript = [message("nested-ai", "ai", "nested answer")];
1186
+ stream.subagents = new Map([
1187
+ [
1188
+ "task-one",
1189
+ {
1190
+ id: "task-one",
1191
+ namespace: ["tools:task-one"],
1192
+ status: "running",
1193
+ parentId: null,
1194
+ depth: 1,
1195
+ startedAt: new Date(1_000),
1196
+ completedAt: null,
1197
+ },
1198
+ ],
1199
+ ]);
1200
+ stream[streamController]!.registry.acquire.mockReturnValue({
1201
+ store: { getSnapshot: () => transcript, subscribe: () => () => {} },
1202
+ release: vi.fn(),
1203
+ });
1204
+ const chartEvent = (
1205
+ props: Record<string, unknown>,
1206
+ metadata: Record<string, unknown> = {},
1207
+ ) => ({
1208
+ method: "custom",
1209
+ params: {
1210
+ namespace: ["tools:task-one"],
1211
+ data: {
1212
+ type: "ui",
1213
+ id: "ui-1",
1214
+ name: "chart",
1215
+ props,
1216
+ metadata: { message_id: "nested-ai", ...metadata },
1217
+ },
1218
+ },
1219
+ });
1220
+ const events = [
1221
+ chartEvent({ points: [1, 2] }),
1222
+ chartEvent({ label: "first" }, { merge: true }),
1223
+ ];
1224
+ mockUseChannel.mockReturnValue(events);
1225
+ const { auiResult, rerender } = renderAui(stream);
1226
+ const nestedTranscript = () => {
1227
+ const { messages } = auiResult.current.thread.getState();
1228
+ for (const threadMessage of messages) {
1229
+ for (const part of threadMessage.content) {
1230
+ if (part.type === "tool-call" && part.toolCallId === "task-one")
1231
+ return part.messages;
1232
+ }
1233
+ }
1234
+ return undefined;
1235
+ };
1236
+
1237
+ await waitFor(() =>
1238
+ expect(nestedTranscript()?.[0]?.content).toMatchObject([
1239
+ { type: "text", text: "nested answer" },
1240
+ {
1241
+ type: "data",
1242
+ name: "chart",
1243
+ data: { points: [1, 2], label: "first" },
1244
+ },
1245
+ ]),
1246
+ );
1247
+ const [human, ai] = auiResult.current.thread.getState().messages;
1248
+ const rendered = nestedTranscript();
1249
+
1250
+ const withProgress = [
1251
+ ...events,
1252
+ {
1253
+ method: "custom",
1254
+ params: { namespace: [], data: { name: "progress", payload: 1 } },
1255
+ },
1256
+ ];
1257
+ mockUseChannel.mockReturnValue(withProgress);
1258
+ await act(async () => {
1259
+ rerender();
1260
+ });
1261
+
1262
+ const messages = auiResult.current.thread.getState().messages;
1263
+ expect(messages[0]).toBe(human);
1264
+ expect(messages[1]).toBe(ai);
1265
+ expect(nestedTranscript()).toBe(rendered);
1266
+
1267
+ mockUseChannel.mockReturnValue([
1268
+ ...withProgress,
1269
+ chartEvent({ label: "second" }, { merge: true }),
1270
+ ]);
1271
+ await act(async () => {
1272
+ rerender();
1273
+ });
1274
+
1275
+ expect(nestedTranscript()?.[0]?.content).toMatchObject([
1276
+ { type: "text", text: "nested answer" },
1277
+ {
1278
+ type: "data",
1279
+ name: "chart",
1280
+ data: { points: [1, 2], label: "second" },
1281
+ },
1282
+ ]);
1283
+ });
1072
1284
  });
@@ -1,4 +1,4 @@
1
- /// <reference types="@assistant-ui/core/store" />
1
+ /// <reference types="@assistant-ui/core/store" preserve="true" />
2
2
  "use client";
3
3
 
4
4
  import {
@@ -19,6 +19,7 @@ import type { ThreadMessage } from "@assistant-ui/core";
19
19
  import {
20
20
  createCloudThreadListAdapterCreateFallback,
21
21
  createToolCallCancellationStub,
22
+ getThreadMessageText,
22
23
  scanPendingToolCalls,
23
24
  } from "@assistant-ui/core/internal";
24
25
  import {
@@ -29,14 +30,13 @@ import {
29
30
  } from "@assistant-ui/core/react";
30
31
  import { useAui, useAuiState } from "@assistant-ui/store";
31
32
  import { STREAM_CONTROLLER, useChannel, useStream } from "@langchain/react";
32
- import type { Channel } from "@langchain/react";
33
33
  import type {
34
34
  LangChainBaseMessage,
35
35
  LangChainToolCall,
36
36
  UIMessage,
37
37
  UseStreamRuntimeOptions,
38
38
  } from "./types";
39
- import { groupUIMessagesByParent } from "./converter";
39
+ import { getMessageModality, groupUIMessagesByParent } from "./converter";
40
40
  export { groupUIMessagesByParent } from "./converter";
41
41
  import {
42
42
  convertLangChainBaseMessage,
@@ -48,14 +48,19 @@ import {
48
48
  createAttachMemo,
49
49
  } from "./attachSubagentTranscripts";
50
50
  import { useSubagentTranscripts } from "./useSubagentTranscripts";
51
- import { foldUIUpdates, mergeUIMessages } from "./uiMessages";
51
+ import {
52
+ createUIFoldMemo,
53
+ createUISnapshotMemo,
54
+ foldUIUpdates,
55
+ mergeUIMessages,
56
+ reconcileUISnapshot,
57
+ UI_CUSTOM_CHANNELS,
58
+ } from "./uiMessages";
52
59
  import { langChainExtras } from "./runtimeExtras";
53
60
  import { resolveForkCheckpoint } from "./resolveForkCheckpoint";
54
61
  import { useLangChainStreamingTiming } from "./streamingTiming";
55
62
  import { LANGCHAIN_SDK } from "./sdkIdentity";
56
63
 
57
- const UI_CUSTOM_CHANNELS: readonly Channel[] = ["custom"];
58
-
59
64
  export const runConfigToSubmitOptions = (
60
65
  runConfig: AppendMessage["runConfig"],
61
66
  ) =>
@@ -92,6 +97,15 @@ const toStagedHumanMessage = (
92
97
  content: getMessageContent(msg),
93
98
  });
94
99
 
100
+ const toStagedMessageInput = (message: LangChainBaseMessage) => ({
101
+ id: message.id,
102
+ type: getMessageType(message) === "ai" ? ("ai" as const) : ("human" as const),
103
+ content: message.content,
104
+ ...(message.additional_kwargs && {
105
+ additional_kwargs: message.additional_kwargs,
106
+ }),
107
+ });
108
+
95
109
  const humanContentText = (content: LangChainBaseMessage["content"]) => {
96
110
  if (typeof content === "string") return content;
97
111
  if (!Array.isArray(content)) return "";
@@ -164,12 +178,17 @@ const useStreamThreadRuntime = (
164
178
  );
165
179
  const effectiveIsRunning = stream.isLoading || hasExecutingTools;
166
180
 
167
- const uiStateValue = stream.values[uiStateKey];
181
+ const [uiSnapshotMemo] = useState(createUISnapshotMemo);
182
+ const uiStateValue = reconcileUISnapshot(
183
+ stream.values[uiStateKey],
184
+ uiSnapshotMemo,
185
+ );
168
186
 
169
187
  const customEvents = useChannel(stream, UI_CUSTOM_CHANNELS);
188
+ const [uiFoldMemo] = useState(createUIFoldMemo);
170
189
  const liveUiMessages = useMemo(
171
- () => foldUIUpdates(customEvents),
172
- [customEvents],
190
+ () => foldUIUpdates(customEvents, uiFoldMemo),
191
+ [customEvents, uiFoldMemo],
173
192
  );
174
193
 
175
194
  const mergedUiMessages = useMemo(
@@ -302,6 +321,7 @@ const useStreamThreadRuntime = (
302
321
  runConfig: AppendMessage["runConfig"];
303
322
  reconcileOnEcho: boolean;
304
323
  baseMessageCount: number;
324
+ transcriptStatus?: "unsent" | "sent";
305
325
  }
306
326
  >(),
307
327
  );
@@ -357,19 +377,121 @@ const useStreamThreadRuntime = (
357
377
  }, [stream.messages]);
358
378
 
359
379
  const getStagedRun = (parentId: string | null) => {
360
- if (!parentId || !stagedMessagesRef.current.has(parentId)) return null;
380
+ const parent = parentId
381
+ ? stagedMessagesRef.current.get(parentId)
382
+ : undefined;
383
+ if (!parent || parent.transcriptStatus === "sent") return null;
361
384
 
362
385
  const staged: LangChainBaseMessage[] = [];
363
386
  for (const message of visibleMessagesRef.current) {
364
- if (message.id && stagedMessagesRef.current.has(message.id)) {
365
- staged.push(stagedMessagesRef.current.get(message.id)!.message);
387
+ const entry = message.id
388
+ ? stagedMessagesRef.current.get(message.id)
389
+ : undefined;
390
+ if (entry && entry.transcriptStatus !== "sent") {
391
+ staged.push(entry.message);
366
392
  }
367
393
  if (message.id === parentId) break;
368
394
  }
369
395
 
370
396
  return {
371
397
  messages: staged,
372
- runConfig: stagedMessagesRef.current.get(parentId)!.runConfig,
398
+ runConfig: parent.runConfig,
399
+ };
400
+ };
401
+
402
+ const appendVoiceTranscript = (message: ThreadMessage) => {
403
+ const transcript = {
404
+ id: message.id,
405
+ _getType: () => (message.role === "assistant" ? "ai" : "human"),
406
+ content: getThreadMessageText(message),
407
+ ...(message.metadata.modality && {
408
+ additional_kwargs: { modality: message.metadata.modality },
409
+ }),
410
+ };
411
+ stagedMessagesRef.current.set(transcript.id, {
412
+ message: transcript,
413
+ runConfig: undefined,
414
+ reconcileOnEcho: false,
415
+ baseMessageCount: streamRef.current.messages.length,
416
+ transcriptStatus: "unsent",
417
+ });
418
+ const nextMessages = [...visibleMessagesRef.current, transcript];
419
+ visibleMessagesRef.current = nextMessages;
420
+ setStagedMessages(nextMessages);
421
+ };
422
+
423
+ const getUnsentTranscripts = () =>
424
+ visibleMessagesRef.current.filter(
425
+ (message) =>
426
+ message.id !== undefined &&
427
+ stagedMessagesRef.current.get(message.id)?.transcriptStatus ===
428
+ "unsent",
429
+ );
430
+
431
+ const setTranscriptStatus = (
432
+ messages: readonly LangChainBaseMessage[],
433
+ status: "unsent" | "sent",
434
+ ) => {
435
+ for (const message of messages) {
436
+ const staged = message.id
437
+ ? stagedMessagesRef.current.get(message.id)
438
+ : undefined;
439
+ if (staged?.transcriptStatus) staged.transcriptStatus = status;
440
+ }
441
+ };
442
+
443
+ // Reserving before the submit keeps an overlapping submit from carrying the
444
+ // same transcript; a failed submit hands it back to the next run.
445
+ const submitCarryingTranscripts = async (
446
+ transcripts: readonly LangChainBaseMessage[],
447
+ submit: () => Promise<void>,
448
+ ) => {
449
+ setTranscriptStatus(transcripts, "sent");
450
+ try {
451
+ await submit();
452
+ } catch (error) {
453
+ setTranscriptStatus(transcripts, "unsent");
454
+ throw error;
455
+ }
456
+ };
457
+
458
+ const dropTranscripts = (messages: readonly LangChainBaseMessage[]) => {
459
+ for (const message of messages) {
460
+ if (
461
+ message.id &&
462
+ stagedMessagesRef.current.get(message.id)?.transcriptStatus
463
+ )
464
+ removeStagedMessage(message.id);
465
+ }
466
+ };
467
+
468
+ const isTranscriptMessage = (message: LangChainBaseMessage) => {
469
+ const staged = message.id
470
+ ? stagedMessagesRef.current.get(message.id)
471
+ : undefined;
472
+ if (staged?.transcriptStatus === "unsent") return true;
473
+ const type = getMessageType(message);
474
+ return (
475
+ (type === "human" || type === "ai") &&
476
+ getMessageModality(message.additional_kwargs) !== undefined
477
+ );
478
+ };
479
+
480
+ // A transcript reaches the graph in the same input as the message after it, so
481
+ // no checkpoint ends at one; a fork starts before the trailing transcripts.
482
+ const planForkTranscripts = (parentId: string | null) => {
483
+ const visible = visibleMessagesRef.current;
484
+ const parentIndex =
485
+ parentId == null ? -1 : visible.findIndex((m) => m.id === parentId);
486
+ if (parentId != null && parentIndex === -1)
487
+ return { forkParentId: parentId, transcripts: [], truncated: [] };
488
+
489
+ let start = parentIndex + 1;
490
+ while (start > 0 && isTranscriptMessage(visible[start - 1]!)) start--;
491
+ return {
492
+ forkParentId: start > 0 ? (visible[start - 1]!.id ?? null) : null,
493
+ transcripts: visible.slice(start, parentIndex + 1),
494
+ truncated: visible.slice(parentIndex + 1),
373
495
  };
374
496
  };
375
497
 
@@ -459,27 +581,32 @@ const useStreamThreadRuntime = (
459
581
  // longer holds appends on that barrier.
460
582
  try {
461
583
  const { externalId } = await aui.threadListItem.initialize();
462
- await streamRef.current.submit(
463
- {
464
- [messagesKey]: [
465
- ...cancellations,
466
- {
467
- id: stagedMessageId,
468
- type: "human",
469
- content,
470
- },
471
- ],
472
- },
473
- {
474
- ...runConfigToSubmitOptions(msg.runConfig),
475
- ...(externalId != null ? { threadId: externalId } : {}),
476
- },
584
+ const transcripts = getUnsentTranscripts();
585
+ await submitCarryingTranscripts(transcripts, () =>
586
+ streamRef.current.submit(
587
+ {
588
+ [messagesKey]: [
589
+ ...cancellations,
590
+ ...transcripts.map(toStagedMessageInput),
591
+ {
592
+ id: stagedMessageId,
593
+ type: "human",
594
+ content,
595
+ },
596
+ ],
597
+ },
598
+ {
599
+ ...runConfigToSubmitOptions(msg.runConfig),
600
+ ...(externalId != null ? { threadId: externalId } : {}),
601
+ },
602
+ ),
477
603
  );
478
604
  } catch (error) {
479
605
  removeStagedMessage(stagedMessageId);
480
606
  throw error;
481
607
  }
482
608
  },
609
+ onVoiceTranscript: appendVoiceTranscript,
483
610
  onAddToolResult: async ({
484
611
  messageId,
485
612
  toolCallId,
@@ -510,9 +637,18 @@ const useStreamThreadRuntime = (
510
637
  onReload: async (parentId, config) => {
511
638
  const stagedRun = getStagedRun(parentId);
512
639
  if (stagedRun) {
640
+ if (
641
+ config.sourceId &&
642
+ stagedMessagesRef.current.get(config.sourceId)?.transcriptStatus
643
+ )
644
+ removeStagedMessage(config.sourceId);
513
645
  const promotedIds = new Set<string>();
514
646
  for (const message of stagedRun.messages) {
515
- if (!message.id) continue;
647
+ if (
648
+ !message.id ||
649
+ stagedMessagesRef.current.get(message.id)?.transcriptStatus
650
+ )
651
+ continue;
516
652
  promotedIds.add(message.id);
517
653
  stagedMessagesRef.current.delete(message.id);
518
654
  }
@@ -528,15 +664,13 @@ const useStreamThreadRuntime = (
528
664
  }
529
665
  const runConfig = config.runConfig ?? stagedRun.runConfig;
530
666
  setActiveRunConfig(runConfig);
531
- await stream.submit(
532
- {
533
- [messagesKey]: stagedRun.messages.map((message) => ({
534
- id: message.id,
535
- type: "human",
536
- content: message.content,
537
- })),
538
- },
539
- runConfigToSubmitOptions(runConfig),
667
+ await submitCarryingTranscripts(stagedRun.messages, () =>
668
+ stream.submit(
669
+ {
670
+ [messagesKey]: stagedRun.messages.map(toStagedMessageInput),
671
+ },
672
+ runConfigToSubmitOptions(runConfig),
673
+ ),
540
674
  );
541
675
  return;
542
676
  }
@@ -544,21 +678,30 @@ const useStreamThreadRuntime = (
544
678
  const threadId = externalId;
545
679
  if (!threadId || parentId == null) return;
546
680
  const s = streamRef.current;
681
+ const fork = planForkTranscripts(parentId);
547
682
  const checkpointId = await resolveForkCheckpoint(
548
683
  s.client,
549
684
  threadId,
550
685
  s.messages as readonly LangChainBaseMessage[],
551
- parentId,
686
+ fork.forkParentId,
552
687
  config.sourceId,
553
688
  s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),
554
689
  messagesKey,
555
690
  );
556
691
  if (!checkpointId) return;
692
+ dropTranscripts(fork.truncated);
557
693
  setActiveRunConfig(config.runConfig);
558
- await s.submit(null, {
559
- forkFrom: checkpointId,
560
- ...runConfigToSubmitOptions(config.runConfig),
561
- });
694
+ await submitCarryingTranscripts(fork.transcripts, () =>
695
+ s.submit(
696
+ fork.transcripts.length > 0
697
+ ? { [messagesKey]: fork.transcripts.map(toStagedMessageInput) }
698
+ : null,
699
+ {
700
+ forkFrom: checkpointId,
701
+ ...runConfigToSubmitOptions(config.runConfig),
702
+ },
703
+ ),
704
+ );
562
705
  },
563
706
  onEdit: async (message) => {
564
707
  if (!(message.startRun ?? message.role === "user")) {
@@ -583,24 +726,33 @@ const useStreamThreadRuntime = (
583
726
  const threadId = externalId;
584
727
  if (!threadId) return;
585
728
  const s = streamRef.current;
729
+ const fork = planForkTranscripts(message.parentId);
586
730
  const checkpointId = await resolveForkCheckpoint(
587
731
  s.client,
588
732
  threadId,
589
733
  s.messages as readonly LangChainBaseMessage[],
590
- message.parentId,
734
+ fork.forkParentId,
591
735
  message.sourceId,
592
736
  s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),
593
737
  messagesKey,
594
738
  );
595
739
  if (!checkpointId) return;
740
+ dropTranscripts(fork.truncated);
596
741
  const content = getMessageContent(message);
597
742
  setActiveRunConfig(message.runConfig);
598
- await s.submit(
599
- { [messagesKey]: [{ type: "human", content }] },
600
- {
601
- forkFrom: checkpointId,
602
- ...runConfigToSubmitOptions(message.runConfig),
603
- },
743
+ await submitCarryingTranscripts(fork.transcripts, () =>
744
+ s.submit(
745
+ {
746
+ [messagesKey]: [
747
+ ...fork.transcripts.map(toStagedMessageInput),
748
+ { type: "human", content },
749
+ ],
750
+ },
751
+ {
752
+ forkFrom: checkpointId,
753
+ ...runConfigToSubmitOptions(message.runConfig),
754
+ },
755
+ ),
604
756
  );
605
757
  },
606
758
  onCancel: