@agent-native/core 0.84.20 → 0.84.21

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.
Files changed (109) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +8 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/a2a/caller-auth.ts +37 -8
  5. package/corpus/core/src/a2a/client.ts +178 -67
  6. package/corpus/core/src/agent/production-agent.ts +19 -3
  7. package/corpus/core/src/agent/types.ts +1 -1
  8. package/corpus/core/src/cli/pr-visual-recap-workflow.ts +1 -1
  9. package/corpus/core/src/cli/recap.ts +43 -34
  10. package/corpus/core/src/client/PoweredByBadge.tsx +70 -17
  11. package/corpus/core/src/client/agent-chat-adapter.ts +5 -1
  12. package/corpus/core/src/client/i18n.tsx +10 -5
  13. package/corpus/core/src/client/sse-event-processor.ts +147 -5
  14. package/corpus/core/src/file-upload/builder.ts +35 -14
  15. package/corpus/core/src/file-upload/types.ts +3 -0
  16. package/corpus/core/src/integrations/a2a-continuation-processor.ts +49 -24
  17. package/corpus/core/src/mcp/builtin-tools.ts +3 -0
  18. package/corpus/core/src/mcp/org-directory.ts +66 -20
  19. package/corpus/core/src/scripts/call-agent.ts +7 -2
  20. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +161 -12
  21. package/corpus/templates/analytics/changelog/2026-07-01-session-replays-load-large-production-recordings-from-scoped.md +6 -0
  22. package/corpus/templates/calendar/app/pages/BookingPage.tsx +14 -8
  23. package/corpus/templates/calendar/changelog/2026-07-01-meeting-invite-pages-have-cleaner-branding-a-black-dark-mo.md +6 -0
  24. package/corpus/templates/clips/actions/create-recording.ts +7 -2
  25. package/corpus/templates/clips/actions/finalize-recording.ts +11 -0
  26. package/corpus/templates/clips/actions/get-recording-player-data.ts +4 -2
  27. package/corpus/templates/clips/actions/import-loom-recording.ts +1 -0
  28. package/corpus/templates/clips/actions/lib/loom-video.ts +16 -0
  29. package/corpus/templates/clips/app/components/player/scrubber.tsx +2 -2
  30. package/corpus/templates/clips/app/components/recorder/countdown-overlay.tsx +4 -3
  31. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +39 -12
  32. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +65 -1
  33. package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +1 -22
  34. package/corpus/templates/clips/app/routes/record.tsx +48 -8
  35. package/corpus/templates/clips/changelog/2026-07-01-clip-uploads-and-loom-imports-finish-sooner-by-skipping-the-.md +6 -0
  36. package/corpus/templates/clips/changelog/2026-07-01-clip-uploads-and-playback-recover-more-reliably-from-storage.md +6 -0
  37. package/corpus/templates/clips/changelog/2026-07-01-desktop-recording-avoids-unsupported-browser-tab-capture.md +6 -0
  38. package/corpus/templates/clips/changelog/2026-07-01-embedded-player-progress-is-easier-to-see-on-dark-videos.md +6 -0
  39. package/corpus/templates/clips/changelog/2026-07-01-loom-imports-show-clear-guidance-when-loom-does-not-provide-a-downloadable-video.md +6 -0
  40. package/corpus/templates/clips/changelog/2026-07-01-preserve-in-flight-streaming-uploads-when-the-streaming-kill.md +6 -0
  41. package/corpus/templates/clips/changelog/2026-07-01-recording-controls-are-cleaner-and-the-countdown-uses-shadow.md +6 -0
  42. package/corpus/templates/clips/changelog/2026-07-01-recordings-now-recover-when-final-upload-status-gets-lost.md +6 -0
  43. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  44. package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +7 -224
  45. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +64 -6
  46. package/corpus/templates/clips/desktop/src/lib/recorder.ts +87 -5
  47. package/corpus/templates/clips/server/lib/streaming-upload-mode.ts +7 -0
  48. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +14 -1
  49. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +4 -1
  50. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +96 -1
  51. package/corpus/templates/clips/shared/finalize-recovery.ts +242 -0
  52. package/corpus/templates/design/changelog/2026-07-01-design-recovers-sooner-when-an-agent-gets-stuck-preparing-a-.md +6 -0
  53. package/corpus/templates/plan/app/global.css +1 -1
  54. package/dist/a2a/caller-auth.d.ts +1 -0
  55. package/dist/a2a/caller-auth.d.ts.map +1 -1
  56. package/dist/a2a/caller-auth.js +34 -8
  57. package/dist/a2a/caller-auth.js.map +1 -1
  58. package/dist/a2a/client.d.ts +4 -0
  59. package/dist/a2a/client.d.ts.map +1 -1
  60. package/dist/a2a/client.js +148 -58
  61. package/dist/a2a/client.js.map +1 -1
  62. package/dist/agent/production-agent.d.ts.map +1 -1
  63. package/dist/agent/production-agent.js +19 -4
  64. package/dist/agent/production-agent.js.map +1 -1
  65. package/dist/agent/types.d.ts +1 -0
  66. package/dist/agent/types.d.ts.map +1 -1
  67. package/dist/agent/types.js.map +1 -1
  68. package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
  69. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  70. package/dist/cli/pr-visual-recap-workflow.js +1 -1
  71. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  72. package/dist/cli/recap.d.ts +3 -3
  73. package/dist/cli/recap.d.ts.map +1 -1
  74. package/dist/cli/recap.js +41 -34
  75. package/dist/cli/recap.js.map +1 -1
  76. package/dist/client/PoweredByBadge.d.ts +9 -3
  77. package/dist/client/PoweredByBadge.d.ts.map +1 -1
  78. package/dist/client/PoweredByBadge.js +47 -15
  79. package/dist/client/PoweredByBadge.js.map +1 -1
  80. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  81. package/dist/client/agent-chat-adapter.js +5 -1
  82. package/dist/client/agent-chat-adapter.js.map +1 -1
  83. package/dist/client/i18n.d.ts +1 -1
  84. package/dist/client/i18n.d.ts.map +1 -1
  85. package/dist/client/i18n.js +6 -2
  86. package/dist/client/i18n.js.map +1 -1
  87. package/dist/client/sse-event-processor.d.ts +2 -0
  88. package/dist/client/sse-event-processor.d.ts.map +1 -1
  89. package/dist/client/sse-event-processor.js +121 -5
  90. package/dist/client/sse-event-processor.js.map +1 -1
  91. package/dist/collab/routes.d.ts +1 -1
  92. package/dist/file-upload/builder.d.ts.map +1 -1
  93. package/dist/file-upload/builder.js +22 -8
  94. package/dist/file-upload/builder.js.map +1 -1
  95. package/dist/file-upload/types.d.ts +5 -1
  96. package/dist/file-upload/types.d.ts.map +1 -1
  97. package/dist/file-upload/types.js.map +1 -1
  98. package/dist/integrations/a2a-continuation-processor.js +43 -18
  99. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  100. package/dist/mcp/builtin-tools.d.ts.map +1 -1
  101. package/dist/mcp/builtin-tools.js +3 -0
  102. package/dist/mcp/builtin-tools.js.map +1 -1
  103. package/dist/mcp/org-directory.d.ts.map +1 -1
  104. package/dist/mcp/org-directory.js +58 -20
  105. package/dist/mcp/org-directory.js.map +1 -1
  106. package/dist/scripts/call-agent.d.ts.map +1 -1
  107. package/dist/scripts/call-agent.js +2 -2
  108. package/dist/scripts/call-agent.js.map +1 -1
  109. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 2043
