@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.
- package/LICENSE +1 -1
- package/dist/attachSubagentTranscripts.d.ts +13 -14
- package/dist/attachSubagentTranscripts.d.ts.map +1 -1
- package/dist/convertMessages.d.ts +6 -9
- package/dist/convertMessages.d.ts.map +1 -1
- package/dist/convertMessages.js +57 -15
- package/dist/convertMessages.js.map +1 -1
- package/dist/converter.d.ts +128 -128
- package/dist/converter.d.ts.map +1 -1
- package/dist/converter.js +2 -1
- package/dist/converter.js.map +1 -1
- package/dist/findForkCheckpointInHistory.d.ts +15 -16
- package/dist/findForkCheckpointInHistory.d.ts.map +1 -1
- package/dist/hooks.d.ts +5 -7
- package/dist/hooks.d.ts.map +1 -1
- package/dist/index.d.ts +7 -8
- package/dist/index.d.ts.map +1 -0
- package/dist/resolveForkCheckpoint.d.ts +4 -5
- package/dist/resolveForkCheckpoint.d.ts.map +1 -1
- package/dist/runtimeExtras.d.ts +1 -3
- package/dist/runtimeExtras.d.ts.map +1 -1
- package/dist/sdkIdentity.d.ts +1 -3
- package/dist/sdkIdentity.d.ts.map +1 -1
- package/dist/sdkIdentity.js +1 -1
- package/dist/streamingTiming.d.ts +2 -4
- package/dist/streamingTiming.d.ts.map +1 -1
- package/dist/subagentMessagesProjection.d.ts +17 -0
- package/dist/subagentMessagesProjection.d.ts.map +1 -0
- package/dist/subagentMessagesProjection.js +40 -0
- package/dist/subagentMessagesProjection.js.map +1 -0
- package/dist/types.d.ts +100 -103
- package/dist/types.d.ts.map +1 -1
- package/dist/uiMessages.d.ts +18 -6
- package/dist/uiMessages.d.ts.map +1 -1
- package/dist/uiMessages.js +33 -1
- package/dist/uiMessages.js.map +1 -1
- package/dist/useStreamRuntime.d.ts +7 -10
- package/dist/useStreamRuntime.d.ts.map +1 -1
- package/dist/useStreamRuntime.js +100 -26
- package/dist/useStreamRuntime.js.map +1 -1
- package/dist/useSubagentTranscripts.d.ts +3 -5
- package/dist/useSubagentTranscripts.d.ts.map +1 -1
- package/dist/useSubagentTranscripts.js +5 -3
- package/dist/useSubagentTranscripts.js.map +1 -1
- package/package.json +16 -9
- package/src/convertMessages.test.ts +153 -0
- package/src/convertMessages.ts +54 -8
- package/src/converter.ts +6 -0
- package/src/subagentMessagesProjection.test.ts +175 -0
- package/src/subagentMessagesProjection.ts +50 -0
- package/src/tests/settleOutsideAct.ts +15 -0
- package/src/uiMessages.test.ts +106 -0
- package/src/uiMessages.ts +43 -0
- package/src/useStreamRuntime.test.tsx +147 -7
- package/src/useStreamRuntime.ts +197 -45
- package/src/useStreamRuntime.voice.test.tsx +713 -0
- package/src/useSubagentTranscripts.fast-refresh.test.tsx +152 -0
- package/src/useSubagentTranscripts.test.tsx +2 -0
- package/src/useSubagentTranscripts.ts +5 -7
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { act, useMemo } from "react";
|
|
4
|
+
import { afterAll, afterEach, expect, it, vi } from "vitest";
|
|
5
|
+
import type { UIMessage } from "./types";
|
|
6
|
+
|
|
7
|
+
type Family = { current: unknown };
|
|
8
|
+
type RendererInternals = {
|
|
9
|
+
setRefreshHandler: (resolve: (type: unknown) => Family | undefined) => void;
|
|
10
|
+
scheduleRefresh: (
|
|
11
|
+
root: unknown,
|
|
12
|
+
update: { staleFamilies: Set<Family>; updatedFamilies: Set<Family> },
|
|
13
|
+
) => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const { streamController } = vi.hoisted(() => ({
|
|
17
|
+
streamController: Symbol("STREAM_CONTROLLER"),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock("@langchain/react", async (importOriginal) => ({
|
|
21
|
+
...(await importOriginal<typeof import("@langchain/react")>()),
|
|
22
|
+
STREAM_CONTROLLER: streamController,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
let renderer: RendererInternals | undefined;
|
|
26
|
+
const fiberRoots = new Set<unknown>();
|
|
27
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
28
|
+
vi.stubGlobal("__REACT_DEVTOOLS_GLOBAL_HOOK__", {
|
|
29
|
+
supportsFiber: true,
|
|
30
|
+
inject: (internals: RendererInternals) => {
|
|
31
|
+
renderer = internals;
|
|
32
|
+
return 1;
|
|
33
|
+
},
|
|
34
|
+
onScheduleFiberRoot: () => {},
|
|
35
|
+
onCommitFiberRoot: (_id: number, root: unknown) => fiberRoots.add(root),
|
|
36
|
+
onCommitFiberUnmount: () => {},
|
|
37
|
+
});
|
|
38
|
+
const { cleanup, render, waitFor } = await import("@testing-library/react");
|
|
39
|
+
const { useSubagentTranscripts } = await import("./useSubagentTranscripts");
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
cleanup();
|
|
43
|
+
renderer?.setRefreshHandler(() => undefined);
|
|
44
|
+
fiberRoots.clear();
|
|
45
|
+
});
|
|
46
|
+
afterAll(() => vi.unstubAllGlobals());
|
|
47
|
+
|
|
48
|
+
it("keeps a live subagent projection and namespace request through Fast Refresh, then disposes on unmount", async () => {
|
|
49
|
+
const listeners = new Set<() => void>();
|
|
50
|
+
let messages = [
|
|
51
|
+
{ id: "message-one", _getType: () => "ai", content: "partial" },
|
|
52
|
+
];
|
|
53
|
+
const store = {
|
|
54
|
+
getSnapshot: () => messages,
|
|
55
|
+
subscribe: (listener: () => void) => {
|
|
56
|
+
listeners.add(listener);
|
|
57
|
+
return () => listeners.delete(listener);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
const release = vi.fn();
|
|
61
|
+
const acquire = vi.fn(() => ({ store, release }));
|
|
62
|
+
let settleFirst!: () => void;
|
|
63
|
+
const pending = new Promise<void>((resolve) => {
|
|
64
|
+
settleFirst = resolve;
|
|
65
|
+
});
|
|
66
|
+
const resolveSubagentNamespace = vi
|
|
67
|
+
.fn<() => Promise<void>>()
|
|
68
|
+
.mockReturnValueOnce(pending)
|
|
69
|
+
.mockResolvedValue(undefined);
|
|
70
|
+
const snapshot = (status: "running" | "complete") => ({
|
|
71
|
+
id: "task-one",
|
|
72
|
+
namespace: ["tools:task-one"],
|
|
73
|
+
status,
|
|
74
|
+
parentId: null,
|
|
75
|
+
depth: 1,
|
|
76
|
+
startedAt: new Date(1_000),
|
|
77
|
+
completedAt: status === "complete" ? new Date(4_000) : null,
|
|
78
|
+
});
|
|
79
|
+
const stream = {
|
|
80
|
+
subagents: new Map([["task-one", snapshot("running")]]),
|
|
81
|
+
[streamController]: {
|
|
82
|
+
registry: { acquire },
|
|
83
|
+
resolveSubagentNamespace,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
const uiMaps = new Set<Map<string, UIMessage[]>>();
|
|
87
|
+
let transcript: ReturnType<typeof useSubagentTranscripts> | undefined;
|
|
88
|
+
let rendered: string | undefined;
|
|
89
|
+
const host = (name: string) => () => {
|
|
90
|
+
const uiMessagesByParent = useMemo(
|
|
91
|
+
() => new Map<string, UIMessage[]>(),
|
|
92
|
+
[],
|
|
93
|
+
);
|
|
94
|
+
uiMaps.add(uiMessagesByParent);
|
|
95
|
+
rendered = name;
|
|
96
|
+
transcript = useSubagentTranscripts(stream as never, uiMessagesByParent);
|
|
97
|
+
return null;
|
|
98
|
+
};
|
|
99
|
+
const Before = host("before");
|
|
100
|
+
const After = host("after");
|
|
101
|
+
const view = render(<Before />);
|
|
102
|
+
|
|
103
|
+
await waitFor(() => expect(transcript?.has("task-one")).toBe(true));
|
|
104
|
+
expect(resolveSubagentNamespace).toHaveBeenCalledOnce();
|
|
105
|
+
expect(listeners.size).toBe(1);
|
|
106
|
+
await act(async () => {
|
|
107
|
+
messages = [
|
|
108
|
+
{ id: "message-one", _getType: () => "ai", content: "partial answer" },
|
|
109
|
+
];
|
|
110
|
+
for (const listener of listeners) listener();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const family: Family = { current: After };
|
|
114
|
+
renderer!.setRefreshHandler((type) =>
|
|
115
|
+
type === Before || type === After ? family : undefined,
|
|
116
|
+
);
|
|
117
|
+
await act(async () => {
|
|
118
|
+
for (const root of fiberRoots) {
|
|
119
|
+
renderer!.scheduleRefresh(root, {
|
|
120
|
+
staleFamilies: new Set(),
|
|
121
|
+
updatedFamilies: new Set([family]),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
await act(async () => {});
|
|
126
|
+
|
|
127
|
+
expect(rendered).toBe("after");
|
|
128
|
+
expect(uiMaps.size).toBeGreaterThan(1);
|
|
129
|
+
expect(release).not.toHaveBeenCalled();
|
|
130
|
+
expect(acquire).toHaveBeenCalledOnce();
|
|
131
|
+
expect(listeners.size).toBe(1);
|
|
132
|
+
expect(resolveSubagentNamespace).toHaveBeenCalledOnce();
|
|
133
|
+
|
|
134
|
+
stream.subagents = new Map([["task-one", snapshot("complete")]]);
|
|
135
|
+
view.rerender(<After />);
|
|
136
|
+
await waitFor(() =>
|
|
137
|
+
expect(
|
|
138
|
+
transcript?.get("task-one")?.messages[0]?.metadata?.timing,
|
|
139
|
+
).toMatchObject({
|
|
140
|
+
totalChunks: 2,
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
await act(async () => settleFirst());
|
|
144
|
+
await waitFor(() =>
|
|
145
|
+
expect(resolveSubagentNamespace).toHaveBeenCalledTimes(2),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
view.unmount();
|
|
149
|
+
await act(async () => {});
|
|
150
|
+
expect(release).toHaveBeenCalledOnce();
|
|
151
|
+
expect(listeners.size).toBe(0);
|
|
152
|
+
});
|
|
@@ -185,6 +185,7 @@ describe("useSubagentTranscripts", () => {
|
|
|
185
185
|
expect(stream.acquire).toHaveBeenCalledTimes(2);
|
|
186
186
|
expect(stream.resolveSubagentNamespace).not.toHaveBeenCalled();
|
|
187
187
|
hook.unmount();
|
|
188
|
+
await act(async () => {});
|
|
188
189
|
expect(stream.releases.get("tools:one")).toHaveBeenCalledOnce();
|
|
189
190
|
expect(stream.releases.get("tools:two")).toHaveBeenCalledOnce();
|
|
190
191
|
});
|
|
@@ -773,6 +774,7 @@ describe("useSubagentTranscripts", () => {
|
|
|
773
774
|
expect(channelSpecs[0]?.namespace).toEqual(["tools:parent", "tools:child"]);
|
|
774
775
|
|
|
775
776
|
hook.unmount();
|
|
777
|
+
await act(async () => {});
|
|
776
778
|
expect(
|
|
777
779
|
stream.uiReleases.get("tools:parent/tools:child"),
|
|
778
780
|
).toHaveBeenCalledOnce();
|
|
@@ -14,13 +14,10 @@ import {
|
|
|
14
14
|
type ExternalMessageConversionCache,
|
|
15
15
|
type useExternalMessageConverter,
|
|
16
16
|
} from "@assistant-ui/core/react";
|
|
17
|
+
import { useReplaySafeEffect } from "@assistant-ui/store/internal";
|
|
17
18
|
import { STREAM_CONTROLLER, type AnyStream } from "@langchain/react";
|
|
18
19
|
import type { BaseMessage } from "@langchain/core/messages";
|
|
19
|
-
import {
|
|
20
|
-
channelProjection,
|
|
21
|
-
messagesProjection,
|
|
22
|
-
type Event,
|
|
23
|
-
} from "@langchain/langgraph-sdk/stream";
|
|
20
|
+
import { channelProjection, type Event } from "@langchain/langgraph-sdk/stream";
|
|
24
21
|
import type { SubagentDiscoverySnapshot } from "@langchain/react";
|
|
25
22
|
import {
|
|
26
23
|
attachSubagentTranscripts,
|
|
@@ -31,6 +28,7 @@ import {
|
|
|
31
28
|
import { convertLangChainBaseMessage } from "./convertMessages";
|
|
32
29
|
import { groupUIMessagesByParent } from "./converter";
|
|
33
30
|
import { langChainStreamingTimingAccessors } from "./streamingTiming";
|
|
31
|
+
import { subagentMessagesProjection } from "./subagentMessagesProjection";
|
|
34
32
|
import type { LangChainBaseMessage, UIMessage } from "./types";
|
|
35
33
|
import {
|
|
36
34
|
createUIFoldMemo,
|
|
@@ -301,7 +299,7 @@ const createSubagentTranscriptSource = (): SubagentTranscriptSource => {
|
|
|
301
299
|
}
|
|
302
300
|
if (source.resources.has(snapshot.id)) continue;
|
|
303
301
|
const acquired = controller.registry.acquire(
|
|
304
|
-
|
|
302
|
+
subagentMessagesProjection(snapshot.namespace),
|
|
305
303
|
);
|
|
306
304
|
const acquiredUI =
|
|
307
305
|
snapshot.namespace.length > ROOT_UI_CHANNEL_DEPTH
|
|
@@ -516,7 +514,7 @@ export const useSubagentTranscripts = (
|
|
|
516
514
|
source.reconcile(controller, stream.subagents, uiMessagesByParent);
|
|
517
515
|
}, [controller, source, stream.subagents, uiMessagesByParent]);
|
|
518
516
|
|
|
519
|
-
|
|
517
|
+
useReplaySafeEffect(() => () => source.dispose(), [source]);
|
|
520
518
|
|
|
521
519
|
return useSyncExternalStore(
|
|
522
520
|
source.subscribe,
|