@assistant-ui/react-langchain 0.0.30 → 0.0.32

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 (47) hide show
  1. package/README.md +1 -1
  2. package/dist/attachSubagentTranscripts.d.ts +24 -0
  3. package/dist/attachSubagentTranscripts.d.ts.map +1 -0
  4. package/dist/attachSubagentTranscripts.js +43 -0
  5. package/dist/attachSubagentTranscripts.js.map +1 -0
  6. package/dist/convertMessages.d.ts +4 -4
  7. package/dist/convertMessages.d.ts.map +1 -1
  8. package/dist/converter.d.ts +10 -11
  9. package/dist/converter.d.ts.map +1 -1
  10. package/dist/findForkCheckpointInHistory.d.ts +2 -3
  11. package/dist/findForkCheckpointInHistory.d.ts.map +1 -1
  12. package/dist/hooks.d.ts +14 -15
  13. package/dist/hooks.d.ts.map +1 -1
  14. package/dist/resolveForkCheckpoint.d.ts +1 -2
  15. package/dist/resolveForkCheckpoint.d.ts.map +1 -1
  16. package/dist/runtimeExtras.d.ts +1 -2
  17. package/dist/runtimeExtras.d.ts.map +1 -1
  18. package/dist/sdkIdentity.d.ts +5 -0
  19. package/dist/sdkIdentity.d.ts.map +1 -0
  20. package/dist/sdkIdentity.js +9 -0
  21. package/dist/sdkIdentity.js.map +1 -0
  22. package/dist/streamingTiming.d.ts +2 -3
  23. package/dist/streamingTiming.d.ts.map +1 -1
  24. package/dist/types.d.ts +8 -8
  25. package/dist/types.d.ts.map +1 -1
  26. package/dist/uiMessages.d.ts +22 -7
  27. package/dist/uiMessages.d.ts.map +1 -1
  28. package/dist/uiMessages.js +23 -5
  29. package/dist/uiMessages.js.map +1 -1
  30. package/dist/useStreamRuntime.d.ts +3 -3
  31. package/dist/useStreamRuntime.d.ts.map +1 -1
  32. package/dist/useStreamRuntime.js +26 -17
  33. package/dist/useStreamRuntime.js.map +1 -1
  34. package/dist/useSubagentTranscripts.d.ts +8 -0
  35. package/dist/useSubagentTranscripts.d.ts.map +1 -0
  36. package/dist/useSubagentTranscripts.js +265 -0
  37. package/dist/useSubagentTranscripts.js.map +1 -0
  38. package/package.json +17 -12
  39. package/src/attachSubagentTranscripts.test.ts +198 -0
  40. package/src/attachSubagentTranscripts.ts +91 -0
  41. package/src/sdkIdentity.ts +9 -0
  42. package/src/uiMessages.test.ts +43 -0
  43. package/src/uiMessages.ts +36 -4
  44. package/src/useStreamRuntime.test.tsx +218 -7
  45. package/src/useStreamRuntime.ts +40 -16
  46. package/src/useSubagentTranscripts.test.tsx +1258 -0
  47. package/src/useSubagentTranscripts.ts +526 -0
@@ -1,7 +1,7 @@
1
1
  // @vitest-environment jsdom
2
2
 
3
3
  import { act, render, renderHook, waitFor } from "@testing-library/react";
4
- import { describe, expect, it, vi } from "vitest";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
5
  import { AssistantRuntimeProvider } from "@assistant-ui/core/react";
6
6
  import type {
7
7
  AssistantRuntime,
@@ -20,7 +20,7 @@ import {
20
20
  } from "./hooks";
21
21
 
22
22
  const { mockUseChannel, mockUseStream, streamController } = vi.hoisted(() => ({
23
- mockUseChannel: vi.fn(() => []),
23
+ mockUseChannel: vi.fn((): unknown[] => []),
24
24
  mockUseStream: vi.fn(),
25
25
  streamController: Symbol("STREAM_CONTROLLER"),
26
26
  }));
@@ -40,7 +40,7 @@ type MockStream = {
40
40
  values: Record<string, unknown>;
41
41
  interrupts: unknown[];
42
42
  toolCalls: unknown[];
43
- subagents: unknown[];
43
+ subagents: ReadonlyMap<string, unknown>;
44
44
  subgraphs: unknown[];
45
45
  error: unknown;
46
46
  submit: ReturnType<typeof vi.fn>;
@@ -53,6 +53,10 @@ type MockStream = {
53
53
  messageMetadataStore: {
54
54
  getSnapshot: ReturnType<typeof vi.fn>;
55
55
  };
56
+ resolveSubagentNamespace: ReturnType<typeof vi.fn>;
57
+ registry: {
58
+ acquire: ReturnType<typeof vi.fn>;
59
+ };
56
60
  };
57
61
  };
58
62
 
@@ -75,7 +79,7 @@ const createMockStream = (
75
79
  values: {},
76
80
  interrupts: [],
77
81
  toolCalls: [],
78
- subagents: [],
82
+ subagents: new Map(),
79
83
  subgraphs: [],
80
84
  error: undefined,
81
85
  submit: vi.fn(async () => {}),
@@ -88,6 +92,10 @@ const createMockStream = (
88
92
  messageMetadataStore: {
89
93
  getSnapshot: vi.fn(),
90
94
  },
95
+ resolveSubagentNamespace: vi.fn(async () => {}),
96
+ registry: {
97
+ acquire: vi.fn(),
98
+ },
91
99
  },
92
100
  });
93
101
 
@@ -177,7 +185,11 @@ describe("useStreamRuntime thread options", () => {
177
185
  unstable_threadListAdapter: threadListAdapter,
178
186
  } as never);
179
187
  capture.runtime = runtime;
180
- return <AssistantRuntimeProvider runtime={runtime} />;
188
+ return (
189
+ <AssistantRuntimeProvider runtime={runtime}>
190
+ {null}
191
+ </AssistantRuntimeProvider>
192
+ );
181
193
  };
182
194
 
183
195
  const view = render(<TestRuntime />);
