@assistant-ui/react-ag-ui 0.0.65 → 0.0.66

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.
@@ -1,20 +1,25 @@
1
1
  import { describe, it, expect, vi } from "vitest";
2
2
  import type { AbstractAgent } from "@ag-ui/client";
3
- import type { AppendMessage, ThreadHistoryAdapter } from "@assistant-ui/core";
3
+ import type {
4
+ AppendMessage,
5
+ ThreadAssistantMessage,
6
+ ThreadHistoryAdapter,
7
+ ThreadMessage,
8
+ } from "@assistant-ui/core";
4
9
  import { AgUiThreadRuntimeCore } from "./AgUiThreadRuntimeCore";
5
10
  import { makeLogger } from "./logger";
6
11
 
7
12
  const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
8
13
 
9
- function createCore(history?: ThreadHistoryAdapter) {
10
- const agent = {} as AbstractAgent;
11
- const notifyUpdate = vi.fn();
14
+ function createCore(
15
+ history?: ThreadHistoryAdapter,
16
+ agent = {} as AbstractAgent,
17
+ ) {
12
18
  const core = new AgUiThreadRuntimeCore({
13
19
  agent,
14
20
  logger: makeLogger(),
15
21
  showThinking: true,
16
22
  ...(history && { history }),
17
- notifyUpdate,
18
23
  });
19
24
  const update = (nextHistory?: ThreadHistoryAdapter) =>
20
25
  core.updateOptions({
@@ -55,7 +60,377 @@ const userMessage = (text: string) =>
55
60
  startRun: false,
56
61
  }) as unknown as AppendMessage;
57
62
 
63
+ const storedUser: ThreadMessage = {
64
+ id: "restored",
65
+ role: "user",
66
+ content: [{ type: "text", text: "earlier" }],
67
+ attachments: [],
68
+ createdAt: new Date(0),
69
+ metadata: { custom: {} },
70
+ };
71
+
72
+ const storedToolCall: ThreadAssistantMessage = {
73
+ id: "assistant",
74
+ role: "assistant",
75
+ content: [
76
+ {
77
+ type: "tool-call",
78
+ toolCallId: "call",
79
+ toolName: "lookup",
80
+ args: {},
81
+ argsText: "{}",
82
+ },
83
+ ],
84
+ status: { type: "requires-action", reason: "tool-calls" },
85
+ createdAt: new Date(0),
86
+ metadata: {
87
+ custom: {},
88
+ unstable_state: null,
89
+ unstable_annotations: [],
90
+ unstable_data: [],
91
+ steps: [],
92
+ },
93
+ };
94
+
95
+ const storedInterrupt: ThreadAssistantMessage = {
96
+ ...storedToolCall,
97
+ status: { type: "requires-action", reason: "interrupt" },
98
+ content: [
99
+ {
100
+ ...storedToolCall.content[0]!,
101
+ approval: { id: "gate", prompt: "Continue?" },
102
+ },
103
+ ] as ThreadAssistantMessage["content"],
104
+ metadata: {
105
+ ...storedToolCall.metadata,
106
+ custom: {
107
+ agui: {
108
+ interrupts: [{ id: "gate", reason: "tool_call", toolCallId: "call" }],
109
+ },
110
+ },
111
+ },
112
+ };
113
+
114
+ function deferredHistory(messages: ThreadMessage[] = [storedUser]) {
115
+ let finishLoad!: (
116
+ repo: Awaited<ReturnType<ThreadHistoryAdapter["load"]>>,
117
+ ) => void;
118
+ const load = vi.fn(
119
+ () =>
120
+ new Promise<Awaited<ReturnType<ThreadHistoryAdapter["load"]>>>(
121
+ (resolve) => {
122
+ finishLoad = resolve;
123
+ },
124
+ ),
125
+ );
126
+ const history: ThreadHistoryAdapter = {
127
+ load,
128
+ append: vi.fn().mockResolvedValue(undefined),
129
+ };
130
+ return {
131
+ history,
132
+ finish: (unstableResume = false) =>
133
+ finishLoad({
134
+ headId: messages.at(-1)?.id ?? null,
135
+ ...(unstableResume && { unstable_resume: true }),
136
+ messages: messages.map((message, index) => ({
137
+ parentId: messages[index - 1]?.id ?? null,
138
+ message,
139
+ })),
140
+ }),
141
+ };
142
+ }
143
+
58
144
  describe("AgUiThreadRuntimeCore late history loading", () => {
145
+ it("drops a queued send when loading is cancelled", async () => {
146
+ const { history, finish } = deferredHistory();
147
+ const runAgent = vi.fn(async () => {});
148
+ const { core } = createCore(history, {
149
+ runAgent,
150
+ abortRun: vi.fn(),
151
+ } as unknown as AbstractAgent);
152
+ const loading = core.__internal_load();
153
+ const sending = core.append(userMessage("cancelled"));
154
+
155
+ await core.cancel();
156
+ finish();
157
+ await Promise.all([loading, sending]);
158
+
159
+ expect(core.getMessages().map((message) => message.id)).toEqual([
160
+ "restored",
161
+ ]);
162
+ expect(runAgent).not.toHaveBeenCalled();
163
+ });
164
+
165
+ it("sends while the restored stream is still open after cancelling a queued send", async () => {
166
+ const { history, finish } = deferredHistory();
167
+ history.resume = async function* ({ abortSignal }) {
168
+ yield {
169
+ content: [{ type: "text", text: "partial" }],
170
+ status: { type: "running" },
171
+ };
172
+ await new Promise<void>((resolve) => {
173
+ abortSignal.addEventListener("abort", () => resolve(), { once: true });
174
+ });
175
+ };
176
+ const runAgent = vi.fn(async (_input: unknown) => {});
177
+ const { core } = createCore(history, {
178
+ runAgent,
179
+ abortRun: vi.fn(),
180
+ } as unknown as AbstractAgent);
181
+ const loading = core.__internal_load();
182
+ const cancelled = core.append({
183
+ ...userMessage("cancelled"),
184
+ startRun: true,
185
+ });
186
+ await core.cancel();
187
+ finish(true);
188
+ await vi.waitFor(() => expect(core.isRunning()).toBe(true));
189
+ expect(core.isLoading).toBe(true);
190
+
191
+ const sending = core.append({
192
+ ...userMessage("sent during resume"),
193
+ parentId: core.getMessages().at(-1)?.id ?? null,
194
+ startRun: true,
195
+ });
196
+ await Promise.all([loading, cancelled, sending]);
197
+
198
+ expect(
199
+ core
200
+ .getMessages()
201
+ .filter((message) => message.role === "user")
202
+ .map((message) => message.content[0]),
203
+ ).toEqual([
204
+ { type: "text", text: "earlier" },
205
+ { type: "text", text: "sent during resume" },
206
+ ]);
207
+ expect(runAgent).toHaveBeenCalledOnce();
208
+ });
209
+
210
+ it("waits for history before reloading", async () => {
211
+ const { history, finish } = deferredHistory();
212
+ const runAgent = vi.fn(async () => {});
213
+ const { core } = createCore(history, {
214
+ runAgent,
215
+ abortRun: vi.fn(),
216
+ } as unknown as AbstractAgent);
217
+ const loading = core.__internal_load();
218
+ const reloading = core.reload("restored");
219
+
220
+ expect(runAgent).not.toHaveBeenCalled();
221
+ finish();
222
+ await Promise.all([loading, reloading]);
223
+ expect(runAgent).toHaveBeenCalledOnce();
224
+ });
225
+
226
+ it("waits for history before resuming a run", async () => {
227
+ const { history, finish } = deferredHistory();
228
+ const runAgent = vi.fn(async () => {});
229
+ const { core } = createCore(history, {
230
+ runAgent,
231
+ abortRun: vi.fn(),
232
+ } as unknown as AbstractAgent);
233
+ const loading = core.__internal_load();
234
+ const resuming = core.resume({
235
+ parentId: "restored",
236
+ sourceId: null,
237
+ runConfig: {},
238
+ });
239
+
240
+ expect(runAgent).not.toHaveBeenCalled();
241
+ finish();
242
+ await Promise.all([loading, resuming]);
243
+ expect(runAgent).toHaveBeenCalledOnce();
244
+ });
245
+
246
+ it("waits for history before resuming an in-flight run", async () => {
247
+ const { history, finish } = deferredHistory();
248
+ const resume = vi.fn(async function* () {
249
+ yield {
250
+ content: [{ type: "text" as const, text: "continued" }],
251
+ status: { type: "complete" as const, reason: "unknown" as const },
252
+ };
253
+ });
254
+ history.resume = resume;
255
+ const { core } = createCore(history);
256
+ const loading = core.__internal_load();
257
+ const resuming = core.resumeInFlightRun([storedUser]);
258
+
259
+ expect(resume).not.toHaveBeenCalled();
260
+ finish();
261
+ await Promise.all([loading, resuming]);
262
+ expect(resume).toHaveBeenCalledOnce();
263
+ });
264
+
265
+ it("waits for history before applying a tool result", async () => {
266
+ const { history, finish } = deferredHistory([storedUser, storedToolCall]);
267
+ const { core } = createCore(history);
268
+ const loading = core.__internal_load();
269
+ core.addToolResult({
270
+ messageId: "assistant",
271
+ toolCallId: "call",
272
+ toolName: "lookup",
273
+ result: "found",
274
+ isError: false,
275
+ });
276
+
277
+ finish();
278
+ await loading;
279
+ expect(
280
+ (core.getMessages()[1] as ThreadAssistantMessage).content[0],
281
+ ).toMatchObject({
282
+ result: "found",
283
+ });
284
+ });
285
+
286
+ it("waits for history before recording a tool interaction", async () => {
287
+ const { history, finish } = deferredHistory([storedUser, storedToolCall]);
288
+ const { core } = createCore(history);
289
+ const loading = core.__internal_load();
290
+ const recording = core.recordToolInteraction({
291
+ messageId: "assistant",
292
+ toolCallId: "call",
293
+ interaction: {
294
+ type: "action",
295
+ occurredAt: 1,
296
+ payload: { name: "confirm" },
297
+ },
298
+ });
299
+
300
+ finish();
301
+ await Promise.all([loading, recording]);
302
+ expect(
303
+ (core.getMessages()[1] as ThreadAssistantMessage).content[0],
304
+ ).toMatchObject({
305
+ unstable_interactions: { entries: [{ payload: { name: "confirm" } }] },
306
+ });
307
+ });
308
+
309
+ it("waits for history before submitting interrupt responses", async () => {
310
+ const { history, finish } = deferredHistory([storedUser, storedInterrupt]);
311
+ const runAgent = vi.fn(async () => {});
312
+ const { core } = createCore(history, {
313
+ runAgent,
314
+ abortRun: vi.fn(),
315
+ } as unknown as AbstractAgent);
316
+ const loading = core.__internal_load();
317
+ const responding = core.submitInterruptResponses([
318
+ { interruptId: "gate", status: "resolved" },
319
+ ]);
320
+
321
+ finish();
322
+ await Promise.all([loading, responding]);
323
+ expect(runAgent).toHaveBeenCalledOnce();
324
+ expect(core.getPendingInterrupts()).toBeNull();
325
+ });
326
+
327
+ it("waits for history before responding to a tool approval", async () => {
328
+ const { history, finish } = deferredHistory([storedUser, storedInterrupt]);
329
+ const runAgent = vi.fn(async () => {});
330
+ const { core } = createCore(history, {
331
+ runAgent,
332
+ abortRun: vi.fn(),
333
+ } as unknown as AbstractAgent);
334
+ const loading = core.__internal_load();
335
+ const responding = core.respondToToolApproval({
336
+ approvalId: "gate",
337
+ approved: true,
338
+ });
339
+
340
+ finish();
341
+ await Promise.all([loading, responding]);
342
+ expect(runAgent).toHaveBeenCalledOnce();
343
+ expect(
344
+ (core.getMessages()[1] as ThreadAssistantMessage).content[0],
345
+ ).toMatchObject({
346
+ approval: { id: "gate", approved: true },
347
+ });
348
+ });
349
+
350
+ it("waits for history before steering away from an interrupt", async () => {
351
+ const { history, finish } = deferredHistory([storedUser, storedInterrupt]);
352
+ const runAgent = vi.fn(async () => {});
353
+ const { core } = createCore(history, {
354
+ runAgent,
355
+ abortRun: vi.fn(),
356
+ } as unknown as AbstractAgent);
357
+ const loading = core.__internal_load();
358
+ const steering = core.steerAway("changed my mind");
359
+
360
+ finish();
361
+ await Promise.all([loading, steering]);
362
+ expect(core.getPendingInterrupts()).toBeNull();
363
+ expect(
364
+ core
365
+ .getMessages()
366
+ .some((message) =>
367
+ message.content.some(
368
+ (part) => part.type === "text" && part.text === "changed my mind",
369
+ ),
370
+ ),
371
+ ).toBe(true);
372
+ expect(runAgent).toHaveBeenCalledOnce();
373
+ });
374
+
375
+ it("waits for history before sending an A2UI action", async () => {
376
+ const { history, finish } = deferredHistory();
377
+ const runAgent = vi.fn(async (_input: unknown) => {});
378
+ const { core } = createCore(history, {
379
+ runAgent,
380
+ abortRun: vi.fn(),
381
+ } as unknown as AbstractAgent);
382
+ const loading = core.__internal_load();
383
+ core.sendA2uiAction({ type: "a2ui:action", name: "submit" });
384
+
385
+ finish();
386
+ await loading;
387
+ expect(runAgent).toHaveBeenCalledOnce();
388
+ expect(runAgent.mock.calls[0]?.[0]).toMatchObject({
389
+ forwardedProps: { a2uiAction: { userAction: { name: "submit" } } },
390
+ });
391
+ });
392
+
393
+ it("links a new tail append after initial history imports", async () => {
394
+ const history = createHistory();
395
+ let finishLoad!: (
396
+ repo: Awaited<ReturnType<ThreadHistoryAdapter["load"]>>,
397
+ ) => void;
398
+ history.load.mockReturnValueOnce(
399
+ new Promise((resolve) => {
400
+ finishLoad = resolve;
401
+ }),
402
+ );
403
+ const { core } = createCore(history);
404
+ const loading = core.__internal_load();
405
+ const sending = core.append(userMessage("during load"));
406
+
407
+ finishLoad({
408
+ headId: "restored",
409
+ messages: [
410
+ {
411
+ parentId: null,
412
+ message: {
413
+ id: "restored",
414
+ role: "user",
415
+ content: [{ type: "text", text: "earlier" }],
416
+ attachments: [],
417
+ createdAt: new Date(0),
418
+ metadata: { custom: {} },
419
+ },
420
+ },
421
+ ],
422
+ });
423
+ await Promise.all([loading, sending]);
424
+
425
+ expect(core.getMessages().map((message) => message.content)).toEqual([
426
+ [{ type: "text", text: "earlier" }],
427
+ [{ type: "text", text: "during load" }],
428
+ ]);
429
+ expect(core.getMessageRepository().messages.at(-1)?.parentId).toBe(
430
+ "restored",
431
+ );
432
+ });
433
+
59
434
  it("loads history when the adapter arrives after the first load", async () => {
60
435
  const { core, update } = createCore();
61
436
  const history = createHistory();
@@ -150,7 +525,6 @@ describe("AgUiThreadRuntimeCore activity deltas", () => {
150
525
  agent,
151
526
  logger: makeLogger(),
152
527
  showThinking: true,
153
- notifyUpdate: vi.fn(),
154
528
  });
155
529
 
156
530
  await core.append({ ...userMessage("go"), startRun: true });