@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
@@ -0,0 +1,61 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { renderHook } from "@testing-library/react";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import type { AssistantCloud } from "assistant-cloud";
6
+ import type { AssistantRuntime } from "@assistant-ui/core";
7
+ import type { UseStreamRuntimeOptions } from "./types";
8
+
9
+ const mocks = vi.hoisted(() => ({
10
+ cloudAdapter: {},
11
+ runtime: {},
12
+ useCloudThreadListAdapter: vi.fn(() => ({})),
13
+ useRemoteThreadListRuntime: vi.fn(() => ({})),
14
+ }));
15
+
16
+ vi.mock("@assistant-ui/core/react", async (importOriginal) => ({
17
+ ...(await importOriginal<typeof import("@assistant-ui/core/react")>()),
18
+ useCloudThreadListAdapter: mocks.useCloudThreadListAdapter,
19
+ useRemoteThreadListRuntime: mocks.useRemoteThreadListRuntime,
20
+ }));
21
+
22
+ vi.mock("@assistant-ui/store", async (importOriginal) => ({
23
+ ...(await importOriginal<typeof import("@assistant-ui/store")>()),
24
+ useAui: () => ({
25
+ threadListItem: {
26
+ source: null,
27
+ getState: () => ({ externalId: undefined }),
28
+ initialize: vi.fn(),
29
+ },
30
+ }),
31
+ }));
32
+
33
+ import { LANGCHAIN_SDK } from "./sdkIdentity";
34
+ import { useStreamRuntime } from "./useStreamRuntime";
35
+
36
+ describe("useStreamRuntime Cloud options", () => {
37
+ it("forwards the Cloud scope to the thread-list adapter", () => {
38
+ const cloud = {} as AssistantCloud;
39
+ mocks.useCloudThreadListAdapter.mockReturnValue(mocks.cloudAdapter);
40
+ mocks.useRemoteThreadListRuntime.mockReturnValue(
41
+ mocks.runtime as AssistantRuntime,
42
+ );
43
+
44
+ const options = {
45
+ apiUrl: "/api",
46
+ assistantId: "assistant-1",
47
+ cloud,
48
+ scopeId: "workspace-1",
49
+ } satisfies UseStreamRuntimeOptions;
50
+
51
+ renderHook(() => useStreamRuntime(options));
52
+
53
+ expect(mocks.useCloudThreadListAdapter).toHaveBeenCalledWith(
54
+ expect.objectContaining({
55
+ cloud,
56
+ scopeId: "workspace-1",
57
+ sdk: LANGCHAIN_SDK,
58
+ }),
59
+ );
60
+ });
61
+ });
@@ -31,7 +31,8 @@ const { conversionSpy, mockUseChannel, mockUseStream, streamController } =
31
31
  streamController: Symbol("STREAM_CONTROLLER"),
32
32
  }));
33
33
 
