@assistant-ui/react-langchain 0.0.32 → 0.0.34

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 (59) hide show
  1. package/LICENSE +1 -1
  2. package/dist/attachSubagentTranscripts.d.ts +13 -14
  3. package/dist/attachSubagentTranscripts.d.ts.map +1 -1
  4. package/dist/convertMessages.d.ts +6 -9
  5. package/dist/convertMessages.d.ts.map +1 -1
  6. package/dist/convertMessages.js +57 -15
  7. package/dist/convertMessages.js.map +1 -1
  8. package/dist/converter.d.ts +128 -128
  9. package/dist/converter.d.ts.map +1 -1
  10. package/dist/converter.js +2 -1
  11. package/dist/converter.js.map +1 -1
  12. package/dist/findForkCheckpointInHistory.d.ts +15 -16
  13. package/dist/findForkCheckpointInHistory.d.ts.map +1 -1
  14. package/dist/hooks.d.ts +5 -7
  15. package/dist/hooks.d.ts.map +1 -1
  16. package/dist/index.d.ts +7 -8
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/resolveForkCheckpoint.d.ts +4 -5
  19. package/dist/resolveForkCheckpoint.d.ts.map +1 -1
  20. package/dist/runtimeExtras.d.ts +1 -3
  21. package/dist/runtimeExtras.d.ts.map +1 -1
  22. package/dist/sdkIdentity.d.ts +1 -3
  23. package/dist/sdkIdentity.d.ts.map +1 -1
  24. package/dist/sdkIdentity.js +1 -1
  25. package/dist/streamingTiming.d.ts +2 -4
  26. package/dist/streamingTiming.d.ts.map +1 -1
  27. package/dist/subagentMessagesProjection.d.ts +17 -0
  28. package/dist/subagentMessagesProjection.d.ts.map +1 -0
  29. package/dist/subagentMessagesProjection.js +40 -0
  30. package/dist/subagentMessagesProjection.js.map +1 -0
  31. package/dist/types.d.ts +100 -103
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/uiMessages.d.ts +18 -6
  34. package/dist/uiMessages.d.ts.map +1 -1
  35. package/dist/uiMessages.js +33 -1
  36. package/dist/uiMessages.js.map +1 -1
  37. package/dist/useStreamRuntime.d.ts +7 -10
  38. package/dist/useStreamRuntime.d.ts.map +1 -1
  39. package/dist/useStreamRuntime.js +100 -26
  40. package/dist/useStreamRuntime.js.map +1 -1
  41. package/dist/useSubagentTranscripts.d.ts +3 -5
  42. package/dist/useSubagentTranscripts.d.ts.map +1 -1
  43. package/dist/useSubagentTranscripts.js +5 -3
  44. package/dist/useSubagentTranscripts.js.map +1 -1
  45. package/package.json +16 -9
  46. package/src/convertMessages.test.ts +153 -0
  47. package/src/convertMessages.ts +54 -8
  48. package/src/converter.ts +6 -0
  49. package/src/subagentMessagesProjection.test.ts +175 -0
  50. package/src/subagentMessagesProjection.ts +50 -0
  51. package/src/tests/settleOutsideAct.ts +15 -0
  52. package/src/uiMessages.test.ts +106 -0
  53. package/src/uiMessages.ts +43 -0
  54. package/src/useStreamRuntime.test.tsx +147 -7
  55. package/src/useStreamRuntime.ts +197 -45
  56. package/src/useStreamRuntime.voice.test.tsx +713 -0
  57. package/src/useSubagentTranscripts.fast-refresh.test.tsx +152 -0
  58. package/src/useSubagentTranscripts.test.tsx +2 -0
  59. package/src/useSubagentTranscripts.ts +5 -7
