@assistant-ui/react-a2a 0.2.5 → 0.2.7
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/README.md +47 -1
- package/dist/A2AClient.d.ts +43 -0
- package/dist/A2AClient.d.ts.map +1 -0
- package/dist/A2AClient.js +358 -0
- package/dist/A2AClient.js.map +1 -0
- package/dist/A2AThreadRuntimeCore.d.ts +75 -0
- package/dist/A2AThreadRuntimeCore.d.ts.map +1 -0
- package/dist/A2AThreadRuntimeCore.js +483 -0
- package/dist/A2AThreadRuntimeCore.js.map +1 -0
- package/dist/conversions.d.ts +14 -0
- package/dist/conversions.d.ts.map +1 -0
- package/dist/conversions.js +92 -0
- package/dist/conversions.js.map +1 -0
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +228 -84
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -9
- package/dist/types.js.map +1 -1
- package/dist/useA2ARuntime.d.ts +35 -48
- package/dist/useA2ARuntime.d.ts.map +1 -1
- package/dist/useA2ARuntime.js +126 -172
- package/dist/useA2ARuntime.js.map +1 -1
- package/package.json +9 -9
- package/src/A2AClient.test.ts +773 -0
- package/src/A2AClient.ts +519 -0
- package/src/A2AThreadRuntimeCore.test.ts +692 -0
- package/src/A2AThreadRuntimeCore.ts +633 -0
- package/src/conversions.test.ts +276 -0
- package/src/conversions.ts +115 -0
- package/src/index.ts +66 -6
- package/src/types.ts +276 -95
- package/src/useA2ARuntime.ts +204 -296
- package/dist/A2AMessageAccumulator.d.ts +0 -16
- package/dist/A2AMessageAccumulator.d.ts.map +0 -1
- package/dist/A2AMessageAccumulator.js +0 -29
- package/dist/A2AMessageAccumulator.js.map +0 -1
- package/dist/appendA2AChunk.d.ts +0 -3
- package/dist/appendA2AChunk.d.ts.map +0 -1
- package/dist/appendA2AChunk.js +0 -110
- package/dist/appendA2AChunk.js.map +0 -1
- package/dist/convertA2AMessages.d.ts +0 -64
- package/dist/convertA2AMessages.d.ts.map +0 -1
- package/dist/convertA2AMessages.js +0 -90
- package/dist/convertA2AMessages.js.map +0 -1
- package/dist/testUtils.d.ts +0 -4
- package/dist/testUtils.d.ts.map +0 -1
- package/dist/testUtils.js +0 -6
- package/dist/testUtils.js.map +0 -1
- package/dist/useA2AMessages.d.ts +0 -25
- package/dist/useA2AMessages.d.ts.map +0 -1
- package/dist/useA2AMessages.js +0 -122
- package/dist/useA2AMessages.js.map +0 -1
- package/src/A2AMessageAccumulator.ts +0 -48
- package/src/appendA2AChunk.ts +0 -121
- package/src/convertA2AMessages.ts +0 -108
- package/src/testUtils.ts +0 -11
- package/src/useA2AMessages.ts +0 -180
package/src/A2AClient.ts
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
A2AAgentCard,
|
|
3
|
+
A2AErrorInfo,
|
|
4
|
+
A2AListTaskPushNotificationConfigsResponse,
|
|
5
|
+
A2AListTasksRequest,
|
|
6
|
+
A2AListTasksResponse,
|
|
7
|
+
A2AMessage,
|
|
8
|
+
A2ARole,
|
|
9
|
+
A2ASendMessageConfiguration,
|
|
10
|
+
A2AStreamEvent,
|
|
11
|
+
A2ATask,
|
|
12
|
+
A2ATaskPushNotificationConfig,
|
|
13
|
+
A2ATaskState,
|
|
14
|
+
} from "./types";
|
|
15
|
+
import { A2A_PROTOCOL_VERSION } from "./types";
|
|
16
|
+
|
|
17
|
+
export type A2AClientOptions = {
|
|
18
|
+
baseUrl: string;
|
|
19
|
+
/** Optional tenant ID for multi-tenant servers. */
|
|
20
|
+
tenant?: string | undefined;
|
|
21
|
+
headers?:
|
|
22
|
+
| Record<string, string>
|
|
23
|
+
| (() => Record<string, string> | Promise<Record<string, string>>)
|
|
24
|
+
| undefined;
|
|
25
|
+
/** A2A extension URIs to negotiate. Sent as A2A-Extensions header. */
|
|
26
|
+
extensions?: string[] | undefined;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// === Structured A2A Error ===
|
|
30
|
+
|
|
31
|
+
export class A2AError extends Error {
|
|
32
|
+
code: number;
|
|
33
|
+
status: string;
|
|
34
|
+
details: unknown[] | undefined;
|
|
35
|
+
|
|
36
|
+
constructor(info: A2AErrorInfo) {
|
|
37
|
+
super(info.message);
|
|
38
|
+
this.name = "A2AError";
|
|
39
|
+
this.code = info.code;
|
|
40
|
+
this.status = info.status;
|
|
41
|
+
this.details = info.details;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// === Key normalization (incoming: snake_case → camelCase + enum normalization) ===
|
|
46
|
+
|
|
47
|
+
function toCamelCase(key: string): string {
|
|
48
|
+
return key.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Fields whose values are opaque user data (google.protobuf.Struct / Value).
|
|
52
|
+
// Keys inside these objects must NOT be camelCased or have enum normalization applied.
|
|
53
|
+
const OPAQUE_FIELDS = new Set([
|
|
54
|
+
"metadata",
|
|
55
|
+
"data",
|
|
56
|
+
"params",
|
|
57
|
+
"forwardedProps",
|
|
58
|
+
"scopes",
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
function normalizeKeys(obj: unknown, opaque = false): unknown {
|
|
62
|
+
if (Array.isArray(obj)) return obj.map((v) => normalizeKeys(v, opaque));
|
|
63
|
+
if (obj !== null && typeof obj === "object") {
|
|
64
|
+
const result: Record<string, unknown> = {};
|
|
65
|
+
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
|
66
|
+
// Inside opaque fields: preserve keys and values as-is (only recurse arrays/objects structurally)
|
|
67
|
+
if (opaque) {
|
|
68
|
+
result[key] =
|
|
69
|
+
typeof value === "object" && value !== null
|
|
70
|
+
? normalizeKeys(value, true)
|
|
71
|
+
: value;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const camelKey = toCamelCase(key);
|
|
76
|
+
const isOpaqueChild = OPAQUE_FIELDS.has(camelKey);
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
camelKey === "state" &&
|
|
80
|
+
typeof value === "string" &&
|
|
81
|
+
value.startsWith("TASK_STATE_")
|
|
82
|
+
) {
|
|
83
|
+
result[camelKey] = value.slice(11).toLowerCase();
|
|
84
|
+
} else if (
|
|
85
|
+
camelKey === "role" &&
|
|
86
|
+
typeof value === "string" &&
|
|
87
|
+
value.startsWith("ROLE_")
|
|
88
|
+
) {
|
|
89
|
+
result[camelKey] = value.slice(5).toLowerCase();
|
|
90
|
+
} else {
|
|
91
|
+
result[camelKey] = isOpaqueChild ? value : normalizeKeys(value, false);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
return obj;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// === Outgoing enum conversion (v1.0 ProtoJSON format) ===
|
|
100
|
+
|
|
101
|
+
function toWireRole(role: A2ARole): string {
|
|
102
|
+
if (role === "user") return "ROLE_USER";
|
|
103
|
+
if (role === "agent") return "ROLE_AGENT";
|
|
104
|
+
return "ROLE_UNSPECIFIED";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function toWireTaskState(state: A2ATaskState): string {
|
|
108
|
+
return `TASK_STATE_${state.toUpperCase()}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function toWireMessage(msg: A2AMessage): unknown {
|
|
112
|
+
return { ...msg, role: toWireRole(msg.role) };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// === Stream response discrimination ===
|
|
116
|
+
|
|
117
|
+
function discriminateStreamResponse(
|
|
118
|
+
data: Record<string, unknown>,
|
|
119
|
+
): A2AStreamEvent | null {
|
|
120
|
+
if ("task" in data && data.task) {
|
|
121
|
+
return { type: "task", task: data.task as A2ATask };
|
|
122
|
+
}
|
|
123
|
+
if ("message" in data && data.message) {
|
|
124
|
+
return { type: "message", message: data.message as A2AMessage };
|
|
125
|
+
}
|
|
126
|
+
if ("statusUpdate" in data && data.statusUpdate) {
|
|
127
|
+
return {
|
|
128
|
+
type: "statusUpdate",
|
|
129
|
+
event: data.statusUpdate as A2AStreamEvent extends {
|
|
130
|
+
type: "statusUpdate";
|
|
131
|
+
event: infer E;
|
|
132
|
+
}
|
|
133
|
+
? E
|
|
134
|
+
: never,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if ("artifactUpdate" in data && data.artifactUpdate) {
|
|
138
|
+
return {
|
|
139
|
+
type: "artifactUpdate",
|
|
140
|
+
event: data.artifactUpdate as A2AStreamEvent extends {
|
|
141
|
+
type: "artifactUpdate";
|
|
142
|
+
event: infer E;
|
|
143
|
+
}
|
|
144
|
+
? E
|
|
145
|
+
: never,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function signalInit(signal?: AbortSignal): RequestInit {
|
|
152
|
+
return signal ? { signal } : {};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// === A2A Client ===
|
|
156
|
+
|
|
157
|
+
export class A2AClient {
|
|
158
|
+
private baseUrl: string;
|
|
159
|
+
private tenant: string | undefined;
|
|
160
|
+
private extensionUris: string[] | undefined;
|
|
161
|
+
private headersFn:
|
|
162
|
+
| Record<string, string>
|
|
163
|
+
| (() => Record<string, string> | Promise<Record<string, string>>);
|
|
164
|
+
|
|
165
|
+
constructor(options: A2AClientOptions) {
|
|
166
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
167
|
+
this.tenant = options.tenant;
|
|
168
|
+
this.extensionUris = options.extensions;
|
|
169
|
+
this.headersFn = options.headers ?? {};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private getBasePath(): string {
|
|
173
|
+
return this.tenant ? `/${encodeURIComponent(this.tenant)}` : "";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private async getHeaders(
|
|
177
|
+
includeContentType = true,
|
|
178
|
+
): Promise<Record<string, string>> {
|
|
179
|
+
const custom =
|
|
180
|
+
typeof this.headersFn === "function"
|
|
181
|
+
? await this.headersFn()
|
|
182
|
+
: this.headersFn;
|
|
183
|
+
const headers: Record<string, string> = {
|
|
184
|
+
Accept: "application/a2a+json, application/json",
|
|
185
|
+
"A2A-Version": A2A_PROTOCOL_VERSION,
|
|
186
|
+
...custom,
|
|
187
|
+
};
|
|
188
|
+
if (includeContentType) {
|
|
189
|
+
headers["Content-Type"] = "application/a2a+json";
|
|
190
|
+
}
|
|
191
|
+
if (this.extensionUris?.length) {
|
|
192
|
+
headers["A2A-Extensions"] = this.extensionUris.join(", ");
|
|
193
|
+
}
|
|
194
|
+
return headers;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private async throwResponseError(response: Response): Promise<never> {
|
|
198
|
+
let errorBody: unknown;
|
|
199
|
+
try {
|
|
200
|
+
errorBody = await response.json();
|
|
201
|
+
} catch {
|
|
202
|
+
// no parseable body
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (errorBody && typeof errorBody === "object" && "error" in errorBody) {
|
|
206
|
+
const err = (errorBody as Record<string, any>).error;
|
|
207
|
+
throw new A2AError({
|
|
208
|
+
code: err.code ?? response.status,
|
|
209
|
+
status: err.status ?? response.statusText,
|
|
210
|
+
message: err.message ?? `A2A request failed: ${response.status}`,
|
|
211
|
+
details: err.details,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
throw new A2AError({
|
|
216
|
+
code: response.status,
|
|
217
|
+
status: response.statusText,
|
|
218
|
+
message: `A2A request failed: ${response.status} ${response.statusText}`,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private async fetchJSON<T>(
|
|
223
|
+
path: string,
|
|
224
|
+
options: RequestInit = {},
|
|
225
|
+
): Promise<T> {
|
|
226
|
+
const isGet = !options.method || options.method.toUpperCase() === "GET";
|
|
227
|
+
const headers = await this.getHeaders(!isGet);
|
|
228
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
229
|
+
...options,
|
|
230
|
+
headers: {
|
|
231
|
+
...headers,
|
|
232
|
+
...(options.headers as Record<string, string>),
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
if (!response.ok) {
|
|
236
|
+
await this.throwResponseError(response);
|
|
237
|
+
}
|
|
238
|
+
const json = await response.json();
|
|
239
|
+
return normalizeKeys(json) as T;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// --- Agent Card ---
|
|
243
|
+
|
|
244
|
+
async getAgentCard(signal?: AbortSignal): Promise<A2AAgentCard> {
|
|
245
|
+
const headers = await this.getHeaders(false); // GET: no Content-Type
|
|
246
|
+
const url = `${this.baseUrl}/.well-known/agent-card.json`;
|
|
247
|
+
const response = await fetch(url, { headers, ...signalInit(signal) });
|
|
248
|
+
if (!response.ok) {
|
|
249
|
+
await this.throwResponseError(response);
|
|
250
|
+
}
|
|
251
|
+
const json = await response.json();
|
|
252
|
+
return normalizeKeys(json) as A2AAgentCard;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async getExtendedAgentCard(signal?: AbortSignal): Promise<A2AAgentCard> {
|
|
256
|
+
return this.fetchJSON<A2AAgentCard>(
|
|
257
|
+
`${this.getBasePath()}/extendedAgentCard`,
|
|
258
|
+
signalInit(signal),
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// --- Message ---
|
|
263
|
+
|
|
264
|
+
async sendMessage(
|
|
265
|
+
message: A2AMessage,
|
|
266
|
+
configuration?: A2ASendMessageConfiguration,
|
|
267
|
+
metadata?: Record<string, unknown>,
|
|
268
|
+
signal?: AbortSignal,
|
|
269
|
+
): Promise<A2ATask | A2AMessage> {
|
|
270
|
+
const body: Record<string, unknown> = {
|
|
271
|
+
message: toWireMessage(message),
|
|
272
|
+
};
|
|
273
|
+
if (configuration) body.configuration = configuration;
|
|
274
|
+
if (metadata) body.metadata = metadata;
|
|
275
|
+
|
|
276
|
+
const result = await this.fetchJSON<Record<string, unknown>>(
|
|
277
|
+
`${this.getBasePath()}/message:send`,
|
|
278
|
+
{
|
|
279
|
+
method: "POST",
|
|
280
|
+
body: JSON.stringify(body),
|
|
281
|
+
...signalInit(signal),
|
|
282
|
+
},
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// Unwrap SendMessageResponse: {task: Task} | {message: Message}
|
|
286
|
+
if ("task" in result && result.task) return result.task as A2ATask;
|
|
287
|
+
if ("message" in result && result.message)
|
|
288
|
+
return result.message as A2AMessage;
|
|
289
|
+
return result as unknown as A2ATask | A2AMessage;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async *streamMessage(
|
|
293
|
+
message: A2AMessage,
|
|
294
|
+
configuration?: A2ASendMessageConfiguration,
|
|
295
|
+
metadata?: Record<string, unknown>,
|
|
296
|
+
signal?: AbortSignal,
|
|
297
|
+
): AsyncGenerator<A2AStreamEvent> {
|
|
298
|
+
const headers = await this.getHeaders(true);
|
|
299
|
+
headers["Accept"] = "text/event-stream";
|
|
300
|
+
|
|
301
|
+
const body: Record<string, unknown> = {
|
|
302
|
+
message: toWireMessage(message),
|
|
303
|
+
};
|
|
304
|
+
if (configuration) body.configuration = configuration;
|
|
305
|
+
if (metadata) body.metadata = metadata;
|
|
306
|
+
|
|
307
|
+
const response = await fetch(
|
|
308
|
+
`${this.baseUrl}${this.getBasePath()}/message:stream`,
|
|
309
|
+
{
|
|
310
|
+
method: "POST",
|
|
311
|
+
headers,
|
|
312
|
+
body: JSON.stringify(body),
|
|
313
|
+
...signalInit(signal),
|
|
314
|
+
},
|
|
315
|
+
);
|
|
316
|
+
if (!response.ok) {
|
|
317
|
+
await this.throwResponseError(response);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
yield* this.parseSSE(response);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// --- Tasks ---
|
|
324
|
+
|
|
325
|
+
async getTask(
|
|
326
|
+
taskId: string,
|
|
327
|
+
historyLength?: number,
|
|
328
|
+
signal?: AbortSignal,
|
|
329
|
+
): Promise<A2ATask> {
|
|
330
|
+
const params = new URLSearchParams();
|
|
331
|
+
if (historyLength !== undefined) {
|
|
332
|
+
// Proto field name for HTTP transcoding query params
|
|
333
|
+
params.set("history_length", String(historyLength));
|
|
334
|
+
}
|
|
335
|
+
const qs = params.toString();
|
|
336
|
+
return this.fetchJSON<A2ATask>(
|
|
337
|
+
`${this.getBasePath()}/tasks/${encodeURIComponent(taskId)}${qs ? `?${qs}` : ""}`,
|
|
338
|
+
signalInit(signal),
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async listTasks(
|
|
343
|
+
request?: A2AListTasksRequest,
|
|
344
|
+
signal?: AbortSignal,
|
|
345
|
+
): Promise<A2AListTasksResponse> {
|
|
346
|
+
const params = new URLSearchParams();
|
|
347
|
+
if (request?.contextId) params.set("context_id", request.contextId);
|
|
348
|
+
if (request?.status) params.set("status", toWireTaskState(request.status));
|
|
349
|
+
if (request?.pageSize !== undefined)
|
|
350
|
+
params.set("page_size", String(request.pageSize));
|
|
351
|
+
if (request?.pageToken) params.set("page_token", request.pageToken);
|
|
352
|
+
if (request?.historyLength !== undefined)
|
|
353
|
+
params.set("history_length", String(request.historyLength));
|
|
354
|
+
if (request?.statusTimestampAfter)
|
|
355
|
+
params.set("status_timestamp_after", request.statusTimestampAfter);
|
|
356
|
+
if (request?.includeArtifacts !== undefined)
|
|
357
|
+
params.set("include_artifacts", String(request.includeArtifacts));
|
|
358
|
+
const qs = params.toString();
|
|
359
|
+
return this.fetchJSON<A2AListTasksResponse>(
|
|
360
|
+
`${this.getBasePath()}/tasks${qs ? `?${qs}` : ""}`,
|
|
361
|
+
signalInit(signal),
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async cancelTask(
|
|
366
|
+
taskId: string,
|
|
367
|
+
metadata?: Record<string, unknown>,
|
|
368
|
+
signal?: AbortSignal,
|
|
369
|
+
): Promise<A2ATask> {
|
|
370
|
+
const body = metadata ? { metadata } : {};
|
|
371
|
+
return this.fetchJSON<A2ATask>(
|
|
372
|
+
`${this.getBasePath()}/tasks/${encodeURIComponent(taskId)}:cancel`,
|
|
373
|
+
{
|
|
374
|
+
method: "POST",
|
|
375
|
+
body: JSON.stringify(body),
|
|
376
|
+
...signalInit(signal),
|
|
377
|
+
},
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async *subscribeToTask(
|
|
382
|
+
taskId: string,
|
|
383
|
+
signal?: AbortSignal,
|
|
384
|
+
): AsyncGenerator<A2AStreamEvent> {
|
|
385
|
+
const headers = await this.getHeaders(false); // GET: no Content-Type
|
|
386
|
+
headers["Accept"] = "text/event-stream";
|
|
387
|
+
|
|
388
|
+
const response = await fetch(
|
|
389
|
+
`${this.baseUrl}${this.getBasePath()}/tasks/${encodeURIComponent(taskId)}:subscribe`,
|
|
390
|
+
{ headers, ...signalInit(signal) },
|
|
391
|
+
);
|
|
392
|
+
if (!response.ok) {
|
|
393
|
+
await this.throwResponseError(response);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
yield* this.parseSSE(response);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// --- Push Notification Configs ---
|
|
400
|
+
|
|
401
|
+
async createTaskPushNotificationConfig(
|
|
402
|
+
config: A2ATaskPushNotificationConfig,
|
|
403
|
+
signal?: AbortSignal,
|
|
404
|
+
): Promise<A2ATaskPushNotificationConfig> {
|
|
405
|
+
const taskId = config.taskId;
|
|
406
|
+
if (!taskId) throw new Error("taskId is required");
|
|
407
|
+
return this.fetchJSON<A2ATaskPushNotificationConfig>(
|
|
408
|
+
`${this.getBasePath()}/tasks/${encodeURIComponent(taskId)}/pushNotificationConfigs`,
|
|
409
|
+
{
|
|
410
|
+
method: "POST",
|
|
411
|
+
body: JSON.stringify(config),
|
|
412
|
+
...signalInit(signal),
|
|
413
|
+
},
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async getTaskPushNotificationConfig(
|
|
418
|
+
taskId: string,
|
|
419
|
+
configId: string,
|
|
420
|
+
signal?: AbortSignal,
|
|
421
|
+
): Promise<A2ATaskPushNotificationConfig> {
|
|
422
|
+
return this.fetchJSON<A2ATaskPushNotificationConfig>(
|
|
423
|
+
`${this.getBasePath()}/tasks/${encodeURIComponent(taskId)}/pushNotificationConfigs/${encodeURIComponent(configId)}`,
|
|
424
|
+
signalInit(signal),
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async listTaskPushNotificationConfigs(
|
|
429
|
+
taskId: string,
|
|
430
|
+
options?: { pageSize?: number; pageToken?: string },
|
|
431
|
+
signal?: AbortSignal,
|
|
432
|
+
): Promise<A2AListTaskPushNotificationConfigsResponse> {
|
|
433
|
+
const params = new URLSearchParams();
|
|
434
|
+
if (options?.pageSize !== undefined)
|
|
435
|
+
params.set("page_size", String(options.pageSize));
|
|
436
|
+
if (options?.pageToken) params.set("page_token", options.pageToken);
|
|
437
|
+
const qs = params.toString();
|
|
438
|
+
return this.fetchJSON<A2AListTaskPushNotificationConfigsResponse>(
|
|
439
|
+
`${this.getBasePath()}/tasks/${encodeURIComponent(taskId)}/pushNotificationConfigs${qs ? `?${qs}` : ""}`,
|
|
440
|
+
signalInit(signal),
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async deleteTaskPushNotificationConfig(
|
|
445
|
+
taskId: string,
|
|
446
|
+
configId: string,
|
|
447
|
+
signal?: AbortSignal,
|
|
448
|
+
): Promise<void> {
|
|
449
|
+
const isGet = false;
|
|
450
|
+
const headers = await this.getHeaders(!isGet);
|
|
451
|
+
const response = await fetch(
|
|
452
|
+
`${this.baseUrl}${this.getBasePath()}/tasks/${encodeURIComponent(taskId)}/pushNotificationConfigs/${encodeURIComponent(configId)}`,
|
|
453
|
+
{ method: "DELETE", headers, ...signalInit(signal) },
|
|
454
|
+
);
|
|
455
|
+
if (!response.ok) {
|
|
456
|
+
await this.throwResponseError(response);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// --- SSE Parsing ---
|
|
461
|
+
|
|
462
|
+
private async *parseSSE(response: Response): AsyncGenerator<A2AStreamEvent> {
|
|
463
|
+
const reader = response.body?.getReader();
|
|
464
|
+
if (!reader) throw new Error("No response body");
|
|
465
|
+
|
|
466
|
+
const decoder = new TextDecoder();
|
|
467
|
+
let buffer = "";
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
while (true) {
|
|
471
|
+
const { done, value } = await reader.read();
|
|
472
|
+
if (done) break;
|
|
473
|
+
|
|
474
|
+
buffer += decoder.decode(value, { stream: true });
|
|
475
|
+
|
|
476
|
+
let eventEnd: number;
|
|
477
|
+
while ((eventEnd = buffer.indexOf("\n\n")) !== -1) {
|
|
478
|
+
const eventText = buffer.slice(0, eventEnd);
|
|
479
|
+
buffer = buffer.slice(eventEnd + 2);
|
|
480
|
+
|
|
481
|
+
const dataLines: string[] = [];
|
|
482
|
+
|
|
483
|
+
for (const line of eventText.split("\n")) {
|
|
484
|
+
const trimmed = line.replace(/\r$/, "");
|
|
485
|
+
if (trimmed.startsWith("data:")) {
|
|
486
|
+
dataLines.push(trimmed.slice(5).trim());
|
|
487
|
+
}
|
|
488
|
+
// event:, id:, retry: lines are parsed but not used —
|
|
489
|
+
// we discriminate event type from the JSON payload.
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (dataLines.length === 0) continue;
|
|
493
|
+
|
|
494
|
+
try {
|
|
495
|
+
let parsed = JSON.parse(dataLines.join("\n"));
|
|
496
|
+
|
|
497
|
+
// Unwrap JSON-RPC envelope if present
|
|
498
|
+
if (
|
|
499
|
+
parsed &&
|
|
500
|
+
typeof parsed === "object" &&
|
|
501
|
+
"jsonrpc" in parsed &&
|
|
502
|
+
"result" in parsed
|
|
503
|
+
) {
|
|
504
|
+
parsed = parsed.result;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const normalized = normalizeKeys(parsed) as Record<string, unknown>;
|
|
508
|
+
const event = discriminateStreamResponse(normalized);
|
|
509
|
+
if (event) yield event;
|
|
510
|
+
} catch {
|
|
511
|
+
// Skip malformed events
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
} finally {
|
|
516
|
+
reader.releaseLock();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|