@assistant-ui/react-a2a 0.2.36 → 0.2.38

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/react-a2a",
3
- "version": "0.2.36",
3
+ "version": "0.2.38",
4
4
  "description": "A2A (Agent-to-Agent) v1.0 protocol adapter for assistant-ui",
5
5
  "keywords": [
6
6
  "a2a",
@@ -29,9 +29,10 @@
29
29
  ],
30
30
  "sideEffects": false,
31
31
  "dependencies": {
32
- "@assistant-ui/core": "^0.3.20",
33
- "@assistant-ui/store": "^0.3.14",
34
- "assistant-stream": "^0.3.44"
32
+ "@assistant-ui/core": "^0.3.21",
33
+ "@assistant-ui/react-generative-ui": "^0.0.21",
34
+ "@assistant-ui/store": "^0.3.15",
35
+ "assistant-stream": "^0.3.45"
35
36
  },
36
37
  "peerDependencies": {
37
38
  "@types/react": "*",
@@ -43,7 +44,7 @@
43
44
  }
44
45
  },
45
46
  "devDependencies": {
46
- "@assistant-ui/x-buildutils": "0.0.28",
47
+ "@assistant-ui/x-buildutils": "0.0.29",
47
48
  "@testing-library/dom": "^10.4.2",
48
49
  "@testing-library/react": "^16.3.3",
49
50
  "@types/react": "^19.3.0",
@@ -160,6 +160,47 @@ describe("A2AClient", () => {
160
160
  const [, init] = fetchMock.mock.calls[0]!;
161
161
  expect(init.headers.Authorization).toBe("Bearer tok123");
162
162
  });
163
+
164
+ it("aborts a request while dynamic headers are pending", async () => {
165
+ const dynamicClient = new A2AClient({
166
+ baseUrl: "https://agent.test",
167
+ headers: () => new Promise<Record<string, string>>(() => {}),
168
+ });
169
+ const controller = new AbortController();
170
+ const reason = new Error("cancelled");
171
+
172
+ const request = dynamicClient.sendMessage(
173
+ userMessage,
174
+ undefined,
175
+ undefined,
176
+ controller.signal,
177
+ );
178
+ controller.abort(reason);
179
+
180
+ await expect(request).rejects.toBe(reason);
181
+ expect(fetchMock).not.toHaveBeenCalled();
182
+ });
183
+
184
+ it("aborts a stream while dynamic headers are pending", async () => {
185
+ const dynamicClient = new A2AClient({
186
+ baseUrl: "https://agent.test",
187
+ headers: () => new Promise<Record<string, string>>(() => {}),
188
+ });
189
+ const controller = new AbortController();
190
+ const reason = new Error("cancelled");
191
+ const stream = dynamicClient.streamMessage(
192
+ userMessage,
193
+ undefined,
194
+ undefined,
195
+ controller.signal,
196
+ );
197
+
198
+ const next = stream.next();
199
+ controller.abort(reason);
200
+
201
+ await expect(next).rejects.toBe(reason);
202
+ expect(fetchMock).not.toHaveBeenCalled();
203
+ });
163
204
  });
164
205
 