@@ -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 {
@@ -35,7 +36,7 @@ import type {
35
36
  UIMessage,
36
37
  UseStreamRuntimeOptions,
37
38
  } from "./types";
38
- import { groupUIMessagesByParent } from "./converter";
39
+ import { getMessageModality, groupUIMessagesByParent } from "./converter";
39
40
  export { groupUIMessagesByParent } from "./converter";
40
41
  import {
41
42
  convertLangChainBaseMessage,
@@ -49,8 +50,10 @@ import {
49
50
  import { useSubagentTranscripts } from "./useSubagentTranscripts";
50
51
  import {
51
52
  createUIFoldMemo,
53
+ createUISnapshotMemo,
52
54
  foldUIUpdates,
53
55
  mergeUIMessages,
56
+ reconcileUISnapshot,
54
57
  UI_CUSTOM_CHANNELS,
55
58
  } from "./uiMessages";
56
59
  import { langChainExtras } from "./runtimeExtras";
@@ -94,6 +97,15 @@ const toStagedHumanMessage = (
94
97
  content: getMessageContent(msg),
95
98
  });
96
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
+
97
109
  const humanContentText = (content: LangChainBaseMessage["content"]) => {
98
110
  if (typeof content === "string") return content;
99
111
  if (!Array.isArray(content)) return "";
@@ -166,7 +178,11 @@ const useStreamThreadRuntime = (
166
178
  );
167
179
  const effectiveIsRunning = stream.isLoading || hasExecutingTools;
168
180
 
169
- const uiStateValue = stream.values[uiStateKey];
181
+ const [uiSnapshotMemo] = useState(createUISnapshotMemo);
182
+ const uiStateValue = reconcileUISnapshot(
183
+ stream.values[uiStateKey],
184
+ uiSnapshotMemo,
185
+ );
170
186
 
171
187
  const customEvents = useChannel(stream, UI_CUSTOM_CHANNELS);
172
188
  const [uiFoldMemo] = useState(createUIFoldMemo);
@@ -305,6 +321,7 @@ const useStreamThreadRuntime = (
305
321
  runConfig: AppendMessage["runConfig"];
306
322
  reconcileOnEcho: boolean;
307
323
  baseMessageCount: number;
324
+ transcriptStatus?: "unsent" | "sent";
308
325
  }
309
326
  >(),
310
327
  );
@@ -360,19 +377,121 @@ const useStreamThreadRuntime = (
360
377
  }, [stream.messages]);
361
378
 
362
379
  const getStagedRun = (parentId: string | null) => {
363
- 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;
364
384
 
365
385
  const staged: LangChainBaseMessage[] = [];
366
386
  for (const message of visibleMessagesRef.current) {
367
- if (message.id && stagedMessagesRef.current.has(message.id)) {
368
- 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);
369
392
  }
370
393
  if (message.id === parentId) break;
371
394
  }
372
395
 
373
396
  return {
374
397
  messages: staged,
375
- 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),
376
495
  };
377
496
  };
378
497
 
@@ -454,7 +573,10 @@ const useStreamThreadRuntime = (
454
573
  autoCancelPendingToolCalls !== false
455
574
  ? getPendingToolCalls(
456
575
  streamRef.current.messages as readonly LangChainBaseMessage[],
457
- ).map(createToolCallCancellationStub)
576
+ )
577
+ // LangChain rejects a tool message without a tool_call_id.
578
+ .filter((toolCall) => toolCall.id)
579
+ .map(createToolCallCancellationStub)
458
580
  : [];
459
581
  // A null threadId is not a no-op for the SDK: it rebinds the controller
460
582
  // away from its self-created thread and forces a fresh one, so the
