@assistant-ui/react-a2a 0.2.36 → 0.2.38
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
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
applyA2uiOperations,
|
|
4
|
+
convertSurfaceToUISpec,
|
|
5
|
+
} from "@assistant-ui/react-generative-ui/a2ui";
|
|
2
6
|
import { A2AThreadRuntimeCore } from "./A2AThreadRuntimeCore";
|
|
3
7
|
import type { A2AClient } from "./A2AClient";
|
|
4
8
|
import type {
|
|
5
9
|
A2AAgentCard,
|
|
6
10
|
A2AMessage,
|
|
11
|
+
A2APart,
|
|
7
12
|
A2AStreamEvent,
|
|
8
13
|
A2ATask,
|
|
9
14
|
} from "./types";
|
|
@@ -100,7 +105,11 @@ function createBranchedHistory() {
|
|
|
100
105
|
return { user, firstAssistant, secondAssistant, history };
|
|
101
106
|
}
|
|
102
107
|
|
|
103
|
-
function statusUpdateEvent(
|
|
108
|
+
function statusUpdateEvent(
|
|
109
|
+
state: string,
|
|
110
|
+
text?: string,
|
|
111
|
+
parts?: A2APart[],
|
|
112
|
+
): A2AStreamEvent {
|
|
104
113
|
return {
|
|
105
114
|
type: "statusUpdate",
|
|
106
115
|
event: {
|
|
@@ -108,13 +117,15 @@ function statusUpdateEvent(state: string, text?: string): A2AStreamEvent {
|
|
|
108
117
|
contextId: "ctx-1",
|
|
109
118
|
status: {
|
|
110
119
|
state: state as any,
|
|
111
|
-
...(text
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
...(text !== undefined || parts !== undefined
|
|
121
|
+
? {
|
|
122
|
+
message: {
|
|
123
|
+
messageId: "s1",
|
|
124
|
+
role: "agent" as const,
|
|
125
|
+
parts: parts ?? [{ text }],
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
: {}),
|
|
118
129
|
},
|
|
119
130
|
},
|
|
120
131
|
};
|
|
@@ -122,7 +133,7 @@ function statusUpdateEvent(state: string, text?: string): A2AStreamEvent {
|
|
|
122
133
|
|
|
123
134
|
function artifactUpdateEvent(
|
|
124
135
|
artifactId: string,
|
|
125
|
-
parts:
|
|
136
|
+
parts: A2APart[],
|
|
126
137
|
opts: { append?: boolean; lastChunk?: boolean } = {},
|
|
127
138
|
): A2AStreamEvent {
|
|
128
139
|
return {
|
|
@@ -137,6 +148,12 @@ function artifactUpdateEvent(
|
|
|
137
148
|
};
|
|
138
149
|
}
|
|
139
150
|
|
|
151
|
+
const toolInteraction = {
|
|
152
|
+
type: "action" as const,
|
|
153
|
+
occurredAt: 1_700_000_000_000,
|
|
154
|
+
payload: { value: "selected" },
|
|
155
|
+
};
|
|
156
|
+
|
|
140
157
|
describe("A2AThreadRuntimeCore", () => {
|
|
141
158
|
let notifyUpdate: ReturnType<typeof vi.fn>;
|
|
142
159
|
|
|
@@ -273,6 +290,35 @@ describe("A2AThreadRuntimeCore", () => {
|
|
|
273
290
|
});
|
|
274
291
|
|
|
275
292
|
describe("history loading", () => {
|
|
293
|
+
it("loads persisted history without waiting for agent-card discovery", async () => {
|
|
294
|
+
const restored = createHistoryMessage(
|
|
295
|
+
"restored",
|
|
296
|
+
"user",
|
|
297
|
+
"Saved history",
|
|
298
|
+
);
|
|
299
|
+
const history = {
|
|
300
|
+
load: vi.fn().mockResolvedValue({
|
|
301
|
+
headId: restored.id,
|
|
302
|
+
messages: [{ parentId: null, message: restored }],
|
|
303
|
+
}),
|
|
304
|
+
append: vi.fn().mockResolvedValue(undefined),
|
|
305
|
+
};
|
|
306
|
+
const getAgentCard = vi.fn(() => new Promise<A2AAgentCard>(() => {}));
|
|
307
|
+
const core = createCore({ getAgentCard }, { history });
|
|
308
|
+
|
|
309
|
+
const result = await Promise.race([
|
|
310
|
+
core.__internal_load().then(() => "loaded"),
|
|
311
|
+
new Promise<"timed out">((resolve) => {
|
|
312
|
+
setTimeout(() => resolve("timed out"), 0);
|
|
313
|
+
}),
|
|
314
|
+
]);
|
|
315
|
+
|
|
316
|
+
expect(result).toBe("loaded");
|
|
317
|
+
expect(getAgentCard).toHaveBeenCalledOnce();
|
|
318
|
+
expect(core.getMessages()).toEqual([restored]);
|
|
319
|
+
expect(core.isLoading).toBe(false);
|
|
320
|
+
});
|
|
321
|
+
|
|
276
322
|
it("keeps initial history loading across same-thread message resyncs", async () => {
|
|
277
323
|
let resolve!: (repo: ExportedMessageRepository) => void;
|
|
278
324
|
const pending = new Promise<ExportedMessageRepository>((res) => {
|
|
@@ -907,6 +953,476 @@ describe("A2AThreadRuntimeCore", () => {
|
|
|
907
953
|
expect(wasRunningDuringStream).toBe(true);
|
|
908
954
|
expect(core.isRunning()).toBe(false);
|
|
909
955
|
});
|
|
956
|
+
|
|
957
|
+
it("rebuilds an A2UI surface across status updates with replayable operations", async () => {
|
|
958
|
+
const core = createCore({
|
|
959
|
+
streamMessage: vi.fn().mockImplementation(async function* () {
|
|
960
|
+
yield statusUpdateEvent("working", undefined, [
|
|
961
|
+
{
|
|
962
|
+
data: [
|
|
963
|
+
{
|
|
964
|
+
version: "v0.9",
|
|
965
|
+
createSurface: { surfaceId: "summary" },
|
|
966
|
+
},
|
|
967
|
+
],
|
|
968
|
+
},
|
|
969
|
+
]);
|
|
970
|
+
yield statusUpdateEvent("completed", undefined, [
|
|
971
|
+
{
|
|
972
|
+
data: [
|
|
973
|
+
{
|
|
974
|
+
version: "v0.9",
|
|
975
|
+
updateComponents: {
|
|
976
|
+
surfaceId: "summary",
|
|
977
|
+
components: [
|
|
978
|
+
{
|
|
979
|
+
id: "root",
|
|
980
|
+
component: "Text",
|
|
981
|
+
text: { path: "/summary" },
|
|
982
|
+
},
|
|
983
|
+
],
|
|
984
|
+
},
|
|
985
|
+
},
|
|
986
|
+
{
|
|
987
|
+
version: "v0.9",
|
|
988
|
+
updateDataModel: {
|
|
989
|
+
surfaceId: "summary",
|
|
990
|
+
contents: { summary: "Ready" },
|
|
991
|
+
},
|
|
992
|
+
},
|
|
993
|
+
],
|
|
994
|
+
},
|
|
995
|
+
]);
|
|
996
|
+
}),
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
await core.append(createUserAppendMessage("Go"));
|
|
1000
|
+
|
|
1001
|
+
const assistant = core.getMessages()[1]!;
|
|
1002
|
+
expect(assistant.content).toHaveLength(1);
|
|
1003
|
+
const part = assistant.content[0]!;
|
|
1004
|
+
if (part.type !== "tool-call") throw new Error("expected A2UI tool call");
|
|
1005
|
+
expect(part).toMatchObject({
|
|
1006
|
+
toolCallId: "a2ui:summary",
|
|
1007
|
+
toolName: "present",
|
|
1008
|
+
result: {},
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
const artifact = part.artifact as { a2ui: unknown };
|
|
1012
|
+
const replayed = applyA2uiOperations(new Map(), artifact.a2ui);
|
|
1013
|
+
const surface = replayed.state.get("summary");
|
|
1014
|
+
expect(replayed.warnings).toEqual([]);
|
|
1015
|
+
expect(surface).toBeDefined();
|
|
1016
|
+
expect(convertSurfaceToUISpec(surface!).spec).toEqual(part.args);
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
it("keeps a recorded A2UI interaction through later stream updates and writes it when settled", async () => {
|
|
1020
|
+
let release!: () => void;
|
|
1021
|
+
const continueStream = new Promise<void>((resolve) => {
|
|
1022
|
+
release = resolve;
|
|
1023
|
+
});
|
|
1024
|
+
const history = {
|
|
1025
|
+
load: vi.fn().mockResolvedValue({ messages: [] }),
|
|
1026
|
+
append: vi.fn().mockResolvedValue(undefined),
|
|
1027
|
+
};
|
|
1028
|
+
const core = createCore(
|
|
1029
|
+
{
|
|
1030
|
+
streamMessage: vi.fn().mockImplementation(async function* () {
|
|
1031
|
+
yield statusUpdateEvent("working", undefined, [
|
|
1032
|
+
{
|
|
1033
|
+
data: [
|
|
1034
|
+
{
|
|
1035
|
+
version: "v0.9",
|
|
1036
|
+
createSurface: { surfaceId: "summary" },
|
|
1037
|
+
},
|
|
1038
|
+
{
|
|
1039
|
+
version: "v0.9",
|
|
1040
|
+
updateComponents: {
|
|
1041
|
+
surfaceId: "summary",
|
|
1042
|
+
components: [
|
|
1043
|
+
{
|
|
1044
|
+
id: "root",
|
|
1045
|
+
component: "Text",
|
|
1046
|
+
text: "Ready",
|
|
1047
|
+
},
|
|
1048
|
+
],
|
|
1049
|
+
},
|
|
1050
|
+
},
|
|
1051
|
+
],
|
|
1052
|
+
},
|
|
1053
|
+
]);
|
|
1054
|
+
await continueStream;
|
|
1055
|
+
yield statusUpdateEvent("working", "Status update");
|
|
1056
|
+
yield artifactUpdateEvent("a1", [
|
|
1057
|
+
{
|
|
1058
|
+
data: [
|
|
1059
|
+
{
|
|
1060
|
+
version: "v0.9",
|
|
1061
|
+
updateDataModel: {
|
|
1062
|
+
surfaceId: "summary",
|
|
1063
|
+
contents: { summary: "Updated" },
|
|
1064
|
+
},
|
|
1065
|
+
},
|
|
1066
|
+
],
|
|
1067
|
+
},
|
|
1068
|
+
]);
|
|
1069
|
+
yield statusUpdateEvent("completed", "Done");
|
|
1070
|
+
}),
|
|
1071
|
+
},
|
|
1072
|
+
{ history },
|
|
1073
|
+
);
|
|
1074
|
+
|
|
1075
|
+
const run = core.append(createUserAppendMessage("Go"));
|
|
1076
|
+
await vi.waitFor(() => {
|
|
1077
|
+
const part = core.getMessages()[1]?.content[0];
|
|
1078
|
+
expect(part).toMatchObject({
|
|
1079
|
+
type: "tool-call",
|
|
1080
|
+
toolCallId: "a2ui:summary",
|
|
1081
|
+
});
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
const assistantId = core.getMessages()[1]!.id;
|
|
1085
|
+
await core.recordToolInteraction({
|
|
1086
|
+
messageId: assistantId,
|
|
1087
|
+
toolCallId: "a2ui:summary",
|
|
1088
|
+
interaction: toolInteraction,
|
|
1089
|
+
});
|
|
1090
|
+
expect(core.getMessages()[1]!.content[0]).toMatchObject({
|
|
1091
|
+
unstable_interactions: { entries: [toolInteraction] },
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
release();
|
|
1095
|
+
await run;
|
|
1096
|
+
|
|
1097
|
+
await vi.waitFor(() => expect(history.append).toHaveBeenCalledTimes(2));
|
|
1098
|
+
const persisted = history.append.mock.calls[1]![0].message;
|
|
1099
|
+
const part = persisted.content.find(
|
|
1100
|
+
(content: { type: string; toolCallId?: string }) =>
|
|
1101
|
+
content.type === "tool-call" && content.toolCallId === "a2ui:summary",
|
|
1102
|
+
);
|
|
1103
|
+
expect(part).toMatchObject({
|
|
1104
|
+
unstable_interactions: { entries: [toolInteraction] },
|
|
1105
|
+
});
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
it("rebuilds A2UI state from a task snapshot in full-state order", async () => {
|
|
1109
|
+
const core = createCore({
|
|
1110
|
+
streamMessage: vi.fn().mockImplementation(async function* () {
|
|
1111
|
+
yield statusUpdateEvent("working", undefined, [
|
|
1112
|
+
{
|
|
1113
|
+
data: [
|
|
1114
|
+
{
|
|
1115
|
+
version: "v0.9",
|
|
1116
|
+
createSurface: { surfaceId: "obsolete" },
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
version: "v0.9",
|
|
1120
|
+
updateComponents: {
|
|
1121
|
+
surfaceId: "obsolete",
|
|
1122
|
+
components: [
|
|
1123
|
+
{ id: "root", component: "Text", text: "Obsolete" },
|
|
1124
|
+
],
|
|
1125
|
+
},
|
|
1126
|
+
},
|
|
1127
|
+
],
|
|
1128
|
+
},
|
|
1129
|
+
]);
|
|
1130
|
+
yield {
|
|
1131
|
+
type: "artifactUpdate",
|
|
1132
|
+
event: {
|
|
1133
|
+
taskId: "t1",
|
|
1134
|
+
contextId: "ctx-1",
|
|
1135
|
+
artifact: {
|
|
1136
|
+
artifactId: "a1",
|
|
1137
|
+
parts: [
|
|
1138
|
+
{
|
|
1139
|
+
data: [
|
|
1140
|
+
{
|
|
1141
|
+
version: "v0.9",
|
|
1142
|
+
updateDataModel: {
|
|
1143
|
+
surfaceId: "obsolete",
|
|
1144
|
+
contents: { summary: "Stale artifact event" },
|
|
1145
|
+
},
|
|
1146
|
+
},
|
|
1147
|
+
],
|
|
1148
|
+
},
|
|
1149
|
+
],
|
|
1150
|
+
},
|
|
1151
|
+
},
|
|
1152
|
+
} satisfies A2AStreamEvent;
|
|
1153
|
+
yield {
|
|
1154
|
+
type: "task",
|
|
1155
|
+
task: {
|
|
1156
|
+
id: "t1",
|
|
1157
|
+
contextId: "ctx-1",
|
|
1158
|
+
status: {
|
|
1159
|
+
state: "completed",
|
|
1160
|
+
message: {
|
|
1161
|
+
messageId: "s1",
|
|
1162
|
+
role: "agent",
|
|
1163
|
+
parts: [
|
|
1164
|
+
{
|
|
1165
|
+
data: [
|
|
1166
|
+
{
|
|
1167
|
+
version: "v0.9",
|
|
1168
|
+
updateDataModel: {
|
|
1169
|
+
surfaceId: "summary",
|
|
1170
|
+
path: "/summary",
|
|
1171
|
+
contents: "From task status",
|
|
1172
|
+
},
|
|
1173
|
+
},
|
|
1174
|
+
],
|
|
1175
|
+
},
|
|
1176
|
+
],
|
|
1177
|
+
},
|
|
1178
|
+
},
|
|
1179
|
+
history: [
|
|
1180
|
+
{
|
|
1181
|
+
messageId: "h1",
|
|
1182
|
+
role: "agent",
|
|
1183
|
+
parts: [
|
|
1184
|
+
{
|
|
1185
|
+
data: [
|
|
1186
|
+
{
|
|
1187
|
+
version: "v0.9",
|
|
1188
|
+
createSurface: { surfaceId: "summary" },
|
|
1189
|
+
},
|
|
1190
|
+
{
|
|
1191
|
+
version: "v0.9",
|
|
1192
|
+
updateComponents: {
|
|
1193
|
+
surfaceId: "summary",
|
|
1194
|
+
components: [
|
|
1195
|
+
{
|
|
1196
|
+
id: "root",
|
|
1197
|
+
component: "Text",
|
|
1198
|
+
text: { path: "/summary" },
|
|
1199
|
+
},
|
|
1200
|
+
],
|
|
1201
|
+
},
|
|
1202
|
+
},
|
|
1203
|
+
],
|
|
1204
|
+
},
|
|
1205
|
+
],
|
|
1206
|
+
},
|
|
1207
|
+
],
|
|
1208
|
+
artifacts: [
|
|
1209
|
+
{
|
|
1210
|
+
artifactId: "a1",
|
|
1211
|
+
parts: [
|
|
1212
|
+
{
|
|
1213
|
+
data: [
|
|
1214
|
+
{
|
|
1215
|
+
version: "v0.9",
|
|
1216
|
+
updateDataModel: {
|
|
1217
|
+
surfaceId: "summary",
|
|
1218
|
+
path: "/summary",
|
|
1219
|
+
contents: "From task artifact",
|
|
1220
|
+
},
|
|
1221
|
+
},
|
|
1222
|
+
],
|
|
1223
|
+
},
|
|
1224
|
+
],
|
|
1225
|
+
},
|
|
1226
|
+
],
|
|
1227
|
+
},
|
|
1228
|
+
} satisfies A2AStreamEvent;
|
|
1229
|
+
}),
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
await core.append(createUserAppendMessage("Go"));
|
|
1233
|
+
|
|
1234
|
+
expect(core.getMessages()[1]!.content).toHaveLength(1);
|
|
1235
|
+
const part = core.getMessages()[1]!.content[0]!;
|
|
1236
|
+
if (part.type !== "tool-call") throw new Error("expected A2UI tool call");
|
|
1237
|
+
const replayed = applyA2uiOperations(
|
|
1238
|
+
new Map(),
|
|
1239
|
+
(part.artifact as { a2ui: unknown }).a2ui,
|
|
1240
|
+
);
|
|
1241
|
+
expect(replayed.state.get("summary")?.dataModel).toEqual({
|
|
1242
|
+
summary: "From task status",
|
|
1243
|
+
});
|
|
1244
|
+
expect(replayed.state.has("obsolete")).toBe(false);
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
it("keeps an A2UI surface when a partial task snapshot repeats an artifact", async () => {
|
|
1248
|
+
const core = createCore({
|
|
1249
|
+
streamMessage: vi.fn().mockImplementation(async function* () {
|
|
1250
|
+
yield artifactUpdateEvent("a1", [
|
|
1251
|
+
{
|
|
1252
|
+
data: [
|
|
1253
|
+
{
|
|
1254
|
+
version: "v0.9",
|
|
1255
|
+
createSurface: { surfaceId: "summary" },
|
|
1256
|
+
},
|
|
1257
|
+
],
|
|
1258
|
+
},
|
|
1259
|
+
]);
|
|
1260
|
+
yield statusUpdateEvent("working", undefined, [
|
|
1261
|
+
{
|
|
1262
|
+
data: [
|
|
1263
|
+
{
|
|
1264
|
+
version: "v0.9",
|
|
1265
|
+
updateComponents: {
|
|
1266
|
+
surfaceId: "summary",
|
|
1267
|
+
components: [
|
|
1268
|
+
{ id: "root", component: "Text", text: "Ready" },
|
|
1269
|
+
],
|
|
1270
|
+
},
|
|
1271
|
+
},
|
|
1272
|
+
{
|
|
1273
|
+
version: "v0.9",
|
|
1274
|
+
updateDataModel: {
|
|
1275
|
+
surfaceId: "summary",
|
|
1276
|
+
contents: { summary: "Ready" },
|
|
1277
|
+
},
|
|
1278
|
+
},
|
|
1279
|
+
],
|
|
1280
|
+
},
|
|
1281
|
+
]);
|
|
1282
|
+
yield {
|
|
1283
|
+
type: "task",
|
|
1284
|
+
task: {
|
|
1285
|
+
id: "t1",
|
|
1286
|
+
contextId: "ctx-1",
|
|
1287
|
+
status: { state: "completed" },
|
|
1288
|
+
artifacts: [
|
|
1289
|
+
{
|
|
1290
|
+
artifactId: "a1",
|
|
1291
|
+
parts: [
|
|
1292
|
+
{
|
|
1293
|
+
data: [
|
|
1294
|
+
{
|
|
1295
|
+
version: "v0.9",
|
|
1296
|
+
createSurface: { surfaceId: "summary" },
|
|
1297
|
+
},
|
|
1298
|
+
],
|
|
1299
|
+
},
|
|
1300
|
+
],
|
|
1301
|
+
},
|
|
1302
|
+
],
|
|
1303
|
+
},
|
|
1304
|
+
} satisfies A2AStreamEvent;
|
|
1305
|
+
}),
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
await core.append(createUserAppendMessage("Go"));
|
|
1309
|
+
|
|
1310
|
+
expect(core.getMessages()[1]!.content).toHaveLength(1);
|
|
1311
|
+
const part = core.getMessages()[1]!.content[0]!;
|
|
1312
|
+
if (part.type !== "tool-call") throw new Error("expected A2UI tool call");
|
|
1313
|
+
const replayed = applyA2uiOperations(
|
|
1314
|
+
new Map(),
|
|
1315
|
+
(part.artifact as { a2ui: unknown }).a2ui,
|
|
1316
|
+
);
|
|
1317
|
+
expect(replayed.state.get("summary")?.dataModel).toEqual({
|
|
1318
|
+
summary: "Ready",
|
|
1319
|
+
});
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
it("does not replay A2UI history already applied when a partial task snapshot carries history", async () => {
|
|
1323
|
+
const created = {
|
|
1324
|
+
messageId: "agent-1",
|
|
1325
|
+
role: "agent" as const,
|
|
1326
|
+
parts: [
|
|
1327
|
+
{
|
|
1328
|
+
data: [
|
|
1329
|
+
{ version: "v0.9", createSurface: { surfaceId: "summary" } },
|
|
1330
|
+
],
|
|
1331
|
+
},
|
|
1332
|
+
],
|
|
1333
|
+
};
|
|
1334
|
+
const core = createCore({
|
|
1335
|
+
streamMessage: vi.fn().mockImplementation(async function* () {
|
|
1336
|
+
yield { type: "message", message: created } satisfies A2AStreamEvent;
|
|
1337
|
+
yield statusUpdateEvent("working", undefined, [
|
|
1338
|
+
{
|
|
1339
|
+
data: [
|
|
1340
|
+
{
|
|
1341
|
+
version: "v0.9",
|
|
1342
|
+
updateComponents: {
|
|
1343
|
+
surfaceId: "summary",
|
|
1344
|
+
components: [
|
|
1345
|
+
{ id: "root", component: "Text", text: "Ready" },
|
|
1346
|
+
],
|
|
1347
|
+
},
|
|
1348
|
+
},
|
|
1349
|
+
{
|
|
1350
|
+
version: "v0.9",
|
|
1351
|
+
updateDataModel: {
|
|
1352
|
+
surfaceId: "summary",
|
|
1353
|
+
contents: { summary: "Ready" },
|
|
1354
|
+
},
|
|
1355
|
+
},
|
|
1356
|
+
],
|
|
1357
|
+
},
|
|
1358
|
+
]);
|
|
1359
|
+
yield {
|
|
1360
|
+
type: "task",
|
|
1361
|
+
task: {
|
|
1362
|
+
id: "t1",
|
|
1363
|
+
contextId: "ctx-1",
|
|
1364
|
+
status: { state: "completed" },
|
|
1365
|
+
history: [created],
|
|
1366
|
+
},
|
|
1367
|
+
} satisfies A2AStreamEvent;
|
|
1368
|
+
}),
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
await core.append(createUserAppendMessage("Go"));
|
|
1372
|
+
|
|
1373
|
+
const part = core
|
|
1374
|
+
.getMessages()[1]!
|
|
1375
|
+
.content.find((candidate) => candidate.type === "tool-call");
|
|
1376
|
+
if (part?.type !== "tool-call")
|
|
1377
|
+
throw new Error("expected A2UI tool call");
|
|
1378
|
+
const replayed = applyA2uiOperations(
|
|
1379
|
+
new Map(),
|
|
1380
|
+
(part.artifact as { a2ui: unknown }).a2ui,
|
|
1381
|
+
);
|
|
1382
|
+
expect(replayed.state.get("summary")?.dataModel).toEqual({
|
|
1383
|
+
summary: "Ready",
|
|
1384
|
+
});
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
it("removes a deleted A2UI surface", async () => {
|
|
1388
|
+
const core = createCore({
|
|
1389
|
+
streamMessage: vi.fn().mockImplementation(async function* () {
|
|
1390
|
+
yield statusUpdateEvent("working", undefined, [
|
|
1391
|
+
{
|
|
1392
|
+
data: [
|
|
1393
|
+
{
|
|
1394
|
+
version: "v0.9",
|
|
1395
|
+
createSurface: { surfaceId: "summary" },
|
|
1396
|
+
},
|
|
1397
|
+
{
|
|
1398
|
+
version: "v0.9",
|
|
1399
|
+
updateComponents: {
|
|
1400
|
+
surfaceId: "summary",
|
|
1401
|
+
components: [
|
|
1402
|
+
{ id: "root", component: "Text", text: "Ready" },
|
|
1403
|
+
],
|
|
1404
|
+
},
|
|
1405
|
+
},
|
|
1406
|
+
],
|
|
1407
|
+
},
|
|
1408
|
+
]);
|
|
1409
|
+
yield statusUpdateEvent("completed", undefined, [
|
|
1410
|
+
{
|
|
1411
|
+
data: [
|
|
1412
|
+
{
|
|
1413
|
+
version: "v0.9",
|
|
1414
|
+
deleteSurface: { surfaceId: "summary" },
|
|
1415
|
+
},
|
|
1416
|
+
],
|
|
1417
|
+
},
|
|
1418
|
+
]);
|
|
1419
|
+
}),
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
await core.append(createUserAppendMessage("Go"));
|
|
1423
|
+
|
|
1424
|
+
expect(core.getMessages()[1]!.content).toEqual([]);
|
|
1425
|
+
});
|
|
910
1426
|
});
|
|
911
1427
|
|
|
912
1428
|
// --- Sync (non-streaming) fallback ---
|
|
@@ -1176,6 +1692,62 @@ describe("A2AThreadRuntimeCore", () => {
|
|
|
1176
1692
|
expect(artifacts[0]!.parts[1]!.text).toBe("part2");
|
|
1177
1693
|
});
|
|
1178
1694
|
|
|
1695
|
+
it("stores merged task artifacts on the assistant message", async () => {
|
|
1696
|
+
const core = createCore({
|
|
1697
|
+
streamMessage: vi.fn().mockImplementation(async function* () {
|
|
1698
|
+
yield artifactUpdateEvent("a1", [{ text: "part1" }]);
|
|
1699
|
+
yield artifactUpdateEvent("a1", [{ text: "part2" }], {
|
|
1700
|
+
append: true,
|
|
1701
|
+
});
|
|
1702
|
+
yield statusUpdateEvent("completed", "Done");
|
|
1703
|
+
}),
|
|
1704
|
+
});
|
|
1705
|
+
|
|
1706
|
+
await core.append(createUserAppendMessage("Go"));
|
|
1707
|
+
|
|
1708
|
+
expect(core.getMessages()[1]!.metadata.custom.a2a).toEqual({
|
|
1709
|
+
artifacts: [
|
|
1710
|
+
{
|
|
1711
|
+
artifactId: "a1",
|
|
1712
|
+
name: "a1",
|
|
1713
|
+
parts: [{ text: "part1" }, { text: "part2" }],
|
|
1714
|
+
},
|
|
1715
|
+
],
|
|
1716
|
+
});
|
|
1717
|
+
});
|
|
1718
|
+
|
|
1719
|
+
it("stores artifact files on the assistant message without their inline bytes", async () => {
|
|
1720
|
+
const core = createCore({
|
|
1721
|
+
streamMessage: vi.fn().mockImplementation(async function* () {
|
|
1722
|
+
yield artifactUpdateEvent("a1", [
|
|
1723
|
+
{
|
|
1724
|
+
raw: "JVBERi0xLjQK",
|
|
1725
|
+
mediaType: "application/pdf",
|
|
1726
|
+
filename: "report.pdf",
|
|
1727
|
+
},
|
|
1728
|
+
{ url: "https://example.com/chart.png", mediaType: "image/png" },
|
|
1729
|
+
]);
|
|
1730
|
+
yield statusUpdateEvent("completed", "Done");
|
|
1731
|
+
}),
|
|
1732
|
+
});
|
|
1733
|
+
|
|
1734
|
+
await core.append(createUserAppendMessage("Go"));
|
|
1735
|
+
|
|
1736
|
+
expect(core.getArtifacts()[0]!.parts[0]!.raw).toBe("JVBERi0xLjQK");
|
|
1737
|
+
expect(core.getMessages()[1]!.metadata.custom.a2a).toEqual({
|
|
1738
|
+
artifacts: [
|
|
1739
|
+
{
|
|
1740
|
+
artifactId: "a1",
|
|
1741
|
+
name: "a1",
|
|
1742
|
+
parts: [
|
|
1743
|
+
{ mediaType: "application/pdf", filename: "report.pdf" },
|
|
1744
|
+
{ url: "https://example.com/chart.png", mediaType: "image/png" },
|
|
1745
|
+
],
|
|
1746
|
+
},
|
|
1747
|
+
],
|
|
1748
|
+
});
|
|
1749
|
+
});
|
|
1750
|
+
|
|
1179
1751
|
it("replaces artifact when append=false", async () => {
|
|
1180
1752
|
const core = createCore({
|
|
1181
1753
|
streamMessage: vi.fn().mockImplementation(async function* () {
|
|
@@ -1331,6 +1903,312 @@ describe("A2AThreadRuntimeCore", () => {
|
|
|
1331
1903
|
const secondCallMsg = streamMessage.mock.calls[1]![0] as A2AMessage;
|
|
1332
1904
|
expect(secondCallMsg.taskId).toBe("t1");
|
|
1333
1905
|
});
|
|
1906
|
+
|
|
1907
|
+
it("persists a paused assistant message and updates it when it settles", async () => {
|
|
1908
|
+
let release!: () => void;
|
|
1909
|
+
const settled = new Promise<void>((resolve) => {
|
|
1910
|
+
release = resolve;
|
|
1911
|
+
});
|
|
1912
|
+
const stored = new Map<string, ThreadMessage>();
|
|
1913
|
+
const history = {
|
|
1914
|
+
load: vi.fn().mockResolvedValue({ messages: [] }),
|
|
1915
|
+
append: vi.fn(async (item: { message: ThreadMessage }) => {
|
|
1916
|
+
stored.set(item.message.id, item.message);
|
|
1917
|
+
}),
|
|
1918
|
+
update: vi.fn(async (item: { message: ThreadMessage }) => {
|
|
1919
|
+
stored.set(item.message.id, item.message);
|
|
1920
|
+
}),
|
|
1921
|
+
};
|
|
1922
|
+
const core = createCore(
|
|
1923
|
+
{
|
|
1924
|
+
streamMessage: vi.fn().mockImplementation(async function* () {
|
|
1925
|
+
yield statusUpdateEvent("input_required", "What should I do?");
|
|
1926
|
+
await settled;
|
|
1927
|
+
yield statusUpdateEvent("completed", "Done");
|
|
1928
|
+
}),
|
|
1929
|
+
},
|
|
1930
|
+
{ history },
|
|
1931
|
+
);
|
|
1932
|
+
|
|
1933
|
+
const run = core.append(createUserAppendMessage("Start"));
|
|
1934
|
+
|
|
1935
|
+
await vi.waitFor(() => expect(history.append).toHaveBeenCalledTimes(2));
|
|
1936
|
+
const paused = history.append.mock.calls[1]![0].message;
|
|
1937
|
+
expect(paused.status).toEqual({
|
|
1938
|
+
type: "requires-action",
|
|
1939
|
+
reason: "interrupt",
|
|
1940
|
+
});
|
|
1941
|
+
expect(paused.content).toEqual([
|
|
1942
|
+
{ type: "text", text: "What should I do?" },
|
|
1943
|
+
]);
|
|
1944
|
+
|
|
1945
|
+
release();
|
|
1946
|
+
await run;
|
|
1947
|
+
|
|
1948
|
+
await vi.waitFor(() => expect(history.update).toHaveBeenCalledOnce());
|
|
1949
|
+
expect(stored.get(paused.id)).toMatchObject({
|
|
1950
|
+
content: [{ type: "text", text: "Done" }],
|
|
1951
|
+
status: { type: "complete", reason: "stop" },
|
|
1952
|
+
});
|
|
1953
|
+
});
|
|
1954
|
+
|
|
1955
|
+
it("appends only the settled assistant message without history updates", async () => {
|
|
1956
|
+
let release!: () => void;
|
|
1957
|
+
const settled = new Promise<void>((resolve) => {
|
|
1958
|
+
release = resolve;
|
|
1959
|
+
});
|
|
1960
|
+
const stored = new Map<string, ThreadMessage>();
|
|
1961
|
+
const history = {
|
|
1962
|
+
load: vi.fn().mockResolvedValue({ messages: [] }),
|
|
1963
|
+
append: vi.fn(async (item: { message: ThreadMessage }) => {
|
|
1964
|
+
stored.set(item.message.id, item.message);
|
|
1965
|
+
}),
|
|
1966
|
+
};
|
|
1967
|
+
const core = createCore(
|
|
1968
|
+
{
|
|
1969
|
+
streamMessage: vi.fn().mockImplementation(async function* () {
|
|
1970
|
+
yield statusUpdateEvent("input_required", "What should I do?");
|
|
1971
|
+
await settled;
|
|
1972
|
+
yield statusUpdateEvent("completed", "Done");
|
|
1973
|
+
}),
|
|
1974
|
+
},
|
|
1975
|
+
{ history },
|
|
1976
|
+
);
|
|
1977
|
+
|
|
1978
|
+
const run = core.append(createUserAppendMessage("Start"));
|
|
1979
|
+
await vi.waitFor(() => expect(history.append).toHaveBeenCalledOnce());
|
|
1980
|
+
release();
|
|
1981
|
+
await run;
|
|
1982
|
+
await vi.waitFor(() => expect(history.append).toHaveBeenCalledTimes(2));
|
|
1983
|
+
const assistant = core.getMessages()[1]!;
|
|
1984
|
+
expect(stored.get(assistant.id)).toMatchObject({
|
|
1985
|
+
content: [{ type: "text", text: "Done" }],
|
|
1986
|
+
status: { type: "complete", reason: "stop" },
|
|
1987
|
+
});
|
|
1988
|
+
});
|
|
1989
|
+
|
|
1990
|
+
it("retries a rejected final history update", async () => {
|
|
1991
|
+
let releaseSettlement!: () => void;
|
|
1992
|
+
const settlement = new Promise<void>((resolve) => {
|
|
1993
|
+
releaseSettlement = resolve;
|
|
1994
|
+
});
|
|
1995
|
+
let releaseRetry!: () => void;
|
|
1996
|
+
const retry = new Promise<void>((resolve) => {
|
|
1997
|
+
releaseRetry = resolve;
|
|
1998
|
+
});
|
|
1999
|
+
const error = new Error("history update failed");
|
|
2000
|
+
const stored = new Map<string, ThreadMessage>();
|
|
2001
|
+
const consoleError = vi
|
|
2002
|
+
.spyOn(console, "error")
|
|
2003
|
+
.mockImplementation(() => {});
|
|
2004
|
+
const history = {
|
|
2005
|
+
load: vi.fn().mockResolvedValue({ messages: [] }),
|
|
2006
|
+
append: vi.fn(async (item: { message: ThreadMessage }) => {
|
|
2007
|
+
stored.set(item.message.id, item.message);
|
|
2008
|
+
}),
|
|
2009
|
+
update: vi
|
|
2010
|
+
.fn()
|
|
2011
|
+
.mockRejectedValueOnce(error)
|
|
2012
|
+
.mockImplementation(async (item: { message: ThreadMessage }) => {
|
|
2013
|
+
stored.set(item.message.id, item.message);
|
|
2014
|
+
}),
|
|
2015
|
+
};
|
|
2016
|
+
const core = createCore(
|
|
2017
|
+
{
|
|
2018
|
+
streamMessage: vi.fn().mockImplementation(async function* () {
|
|
2019
|
+
yield statusUpdateEvent("input_required", "What should I do?");
|
|
2020
|
+
await settlement;
|
|
2021
|
+
yield statusUpdateEvent("completed", "Done");
|
|
2022
|
+
await retry;
|
|
2023
|
+
yield statusUpdateEvent("completed", "Done");
|
|
2024
|
+
}),
|
|
2025
|
+
},
|
|
2026
|
+
{ history },
|
|
2027
|
+
);
|
|
2028
|
+
|
|
2029
|
+
const run = core.append(createUserAppendMessage("Start"));
|
|
2030
|
+
await vi.waitFor(() => expect(history.append).toHaveBeenCalledTimes(2));
|
|
2031
|
+
releaseSettlement();
|
|
2032
|
+
await vi.waitFor(() => {
|
|
2033
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
2034
|
+
"[react-a2a] failed to update history entry",
|
|
2035
|
+
error,
|
|
2036
|
+
);
|
|
2037
|
+
});
|
|
2038
|
+
releaseRetry();
|
|
2039
|
+
await run;
|
|
2040
|
+
|
|
2041
|
+
await vi.waitFor(() => expect(history.update).toHaveBeenCalledTimes(2));
|
|
2042
|
+
const assistant = core.getMessages()[1]!;
|
|
2043
|
+
expect(stored.get(assistant.id)).toMatchObject({
|
|
2044
|
+
content: [{ type: "text", text: "Done" }],
|
|
2045
|
+
status: { type: "complete", reason: "stop" },
|
|
2046
|
+
});
|
|
2047
|
+
});
|
|
2048
|
+
|
|
2049
|
+
it("retries a rejected final history append", async () => {
|
|
2050
|
+
let releaseSettlement!: () => void;
|
|
2051
|
+
const settlement = new Promise<void>((resolve) => {
|
|
2052
|
+
releaseSettlement = resolve;
|
|
2053
|
+
});
|
|
2054
|
+
let releaseRetry!: () => void;
|
|
2055
|
+
const retry = new Promise<void>((resolve) => {
|
|
2056
|
+
releaseRetry = resolve;
|
|
2057
|
+
});
|
|
2058
|
+
const error = new Error("history append failed");
|
|
2059
|
+
const stored = new Map<string, ThreadMessage>();
|
|
2060
|
+
const consoleError = vi
|
|
2061
|
+
.spyOn(console, "error")
|
|
2062
|
+
.mockImplementation(() => {});
|
|
2063
|
+
let assistantAppendAttempts = 0;
|
|
2064
|
+
const history = {
|
|
2065
|
+
load: vi.fn().mockResolvedValue({ messages: [] }),
|
|
2066
|
+
append: vi.fn(async (item: { message: ThreadMessage }) => {
|
|
2067
|
+
if (
|
|
2068
|
+
item.message.role === "assistant" &&
|
|
2069
|
+
assistantAppendAttempts++ === 0
|
|
2070
|
+
) {
|
|
2071
|
+
throw error;
|
|
2072
|
+
}
|
|
2073
|
+
stored.set(item.message.id, item.message);
|
|
2074
|
+
}),
|
|
2075
|
+
};
|
|
2076
|
+
const core = createCore(
|
|
2077
|
+
{
|
|
2078
|
+
streamMessage: vi.fn().mockImplementation(async function* () {
|
|
2079
|
+
yield statusUpdateEvent("input_required", "What should I do?");
|
|
2080
|
+
await settlement;
|
|
2081
|
+
yield statusUpdateEvent("completed", "Done");
|
|
2082
|
+
await retry;
|
|
2083
|
+
yield statusUpdateEvent("completed", "Done");
|
|
2084
|
+
}),
|
|
2085
|
+
},
|
|
2086
|
+
{ history },
|
|
2087
|
+
);
|
|
2088
|
+
|
|
2089
|
+
const run = core.append(createUserAppendMessage("Start"));
|
|
2090
|
+
await vi.waitFor(() => expect(history.append).toHaveBeenCalledOnce());
|
|
2091
|
+
releaseSettlement();
|
|
2092
|
+
await vi.waitFor(() => {
|
|
2093
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
2094
|
+
"[react-a2a] failed to append history entry",
|
|
2095
|
+
error,
|
|
2096
|
+
);
|
|
2097
|
+
});
|
|
2098
|
+
releaseRetry();
|
|
2099
|
+
await run;
|
|
2100
|
+
|
|
2101
|
+
await vi.waitFor(() => expect(history.append).toHaveBeenCalledTimes(3));
|
|
2102
|
+
const assistant = core.getMessages()[1]!;
|
|
2103
|
+
expect(stored.get(assistant.id)).toMatchObject({
|
|
2104
|
+
content: [{ type: "text", text: "Done" }],
|
|
2105
|
+
status: { type: "complete", reason: "stop" },
|
|
2106
|
+
});
|
|
2107
|
+
});
|
|
2108
|
+
});
|
|
2109
|
+
|
|
2110
|
+
describe("tool interactions", () => {
|
|
2111
|
+
it("updates a settled assistant message in history", async () => {
|
|
2112
|
+
const history = {
|
|
2113
|
+
load: vi.fn().mockResolvedValue({ messages: [] }),
|
|
2114
|
+
append: vi.fn().mockResolvedValue(undefined),
|
|
2115
|
+
update: vi.fn().mockResolvedValue(undefined),
|
|
2116
|
+
};
|
|
2117
|
+
const core = createCore(
|
|
2118
|
+
{
|
|
2119
|
+
streamMessage: vi.fn().mockImplementation(async function* () {
|
|
2120
|
+
yield statusUpdateEvent("completed", undefined, [
|
|
2121
|
+
{
|
|
2122
|
+
data: [
|
|
2123
|
+
{
|
|
2124
|
+
version: "v0.9",
|
|
2125
|
+
createSurface: { surfaceId: "summary" },
|
|
2126
|
+
},
|
|
2127
|
+
{
|
|
2128
|
+
version: "v0.9",
|
|
2129
|
+
updateComponents: {
|
|
2130
|
+
surfaceId: "summary",
|
|
2131
|
+
components: [
|
|
2132
|
+
{
|
|
2133
|
+
id: "root",
|
|
2134
|
+
component: "Text",
|
|
2135
|
+
text: "Ready",
|
|
2136
|
+
},
|
|
2137
|
+
],
|
|
2138
|
+
},
|
|
2139
|
+
},
|
|
2140
|
+
],
|
|
2141
|
+
},
|
|
2142
|
+
]);
|
|
2143
|
+
}),
|
|
2144
|
+
},
|
|
2145
|
+
{ history },
|
|
2146
|
+
);
|
|
2147
|
+
|
|
2148
|
+
await core.append(createUserAppendMessage("Go"));
|
|
2149
|
+
await vi.waitFor(() => expect(history.append).toHaveBeenCalledTimes(2));
|
|
2150
|
+
|
|
2151
|
+
await core.recordToolInteraction({
|
|
2152
|
+
messageId: core.getMessages()[1]!.id,
|
|
2153
|
+
toolCallId: "a2ui:summary",
|
|
2154
|
+
interaction: toolInteraction,
|
|
2155
|
+
});
|
|
2156
|
+
|
|
2157
|
+
await vi.waitFor(() => expect(history.update).toHaveBeenCalledOnce());
|
|
2158
|
+
expect(history.update.mock.calls[0]![0].parentId).toBe(
|
|
2159
|
+
core.getMessages()[0]!.id,
|
|
2160
|
+
);
|
|
2161
|
+
expect(history.update.mock.calls[0]![0].message.content).toContainEqual(
|
|
2162
|
+
expect.objectContaining({
|
|
2163
|
+
toolCallId: "a2ui:summary",
|
|
2164
|
+
unstable_interactions: { entries: [toolInteraction] },
|
|
2165
|
+
}),
|
|
2166
|
+
);
|
|
2167
|
+
});
|
|
2168
|
+
|
|
2169
|
+
it("rejects an unknown message or tool call", async () => {
|
|
2170
|
+
const core = createCore();
|
|
2171
|
+
core.applyExternalMessages([
|
|
2172
|
+
{
|
|
2173
|
+
id: "assistant",
|
|
2174
|
+
role: "assistant",
|
|
2175
|
+
createdAt: new Date(),
|
|
2176
|
+
status: { type: "complete", reason: "stop" },
|
|
2177
|
+
content: [
|
|
2178
|
+
{
|
|
2179
|
+
type: "tool-call",
|
|
2180
|
+
toolCallId: "known",
|
|
2181
|
+
toolName: "present",
|
|
2182
|
+
args: {},
|
|
2183
|
+
argsText: "{}",
|
|
2184
|
+
result: {},
|
|
2185
|
+
},
|
|
2186
|
+
],
|
|
2187
|
+
metadata: {
|
|
2188
|
+
unstable_state: null,
|
|
2189
|
+
unstable_annotations: [],
|
|
2190
|
+
unstable_data: [],
|
|
2191
|
+
steps: [],
|
|
2192
|
+
custom: {},
|
|
2193
|
+
},
|
|
2194
|
+
} as ThreadMessage,
|
|
2195
|
+
]);
|
|
2196
|
+
|
|
2197
|
+
await expect(
|
|
2198
|
+
core.recordToolInteraction({
|
|
2199
|
+
messageId: "missing",
|
|
2200
|
+
toolCallId: "known",
|
|
2201
|
+
interaction: toolInteraction,
|
|
2202
|
+
}),
|
|
2203
|
+
).rejects.toThrow("non-existing message");
|
|
2204
|
+
await expect(
|
|
2205
|
+
core.recordToolInteraction({
|
|
2206
|
+
messageId: "assistant",
|
|
2207
|
+
toolCallId: "missing",
|
|
2208
|
+
interaction: toolInteraction,
|
|
2209
|
+
}),
|
|
2210
|
+
).rejects.toThrow("non-existing tool call");
|
|
2211
|
+
});
|
|
1334
2212
|
});
|
|
1335
2213
|
|
|
1336
2214
|
// --- Task snapshot ---
|