@assistant-ui/react-ag-ui 0.1.1 → 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/README.md +2 -0
- package/dist/runtime/AgUiThreadController.d.ts +11 -0
- package/dist/runtime/AgUiThreadController.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadController.js +242 -25
- package/dist/runtime/AgUiThreadController.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts +9 -1
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +184 -9
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +2 -0
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +42 -12
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/adapter/subscriber.d.ts +0 -1
- package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
- package/dist/runtime/adapter/subscriber.js +1 -8
- package/dist/runtime/adapter/subscriber.js.map +1 -1
- package/dist/runtime/event-parser.d.ts.map +1 -1
- package/dist/runtime/event-parser.js +1 -0
- package/dist/runtime/event-parser.js.map +1 -1
- package/dist/runtime/types.d.ts +2 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/types.js +2 -1
- package/dist/runtime/types.js.map +1 -1
- package/package.json +6 -5
- package/src/runtime/AgUiThreadController.disconnect.test.ts +914 -0
- package/src/runtime/AgUiThreadController.interrupt-history.test.ts +392 -0
- package/src/runtime/AgUiThreadController.replay.test.ts +8 -1
- package/src/runtime/AgUiThreadController.snapshot-head-move.test.ts +346 -0
- package/src/runtime/AgUiThreadController.subagent-snapshot.test.ts +854 -0
- package/src/runtime/AgUiThreadController.test.ts +173 -1
- package/src/runtime/AgUiThreadController.ts +528 -33
- package/src/runtime/adapter/conversions.content.test.ts +129 -0
- package/src/runtime/adapter/conversions.subagent-messages.test.ts +550 -0
- package/src/runtime/adapter/conversions.ts +331 -20
- package/src/runtime/adapter/run-aggregator.metadata.test.ts +151 -0
- package/src/runtime/adapter/run-aggregator.ts +63 -9
- package/src/runtime/adapter/subscriber.test.ts +1 -1
- package/src/runtime/adapter/subscriber.ts +1 -6
- package/src/runtime/event-parser.ts +3 -0
- package/src/runtime/types.ts +5 -0
- package/src/useAgUiRuntime.cloud-run-leases.test.tsx +384 -0
- package/src/useAgUiRuntime.state.test.tsx +87 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { HttpAgent } from "@ag-ui/client";
|
|
3
|
+
import type { AppendMessage } from "@assistant-ui/core";
|
|
4
|
+
import { AgUiThreadController } from "./AgUiThreadController";
|
|
5
|
+
import { makeLogger } from "./logger";
|
|
6
|
+
|
|
7
|
+
const userMessage: AppendMessage = {
|
|
8
|
+
role: "user",
|
|
9
|
+
content: [{ type: "text", text: "hello" }],
|
|
10
|
+
attachments: [],
|
|
11
|
+
metadata: { custom: {} },
|
|
12
|
+
createdAt: new Date(0),
|
|
13
|
+
parentId: null,
|
|
14
|
+
sourceId: null,
|
|
15
|
+
runConfig: {},
|
|
16
|
+
startRun: true,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type Wire = { messages: unknown[] };
|
|
20
|
+
|
|
21
|
+
function createController(events: (wire: Wire) => object[]) {
|
|
22
|
+
const agent = new HttpAgent({
|
|
23
|
+
url: "https://example.invalid",
|
|
24
|
+
fetch: async (_url: unknown, init: RequestInit) => {
|
|
25
|
+
const wire = { messages: JSON.parse(init.body as string).messages ?? [] };
|
|
26
|
+
return new Response(
|
|
27
|
+
events(wire)
|
|
28
|
+
.map((event) => `data: ${JSON.stringify(event)}\n\n`)
|
|
29
|
+
.join(""),
|
|
30
|
+
{ headers: { "Content-Type": "text/event-stream" } },
|
|
31
|
+
);
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
return new AgUiThreadController({
|
|
35
|
+
agent,
|
|
36
|
+
logger: makeLogger(),
|
|
37
|
+
showThinking: false,
|
|
38
|
+
onError: () => {},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const start = { type: "RUN_STARTED", threadId: "thread", runId: "run" };
|
|
43
|
+
const answerText = [
|
|
44
|
+
{ type: "TEXT_MESSAGE_START", messageId: "answer", role: "assistant" },
|
|
45
|
+
{ type: "TEXT_MESSAGE_CONTENT", messageId: "answer", delta: "Let me check." },
|
|
46
|
+
{ type: "TEXT_MESSAGE_END", messageId: "answer" },
|
|
47
|
+
];
|
|
48
|
+
const answerRecord = {
|
|
49
|
+
id: "answer",
|
|
50
|
+
role: "assistant",
|
|
51
|
+
content: "Let me check.",
|
|
52
|
+
toolCalls: [
|
|
53
|
+
{
|
|
54
|
+
id: "task",
|
|
55
|
+
type: "function",
|
|
56
|
+
function: { name: "task", arguments: "{}" },
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
const otherRecord = { id: "other", role: "assistant", content: "Next step." };
|
|
61
|
+
const taskCallEvents = [
|
|
62
|
+
{ type: "TOOL_CALL_START", toolCallId: "task", toolCallName: "task" },
|
|
63
|
+
{ type: "TOOL_CALL_END", toolCallId: "task" },
|
|
64
|
+
];
|
|
65
|
+
const taskAndSubagentEvents = [
|
|
66
|
+
...taskCallEvents,
|
|
67
|
+
{
|
|
68
|
+
type: "SUBAGENT_STARTED",
|
|
69
|
+
subagentRunId: "sub",
|
|
70
|
+
name: "worker",
|
|
71
|
+
parentToolCallId: "task",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
type: "TEXT_MESSAGE_START",
|
|
75
|
+
messageId: "s1",
|
|
76
|
+
role: "assistant",
|
|
77
|
+
subagentRunId: "sub",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: "TEXT_MESSAGE_CONTENT",
|
|
81
|
+
messageId: "s1",
|
|
82
|
+
delta: "working",
|
|
83
|
+
subagentRunId: "sub",
|
|
84
|
+
},
|
|
85
|
+
{ type: "TEXT_MESSAGE_END", messageId: "s1", subagentRunId: "sub" },
|
|
86
|
+
];
|
|
87
|
+
const subFinished = { type: "SUBAGENT_FINISHED", subagentRunId: "sub" };
|
|
88
|
+
const taskResult = {
|
|
89
|
+
type: "TOOL_CALL_RESULT",
|
|
90
|
+
messageId: "tool-task",
|
|
91
|
+
toolCallId: "task",
|
|
92
|
+
content: "done",
|
|
93
|
+
};
|
|
94
|
+
const finished = { type: "RUN_FINISHED", threadId: "thread", runId: "run" };
|
|
95
|
+
const snapshot = (wire: Wire, ...rest: object[]) => ({
|
|
96
|
+
type: "MESSAGES_SNAPSHOT",
|
|
97
|
+
messages: [...wire.messages, answerRecord, ...rest],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
function statuses(core: AgUiThreadController) {
|
|
101
|
+
const answer = core.getMessages().find(({ id }) => id === "answer")!;
|
|
102
|
+
const toolCall = answer.content.find(
|
|
103
|
+
(part) => part.type === "tool-call" && part.toolCallId === "task",
|
|
104
|
+
);
|
|
105
|
+
const nested =
|
|
106
|
+
toolCall?.type === "tool-call"
|
|
107
|
+
? toolCall.messages?.find((message) => message.id === "sub")
|
|
108
|
+
: undefined;
|
|
109
|
+
return { answer: answer.status?.type, subagent: nested?.status?.type };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function taskPartFor(core: AgUiThreadController, messageId: string) {
|
|
113
|
+
const message = core.getMessages().find(({ id }) => id === messageId);
|
|
114
|
+
if (message?.role !== "assistant") return undefined;
|
|
115
|
+
return message.content.find(
|
|
116
|
+
(part) => part.type === "tool-call" && part.toolCallId === "task",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
describe("AgUiThreadController snapshot head moves", () => {
|
|
121
|
+
it("reopens a settled snapshot-only assistant when a later snapshot adds a tool call", async () => {
|
|
122
|
+
const answer = {
|
|
123
|
+
id: "m1",
|
|
124
|
+
role: "assistant",
|
|
125
|
+
content: "Let me ask.",
|
|
126
|
+
};
|
|
127
|
+
const core = createController((wire) => [
|
|
128
|
+
start,
|
|
129
|
+
{ type: "MESSAGES_SNAPSHOT", messages: [...wire.messages, answer] },
|
|
130
|
+
{
|
|
131
|
+
type: "MESSAGES_SNAPSHOT",
|
|
132
|
+
messages: [
|
|
133
|
+
...wire.messages,
|
|
134
|
+
{
|
|
135
|
+
...answer,
|
|
136
|
+
toolCalls: [
|
|
137
|
+
{
|
|
138
|
+
id: "ask",
|
|
139
|
+
type: "function",
|
|
140
|
+
function: { name: "ask_user", arguments: "{}" },
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
finished,
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
await core.append(userMessage);
|
|
150
|
+
|
|
151
|
+
expect(
|
|
152
|
+
core.getMessages().find(({ id }) => id === "m1")?.status,
|
|
153
|
+
).toMatchObject({
|
|
154
|
+
type: "requires-action",
|
|
155
|
+
reason: "tool-calls",
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("settles the moved-off answer when the snapshot has no tool call", async () => {
|
|
160
|
+
const core = createController((wire) => [
|
|
161
|
+
start,
|
|
162
|
+
...answerText,
|
|
163
|
+
{
|
|
164
|
+
type: "MESSAGES_SNAPSHOT",
|
|
165
|
+
messages: [
|
|
166
|
+
...wire.messages,
|
|
167
|
+
{ id: "answer", role: "assistant", content: "Let me check." },
|
|
168
|
+
otherRecord,
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
finished,
|
|
172
|
+
]);
|
|
173
|
+
await core.append(userMessage);
|
|
174
|
+
expect(statuses(core).answer).toBe("complete");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("settles the moved-off answer and sub-agent after the tool result", async () => {
|
|
178
|
+
const core = createController((wire) => [
|
|
179
|
+
start,
|
|
180
|
+
...answerText,
|
|
181
|
+
...taskAndSubagentEvents,
|
|
182
|
+
snapshot(wire, otherRecord),
|
|
183
|
+
subFinished,
|
|
184
|
+
taskResult,
|
|
185
|
+
finished,
|
|
186
|
+
]);
|
|
187
|
+
await core.append(userMessage);
|
|
188
|
+
expect(core.isRunning()).toBe(false);
|
|
189
|
+
expect(statuses(core)).toEqual({
|
|
190
|
+
answer: "complete",
|
|
191
|
+
subagent: "complete",
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("keeps a moved-off answer settled and owns the result after repeated snapshots", async () => {
|
|
196
|
+
const core = createController((wire) => [
|
|
197
|
+
start,
|
|
198
|
+
...answerText,
|
|
199
|
+
...taskCallEvents,
|
|
200
|
+
snapshot(wire, otherRecord),
|
|
201
|
+
snapshot(wire, otherRecord),
|
|
202
|
+
taskResult,
|
|
203
|
+
finished,
|
|
204
|
+
]);
|
|
205
|
+
await core.append(userMessage);
|
|
206
|
+
|
|
207
|
+
expect(core.isRunning()).toBe(false);
|
|
208
|
+
expect(statuses(core).answer).toBe("complete");
|
|
209
|
+
expect(
|
|
210
|
+
core.getMessages().find(({ id }) => id === "other")?.status?.type,
|
|
211
|
+
).toBe("complete");
|
|
212
|
+
expect(taskPartFor(core, "answer")).toMatchObject({ result: "done" });
|
|
213
|
+
expect(taskPartFor(core, "other")).toBeUndefined();
|
|
214
|
+
expect(
|
|
215
|
+
core.getMessages().find(({ id }) => id === "answer")?.content,
|
|
216
|
+
).toEqual([
|
|
217
|
+
{ type: "text", text: "Let me check." },
|
|
218
|
+
{
|
|
219
|
+
type: "tool-call",
|
|
220
|
+
toolCallId: "task",
|
|
221
|
+
toolName: "task",
|
|
222
|
+
args: {},
|
|
223
|
+
argsText: "{}",
|
|
224
|
+
result: "done",
|
|
225
|
+
unstable_toolMessageId: "tool-task",
|
|
226
|
+
},
|
|
227
|
+
]);
|
|
228
|
+
expect(
|
|
229
|
+
core.getMessages().find(({ id }) => id === "other")?.content,
|
|
230
|
+
).toEqual([{ type: "text", text: "Next step." }]);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("preserves full content and settled statuses with a sub-agent after repeated snapshots", async () => {
|
|
234
|
+
const core = createController((wire) => [
|
|
235
|
+
start,
|
|
236
|
+
...answerText,
|
|
237
|
+
...taskAndSubagentEvents,
|
|
238
|
+
snapshot(wire, otherRecord),
|
|
239
|
+
snapshot(wire, otherRecord),
|
|
240
|
+
subFinished,
|
|
241
|
+
taskResult,
|
|
242
|
+
finished,
|
|
243
|
+
]);
|
|
244
|
+
await core.append(userMessage);
|
|
245
|
+
|
|
246
|
+
expect(core.isRunning()).toBe(false);
|
|
247
|
+
expect(statuses(core)).toEqual({
|
|
248
|
+
answer: "complete",
|
|
249
|
+
subagent: "complete",
|
|
250
|
+
});
|
|
251
|
+
expect(
|
|
252
|
+
core.getMessages().find(({ id }) => id === "other")?.status?.type,
|
|
253
|
+
).toBe("complete");
|
|
254
|
+
expect(
|
|
255
|
+
core.getMessages().find(({ id }) => id === "answer")?.content,
|
|
256
|
+
).toEqual([
|
|
257
|
+
{ type: "text", text: "Let me check." },
|
|
258
|
+
{
|
|
259
|
+
type: "tool-call",
|
|
260
|
+
toolCallId: "task",
|
|
261
|
+
toolName: "task",
|
|
262
|
+
args: {},
|
|
263
|
+
argsText: "{}",
|
|
264
|
+
result: "done",
|
|
265
|
+
unstable_toolMessageId: "tool-task",
|
|
266
|
+
messages: [
|
|
267
|
+
expect.objectContaining({
|
|
268
|
+
id: "sub",
|
|
269
|
+
role: "assistant",
|
|
270
|
+
status: { type: "complete", reason: "unknown" },
|
|
271
|
+
content: [{ type: "text", text: "working" }],
|
|
272
|
+
}),
|
|
273
|
+
],
|
|
274
|
+
},
|
|
275
|
+
]);
|
|
276
|
+
expect(
|
|
277
|
+
core.getMessages().find(({ id }) => id === "other")?.content,
|
|
278
|
+
).toEqual([{ type: "text", text: "Next step." }]);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it.each([false, true])(
|
|
282
|
+
"preserves current text when the moved result arrives during streaming: %s",
|
|
283
|
+
async (duringStreaming) => {
|
|
284
|
+
const core = createController((wire) => [
|
|
285
|
+
start,
|
|
286
|
+
...answerText,
|
|
287
|
+
...taskCallEvents,
|
|
288
|
+
snapshot(wire, otherRecord),
|
|
289
|
+
snapshot(wire, otherRecord),
|
|
290
|
+
...(!duringStreaming ? [taskResult] : []),
|
|
291
|
+
{ type: "TEXT_MESSAGE_START", messageId: "other", role: "assistant" },
|
|
292
|
+
{ type: "TEXT_MESSAGE_CONTENT", messageId: "other", delta: " Before." },
|
|
293
|
+
...(duringStreaming ? [taskResult] : []),
|
|
294
|
+
{ type: "TEXT_MESSAGE_CONTENT", messageId: "other", delta: " After." },
|
|
295
|
+
{ type: "TEXT_MESSAGE_END", messageId: "other" },
|
|
296
|
+
finished,
|
|
297
|
+
]);
|
|
298
|
+
await core.append(userMessage);
|
|
299
|
+
|
|
300
|
+
expect(core.isRunning()).toBe(false);
|
|
301
|
+
expect(statuses(core).answer).toBe("complete");
|
|
302
|
+
expect(
|
|
303
|
+
core.getMessages().find(({ id }) => id === "answer")?.content,
|
|
304
|
+
).toEqual([
|
|
305
|
+
{ type: "text", text: "Let me check." },
|
|
306
|
+
{
|
|
307
|
+
type: "tool-call",
|
|
308
|
+
toolCallId: "task",
|
|
309
|
+
toolName: "task",
|
|
310
|
+
args: {},
|
|
311
|
+
argsText: "{}",
|
|
312
|
+
result: "done",
|
|
313
|
+
unstable_toolMessageId: "tool-task",
|
|
314
|
+
},
|
|
315
|
+
]);
|
|
316
|
+
expect(core.getMessages().find(({ id }) => id === "other")).toMatchObject(
|
|
317
|
+
{
|
|
318
|
+
status: { type: "complete" },
|
|
319
|
+
content: [{ type: "text", text: "Next step. Before. After." }],
|
|
320
|
+
},
|
|
321
|
+
);
|
|
322
|
+
},
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
it("does not show a complete answer with a running sub-agent after a result snapshot", async () => {
|
|
326
|
+
const core = createController((wire) => [
|
|
327
|
+
start,
|
|
328
|
+
...answerText,
|
|
329
|
+
...taskAndSubagentEvents,
|
|
330
|
+
snapshot(wire, otherRecord),
|
|
331
|
+
subFinished,
|
|
332
|
+
taskResult,
|
|
333
|
+
snapshot(
|
|
334
|
+
wire,
|
|
335
|
+
{ id: "tool-task", role: "tool", toolCallId: "task", content: "done" },
|
|
336
|
+
otherRecord,
|
|
337
|
+
),
|
|
338
|
+
finished,
|
|
339
|
+
]);
|
|
340
|
+
await core.append(userMessage);
|
|
341
|
+
expect(statuses(core)).toEqual({
|
|
342
|
+
answer: "complete",
|
|
343
|
+
subagent: "complete",
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
});
|