@@ -462,27 +584,32 @@ const useStreamThreadRuntime = (
462
584
  // longer holds appends on that barrier.
463
585
  try {
464
586
  const { externalId } = await aui.threadListItem.initialize();
465
- await streamRef.current.submit(
466
- {
467
- [messagesKey]: [
468
- ...cancellations,
469
- {
470
- id: stagedMessageId,
471
- type: "human",
472
- content,
473
- },
474
- ],
475
- },
476
- {
477
- ...runConfigToSubmitOptions(msg.runConfig),
478
- ...(externalId != null ? { threadId: externalId } : {}),
479
- },
587
+ const transcripts = getUnsentTranscripts();
588
+ await submitCarryingTranscripts(transcripts, () =>
589
+ streamRef.current.submit(
590
+ {
591
+ [messagesKey]: [
592
+ ...cancellations,
593
+ ...transcripts.map(toStagedMessageInput),
594
+ {
595
+ id: stagedMessageId,
596
+ type: "human",
597
+ content,
598
+ },
599
+ ],
600
+ },
601
+ {
602
+ ...runConfigToSubmitOptions(msg.runConfig),
603
+ ...(externalId != null ? { threadId: externalId } : {}),
604
+ },
605
+ ),
480
606
  );
481
607
  } catch (error) {
482
608
  removeStagedMessage(stagedMessageId);
483
609
  throw error;
484
610
  }
485
611
  },
612
+ onVoiceTranscript: appendVoiceTranscript,
486
613
  onAddToolResult: async ({
487
614
  messageId,
488
615
  toolCallId,
@@ -513,9 +640,18 @@ const useStreamThreadRuntime = (
513
640
  onReload: async (parentId, config) => {
514
641
  const stagedRun = getStagedRun(parentId);
515
642
  if (stagedRun) {
643
+ if (
644
+ config.sourceId &&
645
+ stagedMessagesRef.current.get(config.sourceId)?.transcriptStatus
646
+ )
647
+ removeStagedMessage(config.sourceId);
516
648
  const promotedIds = new Set<string>();
517
649
  for (const message of stagedRun.messages) {
518
- if (!message.id) continue;
650
+ if (
651
+ !message.id ||
652
+ stagedMessagesRef.current.get(message.id)?.transcriptStatus
653
+ )
654
+ continue;
519
655
  promotedIds.add(message.id);
520
656
  stagedMessagesRef.current.delete(message.id);
521
657
  }
@@ -531,15 +667,13 @@ const useStreamThreadRuntime = (
531
667
  }
532
668
  const runConfig = config.runConfig ?? stagedRun.runConfig;
533
669
  setActiveRunConfig(runConfig);
534
- await stream.submit(
535
- {
536
- [messagesKey]: stagedRun.messages.map((message) => ({
537
- id: message.id,
538
- type: "human",
539
- content: message.content,
540
- })),
541
- },
542
- runConfigToSubmitOptions(runConfig),
670
+ await submitCarryingTranscripts(stagedRun.messages, () =>
671
+ stream.submit(
672
+ {
673
+ [messagesKey]: stagedRun.messages.map(toStagedMessageInput),
674
+ },
675
+ runConfigToSubmitOptions(runConfig),
676
+ ),
543
677
  );
544
678
  return;
545
679
  }
@@ -547,21 +681,30 @@ const useStreamThreadRuntime = (
547
681
  const threadId = externalId;
548
682
  if (!threadId || parentId == null) return;
549
683
  const s = streamRef.current;
684
+ const fork = planForkTranscripts(parentId);
550
685
  const checkpointId = await resolveForkCheckpoint(
551
686
  s.client,
552
687
  threadId,
553
688
  s.messages as readonly LangChainBaseMessage[],
554
- parentId,
689
+ fork.forkParentId,
555
690
  config.sourceId,
556
691
  s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),
557
692
  messagesKey,
558
693
  );
559
694
  if (!checkpointId) return;
695
+ dropTranscripts(fork.truncated);
560
696
  setActiveRunConfig(config.runConfig);
561
- await s.submit(null, {
562
- forkFrom: checkpointId,
563
- ...runConfigToSubmitOptions(config.runConfig),
564
- });
697
+ await submitCarryingTranscripts(fork.transcripts, () =>
698
+ s.submit(
699
+ fork.transcripts.length > 0
700
+ ? { [messagesKey]: fork.transcripts.map(toStagedMessageInput) }
701
+ : null,
702
+ {
703
+ forkFrom: checkpointId,
704
+ ...runConfigToSubmitOptions(config.runConfig),
705
+ },
706
+ ),
707
+ );
565
708
  },
566
709
  onEdit: async (message) => {
567
710
  if (!(message.startRun ?? message.role === "user")) {
@@ -586,24 +729,33 @@ const useStreamThreadRuntime = (
586
729
  const threadId = externalId;
587
730
  if (!threadId) return;
588
731
  const s = streamRef.current;
732
+ const fork = planForkTranscripts(message.parentId);
589
733
  const checkpointId = await resolveForkCheckpoint(
590
734
  s.client,
591
735
  threadId,
592
736
  s.messages as readonly LangChainBaseMessage[],
593
- message.parentId,
737
+ fork.forkParentId,
594
738
  message.sourceId,
595
739
  s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),
596
740
  messagesKey,
597
741
  );
598
742
  if (!checkpointId) return;
743
+ dropTranscripts(fork.truncated);
599
744
  const content = getMessageContent(message);
600
745
  setActiveRunConfig(message.runConfig);
601
- await s.submit(
602
- { [messagesKey]: [{ type: "human", content }] },
603
- {
604
- forkFrom: checkpointId,
605
- ...runConfigToSubmitOptions(message.runConfig),
606
- },
746
+ await submitCarryingTranscripts(fork.transcripts, () =>
747
+ s.submit(
748
+ {
749
+ [messagesKey]: [
750
+ ...fork.transcripts.map(toStagedMessageInput),
751
+ { type: "human", content },
752
+ ],
753
+ },
754
+ {
755
+ forkFrom: checkpointId,
756
+ ...runConfigToSubmitOptions(message.runConfig),
757
+ },
758
+ ),
607
759
  );
608
760
  },
609
761
  onCancel: