@assistant-ui/react-a2a 0.2.20 → 0.2.22

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.
@@ -1,24 +1,10 @@
1
1
  "use client";
2
2
  import { A2AClient } from "./A2AClient.js";
3
3
  import { A2AThreadRuntimeCore } from "./A2AThreadRuntimeCore.js";
4
+ import { a2aExtras } from "./a2aExtras.js";
4
5
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
5
6
  import { useExternalStoreRuntime, useExternalStoreSharedOptions, useRuntimeAdapters } from "@assistant-ui/core/react";
6
- import { useAuiState } from "@assistant-ui/store";
7
7
  //#region src/useA2ARuntime.ts
8
- const symbolA2AExtras = Symbol("a2a-extras");
9
- const asA2AExtras = (extras) => {
10
- if (typeof extras !== "object" || extras == null || !(symbolA2AExtras in extras)) throw new Error("This hook can only be used inside a useA2ARuntime provider");
11
- return extras;
12
- };
13
- const useA2ATask = () => {
14
- return useAuiState((s) => asA2AExtras(s.thread.extras).task);
15
- };
16
- const useA2AArtifacts = () => {
17
- return useAuiState((s) => asA2AExtras(s.thread.extras).artifacts);
18
- };
19
- const useA2AAgentCard = () => {
20
- return useAuiState((s) => asA2AExtras(s.thread.extras).agentCard);
21
- };
22
8
  function useA2ARuntime(options) {
23
9
  const [_version, setVersion] = useState(0);
24
10
  const notifyUpdate = useCallback(() => setVersion((v) => v + 1), []);
@@ -93,12 +79,11 @@ function useA2ARuntime(options) {
93
79
  isLoading: core.isLoading,
94
80
  messages: core.getMessages(),
95
81
  isRunning: core.isRunning(),
96
- extras: {
97
- [symbolA2AExtras]: true,
82
+ extras: a2aExtras.provide({
98
83
  task: core.getTask(),
99
84
  artifacts: core.getArtifacts(),
100
85
  agentCard: core.getAgentCard()
101
- },
86
+ }),
102
87
  onNew: (message) => core.append(message),
103
88
  onEdit: (message) => core.edit(message),
104
89
  onReload: (parentId) => core.reload(parentId),
@@ -125,6 +110,6 @@ function useA2ARuntime(options) {
125
110
  return runtime;
126
111
  }
127
112
  //#endregion
128
- export { useA2AAgentCard, useA2AArtifacts, useA2ARuntime, useA2ATask };
113
+ export { useA2ARuntime };
129
114
 
130
115
  //# sourceMappingURL=useA2ARuntime.js.map
@@ -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 AttachmentAdapter,\n DictationAdapter,\n ExternalStoreAdapter,\n ExternalStoreSharedOptions,\n FeedbackAdapter,\n RealtimeVoiceAdapter,\n SpeechSynthesisAdapter,\n ThreadHistoryAdapter,\n ThreadMessage,\n} from \"@assistant-ui/core\";\nimport { useAuiState } from \"@assistant-ui/store\";\nimport { A2AClient } from \"./A2AClient\";\nimport type { A2AClientOptions } from \"./A2AClient\";\nimport { A2AThreadRuntimeCore } from \"./A2AThreadRuntimeCore\";\nimport type {\n A2AArtifact,\n A2AAgentCard,\n A2ASendMessageConfiguration,\n A2ATask,\n} from \"./types\";\n\n// --- Extras symbol for A2A-specific state ---\n\nconst symbolA2AExtras = Symbol(\"a2a-extras\");\n\ntype A2AExtras = {\n [symbolA2AExtras]: true;\n task: A2ATask | undefined;\n artifacts: readonly A2AArtifact[];\n agentCard: A2AAgentCard | undefined;\n};\n\nconst asA2AExtras = (extras: unknown): A2AExtras => {\n if (\n typeof extras !== \"object\" ||\n extras == null ||\n !(symbolA2AExtras in extras)\n )\n throw new Error(\n \"This hook can only be used inside a useA2ARuntime provider\",\n );\n return extras as A2AExtras;\n};\n\n// --- Public hooks for A2A state ---\n\nexport const useA2ATask = () => {\n return useAuiState((s) => asA2AExtras(s.thread.extras).task);\n};\n\nexport const useA2AArtifacts = () => {\n return useAuiState((s) => asA2AExtras(s.thread.extras).artifacts);\n};\n\nexport const useA2AAgentCard = () => {\n return useAuiState((s) => asA2AExtras(s.thread.extras).agentCard);\n};\n\n// --- Thread list adapter type ---\n\nexport type UseA2AThreadListAdapter = {\n threadId?: string;\n onSwitchToNewThread?: () => Promise<void> | void;\n onSwitchToThread?: (threadId: string) => Promise<{\n messages: readonly ThreadMessage[];\n }>;\n};\n\n// --- Options ---\n\nexport type UseA2ARuntimeOptions = ExternalStoreSharedOptions & {\n /** Pre-built A2A client instance. Provide this OR baseUrl. */\n client?: A2AClient;\n /** Base URL of the A2A server. Used to create a client if `client` is not provided. */\n baseUrl?: string;\n /** Optional path prefix for all API endpoints (e.g. \"/v1\"). Does not affect agent card discovery. Only used with baseUrl. */\n basePath?: string;\n /** Optional tenant ID for multi-tenant servers. Only used with baseUrl. */\n tenant?: string;\n /** Headers for the A2A client (only used with baseUrl). */\n headers?: A2AClientOptions[\"headers\"];\n /** A2A extension URIs to negotiate. Only used with baseUrl. */\n extensions?: string[];\n /** Extra fetch options (e.g. `{ credentials: 'include' }`). Only used with `baseUrl`. */\n fetchOptions?: A2AClientOptions[\"fetchOptions\"];\n\n /** Initial context ID for the conversation. */\n contextId?: string;\n /** Default send message configuration. */\n configuration?: A2ASendMessageConfiguration;\n\n /** Called when an error occurs. */\n onError?: (error: Error) => void;\n /** Called when a run is cancelled. */\n onCancel?: () => void;\n /** Called when an artifact is fully received (lastChunk). */\n onArtifactComplete?: (artifact: import(\"./types\").A2AArtifact) => void;\n\n adapters?: {\n attachments?: AttachmentAdapter;\n speech?: SpeechSynthesisAdapter;\n dictation?: DictationAdapter;\n voice?: RealtimeVoiceAdapter;\n feedback?: FeedbackAdapter;\n history?: ThreadHistoryAdapter;\n threadList?: UseA2AThreadListAdapter;\n };\n};\n\n// --- Main hook ---\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: {\n [symbolA2AExtras]: true,\n task: core.getTask(),\n artifacts: core.getArtifacts(),\n agentCard: core.getAgentCard(),\n } satisfies A2AExtras,\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 } 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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/react-a2a",
3
- "version": "0.2.20",
3
+ "version": "0.2.22",
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.17",
33
- "@assistant-ui/store": "^0.2.18"
32
+ "@assistant-ui/core": "^0.2.21",
33
+ "@assistant-ui/store": "^0.2.20",
34
+ "assistant-stream": "^0.3.26"
34
35
  },
