@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.
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts +19 -3
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +193 -39
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +11 -12
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +4 -16
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +80 -15
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/types.d.ts +22 -5
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/types.js +8 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/useAgUiRuntime.d.ts.map +1 -1
- package/dist/useAgUiRuntime.js +76 -33
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +6 -6
- package/src/runtime/AgUiThreadRuntimeCore.test.ts +380 -6
- package/src/runtime/AgUiThreadRuntimeCore.ts +314 -70
- package/src/runtime/adapter/conversions.content.test.ts +41 -0
- package/src/runtime/adapter/conversions.ts +16 -19
- package/src/runtime/adapter/run-aggregator.reasoning.test.ts +143 -0
- package/src/runtime/adapter/run-aggregator.ts +128 -33
- package/src/runtime/types.ts +25 -5
- package/src/useAgUiRuntime.external-store.test.tsx +51 -0
- package/src/useAgUiRuntime.thread-switch.test.tsx +587 -3
- package/src/useAgUiRuntime.ts +98 -44
|
@@ -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 {
|
|
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(
|
|
10
|
-
|
|
11
|
-
|
|
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 });
|