@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
package/src/A2AClient.test.ts
CHANGED
|
@@ -43,6 +43,7 @@ function mockSSETextResponse(
|
|
|
43
43
|
return Promise.resolve({ done: true, value: undefined });
|
|
44
44
|
}),
|
|
45
45
|
releaseLock: vi.fn(),
|
|
46
|
+
cancel: vi.fn(async () => {}),
|
|
46
47
|
}),
|
|
47
48
|
},
|
|
48
49
|
} as unknown as Response;
|
|
@@ -742,6 +743,20 @@ describe("A2AClient", () => {
|
|
|
742
743
|
"Invalid A2A tasks:get response: expected a valid task payload.",
|
|
743
744
|
);
|
|
744
745
|
});
|
|
746
|
+
|
|
747
|
+
it("rejects a non-string contextId", async () => {
|
|
748
|
+
fetchMock.mockResolvedValue(
|
|
749
|
+
mockFetchResponse({
|
|
750
|
+
id: "t1",
|
|
751
|
+
contextId: 999,
|
|
752
|
+
status: { state: "completed" },
|
|
753
|
+
}),
|
|
754
|
+
);
|
|
755
|
+
|
|
756
|
+
await expect(client.getTask("t1")).rejects.toThrow(
|
|
757
|
+
"Invalid A2A tasks:get response: expected a valid task payload.",
|
|
758
|
+
);
|
|
759
|
+
});
|
|
745
760
|
});
|
|
746
761
|
|
|
747
762
|
// --- listTasks ---
|
|
@@ -1016,6 +1031,228 @@ describe("A2AClient", () => {
|
|
|
1016
1031
|
expect(evt.event.status.message?.role).toBe("agent");
|
|
1017
1032
|
});
|
|
1018
1033
|
|
|
1034
|
+
it("drops a wrapped task or message whose ids are not strings", async () => {
|
|
1035
|
+
const frames = [
|
|
1036
|
+
{
|
|
1037
|
+
task: {
|
|
1038
|
+
id: "t1",
|
|
1039
|
+
context_id: 999,
|
|
1040
|
+
status: { state: "TASK_STATE_WORKING" },
|
|
1041
|
+
},
|
|
1042
|
+
},
|
|
1043
|
+
{
|
|
1044
|
+
message: {
|
|
1045
|
+
message_id: "m1",
|
|
1046
|
+
context_id: 42,
|
|
1047
|
+
role: "ROLE_AGENT",
|
|
1048
|
+
parts: [{ text: "hi" }],
|
|
1049
|
+
},
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
message: {
|
|
1053
|
+
message_id: "m2",
|
|
1054
|
+
task_id: { nested: "object" },
|
|
1055
|
+
role: "ROLE_AGENT",
|
|
1056
|
+
parts: [{ text: "hi" }],
|
|
1057
|
+
},
|
|
1058
|
+
},
|
|
1059
|
+
];
|
|
1060
|
+
|
|
1061
|
+
for (const frame of frames) {
|
|
1062
|
+
fetchMock.mockResolvedValue(
|
|
1063
|
+
mockSSEResponse([`data: ${JSON.stringify(frame)}`, "", ""]),
|
|
1064
|
+
);
|
|
1065
|
+
|
|
1066
|
+
const events: A2AStreamEvent[] = [];
|
|
1067
|
+
for await (const event of client.streamMessage(userMessage)) {
|
|
1068
|
+
events.push(event);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
expect(events).toEqual([]);
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
it("drops a wrapped status update whose contextId is not a string", async () => {
|
|
1076
|
+
const sseData = JSON.stringify({
|
|
1077
|
+
status_update: {
|
|
1078
|
+
task_id: "t1",
|
|
1079
|
+
context_id: 999,
|
|
1080
|
+
status: { state: "TASK_STATE_WORKING" },
|
|
1081
|
+
},
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
fetchMock.mockResolvedValue(
|
|
1085
|
+
mockSSEResponse([`data: ${sseData}`, "", ""]),
|
|
1086
|
+
);
|
|
1087
|
+
|
|
1088
|
+
const events: A2AStreamEvent[] = [];
|
|
1089
|
+
for await (const event of client.streamMessage(userMessage)) {
|
|
1090
|
+
events.push(event);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
expect(events).toEqual([]);
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
it("drops a wrapped artifact update whose ids are not strings", async () => {
|
|
1097
|
+
const artifact = { artifact_id: "a1", parts: [{ text: "hi" }] };
|
|
1098
|
+
for (const ids of [
|
|
1099
|
+
{ task_id: 12345, context_id: "c1" },
|
|
1100
|
+
{ task_id: "t1", context_id: { nested: "object" } },
|
|
1101
|
+
]) {
|
|
1102
|
+
const sseData = JSON.stringify({
|
|
1103
|
+
artifact_update: { ...ids, artifact },
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
fetchMock.mockResolvedValue(
|
|
1107
|
+
mockSSEResponse([`data: ${sseData}`, "", ""]),
|
|
1108
|
+
);
|
|
1109
|
+
|
|
1110
|
+
const events: A2AStreamEvent[] = [];
|
|
1111
|
+
for await (const event of client.streamMessage(userMessage)) {
|
|
1112
|
+
events.push(event);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
expect(events).toEqual([]);
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
it("normalizes default-valued wrapped status updates", async () => {
|
|
1120
|
+
const sseData = JSON.stringify({
|
|
1121
|
+
status_update: {
|
|
1122
|
+
status: {
|
|
1123
|
+
message: {
|
|
1124
|
+
message_id: "s1",
|
|
1125
|
+
role: "ROLE_AGENT",
|
|
1126
|
+
parts: [{ text: "Waiting..." }],
|
|
1127
|
+
},
|
|
1128
|
+
},
|
|
1129
|
+
},
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
fetchMock.mockResolvedValue(
|
|
1133
|
+
mockSSEResponse([`data: ${sseData}`, "", ""]),
|
|
1134
|
+
);
|
|
1135
|
+
|
|
1136
|
+
const events: A2AStreamEvent[] = [];
|
|
1137
|
+
for await (const event of client.streamMessage(userMessage)) {
|
|
1138
|
+
events.push(event);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
expect(events).toHaveLength(1);
|
|
1142
|
+
const evt = events[0] as Extract<
|
|
1143
|
+
A2AStreamEvent,
|
|
1144
|
+
{ type: "statusUpdate" }
|
|
1145
|
+
>;
|
|
1146
|
+
expect(evt.event).toMatchObject({
|
|
1147
|
+
taskId: "",
|
|
1148
|
+
contextId: "",
|
|
1149
|
+
status: {
|
|
1150
|
+
state: "unspecified",
|
|
1151
|
+
message: { parts: [{ text: "Waiting..." }] },
|
|
1152
|
+
},
|
|
1153
|
+
});
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
it("normalizes null-valued wrapped status defaults", async () => {
|
|
1157
|
+
const sseData = JSON.stringify({
|
|
1158
|
+
status_update: {
|
|
1159
|
+
task_id: null,
|
|
1160
|
+
context_id: null,
|
|
1161
|
+
status: { state: null },
|
|
1162
|
+
},
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
fetchMock.mockResolvedValue(
|
|
1166
|
+
mockSSEResponse([`data: ${sseData}`, "", ""]),
|
|
1167
|
+
);
|
|
1168
|
+
|
|
1169
|
+
const events: A2AStreamEvent[] = [];
|
|
1170
|
+
for await (const event of client.streamMessage(userMessage)) {
|
|
1171
|
+
events.push(event);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
expect(events).toMatchObject([
|
|
1175
|
+
{
|
|
1176
|
+
type: "statusUpdate",
|
|
1177
|
+
event: {
|
|
1178
|
+
taskId: "",
|
|
1179
|
+
contextId: "",
|
|
1180
|
+
status: { state: "unspecified" },
|
|
1181
|
+
},
|
|
1182
|
+
},
|
|
1183
|
+
]);
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
it("normalizes ProtoJSON defaults across wrapped event types", async () => {
|
|
1187
|
+
const frames = [
|
|
1188
|
+
{
|
|
1189
|
+
task: {
|
|
1190
|
+
id: "t1",
|
|
1191
|
+
status: {
|
|
1192
|
+
message: {
|
|
1193
|
+
message_id: "m1",
|
|
1194
|
+
role: "ROLE_AGENT",
|
|
1195
|
+
parts: [{ text: "Working..." }],
|
|
1196
|
+
},
|
|
1197
|
+
},
|
|
1198
|
+
},
|
|
1199
|
+
},
|
|
1200
|
+
{
|
|
1201
|
+
message: {
|
|
1202
|
+
message_id: "m2",
|
|
1203
|
+
role: "ROLE_AGENT",
|
|
1204
|
+
},
|
|
1205
|
+
},
|
|
1206
|
+
{
|
|
1207
|
+
artifact_update: {
|
|
1208
|
+
artifact: {},
|
|
1209
|
+
},
|
|
1210
|
+
},
|
|
1211
|
+
];
|
|
1212
|
+
|
|
1213
|
+
fetchMock.mockResolvedValue(
|
|
1214
|
+
mockSSEResponse(
|
|
1215
|
+
frames
|
|
1216
|
+
.flatMap((frame) => [`data: ${JSON.stringify(frame)}`, ""])
|
|
1217
|
+
.concat(""),
|
|
1218
|
+
),
|
|
1219
|
+
);
|
|
1220
|
+
|
|
1221
|
+
const events: A2AStreamEvent[] = [];
|
|
1222
|
+
for await (const event of client.streamMessage(userMessage)) {
|
|
1223
|
+
events.push(event);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
expect(events).toMatchObject([
|
|
1227
|
+
{
|
|
1228
|
+
type: "task",
|
|
1229
|
+
task: {
|
|
1230
|
+
id: "t1",
|
|
1231
|
+
contextId: "",
|
|
1232
|
+
status: { state: "unspecified" },
|
|
1233
|
+
},
|
|
1234
|
+
},
|
|
1235
|
+
{
|
|
1236
|
+
type: "message",
|
|
1237
|
+
message: {
|
|
1238
|
+
messageId: "m2",
|
|
1239
|
+
contextId: "",
|
|
1240
|
+
taskId: "",
|
|
1241
|
+
role: "agent",
|
|
1242
|
+
parts: [],
|
|
1243
|
+
},
|
|
1244
|
+
},
|
|
1245
|
+
{
|
|
1246
|
+
type: "artifactUpdate",
|
|
1247
|
+
event: {
|
|
1248
|
+
taskId: "",
|
|
1249
|
+
contextId: "",
|
|
1250
|
+
artifact: { artifactId: "", parts: [] },
|
|
1251
|
+
},
|
|
1252
|
+
},
|
|
1253
|
+
]);
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1019
1256
|
it("cancels the response body when iteration stops early", async () => {
|
|
1020
1257
|
const sseData = JSON.stringify({
|
|
1021
1258
|
status_update: {
|
|
@@ -1425,6 +1662,14 @@ describe("A2AClient", () => {
|
|
|
1425
1662
|
"",
|
|
1426
1663
|
"data: {}",
|
|
1427
1664
|
"",
|
|
1665
|
+
'data: {"task":{}}',
|
|
1666
|
+
"",
|
|
1667
|
+
'data: {"message":{}}',
|
|
1668
|
+
"",
|
|
1669
|
+
'data: {"status_update":{}}',
|
|
1670
|
+
"",
|
|
1671
|
+
'data: {"artifact_update":{}}',
|
|
1672
|
+
"",
|
|
1428
1673
|
`data: ${second}`,
|
|
1429
1674
|
"",
|
|
1430
1675
|
"",
|
|
@@ -1442,6 +1687,56 @@ describe("A2AClient", () => {
|
|
|
1442
1687
|
"statusUpdate",
|
|
1443
1688
|
]);
|
|
1444
1689
|
});
|
|
1690
|
+
|
|
1691
|
+
it("returns the first skip reason when a frame fails to parse", async () => {
|
|
1692
|
+
fetchMock.mockResolvedValue(
|
|
1693
|
+
mockSSEResponse(["data: {invalid json}", "", "data: {}", "", ""]),
|
|
1694
|
+
);
|
|
1695
|
+
|
|
1696
|
+
const stream = client.streamMessage(userMessage);
|
|
1697
|
+
const events: A2AStreamEvent[] = [];
|
|
1698
|
+
let result = await stream.next();
|
|
1699
|
+
while (!result.done) {
|
|
1700
|
+
events.push(result.value);
|
|
1701
|
+
result = await stream.next();
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
let parseMessage = "";
|
|
1705
|
+
try {
|
|
1706
|
+
JSON.parse("{invalid json}");
|
|
1707
|
+
} catch (error) {
|
|
1708
|
+
parseMessage = (error as Error).message;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
expect(events).toEqual([]);
|
|
1712
|
+
expect(result.value).toBe(`${parseMessage} (frame: {invalid json})`);
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
it("returns an unrecognized-shape skip reason with the frame on one line", async () => {
|
|
1716
|
+
fetchMock.mockResolvedValue(
|
|
1717
|
+
mockSSEResponse(["data: {", "data: }", "", ""]),
|
|
1718
|
+
);
|
|
1719
|
+
|
|
1720
|
+
const stream = client.streamMessage(userMessage);
|
|
1721
|
+
const result = await stream.next();
|
|
1722
|
+
|
|
1723
|
+
expect(result).toEqual({
|
|
1724
|
+
done: true,
|
|
1725
|
+
value: "unrecognized event shape (frame: { })",
|
|
1726
|
+
});
|
|
1727
|
+
});
|
|
1728
|
+
|
|
1729
|
+
it("truncates an oversized skipped frame", async () => {
|
|
1730
|
+
const frame = `{"nonsense":"${"x".repeat(400)}"}`;
|
|
1731
|
+
fetchMock.mockResolvedValue(mockSSEResponse([`data: ${frame}`, "", ""]));
|
|
1732
|
+
|
|
1733
|
+
const stream = client.streamMessage(userMessage);
|
|
1734
|
+
const result = await stream.next();
|
|
1735
|
+
|
|
1736
|
+
expect(result.value).toBe(
|
|
1737
|
+
`unrecognized event shape (frame: ${frame.slice(0, 120)}…)`,
|
|
1738
|
+
);
|
|
1739
|
+
});
|
|
1445
1740
|
});
|
|
1446
1741
|
|
|
1447
1742
|
// --- Push notification configs ---
|
|
@@ -1681,4 +1976,423 @@ describe("A2AClient", () => {
|
|
|
1681
1976
|
expect(card.skills[0]).toMatchObject({ id: "s", tags: [] });
|
|
1682
1977
|
});
|
|
1683
1978
|
});
|
|
1979
|
+
|
|
1980
|
+
describe("streamMessage - JSON-RPC kind-discriminated results", () => {
|
|
1981
|
+
const rpc = (result: unknown) =>
|
|
1982
|
+
`data: ${JSON.stringify({ jsonrpc: "2.0", id: 1, result })}`;
|
|
1983
|
+
|
|
1984
|
+
it("parses a flat status-update result", async () => {
|
|
1985
|
+
fetchMock.mockResolvedValue(
|
|
1986
|
+
mockSSEResponse([
|
|
1987
|
+
rpc({
|
|
1988
|
+
kind: "status-update",
|
|
1989
|
+
taskId: "t1",
|
|
1990
|
+
contextId: "ctx-1",
|
|
1991
|
+
status: { state: "working" },
|
|
1992
|
+
final: false,
|
|
1993
|
+
}),
|
|
1994
|
+
"",
|
|
1995
|
+
"",
|
|
1996
|
+
]),
|
|
1997
|
+
);
|
|
1998
|
+
|
|
1999
|
+
const events: A2AStreamEvent[] = [];
|
|
2000
|
+
for await (const event of client.streamMessage(userMessage)) {
|
|
2001
|
+
events.push(event);
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
expect(events).toHaveLength(1);
|
|
2005
|
+
expect(events[0]!.type).toBe("statusUpdate");
|
|
2006
|
+
const evt = events[0] as Extract<
|
|
2007
|
+
A2AStreamEvent,
|
|
2008
|
+
{ type: "statusUpdate" }
|
|
2009
|
+
>;
|
|
2010
|
+
expect(evt.event.taskId).toBe("t1");
|
|
2011
|
+
expect(evt.event.status.state).toBe("working");
|
|
2012
|
+
});
|
|
2013
|
+
|
|
2014
|
+
it("parses a flat task result and maps hyphenated states", async () => {
|
|
2015
|
+
fetchMock.mockResolvedValue(
|
|
2016
|
+
mockSSEResponse([
|
|
2017
|
+
rpc({
|
|
2018
|
+
kind: "task",
|
|
2019
|
+
id: "t1",
|
|
2020
|
+
contextId: "ctx-1",
|
|
2021
|
+
status: { state: "input-required" },
|
|
2022
|
+
}),
|
|
2023
|
+
"",
|
|
2024
|
+
"",
|
|
2025
|
+
]),
|
|
2026
|
+
);
|
|
2027
|
+
|
|
2028
|
+
const events: A2AStreamEvent[] = [];
|
|
2029
|
+
for await (const event of client.streamMessage(userMessage)) {
|
|
2030
|
+
events.push(event);
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
expect(events).toHaveLength(1);
|
|
2034
|
+
expect(events[0]!.type).toBe("task");
|
|
2035
|
+
const evt = events[0] as Extract<A2AStreamEvent, { type: "task" }>;
|
|
2036
|
+
expect(evt.task.status.state).toBe("input_required");
|
|
2037
|
+
});
|
|
2038
|
+
|
|
2039
|
+
it("maps the unknown wire state to unspecified", async () => {
|
|
2040
|
+
fetchMock.mockResolvedValue(
|
|
2041
|
+
mockSSEResponse([
|
|
2042
|
+
rpc({
|
|
2043
|
+
kind: "task",
|
|
2044
|
+
id: "t1",
|
|
2045
|
+
contextId: "ctx-1",
|
|
2046
|
+
status: { state: "unknown" },
|
|
2047
|
+
}),
|
|
2048
|
+
"",
|
|
2049
|
+
"",
|
|
2050
|
+
]),
|
|
2051
|
+
);
|
|
2052
|
+
|
|
2053
|
+
const events: A2AStreamEvent[] = [];
|
|
2054
|
+
for await (const event of client.streamMessage(userMessage)) {
|
|
2055
|
+
events.push(event);
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
const evt = events[0] as Extract<A2AStreamEvent, { type: "task" }>;
|
|
2059
|
+
expect(evt.task.status.state).toBe("unspecified");
|
|
2060
|
+
});
|
|
2061
|
+
|
|
2062
|
+
it("strips the wire discriminators from emitted events", async () => {
|
|
2063
|
+
fetchMock.mockResolvedValue(
|
|
2064
|
+
mockSSEResponse([
|
|
2065
|
+
rpc({
|
|
2066
|
+
kind: "status-update",
|
|
2067
|
+
taskId: "t1",
|
|
2068
|
+
contextId: "ctx-1",
|
|
2069
|
+
status: { state: "working" },
|
|
2070
|
+
final: true,
|
|
2071
|
+
}),
|
|
2072
|
+
"",
|
|
2073
|
+
"",
|
|
2074
|
+
]),
|
|
2075
|
+
);
|
|
2076
|
+
|
|
2077
|
+
const events: A2AStreamEvent[] = [];
|
|
2078
|
+
for await (const event of client.streamMessage(userMessage)) {
|
|
2079
|
+
events.push(event);
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
const evt = events[0] as Extract<
|
|
2083
|
+
A2AStreamEvent,
|
|
2084
|
+
{ type: "statusUpdate" }
|
|
2085
|
+
>;
|
|
2086
|
+
expect(evt.event).not.toHaveProperty("kind");
|
|
2087
|
+
expect(evt.event).not.toHaveProperty("final");
|
|
2088
|
+
});
|
|
2089
|
+
|
|
2090
|
+
it("strips the kind discriminator from text and data parts", async () => {
|
|
2091
|
+
fetchMock.mockResolvedValue(
|
|
2092
|
+
mockSSEResponse([
|
|
2093
|
+
rpc({
|
|
2094
|
+
kind: "message",
|
|
2095
|
+
messageId: "m1",
|
|
2096
|
+
role: "agent",
|
|
2097
|
+
parts: [
|
|
2098
|
+
{ kind: "text", text: "hello" },
|
|
2099
|
+
{ kind: "data", data: { answer: 42 } },
|
|
2100
|
+
],
|
|
2101
|
+
}),
|
|
2102
|
+
"",
|
|
2103
|
+
"",
|
|
2104
|
+
]),
|
|
2105
|
+
);
|
|
2106
|
+
|
|
2107
|
+
const events: A2AStreamEvent[] = [];
|
|
2108
|
+
for await (const event of client.streamMessage(userMessage)) {
|
|
2109
|
+
events.push(event);
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
const evt = events[0] as Extract<A2AStreamEvent, { type: "message" }>;
|
|
2113
|
+
expect(evt.message.parts).toEqual([
|
|
2114
|
+
{ text: "hello" },
|
|
2115
|
+
{ data: { answer: 42 } },
|
|
2116
|
+
]);
|
|
2117
|
+
});
|
|
2118
|
+
|
|
2119
|
+
it("flattens nested file parts onto the internal part shape", async () => {
|
|
2120
|
+
fetchMock.mockResolvedValue(
|
|
2121
|
+
mockSSEResponse([
|
|
2122
|
+
rpc({
|
|
2123
|
+
kind: "message",
|
|
2124
|
+
messageId: "m1",
|
|
2125
|
+
role: "agent",
|
|
2126
|
+
parts: [
|
|
2127
|
+
{
|
|
2128
|
+
kind: "file",
|
|
2129
|
+
file: {
|
|
2130
|
+
uri: "https://files.test/y.png",
|
|
2131
|
+
mimeType: "image/png",
|
|
2132
|
+
name: "y.png",
|
|
2133
|
+
},
|
|
2134
|
+
},
|
|
2135
|
+
],
|
|
2136
|
+
}),
|
|
2137
|
+
"",
|
|
2138
|
+
"",
|
|
2139
|
+
]),
|
|
2140
|
+
);
|
|
2141
|
+
|
|
2142
|
+
const events: A2AStreamEvent[] = [];
|
|
2143
|
+
for await (const event of client.streamMessage(userMessage)) {
|
|
2144
|
+
events.push(event);
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
const evt = events[0] as Extract<A2AStreamEvent, { type: "message" }>;
|
|
2148
|
+
expect(evt.message.parts[0]).toEqual({
|
|
2149
|
+
url: "https://files.test/y.png",
|
|
2150
|
+
mediaType: "image/png",
|
|
2151
|
+
filename: "y.png",
|
|
2152
|
+
});
|
|
2153
|
+
});
|
|
2154
|
+
|
|
2155
|
+
it("surfaces JSON-RPC error frames instead of an empty stream", async () => {
|
|
2156
|
+
fetchMock.mockResolvedValue(
|
|
2157
|
+
mockSSEResponse([
|
|
2158
|
+
`data: ${JSON.stringify({
|
|
2159
|
+
jsonrpc: "2.0",
|
|
2160
|
+
id: 1,
|
|
2161
|
+
error: { code: -32001, message: "Task not found" },
|
|
2162
|
+
})}`,
|
|
2163
|
+
"",
|
|
2164
|
+
"",
|
|
2165
|
+
]),
|
|
2166
|
+
);
|
|
2167
|
+
|
|
2168
|
+
const read = async () => {
|
|
2169
|
+
for await (const _event of client.streamMessage(userMessage)) {
|
|
2170
|
+
// consume
|
|
2171
|
+
}
|
|
2172
|
+
};
|
|
2173
|
+
|
|
2174
|
+
await expect(read()).rejects.toThrow("Task not found");
|
|
2175
|
+
});
|
|
2176
|
+
|
|
2177
|
+
it("unwraps JSON-RPC envelopes on non-streaming responses", async () => {
|
|
2178
|
+
fetchMock.mockResolvedValue({
|
|
2179
|
+
ok: true,
|
|
2180
|
+
json: async () => ({
|
|
2181
|
+
jsonrpc: "2.0",
|
|
2182
|
+
id: 1,
|
|
2183
|
+
result: {
|
|
2184
|
+
kind: "task",
|
|
2185
|
+
id: "t1",
|
|
2186
|
+
contextId: "ctx-1",
|
|
2187
|
+
status: { state: "working" },
|
|
2188
|
+
},
|
|
2189
|
+
}),
|
|
2190
|
+
});
|
|
2191
|
+
|
|
2192
|
+
const task = await client.getTask("t1");
|
|
2193
|
+
expect(task.id).toBe("t1");
|
|
2194
|
+
expect(task.status.state).toBe("working");
|
|
2195
|
+
expect(task).not.toHaveProperty("kind");
|
|
2196
|
+
});
|
|
2197
|
+
|
|
2198
|
+
it("surfaces JSON-RPC error responses on non-streaming requests", async () => {
|
|
2199
|
+
fetchMock.mockResolvedValue({
|
|
2200
|
+
ok: true,
|
|
2201
|
+
json: async () => ({
|
|
2202
|
+
jsonrpc: "2.0",
|
|
2203
|
+
id: 1,
|
|
2204
|
+
error: {
|
|
2205
|
+
code: -32001,
|
|
2206
|
+
message: "Task not found",
|
|
2207
|
+
data: { taskId: "t1" },
|
|
2208
|
+
},
|
|
2209
|
+
}),
|
|
2210
|
+
});
|
|
2211
|
+
|
|
2212
|
+
const error = await client.getTask("t1").catch((e: unknown) => e);
|
|
2213
|
+
expect(error).toBeInstanceOf(A2AError);
|
|
2214
|
+
const a2aError = error as A2AError;
|
|
2215
|
+
expect(a2aError.message).toBe("Task not found");
|
|
2216
|
+
expect(a2aError.code).toBe(-32001);
|
|
2217
|
+
expect(a2aError.status).toBe("JSONRPC_ERROR");
|
|
2218
|
+
expect(a2aError.details).toEqual([{ taskId: "t1" }]);
|
|
2219
|
+
});
|
|
2220
|
+
|
|
2221
|
+
it("skips malformed kind-discriminated frames instead of emitting empty events", async () => {
|
|
2222
|
+
fetchMock.mockResolvedValue(
|
|
2223
|
+
mockSSEResponse([
|
|
2224
|
+
rpc({ kind: "message" }),
|
|
2225
|
+
"",
|
|
2226
|
+
rpc({ kind: "status-update", status: { state: "working" } }),
|
|
2227
|
+
"",
|
|
2228
|
+
rpc({ kind: "artifact-update", taskId: "t1", artifact: {} }),
|
|
2229
|
+
"",
|
|
2230
|
+
rpc({
|
|
2231
|
+
kind: "status-update",
|
|
2232
|
+
taskId: "t1",
|
|
2233
|
+
contextId: "ctx-1",
|
|
2234
|
+
status: { state: "working" },
|
|
2235
|
+
}),
|
|
2236
|
+
"",
|
|
2237
|
+
"",
|
|
2238
|
+
]),
|
|
2239
|
+
);
|
|
2240
|
+
|
|
2241
|
+
const events: A2AStreamEvent[] = [];
|
|
2242
|
+
for await (const event of client.streamMessage(userMessage)) {
|
|
2243
|
+
events.push(event);
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
expect(events).toHaveLength(1);
|
|
2247
|
+
expect(events[0]!.type).toBe("statusUpdate");
|
|
2248
|
+
});
|
|
2249
|
+
|
|
2250
|
+
it("skips kind-discriminated frames whose ids are not strings", async () => {
|
|
2251
|
+
fetchMock.mockResolvedValue(
|
|
2252
|
+
mockSSEResponse([
|
|
2253
|
+
rpc({
|
|
2254
|
+
kind: "status-update",
|
|
2255
|
+
taskId: "t1",
|
|
2256
|
+
contextId: 999,
|
|
2257
|
+
status: { state: "working" },
|
|
2258
|
+
}),
|
|
2259
|
+
"",
|
|
2260
|
+
rpc({
|
|
2261
|
+
kind: "artifact-update",
|
|
2262
|
+
taskId: "t1",
|
|
2263
|
+
contextId: 999,
|
|
2264
|
+
artifact: {
|
|
2265
|
+
artifactId: "a1",
|
|
2266
|
+
parts: [{ kind: "text", text: "x" }],
|
|
2267
|
+
},
|
|
2268
|
+
}),
|
|
2269
|
+
"",
|
|
2270
|
+
rpc({
|
|
2271
|
+
kind: "task",
|
|
2272
|
+
id: "t1",
|
|
2273
|
+
contextId: 999,
|
|
2274
|
+
status: { state: "working" },
|
|
2275
|
+
}),
|
|
2276
|
+
"",
|
|
2277
|
+
rpc({
|
|
2278
|
+
kind: "message",
|
|
2279
|
+
messageId: "m1",
|
|
2280
|
+
contextId: 999,
|
|
2281
|
+
role: "agent",
|
|
2282
|
+
parts: [{ kind: "text", text: "x" }],
|
|
2283
|
+
}),
|
|
2284
|
+
"",
|
|
2285
|
+
rpc({
|
|
2286
|
+
kind: "message",
|
|
2287
|
+
messageId: "m2",
|
|
2288
|
+
taskId: 999,
|
|
2289
|
+
role: "agent",
|
|
2290
|
+
parts: [{ kind: "text", text: "x" }],
|
|
2291
|
+
}),
|
|
2292
|
+
"",
|
|
2293
|
+
rpc({
|
|
2294
|
+
kind: "artifact-update",
|
|
2295
|
+
taskId: 999,
|
|
2296
|
+
artifact: {
|
|
2297
|
+
artifactId: "a1",
|
|
2298
|
+
parts: [{ kind: "text", text: "x" }],
|
|
2299
|
+
},
|
|
2300
|
+
}),
|
|
2301
|
+
"",
|
|
2302
|
+
rpc({
|
|
2303
|
+
kind: "status-update",
|
|
2304
|
+
taskId: "t1",
|
|
2305
|
+
contextId: "ctx-1",
|
|
2306
|
+
status: { state: "working" },
|
|
2307
|
+
}),
|
|
2308
|
+
"",
|
|
2309
|
+
"",
|
|
2310
|
+
]),
|
|
2311
|
+
);
|
|
2312
|
+
|
|
2313
|
+
const events: A2AStreamEvent[] = [];
|
|
2314
|
+
for await (const event of client.streamMessage(userMessage)) {
|
|
2315
|
+
events.push(event);
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
expect(events).toHaveLength(1);
|
|
2319
|
+
expect(events[0]!.type).toBe("statusUpdate");
|
|
2320
|
+
});
|
|
2321
|
+
|
|
2322
|
+
it("accepts kind-discriminated frames that omit contextId", async () => {
|
|
2323
|
+
fetchMock.mockResolvedValue(
|
|
2324
|
+
mockSSEResponse([
|
|
2325
|
+
rpc({
|
|
2326
|
+
kind: "status-update",
|
|
2327
|
+
taskId: "t1",
|
|
2328
|
+
status: { state: "working" },
|
|
2329
|
+
}),
|
|
2330
|
+
"",
|
|
2331
|
+
rpc({
|
|
2332
|
+
kind: "artifact-update",
|
|
2333
|
+
taskId: "t1",
|
|
2334
|
+
artifact: {
|
|
2335
|
+
artifactId: "a1",
|
|
2336
|
+
parts: [{ kind: "text", text: "x" }],
|
|
2337
|
+
},
|
|
2338
|
+
}),
|
|
2339
|
+
"",
|
|
2340
|
+
rpc({ kind: "task", id: "t1", status: { state: "working" } }),
|
|
2341
|
+
"",
|
|
2342
|
+
rpc({
|
|
2343
|
+
kind: "message",
|
|
2344
|
+
messageId: "m1",
|
|
2345
|
+
role: "agent",
|
|
2346
|
+
parts: [{ kind: "text", text: "x" }],
|
|
2347
|
+
}),
|
|
2348
|
+
"",
|
|
2349
|
+
"",
|
|
2350
|
+
]),
|
|
2351
|
+
);
|
|
2352
|
+
|
|
2353
|
+
const events: A2AStreamEvent[] = [];
|
|
2354
|
+
for await (const event of client.streamMessage(userMessage)) {
|
|
2355
|
+
events.push(event);
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
expect(events.map((event) => event.type)).toEqual([
|
|
2359
|
+
"statusUpdate",
|
|
2360
|
+
"artifactUpdate",
|
|
2361
|
+
"task",
|
|
2362
|
+
"message",
|
|
2363
|
+
]);
|
|
2364
|
+
});
|
|
2365
|
+
|
|
2366
|
+
it("parses flat message and artifact-update results", async () => {
|
|
2367
|
+
fetchMock.mockResolvedValue(
|
|
2368
|
+
mockSSEResponse([
|
|
2369
|
+
rpc({
|
|
2370
|
+
kind: "message",
|
|
2371
|
+
messageId: "m1",
|
|
2372
|
+
role: "agent",
|
|
2373
|
+
parts: [{ kind: "text", text: "hello" }],
|
|
2374
|
+
}),
|
|
2375
|
+
"",
|
|
2376
|
+
rpc({
|
|
2377
|
+
kind: "artifact-update",
|
|
2378
|
+
taskId: "t1",
|
|
2379
|
+
contextId: "ctx-1",
|
|
2380
|
+
artifact: {
|
|
2381
|
+
artifactId: "a1",
|
|
2382
|
+
parts: [{ kind: "text", text: "x" }],
|
|
2383
|
+
},
|
|
2384
|
+
}),
|
|
2385
|
+
"",
|
|
2386
|
+
"",
|
|
2387
|
+
]),
|
|
2388
|
+
);
|
|
2389
|
+
|
|
2390
|
+
const events: A2AStreamEvent[] = [];
|
|
2391
|
+
for await (const event of client.streamMessage(userMessage)) {
|
|
2392
|
+
events.push(event);
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
expect(events.map((e) => e.type)).toEqual(["message", "artifactUpdate"]);
|
|
2396
|
+
});
|
|
2397
|
+
});
|
|
1684
2398
|
});
|