34
- vi.mock("@langchain/react", () => ({
34
+ vi.mock("@langchain/react", async (importOriginal) => ({
35
+ ...(await importOriginal<typeof import("@langchain/react")>()),
35
36
  STREAM_CONTROLLER: streamController,
36
37
  useChannel: mockUseChannel,
37
38
  useStream: mockUseStream,
@@ -980,6 +981,231 @@ describe("useStreamRuntime staged messages", () => {
980
981
  });
981
982
  });
982
983
 
984
+ describe("useStreamRuntime fork ownership", () => {
985
+ type ForkHistory = {
986
+ values: { messages: LangChainBaseMessage[] };
987
+ checkpoint: { checkpoint_id: string };
988
+ }[];
989
+
990
+ const renderFork = async (stream: MockStream) => {
991
+ mockUseStream.mockReturnValue(stream);
992
+ const runtime = renderHook(() =>
993
+ useStreamRuntime({
994
+ apiUrl: "/api",
995
+ unstable_threadListAdapter: makeThreadListAdapter(),
996
+ } as never),
997
+ );
998
+ const view = render(
999
+ <AssistantRuntimeProvider runtime={runtime.result.current}>
1000
+ {null}
1001
+ </AssistantRuntimeProvider>,
1002
+ );
1003
+ await settleOutsideAct(() =>
1004
+ runtime.result.current.threads.switchToThread("thread-a"),
1005
+ );
1006
+ return { runtime, view };
1007
+ };
1008
+
1009
+ it("stops the streaming reply before an edit looks up its checkpoint and submits once", async () => {
1010
+ const events: string[] = [];
1011
+ const lookup = deferred<ForkHistory>();
1012
+ const stream = createMockStream([
1013
+ message("u1", "human", "original"),
1014
+ message("a1", "ai", "partial reply"),
1015
+ ]);
1016
+ stream.isLoading = true;
1017
+ stream.stop.mockImplementation(() => events.push("stop"));
1018
+ stream.client = {
1019
+ threads: {
1020
+ getHistory: vi.fn(() => {
1021
+ events.push("lookup");
1022
+ return lookup.promise;
1023
+ }),
1024
+ },
1025
+ };
1026
+ stream.submit.mockImplementation(async () => {
1027
+ events.push("submit");
1028
+ });
1029
+ const { runtime, view } = await renderFork(stream);
1030
+
1031
+ await act(async () => {
1032
+ runtime.result.current.thread.append({
1033
+ role: "user",
1034
+ parentId: null,
1035
+ sourceId: "u1",
1036
+ content: [{ type: "text", text: "edited" }],
1037
+ });
1038
+ await Promise.resolve();
1039
+ });
1040
+ expect(events).toEqual(["stop", "lookup"]);
1041
+ expect(stream.submit).not.toHaveBeenCalled();
1042
+
1043
+ await act(async () => {
1044
+ lookup.resolve([
1045
+ { values: { messages: [] }, checkpoint: { checkpoint_id: "cp-1" } },
1046
+ ]);
1047
+ await Promise.resolve();
1048
+ });
1049
+ expect(events).toEqual(["stop", "lookup", "submit"]);
1050
+ expect(stream.submit).toHaveBeenCalledExactlyOnceWith(
1051
+ { messages: [{ type: "human", content: "edited" }] },
1052
+ { forkFrom: "cp-1" },
1053
+ );
1054
+ view.unmount();
1055
+ runtime.unmount();
1056
+ });
1057
+
1058
+ it("drops the first edit when a second edit supersedes its checkpoint lookup", async () => {
1059
+ const firstLookup = deferred<ForkHistory>();
1060
+ const secondLookup = deferred<ForkHistory>();
1061
+ const stream = createMockStream([
1062
+ message("u1", "human", "original"),
1063
+ message("a1", "ai", "partial reply"),
1064
+ ]);
1065
+ stream.isLoading = true;
1066
+ const getHistory = vi
1067
+ .fn()
1068
+ .mockImplementationOnce(() => firstLookup.promise)
1069
+ .mockImplementationOnce(() => secondLookup.promise);
1070
+ stream.client = { threads: { getHistory } };
1071
+ const { runtime, view } = await renderFork(stream);
1072
+
1073
+ await act(async () => {
1074
+ runtime.result.current.thread.append({
1075
+ role: "user",
1076
+ parentId: null,
1077
+ sourceId: "u1",
1078
+ content: [{ type: "text", text: "first edit" }],
1079
+ });
1080
+ await Promise.resolve();
1081
+ });
1082
+ await waitFor(() => expect(getHistory).toHaveBeenCalledTimes(1));
1083
+ await act(async () => {
1084
+ runtime.result.current.thread.append({
1085
+ role: "user",
1086
+ parentId: null,
1087
+ sourceId: "u1",
1088
+ content: [{ type: "text", text: "second edit" }],
1089
+ });
1090
+ await Promise.resolve();
1091
+ });
1092
+ await waitFor(() => expect(getHistory).toHaveBeenCalledTimes(2));
1093
+
1094
+ await act(async () => {
1095
+ firstLookup.resolve([
1096
+ { values: { messages: [] }, checkpoint: { checkpoint_id: "cp-1" } },
1097
+ ]);
1098
+ await Promise.resolve();
1099
+ });
1100
+ expect(stream.submit).not.toHaveBeenCalled();
1101
+
1102
+ await act(async () => {
1103
+ secondLookup.resolve([
1104
+ { values: { messages: [] }, checkpoint: { checkpoint_id: "cp-2" } },
1105
+ ]);
1106
+ await Promise.resolve();
1107
+ });
1108
+ expect(stream.submit).toHaveBeenCalledExactlyOnceWith(
1109
+ { messages: [{ type: "human", content: "second edit" }] },
1110
+ { forkFrom: "cp-2" },
1111
+ );
1112
+ view.unmount();
1113
+ runtime.unmount();
1114
+ });
1115
+
1116
+ it("stops the streaming reply before reload looks up its checkpoint", async () => {
1117
+ const events: string[] = [];
1118
+ const lookup = deferred<ForkHistory>();
1119
+ const stream = createMockStream([
1120
+ message("u1", "human", "question"),
1121
+ message("a1", "ai", "partial reply"),
1122
+ ]);
1123
+ stream.isLoading = true;
1124
+ stream.stop.mockImplementation(() => events.push("stop"));
1125
+ stream.client = {
1126
+ threads: {
1127
+ getHistory: vi.fn(() => {
1128
+ events.push("lookup");
1129
+ return lookup.promise;
1130
+ }),
1131
+ },
1132
+ };
1133
+ stream.submit.mockImplementation(async () => {
1134
+ events.push("submit");
1135
+ });
1136
+ const { runtime, view } = await renderFork(stream);
1137
+
1138
+ await act(async () => {
1139
+ runtime.result.current.thread.getMessageById("a1").reload();
1140
+ await Promise.resolve();
1141
+ });
1142
+ expect(events).toEqual(["stop", "lookup"]);
1143
+ expect(stream.submit).not.toHaveBeenCalled();
1144
+
1145
+ await act(async () => {
1146
+ lookup.resolve([
1147
+ {
1148
+ values: { messages: [message("u1", "human", "question")] },
1149
+ checkpoint: { checkpoint_id: "cp-1" },
1150
+ },
1151
+ ]);
1152
+ await Promise.resolve();
1153
+ });
1154
+ expect(events).toEqual(["stop", "lookup", "submit"]);
1155
+ expect(stream.submit).toHaveBeenCalledExactlyOnceWith(null, {
1156
+ forkFrom: "cp-1",
1157
+ });
1158
+ view.unmount();
1159
+ runtime.unmount();
1160
+ });
1161
+
1162
+ it("drops an edit lookup after Stop", async () => {
1163
+ const lookup = deferred<ForkHistory>();
1164
+ const stream = createMockStream([
1165
+ message("u1", "human", "original"),
1166
+ message("a1", "ai", "partial reply"),
1167
+ ]);
1168
+ stream.stop.mockImplementation(() => {
1169
+ stream.isLoading = false;
1170
+ });
1171
+ const getHistory = vi.fn(() => lookup.promise);
1172
+ stream.client = { threads: { getHistory } };
1173
+ const { runtime, view } = await renderFork(stream);
1174
+
1175
+ await act(async () => {
1176
+ runtime.result.current.thread.append({
1177
+ role: "user",
1178
+ parentId: null,
1179
+ sourceId: "u1",
1180
+ content: [{ type: "text", text: "edited" }],
1181
+ });
1182
+ await Promise.resolve();
1183
+ });
1184
+ await waitFor(() => expect(getHistory).toHaveBeenCalledOnce());
1185
+ runtime.rerender();
1186
+ expect(runtime.result.current.thread.getState().isRunning).toBe(true);
1187
+ await act(async () => {
1188
+ await runtime.result.current.thread.cancelRun();
1189
+ });
1190
+
1191
+ await act(async () => {
1192
+ lookup.resolve([
1193
+ { values: { messages: [] }, checkpoint: { checkpoint_id: "cp-1" } },
1194
+ ]);
1195
+ await Promise.resolve();
1196
+ });
1197
+ expect(stream.submit).not.toHaveBeenCalled();
1198
+ expect(runtime.result.current.thread.getState().isRunning).toBe(false);
1199
+ expect(
1200
+ runtime.result.current.thread
1201
+ .getState()
1202
+ .messages.find((m) => m.id === "a1")?.metadata,
1203
+ ).not.toHaveProperty("timing");
1204
+ view.unmount();
1205
+ runtime.unmount();
1206
+ });
1207
+ });
1208
+
983
1209
  describe("useStreamRuntime pending tool call cancellation", () => {
984
1210
  it("cancels only the pending tool calls that carry an id", async () => {
985
1211
  const stream = createMockStream([