@assistant-ui/react-langchain 0.0.35 → 0.0.36

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 (36) hide show
  1. package/dist/LangChainThreadController.d.ts +9 -0
  2. package/dist/LangChainThreadController.d.ts.map +1 -0
  3. package/dist/LangChainThreadController.js +21 -0
  4. package/dist/LangChainThreadController.js.map +1 -0
  5. package/dist/convertMessages.d.ts.map +1 -1
  6. package/dist/convertMessages.js +6 -11
  7. package/dist/convertMessages.js.map +1 -1
  8. package/dist/converter.d.ts +24 -1
  9. package/dist/converter.d.ts.map +1 -1
  10. package/dist/converter.js +84 -18
  11. package/dist/converter.js.map +1 -1
  12. package/dist/langChainThreadState.d.ts +54 -0
  13. package/dist/langChainThreadState.d.ts.map +1 -0
  14. package/dist/langChainThreadState.js +135 -0
  15. package/dist/langChainThreadState.js.map +1 -0
  16. package/dist/sdkIdentity.js +1 -1
  17. package/dist/types.d.ts +9 -0
  18. package/dist/types.d.ts.map +1 -1
  19. package/dist/useStreamRuntime.d.ts +3 -2
  20. package/dist/useStreamRuntime.d.ts.map +1 -1
  21. package/dist/useStreamRuntime.js +152 -139
  22. package/dist/useStreamRuntime.js.map +1 -1
  23. package/dist/useSubagentTranscripts.js +1 -1
  24. package/package.json +8 -8
  25. package/src/LangChainThreadController.ts +24 -0
  26. package/src/convertMessages.test.ts +23 -0
  27. package/src/convertMessages.ts +9 -16
  28. package/src/converter.test.ts +322 -0
  29. package/src/converter.ts +138 -27
  30. package/src/getPendingToolCalls.test.ts +35 -0
  31. package/src/langChainThreadState.test.ts +203 -0
  32. package/src/langChainThreadState.ts +239 -0
  33. package/src/types.ts +9 -0
  34. package/src/useStreamRuntime.cloud-options.test.tsx +61 -0
  35. package/src/useStreamRuntime.test.tsx +227 -1
  36. package/src/useStreamRuntime.ts +210 -239
@@ -8,6 +8,7 @@ import {
8
8
  useMemo,
9
9
  useRef,
10
10
  useState,
11
+ useSyncExternalStore,
11
12
  } from "react";
12
13
  import type { AppendMessage, ToolExecutionStatus } from "@assistant-ui/core";
