@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/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; /** @deprecated */
161
- implicit?: A2AImplicitOAuthFlow | undefined; /** @deprecated */
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. */client?: A2AClient; /** Base URL of the A2A server. Used to create a client if `client` is not provided. */
255
- baseUrl?: string; /** Optional path prefix for all API endpoints (e.g. "/v1"). Does not affect agent card discovery. Only used with baseUrl. */
256
- basePath?: string; /** Optional tenant ID for multi-tenant servers. Only used with baseUrl. */
257
- tenant?: string; /** Headers for the A2A client (only used with baseUrl). */
258
- headers?: A2AClientOptions["headers"]; /** A2A extension URIs to negotiate. Only used with baseUrl. */
259
- extensions?: string[]; /** Extra fetch options (e.g. `{ credentials: 'include' }`). Only used with `baseUrl`. */
260
- fetchOptions?: A2AClientOptions["fetchOptions"]; /** Initial context ID for the conversation. */
261
- contextId?: string; /** Default send message configuration. */
262
- configuration?: A2ASendMessageConfiguration; /** Called when an error occurs. */
263
- onError?: (error: Error) => void; /** Called when a run is cancelled. */
264
- onCancel?: () => void; /** Called when an artifact is fully received (lastChunk). */
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;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":"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"}
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"}
@@ -1,6 +1,5 @@
1
1
  import { UseA2ARuntimeOptions } from "./types.js";
2
2
  import { AssistantRuntime } from "@assistant-ui/core";
3
-
4
3
  //#region src/useA2ARuntime.d.ts
5
4
  declare function useA2ARuntime(options: UseA2ARuntimeOptions): AssistantRuntime;
6
5
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"useA2ARuntime.d.ts","names":[],"sources":["../src/useA2ARuntime.ts"],"mappings":";;;;iBAmBgB,aAAA,CAAc,OAAA,EAAS,oBAAA,GAAuB,gBAAgB"}
1
+ {"version":3,"file":"useA2ARuntime.d.ts","names":[],"sources":["../src/useA2ARuntime.ts"],"mappings":";;;iBAoCgB,cAAc,SAAS,uBAAuB"}
@@ -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 clientRef = useRef(null);
15
- if (!clientRef.current) if (options.client) clientRef.current = options.client;
16
- else if (options.baseUrl) clientRef.current = new A2AClient({
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
- else throw new Error("useA2ARuntime requires either `client` or `baseUrl`");
25
- const client = clientRef.current;
26
- const coreRef = useRef(null);
27
- if (!coreRef.current) coreRef.current = new A2AThreadRuntimeCore({
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
- messages: core.getMessages(),
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 // Create or reuse client\n const clientRef = useRef<A2AClient | null>(null);\n if (!clientRef.current) {\n if (options.client) {\n clientRef.current = options.client;\n } else if (options.baseUrl) {\n clientRef.current = new A2AClient({\n baseUrl: options.baseUrl,\n basePath: options.basePath,\n tenant: options.tenant,\n headers: options.headers,\n extensions: options.extensions,\n fetchOptions: options.fetchOptions,\n });\n } else {\n throw new Error(\"useA2ARuntime requires either `client` or `baseUrl`\");\n }\n }\n const client = clientRef.current;\n\n // Create or reuse core\n const coreRef = useRef<A2AThreadRuntimeCore | null>(null);\n if (!coreRef.current) {\n coreRef.current = new A2AThreadRuntimeCore({\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 notifyUpdate,\n });\n }\n\n const core = coreRef.current;\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 messages: core.getMessages(),\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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"}
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.21",
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.19",
33
- "@assistant-ui/store": "^0.2.19"
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
- "react": "^19.2.7",
47
- "vitest": "^4.1.9",
48
- "@assistant-ui/x-buildutils": "0.0.16"
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",
@@ -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 mockSSEResponse(lines: string[]): Response {
18
- const text = lines.join("\n");
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 = [encoder.encode(text)];
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: new Headers({ "content-type": "text/event-stream" }),
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
  });