@assistant-ui/react-a2a 0.2.30 → 0.2.32
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/A2AClient.d.ts.map +1 -1
- package/dist/A2AClient.js +200 -27
- package/dist/A2AClient.js.map +1 -1
- package/dist/A2AThreadRuntimeCore.d.ts +7 -12
- package/dist/A2AThreadRuntimeCore.d.ts.map +1 -1
- package/dist/A2AThreadRuntimeCore.js +65 -147
- package/dist/A2AThreadRuntimeCore.js.map +1 -1
- package/dist/conversions.d.ts +6 -2
- package/dist/conversions.d.ts.map +1 -1
- package/dist/conversions.js +28 -9
- package/dist/conversions.js.map +1 -1
- package/dist/useA2ARuntime.d.ts.map +1 -1
- package/dist/useA2ARuntime.js +17 -7
- package/dist/useA2ARuntime.js.map +1 -1
- package/package.json +6 -6
- package/src/A2AClient.test.ts +714 -0
- package/src/A2AClient.ts +307 -43
- package/src/A2AThreadRuntimeCore.test.ts +248 -0
- package/src/A2AThreadRuntimeCore.ts +104 -212
- package/src/conversions.test.ts +49 -0
- package/src/conversions.ts +56 -7
- package/src/useA2ARuntime.test.tsx +150 -26
- package/src/useA2ARuntime.ts +33 -12
|
@@ -150,6 +150,95 @@ describe("A2AThreadRuntimeCore", () => {
|
|
|
150
150
|
});
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
describe("late history loading", () => {
|
|
154
|
+
const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
155
|
+
|
|
156
|
+
function createLateHistory() {
|
|
157
|
+
const user = createHistoryMessage("user", "user", "Question");
|
|
158
|
+
return {
|
|
159
|
+
user,
|
|
160
|
+
history: {
|
|
161
|
+
load: vi.fn().mockResolvedValue({
|
|
162
|
+
headId: user.id,
|
|
163
|
+
messages: [{ parentId: null, message: user }],
|
|
164
|
+
}),
|
|
165
|
+
append: vi.fn().mockResolvedValue(undefined),
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
it("loads history when the adapter arrives after the first load", async () => {
|
|
171
|
+
const client = createMockClient();
|
|
172
|
+
const core = createCore(client);
|
|
173
|
+
const { user, history } = createLateHistory();
|
|
174
|
+
|
|
175
|
+
await core.__internal_load();
|
|
176
|
+
expect(history.load).not.toHaveBeenCalled();
|
|
177
|
+
expect(core.getMessages()).toEqual([]);
|
|
178
|
+
|
|
179
|
+
core.updateOptions({ client, history });
|
|
180
|
+
await flush();
|
|
181
|
+
|
|
182
|
+
expect(history.load).toHaveBeenCalledOnce();
|
|
183
|
+
expect(core.getMessages().map((message) => message.id)).toEqual([
|
|
184
|
+
user.id,
|
|
185
|
+
]);
|
|
186
|
+
expect(core.isLoading).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("fetches the agent card once across the early load and the late history load", async () => {
|
|
190
|
+
const agentCard = { name: "Agent", url: "https://agent.example" };
|
|
191
|
+
const client = createMockClient({
|
|
192
|
+
getAgentCard: vi.fn().mockResolvedValue(agentCard),
|
|
193
|
+
});
|
|
194
|
+
const core = createCore(client);
|
|
195
|
+
const { history } = createLateHistory();
|
|
196
|
+
|
|
197
|
+
await core.__internal_load();
|
|
198
|
+
expect(core.getAgentCard()).toEqual(agentCard);
|
|
199
|
+
|
|
200
|
+
core.updateOptions({ client, history });
|
|
201
|
+
await flush();
|
|
202
|
+
|
|
203
|
+
expect(client.getAgentCard).toHaveBeenCalledOnce();
|
|
204
|
+
expect(history.load).toHaveBeenCalledOnce();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("does not load late history over a thread that already has messages", async () => {
|
|
208
|
+
const client = createMockClient();
|
|
209
|
+
const core = createCore(client);
|
|
210
|
+
const { history } = createLateHistory();
|
|
211
|
+
|
|
212
|
+
await core.__internal_load();
|
|
213
|
+
await core.append({
|
|
214
|
+
...createUserAppendMessage("Typed"),
|
|
215
|
+
startRun: false,
|
|
216
|
+
} as AppendMessage);
|
|
217
|
+
expect(core.getMessages()).toHaveLength(1);
|
|
218
|
+
|
|
219
|
+
core.updateOptions({ client, history });
|
|
220
|
+
await flush();
|
|
221
|
+
|
|
222
|
+
expect(history.load).not.toHaveBeenCalled();
|
|
223
|
+
expect(core.getMessages()).toHaveLength(1);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("does not reload when the adapter is replaced after a completed load", async () => {
|
|
227
|
+
const client = createMockClient();
|
|
228
|
+
const { history } = createLateHistory();
|
|
229
|
+
const core = createCore(client, { history });
|
|
230
|
+
const replacement = createLateHistory().history;
|
|
231
|
+
|
|
232
|
+
await core.__internal_load();
|
|
233
|
+
expect(history.load).toHaveBeenCalledOnce();
|
|
234
|
+
|
|
235
|
+
core.updateOptions({ client, history: replacement });
|
|
236
|
+
await flush();
|
|
237
|
+
|
|
238
|
+
expect(replacement.load).not.toHaveBeenCalled();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
153
242
|
// --- Basic state ---
|
|
154
243
|
|
|
155
244
|
describe("initial state", () => {
|
|
@@ -371,6 +460,122 @@ describe("A2AThreadRuntimeCore", () => {
|
|
|
371
460
|
});
|
|
372
461
|
});
|
|
373
462
|
|
|
463
|
+
describe("updateOptions", () => {
|
|
464
|
+
it("keeps the server-assigned contextId across re-renders", async () => {
|
|
465
|
+
const streamMessage = vi.fn().mockImplementation(async function* () {
|
|
466
|
+
yield statusUpdateEvent("completed", "Answer");
|
|
467
|
+
});
|
|
468
|
+
const client = createMockClient({ streamMessage });
|
|
469
|
+
const core = new A2AThreadRuntimeCore({
|
|
470
|
+
client,
|
|
471
|
+
notifyUpdate: notifyUpdate as unknown as () => void,
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
await core.append(createUserAppendMessage("First"));
|
|
475
|
+
|
|
476
|
+
// useA2ARuntime re-applies its options on every render, including the
|
|
477
|
+
// renders triggered by the stream's own notifyUpdate calls.
|
|
478
|
+
core.updateOptions({ client, contextId: undefined });
|
|
479
|
+
|
|
480
|
+
await core.append(createUserAppendMessage("Second"));
|
|
481
|
+
|
|
482
|
+
const secondSend = streamMessage.mock.calls[1]?.[0];
|
|
483
|
+
expect(secondSend?.contextId).toBe("ctx-1");
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it("resets the contextId when the thread is switched", async () => {
|
|
487
|
+
const streamMessage = vi.fn().mockImplementation(async function* () {
|
|
488
|
+
yield statusUpdateEvent("completed", "Answer");
|
|
489
|
+
});
|
|
490
|
+
const client = createMockClient({ streamMessage });
|
|
491
|
+
const core = new A2AThreadRuntimeCore({
|
|
492
|
+
client,
|
|
493
|
+
notifyUpdate: notifyUpdate as unknown as () => void,
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
await core.append(createUserAppendMessage("First"));
|
|
497
|
+
core.applyExternalMessages([]);
|
|
498
|
+
core.resetContext();
|
|
499
|
+
await core.append(createUserAppendMessage("Fresh thread"));
|
|
500
|
+
|
|
501
|
+
const secondSend = streamMessage.mock.calls[1]?.[0];
|
|
502
|
+
expect(secondSend?.contextId).toBeUndefined();
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("does not persist a partial message when switching away mid-run", async () => {
|
|
506
|
+
let releaseStream!: () => void;
|
|
507
|
+
const gate = new Promise<void>((resolve) => {
|
|
508
|
+
releaseStream = resolve;
|
|
509
|
+
});
|
|
510
|
+
const streamMessage = vi.fn().mockImplementation(async function* () {
|
|
511
|
+
yield statusUpdateEvent("working");
|
|
512
|
+
await gate;
|
|
513
|
+
});
|
|
514
|
+
const history = {
|
|
515
|
+
load: vi.fn().mockResolvedValue({ messages: [] }),
|
|
516
|
+
append: vi.fn().mockResolvedValue(undefined),
|
|
517
|
+
};
|
|
518
|
+
const core = createCore({ streamMessage }, { history });
|
|
519
|
+
|
|
520
|
+
const run = core.append(createUserAppendMessage("First"));
|
|
521
|
+
await vi.waitFor(() => {
|
|
522
|
+
expect(streamMessage).toHaveBeenCalledTimes(1);
|
|
523
|
+
});
|
|
524
|
+
history.append.mockClear();
|
|
525
|
+
|
|
526
|
+
core.applyExternalMessages([]);
|
|
527
|
+
core.resetContext();
|
|
528
|
+
releaseStream();
|
|
529
|
+
await run.catch(() => {});
|
|
530
|
+
|
|
531
|
+
const cancelledAppend = history.append.mock.calls.find((call) => {
|
|
532
|
+
const entry = call[0] as
|
|
533
|
+
| { message?: { status?: { reason?: string } } }
|
|
534
|
+
| undefined;
|
|
535
|
+
return entry?.message?.status?.reason === "cancelled";
|
|
536
|
+
});
|
|
537
|
+
expect(cancelledAppend).toBeUndefined();
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it("keeps the contextId across a bare external apply", async () => {
|
|
541
|
+
const streamMessage = vi.fn().mockImplementation(async function* () {
|
|
542
|
+
yield statusUpdateEvent("completed", "Answer");
|
|
543
|
+
});
|
|
544
|
+
const client = createMockClient({ streamMessage });
|
|
545
|
+
const core = new A2AThreadRuntimeCore({
|
|
546
|
+
client,
|
|
547
|
+
notifyUpdate: notifyUpdate as unknown as () => void,
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
await core.append(createUserAppendMessage("First"));
|
|
551
|
+
// Branch switches, deletes, and cancel resyncs route through
|
|
552
|
+
// applyExternalMessages without a thread switch.
|
|
553
|
+
core.applyExternalMessages(core.getMessages());
|
|
554
|
+
await core.append(createUserAppendMessage("Second"));
|
|
555
|
+
|
|
556
|
+
const secondSend = streamMessage.mock.calls[1]?.[0];
|
|
557
|
+
expect(secondSend?.contextId).toBe("ctx-1");
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it("applies a changed contextId option", async () => {
|
|
561
|
+
const streamMessage = vi.fn().mockImplementation(async function* () {
|
|
562
|
+
yield statusUpdateEvent("completed", "Answer");
|
|
563
|
+
});
|
|
564
|
+
const client = createMockClient({ streamMessage });
|
|
565
|
+
const core = new A2AThreadRuntimeCore({
|
|
566
|
+
client,
|
|
567
|
+
notifyUpdate: notifyUpdate as unknown as () => void,
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
await core.append(createUserAppendMessage("First"));
|
|
571
|
+
core.updateOptions({ client, contextId: "ctx-override" });
|
|
572
|
+
await core.append(createUserAppendMessage("Second"));
|
|
573
|
+
|
|
574
|
+
const secondSend = streamMessage.mock.calls[1]?.[0];
|
|
575
|
+
expect(secondSend?.contextId).toBe("ctx-override");
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
|
|
374
579
|
// --- Edit & Reload ---
|
|
375
580
|
|
|
376
581
|
describe("edit", () => {
|
|
@@ -1214,6 +1419,49 @@ describe("A2AThreadRuntimeCore", () => {
|
|
|
1214
1419
|
reason: "error",
|
|
1215
1420
|
});
|
|
1216
1421
|
});
|
|
1422
|
+
|
|
1423
|
+
it("appends the first skipped frame reason to the empty-stream error", async () => {
|
|
1424
|
+
const onError = vi.fn();
|
|
1425
|
+
const core = createCore(
|
|
1426
|
+
{
|
|
1427
|
+
streamMessage: vi.fn().mockImplementation(async function* () {
|
|
1428
|
+
return "unrecognized event shape (frame: {})";
|
|
1429
|
+
}),
|
|
1430
|
+
},
|
|
1431
|
+
{ onError },
|
|
1432
|
+
);
|
|
1433
|
+
|
|
1434
|
+
await expect(core.append(createUserAppendMessage("Go"))).rejects.toThrow(
|
|
1435
|
+
"A2A message stream ended without any events. First skipped frame: unrecognized event shape (frame: {})",
|
|
1436
|
+
);
|
|
1437
|
+
expect(onError).toHaveBeenCalledWith(
|
|
1438
|
+
expect.objectContaining({
|
|
1439
|
+
message:
|
|
1440
|
+
"A2A message stream ended without any events. First skipped frame: unrecognized event shape (frame: {})",
|
|
1441
|
+
}),
|
|
1442
|
+
);
|
|
1443
|
+
});
|
|
1444
|
+
|
|
1445
|
+
it("ignores the skipped frame reason when the stream produced events", async () => {
|
|
1446
|
+
const onError = vi.fn();
|
|
1447
|
+
const core = createCore(
|
|
1448
|
+
{
|
|
1449
|
+
streamMessage: vi.fn().mockImplementation(async function* () {
|
|
1450
|
+
yield statusUpdateEvent("completed", "Done");
|
|
1451
|
+
return "unrecognized event shape (frame: {})";
|
|
1452
|
+
}),
|
|
1453
|
+
},
|
|
1454
|
+
{ onError },
|
|
1455
|
+
);
|
|
1456
|
+
|
|
1457
|
+
await core.append(createUserAppendMessage("Go"));
|
|
1458
|
+
|
|
1459
|
+
expect(onError).not.toHaveBeenCalled();
|
|
1460
|
+
expect(core.getMessages()[1]!.status).toEqual({
|
|
1461
|
+
type: "complete",
|
|
1462
|
+
reason: "stop",
|
|
1463
|
+
});
|
|
1464
|
+
});
|
|
1217
1465
|
});
|
|
1218
1466
|
|
|
1219
1467
|
// --- Concurrent run protection ---
|