165
206
  describe("fetchOptions", () => {
package/src/A2AClient.ts CHANGED
@@ -680,6 +680,49 @@ function signalInit(signal?: AbortSignal): RequestInit {
680
680
  return signal ? { signal } : {};
681
681
  }
682
682
 
683
+ const getAbortReason = (signal: AbortSignal): unknown => {
684
+ if (signal.reason !== undefined) return signal.reason;
685
+ const error = new Error("The operation was aborted");
686
+ error.name = "AbortError";
687
+ return error;
688
+ };
689
+
690
+ const raceWithAbortSignal = <T>(
691
+ signal: AbortSignal | undefined,
692
+ operation: () => T | PromiseLike<T>,
693
+ ): Promise<T> => {
694
+ if (!signal) return Promise.resolve().then(operation);
695
+ if (signal.aborted) return Promise.reject(getAbortReason(signal));
696
+
697
+ return new Promise<T>((resolve, reject) => {
698
+ let settled = false;
699
+ const cleanup = () => signal.removeEventListener("abort", handleAbort);
700
+ const resolveOnce = (value: T) => {
701
+ if (settled) return;
702
+ settled = true;
703
+ cleanup();
704
+ resolve(value);
705
+ };
706
+ const rejectOnce = (error: unknown) => {
707
+ if (settled) return;
708
+ settled = true;
709
+ cleanup();
710
+ reject(error);
711
+ };
712
+ const handleAbort = () => rejectOnce(getAbortReason(signal));
713
+
714
+ signal.addEventListener("abort", handleAbort, { once: true });
715
+ let result: T | PromiseLike<T>;
716
+ try {
717
+ result = operation();
718
+ } catch (error) {
719
+ rejectOnce(error);
720
+ return;
721
+ }
722
+ Promise.resolve(result).then(resolveOnce, rejectOnce);
723
+ });
724
+ };
725
+
683
726
  const SKIPPED_FRAME_SNIPPET_LENGTH = 120;
684
727
 
685
728
  function describeSkippedFrame(data: string, reason: string): string {
@@ -728,10 +771,11 @@ export class A2AClient {
728
771
 
729
772
  private async getHeaders(
730
773
  includeContentType = true,
774
+ signal?: AbortSignal,
731
775
  ): Promise<Record<string, string>> {
732
776
  const custom =
733
777
  typeof this.headersFn === "function"
734
- ? await this.headersFn()
778
+ ? await raceWithAbortSignal(signal, this.headersFn)
735
779
  : this.headersFn;
736
780
  const headers: Record<string, string> = {
737
781
  Accept: "application/a2a+json, application/json",
@@ -777,7 +821,7 @@ export class A2AClient {
777
821
  options: RequestInit = {},
778
822
  ): Promise<T> {
779
823
  const isGet = !options.method || options.method.toUpperCase() === "GET";
780
- const headers = await this.getHeaders(!isGet);
824
+ const headers = await this.getHeaders(!isGet, options.signal ?? undefined);
781
825
  const response = await fetch(`${this.baseUrl}${path}`, {
782
826
  ...this.fetchOptions,
783
827
  ...options,
@@ -809,7 +853,7 @@ export class A2AClient {
809
853
  // --- Agent Card ---
810
854
 
811
855
  async getAgentCard(signal?: AbortSignal): Promise<A2AAgentCard> {
812
- const headers = await this.getHeaders(false); // GET: no Content-Type
856
+ const headers = await this.getHeaders(false, signal); // GET: no Content-Type
813
857
  const url = `${this.baseUrl}/.well-known/agent-card.json`;
814
858
  const response = await fetch(url, {
815
859
  ...this.fetchOptions,
@@ -863,7 +907,7 @@ export class A2AClient {
863
907
  metadata?: Record<string, unknown>,
864
908
  signal?: AbortSignal,
865
909
  ): AsyncGenerator<A2AStreamEvent> {
866
- const headers = await this.getHeaders(true);
910
+ const headers = await this.getHeaders(true, signal);
867
911
  headers.Accept = "text/event-stream";
868
912
 
869
913
  const body: Record<string, unknown> = {
@@ -954,7 +998,7 @@ export class A2AClient {
954
998
  taskId: string,
955
999
  signal?: AbortSignal,
956
1000
  ): AsyncGenerator<A2AStreamEvent> {
957
- const headers = await this.getHeaders(false); // GET: no Content-Type
1001
+ const headers = await this.getHeaders(false, signal); // GET: no Content-Type
958
1002
  headers.Accept = "text/event-stream";
959
1003
 
960
1004
  const response = await fetch(
@@ -1032,7 +1076,7 @@ export class A2AClient {
1032
1076
  signal?: AbortSignal,
1033
1077
  ): Promise<void> {
1034
1078
  const isGet = false;
1035
- const headers = await this.getHeaders(!isGet);
1079
+ const headers = await this.getHeaders(!isGet, signal);
1036
1080
  const response = await fetch(
1037
1081
  `${this.baseUrl}${this.getBasePath()}/tasks/${encodeURIComponent(taskId)}/pushNotificationConfigs/${encodeURIComponent(configId)}`,
1038
1082
  {