@@ -286,7 +298,11 @@ describe("useStreamRuntime thread options", () => {
286
298
  const TestRuntime = () => {
287
299
  const runtime = useStreamRuntime({ apiUrl: "/api" } as never);
288
300
  capture.runtime = runtime;
289
- return <AssistantRuntimeProvider runtime={runtime} />;
301
+ return (
302
+ <AssistantRuntimeProvider runtime={runtime}>
303
+ {null}
304
+ </AssistantRuntimeProvider>
305
+ );
290
306
  };
291
307
  const view = render(<TestRuntime />);
292
308
  await waitFor(() => expect(capture.runtime).not.toBeNull());
@@ -721,7 +737,9 @@ describe("useStreamRuntime run configuration", () => {
721
737
  });
722
738
 
723
739
  await act(async () => {
724
- await capture.send!([{ type: "human", content: "next" }]);
740
+ await capture.send!([
741
+ { type: "human", content: "next" } as unknown as LangChainBaseMessage,
742
+ ]);
725
743
  });
726
744
 
727
745
  expect(stream.submit).toHaveBeenLastCalledWith(
@@ -971,3 +989,196 @@ describe("useStreamRuntime committed refs", () => {
971
989
  host.unmount();
972
990
  });
973
991
  });
992
+
993
+ describe("useStreamRuntime subagent transcripts", () => {
994
+ afterEach(() => {
995
+ mockUseChannel.mockReset();
996
+ });
997
+
998
+ it("renders live UI messages inside the transcript of the subagent that pushed them", async () => {
999
+ const stream = createMockStream([
1000
+ message("human-1", "human", "delegate"),
1001
+ {
1002
+ id: "root-ai",
1003
+ _getType: () => "ai",
1004
+ content: "",
1005
+ tool_calls: [{ id: "task-one", name: "task", args: {} }],
1006
+ },
1007
+ ]);
1008
+ const transcript = [message("nested-ai", "ai", "nested answer")];
1009
+ stream.subagents = new Map([
1010
+ [
1011
+ "task-one",
1012
+ {
1013
+ id: "task-one",
1014
+ namespace: ["tools:task-one"],
1015
+ status: "running",
1016
+ parentId: null,
1017
+ depth: 1,
1018
+ startedAt: new Date(1_000),
1019
+ completedAt: null,
1020
+ },
1021
+ ],
1022
+ ]);
1023
+ stream[streamController]!.registry.acquire.mockReturnValue({
1024
+ store: { getSnapshot: () => transcript, subscribe: () => () => {} },
1025
+ release: vi.fn(),
1026
+ });
1027
+ const uiEvent = {
1028
+ method: "custom",
1029
+ params: {
1030
+ namespace: ["tools:task-one"],
1031
+ data: {
1032
+ type: "ui",
1033
+ id: "ui-1",
1034
+ name: "chart",
1035
+ props: { points: [1, 2] },
1036
+ metadata: { message_id: "nested-ai" },
1037
+ },
1038
+ },
1039
+ };
1040
+ mockUseChannel.mockReturnValue([uiEvent]);
1041
+ const { auiResult, rerender } = renderAui(stream);
1042
+ const nestedTranscript = () => {
1043
+ const { messages } = auiResult.current.thread.getState();
1044
+ for (const threadMessage of messages) {
1045
+ for (const part of threadMessage.content) {
1046
+ if (part.type === "tool-call" && part.toolCallId === "task-one")
1047
+ return part.messages;
1048
+ }
1049
+ }
1050
+ return undefined;
1051
+ };
1052
+
1053
+ await waitFor(() =>
1054
+ expect(nestedTranscript()?.[0]?.content).toMatchObject([
1055
+ { type: "text", text: "nested answer" },
1056
+ { type: "data", name: "chart", data: { points: [1, 2] } },
1057
+ ]),
1058
+ );
1059
+ const rendered = nestedTranscript();
1060
+
1061
+ mockUseChannel.mockReturnValue([
1062
+ uiEvent,
1063
+ {
1064
+ method: "custom",
1065
+ params: { namespace: [], data: { name: "progress", payload: 1 } },
1066
+ },
1067
+ ]);
1068
+ await act(async () => {
1069
+ rerender();
1070
+ });
1071
+
1072
+ expect(nestedTranscript()).toBe(rendered);
1073
+ });
1074
+
1075
+ it("keeps messages and transcripts when custom events carry no UI update", 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 chartEvent = (
1105
+ props: Record<string, unknown>,
1106
+ metadata: Record<string, unknown> = {},
1107
+ ) => ({
1108
+ method: "custom",
1109
+ params: {
1110
+ namespace: ["tools:task-one"],
1111
+ data: {
1112
+ type: "ui",
1113
+ id: "ui-1",
1114
+ name: "chart",
1115
+ props,
1116
+ metadata: { message_id: "nested-ai", ...metadata },
1117
+ },
1118
+ },
1119
+ });
1120
+ const events = [
1121
+ chartEvent({ points: [1, 2] }),
1122
+ chartEvent({ label: "first" }, { merge: true }),
1123
+ ];
1124
+ mockUseChannel.mockReturnValue(events);
1125
+ const { auiResult, rerender } = renderAui(stream);
1126
+ const nestedTranscript = () => {
1127
+ const { messages } = auiResult.current.thread.getState();
1128
+ for (const threadMessage of messages) {
1129
+ for (const part of threadMessage.content) {
1130
+ if (part.type === "tool-call" && part.toolCallId === "task-one")
1131
+ return part.messages;
1132
+ }
1133
+ }
1134
+ return undefined;
1135
+ };
1136
+
1137
+ await waitFor(() =>
1138
+ expect(nestedTranscript()?.[0]?.content).toMatchObject([
1139
+ { type: "text", text: "nested answer" },
1140
+ {
1141
+ type: "data",
1142
+ name: "chart",
1143
+ data: { points: [1, 2], label: "first" },
1144
+ },
1145
+ ]),
1146
+ );
1147
+ const [human, ai] = auiResult.current.thread.getState().messages;
1148
+ const rendered = nestedTranscript();
1149
+
1150
+ const withProgress = [
1151
+ ...events,
1152
+ {
1153
+ method: "custom",
1154
+ params: { namespace: [], data: { name: "progress", payload: 1 } },
1155
+ },
1156
+ ];
1157
+ mockUseChannel.mockReturnValue(withProgress);
1158
+ await act(async () => {
1159
+ rerender();
1160
+ });
1161
+
1162
+ const messages = auiResult.current.thread.getState().messages;
1163
+ expect(messages[0]).toBe(human);
1164
+ expect(messages[1]).toBe(ai);
1165
+ expect(nestedTranscript()).toBe(rendered);
1166
+
1167
+ mockUseChannel.mockReturnValue([
1168
+ ...withProgress,
1169
+ chartEvent({ label: "second" }, { merge: true }),
1170
+ ]);
1171
+ await act(async () => {
1172
+ rerender();
1173
+ });
1174
+
1175
+ expect(nestedTranscript()?.[0]?.content).toMatchObject([
1176
+ { type: "text", text: "nested answer" },
1177
+ {
1178
+ type: "data",
1179
+ name: "chart",
1180
+ data: { points: [1, 2], label: "second" },
1181
+ },
1182
+ ]);
1183
+ });
1184
+ });
@@ -29,7 +29,6 @@ import {
29
29
  } from "@assistant-ui/core/react";