35
36
  "peerDependencies": {
36
37
  "@types/react": "*",
@@ -44,8 +45,8 @@
44
45
  "devDependencies": {
45
46
  "@types/react": "^19.2.17",
46
47
  "react": "^19.2.7",
47
- "vitest": "^4.1.8",
48
- "@assistant-ui/x-buildutils": "0.0.15"
48
+ "vitest": "^4.1.10",
49
+ "@assistant-ui/x-buildutils": "0.0.18"
49
50
  },
50
51
  "publishConfig": {
51
52
  "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",
@@ -799,6 +812,111 @@ describe("A2AClient", () => {
799
812
  expect(evt.event.status.message?.role).toBe("agent");
800
813
  });
801
814
 
815
+ it("parses CRLF-delimited SSE events", async () => {
816
+ const sseData = JSON.stringify({
817
+ status_update: {
818
+ task_id: "t1",
819
+ context_id: "ctx-1",
820
+ status: { state: "TASK_STATE_WORKING" },
821
+ },
822
+ });
823
+
824
+ fetchMock.mockResolvedValue(
825
+ mockSSETextResponse(`data: ${sseData}\r\n\r\n`),
826
+ );
827
+
828
+ const events: A2AStreamEvent[] = [];
829
+ for await (const event of client.streamMessage(userMessage)) {
830
+ events.push(event);
831
+ }
832
+
833
+ expect(events).toHaveLength(1);
834
+ expect(events[0]!.type).toBe("statusUpdate");
835
+ });
836
+
837
+ it("parses CR-delimited SSE events", async () => {
838
+ const sseData = JSON.stringify({
839
+ status_update: {
840
+ task_id: "t1",
841
+ context_id: "ctx-1",
842
+ status: { state: "TASK_STATE_WORKING" },
843
+ },
844
+ });
845
+
846
+ fetchMock.mockResolvedValue(mockSSETextResponse(`data: ${sseData}\r\r`));
847
+
848
+ const events: A2AStreamEvent[] = [];
849
+ for await (const event of client.streamMessage(userMessage)) {
850
+ events.push(event);
851
+ }
852
+
853
+ expect(events).toHaveLength(1);
854
+ expect(events[0]!.type).toBe("statusUpdate");
855
+ });
856
+
857
+ it("parses CRLF delimiters split across chunks", async () => {
858
+ const sseData = JSON.stringify({
859
+ status_update: {
860
+ task_id: "t1",
861
+ context_id: "ctx-1",
862
+ status: { state: "TASK_STATE_WORKING" },
863
+ },
864
+ });
865
+
866
+ fetchMock.mockResolvedValue(
867
+ mockSSETextResponse([`data: ${sseData}\r`, "\n\r", "\n"]),
868
+ );
869
+
870
+ const events: A2AStreamEvent[] = [];
871
+ for await (const event of client.streamMessage(userMessage)) {
872
+ events.push(event);
873
+ }
874
+
875
+ expect(events).toHaveLength(1);
876
+ expect(events[0]!.type).toBe("statusUpdate");
877
+ });
878
+
879
+ it("parses CR-delimited events split across chunks", async () => {
880
+ const sseData = JSON.stringify({
881
+ status_update: {
882
+ task_id: "t1",
883
+ context_id: "ctx-1",
884
+ status: { state: "TASK_STATE_WORKING" },
885
+ },
886
+ });
887
+
888
+ fetchMock.mockResolvedValue(
889
+ mockSSETextResponse([`data: ${sseData}\r`, "\r"]),
890
+ );
891
+
892
+ const events: A2AStreamEvent[] = [];
893
+ for await (const event of client.streamMessage(userMessage)) {
894
+ events.push(event);
895
+ }
896
+
897
+ expect(events).toHaveLength(1);
898
+ expect(events[0]!.type).toBe("statusUpdate");
899
+ });
900
+
901
+ it("does not dispatch unterminated SSE events", async () => {
902
+ const sseData = JSON.stringify({
903
+ status_update: {
904
+ task_id: "t1",
905
+ context_id: "ctx-1",
906
+ status: { state: "TASK_STATE_WORKING" },
907
+ },
908
+ });
909
+
910
+ fetchMock.mockResolvedValue(mockSSETextResponse(`data: ${sseData}`));
911
+
912
+ const events: A2AStreamEvent[] = [];
913
+ for await (const event of client.streamMessage(userMessage)) {
914
+ events.push(event);
915
+ }
916
+
917
+ expect(events).toHaveLength(0);
918
+ });
919
+
802
920
  it("parses SSE artifact update events", async () => {
803
921
  const sseData = JSON.stringify({
804
922
  artifact_update: {
@@ -887,6 +1005,63 @@ describe("A2AClient", () => {
887
1005
  expect(events).toHaveLength(2);
888
1006
  });
889
1007
 
1008
+ it("accepts parameterized event-stream content types", async () => {
1009
+ const sseData = JSON.stringify({
1010
+ status_update: {
1011
+ task_id: "t1",
1012
+ context_id: "ctx-1",
1013
+ status: { state: "TASK_STATE_COMPLETED" },
1014
+ },
1015
+ });
1016
+
1017
+ fetchMock.mockResolvedValue(
1018
+ mockSSEResponse(
1019
+ [`data: ${sseData}`, "", ""],
1020
+ "text/event-stream; charset=utf-8",
1021
+ ),
1022
+ );
1023
+
1024
+ const events: A2AStreamEvent[] = [];
1025
+ for await (const event of client.streamMessage(userMessage)) {
1026
+ events.push(event);
1027
+ }
1028
+
1029
+ expect(events).toHaveLength(1);
1030
+ });
1031
+
1032
+ it("rejects successful responses that are not event streams", async () => {
1033
+ fetchMock.mockResolvedValue(
1034
+ mockSSETextResponse(
1035
+ "<html><body>Please sign in</body></html>",
1036
+ "text/html; charset=utf-8",
1037
+ ),
1038
+ );
1039
+
1040
+ const consumeStream = async () => {
1041
+ for await (const event of client.streamMessage(userMessage)) {
1042
+ void event;
1043
+ }
1044
+ };
1045
+
1046
+ await expect(consumeStream()).rejects.toThrow(
1047
+ 'Expected A2A stream response Content-Type "text/event-stream", received "text/html; charset=utf-8"',
1048
+ );
1049
+ });
1050
+
1051
+ it("rejects task subscriptions without a content type", async () => {
1052
+ fetchMock.mockResolvedValue(mockSSETextResponse("", null));
1053
+
1054
+ const consumeStream = async () => {
1055
+ for await (const event of client.subscribeToTask("t1")) {
1056
+ void event;
1057
+ }
1058
+ };
1059
+
1060
+ await expect(consumeStream()).rejects.toThrow(
1061
+ 'Expected A2A stream response Content-Type "text/event-stream", received no Content-Type header',
1062
+ );
1063
+ });
1064
+
890
1065
  it("normalizes 'content' array from v0.3 server response to 'parts' in SSE artifact update events", async () => {
891
1066
  const sseData = JSON.stringify({
892
1067
  artifact_update: {
package/src/A2AClient.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { SSEEventDecoder, type SSEEvent } from "assistant-stream/utils";
1
2
  import type {
2
3
  A2AAgentCard,
3
4
  A2AErrorInfo,
@@ -493,57 +494,59 @@ export class A2AClient {
493
494
  // --- SSE Parsing ---
494
495
 
495
496
  private async *parseSSE(response: Response): AsyncGenerator<A2AStreamEvent> {
497
+ const contentType = response.headers.get("Content-Type");
498
+ const mediaType = contentType?.split(";", 1)[0]?.trim().toLowerCase();
499
+ if (mediaType !== "text/event-stream") {
500
+ const received = contentType
501
+ ? `"${contentType}"`
502
+ : "no Content-Type header";
503
+ throw new Error(
504
+ `Expected A2A stream response Content-Type "text/event-stream", received ${received}`,
505
+ );
506
+ }
507
+
496
508
  const reader = response.body?.getReader();
497
509
  if (!reader) throw new Error("No response body");
498
510
 
499
511
  const decoder = new TextDecoder();
500
- let buffer = "";
512
+ const sseDecoder = new SSEEventDecoder();
513
+
514
+ const readEvent = (event: SSEEvent): A2AStreamEvent | null => {
515
+ try {
516
+ let parsed = JSON.parse(event.data);
517
+
518
+ if (
519
+ parsed &&
520
+ typeof parsed === "object" &&
521
+ "jsonrpc" in parsed &&
522
+ "result" in parsed
523
+ ) {
524
+ parsed = parsed.result;
525
+ }
526
+
527
+ const normalized = normalizeKeys(parsed) as Record<string, unknown>;
528
+ return discriminateStreamResponse(normalized);
529
+ } catch {
530
+ return null;
531
+ }
532
+ };
501
533
 
502
534
  try {
503
535
  while (true) {
504
536
  const { done, value } = await reader.read();
505
- if (done) break;
506
-
507
- buffer += decoder.decode(value, { stream: true });
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.
537
+ if (done) {
538
+ for (const event of sseDecoder.push(decoder.decode())) {
539
+ const parsed = readEvent(event);
540
+ if (parsed) yield parsed;
523
541
  }
542
+ break;
543
+ }
524
544
 
525
- if (dataLines.length === 0) continue;
526
-
527
- try {
528
- let parsed = JSON.parse(dataLines.join("\n"));
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");
545
+ for (const event of sseDecoder.push(
546
+ decoder.decode(value, { stream: true }),
547
+ )) {
548
+ const parsed = readEvent(event);
549
+ if (parsed) yield parsed;
547
550
  }
548
551
  }
549
552
  } finally {
@@ -0,0 +1,4 @@
1
+ import { createRuntimeExtras } from "@assistant-ui/core/internal";
2
+ import type { A2AExtras } from "./types";
3
+
4
+ export const a2aExtras = createRuntimeExtras<A2AExtras>("useA2ARuntime");
package/src/hooks.ts ADDED
@@ -0,0 +1,12 @@
1
+ "use client";
2
+
3
+ import { a2aExtras } from "./a2aExtras";
4
+
5
+ /** The current A2A task, if a run is in progress. */
6
+ export const useA2ATask = () => a2aExtras.use((e) => e.task);
7
+
8
+ /** The artifacts accumulated for the current A2A run. */
9
+ export const useA2AArtifacts = () => a2aExtras.use((e) => e.artifacts);
10
+
11
+ /** The discovered A2A agent card. */
12
+ export const useA2AAgentCard = () => a2aExtras.use((e) => e.agentCard);
package/src/index.ts CHANGED
@@ -1,14 +1,7 @@
1
1
  // Main hook
2
- export {
3
- useA2ARuntime,
4
- useA2ATask,
5
- useA2AArtifacts,
6
- useA2AAgentCard,
7
- } from "./useA2ARuntime";
8
- export type {
9
- UseA2ARuntimeOptions,
10
- UseA2AThreadListAdapter,
11
- } from "./useA2ARuntime";
2
+ export { useA2ARuntime } from "./useA2ARuntime";
3
+ export { useA2ATask, useA2AArtifacts, useA2AAgentCard } from "./hooks";
4
+ export type { UseA2ARuntimeOptions, UseA2AThreadListAdapter } from "./types";
12
5
 
13
6
  // Client
14
7
  export { A2AClient, A2AError } from "./A2AClient";
package/src/types.ts CHANGED
@@ -2,6 +2,18 @@
2
2
  // Enum values use lowercase internally; normalized from ProtoJSON SCREAMING_SNAKE_CASE on read.
3
3
  // Wire format uses ROLE_USER/ROLE_AGENT and TASK_STATE_* per ADR-001.
4
4
 
5
+ import type {
6
+ AttachmentAdapter,
7
+ DictationAdapter,
8
+ ExternalStoreSharedOptions,
9
+ FeedbackAdapter,
10
+ RealtimeVoiceAdapter,
11
+ SpeechSynthesisAdapter,
12
+ ThreadHistoryAdapter,
13
+ ThreadMessage,
14
+ } from "@assistant-ui/core";
15
+ import type { A2AClient, A2AClientOptions } from "./A2AClient";
16
+
5
17
  export const A2A_PROTOCOL_VERSION = "1.0";
6
18
 
7
19
  export type A2ARole = "unspecified" | "user" | "agent";
@@ -281,3 +293,57 @@ export type A2AAgentCard = {
281
293
  signatures?: A2AAgentCardSignature[] | undefined;
282
294
  iconUrl?: string | undefined;
283
295
  };
296
+
297
+ /** Private state `useA2ARuntime` exposes through `thread.extras`. */
298
+ export type A2AExtras = {
299
+ task: A2ATask | undefined;
300
+ artifacts: readonly A2AArtifact[];
301
+ agentCard: A2AAgentCard | undefined;
302
+ };
303
+
304
+ export type UseA2AThreadListAdapter = {
305
+ threadId?: string;
306
+ onSwitchToNewThread?: () => Promise<void> | void;
307
+ onSwitchToThread?: (threadId: string) => Promise<{
308
+ messages: readonly ThreadMessage[];
309
+ }>;
310
+ };
311
+
312
+ export type UseA2ARuntimeOptions = ExternalStoreSharedOptions & {
313
+ /** Pre-built A2A client instance. Provide this OR baseUrl. */
314
+ client?: A2AClient;
315
+ /** Base URL of the A2A server. Used to create a client if `client` is not provided. */
316
+ baseUrl?: string;
317
+ /** Optional path prefix for all API endpoints (e.g. "/v1"). Does not affect agent card discovery. Only used with baseUrl. */
318
+ basePath?: string;
319
+ /** Optional tenant ID for multi-tenant servers. Only used with baseUrl. */
320
+ tenant?: string;
321
+ /** Headers for the A2A client (only used with baseUrl). */
322
+ headers?: A2AClientOptions["headers"];
323
+ /** A2A extension URIs to negotiate. Only used with baseUrl. */
324
+ extensions?: string[];
325
+ /** Extra fetch options (e.g. `{ credentials: 'include' }`). Only used with `baseUrl`. */
326
+ fetchOptions?: A2AClientOptions["fetchOptions"];
327
+
328
+ /** Initial context ID for the conversation. */
329
+ contextId?: string;
330
+ /** Default send message configuration. */
331
+ configuration?: A2ASendMessageConfiguration;
332
+
333
+ /** Called when an error occurs. */
334
+ onError?: (error: Error) => void;
335
+ /** Called when a run is cancelled. */
336
+ onCancel?: () => void;
337
+ /** Called when an artifact is fully received (lastChunk). */
338
+ onArtifactComplete?: (artifact: A2AArtifact) => void;
339
+
340
+ adapters?: {
341
+ attachments?: AttachmentAdapter;
342
+ speech?: SpeechSynthesisAdapter;
343
+ dictation?: DictationAdapter;
344
+ voice?: RealtimeVoiceAdapter;
345
+ feedback?: FeedbackAdapter;
346
+ history?: ThreadHistoryAdapter;
347
+ threadList?: UseA2AThreadListAdapter;
348
+ };
349
+ };