@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/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { A2AClient, A2AClientOptions } from "./A2AClient.js";
|
|
2
2
|
import { AttachmentAdapter, DictationAdapter, ExternalStoreSharedOptions, FeedbackAdapter, RealtimeVoiceAdapter, SpeechSynthesisAdapter, ThreadHistoryAdapter, ThreadMessage } from "@assistant-ui/core";
|
|
3
|
-
|
|
4
3
|
//#region src/types.d.ts
|
|
5
4
|
declare const A2A_PROTOCOL_VERSION = "1.0";
|
|
6
5
|
type A2ARole = "unspecified" | "user" | "agent";
|
|
@@ -157,8 +156,10 @@ type A2APasswordOAuthFlow = {
|
|
|
157
156
|
};
|
|
158
157
|
type A2AOAuthFlows = {
|
|
159
158
|
authorizationCode?: A2AAuthorizationCodeOAuthFlow | undefined;
|
|
160
|
-
clientCredentials?: A2AClientCredentialsOAuthFlow | undefined;
|
|
161
|
-
|
|
159
|
+
clientCredentials?: A2AClientCredentialsOAuthFlow | undefined;
|
|
160
|
+
/** @deprecated */
|
|
161
|
+
implicit?: A2AImplicitOAuthFlow | undefined;
|
|
162
|
+
/** @deprecated */
|
|
162
163
|
password?: A2APasswordOAuthFlow | undefined;
|
|
163
164
|
deviceCode?: A2ADeviceCodeOAuthFlow | undefined;
|
|
164
165
|
};
|
|
@@ -251,17 +252,29 @@ type UseA2AThreadListAdapter = {
|
|
|
251
252
|
}>;
|
|
252
253
|
};
|
|
253
254
|
type UseA2ARuntimeOptions = ExternalStoreSharedOptions & {
|
|
254
|
-
/** Pre-built A2A client instance. Provide this OR baseUrl. */
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
255
|
+
/** Pre-built A2A client instance. Provide this OR baseUrl. */
|
|
256
|
+
client?: A2AClient;
|
|
257
|
+
/** Base URL of the A2A server. Used to create a client if `client` is not provided. */
|
|
258
|
+
baseUrl?: string;
|
|
259
|
+
/** Optional path prefix for all API endpoints (e.g. "/v1"). Does not affect agent card discovery. Only used with baseUrl. */
|
|
260
|
+
basePath?: string;
|
|
261
|
+
/** Optional tenant ID for multi-tenant servers. Only used with baseUrl. */
|
|
262
|
+
tenant?: string;
|
|
263
|
+
/** Headers for the A2A client (only used with baseUrl). */
|
|
264
|
+
headers?: A2AClientOptions["headers"];
|
|
265
|
+
/** A2A extension URIs to negotiate. Only used with baseUrl. */
|
|
266
|
+
extensions?: string[];
|
|
267
|
+
/** Extra fetch options (e.g. `{ credentials: 'include' }`). Only used with `baseUrl`. */
|
|
268
|
+
fetchOptions?: A2AClientOptions["fetchOptions"];
|
|
269
|
+
/** Initial context ID for the conversation. */
|
|
270
|
+
contextId?: string;
|
|
271
|
+
/** Default send message configuration. */
|
|
272
|
+
configuration?: A2ASendMessageConfiguration;
|
|
273
|
+
/** Called when an error occurs. */
|
|
274
|
+
onError?: (error: Error) => void;
|
|
275
|
+
/** Called when a run is cancelled. */
|
|
276
|
+
onCancel?: () => void;
|
|
277
|
+
/** Called when an artifact is fully received (lastChunk). */
|
|
265
278
|
onArtifactComplete?: (artifact: A2AArtifact) => void;
|
|
266
279
|
adapters?: {
|
|
267
280
|
attachments?: AttachmentAdapter;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;cAgBa;KAED;KAEA;EACV;EACA;EACA;EACA;EAEA,WAAW;EACX;EACA;;KAGU;EACV;EACA;EACA;EACA,MAAM;EACN,OAAO;EACP,WAAW;EACX;EACA;;KAGU;KAWA;EACV,OAAO;EACP,UAAU;EACV;;KAGU;EACV;EACA;EACA;EACA,OAAO;EACP,WAAW;EACX;;KAGU;EACV;EACA;EACA,QAAQ;EACR,YAAY;EACZ,UAAU;EACV,WAAW;;KAID;EACV;EACA;EACA,QAAQ;EACR,WAAW;;KAGD;EACV;EACA;EACA,UAAU;EACV;EACA;EACA,WAAW;;KAGD;EACN;EAAc,MAAM;;EACpB;EAAiB,SAAS;;EAC1B;EAAsB,OAAO;;EAC7B;EAAwB,OAAO;;KAEzB;EACV;EACA;;KAGU;EACV;EACA;EACA;EACA;EACA;EACA,iBAAiB;;KAGP;EACV,SAAS;EACT;;KAGU;EACV;EACA,6BAA6B;EAC7B;EACA;;KAGU;EACV;EACA,SAAS;EACT;EACA;EACA;EACA;EACA;;KAGU;EACV,OAAO;EACP;EACA;EACA;;KAIU;EACV;EACA;EACA;EACA;;KAGU;EACV;EACA;EACA;;KAGU;EACV;EACA;EACA;;KAGU;EACV;EACA;EACA;EACA,QAAQ;EACR;;KAGU;EACV;EACA;EACA,QAAQ;;KAGE;EACV;EACA;EACA;EACA,QAAQ;;;KAIE;EACV;EACA;EACA,SAAS;;;KAIC;EACV;EACA;EACA,SAAS;;KAGC;EACV,oBAAoB;EACpB,oBAAoB;;EAEpB,WAAW;;EAEX,WAAW;EACX,aAAa;;KAGH;EACV;EACA,OAAO;EACP;;KAGU;EACV;EACA;;KAGU;EACV;;KAGU;EACV,uBAAuB;EACvB,yBAAyB;EACzB,uBAAuB;EACvB,8BAA8B;EAC9B,qBAAqB;;KAGX;EACV,SAAS;IAAiB;;;KAIhB;EACV;EACA;EACA,SAAS;;KAGC;EACV;EACA;EACA;EACA;EACA;EACA;EACA;EACA,uBAAuB;;KAGb;EACV;EACA;EACA,aACI;IACE;IACA;IACA;IACA,SAAS;;EAGf;;KAGU;EACV;EACA;EACA;EACA;;KAGU;EACV;EACA;EACA,qBAAqB;EACrB;IAEM;IACA;;EAGN;EACA;EACA,cAAc;EACd,kBAAkB,eAAe;EACjC,uBAAuB;EACvB;EACA;EACA,QAAQ;EACR,aAAa;EACb;;;KAIU;EACV,MAAM;EACN,oBAAoB;EACpB,WAAW;;KAGD;EACV;EACA,4BAA4B;EAC5B,oBAAoB,qBAAqB;IACvC,mBAAmB;;;KAIX,uBAAuB;;EAEjC,SAAS;;EAET;;EAEA;;EAEA;;EAEA,UAAU;;EAEV;;EAEA,eAAe;;EAGf;;EAEA,gBAAgB;;EAGhB,WAAW,OAAO;;EAElB;;EAEA,sBAAsB,UAAU;EAEhC;IACE,cAAc;IACd,SAAS;IACT,YAAY;IACZ,QAAQ;IACR,WAAW;IACX,UAAU;IACV,aAAa"}
|
package/dist/useA2ARuntime.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useA2ARuntime.d.ts","names":[],"sources":["../src/useA2ARuntime.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"useA2ARuntime.d.ts","names":[],"sources":["../src/useA2ARuntime.ts"],"mappings":";;;iBAoCgB,cAAc,SAAS,uBAAuB"}
|
package/dist/useA2ARuntime.js
CHANGED
|
@@ -5,36 +5,48 @@ import { a2aExtras } from "./a2aExtras.js";
|
|
|
5
5
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
6
6
|
import { useExternalStoreRuntime, useExternalStoreSharedOptions, useRuntimeAdapters } from "@assistant-ui/core/react";
|
|
7
7
|
//#region src/useA2ARuntime.ts
|
|
8
|
+
const serializeManagedClientOptions = (options) => {
|
|
9
|
+
const fetchOptions = Object.fromEntries(Object.entries(options.fetchOptions ?? {}).sort(([a], [b]) => a.localeCompare(b)));
|
|
10
|
+
return JSON.stringify({
|
|
11
|
+
...options,
|
|
12
|
+
fetchOptions
|
|
13
|
+
});
|
|
14
|
+
};
|
|
8
15
|
function useA2ARuntime(options) {
|
|
9
16
|
const [_version, setVersion] = useState(0);
|
|
10
17
|
const notifyUpdate = useCallback(() => setVersion((v) => v + 1), []);
|
|
11
18
|
const runtimeAdapters = useRuntimeAdapters();
|
|
12
19
|
const historyAdapter = options.adapters?.history ?? runtimeAdapters?.history;
|
|
13
20
|
const threadListAdapter = options.adapters?.threadList;
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
const headersRef = useRef(options.headers);
|
|
22
|
+
headersRef.current = options.headers;
|
|
23
|
+
const resolveHeaders = useCallback(() => {
|
|
24
|
+
const headers = headersRef.current;
|
|
25
|
+
return typeof headers === "function" ? headers() : headers ?? {};
|
|
26
|
+
}, []);
|
|
27
|
+
const managedClientOptionsKey = options.client ? null : options.baseUrl ? serializeManagedClientOptions({
|
|
17
28
|
baseUrl: options.baseUrl,
|
|
18
29
|
basePath: options.basePath,
|
|
19
30
|
tenant: options.tenant,
|
|
20
|
-
headers: options.headers,
|
|
21
31
|
extensions: options.extensions,
|
|
22
32
|
fetchOptions: options.fetchOptions
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
}) : null;
|
|
34
|
+
const client = useMemo(() => {
|
|
35
|
+
if (options.client) return options.client;
|
|
36
|
+
if (!managedClientOptionsKey) throw new Error("useA2ARuntime requires either `client` or `baseUrl`");
|
|
37
|
+
return new A2AClient({
|
|
38
|
+
...JSON.parse(managedClientOptionsKey),
|
|
39
|
+
headers: resolveHeaders
|
|
40
|
+
});
|
|
41
|
+
}, [
|
|
42
|
+
managedClientOptionsKey,
|
|
43
|
+
options.client,
|
|
44
|
+
resolveHeaders
|
|
45
|
+
]);
|
|
46
|
+
const core = useMemo(() => new A2AThreadRuntimeCore({
|
|
28
47
|
client,
|
|
29
|
-
contextId: options.contextId,
|
|
30
|
-
configuration: options.configuration,
|
|
31
|
-
...options.onError && { onError: options.onError },
|
|
32
|
-
...options.onCancel && { onCancel: options.onCancel },
|
|
33
|
-
...options.onArtifactComplete && { onArtifactComplete: options.onArtifactComplete },
|
|
34
|
-
...historyAdapter && { history: historyAdapter },
|
|
35
48
|
notifyUpdate
|
|
36
|
-
});
|
|
37
|
-
const core = coreRef.current;
|
|
49
|
+
}), [client, notifyUpdate]);
|
|
38
50
|
core.updateOptions({
|
|
39
51
|
client,
|
|
40
52
|
contextId: options.contextId,
|
|
@@ -77,7 +89,7 @@ function useA2ARuntime(options) {
|
|
|
77
89
|
return {
|
|
78
90
|
...shared,
|
|
79
91
|
isLoading: core.isLoading,
|
|
80
|
-
|
|
92
|
+
messageRepository: core.getMessageRepository(),
|
|
81
93
|
isRunning: core.isRunning(),
|
|
82
94
|
extras: a2aExtras.provide({
|
|
83
95
|
task: core.getTask(),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useA2ARuntime.js","names":[],"sources":["../src/useA2ARuntime.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n useExternalStoreRuntime,\n useExternalStoreSharedOptions,\n useRuntimeAdapters,\n} from \"@assistant-ui/core/react\";\nimport type {\n AssistantRuntime,\n AppendMessage,\n ExternalStoreAdapter,\n ThreadMessage,\n} from \"@assistant-ui/core\";\nimport { A2AClient } from \"./A2AClient\";\nimport { A2AThreadRuntimeCore } from \"./A2AThreadRuntimeCore\";\nimport { a2aExtras } from \"./a2aExtras\";\nimport type { UseA2ARuntimeOptions } from \"./types\";\n\nexport function useA2ARuntime(options: UseA2ARuntimeOptions): AssistantRuntime {\n const [_version, setVersion] = useState(0);\n const notifyUpdate = useCallback(() => setVersion((v) => v + 1), []);\n const runtimeAdapters = useRuntimeAdapters();\n const historyAdapter = options.adapters?.history ?? runtimeAdapters?.history;\n const threadListAdapter = options.adapters?.threadList;\n\n
|
|
1
|
+
{"version":3,"file":"useA2ARuntime.js","names":[],"sources":["../src/useA2ARuntime.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n useExternalStoreRuntime,\n useExternalStoreSharedOptions,\n useRuntimeAdapters,\n} from \"@assistant-ui/core/react\";\nimport type {\n AssistantRuntime,\n AppendMessage,\n ExternalStoreAdapter,\n ThreadMessage,\n} from \"@assistant-ui/core\";\nimport { A2AClient, type A2AClientOptions } from \"./A2AClient\";\nimport { A2AThreadRuntimeCore } from \"./A2AThreadRuntimeCore\";\nimport { a2aExtras } from \"./a2aExtras\";\nimport type { UseA2ARuntimeOptions } from \"./types\";\n\ntype ManagedA2AClientOptions = Omit<A2AClientOptions, \"headers\">;\n\nconst serializeManagedClientOptions = (\n options: ManagedA2AClientOptions,\n): string => {\n const fetchOptions = Object.fromEntries(\n Object.entries(options.fetchOptions ?? {}).sort(([a], [b]) =>\n a.localeCompare(b),\n ),\n );\n\n return JSON.stringify({\n ...options,\n fetchOptions,\n });\n};\n\nexport function useA2ARuntime(options: UseA2ARuntimeOptions): AssistantRuntime {\n const [_version, setVersion] = useState(0);\n const notifyUpdate = useCallback(() => setVersion((v) => v + 1), []);\n const runtimeAdapters = useRuntimeAdapters();\n const historyAdapter = options.adapters?.history ?? runtimeAdapters?.history;\n const threadListAdapter = options.adapters?.threadList;\n\n const headersRef = useRef(options.headers);\n headersRef.current = options.headers;\n const resolveHeaders = useCallback(() => {\n const headers = headersRef.current;\n return typeof headers === \"function\" ? headers() : (headers ?? {});\n }, []);\n\n const managedClientOptionsKey = options.client\n ? null\n : options.baseUrl\n ? serializeManagedClientOptions({\n baseUrl: options.baseUrl,\n basePath: options.basePath,\n tenant: options.tenant,\n extensions: options.extensions,\n fetchOptions: options.fetchOptions,\n })\n : null;\n\n const client = useMemo(() => {\n if (options.client) return options.client;\n if (!managedClientOptionsKey) {\n throw new Error(\"useA2ARuntime requires either `client` or `baseUrl`\");\n }\n\n return new A2AClient({\n ...(JSON.parse(managedClientOptionsKey) as ManagedA2AClientOptions),\n headers: resolveHeaders,\n });\n }, [managedClientOptionsKey, options.client, resolveHeaders]);\n\n const core = useMemo(\n () =>\n new A2AThreadRuntimeCore({\n client,\n notifyUpdate,\n }),\n [client, notifyUpdate],\n );\n\n core.updateOptions({\n client,\n contextId: options.contextId,\n configuration: options.configuration,\n ...(options.onError && { onError: options.onError }),\n ...(options.onCancel && { onCancel: options.onCancel }),\n ...(options.onArtifactComplete && {\n onArtifactComplete: options.onArtifactComplete,\n }),\n ...(historyAdapter && { history: historyAdapter }),\n });\n\n // Thread list\n const threadList = useMemo(() => {\n if (!threadListAdapter) return undefined;\n\n const { onSwitchToNewThread, onSwitchToThread } = threadListAdapter;\n\n return {\n threadId: threadListAdapter.threadId,\n onSwitchToNewThread: onSwitchToNewThread\n ? async () => {\n await onSwitchToNewThread();\n core.applyExternalMessages([]);\n }\n : undefined,\n onSwitchToThread: onSwitchToThread\n ? async (threadId: string) => {\n const result = await onSwitchToThread(threadId);\n core.applyExternalMessages(result.messages);\n }\n : undefined,\n };\n }, [threadListAdapter, core]);\n\n // Adapters\n const adapters = options.adapters;\n const adapterAdapters = useMemo(\n () => ({\n attachments: adapters?.attachments ?? runtimeAdapters?.attachments,\n speech: adapters?.speech,\n dictation: adapters?.dictation,\n voice: adapters?.voice,\n feedback: adapters?.feedback,\n threadList,\n }),\n [adapters, runtimeAdapters, threadList],\n );\n\n // Build store adapter\n const shared = useExternalStoreSharedOptions(options);\n const store = useMemo(() => {\n void _version;\n\n return {\n ...shared,\n isLoading: core.isLoading,\n messageRepository: core.getMessageRepository(),\n isRunning: core.isRunning(),\n extras: a2aExtras.provide({\n task: core.getTask(),\n artifacts: core.getArtifacts(),\n agentCard: core.getAgentCard(),\n }),\n onNew: (message: AppendMessage) => core.append(message),\n onEdit: (message: AppendMessage) => core.edit(message),\n onReload: (parentId: string | null) => core.reload(parentId),\n onCancel: () => core.cancel(),\n setMessages: (messages: readonly ThreadMessage[]) =>\n core.applyExternalMessages(messages),\n onImport: (messages: readonly ThreadMessage[]) =>\n core.applyExternalMessages(messages),\n adapters: adapterAdapters,\n } satisfies ExternalStoreAdapter<ThreadMessage>;\n }, [adapterAdapters, core, _version, shared]);\n\n const runtime = useExternalStoreRuntime(store);\n\n useEffect(() => {\n core.attachRuntime(runtime);\n return () => {\n core.detachRuntime();\n };\n }, [core, runtime]);\n\n useEffect(() => {\n core.__internal_load();\n }, [core]);\n\n return runtime;\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/react-a2a",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.23",
|
|
4
4
|
"description": "A2A (Agent-to-Agent) v1.0 protocol adapter for assistant-ui",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"a2a",
|
|
@@ -29,8 +29,9 @@
|
|
|
29
29
|
],
|
|
30
30
|
"sideEffects": false,
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@assistant-ui/core": "^0.2.
|
|
33
|
-
"@assistant-ui/store": "^0.2.
|
|
32
|
+
"@assistant-ui/core": "^0.2.22",
|
|
33
|
+
"@assistant-ui/store": "^0.2.21",
|
|
34
|
+
"assistant-stream": "^0.3.27"
|
|
34
35
|
},
|
|
35
36
|
"peerDependencies": {
|
|
36
37
|
"@types/react": "*",
|
|
@@ -42,10 +43,14 @@
|
|
|
42
43
|
}
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
46
|
+
"@testing-library/dom": "^10.4.1",
|
|
47
|
+
"@testing-library/react": "^16.3.2",
|
|
45
48
|
"@types/react": "^19.2.17",
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
+
"jsdom": "^29.1.1",
|
|
50
|
+
"react": "^19.2.8",
|
|
51
|
+
"react-dom": "^19.2.8",
|
|
52
|
+
"vitest": "^4.1.10",
|
|
53
|
+
"@assistant-ui/x-buildutils": "0.0.19"
|
|
49
54
|
},
|
|
50
55
|
"publishConfig": {
|
|
51
56
|
"access": "public",
|
package/src/A2AClient.test.ts
CHANGED
|
@@ -14,17 +14,23 @@ function mockFetchResponse(body: unknown, ok = true, status = 200): Response {
|
|
|
14
14
|
} as unknown as Response;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
function
|
|
18
|
-
|
|
17
|
+
function mockSSETextResponse(
|
|
18
|
+
text: string | string[],
|
|
19
|
+
contentType: string | null = "text/event-stream",
|
|
20
|
+
): Response {
|
|
19
21
|
const encoder = new TextEncoder();
|
|
20
|
-
const chunks =
|
|
22
|
+
const chunks = (Array.isArray(text) ? text : [text]).map((chunk) =>
|
|
23
|
+
encoder.encode(chunk),
|
|
24
|
+
);
|
|
21
25
|
let index = 0;
|
|
26
|
+
const headers = new Headers();
|
|
27
|
+
if (contentType !== null) headers.set("content-type", contentType);
|
|
22
28
|
|
|
23
29
|
return {
|
|
24
30
|
ok: true,
|
|
25
31
|
status: 200,
|
|
26
32
|
statusText: "OK",
|
|
27
|
-
headers
|
|
33
|
+
headers,
|
|
28
34
|
body: {
|
|
29
35
|
getReader: () => ({
|
|
30
36
|
read: vi.fn().mockImplementation(() => {
|
|
@@ -42,6 +48,13 @@ function mockSSEResponse(lines: string[]): Response {
|
|
|
42
48
|
} as unknown as Response;
|
|
43
49
|
}
|
|
44
50
|
|
|
51
|
+
function mockSSEResponse(
|
|
52
|
+
lines: string[],
|
|
53
|
+
contentType: string | null = "text/event-stream",
|
|
54
|
+
): Response {
|
|
55
|
+
return mockSSETextResponse(lines.join("\n"), contentType);
|
|
56
|
+
}
|
|
57
|
+
|
|
45
58
|
const userMessage: A2AMessage = {
|
|
46
59
|
messageId: "msg-1",
|
|
47
60
|
role: "user",
|
|
@@ -429,6 +442,48 @@ describe("A2AClient", () => {
|
|
|
429
442
|
expect((result as any).role).toBe("agent");
|
|
430
443
|
});
|
|
431
444
|
|
|
445
|
+
it.each([
|
|
446
|
+
["an empty object", {}],
|
|
447
|
+
["a malformed task", { task: {} }],
|
|
448
|
+
[
|
|
449
|
+
"a task with an unknown state",
|
|
450
|
+
{ task: { id: "t1", status: { state: "typo" } } },
|
|
451
|
+
],
|
|
452
|
+
["a malformed message", { message: {} }],
|
|
453
|
+
[
|
|
454
|
+
"a message with a malformed part",
|
|
455
|
+
{
|
|
456
|
+
message: {
|
|
457
|
+
messageId: "m2",
|
|
458
|
+
role: "agent",
|
|
459
|
+
parts: [null],
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
],
|
|
463
|
+
])("rejects %s returned with a successful status", async (_name, body) => {
|
|
464
|
+
fetchMock.mockResolvedValue(mockFetchResponse(body));
|
|
465
|
+
|
|
466
|
+
await expect(client.sendMessage(userMessage)).rejects.toThrow(
|
|
467
|
+
"Invalid A2A message:send response: expected a valid task or message payload.",
|
|
468
|
+
);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it.each([
|
|
472
|
+
["task", { id: "t1", status: { state: "completed" } }],
|
|
473
|
+
[
|
|
474
|
+
"message",
|
|
475
|
+
{
|
|
476
|
+
messageId: "m2",
|
|
477
|
+
role: "agent",
|
|
478
|
+
parts: [{ text: "Hi" }],
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
])("accepts a direct %s response", async (_name, body) => {
|
|
482
|
+
fetchMock.mockResolvedValue(mockFetchResponse(body));
|
|
483
|
+
|
|
484
|
+
await expect(client.sendMessage(userMessage)).resolves.toEqual(body);
|
|
485
|
+
});
|
|
486
|
+
|
|
432
487
|
it("normalizes 'content' array from v0.3 server response to internal 'parts'", async () => {
|
|
433
488
|
fetchMock.mockResolvedValue(
|
|
434
489
|
mockFetchResponse({
|
|
@@ -758,6 +813,83 @@ describe("A2AClient", () => {
|
|
|
758
813
|
expect(card.supportedInterfaces).toHaveLength(1);
|
|
759
814
|
expect(card.supportedInterfaces[0]!.protocolBinding).toBe("HTTP+JSON");
|
|
760
815
|
});
|
|
816
|
+
|
|
817
|
+
it.each([
|
|
818
|
+
["an empty payload", {}],
|
|
819
|
+
["a payload without a name", { version: "1.0", skills: [] }],
|
|
820
|
+
["a non-object payload", "not a card"],
|
|
821
|
+
[
|
|
822
|
+
"a wrong-typed supportedInterfaces",
|
|
823
|
+
{ name: "Test Agent", supportedInterfaces: "invalid" },
|
|
824
|
+
],
|
|
825
|
+
["a wrong-typed skill entry", { name: "Test Agent", skills: ["nope"] }],
|
|
826
|
+
[
|
|
827
|
+
"a wrong-typed skill tags field",
|
|
828
|
+
{ name: "Test Agent", skills: [{ id: "s", tags: "broken" }] },
|
|
829
|
+
],
|
|
830
|
+
])("rejects %s", async (_name, body) => {
|
|
831
|
+
fetchMock.mockResolvedValue(mockFetchResponse(body));
|
|
832
|
+
|
|
833
|
+
await expect(client.getAgentCard()).rejects.toThrow(
|
|
834
|
+
"Invalid A2A agent card response: expected a valid agent card payload.",
|
|
835
|
+
);
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
it("treats explicit null fields as defaults", async () => {
|
|
839
|
+
fetchMock.mockResolvedValue(
|
|
840
|
+
mockFetchResponse({
|
|
841
|
+
name: "Test Agent",
|
|
842
|
+
description: null,
|
|
843
|
+
skills: null,
|
|
844
|
+
}),
|
|
845
|
+
);
|
|
846
|
+
|
|
847
|
+
const card = await client.getAgentCard();
|
|
848
|
+
|
|
849
|
+
expect(card.description).toBe("");
|
|
850
|
+
expect(card.skills).toEqual([]);
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it("fills defaults for fields omitted by proto3 JSON serialization", async () => {
|
|
854
|
+
fetchMock.mockResolvedValue(mockFetchResponse({ name: "Test Agent" }));
|
|
855
|
+
|
|
856
|
+
const card = await client.getAgentCard();
|
|
857
|
+
|
|
858
|
+
expect(card).toMatchObject({
|
|
859
|
+
name: "Test Agent",
|
|
860
|
+
description: "",
|
|
861
|
+
version: "",
|
|
862
|
+
supportedInterfaces: [],
|
|
863
|
+
capabilities: {},
|
|
864
|
+
defaultInputModes: [],
|
|
865
|
+
defaultOutputModes: [],
|
|
866
|
+
skills: [],
|
|
867
|
+
});
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
it("fills defaults inside interfaces and skills", async () => {
|
|
871
|
+
fetchMock.mockResolvedValue(
|
|
872
|
+
mockFetchResponse({
|
|
873
|
+
name: "Test Agent",
|
|
874
|
+
supported_interfaces: [{ url: "https://agent.test/a2a" }],
|
|
875
|
+
skills: [{ id: "recipes", name: "Recipes" }],
|
|
876
|
+
}),
|
|
877
|
+
);
|
|
878
|
+
|
|
879
|
+
const card = await client.getAgentCard();
|
|
880
|
+
|
|
881
|
+
expect(card.supportedInterfaces[0]).toMatchObject({
|
|
882
|
+
url: "https://agent.test/a2a",
|
|
883
|
+
protocolBinding: "",
|
|
884
|
+
protocolVersion: "",
|
|
885
|
+
});
|
|
886
|
+
expect(card.skills[0]).toMatchObject({
|
|
887
|
+
id: "recipes",
|
|
888
|
+
name: "Recipes",
|
|
889
|
+
description: "",
|
|
890
|
+
tags: [],
|
|
891
|
+
});
|
|
892
|
+
});
|
|
761
893
|
});
|
|
762
894
|
|
|
763
895
|
// --- SSE streaming ---
|
|
@@ -799,6 +931,111 @@ describe("A2AClient", () => {
|
|
|
799
931
|
expect(evt.event.status.message?.role).toBe("agent");
|
|
800
932
|
});
|
|
801
933
|
|
|
934
|
+
it("parses CRLF-delimited SSE events", async () => {
|
|
935
|
+
const sseData = JSON.stringify({
|
|
936
|
+
status_update: {
|
|
937
|
+
task_id: "t1",
|
|
938
|
+
context_id: "ctx-1",
|
|
939
|
+
status: { state: "TASK_STATE_WORKING" },
|
|
940
|
+
},
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
fetchMock.mockResolvedValue(
|
|
944
|
+
mockSSETextResponse(`data: ${sseData}\r\n\r\n`),
|
|
945
|
+
);
|
|
946
|
+
|
|
947
|
+
const events: A2AStreamEvent[] = [];
|
|
948
|
+
for await (const event of client.streamMessage(userMessage)) {
|
|
949
|
+
events.push(event);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
expect(events).toHaveLength(1);
|
|
953
|
+
expect(events[0]!.type).toBe("statusUpdate");
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
it("parses CR-delimited SSE events", async () => {
|
|
957
|
+
const sseData = JSON.stringify({
|
|
958
|
+
status_update: {
|
|
959
|
+
task_id: "t1",
|
|
960
|
+
context_id: "ctx-1",
|
|
961
|
+
status: { state: "TASK_STATE_WORKING" },
|
|
962
|
+
},
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
fetchMock.mockResolvedValue(mockSSETextResponse(`data: ${sseData}\r\r`));
|
|
966
|
+
|
|
967
|
+
const events: A2AStreamEvent[] = [];
|
|
968
|
+
for await (const event of client.streamMessage(userMessage)) {
|
|
969
|
+
events.push(event);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
expect(events).toHaveLength(1);
|
|
973
|
+
expect(events[0]!.type).toBe("statusUpdate");
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
it("parses CRLF delimiters split across chunks", async () => {
|
|
977
|
+
const sseData = JSON.stringify({
|
|
978
|
+
status_update: {
|
|
979
|
+
task_id: "t1",
|
|
980
|
+
context_id: "ctx-1",
|
|
981
|
+
status: { state: "TASK_STATE_WORKING" },
|
|
982
|
+
},
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
fetchMock.mockResolvedValue(
|
|
986
|
+
mockSSETextResponse([`data: ${sseData}\r`, "\n\r", "\n"]),
|
|
987
|
+
);
|
|
988
|
+
|
|
989
|
+
const events: A2AStreamEvent[] = [];
|
|
990
|
+
for await (const event of client.streamMessage(userMessage)) {
|
|
991
|
+
events.push(event);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
expect(events).toHaveLength(1);
|
|
995
|
+
expect(events[0]!.type).toBe("statusUpdate");
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
it("parses CR-delimited events split across chunks", async () => {
|
|
999
|
+
const sseData = JSON.stringify({
|
|
1000
|
+
status_update: {
|
|
1001
|
+
task_id: "t1",
|
|
1002
|
+
context_id: "ctx-1",
|
|
1003
|
+
status: { state: "TASK_STATE_WORKING" },
|
|
1004
|
+
},
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
fetchMock.mockResolvedValue(
|
|
1008
|
+
mockSSETextResponse([`data: ${sseData}\r`, "\r"]),
|
|
1009
|
+
);
|
|
1010
|
+
|
|
1011
|
+
const events: A2AStreamEvent[] = [];
|
|
1012
|
+
for await (const event of client.streamMessage(userMessage)) {
|
|
1013
|
+
events.push(event);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
expect(events).toHaveLength(1);
|
|
1017
|
+
expect(events[0]!.type).toBe("statusUpdate");
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
it("does not dispatch unterminated SSE events", async () => {
|
|
1021
|
+
const sseData = JSON.stringify({
|
|
1022
|
+
status_update: {
|
|
1023
|
+
task_id: "t1",
|
|
1024
|
+
context_id: "ctx-1",
|
|
1025
|
+
status: { state: "TASK_STATE_WORKING" },
|
|
1026
|
+
},
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
fetchMock.mockResolvedValue(mockSSETextResponse(`data: ${sseData}`));
|
|
1030
|
+
|
|
1031
|
+
const events: A2AStreamEvent[] = [];
|
|
1032
|
+
for await (const event of client.streamMessage(userMessage)) {
|
|
1033
|
+
events.push(event);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
expect(events).toHaveLength(0);
|
|
1037
|
+
});
|
|
1038
|
+
|
|
802
1039
|
it("parses SSE artifact update events", async () => {
|
|
803
1040
|
const sseData = JSON.stringify({
|
|
804
1041
|
artifact_update: {
|
|
@@ -887,6 +1124,63 @@ describe("A2AClient", () => {
|
|
|
887
1124
|
expect(events).toHaveLength(2);
|
|
888
1125
|
});
|
|
889
1126
|
|
|
1127
|
+
it("accepts parameterized event-stream content types", async () => {
|
|
1128
|
+
const sseData = JSON.stringify({
|
|
1129
|
+
status_update: {
|
|
1130
|
+
task_id: "t1",
|
|
1131
|
+
context_id: "ctx-1",
|
|
1132
|
+
status: { state: "TASK_STATE_COMPLETED" },
|
|
1133
|
+
},
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
fetchMock.mockResolvedValue(
|
|
1137
|
+
mockSSEResponse(
|
|
1138
|
+
[`data: ${sseData}`, "", ""],
|
|
1139
|
+
"text/event-stream; charset=utf-8",
|
|
1140
|
+
),
|
|
1141
|
+
);
|
|
1142
|
+
|
|
1143
|
+
const events: A2AStreamEvent[] = [];
|
|
1144
|
+
for await (const event of client.streamMessage(userMessage)) {
|
|
1145
|
+
events.push(event);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
expect(events).toHaveLength(1);
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
it("rejects successful responses that are not event streams", async () => {
|
|
1152
|
+
fetchMock.mockResolvedValue(
|
|
1153
|
+
mockSSETextResponse(
|
|
1154
|
+
"<html><body>Please sign in</body></html>",
|
|
1155
|
+
"text/html; charset=utf-8",
|
|
1156
|
+
),
|
|
1157
|
+
);
|
|
1158
|
+
|
|
1159
|
+
const consumeStream = async () => {
|
|
1160
|
+
for await (const event of client.streamMessage(userMessage)) {
|
|
1161
|
+
void event;
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
await expect(consumeStream()).rejects.toThrow(
|
|
1166
|
+
'Expected A2A stream response Content-Type "text/event-stream", received "text/html; charset=utf-8"',
|
|
1167
|
+
);
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
it("rejects task subscriptions without a content type", async () => {
|
|
1171
|
+
fetchMock.mockResolvedValue(mockSSETextResponse("", null));
|
|
1172
|
+
|
|
1173
|
+
const consumeStream = async () => {
|
|
1174
|
+
for await (const event of client.subscribeToTask("t1")) {
|
|
1175
|
+
void event;
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
|
|
1179
|
+
await expect(consumeStream()).rejects.toThrow(
|
|
1180
|
+
'Expected A2A stream response Content-Type "text/event-stream", received no Content-Type header',
|
|
1181
|
+
);
|
|
1182
|
+
});
|
|
1183
|
+
|
|
890
1184
|
it("normalizes 'content' array from v0.3 server response to 'parts' in SSE artifact update events", async () => {
|
|
891
1185
|
const sseData = JSON.stringify({
|
|
892
1186
|
artifact_update: {
|
|
@@ -1076,5 +1370,28 @@ describe("A2AClient", () => {
|
|
|
1076
1370
|
const [url] = fetchMock.mock.calls[0]!;
|
|
1077
1371
|
expect(url).toBe("https://agent.test/extendedAgentCard");
|
|
1078
1372
|
});
|
|
1373
|
+
|
|
1374
|
+
it.each([
|
|
1375
|
+
["an empty payload", {}],
|
|
1376
|
+
["a payload without a name", { version: "1.0" }],
|
|
1377
|
+
])("rejects %s", async (_name, body) => {
|
|
1378
|
+
fetchMock.mockResolvedValue(mockFetchResponse(body));
|
|
1379
|
+
|
|
1380
|
+
await expect(client.getExtendedAgentCard()).rejects.toThrow(
|
|
1381
|
+
"Invalid A2A agent card response: expected a valid agent card payload.",
|
|
1382
|
+
);
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
it("fills defaults for omitted fields", async () => {
|
|
1386
|
+
fetchMock.mockResolvedValue(
|
|
1387
|
+
mockFetchResponse({ name: "Extended Agent", skills: [{ id: "s" }] }),
|
|
1388
|
+
);
|
|
1389
|
+
|
|
1390
|
+
const card = await client.getExtendedAgentCard();
|
|
1391
|
+
|
|
1392
|
+
expect(card.name).toBe("Extended Agent");
|
|
1393
|
+
expect(card.supportedInterfaces).toEqual([]);
|
|
1394
|
+
expect(card.skills[0]).toMatchObject({ id: "s", tags: [] });
|
|
1395
|
+
});
|
|
1079
1396
|
});
|
|
1080
1397
|
});
|