@assistant-ui/react-ag-ui 0.1.2 → 0.1.3
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/dist/runtime/AgUiThreadController.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadController.js +31 -4
- package/dist/runtime/AgUiThreadController.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +1 -0
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +8 -0
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/package.json +5 -4
- package/src/runtime/AgUiThreadController.snapshot-head-move.test.ts +180 -1
- package/src/runtime/AgUiThreadController.test.ts +133 -1
- package/src/runtime/AgUiThreadController.ts +70 -7
- package/src/runtime/adapter/run-aggregator.ts +11 -0
- package/src/useAgUiRuntime.cloud-run-leases.test.tsx +384 -0
- package/src/useAgUiRuntime.state.test.tsx +87 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { act, cleanup, render, waitFor } from "@testing-library/react";
|
|
4
|
+
import { HttpAgent, type RunAgentInput } from "@ag-ui/client";
|
|
5
|
+
import {
|
|
6
|
+
AssistantRuntimeProvider,
|
|
7
|
+
useCloudThreadListAdapter,
|
|
8
|
+
useRemoteThreadListRuntime,
|
|
9
|
+
} from "@assistant-ui/core/react";
|
|
10
|
+
import type { AssistantRuntime } from "@assistant-ui/core";
|
|
11
|
+
import { useAui, type AssistantClient } from "@assistant-ui/store";
|
|
12
|
+
import { AssistantCloud } from "assistant-cloud";
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
14
|
+
import { useAgUiRuntime } from "./useAgUiRuntime";
|
|
15
|
+
|
|
16
|
+
// The real AssistantCloud client runs against this in-memory server.
|
|
17
|
+
type Row = {
|
|
18
|
+
id: string;
|
|
19
|
+
parent_id: string | null;
|
|
20
|
+
format: string;
|
|
21
|
+
content: unknown;
|
|
22
|
+
external_id: string | null;
|
|
23
|
+
run?: { status: "running" | "ended"; last_heartbeat_ms_ago: number };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type Call = { method: string; path: string; body: any };
|
|
27
|
+
|
|
28
|
+
const createServer = (
|
|
29
|
+
initialRows: Row[] = [],
|
|
30
|
+
heartbeatIntervalMs = 20_000,
|
|
31
|
+
) => {
|
|
32
|
+
const rows = [...initialRows];
|
|
33
|
+
const calls: Call[] = [];
|
|
34
|
+
let created = 0;
|
|
35
|
+
let leases = 0;
|
|
36
|
+
const at = new Date(0).toISOString();
|
|
37
|
+
const json = (value: unknown, status = 200) =>
|
|
38
|
+
new Response(JSON.stringify(value), {
|
|
39
|
+
status,
|
|
40
|
+
headers: { "content-type": "application/json" },
|
|
41
|
+
});
|
|
42
|
+
const fetchMock = vi.fn(
|
|
43
|
+
async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
44
|
+
const url = new URL(String(input));
|
|
45
|
+
const method = init?.method ?? "GET";
|
|
46
|
+
const path = url.pathname.replace(/^\/v1/, "");
|
|
47
|
+
const body = init?.body ? JSON.parse(String(init.body)) : undefined;
|
|
48
|
+
calls.push({ method, path, body });
|
|
49
|
+
if (method === "GET" && path === "/threads") {
|
|
50
|
+
return json({
|
|
51
|
+
threads: rows.length
|
|
52
|
+
? [
|
|
53
|
+
{
|
|
54
|
+
id: "remote-1",
|
|
55
|
+
title: "t",
|
|
56
|
+
is_archived: false,
|
|
57
|
+
external_id: null,
|
|
58
|
+
metadata: null,
|
|
59
|
+
project_id: "p",
|
|
60
|
+
workspace_id: "w",
|
|
61
|
+
created_at: at,
|
|
62
|
+
updated_at: at,
|
|
63
|
+
last_message_at: at,
|
|
64
|
+
},
|
|
65
|
+
]
|
|
66
|
+
: [],
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (method === "POST" && path === "/threads")
|
|
70
|
+
return json({ thread_id: "remote-1" });
|
|
71
|
+
if (method === "GET" && path === "/threads/remote-1/messages") {
|
|
72
|
+
return json({
|
|
73
|
+
messages: [...rows].reverse().map((row, height) => ({
|
|
74
|
+
...row,
|
|
75
|
+
height,
|
|
76
|
+
created_at: at,
|
|
77
|
+
updated_at: at,
|
|
78
|
+
run: row.run ?? null,
|
|
79
|
+
})),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (method === "POST" && path === "/threads/remote-1/messages") {
|
|
83
|
+
const id = `msg_${++created}`;
|
|
84
|
+
rows.push({
|
|
85
|
+
id,
|
|
86
|
+
parent_id: body.parent_id,
|
|
87
|
+
format: body.format,
|
|
88
|
+
content: body.content,
|
|
89
|
+
external_id: body.external_id ?? null,
|
|
90
|
+
...(body.open_run
|
|
91
|
+
? { run: { status: "running", last_heartbeat_ms_ago: 0 } }
|
|
92
|
+
: undefined),
|
|
93
|
+
});
|
|
94
|
+
return json({
|
|
95
|
+
message_id: id,
|
|
96
|
+
...(body.open_run
|
|
97
|
+
? {
|
|
98
|
+
lease_id: `lease_${++leases}`,
|
|
99
|
+
heartbeat_interval_ms: heartbeatIntervalMs,
|
|
100
|
+
}
|
|
101
|
+
: undefined),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const write = path.match(/^\/threads\/remote-1\/messages\/([^/]+)$/);
|
|
105
|
+
if (method === "PUT" && write) {
|
|
106
|
+
const row = rows.find((r) => r.id === write[1]);
|
|
107
|
+
if (row) row.content = body.content;
|
|
108
|
+
return json({});
|
|
109
|
+
}
|
|
110
|
+
if (/^\/threads\/remote-1\/runs\/[^/]+\/heartbeat$/.test(path))
|
|
111
|
+
return json({ heartbeat_interval_ms: heartbeatIntervalMs });
|
|
112
|
+
if (/^\/threads\/remote-1\/runs\/[^/]+\/end$/.test(path)) return json({});
|
|
113
|
+
return json({});
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
return { rows, calls, fetchMock };
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const createsWith = (calls: Call[], openRun: boolean) =>
|
|
120
|
+
calls.filter(
|
|
121
|
+
(c) =>
|
|
122
|
+
c.method === "POST" &&
|
|
123
|
+
c.path === "/threads/remote-1/messages" &&
|
|
124
|
+
!!c.body.open_run === openRun,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
type Subscriber = Record<
|
|
128
|
+
string,
|
|
129
|
+
((payload: { event: any }) => void) | undefined
|
|
130
|
+
>;
|
|
131
|
+
|
|
132
|
+
// An AG-UI agent whose runs are scripted: it streams one text message, under an id the backend picks, and finishes when the test says so.
|
|
133
|
+
class ScriptedAgent extends HttpAgent {
|
|
134
|
+
runs: { finish: () => void; input: RunAgentInput }[] = [];
|
|
135
|
+
constructor() {
|
|
136
|
+
super({ url: "http://agent.test" });
|
|
137
|
+
}
|
|
138
|
+
override async runAgent(
|
|
139
|
+
parameters?: Partial<RunAgentInput>,
|
|
140
|
+
subscriber?: Subscriber,
|
|
141
|
+
): Promise<any> {
|
|
142
|
+
const input = parameters as RunAgentInput;
|
|
143
|
+
const id = `agent-msg-${this.runs.length + 1}`;
|
|
144
|
+
const emit = (name: string, event: object) =>
|
|
145
|
+
subscriber?.[name]?.({ event });
|
|
146
|
+
emit("onRunStartedEvent", {
|
|
147
|
+
type: "RUN_STARTED",
|
|
148
|
+
threadId: input.threadId,
|
|
149
|
+
runId: input.runId,
|
|
150
|
+
});
|
|
151
|
+
emit("onTextMessageStartEvent", {
|
|
152
|
+
type: "TEXT_MESSAGE_START",
|
|
153
|
+
messageId: id,
|
|
154
|
+
role: "assistant",
|
|
155
|
+
});
|
|
156
|
+
emit("onTextMessageContentEvent", {
|
|
157
|
+
type: "TEXT_MESSAGE_CONTENT",
|
|
158
|
+
messageId: id,
|
|
159
|
+
delta: "partial",
|
|
160
|
+
});
|
|
161
|
+
await new Promise<void>((resolve) =>
|
|
162
|
+
this.runs.push({ finish: resolve, input }),
|
|
163
|
+
);
|
|
164
|
+
emit("onTextMessageContentEvent", {
|
|
165
|
+
type: "TEXT_MESSAGE_CONTENT",
|
|
166
|
+
messageId: id,
|
|
167
|
+
delta: " answer",
|
|
168
|
+
});
|
|
169
|
+
emit("onTextMessageEndEvent", { type: "TEXT_MESSAGE_END", messageId: id });
|
|
170
|
+
emit("onRunFinishedEvent", {
|
|
171
|
+
type: "RUN_FINISHED",
|
|
172
|
+
threadId: input.threadId,
|
|
173
|
+
runId: input.runId,
|
|
174
|
+
});
|
|
175
|
+
return { result: undefined, newMessages: [] };
|
|
176
|
+
}
|
|
177
|
+
override abortRun() {}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const renderApp = async (staleAfter?: number) => {
|
|
181
|
+
const cloud = new AssistantCloud({
|
|
182
|
+
baseUrl: "http://cloud.test",
|
|
183
|
+
apiKey: "key",
|
|
184
|
+
userId: "user",
|
|
185
|
+
workspaceId: "workspace",
|
|
186
|
+
telemetry: false,
|
|
187
|
+
});
|
|
188
|
+
const agent = new ScriptedAgent();
|
|
189
|
+
let aui!: AssistantClient;
|
|
190
|
+
let runtime!: AssistantRuntime;
|
|
191
|
+
const Capture = () => {
|
|
192
|
+
aui = useAui();
|
|
193
|
+
return null;
|
|
194
|
+
};
|
|
195
|
+
const App = () => {
|
|
196
|
+
const adapter = useCloudThreadListAdapter({
|
|
197
|
+
cloud,
|
|
198
|
+
...(staleAfter !== undefined ? { staleAfter } : undefined),
|
|
199
|
+
});
|
|
200
|
+
runtime = useRemoteThreadListRuntime({
|
|
201
|
+
adapter,
|
|
202
|
+
runtimeHook: function RuntimeHook() {
|
|
203
|
+
return useAgUiRuntime({ agent });
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
return (
|
|
207
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
208
|
+
<Capture />
|
|
209
|
+
</AssistantRuntimeProvider>
|
|
210
|
+
);
|
|
211
|
+
};
|
|
212
|
+
render(<App />);
|
|
213
|
+
await act(async () => {
|
|
214
|
+
await aui.threads.getLoadThreadsPromise();
|
|
215
|
+
});
|
|
216
|
+
return { agent, getAui: () => aui, getRuntime: () => runtime };
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const assistantRow = (
|
|
220
|
+
last_heartbeat_ms_ago: number,
|
|
221
|
+
status: "running" | "ended" = "running",
|
|
222
|
+
): Row => ({
|
|
223
|
+
id: "a1",
|
|
224
|
+
parent_id: "u1",
|
|
225
|
+
format: "aui/v0",
|
|
226
|
+
content: {
|
|
227
|
+
role: "assistant",
|
|
228
|
+
content: [{ type: "text", text: "half an answer" }],
|
|
229
|
+
status: { type: "incomplete", reason: "cancelled" },
|
|
230
|
+
},
|
|
231
|
+
external_id: "a1",
|
|
232
|
+
run: { status, last_heartbeat_ms_ago },
|
|
233
|
+
});
|
|
234
|
+
const userRow: Row = {
|
|
235
|
+
id: "u1",
|
|
236
|
+
parent_id: null,
|
|
237
|
+
format: "aui/v0",
|
|
238
|
+
content: { role: "user", content: [{ type: "text", text: "hello" }] },
|
|
239
|
+
external_id: "u1",
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
let server: ReturnType<typeof createServer>;
|
|
243
|
+
const useServer = (rows?: Row[], heartbeatIntervalMs?: number) => {
|
|
244
|
+
server = createServer(rows, heartbeatIntervalMs);
|
|
245
|
+
vi.stubGlobal("fetch", server.fetchMock);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
beforeEach(() => {
|
|
249
|
+
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
250
|
+
});
|
|
251
|
+
afterEach(() => {
|
|
252
|
+
cleanup();
|
|
253
|
+
vi.unstubAllGlobals();
|
|
254
|
+
vi.restoreAllMocks();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe("useAgUiRuntime with Assistant Cloud run leases", () => {
|
|
258
|
+
it("opens a lease when the run starts, heartbeats on the server's interval, writes as its holder and ends it when the run finishes", async () => {
|
|
259
|
+
useServer([], 30);
|
|
260
|
+
const { agent, getAui } = await renderApp();
|
|
261
|
+
|
|
262
|
+
await act(async () => {
|
|
263
|
+
getAui().thread.append("hello");
|
|
264
|
+
});
|
|
265
|
+
await waitFor(() => expect(agent.runs).toHaveLength(1));
|
|
266
|
+
await waitFor(() =>
|
|
267
|
+
expect(createsWith(server.calls, true)).toHaveLength(1),
|
|
268
|
+
);
|
|
269
|
+
const open = createsWith(server.calls, true)[0]!;
|
|
270
|
+
expect(open.body).toMatchObject({
|
|
271
|
+
format: "aui/v0",
|
|
272
|
+
parent_id: "msg_1",
|
|
273
|
+
content: { role: "assistant" },
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
await waitFor(() =>
|
|
277
|
+
expect(
|
|
278
|
+
server.calls.filter((c) => c.path.endsWith("/runs/lease_1/heartbeat"))
|
|
279
|
+
.length,
|
|
280
|
+
).toBeGreaterThanOrEqual(2),
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
await act(async () => {
|
|
284
|
+
agent.runs[0]!.finish();
|
|
285
|
+
});
|
|
286
|
+
await waitFor(() =>
|
|
287
|
+
expect(
|
|
288
|
+
server.calls.some(
|
|
289
|
+
(c) => c.method === "POST" && c.path.endsWith("/runs/lease_1/end"),
|
|
290
|
+
),
|
|
291
|
+
).toBe(true),
|
|
292
|
+
);
|
|
293
|
+
const end = server.calls.find((c) => c.path.endsWith("/runs/lease_1/end"))!;
|
|
294
|
+
expect(end.body).toEqual({ reason: "done" });
|
|
295
|
+
const final = server.calls.filter(
|
|
296
|
+
(c) => c.method === "PUT" && c.body.lease_id === "lease_1",
|
|
297
|
+
);
|
|
298
|
+
expect(final.length).toBeGreaterThan(0);
|
|
299
|
+
expect(JSON.stringify(final.at(-1)!.body.content)).toContain(
|
|
300
|
+
"partial answer",
|
|
301
|
+
);
|
|
302
|
+
expect(createsWith(server.calls, true)).toHaveLength(1);
|
|
303
|
+
const reply = getAui().thread.getState().messages[1]!;
|
|
304
|
+
expect(reply.id).toBe("agent-msg-1");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("shows a reloaded run quiet for longer than staleAfter as interrupted; Resume posts a sibling with its own lease and runs the agent again", async () => {
|
|
308
|
+
useServer([userRow, assistantRow(45_000)]);
|
|
309
|
+
const { agent, getAui, getRuntime } = await renderApp();
|
|
310
|
+
await act(async () => {
|
|
311
|
+
await getRuntime().threads.switchToThread("remote-1");
|
|
312
|
+
});
|
|
313
|
+
await waitFor(() =>
|
|
314
|
+
expect(getAui().thread.getState().messages).toHaveLength(2),
|
|
315
|
+
);
|
|
316
|
+
const stale = getAui().thread.message({ index: 1 });
|
|
317
|
+
await waitFor(() => expect(stale.getState().runState).toBe("stale"));
|
|
318
|
+
|
|
319
|
+
await act(async () => {
|
|
320
|
+
stale.reload();
|
|
321
|
+
});
|
|
322
|
+
await waitFor(() => expect(agent.runs).toHaveLength(1));
|
|
323
|
+
await waitFor(() =>
|
|
324
|
+
expect(createsWith(server.calls, true)).toHaveLength(1),
|
|
325
|
+
);
|
|
326
|
+
expect(createsWith(server.calls, true)[0]!.body).toMatchObject({
|
|
327
|
+
parent_id: "u1",
|
|
328
|
+
open_run: true,
|
|
329
|
+
});
|
|
330
|
+
await act(async () => {
|
|
331
|
+
agent.runs[0]!.finish();
|
|
332
|
+
});
|
|
333
|
+
await waitFor(() =>
|
|
334
|
+
expect(
|
|
335
|
+
server.calls.some((c) => c.path.endsWith("/runs/lease_1/end")),
|
|
336
|
+
).toBe(true),
|
|
337
|
+
);
|
|
338
|
+
expect(
|
|
339
|
+
server.calls.some((c) => c.method === "PUT" && c.path.endsWith("/a1")),
|
|
340
|
+
).toBe(false);
|
|
341
|
+
const messages = getAui().thread.getState().messages;
|
|
342
|
+
expect(messages.map((m) => m.role)).toEqual(["user", "assistant"]);
|
|
343
|
+
expect(messages[1]!.id).not.toBe("a1");
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("shows a reloaded run with a recent beat as in progress, and staleAfter moves the line", async () => {
|
|
347
|
+
useServer([userRow, assistantRow(5_000)]);
|
|
348
|
+
const { getAui, getRuntime } = await renderApp();
|
|
349
|
+
await act(async () => {
|
|
350
|
+
await getRuntime().threads.switchToThread("remote-1");
|
|
351
|
+
});
|
|
352
|
+
await waitFor(() =>
|
|
353
|
+
expect(getAui().thread.message({ index: 1 }).getState().runState).toBe(
|
|
354
|
+
"in-progress",
|
|
355
|
+
),
|
|
356
|
+
);
|
|
357
|
+
cleanup();
|
|
358
|
+
|
|
359
|
+
useServer([userRow, assistantRow(5_000)]);
|
|
360
|
+
const short = await renderApp(2_000);
|
|
361
|
+
await act(async () => {
|
|
362
|
+
await short.getRuntime().threads.switchToThread("remote-1");
|
|
363
|
+
});
|
|
364
|
+
await waitFor(() =>
|
|
365
|
+
expect(
|
|
366
|
+
short.getAui().thread.message({ index: 1 }).getState().runState,
|
|
367
|
+
).toBe("stale"),
|
|
368
|
+
);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("loads an ended run as a plain message", async () => {
|
|
372
|
+
useServer([userRow, assistantRow(90_000, "ended")]);
|
|
373
|
+
const { getAui, getRuntime } = await renderApp();
|
|
374
|
+
await act(async () => {
|
|
375
|
+
await getRuntime().threads.switchToThread("remote-1");
|
|
376
|
+
});
|
|
377
|
+
await waitFor(() =>
|
|
378
|
+
expect(getAui().thread.getState().messages).toHaveLength(2),
|
|
379
|
+
);
|
|
380
|
+
expect(
|
|
381
|
+
getAui().thread.message({ index: 1 }).getState().runState,
|
|
382
|
+
).toBeUndefined();
|
|
383
|
+
});
|
|
384
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { HttpAgent } from "@ag-ui/client";
|
|
4
|
+
import { act, cleanup, renderHook, waitFor } from "@testing-library/react";
|
|
5
|
+
import { afterEach, expect, it } from "vitest";
|
|
6
|
+
import { useAgUiRuntime } from "./useAgUiRuntime";
|
|
7
|
+
|
|
8
|
+
afterEach(cleanup);
|
|
9
|
+
|
|
10
|
+
it("sends initialState on the first request and keeps middleware clientContext out of thread.state", async () => {
|
|
11
|
+
const sentStates: unknown[] = [];
|
|
12
|
+
const agent = new HttpAgent({
|
|
13
|
+
url: "http://agent.test/agent",
|
|
14
|
+
initialState: { fromInitialState: true },
|
|
15
|
+
fetch: async (_url, init) => {
|
|
16
|
+
const body = JSON.parse(String(init?.body));
|
|
17
|
+
sentStates.push(body.state);
|
|
18
|
+
const events = [
|
|
19
|
+
{ type: "RUN_STARTED", threadId: body.threadId, runId: body.runId },
|
|
20
|
+
{ type: "STATE_DELTA", delta: [{ op: "add", path: "/foo", value: 1 }] },
|
|
21
|
+
{
|
|
22
|
+
type: "TEXT_MESSAGE_START",
|
|
23
|
+
messageId: `m-${body.runId}`,
|
|
24
|
+
role: "assistant",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
type: "TEXT_MESSAGE_CONTENT",
|
|
28
|
+
messageId: `m-${body.runId}`,
|
|
29
|
+
delta: "ok",
|
|
30
|
+
},
|
|
31
|
+
{ type: "TEXT_MESSAGE_END", messageId: `m-${body.runId}` },
|
|
32
|
+
{ type: "RUN_FINISHED", threadId: body.threadId, runId: body.runId },
|
|
33
|
+
];
|
|
34
|
+
return new Response(
|
|
35
|
+
events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join(""),
|
|
36
|
+
{ headers: { "content-type": "text/event-stream" } },
|
|
37
|
+
);
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
agent.use((input, next) =>
|
|
41
|
+
next.run({ ...input, state: { ...input.state, clientContext: "x" } }),
|
|
42
|
+
);
|
|
43
|
+
const { result } = renderHook(() => useAgUiRuntime({ agent }));
|
|
44
|
+
const { result: otherThread } = renderHook(() => useAgUiRuntime({ agent }));
|
|
45
|
+
|
|
46
|
+
expect(result.current.thread.getState().state).toEqual({
|
|
47
|
+
fromInitialState: true,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
for (const text of ["first", "second"]) {
|
|
51
|
+
await act(async () => {
|
|
52
|
+
await result.current.thread.append({
|
|
53
|
+
role: "user",
|
|
54
|
+
content: [{ type: "text", text }],
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
await waitFor(() =>
|
|
58
|
+
expect(result.current.thread.getState().isRunning).toBe(false),
|
|
59
|
+
);
|
|
60
|
+
expect(otherThread.current.thread.getState().state).toEqual({
|
|
61
|
+
fromInitialState: true,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
expect(sentStates).toEqual([
|
|
66
|
+
{ fromInitialState: true, clientContext: "x" },
|
|
67
|
+
{ fromInitialState: true, foo: 1, clientContext: "x" },
|
|
68
|
+
]);
|
|
69
|
+
expect(result.current.thread.getState().state).toEqual({
|
|
70
|
+
fromInitialState: true,
|
|
71
|
+
foo: 1,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await act(async () => {
|
|
75
|
+
await otherThread.current.thread.append({
|
|
76
|
+
role: "user",
|
|
77
|
+
content: [{ type: "text", text: "other thread" }],
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
await waitFor(() =>
|
|
81
|
+
expect(otherThread.current.thread.getState().isRunning).toBe(false),
|
|
82
|
+
);
|
|
83
|
+
expect(sentStates[2]).toEqual({
|
|
84
|
+
fromInitialState: true,
|
|
85
|
+
clientContext: "x",
|
|
86
|
+
});
|
|
87
|
+
});
|