@assistant-ui/react-a2a 0.2.36 → 0.2.37
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 +42 -44
- package/dist/A2AClient.d.ts.map +1 -1
- package/dist/A2AClient.js +43 -7
- package/dist/A2AClient.js.map +1 -1
- package/dist/A2AThreadRuntimeCore.d.ts +98 -86
- package/dist/A2AThreadRuntimeCore.d.ts.map +1 -1
- package/dist/A2AThreadRuntimeCore.js +204 -17
- package/dist/A2AThreadRuntimeCore.js.map +1 -1
- package/dist/a2aExtras.d.ts +1 -3
- package/dist/a2aExtras.d.ts.map +1 -1
- package/dist/conversions.d.ts +18 -17
- package/dist/conversions.d.ts.map +1 -1
- package/dist/conversions.js +24 -2
- package/dist/conversions.js.map +1 -1
- package/dist/hooks.d.ts +3 -6
- package/dist/hooks.d.ts.map +1 -1
- package/dist/index.d.ts +9 -6
- package/dist/index.d.ts.map +1 -0
- package/dist/types.d.ts +207 -209
- package/dist/types.d.ts.map +1 -1
- package/dist/useA2ARuntime.d.ts +2 -4
- package/dist/useA2ARuntime.d.ts.map +1 -1
- package/dist/useA2ARuntime.js +8 -0
- package/dist/useA2ARuntime.js.map +1 -1
- package/package.json +6 -5
- package/src/A2AClient.test.ts +41 -0
- package/src/A2AClient.ts +50 -6
- package/src/A2AThreadRuntimeCore.test.ts +887 -9
- package/src/A2AThreadRuntimeCore.ts +347 -18
- package/src/conversions.test.ts +116 -0
- package/src/conversions.ts +48 -1
- package/src/useA2ARuntime.test.tsx +245 -0
- package/src/useA2ARuntime.ts +14 -0
- package/src/useA2ARuntime.voice.test.tsx +370 -0
package/src/conversions.ts
CHANGED
|
@@ -15,6 +15,51 @@ function isImageMediaType(mediaType?: string): boolean {
|
|
|
15
15
|
return !!mediaType && mediaType.startsWith("image/");
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
const A2UI_OPERATION_KEYS = [
|
|
19
|
+
"createSurface",
|
|
20
|
+
"updateComponents",
|
|
21
|
+
"updateDataModel",
|
|
22
|
+
"deleteSurface",
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
26
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27
|
+
|
|
28
|
+
const isA2uiOperation = (value: unknown): boolean =>
|
|
29
|
+
isRecord(value) &&
|
|
30
|
+
typeof value.version === "string" &&
|
|
31
|
+
A2UI_OPERATION_KEYS.some((key) => Object.hasOwn(value, key));
|
|
32
|
+
|
|
33
|
+
const hasA2uiMediaType = (value: unknown): boolean =>
|
|
34
|
+
typeof value === "string" && value.toLowerCase().includes("a2ui");
|
|
35
|
+
|
|
36
|
+
export function isA2uiDataPart(part: A2APart): boolean {
|
|
37
|
+
if (part.data === undefined) return false;
|
|
38
|
+
|
|
39
|
+
const metadata = isRecord(part.metadata) ? part.metadata : {};
|
|
40
|
+
return (
|
|
41
|
+
hasA2uiMediaType(part.mediaType) ||
|
|
42
|
+
hasA2uiMediaType(metadata.mimeType) ||
|
|
43
|
+
hasA2uiMediaType(metadata.mediaType) ||
|
|
44
|
+
isA2uiOperation(part.data) ||
|
|
45
|
+
(Array.isArray(part.data) &&
|
|
46
|
+
part.data.length > 0 &&
|
|
47
|
+
part.data.every(isA2uiOperation))
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function a2uiPartToOperations(part: A2APart): readonly unknown[] {
|
|
52
|
+
if (!isA2uiDataPart(part)) return [];
|
|
53
|
+
return Array.isArray(part.data) ? part.data : [part.data];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function a2uiPartsToOperations(
|
|
57
|
+
parts: readonly A2APart[],
|
|
58
|
+
): readonly unknown[] {
|
|
59
|
+
if (!Array.isArray(parts)) return [];
|
|
60
|
+
return parts.flatMap(a2uiPartToOperations);
|
|
61
|
+
}
|
|
62
|
+
|
|
18
63
|
export function a2aPartToContent(
|
|
19
64
|
part: A2APart,
|
|
20
65
|
): ThreadAssistantMessage["content"][number] {
|
|
@@ -61,7 +106,9 @@ export function a2aPartToContent(
|
|
|
61
106
|
export function a2aPartsToContent(
|
|
62
107
|
parts: A2APart[],
|
|
63
108
|
): ThreadAssistantMessage["content"] {
|
|
64
|
-
return (Array.isArray(parts) ? parts : [])
|
|
109
|
+
return (Array.isArray(parts) ? parts : [])
|
|
110
|
+
.filter((part) => !isA2uiDataPart(part))
|
|
111
|
+
.map(a2aPartToContent);
|
|
65
112
|
}
|
|
66
113
|
|
|
67
114
|
const TERMINAL_STATES = new Set<A2ATaskState>([
|
|
@@ -360,6 +360,251 @@ describe("useA2ARuntime", () => {
|
|
|
360
360
|
]);
|
|
361
361
|
});
|
|
362
362
|
|
|
363
|
+
it("does not keep the previous thread as a sibling branch after a switch", async () => {
|
|
364
|
+
const { client } = createMockClient();
|
|
365
|
+
let resolveNext!: (value: { messages: ThreadMessage[] }) => void;
|
|
366
|
+
let pending = new Promise<{ messages: ThreadMessage[] }>((resolve) => {
|
|
367
|
+
resolveNext = resolve;
|
|
368
|
+
});
|
|
369
|
+
const { result } = renderHook(() => {
|
|
370
|
+
const [threadId, setThreadId] = useState("initial");
|
|
371
|
+
return useA2ARuntime({
|
|
372
|
+
client,
|
|
373
|
+
adapters: {
|
|
374
|
+
threadList: {
|
|
375
|
+
threadId,
|
|
376
|
+
onSwitchToThread: async (nextThreadId) => {
|
|
377
|
+
setThreadId(nextThreadId);
|
|
378
|
+
return pending;
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
await act(async () => {
|
|
386
|
+
const switchA = result.current.threads.switchToThread("thread-a");
|
|
387
|
+
resolveNext({ messages: [createThreadMessage("thread-a")] });
|
|
388
|
+
await switchA;
|
|
389
|
+
});
|
|
390
|
+
expect(
|
|
391
|
+
result.current.thread.export().messages.map((m) => m.message.id),
|
|
392
|
+
).toEqual(["thread-a"]);
|
|
393
|
+
|
|
394
|
+
pending = new Promise<{ messages: ThreadMessage[] }>((resolve) => {
|
|
395
|
+
resolveNext = resolve;
|
|
396
|
+
});
|
|
397
|
+
let switchB!: Promise<void>;
|
|
398
|
+
act(() => {
|
|
399
|
+
switchB = result.current.threads.switchToThread("thread-b");
|
|
400
|
+
});
|
|
401
|
+
expect(result.current.thread.export().messages).toEqual([]);
|
|
402
|
+
|
|
403
|
+
await act(async () => {
|
|
404
|
+
resolveNext({ messages: [createThreadMessage("thread-b")] });
|
|
405
|
+
await switchB;
|
|
406
|
+
});
|
|
407
|
+
expect(
|
|
408
|
+
result.current.thread.export().messages.map((m) => m.message.id),
|
|
409
|
+
).toEqual(["thread-b"]);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("does not keep the previous thread as a sibling branch after switching to a new thread", async () => {
|
|
413
|
+
const { client, streamMessage } = createMockClient(true);
|
|
414
|
+
let resolveNext!: (value: { messages: ThreadMessage[] }) => void;
|
|
415
|
+
let pending = new Promise<{ messages: ThreadMessage[] }>((resolve) => {
|
|
416
|
+
resolveNext = resolve;
|
|
417
|
+
});
|
|
418
|
+
let resolveNew!: () => void;
|
|
419
|
+
const pendingNew = new Promise<void>((resolve) => {
|
|
420
|
+
resolveNew = resolve;
|
|
421
|
+
});
|
|
422
|
+
const { result } = renderHook(() => {
|
|
423
|
+
const [threadId, setThreadId] = useState("initial");
|
|
424
|
+
return useA2ARuntime({
|
|
425
|
+
client,
|
|
426
|
+
adapters: {
|
|
427
|
+
threadList: {
|
|
428
|
+
threadId,
|
|
429
|
+
onSwitchToThread: async (nextThreadId) => {
|
|
430
|
+
setThreadId(nextThreadId);
|
|
431
|
+
return pending;
|
|
432
|
+
},
|
|
433
|
+
onSwitchToNewThread: async () => {
|
|
434
|
+
setThreadId("thread-new");
|
|
435
|
+
await pendingNew;
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
await act(async () => {
|
|
443
|
+
const switchA = result.current.threads.switchToThread("thread-a");
|
|
444
|
+
resolveNext({ messages: [createThreadMessage("thread-a")] });
|
|
445
|
+
await switchA;
|
|
446
|
+
});
|
|
447
|
+
expect(
|
|
448
|
+
result.current.thread.export().messages.map((m) => m.message.id),
|
|
449
|
+
).toEqual(["thread-a"]);
|
|
450
|
+
|
|
451
|
+
act(() => {
|
|
452
|
+
void result.current.thread.append("still running");
|
|
453
|
+
});
|
|
454
|
+
await waitFor(() => expect(streamMessage).toHaveBeenCalledOnce());
|
|
455
|
+
|
|
456
|
+
let switchNew!: Promise<void>;
|
|
457
|
+
act(() => {
|
|
458
|
+
switchNew = result.current.threads.switchToNewThread();
|
|
459
|
+
});
|
|
460
|
+
expect(result.current.thread.export().messages).toEqual([]);
|
|
461
|
+
|
|
462
|
+
await act(async () => {
|
|
463
|
+
resolveNew();
|
|
464
|
+
await switchNew;
|
|
465
|
+
});
|
|
466
|
+
expect(result.current.thread.export().messages).toEqual([]);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("leaves a new thread empty after an active run and failed creation", async () => {
|
|
470
|
+
const { client, streamMessage } = createMockClient(true);
|
|
471
|
+
let rejectNew!: (error: Error) => void;
|
|
472
|
+
const pendingNew = new Promise<void>((_, reject) => {
|
|
473
|
+
rejectNew = reject;
|
|
474
|
+
});
|
|
475
|
+
const { result } = renderHook(() => {
|
|
476
|
+
const [threadId, setThreadId] = useState("initial");
|
|
477
|
+
return useA2ARuntime({
|
|
478
|
+
client,
|
|
479
|
+
adapters: {
|
|
480
|
+
threadList: {
|
|
481
|
+
threadId,
|
|
482
|
+
onSwitchToNewThread: async () => {
|
|
483
|
+
setThreadId("thread-new");
|
|
484
|
+
await pendingNew;
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
act(() => {
|
|
492
|
+
void result.current.thread.append("still running");
|
|
493
|
+
});
|
|
494
|
+
await waitFor(() => expect(streamMessage).toHaveBeenCalledOnce());
|
|
495
|
+
|
|
496
|
+
let switchNew!: Promise<void>;
|
|
497
|
+
act(() => {
|
|
498
|
+
switchNew = result.current.threads.switchToNewThread();
|
|
499
|
+
});
|
|
500
|
+
await waitFor(() =>
|
|
501
|
+
expect(result.current.threads.getState().mainThreadId).toBe("thread-new"),
|
|
502
|
+
);
|
|
503
|
+
rejectNew(new Error("create failed"));
|
|
504
|
+
await expect(switchNew).rejects.toThrow("create failed");
|
|
505
|
+
|
|
506
|
+
expect(result.current.thread.export()).toEqual({
|
|
507
|
+
headId: null,
|
|
508
|
+
messages: [],
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it("does not clear a newer thread when an older creation finishes", async () => {
|
|
513
|
+
const { client } = createMockClient();
|
|
514
|
+
let resolveNew!: () => void;
|
|
515
|
+
const pendingNew = new Promise<void>((resolve) => {
|
|
516
|
+
resolveNew = resolve;
|
|
517
|
+
});
|
|
518
|
+
const { result } = renderHook(() => {
|
|
519
|
+
const [threadId, setThreadId] = useState("initial");
|
|
520
|
+
return useA2ARuntime({
|
|
521
|
+
client,
|
|
522
|
+
adapters: {
|
|
523
|
+
threadList: {
|
|
524
|
+
threadId,
|
|
525
|
+
onSwitchToThread: async (nextThreadId) => {
|
|
526
|
+
setThreadId(nextThreadId);
|
|
527
|
+
return { messages: [createThreadMessage(nextThreadId)] };
|
|
528
|
+
},
|
|
529
|
+
onSwitchToNewThread: async () => {
|
|
530
|
+
setThreadId("thread-new");
|
|
531
|
+
await pendingNew;
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
let switchNew!: Promise<void>;
|
|
539
|
+
act(() => {
|
|
540
|
+
switchNew = result.current.threads.switchToNewThread();
|
|
541
|
+
});
|
|
542
|
+
await act(async () => {
|
|
543
|
+
await result.current.threads.switchToThread("thread-b");
|
|
544
|
+
});
|
|
545
|
+
await act(async () => {
|
|
546
|
+
resolveNew();
|
|
547
|
+
await switchNew;
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
expect(result.current.threads.getState().mainThreadId).toBe("thread-b");
|
|
551
|
+
expect(result.current.thread.getState().messages.map((m) => m.id)).toEqual([
|
|
552
|
+
"thread-b",
|
|
553
|
+
]);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it("does not restore initial history next to the switched thread", async () => {
|
|
557
|
+
const { client } = createMockClient();
|
|
558
|
+
let resolveHistory!: (repo: ExportedMessageRepository) => void;
|
|
559
|
+
const pendingHistory = new Promise<ExportedMessageRepository>((resolve) => {
|
|
560
|
+
resolveHistory = resolve;
|
|
561
|
+
});
|
|
562
|
+
let resolveSwitch!: (value: { messages: ThreadMessage[] }) => void;
|
|
563
|
+
const pendingSwitch = new Promise<{ messages: ThreadMessage[] }>(
|
|
564
|
+
(resolve) => {
|
|
565
|
+
resolveSwitch = resolve;
|
|
566
|
+
},
|
|
567
|
+
);
|
|
568
|
+
const { result } = renderHook(() => {
|
|
569
|
+
const [threadId, setThreadId] = useState("initial");
|
|
570
|
+
return useA2ARuntime({
|
|
571
|
+
client,
|
|
572
|
+
adapters: {
|
|
573
|
+
history: { load: () => pendingHistory, append: async () => {} },
|
|
574
|
+
threadList: {
|
|
575
|
+
threadId,
|
|
576
|
+
onSwitchToThread: async (nextThreadId) => {
|
|
577
|
+
setThreadId(nextThreadId);
|
|
578
|
+
return pendingSwitch;
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
let switchB!: Promise<void>;
|
|
586
|
+
act(() => {
|
|
587
|
+
switchB = result.current.threads.switchToThread("thread-b");
|
|
588
|
+
});
|
|
589
|
+
await act(async () => {
|
|
590
|
+
resolveHistory({
|
|
591
|
+
headId: "history-a",
|
|
592
|
+
messages: [
|
|
593
|
+
{ parentId: null, message: createThreadMessage("history-a") },
|
|
594
|
+
],
|
|
595
|
+
});
|
|
596
|
+
await pendingHistory;
|
|
597
|
+
});
|
|
598
|
+
await act(async () => {
|
|
599
|
+
resolveSwitch({ messages: [createThreadMessage("thread-b")] });
|
|
600
|
+
await switchB;
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
expect(
|
|
604
|
+
result.current.thread.export().messages.map((m) => m.message.id),
|
|
605
|
+
).toEqual(["thread-b"]);
|
|
606
|
+
});
|
|
607
|
+
|
|
363
608
|
it("keeps pending history when a run is cancelled in the same thread", async () => {
|
|
364
609
|
const { client, streamMessage } = createMockClient(true);
|
|
365
610
|
let resolve!: (repo: ExportedMessageRepository) => void;
|
package/src/useA2ARuntime.ts
CHANGED
|
@@ -121,6 +121,10 @@ export function useA2ARuntime(options: UseA2ARuntimeOptions): AssistantRuntime {
|
|
|
121
121
|
onSwitchToNewThread: onSwitchToNewThread
|
|
122
122
|
? async () => {
|
|
123
123
|
const generation = ++threadSwitchGenerationRef.current;
|
|
124
|
+
// Clear before the thread id flips, or the old messages leak
|
|
125
|
+
// into the new thread as a sibling branch.
|
|
126
|
+
core.applyExternalMessages([]);
|
|
127
|
+
core.resetContext();
|
|
124
128
|
await onSwitchToNewThread();
|
|
125
129
|
if (generation !== threadSwitchGenerationRef.current) return;
|
|
126
130
|
// Apply first so the abort inside resetContext finds an already
|
|
@@ -133,8 +137,13 @@ export function useA2ARuntime(options: UseA2ARuntimeOptions): AssistantRuntime {
|
|
|
133
137
|
onSwitchToThread: onSwitchToThread
|
|
134
138
|
? async (threadId: string) => {
|
|
135
139
|
const generation = ++threadSwitchGenerationRef.current;
|
|
140
|
+
// Clear before the thread id flips, or the old messages leak
|
|
141
|
+
// into the new thread as a sibling branch.
|
|
142
|
+
core.applyExternalMessages([]);
|
|
143
|
+
core.resetContext();
|
|
136
144
|
const result = await onSwitchToThread(threadId);
|
|
137
145
|
if (generation !== threadSwitchGenerationRef.current) return;
|
|
146
|
+
core.applyExternalMessages([]);
|
|
138
147
|
core.applyExternalMessages(result.messages);
|
|
139
148
|
core.resetContext();
|
|
140
149
|
}
|
|
@@ -166,15 +175,20 @@ export function useA2ARuntime(options: UseA2ARuntimeOptions): AssistantRuntime {
|
|
|
166
175
|
isLoading: core.isLoading,
|
|
167
176
|
messageRepository: core.getMessageRepository(),
|
|
168
177
|
isRunning: core.isRunning(),
|
|
178
|
+
unstable_persistsHistory: true,
|
|
169
179
|
extras: a2aExtras.provide({
|
|
170
180
|
task: core.getTask(),
|
|
171
181
|
artifacts: core.getArtifacts(),
|
|
172
182
|
agentCard: core.getAgentCard(),
|
|
173
183
|
}),
|
|
174
184
|
onNew: (message: AppendMessage) => core.append(message),
|
|
185
|
+
onVoiceTranscript: (message: ThreadMessage) =>
|
|
186
|
+
core.appendVoiceTranscript(message),
|
|
175
187
|
onEdit: (message: AppendMessage) => core.edit(message),
|
|
176
188
|
onReload: (parentId: string | null) => core.reload(parentId),
|
|
177
189
|
onCancel: () => core.cancel(),
|
|
190
|
+
unstable_onRecordToolInteraction: (options) =>
|
|
191
|
+
core.recordToolInteraction(options),
|
|
178
192
|
setMessages: (messages: readonly ThreadMessage[]) =>
|
|
179
193
|
core.applyExternalMessages(messages),
|
|
180
194
|
onImport: (messages: readonly ThreadMessage[]) =>
|