31
- - template files: 4928
31
+ - template files: 4941
@@ -1,5 +1,13 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.84.21
4
+
5
+ ### Patch Changes
6
+
7
+ - f883173: Refresh public booking branding with a plain Agent Native logo, updated open-source copy, and a ghost icon language picker for public pages.
8
+ - f883173: Fix PR visual recap gating and comments so nested AGENTS.md files no longer count as recap-control files, trusted write actors can recap control-file edits, stale success bodies are replaced by skipped comments, and screenshot capture no longer waits for network-idle.
9
+ - f883173: Offer Claude Opus 4.8 in the managed Builder gateway model picker, keep action-preparation recovery progress-aware for large streamed tool inputs, retry A2A auth with org-scoped bearer fallbacks in mixed deployments, and allow Builder uploads to skip waiting on server-side compression when callers can process asynchronously.
10
+
3
11
  ## 0.84.20
4
12
 
5
13
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.84.20",
3
+ "version": "0.84.21",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -8,6 +8,7 @@ const DEFAULT_A2A_CALLER_TOKEN_TTL = "30m";
8
8
 
9
9
  export interface A2ACallerAuth {
10
10
  apiKey?: string;
11
+ apiKeyFallbacks?: string[];
11
12
  userEmail?: string;
12
13
  orgId?: string;
13
14
  orgDomain?: string;
@@ -38,21 +39,49 @@ export async function resolveA2ACallerAuth(options?: {
38
39
  } catch {}
39
40
  }
40
41
 
41
- let apiKey: string | undefined;
42
+ const apiKeyAttempts: string[] = [];
43
+ const addApiKeyAttempt = (token: string | undefined) => {
44
+ if (!token || apiKeyAttempts.includes(token)) return;
45
+ apiKeyAttempts.push(token);
46
+ };
42
47
  if (userEmail && (orgSecret || process.env.A2A_SECRET)) {
43
- try {
44
- apiKey = await signA2AToken(userEmail, orgDomain, orgSecret, {
45
- expiresIn: options?.expiresIn ?? DEFAULT_A2A_CALLER_TOKEN_TTL,
46
- preferGlobalSecret: !orgSecret,
47
- });
48
- } catch {}
48
+ if (process.env.A2A_SECRET?.trim()) {
49
+ try {
50
+ addApiKeyAttempt(
51
+ await signA2AToken(userEmail, orgDomain, orgSecret, {
52
+ expiresIn: options?.expiresIn ?? DEFAULT_A2A_CALLER_TOKEN_TTL,
53
+ preferGlobalSecret: true,
54
+ }),
55
+ );
56
+ } catch {}
57
+ }
58
+ if (orgSecret) {
59
+ try {
60
+ addApiKeyAttempt(
61
+ await signA2AToken(userEmail, orgDomain, orgSecret, {
62
+ expiresIn: options?.expiresIn ?? DEFAULT_A2A_CALLER_TOKEN_TTL,
63
+ preferGlobalSecret: false,
64
+ }),
65
+ );
66
+ } catch {}
67
+ }
49
68
  }
50
69
 
51
70
  if (options?.includeGoogleToken) {
52
71
  await attachGoogleTokenMetadata(metadata, userEmail);
53
72
  }
54
73
 
55
- return { apiKey, userEmail, orgId, orgDomain, orgSecret, metadata };
74
+ return {
75
+ apiKey: apiKeyAttempts[0],
76
+ ...(apiKeyAttempts.length > 1
77
+ ? { apiKeyFallbacks: apiKeyAttempts.slice(1) }
78
+ : {}),
79
+ userEmail,
80
+ orgId,
81
+ orgDomain,
82
+ orgSecret,
83
+ metadata,
84
+ };
56
85
  }