30
30
  import { useAui, useAuiState } from "@assistant-ui/store";
31
31
  import { STREAM_CONTROLLER, useChannel, useStream } from "@langchain/react";
32
- import type { Channel } from "@langchain/react";
33
32
  import type {
34
33
  LangChainBaseMessage,
35
34
  LangChainToolCall,
@@ -43,12 +42,21 @@ import {
43
42
  getMessageContent,
44
43
  getMessageType,
45
44
  } from "./convertMessages";
46
- import { foldUIUpdates, mergeUIMessages } from "./uiMessages";
45
+ import {
46
+ attachSubagentTranscripts,
47
+ createAttachMemo,
48
+ } from "./attachSubagentTranscripts";
49
+ import { useSubagentTranscripts } from "./useSubagentTranscripts";
50
+ import {
51
+ createUIFoldMemo,
52
+ foldUIUpdates,
53
+ mergeUIMessages,
54
+ UI_CUSTOM_CHANNELS,
55
+ } from "./uiMessages";
47
56
  import { langChainExtras } from "./runtimeExtras";
48
57
  import { resolveForkCheckpoint } from "./resolveForkCheckpoint";
49
58
  import { useLangChainStreamingTiming } from "./streamingTiming";
50
-
51
- const UI_CUSTOM_CHANNELS: readonly Channel[] = ["custom"];
59
+ import { LANGCHAIN_SDK } from "./sdkIdentity";
52
60
 
53
61
  export const runConfigToSubmitOptions = (
54
62
  runConfig: AppendMessage["runConfig"],
@@ -161,9 +169,10 @@ const useStreamThreadRuntime = (
161
169
  const uiStateValue = stream.values[uiStateKey];
162
170
 
163
171
  const customEvents = useChannel(stream, UI_CUSTOM_CHANNELS);
172
+ const [uiFoldMemo] = useState(createUIFoldMemo);
164
173
  const liveUiMessages = useMemo(
165
- () => foldUIUpdates(customEvents),
166
- [customEvents],
174
+ () => foldUIUpdates(customEvents, uiFoldMemo),
175
+ [customEvents, uiFoldMemo],
167
176
  );
168
177
 
169
178
  const mergedUiMessages = useMemo(
@@ -171,6 +180,11 @@ const useStreamThreadRuntime = (
171
180
  [liveUiMessages, uiStateValue],
172
181
  );
173
182
 
183
+ const uiMessagesByParent = useMemo(
184
+ () => groupUIMessagesByParent<UIMessage>(mergedUiMessages),
185
+ [mergedUiMessages],
186
+ );
187
+
174
188
  const visibleMessages =
175
189
  stagedMessages ?? (stream.messages as LangChainBaseMessage[]);
176
190
 
@@ -179,24 +193,33 @@ const useStreamThreadRuntime = (
179
193
  effectiveIsRunning,
180
194
  );
181
195
 
196
+ const subagentTranscripts = useSubagentTranscripts(
197
+ stream,
198
+ uiMessagesByParent,
199
+ );
200
+
182
201
  const convertWithUI = useMemo<
183
202
  useExternalMessageConverter.Callback<LangChainBaseMessage>
184
- >(() => {
185
- const uiMessagesByParent =
186
- groupUIMessagesByParent<UIMessage>(mergedUiMessages);
187
- return (message, metadata) =>
203
+ >(
204
+ () => (message, metadata) =>
188
205
  convertLangChainBaseMessage(message, {
189
206
  ...metadata,
190
207
  uiMessagesByParent,
191
208
  messageTiming,
192
- });
193
- }, [mergedUiMessages, messageTiming]);
209
+ }),
210
+ [uiMessagesByParent, messageTiming],
211
+ );
194
212
 
195
213
  const threadMessages = useExternalMessageConverter({
196
214
  callback: convertWithUI,
197
215
  messages: visibleMessages,
198
216
  isRunning: effectiveIsRunning,
199
217
  });
218
+ const [memo] = useState(createAttachMemo);
219
+ const messagesWithTranscripts = useMemo(
220
+ () => attachSubagentTranscripts(threadMessages, subagentTranscripts, memo),
221
+ [threadMessages, subagentTranscripts, memo],
222
+ );
200
223
 
201
224
  const streamRef = useRef(stream);
202
225
  useInsertionEffect(() => {
@@ -269,10 +292,10 @@ const useStreamThreadRuntime = (
269
292
  visibleMessagesRef.current = visibleMessages;
270
293
  }, [visibleMessages]);
271
294
 
272
- const threadMessagesRef = useRef(threadMessages);
295
+ const threadMessagesRef = useRef(messagesWithTranscripts);
273
296
  useInsertionEffect(() => {
274
- threadMessagesRef.current = threadMessages;
275
- }, [threadMessages]);
297
+ threadMessagesRef.current = messagesWithTranscripts;
298
+ }, [messagesWithTranscripts]);
276
299
 
277
300
  const stagedMessagesRef = useRef(
278
301
  new Map<
@@ -412,7 +435,7 @@ const useStreamThreadRuntime = (
412
435
  ...pickExternalStoreSharedOptions(options),
413
436
  isRunning: stream.isLoading,
414
437
  isLoading: stream.isThreadLoading,
415
- messages: threadMessages,
438
+ messages: messagesWithTranscripts,
416
439
  adapters,
417
440
  extras,
418
441
  unstable_enableToolInvocations: true,
@@ -631,6 +654,7 @@ export const useStreamRuntime = (rawOptions: UseStreamRuntimeOptions) => {
631
654
 
632
655
  const aui = useAui();
633
656
  const cloudAdapter = useCloudThreadListAdapter({
657
+ sdk: LANGCHAIN_SDK,
634
658
  cloud,
635
659
  create: createCloudThreadListAdapterCreateFallback(
636
660
  create,