@assistant-ui/react-a2a 0.2.21 → 0.2.23
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 +8 -5
- package/dist/A2AClient.d.ts.map +1 -1
- package/dist/A2AClient.js +85 -24
- package/dist/A2AClient.js.map +1 -1
- package/dist/A2AThreadRuntimeCore.d.ts +15 -4
- package/dist/A2AThreadRuntimeCore.d.ts.map +1 -1
- package/dist/A2AThreadRuntimeCore.js +162 -45
- package/dist/A2AThreadRuntimeCore.js.map +1 -1
- package/dist/a2aExtras.d.ts +0 -1
- package/dist/a2aExtras.d.ts.map +1 -1
- package/dist/conversions.d.ts +6 -4
- package/dist/conversions.d.ts.map +1 -1
- package/dist/conversions.js +32 -3
- package/dist/conversions.js.map +1 -1
- package/dist/hooks.d.ts +0 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/types.d.ts +27 -14
- package/dist/types.d.ts.map +1 -1
- package/dist/useA2ARuntime.d.ts +0 -1
- package/dist/useA2ARuntime.d.ts.map +1 -1
- package/dist/useA2ARuntime.js +30 -18
- package/dist/useA2ARuntime.js.map +1 -1
- package/package.json +11 -6
- package/src/A2AClient.test.ts +321 -4
- package/src/A2AClient.ts +158 -49
- package/src/A2AThreadRuntimeCore.test.ts +458 -0
- package/src/A2AThreadRuntimeCore.ts +215 -57
- package/src/conversions.test.ts +109 -2
- package/src/conversions.ts +46 -4
- package/src/useA2ARuntime.test.tsx +142 -0
- package/src/useA2ARuntime.ts +55 -36
package/src/A2AClient.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { SSEEventDecoder, type SSEEvent } from "assistant-stream/utils";
|
|
2
|
+
import { isRecord } from "@assistant-ui/core/internal";
|
|
1
3
|
import type {
|
|
2
4
|
A2AAgentCard,
|
|
3
5
|
A2AErrorInfo,
|
|
@@ -152,6 +154,114 @@ function discriminateStreamResponse(
|
|
|
152
154
|
return null;
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
const TASK_STATES: ReadonlySet<string> = new Set(
|
|
158
|
+
Object.keys({
|
|
159
|
+
unspecified: true,
|
|
160
|
+
submitted: true,
|
|
161
|
+
working: true,
|
|
162
|
+
completed: true,
|
|
163
|
+
failed: true,
|
|
164
|
+
canceled: true,
|
|
165
|
+
input_required: true,
|
|
166
|
+
rejected: true,
|
|
167
|
+
auth_required: true,
|
|
168
|
+
} satisfies Record<A2ATaskState, true>),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const isTaskState = (value: unknown): value is A2ATaskState =>
|
|
172
|
+
typeof value === "string" && TASK_STATES.has(value);
|
|
173
|
+
|
|
174
|
+
const isTask = (value: unknown): value is A2ATask =>
|
|
175
|
+
isRecord(value) &&
|
|
176
|
+
typeof value.id === "string" &&
|
|
177
|
+
value.id.length > 0 &&
|
|
178
|
+
isRecord(value.status) &&
|
|
179
|
+
isTaskState(value.status.state);
|
|
180
|
+
|
|
181
|
+
const isMessage = (value: unknown): value is A2AMessage =>
|
|
182
|
+
isRecord(value) &&
|
|
183
|
+
typeof value.messageId === "string" &&
|
|
184
|
+
value.messageId.length > 0 &&
|
|
185
|
+
typeof value.role === "string" &&
|
|
186
|
+
value.role.length > 0 &&
|
|
187
|
+
Array.isArray(value.parts) &&
|
|
188
|
+
value.parts.every(isRecord);
|
|
189
|
+
|
|
190
|
+
const isStringArray = (value: unknown): value is string[] =>
|
|
191
|
+
Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
192
|
+
|
|
193
|
+
const invalidAgentCard = (): never => {
|
|
194
|
+
throw new Error(
|
|
195
|
+
"Invalid A2A agent card response: expected a valid agent card payload.",
|
|
196
|
+
);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const parseCardString = (value: unknown): string =>
|
|
200
|
+
value == null ? "" : typeof value === "string" ? value : invalidAgentCard();
|
|
201
|
+
|
|
202
|
+
const parseCardStringArray = (value: unknown): string[] =>
|
|
203
|
+
value == null ? [] : isStringArray(value) ? value : invalidAgentCard();
|
|
204
|
+
|
|
205
|
+
const parseCardRecordArray = (value: unknown): Record<string, unknown>[] =>
|
|
206
|
+
value == null
|
|
207
|
+
? []
|
|
208
|
+
: Array.isArray(value) && value.every(isRecord)
|
|
209
|
+
? (value as Record<string, unknown>[])
|
|
210
|
+
: invalidAgentCard();
|
|
211
|
+
|
|
212
|
+
const parseCardRecord = (value: unknown): Record<string, unknown> =>
|
|
213
|
+
value == null ? {} : isRecord(value) ? value : invalidAgentCard();
|
|
214
|
+
|
|
215
|
+
// Proto3 JSON parsing treats omitted and null fields as defaults, so a valid
|
|
216
|
+
// card may arrive without its empty lists, strings, or capabilities. Fill
|
|
217
|
+
// those per the proto3 JSON mapping rules; a payload without a name or with a
|
|
218
|
+
// present field of the wrong type rejects.
|
|
219
|
+
const parseAgentCardResponse = (value: unknown): A2AAgentCard => {
|
|
220
|
+
if (
|
|
221
|
+
!isRecord(value) ||
|
|
222
|
+
typeof value.name !== "string" ||
|
|
223
|
+
value.name.length === 0
|
|
224
|
+
) {
|
|
225
|
+
return invalidAgentCard();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
...value,
|
|
230
|
+
name: value.name,
|
|
231
|
+
description: parseCardString(value.description),
|
|
232
|
+
version: parseCardString(value.version),
|
|
233
|
+
supportedInterfaces: parseCardRecordArray(value.supportedInterfaces).map(
|
|
234
|
+
(entry) => ({
|
|
235
|
+
...entry,
|
|
236
|
+
url: parseCardString(entry.url),
|
|
237
|
+
protocolBinding: parseCardString(entry.protocolBinding),
|
|
238
|
+
protocolVersion: parseCardString(entry.protocolVersion),
|
|
239
|
+
}),
|
|
240
|
+
),
|
|
241
|
+
capabilities: parseCardRecord(value.capabilities),
|
|
242
|
+
defaultInputModes: parseCardStringArray(value.defaultInputModes),
|
|
243
|
+
defaultOutputModes: parseCardStringArray(value.defaultOutputModes),
|
|
244
|
+
skills: parseCardRecordArray(value.skills).map((entry) => ({
|
|
245
|
+
...entry,
|
|
246
|
+
id: parseCardString(entry.id),
|
|
247
|
+
name: parseCardString(entry.name),
|
|
248
|
+
description: parseCardString(entry.description),
|
|
249
|
+
tags: parseCardStringArray(entry.tags),
|
|
250
|
+
})),
|
|
251
|
+
} as A2AAgentCard;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const parseSendMessageResponse = (value: unknown): A2ATask | A2AMessage => {
|
|
255
|
+
if (isRecord(value)) {
|
|
256
|
+
const candidate = value.task ?? value.message ?? value;
|
|
257
|
+
if (isTask(candidate) || isMessage(candidate)) return candidate;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
throw new Error(
|
|
261
|
+
"Invalid A2A message:send response: expected a valid task or message payload.",
|
|
262
|
+
);
|
|
263
|
+
};
|
|
264
|
+
|
|
155
265
|
function signalInit(signal?: AbortSignal): RequestInit {
|
|
156
266
|
return signal ? { signal } : {};
|
|
157
267
|
}
|
|
@@ -272,14 +382,15 @@ export class A2AClient {
|
|
|
272
382
|
await this.throwResponseError(response);
|
|
273
383
|
}
|
|
274
384
|
const json = await response.json();
|
|
275
|
-
return normalizeKeys(json)
|
|
385
|
+
return parseAgentCardResponse(normalizeKeys(json));
|
|
276
386
|
}
|
|
277
387
|
|
|
278
388
|
async getExtendedAgentCard(signal?: AbortSignal): Promise<A2AAgentCard> {
|
|
279
|
-
|
|
389
|
+
const result = await this.fetchJSON<unknown>(
|
|
280
390
|
`${this.getBasePath()}/extendedAgentCard`,
|
|
281
391
|
signalInit(signal),
|
|
282
392
|
);
|
|
393
|
+
return parseAgentCardResponse(result);
|
|
283
394
|
}
|
|
284
395
|
|
|
285
396
|
// --- Message ---
|
|
@@ -296,7 +407,7 @@ export class A2AClient {
|
|
|
296
407
|
if (configuration) body.configuration = configuration;
|
|
297
408
|
if (metadata) body.metadata = metadata;
|
|
298
409
|
|
|
299
|
-
const result = await this.fetchJSON<
|
|
410
|
+
const result = await this.fetchJSON<unknown>(
|
|
300
411
|
`${this.getBasePath()}/message:send`,
|
|
301
412
|
{
|
|
302
413
|
method: "POST",
|
|
@@ -305,11 +416,7 @@ export class A2AClient {
|
|
|
305
416
|
},
|
|
306
417
|
);
|
|
307
418
|
|
|
308
|
-
|
|
309
|
-
if ("task" in result && result.task) return result.task as A2ATask;
|
|
310
|
-
if ("message" in result && result.message)
|
|
311
|
-
return result.message as A2AMessage;
|
|
312
|
-
return result as unknown as A2ATask | A2AMessage;
|
|
419
|
+
return parseSendMessageResponse(result);
|
|
313
420
|
}
|
|
314
421
|
|
|
315
422
|
async *streamMessage(
|
|
@@ -493,57 +600,59 @@ export class A2AClient {
|
|
|
493
600
|
// --- SSE Parsing ---
|
|
494
601
|
|
|
495
602
|
private async *parseSSE(response: Response): AsyncGenerator<A2AStreamEvent> {
|
|
603
|
+
const contentType = response.headers.get("Content-Type");
|
|
604
|
+
const mediaType = contentType?.split(";", 1)[0]?.trim().toLowerCase();
|
|
605
|
+
if (mediaType !== "text/event-stream") {
|
|
606
|
+
const received = contentType
|
|
607
|
+
? `"${contentType}"`
|
|
608
|
+
: "no Content-Type header";
|
|
609
|
+
throw new Error(
|
|
610
|
+
`Expected A2A stream response Content-Type "text/event-stream", received ${received}`,
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
|
|
496
614
|
const reader = response.body?.getReader();
|
|
497
615
|
if (!reader) throw new Error("No response body");
|
|
498
616
|
|
|
499
617
|
const decoder = new TextDecoder();
|
|
500
|
-
|
|
618
|
+
const sseDecoder = new SSEEventDecoder();
|
|
619
|
+
|
|
620
|
+
const readEvent = (event: SSEEvent): A2AStreamEvent | null => {
|
|
621
|
+
try {
|
|
622
|
+
let parsed = JSON.parse(event.data);
|
|
623
|
+
|
|
624
|
+
if (
|
|
625
|
+
parsed &&
|
|
626
|
+
typeof parsed === "object" &&
|
|
627
|
+
"jsonrpc" in parsed &&
|
|
628
|
+
"result" in parsed
|
|
629
|
+
) {
|
|
630
|
+
parsed = parsed.result;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const normalized = normalizeKeys(parsed) as Record<string, unknown>;
|
|
634
|
+
return discriminateStreamResponse(normalized);
|
|
635
|
+
} catch {
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
};
|
|
501
639
|
|
|
502
640
|
try {
|
|
503
641
|
while (true) {
|
|
504
642
|
const { done, value } = await reader.read();
|
|
505
|
-
if (done)
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
let eventEnd: number = buffer.indexOf("\n\n");
|
|
510
|
-
while (eventEnd !== -1) {
|
|
511
|
-
const eventText = buffer.slice(0, eventEnd);
|
|
512
|
-
buffer = buffer.slice(eventEnd + 2);
|
|
513
|
-
|
|
514
|
-
const dataLines: string[] = [];
|
|
515
|
-
|
|
516
|
-
for (const line of eventText.split("\n")) {
|
|
517
|
-
const trimmed = line.replace(/\r$/, "");
|
|
518
|
-
if (trimmed.startsWith("data:")) {
|
|
519
|
-
dataLines.push(trimmed.slice(5).trim());
|
|
520
|
-
}
|
|
521
|
-
// event:, id:, retry: lines are parsed but not used —
|
|
522
|
-
// we discriminate event type from the JSON payload.
|
|
643
|
+
if (done) {
|
|
644
|
+
for (const event of sseDecoder.push(decoder.decode())) {
|
|
645
|
+
const parsed = readEvent(event);
|
|
646
|
+
if (parsed) yield parsed;
|
|
523
647
|
}
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
524
650
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
// Unwrap JSON-RPC envelope if present
|
|
531
|
-
if (
|
|
532
|
-
parsed &&
|
|
533
|
-
typeof parsed === "object" &&
|
|
534
|
-
"jsonrpc" in parsed &&
|
|
535
|
-
"result" in parsed
|
|
536
|
-
) {
|
|
537
|
-
parsed = parsed.result;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
const normalized = normalizeKeys(parsed) as Record<string, unknown>;
|
|
541
|
-
const event = discriminateStreamResponse(normalized);
|
|
542
|
-
if (event) yield event;
|
|
543
|
-
} catch {
|
|
544
|
-
// Skip malformed events
|
|
545
|
-
}
|
|
546
|
-
eventEnd = buffer.indexOf("\n\n");
|
|
651
|
+
for (const event of sseDecoder.push(
|
|
652
|
+
decoder.decode(value, { stream: true }),
|
|
653
|
+
)) {
|
|
654
|
+
const parsed = readEvent(event);
|
|
655
|
+
if (parsed) yield parsed;
|
|
547
656
|
}
|
|
548
657
|
}
|
|
549
658
|
} finally {
|