13
14
  import {
@@ -62,6 +63,8 @@ import { langChainExtras } from "./runtimeExtras";
62
63
  import { resolveForkCheckpoint } from "./resolveForkCheckpoint";
63
64
  import { useLangChainStreamingTiming } from "./streamingTiming";
64
65
  import { LANGCHAIN_SDK } from "./sdkIdentity";
66
+ import { LangChainThreadController } from "./LangChainThreadController";
67
+ import type { LangChainThreadAction } from "./langChainThreadState";
65
68
 
66
69
  export const runConfigToSubmitOptions = (
67
70
  runConfig: AppendMessage["runConfig"],
@@ -74,14 +77,20 @@ type NormalizedRunConfigOptions = NonNullable<
74
77
  ReturnType<typeof runConfigToSubmitOptions>
75
78
  >;
76
79
 
77
- const getPendingToolCalls = (
80
+ export const getPendingToolCalls = (
78
81
  messages: readonly LangChainBaseMessage[],
79
82
  ): LangChainToolCall[] =>
80
83
  scanPendingToolCalls(
81
84
  messages,
82
85
  (message) => {
83
86
  const type = getMessageType(message);
84
- if (type === "ai") return { toolCalls: message.tool_calls ?? [] };
87
+ if (type === "ai") {
88
+ return {
89
+ toolCalls: (message.tool_calls ?? []).filter(
90
+ (toolCall) => typeof toolCall === "object" && toolCall !== null,
91
+ ),
92
+ };
93
+ }
85
94
  if (type === "tool" && message.tool_call_id) {
86
95
  return { toolCallId: message.tool_call_id };
87
96
  }
@@ -108,26 +117,6 @@ const toStagedMessageInput = (message: LangChainBaseMessage) => ({
108
117
  }),
109
118
  });
110
119
 
111
- const humanContentText = (content: LangChainBaseMessage["content"]) => {
112
- if (typeof content === "string") return content;
113
- if (!Array.isArray(content)) return "";
114
- return content
115
- .filter(
116
- (part): part is { type: "text"; text: string } =>
117
- typeof part === "object" &&
118
- part !== null &&
119
- part.type === "text" &&
120
- typeof part.text === "string",
121
- )
122
- .map((part) => part.text)
123
- .join("");
124
- };
125
-
126
- const hasSameMessageContent = (
127
- a: LangChainBaseMessage,
128
- b: LangChainBaseMessage,
129
- ) => humanContentText(a.content) === humanContentText(b.content);
130
-
131
120
  const truncateLangChainBaseMessages = (
132
121
  threadMessages: readonly ThreadMessage[],
133
122
  parentId: string | null,
@@ -151,7 +140,7 @@ type DistributiveOmit<T, K extends keyof any> = T extends unknown
151
140
  const useStreamThreadRuntime = (
152
141
  options: DistributiveOmit<
153
142
  UseStreamRuntimeOptions,
154
- "cloud" | "unstable_threadListAdapter" | "create" | "delete"
143
+ "cloud" | "scopeId" | "unstable_threadListAdapter" | "create" | "delete"
155
144
  >,
156
145
  ) => {
157
146
  const { adapters, autoCancelPendingToolCalls, unstable_allowCancellation } =
@@ -168,9 +157,19 @@ const useStreamThreadRuntime = (
168
157
  const stream = useStream(
169
158
  Object.assign({}, options, { threadId: externalId }),
170
159
  );
171
- const [stagedMessages, setStagedMessages] = useState<
172
- LangChainBaseMessage[] | null
173
- >(null);
160
+ const [threadController] = useState(() => new LangChainThreadController());
161
+ const getVisibleStagedMessages = () =>
162
+ threadController.getState().visibleStagedMessages;
163
+ const visibleStagedMessages = useSyncExternalStore(
164
+ threadController.subscribe,
165
+ getVisibleStagedMessages,
166
+ getVisibleStagedMessages,
167
+ );
168
+ const forkPending = useSyncExternalStore(
169
+ threadController.subscribe,
170
+ () => threadController.getState().forkPending,
171
+ () => threadController.getState().forkPending,
172
+ );
174
173
 
175
174
  const [toolStatuses, setToolStatuses] = useState<
176
175
  Record<string, ToolExecutionStatus>
@@ -204,7 +203,7 @@ const useStreamThreadRuntime = (
204
203
  );
205
204
 
206
205
  const visibleMessages =
207
- stagedMessages ?? (stream.messages as LangChainBaseMessage[]);
206
+ visibleStagedMessages ?? (stream.messages as LangChainBaseMessage[]);
208
207
 
209
208
  const messageTiming = useLangChainStreamingTiming(
210
209
  visibleMessages,
@@ -309,85 +308,42 @@ const useStreamThreadRuntime = (
309
308
  visibleMessagesRef.current = visibleMessages;
310
309
  }, [visibleMessages]);
311
310
 
311
+ const dispatchStaging = useCallback(
312
+ (action: LangChainThreadAction, fallback = visibleMessagesRef.current) => {
313
+ threadController.dispatch(action);
314
+ visibleMessagesRef.current =
315
+ threadController.getState().visibleStagedMessages ?? fallback;
316
+ },
317
+ [threadController],
318
+ );
319
+
312
320
  const threadMessagesRef = useRef(messagesWithTranscripts);
313
321
  useInsertionEffect(() => {
314
322
  threadMessagesRef.current = messagesWithTranscripts;
315
323
  }, [messagesWithTranscripts]);
316
324
 
317
- const stagedMessagesRef = useRef(
318
- new Map<
319
- string,
320
- {
321
- message: LangChainBaseMessage & { id: string };
322
- runConfig: AppendMessage["runConfig"];
323
- reconcileOnEcho: boolean;
324
- baseMessageCount: number;
325
- transcriptStatus?: "unsent" | "sent";
326
- }
327
- >(),
328
- );
329
- const stagedBaseMessagesRef = useRef<LangChainBaseMessage[] | null>(null);
330
325
  useEffect(() => {
331
- if (stagedMessagesRef.current.size === 0) return;
332
-
333
- // Staged edits must keep their truncated base while stream updates arrive before promotion.
334
- const baseMessages =
335
- stagedBaseMessagesRef.current ??
336
- (stream.messages as LangChainBaseMessage[]);
337
- const remainingStagedMessages: LangChainBaseMessage[] = [];
338
- const matchedBaseMessageIndexes = new Set<number>();
339
- const visibleStagedIds = new Set(
340
- visibleMessagesRef.current.flatMap((m) => (m.id ? [m.id] : [])),
326
+ const state = threadController.getState();
327
+ if (state.stagedEntries.size === 0) return;
328
+ const messages = stream.messages as LangChainBaseMessage[];
329
+ dispatchStaging(
330
+ {
331
+ type: "reconcile",
332
+ messages,
333
+ visibleMessages: visibleMessagesRef.current,
334
+ },
335
+ state.stagedBaseMessages ?? messages,
341
336
  );
342
- for (const [id, staged] of stagedMessagesRef.current) {
343
- if (!visibleStagedIds.has(id)) continue;
344
- const echoed = baseMessages.some((message, index) => {
345
- if (matchedBaseMessageIndexes.has(index)) return false;
346
- if (message.id === id) {
347
- matchedBaseMessageIndexes.add(index);
348
- return true;
349
- }
350
- if (
351
- !staged.reconcileOnEcho ||
352
- index < staged.baseMessageCount ||
353
- getMessageType(message) !== "human" ||
354
- !hasSameMessageContent(message, staged.message)
355
- ) {
356
- return false;
357
- }
358
- matchedBaseMessageIndexes.add(index);
359
- return true;
360
- });
361
- if (echoed) stagedMessagesRef.current.delete(id);
362
- else remainingStagedMessages.push(staged.message);
363
- }
364
-
365
- if (remainingStagedMessages.length === 0) {
366
- stagedBaseMessagesRef.current = null;
367
- visibleMessagesRef.current = baseMessages;
368
- // Reconciling against the upstream stream mutates the staged refs above,
369
- // which cannot happen during render.
370
- // eslint-disable-next-line react-hooks/set-state-in-effect
371
- setStagedMessages(null);
372
- return;
373
- }
374
-
375
- const nextMessages = [...baseMessages, ...remainingStagedMessages];
376
- visibleMessagesRef.current = nextMessages;
377
- setStagedMessages(nextMessages);
378
- }, [stream.messages]);
337
+ }, [stream.messages, threadController, dispatchStaging]);
379
338
 
380
339
  const getStagedRun = (parentId: string | null) => {
381
- const parent = parentId
382
- ? stagedMessagesRef.current.get(parentId)
383
- : undefined;
340
+ const stagedEntries = threadController.getState().stagedEntries;
341
+ const parent = parentId ? stagedEntries.get(parentId) : undefined;
384
342
  if (!parent || parent.transcriptStatus === "sent") return null;
385
343
 
386
344
  const staged: LangChainBaseMessage[] = [];
387
345
  for (const message of visibleMessagesRef.current) {
388
- const entry = message.id
389
- ? stagedMessagesRef.current.get(message.id)
390
- : undefined;
346
+ const entry = message.id ? stagedEntries.get(message.id) : undefined;
391
347
  if (entry && entry.transcriptStatus !== "sent") {
392
348
  staged.push(entry.message);
393
349
  }
@@ -409,36 +365,33 @@ const useStreamThreadRuntime = (
409
365
  additional_kwargs: { modality: message.metadata.modality },
410
366
  }),
411
367
  };
412
- stagedMessagesRef.current.set(transcript.id, {
413
- message: transcript,
414
- runConfig: undefined,
415
- reconcileOnEcho: false,
416
- baseMessageCount: streamRef.current.messages.length,
417
- transcriptStatus: "unsent",
368
+ dispatchStaging({
369
+ type: "stage",
370
+ entry: {
371
+ message: transcript,
372
+ runConfig: undefined,
373
+ reconcileOnEcho: false,
374
+ baseMessageCount: streamRef.current.messages.length,
375
+ transcriptStatus: "unsent",
376
+ },
377
+ visibleMessages: visibleMessagesRef.current,
418
378
  });
419
- const nextMessages = [...visibleMessagesRef.current, transcript];
420
- visibleMessagesRef.current = nextMessages;
421
- setStagedMessages(nextMessages);
422
379
  };
423
380
 
424
- const getUnsentTranscripts = () =>
425
- visibleMessagesRef.current.filter(
381
+ const getUnsentTranscripts = () => {
382
+ const stagedEntries = threadController.getState().stagedEntries;
383
+ return visibleMessagesRef.current.filter(
426
384
  (message) =>
427
385
  message.id !== undefined &&
428
- stagedMessagesRef.current.get(message.id)?.transcriptStatus ===
429
- "unsent",
386
+ stagedEntries.get(message.id)?.transcriptStatus === "unsent",
430
387
  );
388
+ };
431
389
 
432
390
  const setTranscriptStatus = (
433
391
  messages: readonly LangChainBaseMessage[],
434
392
  status: "unsent" | "sent",
435
393
  ) => {
436
- for (const message of messages) {
437
- const staged = message.id
438
- ? stagedMessagesRef.current.get(message.id)
439
- : undefined;
440
- if (staged?.transcriptStatus) staged.transcriptStatus = status;
441
- }
394
+ dispatchStaging({ type: "markTranscript", messages, status });
442
395
  };
443
396
 
444
397
  // Reserving before the submit keeps an overlapping submit from carrying the
@@ -460,7 +413,8 @@ const useStreamThreadRuntime = (
460
413
  for (const message of messages) {
461
414
  if (
462
415
  message.id &&
463
- stagedMessagesRef.current.get(message.id)?.transcriptStatus
416
+ threadController.getState().stagedEntries.get(message.id)
417
+ ?.transcriptStatus
464
418
  )
465
419
  removeStagedMessage(message.id);
466
420
  }
@@ -468,7 +422,7 @@ const useStreamThreadRuntime = (
468
422
 
469
423
  const isTranscriptMessage = (message: LangChainBaseMessage) => {
470
424
  const staged = message.id
471
- ? stagedMessagesRef.current.get(message.id)
425
+ ? threadController.getState().stagedEntries.get(message.id)
472
426
  : undefined;
473
427
  if (staged?.transcriptStatus === "unsent") return true;
474
428
  const type = getMessageType(message);
@@ -498,30 +452,32 @@ const useStreamThreadRuntime = (
498
452
 
499
453
  const stageUserMessage = (msg: AppendMessage, reconcileOnEcho = false) => {
500
454
  const stagedMessage = toStagedHumanMessage(msg);
501
- stagedMessagesRef.current.set(stagedMessage.id, {
502
- message: stagedMessage,
503
- runConfig: msg.runConfig,
504
- reconcileOnEcho,
505
- baseMessageCount: streamRef.current.messages.length,
455
+ dispatchStaging({
456
+ type: "stage",
457
+ entry: {
458
+ message: stagedMessage,
459
+ runConfig: msg.runConfig,
460
+ reconcileOnEcho,
461
+ baseMessageCount: streamRef.current.messages.length,
462
+ },
463
+ visibleMessages: visibleMessagesRef.current,
506
464
  });
507
- const nextMessages = [...visibleMessagesRef.current, stagedMessage];
508
- visibleMessagesRef.current = nextMessages;
509
- setStagedMessages(nextMessages);
510
465
  return stagedMessage;
511
466
  };
512
467
 
513
468
  const removeStagedMessage = (id: string) => {
514
- if (!stagedMessagesRef.current.delete(id)) return;
515
- const nextMessages = visibleMessagesRef.current.filter(
516
- (message) => message.id !== id,
469
+ if (!threadController.getState().stagedEntries.has(id)) return;
470
+ const visible = visibleMessagesRef.current;
471
+ const nextMessages = visible.filter((message) => message.id !== id);
472
+ dispatchStaging(
473
+ { type: "remove", id, visibleMessages: visible },
474
+ nextMessages,
517
475
  );
518
- visibleMessagesRef.current = nextMessages;
519
- if (stagedMessagesRef.current.size === 0) {
520
- stagedBaseMessagesRef.current = null;
521
- setStagedMessages(null);
522
- } else {
523
- setStagedMessages(nextMessages);
524
- }
476
+ };
477
+
478
+ const supersedeFork = () => {
479
+ threadController.dispatch({ type: "supersedeFork" });
480
+ return threadController.getState().forkGeneration;
525
481
  };
526
482
 
527
483
  const extras = useMemo(
@@ -553,7 +509,7 @@ const useStreamThreadRuntime = (
553
509
 
554
510
  const runtime = useExternalStoreRuntime({
555
511
  ...pickExternalStoreSharedOptions(options),
556
- isRunning: stream.isLoading,
512
+ isRunning: stream.isLoading || forkPending,
557
513
  isLoading: stream.isThreadLoading,
558
514
  messages: messagesWithTranscripts,
559
515
  adapters,
@@ -639,129 +595,142 @@ const useStreamThreadRuntime = (
639
595
  );
640
596
  },
641
597
  onReload: async (parentId, config) => {
642
- const stagedRun = getStagedRun(parentId);
643
- if (stagedRun) {
644
- if (
645
- config.sourceId &&
646
- stagedMessagesRef.current.get(config.sourceId)?.transcriptStatus
647
- )
648
- removeStagedMessage(config.sourceId);
649
- const promotedIds = new Set<string>();
650
- for (const message of stagedRun.messages) {
598
+ const forkGeneration = supersedeFork();
599
+ try {
600
+ await streamRef.current.stop();
601
+ if (threadController.getState().forkGeneration !== forkGeneration)
602
+ return;
603
+ const stagedRun = getStagedRun(parentId);
604
+ if (stagedRun) {
651
605
  if (
652
- !message.id ||
653
- stagedMessagesRef.current.get(message.id)?.transcriptStatus
606
+ config.sourceId &&
607
+ threadController.getState().stagedEntries.get(config.sourceId)
608
+ ?.transcriptStatus
654
609
  )
655
- continue;
656
- promotedIds.add(message.id);
657
- stagedMessagesRef.current.delete(message.id);
658
- }
659
- stagedBaseMessagesRef.current = null;
660
- if (stagedMessagesRef.current.size > 0) {
661
- const nextMessages = visibleMessagesRef.current.filter(
662
- (message) => !message.id || !promotedIds.has(message.id),
610
+ removeStagedMessage(config.sourceId);
611
+ dispatchStaging({
612
+ type: "promote",
613
+ messages: stagedRun.messages,
614
+ visibleMessages: visibleMessagesRef.current,
615
+ });
616
+ const runConfig = config.runConfig ?? stagedRun.runConfig;
617
+ setActiveRunConfig(runConfig);
618
+ await submitCarryingTranscripts(stagedRun.messages, () =>
619
+ stream.submit(
620
+ {
621
+ [messagesKey]: stagedRun.messages.map(toStagedMessageInput),
622
+ },
623
+ runConfigToSubmitOptions(runConfig),
624
+ ),
663
625
  );
664
- visibleMessagesRef.current = nextMessages;
665
- setStagedMessages(nextMessages);
666
- } else {
667
- setStagedMessages(null);
626
+ return;
668
627
  }
669
- const runConfig = config.runConfig ?? stagedRun.runConfig;
670
- setActiveRunConfig(runConfig);
671
- await submitCarryingTranscripts(stagedRun.messages, () =>
672
- stream.submit(
628
+
629
+ const threadId = externalId;
630
+ if (!threadId || parentId == null) return;
631
+ const s = streamRef.current;
632
+ const fork = planForkTranscripts(parentId);
633
+ const checkpointId = await resolveForkCheckpoint(
634
+ s.client,
635
+ threadId,
636
+ s.messages as readonly LangChainBaseMessage[],
637
+ fork.forkParentId,
638
+ config.sourceId,
639
+ s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),
640
+ messagesKey,
641
+ );
642
+ if (threadController.getState().forkGeneration !== forkGeneration)
643
+ return;
644
+ if (!checkpointId) return;
645
+ dropTranscripts(fork.truncated);
646
+ setActiveRunConfig(config.runConfig);
647
+ await submitCarryingTranscripts(fork.transcripts, () =>
648
+ s.submit(
649
+ fork.transcripts.length > 0
650
+ ? { [messagesKey]: fork.transcripts.map(toStagedMessageInput) }
651
+ : null,
673
652
  {
674
- [messagesKey]: stagedRun.messages.map(toStagedMessageInput),
653
+ forkFrom: checkpointId,
654
+ ...runConfigToSubmitOptions(config.runConfig),
675
655
  },
676
- runConfigToSubmitOptions(runConfig),
677
656
  ),
678
657
  );
679
- return;
658
+ } finally {
659
+ threadController.dispatch({
660
+ type: "finishFork",
661
+ generation: forkGeneration,
662
+ });
680
663
  }
681
-
682
- const threadId = externalId;
683
- if (!threadId || parentId == null) return;
684
- const s = streamRef.current;
685
- const fork = planForkTranscripts(parentId);
686
- const checkpointId = await resolveForkCheckpoint(
687
- s.client,
688
- threadId,
689
- s.messages as readonly LangChainBaseMessage[],
690
- fork.forkParentId,
691
- config.sourceId,
692
- s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),
693
- messagesKey,
694
- );
695
- if (!checkpointId) return;
696
- dropTranscripts(fork.truncated);
697
- setActiveRunConfig(config.runConfig);
698
- await submitCarryingTranscripts(fork.transcripts, () =>
699
- s.submit(
700
- fork.transcripts.length > 0
701
- ? { [messagesKey]: fork.transcripts.map(toStagedMessageInput) }
702
- : null,
703
- {
704
- forkFrom: checkpointId,
705
- ...runConfigToSubmitOptions(config.runConfig),
706
- },
707
- ),
708
- );
709
664
  },
710
665
  onEdit: async (message) => {
711
- if (!(message.startRun ?? message.role === "user")) {
712
- const truncated = truncateLangChainBaseMessages(
713
- threadMessagesRef.current,
714
- message.parentId,
666
+ const forkGeneration = supersedeFork();
667
+ try {
668
+ await streamRef.current.stop();
669
+ if (threadController.getState().forkGeneration !== forkGeneration)
670
+ return;
671
+ if (!(message.startRun ?? message.role === "user")) {
672
+ const truncated = truncateLangChainBaseMessages(
673
+ threadMessagesRef.current,
674
+ message.parentId,
675
+ );
676
+ const stagedMessage = toStagedHumanMessage(message);
677
+ dispatchStaging({
678
+ type: "stageEdit",
679
+ entry: {
680
+ message: stagedMessage,
681
+ runConfig: message.runConfig,
682
+ reconcileOnEcho: false,
683
+ baseMessageCount: 0,
684
+ },
685
+ baseMessages: truncated,
686
+ });
687
+ return;
688
+ }
689
+
690
+ const threadId = externalId;
691
+ if (!threadId) return;
692
+ const s = streamRef.current;
693
+ const fork = planForkTranscripts(message.parentId);
694
+ const checkpointId = await resolveForkCheckpoint(
695
+ s.client,
696
+ threadId,
697
+ s.messages as readonly LangChainBaseMessage[],
698
+ fork.forkParentId,
699
+ message.sourceId,
700
+ s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),
701
+ messagesKey,
702
+ );
703
+ if (threadController.getState().forkGeneration !== forkGeneration)
704
+ return;
705
+ if (!checkpointId) return;
706
+ dropTranscripts(fork.truncated);
707
+ const content = getMessageContent(message);
708
+ setActiveRunConfig(message.runConfig);
709
+ await submitCarryingTranscripts(fork.transcripts, () =>
710
+ s.submit(
711
+ {
712
+ [messagesKey]: [
713
+ ...fork.transcripts.map(toStagedMessageInput),
714
+ { type: "human", content },
715
+ ],
716
+ },
717
+ {
718
+ forkFrom: checkpointId,
719
+ ...runConfigToSubmitOptions(message.runConfig),
720
+ },
721
+ ),
715
722
  );
716
- const stagedMessage = toStagedHumanMessage(message);
717
- stagedMessagesRef.current.set(stagedMessage.id, {
718
- message: stagedMessage,
719
- runConfig: message.runConfig,
720
- reconcileOnEcho: false,
721
- baseMessageCount: 0,
723
+ } finally {
724
+ threadController.dispatch({
725
+ type: "finishFork",
726
+ generation: forkGeneration,
722
727
  });
723
- stagedBaseMessagesRef.current = truncated;
724
- const nextMessages = [...truncated, stagedMessage];
725
- visibleMessagesRef.current = nextMessages;
726
- setStagedMessages(nextMessages);
727
- return;
728
728
  }
729
-
730
- const threadId = externalId;
731
- if (!threadId) return;
732
- const s = streamRef.current;
733
- const fork = planForkTranscripts(message.parentId);
734
- const checkpointId = await resolveForkCheckpoint(
735
- s.client,
736
- threadId,
737
- s.messages as readonly LangChainBaseMessage[],
738
- fork.forkParentId,
739
- message.sourceId,
740
- s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),
741
- messagesKey,
742
- );
743
- if (!checkpointId) return;
744
- dropTranscripts(fork.truncated);
745
- const content = getMessageContent(message);
746
- setActiveRunConfig(message.runConfig);
747
- await submitCarryingTranscripts(fork.transcripts, () =>
748
- s.submit(
749
- {
750
- [messagesKey]: [
751
- ...fork.transcripts.map(toStagedMessageInput),
752
- { type: "human", content },
753
- ],
754
- },
755
- {
756
- forkFrom: checkpointId,
757
- ...runConfigToSubmitOptions(message.runConfig),
758
- },
759
- ),
760
- );
761
729
  },
762
730
  onCancel:
763
731
  unstable_allowCancellation !== false
764
732
  ? async () => {
733
+ threadController.dispatch({ type: "cancelFork" });
765
734
  activeRunConfigRef.current = undefined;
766
735
  await stream.stop();
767
736
  }
@@ -774,7 +743,7 @@ const useStreamThreadRuntime = (
774
743
  /**
775
744
  * Creates an assistant-ui runtime backed by LangChain's `useStream` hook.
776
745
  * Accepts the same options as `useStream` from `@langchain/react`, plus
777
- * `cloud` and `adapters`.
746
+ * `cloud`, `scopeId`, and `adapters`.
778
747
  *
779
748
  * @example
780
749
  * ```tsx
@@ -798,6 +767,7 @@ const useStreamThreadRuntime = (
798
767
  export const useStreamRuntime = (rawOptions: UseStreamRuntimeOptions) => {
799
768
  const {
800
769
  cloud,
770
+ scopeId,
801
771
  unstable_threadListAdapter,
802
772
  create,
803
773
  delete: deleteFn,
@@ -809,6 +779,7 @@ export const useStreamRuntime = (rawOptions: UseStreamRuntimeOptions) => {
809
779
  const cloudAdapter = useCloudThreadListAdapter({
810
780
  sdk: LANGCHAIN_SDK,
811
781
  cloud,
782
+ scopeId,
812
783
  create: createCloudThreadListAdapterCreateFallback(
813
784
  create,
814
785
  aui.threadListItem,