@assistant-ui/react-a2a 0.2.29 → 0.2.30
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 +75 -6
- package/dist/A2AClient.js.map +1 -1
- package/dist/A2AThreadRuntimeCore.d.ts.map +1 -1
- package/dist/A2AThreadRuntimeCore.js +19 -3
- package/dist/A2AThreadRuntimeCore.js.map +1 -1
- package/dist/conversions.js +2 -2
- package/dist/conversions.js.map +1 -1
- package/package.json +5 -5
- package/src/A2AClient.test.ts +223 -9
- package/src/A2AClient.ts +155 -6
- package/src/A2AThreadRuntimeCore.test.ts +135 -10
- package/src/A2AThreadRuntimeCore.ts +34 -4
- package/src/conversions.test.ts +16 -0
- package/src/conversions.ts +2 -2
- package/src/useA2ARuntime.test.tsx +20 -6
|
@@ -518,6 +518,25 @@ describe("A2AThreadRuntimeCore", () => {
|
|
|
518
518
|
});
|
|
519
519
|
});
|
|
520
520
|
|
|
521
|
+
it("handles malformed status message parts", async () => {
|
|
522
|
+
const core = createCore({
|
|
523
|
+
streamMessage: vi.fn().mockImplementation(async function* () {
|
|
524
|
+
yield {
|
|
525
|
+
type: "statusUpdate",
|
|
526
|
+
event: {
|
|
527
|
+
taskId: "t1",
|
|
528
|
+
contextId: "ctx-1",
|
|
529
|
+
status: { state: "completed", message: {} },
|
|
530
|
+
},
|
|
531
|
+
} as unknown as A2AStreamEvent;
|
|
532
|
+
}),
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
await core.append(createUserAppendMessage("Go"));
|
|
536
|
+
|
|
537
|
+
expect(core.getMessages()[1]!.content).toEqual([]);
|
|
538
|
+
});
|
|
539
|
+
|
|
521
540
|
it("tracks task state from status updates", async () => {
|
|
522
541
|
const core = createCore({
|
|
523
542
|
streamMessage: vi.fn().mockImplementation(async function* () {
|
|
@@ -628,6 +647,31 @@ describe("A2AThreadRuntimeCore", () => {
|
|
|
628
647
|
// --- Artifact handling ---
|
|
629
648
|
|
|
630
649
|
describe("artifacts", () => {
|
|
650
|
+
it("treats malformed artifact parts as empty", async () => {
|
|
651
|
+
const core = createCore({
|
|
652
|
+
streamMessage: vi.fn().mockImplementation(async function* () {
|
|
653
|
+
yield {
|
|
654
|
+
type: "artifactUpdate",
|
|
655
|
+
event: {
|
|
656
|
+
taskId: "t1",
|
|
657
|
+
contextId: "ctx-1",
|
|
658
|
+
artifact: { artifactId: "a1", parts: {} },
|
|
659
|
+
},
|
|
660
|
+
} as unknown as A2AStreamEvent;
|
|
661
|
+
yield artifactUpdateEvent("a1", [{ text: "part" }], {
|
|
662
|
+
append: true,
|
|
663
|
+
});
|
|
664
|
+
yield statusUpdateEvent("completed", "Done");
|
|
665
|
+
}),
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
await core.append(createUserAppendMessage("Go"));
|
|
669
|
+
|
|
670
|
+
expect(core.getArtifacts()).toEqual([
|
|
671
|
+
{ artifactId: "a1", parts: [{ text: "part" }] },
|
|
672
|
+
]);
|
|
673
|
+
});
|
|
674
|
+
|
|
631
675
|
it("accumulates artifacts from artifact update events", async () => {
|
|
632
676
|
const core = createCore({
|
|
633
677
|
streamMessage: vi.fn().mockImplementation(async function* () {
|
|
@@ -825,6 +869,71 @@ describe("A2AThreadRuntimeCore", () => {
|
|
|
825
869
|
// --- Task snapshot ---
|
|
826
870
|
|
|
827
871
|
describe("task snapshot", () => {
|
|
872
|
+
it("treats malformed artifact parts as empty", async () => {
|
|
873
|
+
const taskSnapshot = {
|
|
874
|
+
id: "t1",
|
|
875
|
+
status: { state: "completed" },
|
|
876
|
+
artifacts: [{ artifactId: "a1", parts: {} }],
|
|
877
|
+
} as unknown as A2ATask;
|
|
878
|
+
|
|
879
|
+
const core = createCore({
|
|
880
|
+
streamMessage: vi.fn().mockImplementation(async function* () {
|
|
881
|
+
yield { type: "task", task: taskSnapshot } as A2AStreamEvent;
|
|
882
|
+
}),
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
await core.append(createUserAppendMessage("Go"));
|
|
886
|
+
|
|
887
|
+
expect(core.getArtifacts()).toEqual([{ artifactId: "a1", parts: [] }]);
|
|
888
|
+
expect(core.getTask()?.artifacts).toEqual([
|
|
889
|
+
{ artifactId: "a1", parts: [] },
|
|
890
|
+
]);
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
it.each([undefined, null, {}, "not-an-array"])(
|
|
894
|
+
"does not consume malformed task artifacts: %j",
|
|
895
|
+
async (artifacts) => {
|
|
896
|
+
const taskSnapshot = {
|
|
897
|
+
id: "t1",
|
|
898
|
+
status: { state: "completed" },
|
|
899
|
+
artifacts,
|
|
900
|
+
history: artifacts,
|
|
901
|
+
} as unknown as A2ATask;
|
|
902
|
+
const core = createCore({
|
|
903
|
+
streamMessage: vi.fn().mockImplementation(async function* () {
|
|
904
|
+
yield { type: "task", task: taskSnapshot } as A2AStreamEvent;
|
|
905
|
+
}),
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
await core.append(createUserAppendMessage("Go"));
|
|
909
|
+
|
|
910
|
+
expect(core.getArtifacts()).toEqual([]);
|
|
911
|
+
if (artifacts === undefined) {
|
|
912
|
+
expect(core.getTask()?.artifacts).toBeUndefined();
|
|
913
|
+
expect(core.getTask()?.history).toBeUndefined();
|
|
914
|
+
} else {
|
|
915
|
+
expect(core.getTask()?.artifacts).toEqual([]);
|
|
916
|
+
expect(core.getTask()?.history).toEqual([]);
|
|
917
|
+
}
|
|
918
|
+
},
|
|
919
|
+
);
|
|
920
|
+
|
|
921
|
+
it("handles malformed status message parts", async () => {
|
|
922
|
+
const taskSnapshot = {
|
|
923
|
+
id: "t1",
|
|
924
|
+
status: { state: "completed", message: {} },
|
|
925
|
+
} as unknown as A2ATask;
|
|
926
|
+
const core = createCore({
|
|
927
|
+
streamMessage: vi.fn().mockImplementation(async function* () {
|
|
928
|
+
yield { type: "task", task: taskSnapshot } as A2AStreamEvent;
|
|
929
|
+
}),
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
await core.append(createUserAppendMessage("Go"));
|
|
933
|
+
|
|
934
|
+
expect(core.getMessages()[1]!.content).toEqual([]);
|
|
935
|
+
});
|
|
936
|
+
|
|
828
937
|
it("handles full task snapshot from stream", async () => {
|
|
829
938
|
const taskSnapshot: A2ATask = {
|
|
830
939
|
id: "t1",
|
|
@@ -1077,19 +1186,32 @@ describe("A2AThreadRuntimeCore", () => {
|
|
|
1077
1186
|
},
|
|
1078
1187
|
);
|
|
1079
1188
|
|
|
1080
|
-
it("
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1189
|
+
it("rejects a stream that ends without any events", async () => {
|
|
1190
|
+
const onError = vi.fn();
|
|
1191
|
+
const core = createCore(
|
|
1192
|
+
{
|
|
1193
|
+
streamMessage: vi.fn().mockImplementation(async function* () {
|
|
1194
|
+
return;
|
|
1195
|
+
}),
|
|
1196
|
+
},
|
|
1197
|
+
{
|
|
1198
|
+
onError,
|
|
1199
|
+
},
|
|
1200
|
+
);
|
|
1086
1201
|
|
|
1087
|
-
await core.append(createUserAppendMessage("Go"))
|
|
1202
|
+
await expect(core.append(createUserAppendMessage("Go"))).rejects.toThrow(
|
|
1203
|
+
"A2A message stream ended without any events.",
|
|
1204
|
+
);
|
|
1088
1205
|
|
|
1206
|
+
expect(onError).toHaveBeenCalledWith(
|
|
1207
|
+
expect.objectContaining({
|
|
1208
|
+
message: "A2A message stream ended without any events.",
|
|
1209
|
+
}),
|
|
1210
|
+
);
|
|
1089
1211
|
const assistant = core.getMessages()[1]!;
|
|
1090
1212
|
expect(assistant.status).toEqual({
|
|
1091
|
-
type: "
|
|
1092
|
-
reason: "
|
|
1213
|
+
type: "incomplete",
|
|
1214
|
+
reason: "error",
|
|
1093
1215
|
});
|
|
1094
1216
|
});
|
|
1095
1217
|
});
|
|
@@ -1152,6 +1274,7 @@ describe("A2AThreadRuntimeCore", () => {
|
|
|
1152
1274
|
streamMessage.mock.calls.length === 1 ? firstPending : secondPending;
|
|
1153
1275
|
return (async function* () {
|
|
1154
1276
|
await pending;
|
|
1277
|
+
yield statusUpdateEvent("completed", "Done");
|
|
1155
1278
|
})();
|
|
1156
1279
|
});
|
|
1157
1280
|
const core = createCore({ streamMessage });
|
|
@@ -1200,7 +1323,9 @@ describe("A2AThreadRuntimeCore", () => {
|
|
|
1200
1323
|
|
|
1201
1324
|
describe("outbound message conversion", () => {
|
|
1202
1325
|
function createCoreWithStream() {
|
|
1203
|
-
const streamMessage = vi.fn().mockImplementation(async function* () {
|
|
1326
|
+
const streamMessage = vi.fn().mockImplementation(async function* () {
|
|
1327
|
+
yield statusUpdateEvent("completed", "Done");
|
|
1328
|
+
});
|
|
1204
1329
|
const core = new A2AThreadRuntimeCore({
|
|
1205
1330
|
client: createMockClient({ streamMessage }),
|
|
1206
1331
|
notifyUpdate: vi.fn() as unknown as () => void,
|
|
@@ -76,6 +76,13 @@ const invokeRuntimeCallback = <TArgs extends unknown[]>(
|
|
|
76
76
|
}
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
+
function normalizeArtifact(artifact: A2AArtifact): A2AArtifact {
|
|
80
|
+
return {
|
|
81
|
+
...artifact,
|
|
82
|
+
parts: Array.isArray(artifact.parts) ? artifact.parts : [],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
79
86
|
export class A2AThreadRuntimeCore {
|
|
80
87
|
private client: A2AClient;
|
|
81
88
|
private contextId: string | undefined;
|
|
@@ -526,12 +533,18 @@ export class A2AThreadRuntimeCore {
|
|
|
526
533
|
abortController.signal,
|
|
527
534
|
);
|
|
528
535
|
|
|
536
|
+
let receivedEvent = false;
|
|
529
537
|
for await (const event of stream) {
|
|
530
538
|
if (abortController.signal.aborted) break;
|
|
539
|
+
receivedEvent = true;
|
|
531
540
|
this.handleStreamEvent(assistantId, event);
|
|
532
541
|
}
|
|
533
542
|
|
|
534
543
|
if (!abortController.signal.aborted) {
|
|
544
|
+
if (!receivedEvent) {
|
|
545
|
+
throw new Error("A2A message stream ended without any events.");
|
|
546
|
+
}
|
|
547
|
+
|
|
535
548
|
const lastStatus = this.getAssistantStatus(assistantId);
|
|
536
549
|
if (lastStatus?.type === "running") {
|
|
537
550
|
this.updateAssistantStatus(assistantId, {
|
|
@@ -617,7 +630,8 @@ export class A2AThreadRuntimeCore {
|
|
|
617
630
|
}
|
|
618
631
|
|
|
619
632
|
private handleArtifactUpdate(event: A2ATaskArtifactUpdateEvent) {
|
|
620
|
-
const {
|
|
633
|
+
const { append, lastChunk } = event;
|
|
634
|
+
const artifact = normalizeArtifact(event.artifact);
|
|
621
635
|
const existingIdx = this.currentArtifacts.findIndex(
|
|
622
636
|
(a) => a.artifactId === artifact.artifactId,
|
|
623
637
|
);
|
|
@@ -666,13 +680,29 @@ export class A2AThreadRuntimeCore {
|
|
|
666
680
|
}
|
|
667
681
|
|
|
668
682
|
private handleTaskSnapshot(assistantId: string, task: A2ATask) {
|
|
669
|
-
|
|
683
|
+
const artifacts =
|
|
684
|
+
task.artifacts === undefined
|
|
685
|
+
? undefined
|
|
686
|
+
: Array.isArray(task.artifacts)
|
|
687
|
+
? task.artifacts.map(normalizeArtifact)
|
|
688
|
+
: [];
|
|
689
|
+
const history =
|
|
690
|
+
task.history === undefined
|
|
691
|
+
? undefined
|
|
692
|
+
: Array.isArray(task.history)
|
|
693
|
+
? task.history
|
|
694
|
+
: [];
|
|
695
|
+
this.currentTask = {
|
|
696
|
+
...task,
|
|
697
|
+
...(artifacts === undefined ? {} : { artifacts }),
|
|
698
|
+
...(history === undefined ? {} : { history }),
|
|
699
|
+
};
|
|
670
700
|
|
|
671
701
|
if (task.contextId) {
|
|
672
702
|
this.contextId = task.contextId;
|
|
673
703
|
}
|
|
674
|
-
if (
|
|
675
|
-
this.currentArtifacts =
|
|
704
|
+
if (artifacts) {
|
|
705
|
+
this.currentArtifacts = artifacts;
|
|
676
706
|
}
|
|
677
707
|
|
|
678
708
|
if (task.status.message) {
|
package/src/conversions.test.ts
CHANGED
|
@@ -228,6 +228,13 @@ describe("a2aPartsToContent", () => {
|
|
|
228
228
|
it("handles empty parts array", () => {
|
|
229
229
|
expect(a2aPartsToContent([])).toEqual([]);
|
|
230
230
|
});
|
|
231
|
+
|
|
232
|
+
it.each([undefined, null, {}, "not-an-array"])(
|
|
233
|
+
"treats %j parts as empty content",
|
|
234
|
+
(parts) => {
|
|
235
|
+
expect(a2aPartsToContent(parts as unknown as A2APart[])).toEqual([]);
|
|
236
|
+
},
|
|
237
|
+
);
|
|
231
238
|
});
|
|
232
239
|
|
|
233
240
|
describe("a2aMessageToContent", () => {
|
|
@@ -242,6 +249,15 @@ describe("a2aMessageToContent", () => {
|
|
|
242
249
|
expect(result[0]).toEqual({ type: "text", text: "Hello" });
|
|
243
250
|
expect(result[1]).toEqual({ type: "text", text: " world" });
|
|
244
251
|
});
|
|
252
|
+
|
|
253
|
+
it.each([undefined, null, {}, "not-an-array"])(
|
|
254
|
+
"treats a message with %j parts as empty content",
|
|
255
|
+
(parts) => {
|
|
256
|
+
expect(a2aMessageToContent({ parts } as unknown as A2AMessage)).toEqual(
|
|
257
|
+
[],
|
|
258
|
+
);
|
|
259
|
+
},
|
|
260
|
+
);
|
|
245
261
|
});
|
|
246
262
|
|
|
247
263
|
describe("taskStateToMessageStatus", () => {
|
package/src/conversions.ts
CHANGED
|
@@ -54,7 +54,7 @@ export function a2aPartToContent(
|
|
|
54
54
|
export function a2aPartsToContent(
|
|
55
55
|
parts: A2APart[],
|
|
56
56
|
): ThreadAssistantMessage["content"] {
|
|
57
|
-
return parts.map(a2aPartToContent);
|
|
57
|
+
return (Array.isArray(parts) ? parts : []).map(a2aPartToContent);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
const TERMINAL_STATES = new Set<A2ATaskState>([
|
|
@@ -183,5 +183,5 @@ export function contentPartsToA2AParts(
|
|
|
183
183
|
export function a2aMessageToContent(
|
|
184
184
|
message: A2AMessage,
|
|
185
185
|
): ThreadAssistantMessage["content"] {
|
|
186
|
-
return a2aPartsToContent(message
|
|
186
|
+
return a2aPartsToContent(message?.parts ?? []);
|
|
187
187
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import type { A2AClient } from "./A2AClient";
|
|
6
|
+
import type { A2AStreamEvent } from "./types";
|
|
6
7
|
import { useA2ARuntime } from "./useA2ARuntime";
|
|
7
8
|
|
|
8
9
|
const createMockClient = (waitForAbort = false) => {
|
|
@@ -14,11 +15,21 @@ const createMockClient = (waitForAbort = false) => {
|
|
|
14
15
|
_configuration: unknown,
|
|
15
16
|
_metadata: unknown,
|
|
16
17
|
signal?: AbortSignal,
|
|
17
|
-
): AsyncIterable<
|
|
18
|
+
): AsyncIterable<A2AStreamEvent> => {
|
|
18
19
|
streamSignal = signal;
|
|
19
20
|
return {
|
|
20
21
|
async *[Symbol.asyncIterator]() {
|
|
21
|
-
if (!waitForAbort || !signal)
|
|
22
|
+
if (!waitForAbort || !signal) {
|
|
23
|
+
yield {
|
|
24
|
+
type: "message",
|
|
25
|
+
message: {
|
|
26
|
+
messageId: "response",
|
|
27
|
+
role: "agent",
|
|
28
|
+
parts: [{ text: "Done" }],
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
22
33
|
await new Promise<void>((resolve) => {
|
|
23
34
|
if (signal.aborted) {
|
|
24
35
|
resolve();
|
|
@@ -103,10 +114,13 @@ describe("useA2ARuntime", () => {
|
|
|
103
114
|
},
|
|
104
115
|
);
|
|
105
116
|
}
|
|
106
|
-
return new Response(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
117
|
+
return new Response(
|
|
118
|
+
'data: {"message":{"message_id":"response","role":"ROLE_AGENT","parts":[{"text":"Done"}]}}\n\n',
|
|
119
|
+
{
|
|
120
|
+
status: 200,
|
|
121
|
+
headers: { "Content-Type": "text/event-stream" },
|
|
122
|
+
},
|
|
123
|
+
);
|
|
110
124
|
},
|
|
111
125
|
);
|
|
112
126
|
vi.stubGlobal("fetch", fetchMock);
|