@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,50 @@
|
|
|
1
|
+
import type { BaseMessage } from "@langchain/core/messages";
|
|
2
|
+
import {
|
|
3
|
+
messagesProjection,
|
|
4
|
+
type ProjectionSpec,
|
|
5
|
+
} from "@langchain/langgraph-sdk/stream";
|
|
6
|
+
|
|
7
|
+
type MessagesProjection = ProjectionSpec<BaseMessage[]>;
|
|
8
|
+
|
|
9
|
+
type ProjectionThread = Parameters<MessagesProjection["open"]>[0]["thread"];
|
|
10
|
+
|
|
11
|
+
const exactDepthThread = (thread: ProjectionThread): ProjectionThread =>
|
|
12
|
+
new Proxy(thread, {
|
|
13
|
+
get(target, property) {
|
|
14
|
+
if (property === "subscribe") {
|
|
15
|
+
return (...[params]: Parameters<ProjectionThread["subscribe"]>) =>
|
|
16
|
+
target.subscribe({ ...params, depth: 0 });
|
|
17
|
+
}
|
|
18
|
+
const value = Reflect.get(target, property, target);
|
|
19
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The SDK's messages projection subscribes at the protocol's default depth
|
|
25
|
+
* of 1 and applies every delivered event, so a nested subagent's `values`
|
|
26
|
+
* snapshots rebuild its parent's store from the child's state while the child
|
|
27
|
+
* runs. The projection exposes no depth option, so this spec opens it against
|
|
28
|
+
* a thread whose subscription is pinned to depth 0: the namespace the SDK
|
|
29
|
+
* sets stays and only events at exactly that namespace are delivered, the
|
|
30
|
+
* rule the SDK's root projection already applies. The client unions every
|
|
31
|
+
* subscription's depth into the server filter, so a depth 0 subscription
|
|
32
|
+
* narrows nothing for other consumers.
|
|
33
|
+
*/
|
|
34
|
+
export const subagentMessagesProjection = (
|
|
35
|
+
namespace: readonly string[],
|
|
36
|
+
): MessagesProjection => {
|
|
37
|
+
const projection = messagesProjection(namespace);
|
|
38
|
+
return {
|
|
39
|
+
key: `exact|${projection.key}`,
|
|
40
|
+
namespace: projection.namespace,
|
|
41
|
+
initial: projection.initial,
|
|
42
|
+
open({ thread, store, rootBus }) {
|
|
43
|
+
return projection.open({
|
|
44
|
+
thread: exactDepthThread(thread),
|
|
45
|
+
store,
|
|
46
|
+
rootBus,
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { act, waitFor } from "@testing-library/react";
|
|
2
|
+
|
|
3
|
+
// React 18's act holds renders until its callback settles, so a task that waits on a render starts inside act and settles outside it, where React can commit what the task waits on.
|
|
4
|
+
export async function settleOutsideAct<T>(task: () => Promise<T>): Promise<T> {
|
|
5
|
+
let pending!: Promise<T>;
|
|
6
|
+
await act(async () => {
|
|
7
|
+
pending = task();
|
|
8
|
+
pending.catch(() => {});
|
|
9
|
+
});
|
|
10
|
+
await waitFor(() => pending.then(settled, settled));
|
|
11
|
+
await act(async () => {});
|
|
12
|
+
return pending;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const settled = () => {};
|
package/src/uiMessages.test.ts
CHANGED
|
@@ -2,10 +2,12 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
import {
|
|
3
3
|
applyUIUpdate,
|
|
4
4
|
createUIFoldMemo,
|
|
5
|
+
createUISnapshotMemo,
|
|
5
6
|
extractUIUpdate,
|
|
6
7
|
foldUIUpdates,
|
|
7
8
|
isUIUpdate,
|
|
8
9
|
mergeUIMessages,
|
|
10
|
+
reconcileUISnapshot,
|
|
9
11
|
} from "./uiMessages";
|
|
10
12
|
import type { UIMessage } from "./types";
|
|
11
13
|
|
|
@@ -138,6 +140,110 @@ describe("extractUIUpdate", () => {
|
|
|
138
140
|
});
|
|
139
141
|
});
|
|
140
142
|
|
|
143
|
+
describe("reconcileUISnapshot", () => {
|
|
144
|
+
const snapshot = (...entries: UIMessage[]) =>
|
|
145
|
+
entries.map((entry) => structuredClone(entry));
|
|
146
|
+
|
|
147
|
+
it("returns the previous list for an equal copy of the snapshot", () => {
|
|
148
|
+
const memo = createUISnapshotMemo();
|
|
149
|
+
const first = reconcileUISnapshot(
|
|
150
|
+
snapshot(ui("a", { x: 1 }), ui("b", { y: [1, 2] })),
|
|
151
|
+
memo,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const second = reconcileUISnapshot(
|
|
155
|
+
snapshot(ui("a", { x: 1 }), ui("b", { y: [1, 2] })),
|
|
156
|
+
memo,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
expect(second).toBe(first);
|
|
160
|
+
expect(second).toEqual([ui("a", { x: 1 }), ui("b", { y: [1, 2] })]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("returns the same list for the same snapshot reference", () => {
|
|
164
|
+
const memo = createUISnapshotMemo();
|
|
165
|
+
const value = snapshot(ui("a"));
|
|
166
|
+
const first = reconcileUISnapshot(value, memo);
|
|
167
|
+
|
|
168
|
+
expect(reconcileUISnapshot(value, memo)).toBe(first);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("replaces only the entry that changed", () => {
|
|
172
|
+
const memo = createUISnapshotMemo();
|
|
173
|
+
const [a, b] = reconcileUISnapshot(
|
|
174
|
+
snapshot(ui("a", { x: 1 }), ui("b", { y: 1 })),
|
|
175
|
+
memo,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const result = reconcileUISnapshot(
|
|
179
|
+
snapshot(ui("a", { x: 1 }), ui("b", { y: 2 })),
|
|
180
|
+
memo,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
expect(result[0]).toBe(a);
|
|
184
|
+
expect(result[1]).not.toBe(b);
|
|
185
|
+
expect(result[1]).toEqual(ui("b", { y: 2 }));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("keeps surviving entries across an addition and a removal", () => {
|
|
189
|
+
const memo = createUISnapshotMemo();
|
|
190
|
+
const [a, b] = reconcileUISnapshot(snapshot(ui("a"), ui("b")), memo);
|
|
191
|
+
|
|
192
|
+
const added = reconcileUISnapshot(
|
|
193
|
+
snapshot(ui("a"), ui("b"), ui("c")),
|
|
194
|
+
memo,
|
|
195
|
+
);
|
|
196
|
+
expect(added[0]).toBe(a);
|
|
197
|
+
expect(added[1]).toBe(b);
|
|
198
|
+
expect(added).toHaveLength(3);
|
|
199
|
+
|
|
200
|
+
const removed = reconcileUISnapshot(snapshot(ui("b"), ui("c")), memo);
|
|
201
|
+
expect(removed[0]).toBe(b);
|
|
202
|
+
expect(removed[1]).toBe(added[2]);
|
|
203
|
+
expect(removed).toHaveLength(2);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("returns a new list when equal entries change position", () => {
|
|
207
|
+
const memo = createUISnapshotMemo();
|
|
208
|
+
const [a, b] = reconcileUISnapshot(snapshot(ui("a"), ui("b")), memo);
|
|
209
|
+
|
|
210
|
+
const result = reconcileUISnapshot(snapshot(ui("b"), ui("a")), memo);
|
|
211
|
+
|
|
212
|
+
expect(result).toEqual([ui("b"), ui("a")]);
|
|
213
|
+
expect(result[0]).toBe(b);
|
|
214
|
+
expect(result[1]).toBe(a);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("treats an entry with a changed name or metadata as new", () => {
|
|
218
|
+
const memo = createUISnapshotMemo();
|
|
219
|
+
const [a] = reconcileUISnapshot(
|
|
220
|
+
snapshot({ ...ui("a"), metadata: { message_id: "m1" } }),
|
|
221
|
+
memo,
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const renamed = reconcileUISnapshot(
|
|
225
|
+
snapshot({ ...ui("a"), name: "table", metadata: { message_id: "m1" } }),
|
|
226
|
+
memo,
|
|
227
|
+
);
|
|
228
|
+
expect(renamed[0]).not.toBe(a);
|
|
229
|
+
|
|
230
|
+
const moved = reconcileUISnapshot(
|
|
231
|
+
snapshot({ ...ui("a"), name: "table", metadata: { message_id: "m2" } }),
|
|
232
|
+
memo,
|
|
233
|
+
);
|
|
234
|
+
expect(moved[0]).not.toBe(renamed[0]);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("returns an empty list for a non-array snapshot", () => {
|
|
238
|
+
const memo = createUISnapshotMemo();
|
|
239
|
+
expect(reconcileUISnapshot(undefined, memo)).toEqual([]);
|
|
240
|
+
reconcileUISnapshot(snapshot(ui("a")), memo);
|
|
241
|
+
|
|
242
|
+
expect(reconcileUISnapshot(null, memo)).toEqual([]);
|
|
243
|
+
expect(reconcileUISnapshot({ id: "a" }, memo)).toEqual([]);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
141
247
|
describe("mergeUIMessages", () => {
|
|
142
248
|
it("returns the snapshot when there are no live messages", () => {
|
|
143
249
|
expect(mergeUIMessages([], [ui("a")])).toEqual([ui("a")]);
|
package/src/uiMessages.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Channel } from "@langchain/react";
|
|
2
|
+
import { isJSONValueEqual } from "@assistant-ui/core/internal";
|
|
2
3
|
import type { RemoveUIMessage, UIMessage } from "./types";
|
|
3
4
|
|
|
4
5
|
/** Channels the generative-UI fold reads, at the root and per subagent. */
|
|
@@ -103,6 +104,48 @@ export const foldUIUpdates = (
|
|
|
103
104
|
return acc;
|
|
104
105
|
};
|
|
105
106
|
|
|
107
|
+
export type UISnapshotMemo = {
|
|
108
|
+
snapshot: unknown;
|
|
109
|
+
entries: readonly UIMessage[];
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const createUISnapshotMemo = (): UISnapshotMemo => ({
|
|
113
|
+
snapshot: undefined,
|
|
114
|
+
entries: [],
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Recovers entry identity across `values` events. The SDK rebuilds the
|
|
119
|
+
* `values` object from every snapshot and reconciles only the messages slot by
|
|
120
|
+
* id, so an unchanged UI list arrives as a new array of new objects on every
|
|
121
|
+
* superstep. An entry structurally equal to the previous entry with its id is
|
|
122
|
+
* replaced by that previous object, and a list whose entries all survive in
|
|
123
|
+
* place is replaced by the previous list.
|
|
124
|
+
*/
|
|
125
|
+
export const reconcileUISnapshot = (
|
|
126
|
+
snapshot: unknown,
|
|
127
|
+
memo: UISnapshotMemo,
|
|
128
|
+
): readonly UIMessage[] => {
|
|
129
|
+
if (snapshot === memo.snapshot) return memo.entries;
|
|
130
|
+
memo.snapshot = snapshot;
|
|
131
|
+
if (!Array.isArray(snapshot)) {
|
|
132
|
+
if (memo.entries.length > 0) memo.entries = [];
|
|
133
|
+
return memo.entries;
|
|
134
|
+
}
|
|
135
|
+
const previous = memo.entries;
|
|
136
|
+
const previousById = new Map(previous.map((ui) => [ui.id, ui]));
|
|
137
|
+
let same = snapshot.length === previous.length;
|
|
138
|
+
const entries = (snapshot as UIMessage[]).map((ui, index) => {
|
|
139
|
+
const before = previousById.get(ui.id);
|
|
140
|
+
const entry =
|
|
141
|
+
before !== undefined && isJSONValueEqual(before, ui) ? before : ui;
|
|
142
|
+
if (entry !== previous[index]) same = false;
|
|
143
|
+
return entry;
|
|
144
|
+
});
|
|
145
|
+
if (!same) memo.entries = entries;
|
|
146
|
+
return memo.entries;
|
|
147
|
+
};
|
|
148
|
+
|
|
106
149
|
/**
|
|
107
150
|
* Merges live-streamed UI with the state snapshot. The snapshot is
|
|
108
151
|
* authoritative by id: once a UI lands in graph state it supersedes its live
|
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
RemoteThreadListAdapter,
|
|
10
10
|
} from "@assistant-ui/core";
|
|
11
11
|
import { useAui } from "@assistant-ui/store";
|
|
12
|
-
import type { LangChainBaseMessage } from "./types";
|
|
12
|
+
import type { LangChainBaseMessage, LangChainToolCall } from "./types";
|
|
13
13
|
import { startTransition, Suspense, type ReactNode } from "react";
|
|
14
14
|
import {
|
|
15
15
|
useLangChainRespond,
|
|
@@ -32,6 +32,7 @@ vi.mock("@langchain/react", () => ({
|
|
|
32
32
|
}));
|
|
33
33
|
|
|
34
34
|
import { useStreamRuntime } from "./useStreamRuntime";
|
|
35
|
+
import { settleOutsideAct } from "./tests/settleOutsideAct";
|
|
35
36
|
|
|
36
37
|
type MockStream = {
|
|
37
38
|
messages: LangChainBaseMessage[];
|
|
@@ -194,18 +195,18 @@ describe("useStreamRuntime thread options", () => {
|
|
|
194
195
|
|
|
195
196
|
const view = render(<TestRuntime />);
|
|
196
197
|
|
|
197
|
-
await
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
await settleOutsideAct(() =>
|
|
199
|
+
capture.runtime!.threads.switchToThread("thread-a"),
|
|
200
|
+
);
|
|
200
201
|
|
|
201
202
|
const threadAOptions = mockUseStream.mock.calls
|
|
202
203
|
.map(([options]) => options as { threadId?: string | null })
|
|
203
204
|
.findLast((options) => options.threadId === "thread-a");
|
|
204
205
|
expect(threadAOptions).toBeDefined();
|
|
205
206
|
|
|
206
|
-
await
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
await settleOutsideAct(() =>
|
|
208
|
+
capture.runtime!.threads.switchToThread("thread-b"),
|
|
209
|
+
);
|
|
209
210
|
|
|
210
211
|
const threadBOptions = mockUseStream.mock.calls
|
|
211
212
|
.map(([options]) => options as { threadId?: string | null })
|
|
@@ -917,6 +918,45 @@ describe("useStreamRuntime staged messages", () => {
|
|
|
917
918
|
});
|
|
918
919
|
});
|
|
919
920
|
|
|
921
|
+
describe("useStreamRuntime pending tool call cancellation", () => {
|
|
922
|
+
it("cancels only the pending tool calls that carry an id", async () => {
|
|
923
|
+
const stream = createMockStream([
|
|
924
|
+
message("u1", "human", "look it up"),
|
|
925
|
+
{
|
|
926
|
+
id: "a1",
|
|
927
|
+
_getType: () => "ai",
|
|
928
|
+
content: "",
|
|
929
|
+
tool_calls: [
|
|
930
|
+
{ name: "lookup", args: {} } as LangChainToolCall,
|
|
931
|
+
{ id: "call-1", name: "search", args: {} },
|
|
932
|
+
],
|
|
933
|
+
},
|
|
934
|
+
]);
|
|
935
|
+
const { auiResult } = renderAui(stream);
|
|
936
|
+
|
|
937
|
+
await act(async () => {
|
|
938
|
+
await auiResult.current.thread.append({
|
|
939
|
+
role: "user",
|
|
940
|
+
content: [{ type: "text", text: "never mind" }],
|
|
941
|
+
});
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
expect(stream.submit).toHaveBeenCalledTimes(1);
|
|
945
|
+
expect(stream.submit.mock.calls[0]![0]).toEqual({
|
|
946
|
+
messages: [
|
|
947
|
+
{
|
|
948
|
+
type: "tool",
|
|
949
|
+
name: "search",
|
|
950
|
+
tool_call_id: "call-1",
|
|
951
|
+
content: JSON.stringify({ cancelled: true }),
|
|
952
|
+
status: "error",
|
|
953
|
+
},
|
|
954
|
+
{ id: expect.any(String), type: "human", content: "never mind" },
|
|
955
|
+
],
|
|
956
|
+
});
|
|
957
|
+
});
|
|
958
|
+
});
|
|
959
|
+
|
|
920
960
|
describe("useStreamRuntime committed refs", () => {
|
|
921
961
|
it("submits through the committed stream after an abandoned render", async () => {
|
|
922
962
|
const streamA = createMockStream();
|
|
@@ -1072,6 +1112,106 @@ describe("useStreamRuntime subagent transcripts", () => {
|
|
|
1072
1112
|
expect(nestedTranscript()).toBe(rendered);
|
|
1073
1113
|
});
|
|
1074
1114
|
|
|
1115
|
+
it("keeps messages and transcripts across equal copies of the UI state", async () => {
|
|
1116
|
+
const stream = createMockStream([
|
|
1117
|
+
message("human-1", "human", "delegate"),
|
|
1118
|
+
{
|
|
1119
|
+
id: "root-ai",
|
|
1120
|
+
_getType: () => "ai",
|
|
1121
|
+
content: "",
|
|
1122
|
+
tool_calls: [{ id: "task-one", name: "task", args: {} }],
|
|
1123
|
+
},
|
|
1124
|
+
]);
|
|
1125
|
+
const transcript = [message("nested-ai", "ai", "nested answer")];
|
|
1126
|
+
stream.subagents = new Map([
|
|
1127
|
+
[
|
|
1128
|
+
"task-one",
|
|
1129
|
+
{
|
|
1130
|
+
id: "task-one",
|
|
1131
|
+
namespace: ["tools:task-one"],
|
|
1132
|
+
status: "running",
|
|
1133
|
+
parentId: null,
|
|
1134
|
+
depth: 1,
|
|
1135
|
+
startedAt: new Date(1_000),
|
|
1136
|
+
completedAt: null,
|
|
1137
|
+
},
|
|
1138
|
+
],
|
|
1139
|
+
]);
|
|
1140
|
+
stream[streamController]!.registry.acquire.mockReturnValue({
|
|
1141
|
+
store: { getSnapshot: () => transcript, subscribe: () => () => {} },
|
|
1142
|
+
release: vi.fn(),
|
|
1143
|
+
});
|
|
1144
|
+
const uiState = (points: number[]) => [
|
|
1145
|
+
{
|
|
1146
|
+
type: "ui",
|
|
1147
|
+
id: "ui-root",
|
|
1148
|
+
name: "chart",
|
|
1149
|
+
props: { points },
|
|
1150
|
+
metadata: { message_id: "root-ai" },
|
|
1151
|
+
},
|
|
1152
|
+
{
|
|
1153
|
+
type: "ui",
|
|
1154
|
+
id: "ui-nested",
|
|
1155
|
+
name: "chart",
|
|
1156
|
+
props: { points },
|
|
1157
|
+
metadata: { message_id: "nested-ai" },
|
|
1158
|
+
},
|
|
1159
|
+
];
|
|
1160
|
+
stream.values = { ui: uiState([1, 2]) };
|
|
1161
|
+
const { auiResult, rerender } = renderAui(stream);
|
|
1162
|
+
const nestedTranscript = () => {
|
|
1163
|
+
const { messages } = auiResult.current.thread.getState();
|
|
1164
|
+
for (const threadMessage of messages) {
|
|
1165
|
+
for (const part of threadMessage.content) {
|
|
1166
|
+
if (part.type === "tool-call" && part.toolCallId === "task-one")
|
|
1167
|
+
return part.messages;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return undefined;
|
|
1171
|
+
};
|
|
1172
|
+
|
|
1173
|
+
await waitFor(() =>
|
|
1174
|
+
expect(nestedTranscript()?.[0]?.content).toMatchObject([
|
|
1175
|
+
{ type: "text", text: "nested answer" },
|
|
1176
|
+
{ type: "data", name: "chart", data: { points: [1, 2] } },
|
|
1177
|
+
]),
|
|
1178
|
+
);
|
|
1179
|
+
const [human, ai] = auiResult.current.thread.getState().messages;
|
|
1180
|
+
expect(ai?.content).toMatchObject([
|
|
1181
|
+
{ type: "tool-call", toolCallId: "task-one" },
|
|
1182
|
+
{ type: "data", name: "chart", data: { points: [1, 2] } },
|
|
1183
|
+
]);
|
|
1184
|
+
const rendered = nestedTranscript();
|
|
1185
|
+
|
|
1186
|
+
for (let i = 0; i < 3; i++) {
|
|
1187
|
+
stream.values = { ui: uiState([1, 2]) };
|
|
1188
|
+
await act(async () => {
|
|
1189
|
+
rerender();
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
const messages = auiResult.current.thread.getState().messages;
|
|
1194
|
+
expect(messages[0]).toBe(human);
|
|
1195
|
+
expect(messages[1]).toBe(ai);
|
|
1196
|
+
expect(nestedTranscript()).toBe(rendered);
|
|
1197
|
+
|
|
1198
|
+
stream.values = { ui: uiState([1, 2, 3]) };
|
|
1199
|
+
await act(async () => {
|
|
1200
|
+
rerender();
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
expect(
|
|
1204
|
+
auiResult.current.thread.getState().messages[1]?.content,
|
|
1205
|
+
).toMatchObject([
|
|
1206
|
+
{ type: "tool-call", toolCallId: "task-one" },
|
|
1207
|
+
{ type: "data", name: "chart", data: { points: [1, 2, 3] } },
|
|
1208
|
+
]);
|
|
1209
|
+
expect(nestedTranscript()?.[0]?.content).toMatchObject([
|
|
1210
|
+
{ type: "text", text: "nested answer" },
|
|
1211
|
+
{ type: "data", name: "chart", data: { points: [1, 2, 3] } },
|
|
1212
|
+
]);
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1075
1215
|
it("keeps messages and transcripts when custom events carry no UI update", async () => {
|
|
1076
1216
|
const stream = createMockStream([
|
|
1077
1217
|
message("human-1", "human", "delegate"),
|