57
86
 
58
87
  async function attachGoogleTokenMetadata(
@@ -86,9 +86,14 @@ export async function signA2AToken(
86
86
  return jwt.sign(new TextEncoder().encode(secret));
87
87
  }
88
88
 
89
+ export function shouldPreferGlobalA2ASecret(orgSecret?: string): boolean {
90
+ return !!process.env.A2A_SECRET?.trim() || !orgSecret;
91
+ }
92
+
89
93
  export class A2AClient {
90
94
  private baseUrl: string;
91
95
  private apiKey?: string;
96
+ private apiKeyAttempts: Array<string | undefined>;
92
97
  private endpointCandidates: string[] = [];
93
98
  private endpointResolved = false;
94
99
  private requestTimeoutMs?: number;
@@ -96,7 +101,7 @@ export class A2AClient {
96
101
  constructor(
97
102
  baseUrl: string,
98
103
  apiKey?: string,
99
- options?: { requestTimeoutMs?: number },
104
+ options?: { requestTimeoutMs?: number; fallbackApiKeys?: string[] },
100
105
  ) {
101
106
  const normalized = baseUrl.replace(/\/$/, "");
102
107
  const explicitEndpoint = splitExplicitA2AEndpoint(normalized);
@@ -106,6 +111,10 @@ export class A2AClient {
106
111
  this.endpointResolved = true;
107
112
  }
108
113
  this.apiKey = apiKey;
114
+ this.apiKeyAttempts = uniqueAuthTokens([
115
+ apiKey,
116
+ ...(options?.fallbackApiKeys ?? []),
117
+ ]);
109
118
  this.requestTimeoutMs = options?.requestTimeoutMs;
110
119
  }
111
120
 
@@ -138,14 +147,22 @@ export class A2AClient {
138
147
  }
139
148
  }
140
149
 
141
- private headers(): Record<string, string> {
150
+ private headers(apiKey = this.apiKey): Record<string, string> {
142
151
  const h: Record<string, string> = { "Content-Type": "application/json" };
143
- if (this.apiKey) {
144
- h["Authorization"] = `Bearer ${this.apiKey}`;
152
+ if (apiKey) {
153
+ h["Authorization"] = `Bearer ${apiKey}`;
145
154
  }
146
155
  return h;
147
156
  }
148
157
 
158
+ private markApiKeySucceeded(apiKey: string | undefined) {
159
+ this.apiKey = apiKey;
160
+ this.apiKeyAttempts = uniqueAuthTokens([
161
+ apiKey,
162
+ ...this.apiKeyAttempts.filter((token) => token !== apiKey),
163
+ ]);
164
+ }
165
+
149
166
  private async rpc(
150
167
  method: string,
151
168
  params: Record<string, unknown>,
@@ -161,22 +178,32 @@ export class A2AClient {
161
178
  let lastError: Error | null = null;
162
179
 
163
180
  for (const url of this.endpointCandidates) {
164
- console.log(`[A2A Client] POST ${url} method=${method}`);
165
- const startTime = Date.now();
166
- const res = await this.postJson(url, body);
167
- console.log(
168
- `[A2A Client] Response: ${res.status} in ${Date.now() - startTime}ms`,
169
- );
181
+ for (let i = 0; i < this.apiKeyAttempts.length; i++) {
182
+ console.log(`[A2A Client] POST ${url} method=${method}`);
183
+ const startTime = Date.now();
184
+ const res = await this.postJson(url, body, this.apiKeyAttempts[i]);
185
+ console.log(
186
+ `[A2A Client] Response: ${res.status} in ${Date.now() - startTime}ms`,
187
+ );
170
188
 
171
- if (res.ok) {
172
- this.endpointCandidates = [url];
173
- return res.json() as Promise<JsonRpcResponse>;
174
- }
189
+ if (res.ok) {
190
+ this.endpointCandidates = [url];
191
+ this.markApiKeySucceeded(this.apiKeyAttempts[i]);
192
+ return res.json() as Promise<JsonRpcResponse>;
193
+ }
175
194
 
176
- const text = await res.text();
177
- lastError = new Error(`A2A request failed (${res.status}): ${text}`);
178
- if (!shouldTryNextEndpoint(res.status)) {
179
- throw lastError;
195
+ const text = await res.text();
196
+ lastError = new Error(`A2A request failed (${res.status}): ${text}`);
197
+ if (
198
+ i < this.apiKeyAttempts.length - 1 &&
199
+ isA2AAuthRejectionResponse(res.status, text)
200
+ ) {
201
+ continue;
202
+ }
203
+ if (!shouldTryNextEndpoint(res.status)) {
204
+ throw lastError;
205
+ }
206
+ break;
180
207
  }
181
208
  }
182
209
 
@@ -309,14 +336,25 @@ export class A2AClient {
309
336
  let res: Response | null = null;
310
337
  let lastError: Error | null = null;
311
338
  for (const candidate of this.endpointCandidates) {
312
- res = await this.postJson(candidate, body);
313
- if (res.ok) {
314
- this.endpointCandidates = [candidate];
339
+ for (let i = 0; i < this.apiKeyAttempts.length; i++) {
340
+ res = await this.postJson(candidate, body, this.apiKeyAttempts[i]);
341
+ if (res.ok) {
342
+ this.endpointCandidates = [candidate];
343
+ this.markApiKeySucceeded(this.apiKeyAttempts[i]);
344
+ break;
345
+ }
346
+ const text = await res.text();
347
+ lastError = new Error(`A2A stream failed (${res.status}): ${text}`);
348
+ if (
349
+ i < this.apiKeyAttempts.length - 1 &&
350
+ isA2AAuthRejectionResponse(res.status, text)
351
+ ) {
352
+ continue;
353
+ }
354
+ if (!shouldTryNextEndpoint(res.status)) throw lastError;
315
355
  break;
316
356
  }
317
- const text = await res.text();
318
- lastError = new Error(`A2A stream failed (${res.status}): ${text}`);
319
- if (!shouldTryNextEndpoint(res.status)) throw lastError;
357
+ if (res?.ok) break;
320
358
  }
321
359
  if (!res?.ok) {
322
360
  throw lastError ?? new Error("No A2A endpoint candidates available");
@@ -379,7 +417,11 @@ export class A2AClient {
379
417
  this.endpointCandidates = unique(candidates);
380
418
  }
381
419
 
382
- private async postJson(url: string, body: JsonRpcRequest): Promise<Response> {
420
+ private async postJson(
421
+ url: string,
422
+ body: JsonRpcRequest,
423
+ apiKey = this.apiKey,
424
+ ): Promise<Response> {
383
425
  const controller = this.requestTimeoutMs
384
426
  ? new AbortController()
385
427
  : undefined;
@@ -392,7 +434,7 @@ export class A2AClient {
392
434
  url,
393
435
  {
394
436
  method: "POST",
395
- headers: this.headers(),
437
+ headers: this.headers(apiKey),
396
438
  body: JSON.stringify(body),
397
439
  signal: controller?.signal,
398
440
  },
@@ -461,6 +503,27 @@ function unique(values: string[]): string[] {
461
503
  return Array.from(new Set(values));
462
504
  }
463
505
 
506
+ function uniqueAuthTokens(
507
+ values: Array<string | undefined>,
508
+ ): Array<string | undefined> {
509
+ const result: Array<string | undefined> = [];
510
+ for (const value of values) {
511
+ if (result.includes(value)) continue;
512
+ result.push(value);
513
+ }
514
+ if (result.length === 0) result.push(undefined);
515
+ return result;
516
+ }
517
+
518
+ function isA2AAuthRejectionResponse(status: number, text: string): boolean {
519
+ return (
520
+ status === 401 ||
521
+ /A2A error \(-32001\): (?:Invalid or expired A2A token|Invalid API key|Authentication required)|Invalid or expired A2A token|Invalid API key|Authentication required/i.test(
522
+ text,
523
+ )
524
+ );
525
+ }
526
+
464
527
  /**
465
528
  * One-shot convenience function: send a text message and get a text response.
466
529
  *
@@ -489,27 +552,6 @@ export async function callAgent(
489
552
  pollIntervalMs?: number;
490
553
  },
491
554
  ): Promise<string> {
492
- let apiKey = opts?.apiKey;
493
-
494
- // Auto-sign with JWT when an A2A secret (org or global) is available and we have a user email
495
- if (
496
- !apiKey &&
497
- opts?.userEmail &&
498
- (opts?.orgSecret || process.env.A2A_SECRET)
499
- ) {
500
- try {
501
- apiKey = await signA2AToken(
502
- opts.userEmail,
503
- opts.orgDomain,
504
- opts.orgSecret,
505
- { preferGlobalSecret: !opts.orgSecret },
506
- );
507
- } catch {
508
- // Fall back to unsigned call
509
- }
510
- }
511
-
512
- const client = new A2AClient(url, apiKey);
513
555
  const metadata: Record<string, unknown> = {};
514
556
  if (opts?.userEmail) metadata.userEmail = opts.userEmail;
515
557
  if (opts?.orgDomain) metadata.orgDomain = opts.orgDomain;
@@ -525,39 +567,108 @@ export async function callAgent(
525
567
  parts: [{ type: "text", text }],
526
568
  };
527
569
 
528
- let task: Task;
529
- if (useAsync) {
570
+ const apiKeyAttempts = await buildA2AApiKeyAttempts(opts);
571
+ let lastAuthError: unknown;
572
+
573
+ for (let i = 0; i < apiKeyAttempts.length; i++) {
530
574
  try {
531
- task = await client.sendAndWait(message, {
532
- contextId: opts?.contextId,
533
- metadata,
534
- timeoutMs: opts?.timeoutMs,
535
- pollIntervalMs: opts?.pollIntervalMs,
575
+ const fallbackApiKeys = apiKeyAttempts
576
+ .slice(i + 1)
577
+ .filter((token): token is string => token !== undefined);
578
+ const client = new A2AClient(url, apiKeyAttempts[i], {
579
+ fallbackApiKeys,
536
580
  });
581
+ let task: Task;
582
+ if (useAsync) {
583
+ task = await client.sendAndWait(message, {
584
+ contextId: opts?.contextId,
585
+ metadata,
586
+ timeoutMs: opts?.timeoutMs,
587
+ pollIntervalMs: opts?.pollIntervalMs,
588
+ });
589
+ } else {
590
+ task = await client.send(message, {
591
+ contextId: opts?.contextId,
592
+ metadata,
593
+ });
594
+ }
595
+
596
+ // Extract text from the response
597
+ const responseMessage = task.status.message;
598
+ if (responseMessage) {
599
+ const textParts = responseMessage.parts
600
+ .filter((p): p is { type: "text"; text: string } => p.type === "text")
601
+ .map((p) => p.text);
602
+ return textParts.join("\n");
603
+ }
604
+
605
+ return "";
537
606
  } catch (err) {
538
607
  if (err instanceof A2ATaskTimeoutError) {
539
608
  const recoverableText = extractRecoverableArtifactText(err.lastTask);
540
609
  if (recoverableText) return recoverableText;
541
610
  }
611
+ if (i < apiKeyAttempts.length - 1 && isA2AAuthRejection(err)) {
612
+ lastAuthError = err;
613
+ continue;
614
+ }
542
615
  throw err;
543
616
  }
544
- } else {
545
- task = await client.send(message, {
546
- contextId: opts?.contextId,
547
- metadata,
548
- });
549
617
  }
550
618
 
551
- // Extract text from the response
552
- const responseMessage = task.status.message;
553
- if (responseMessage) {
554
- const textParts = responseMessage.parts
555
- .filter((p): p is { type: "text"; text: string } => p.type === "text")
556
- .map((p) => p.text);
557
- return textParts.join("\n");
619
+ if (lastAuthError) throw lastAuthError;
620
+ return "";
621
+ }
622
+
623
+ async function buildA2AApiKeyAttempts(opts?: {
624
+ apiKey?: string;
625
+ userEmail?: string;
626
+ orgDomain?: string;
627
+ orgSecret?: string;
628
+ }): Promise<Array<string | undefined>> {
629
+ const attempts: Array<string | undefined> = [];
630
+ const add = (token: string | undefined) => {
631
+ if (token === undefined || attempts.includes(token)) return;
632
+ attempts.push(token);
633
+ };
634
+
635
+ add(opts?.apiKey);
636
+
637
+ if (opts?.userEmail && (opts.orgSecret || process.env.A2A_SECRET)) {
638
+ if (process.env.A2A_SECRET?.trim()) {
639
+ try {
640
+ add(
641
+ await signA2AToken(opts.userEmail, opts.orgDomain, opts.orgSecret, {
642
+ preferGlobalSecret: true,
643
+ }),
644
+ );
645
+ } catch {
646
+ // Keep any explicit token attempt, then fall back below.
647
+ }
648
+ }
649
+
650
+ if (opts.orgSecret) {
651
+ try {
652
+ add(
653
+ await signA2AToken(opts.userEmail, opts.orgDomain, opts.orgSecret, {
654
+ preferGlobalSecret: false,
655
+ }),
656
+ );
657
+ } catch {
658
+ // Fall through to the attempts we already have.
659
+ }
660
+ }
558
661
  }
559
662
 
560
- return "";
663
+ if (attempts.length === 0) attempts.push(undefined);
664
+ return attempts;
665
+ }
666
+
667
+ function isA2AAuthRejection(err: unknown): boolean {
668
+ const message = err instanceof Error ? err.message : String(err ?? "");
669
+ return /A2A request failed \(401\)|A2A error \(-32001\): (?:Invalid or expired A2A token|Invalid API key|Authentication required)|Invalid or expired A2A token|Invalid API key|Authentication required/i.test(
670
+ message,
671
+ );
561
672
  }
562
673
 
563
674
  function extractRecoverableArtifactText(task: Task): string {
@@ -2791,9 +2791,11 @@ export async function runAgentLoop(opts: {
2791
2791
  const eventStream = engine.stream(streamOpts);
2792
2792
  let thinkingBuffer = "";
2793
2793
  const toolInputNames = new Map<string, string>();
2794
+ const toolInputBytes = new Map<string, number>();
2794
2795
  let lastToolInputActivityAt = 0;
2795
2796
  const sendToolInputActivity = (
2796
2797
  toolName: string | undefined,
2798
+ progressBytes?: number,
2797
2799
  force = false,
2798
2800
  ) => {
2799
2801
  const now = Date.now();
@@ -2808,6 +2810,7 @@ export async function runAgentLoop(opts: {
2808
2810
  type: "activity",
2809
2811
  label: toolInputActivityLabel(toolName),
2810
2812
  ...(toolName ? { tool: toolName } : {}),
2813
+ ...(typeof progressBytes === "number" ? { progressBytes } : {}),
2811
2814
  });
2812
2815
  };
2813
2816
 
@@ -2842,13 +2845,22 @@ export async function runAgentLoop(opts: {
2842
2845
  } else if (event.type === "tool-input-start") {
2843
2846
  if (event.id && event.name) {
2844
2847
  toolInputNames.set(event.id, event.name);
2848
+ toolInputBytes.set(event.id, 0);
2845
2849
  }
2846
- sendToolInputActivity(event.name, true);
2850
+ sendToolInputActivity(event.name, undefined, true);
2847
2851
  } else if (event.type === "tool-input-delta") {
2848
2852
  const toolName =
2849
2853
  event.name ??
2850
2854
  (event.id ? toolInputNames.get(event.id) : undefined);
2851
- sendToolInputActivity(toolName);
2855
+ let progressBytes: number | undefined;
2856
+ if (event.id) {
2857
+ const previous = toolInputBytes.get(event.id) ?? 0;
2858
+ progressBytes =
2859
+ previous +
2860
+ new TextEncoder().encode(event.text ?? "").byteLength;
2861
+ toolInputBytes.set(event.id, progressBytes);
2862
+ }
2863
+ sendToolInputActivity(toolName, progressBytes);
2852
2864
  } else if (event.type === "gateway-heartbeat") {
2853
2865
  send({ type: "stream_keepalive" });
2854
2866
  } else if (event.type === "tool-call") {
@@ -5607,7 +5619,11 @@ export function createProductionAgentHandler(
5607
5619
  const callerAuth = await resolveA2ACallerAuth({
5608
5620
  includeGoogleToken: true,
5609
5621
  });
5610
- const a2aClient = new A2AClient(ref.path, callerAuth.apiKey);
5622
+ const a2aClient = new A2AClient(ref.path, callerAuth.apiKey, {
5623
+ ...(callerAuth.apiKeyFallbacks
5624
+ ? { fallbackApiKeys: callerAuth.apiKeyFallbacks }
5625
+ : {}),
5626
+ });
5611
5627
  const a2aMetadata = callerAuth.metadata;
5612
5628
 
5613
5629
  let responseText = "";
@@ -212,7 +212,7 @@ export type AgentToolInput = Record<string, unknown>;
212
212
  export type AgentChatEvent =
213
213
  | { type: "text"; text: string }
214
214
  | { type: "thinking"; text: string }
215
- | { type: "activity"; label: string; tool?: string }
215
+ | { type: "activity"; label: string; tool?: string; progressBytes?: number }
216
216
  | { type: "stream_keepalive" }
217
217
  | { type: "tool_start"; tool: string; input: AgentToolInput }
218
218
  | {