@agent-native/core 0.84.20 → 0.84.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.
Files changed (193) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +16 -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/AssistantChat.tsx +27 -4
  11. package/corpus/core/src/client/PoweredByBadge.tsx +70 -17
  12. package/corpus/core/src/client/agent-chat-adapter.ts +5 -1
  13. package/corpus/core/src/client/guided-questions.tsx +11 -6
  14. package/corpus/core/src/client/i18n.tsx +10 -5
  15. package/corpus/core/src/client/sse-event-processor.ts +147 -5
  16. package/corpus/core/src/client/tool-display.ts +8 -0
  17. package/corpus/core/src/file-upload/builder.ts +40 -14
  18. package/corpus/core/src/file-upload/types.ts +3 -0
  19. package/corpus/core/src/integrations/a2a-continuation-processor.ts +49 -24
  20. package/corpus/core/src/mcp/builtin-tools.ts +3 -0
  21. package/corpus/core/src/mcp/org-directory.ts +66 -20
  22. package/corpus/core/src/notifications/channels.ts +235 -38
  23. package/corpus/core/src/scripts/call-agent.ts +7 -2
  24. package/corpus/core/src/server/analytics.ts +32 -6
  25. package/corpus/core/src/server/ssr-handler.ts +31 -12
  26. package/corpus/templates/analytics/AGENTS.md +21 -0
  27. package/corpus/templates/analytics/README.md +35 -0
  28. package/corpus/templates/analytics/actions/delete-analytics-alert-rule.ts +23 -0
  29. package/corpus/templates/analytics/actions/list-analytics-alert-rules.ts +20 -0
  30. package/corpus/templates/analytics/actions/run-analytics-alerts.ts +23 -0
  31. package/corpus/templates/analytics/actions/save-analytics-alert-rule.ts +69 -0
  32. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +161 -12
  33. package/corpus/templates/analytics/changelog/2026-07-01-analytics-can-alert-when-first-party-events-spike.md +6 -0
  34. package/corpus/templates/analytics/changelog/2026-07-01-session-replays-load-large-production-recordings-from-scoped.md +6 -0
  35. package/corpus/templates/analytics/changelog/2026-07-01-session-replays-play-back-in-production-instead-of-showing.md +6 -0
  36. package/corpus/templates/analytics/netlify.toml +3 -0
  37. package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +109 -0
  38. package/corpus/templates/analytics/server/db/schema.ts +55 -0
  39. package/corpus/templates/analytics/server/handlers/session-replay.ts +5 -2
  40. package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +71 -0
  41. package/corpus/templates/analytics/server/lib/analytics-alerts.ts +734 -0
  42. package/corpus/templates/analytics/server/lib/session-replay.ts +10 -3
  43. package/corpus/templates/analytics/server/plugins/analytics-alert-jobs.ts +54 -0
  44. package/corpus/templates/analytics/server/plugins/db.ts +100 -0
  45. package/corpus/templates/analytics/server/routes/api/analytics-alerts/run.post.ts +60 -0
  46. package/corpus/templates/assets/README.md +30 -8
  47. package/corpus/templates/brain/README.md +20 -552
  48. package/corpus/templates/calendar/README.md +22 -28
  49. package/corpus/templates/calendar/app/pages/BookingPage.tsx +14 -8
  50. package/corpus/templates/calendar/changelog/2026-07-01-meeting-invite-pages-have-cleaner-branding-a-black-dark-mo.md +6 -0
  51. package/corpus/templates/chat/README.md +32 -0
  52. package/corpus/templates/clips/README.md +37 -0
  53. package/corpus/templates/clips/actions/create-recording.ts +7 -2
  54. package/corpus/templates/clips/actions/finalize-recording.ts +11 -0
  55. package/corpus/templates/clips/actions/get-recording-player-data.ts +4 -2
  56. package/corpus/templates/clips/actions/import-loom-recording.ts +1 -0
  57. package/corpus/templates/clips/actions/lib/loom-video.ts +16 -0
  58. package/corpus/templates/clips/app/components/library/library-grid.tsx +18 -7
  59. package/corpus/templates/clips/app/components/library/recording-card.tsx +42 -5
  60. package/corpus/templates/clips/app/components/player/scrubber.tsx +2 -2
  61. package/corpus/templates/clips/app/components/recorder/countdown-overlay.tsx +4 -3
  62. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +39 -12
  63. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +106 -2
  64. package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +1 -22
  65. package/corpus/templates/clips/app/routes/record.tsx +48 -8
  66. package/corpus/templates/clips/changelog/2026-07-01-clip-uploads-and-loom-imports-finish-sooner-by-skipping-the-.md +6 -0
  67. package/corpus/templates/clips/changelog/2026-07-01-clip-uploads-and-playback-recover-more-reliably-from-storage.md +6 -0
  68. package/corpus/templates/clips/changelog/2026-07-01-desktop-recording-avoids-unsupported-browser-tab-capture.md +6 -0
  69. package/corpus/templates/clips/changelog/2026-07-01-embedded-player-progress-is-easier-to-see-on-dark-videos.md +6 -0
  70. package/corpus/templates/clips/changelog/2026-07-01-loom-imports-show-clear-guidance-when-loom-does-not-provide-a-downloadable-video.md +6 -0
  71. package/corpus/templates/clips/changelog/2026-07-01-move-to-folder-now-opens-folder-choices-directly-instead-of.md +6 -0
  72. package/corpus/templates/clips/changelog/2026-07-01-preserve-in-flight-streaming-uploads-when-the-streaming-kill.md +6 -0
  73. package/corpus/templates/clips/changelog/2026-07-01-recording-controls-are-cleaner-and-the-countdown-uses-shadow.md +6 -0
  74. package/corpus/templates/clips/changelog/2026-07-01-recordings-now-recover-when-final-upload-status-gets-lost.md +6 -0
  75. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  76. package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +7 -224
  77. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +64 -6
  78. package/corpus/templates/clips/desktop/src/lib/recorder.ts +87 -5
  79. package/corpus/templates/clips/server/lib/streaming-upload-mode.ts +7 -0
  80. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +51 -1
  81. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +4 -1
  82. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +96 -1
  83. package/corpus/templates/clips/shared/finalize-recovery.ts +242 -0
  84. package/corpus/templates/content/AGENTS.md +7 -3
  85. package/corpus/templates/content/README.md +22 -34
  86. package/corpus/templates/content/actions/list-trashed-content-databases.ts +4 -0
  87. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +58 -6
  88. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +257 -24
  89. package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +41 -15
  90. package/corpus/templates/content/app/i18n/zh-TW.ts +25 -7
  91. package/corpus/templates/content/app/i18n-data.ts +278 -40
  92. package/corpus/templates/content/changelog/2026-06-30-database-once-again-creates-an-inline-database-in-the-curren.md +6 -0
  93. package/corpus/templates/content/shared/api.ts +1 -0
  94. package/corpus/templates/design/AGENTS.md +7 -6
  95. package/corpus/templates/design/README.md +33 -0
  96. package/corpus/templates/design/actions/delete-file.ts +86 -10
  97. package/corpus/templates/design/actions/get-design-snapshot.ts +13 -0
  98. package/corpus/templates/design/actions/present-design-variants.ts +11 -13
  99. package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
  100. package/corpus/templates/design/app/pages/DesignEditor.tsx +2 -2
  101. package/corpus/templates/design/changelog/2026-07-01-design-recovers-sooner-when-an-agent-gets-stuck-preparing-a-.md +6 -0
  102. package/corpus/templates/dispatch/README.md +34 -0
  103. package/corpus/templates/forms/README.md +34 -0
  104. package/corpus/templates/macros/README.md +28 -0
  105. package/corpus/templates/mail/README.md +35 -0
  106. package/corpus/templates/plan/README.md +30 -118
  107. package/corpus/templates/plan/app/global.css +1 -1
  108. package/corpus/templates/slides/README.md +34 -0
  109. package/corpus/templates/videos/README.md +22 -201
  110. package/dist/a2a/caller-auth.d.ts +1 -0
  111. package/dist/a2a/caller-auth.d.ts.map +1 -1
  112. package/dist/a2a/caller-auth.js +34 -8
  113. package/dist/a2a/caller-auth.js.map +1 -1
  114. package/dist/a2a/client.d.ts +4 -0
  115. package/dist/a2a/client.d.ts.map +1 -1
  116. package/dist/a2a/client.js +148 -58
  117. package/dist/a2a/client.js.map +1 -1
  118. package/dist/agent/production-agent.d.ts.map +1 -1
  119. package/dist/agent/production-agent.js +19 -4
  120. package/dist/agent/production-agent.js.map +1 -1
  121. package/dist/agent/types.d.ts +1 -0
  122. package/dist/agent/types.d.ts.map +1 -1
  123. package/dist/agent/types.js.map +1 -1
  124. package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
  125. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  126. package/dist/cli/pr-visual-recap-workflow.js +1 -1
  127. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  128. package/dist/cli/recap.d.ts +3 -3
  129. package/dist/cli/recap.d.ts.map +1 -1
  130. package/dist/cli/recap.js +41 -34
  131. package/dist/cli/recap.js.map +1 -1
  132. package/dist/client/AssistantChat.d.ts.map +1 -1
  133. package/dist/client/AssistantChat.js +28 -4
  134. package/dist/client/AssistantChat.js.map +1 -1
  135. package/dist/client/PoweredByBadge.d.ts +9 -3
  136. package/dist/client/PoweredByBadge.d.ts.map +1 -1
  137. package/dist/client/PoweredByBadge.js +47 -15
  138. package/dist/client/PoweredByBadge.js.map +1 -1
  139. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  140. package/dist/client/agent-chat-adapter.js +5 -1
  141. package/dist/client/agent-chat-adapter.js.map +1 -1
  142. package/dist/client/guided-questions.d.ts.map +1 -1
  143. package/dist/client/guided-questions.js +10 -6
  144. package/dist/client/guided-questions.js.map +1 -1
  145. package/dist/client/i18n.d.ts +1 -1
  146. package/dist/client/i18n.d.ts.map +1 -1
  147. package/dist/client/i18n.js +6 -2
  148. package/dist/client/i18n.js.map +1 -1
  149. package/dist/client/sse-event-processor.d.ts +2 -0
  150. package/dist/client/sse-event-processor.d.ts.map +1 -1
  151. package/dist/client/sse-event-processor.js +121 -5
  152. package/dist/client/sse-event-processor.js.map +1 -1
  153. package/dist/client/tool-display.d.ts.map +1 -1
  154. package/dist/client/tool-display.js +8 -0
  155. package/dist/client/tool-display.js.map +1 -1
  156. package/dist/collab/awareness.d.ts +2 -2
  157. package/dist/collab/awareness.d.ts.map +1 -1
  158. package/dist/collab/routes.d.ts +1 -1
  159. package/dist/file-upload/builder.d.ts.map +1 -1
  160. package/dist/file-upload/builder.js +26 -8
  161. package/dist/file-upload/builder.js.map +1 -1
  162. package/dist/file-upload/types.d.ts +5 -1
  163. package/dist/file-upload/types.d.ts.map +1 -1
  164. package/dist/file-upload/types.js.map +1 -1
  165. package/dist/integrations/a2a-continuation-processor.js +43 -18
  166. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  167. package/dist/mcp/builtin-tools.d.ts.map +1 -1
  168. package/dist/mcp/builtin-tools.js +3 -0
  169. package/dist/mcp/builtin-tools.js.map +1 -1
  170. package/dist/mcp/org-directory.d.ts.map +1 -1
  171. package/dist/mcp/org-directory.js +58 -20
  172. package/dist/mcp/org-directory.js.map +1 -1
  173. package/dist/notifications/channels.d.ts +10 -0
  174. package/dist/notifications/channels.d.ts.map +1 -1
  175. package/dist/notifications/channels.js +172 -20
  176. package/dist/notifications/channels.js.map +1 -1
  177. package/dist/notifications/routes.d.ts +3 -3
  178. package/dist/observability/routes.d.ts +5 -5
  179. package/dist/progress/routes.d.ts +1 -1
  180. package/dist/resources/handlers.d.ts +1 -1
  181. package/dist/scripts/call-agent.d.ts.map +1 -1
  182. package/dist/scripts/call-agent.js +2 -2
  183. package/dist/scripts/call-agent.js.map +1 -1
  184. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  185. package/dist/server/analytics.d.ts +16 -0
  186. package/dist/server/analytics.d.ts.map +1 -1
  187. package/dist/server/analytics.js +30 -7
  188. package/dist/server/analytics.js.map +1 -1
  189. package/dist/server/ssr-handler.d.ts.map +1 -1
  190. package/dist/server/ssr-handler.js +28 -12
  191. package/dist/server/ssr-handler.js.map +1 -1
  192. package/dist/server/transcribe-voice.d.ts +1 -1
  193. package/package.json +1 -1
@@ -944,6 +944,8 @@ function incrementalActionGuidance(tool: string): string | undefined {
944
944
  case "generate-design":
945
945
  case "update-design":
946
946
  return 'if an existing design file or snapshot is already in history, especially after a Design variant pick, stop retrying `generate-design`: call `get-design-snapshot` for the selected file, then call `edit-design` once on that same `fileId` (`mode: "replace-file"` for a compact full-file replacement, or search/replace for smaller edits). Use `generate-design` only for a brand-new compact first file when no target file exists yet';
947
+ case "edit-design":
948
+ return "retry with a smaller `edit-design` payload: prefer a handful of exact search/replace edits against the already-snapshotted file, and avoid `replacementContent` for a huge full-file rewrite. If a selected variant needs expansion, upgrade the highest-value visible sections first, save once, and summarize any remaining refinements";
947
949
  case "present-design-variants":
948
950
  return 'call `present-design-variants` with concise labels, descriptions, accent colors, and feature bullets; omit large `content` HTML when needed so the action can render compact representative screens. After the user picks a direction, delete unchosen screens, snapshot the selected `fileId`, and refine that same file with `edit-design` (`mode: "replace-file"` for placeholder expansion). Do not call `generate-design` after the variant pick';
949
951
  case "create-visual-plan":
@@ -980,7 +982,9 @@ function autoContinueMessage(signal: AgentAutoContinueSignal): string {
980
982
  // toward a compact first version it can actually finish in a single run, then
981
983
  // incremental refinements — never re-streaming the same oversized payload.
982
984
  const cutoffPreparingAction =
983
- signal.reason === "run_timeout" || signal.reason === "stream_ended";
985
+ signal.reason === "run_timeout" ||
986
+ signal.reason === "stream_ended" ||
987
+ signal.reason === "no_progress";
984
988
  let actionInputNote = "";
985
989
  if (cutoffPreparingAction && tool) {
986
990
  const guidance = incrementalActionGuidance(tool);
@@ -150,6 +150,16 @@ export function formatGuidedAnswersForAgent(
150
150
  .join("\n");
151
151
  }
152
152
 
153
+ function defaultGuidedSubmitContext(formattedAnswers: string): string {
154
+ return [
155
+ "The user answered the guided questions.",
156
+ "Use the selected option values below as authoritative. If an answer includes exact ids, file names, or action instructions, follow those exact details instead of inferring them.",
157
+ "",
158
+ "Answers:",
159
+ formattedAnswers,
160
+ ].join("\n");
161
+ }
162
+
153
163
  /** A single option for {@link askUserQuestion}. Mirrors the agent `ask-question`
154
164
  * tool and Claude Code's AskUserQuestion option shape. */
155
165
  export interface AskUserQuestionOption {
@@ -1080,12 +1090,7 @@ export function useGuidedQuestionFlow({
1080
1090
  const resolvedSubmitMessage = payload?.submitMessage ?? submitMessage;
1081
1091
  const context =
1082
1092
  buildSubmitContext?.({ answers, formattedAnswers }) ??
1083
- [
1084
- "The user answered the pre-generation questions.",
1085
- "",
1086
- "Answers:",
1087
- formattedAnswers,
1088
- ].join("\n");
1093
+ defaultGuidedSubmitContext(formattedAnswers);
1089
1094
  sendToAgentChat({
1090
1095
  message: resolvedSubmitMessage,
1091
1096
  context,
@@ -674,7 +674,7 @@ export function LanguagePicker({
674
674
  className?: string;
675
675
  includeSystem?: boolean;
676
676
  label?: string;
677
- variant?: "select" | "icon";
677
+ variant?: "select" | "icon" | "ghost-icon";
678
678
  }) {
679
679
  const { locale, preference, setPreference } = useLocale();
680
680
  const [open, setOpen] = useState(false);
@@ -716,10 +716,15 @@ export function LanguagePicker({
716
716
  title={triggerLabel}
717
717
  data-language-picker-trigger
718
718
  className={cn(
719
- "shrink-0 rounded-md border border-border bg-background text-foreground outline-none transition-colors hover:border-foreground/30 hover:bg-accent/40 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 data-[state=open]:border-foreground/30 data-[state=open]:bg-accent/40",
719
+ "shrink-0 rounded-md outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
720
+ variant === "ghost-icon"
721
+ ? "flex h-9 w-9 items-center justify-center text-muted-foreground hover:bg-accent hover:text-foreground data-[state=open]:bg-accent data-[state=open]:text-foreground"
722
+ : "border border-border bg-background text-foreground hover:border-foreground/30 hover:bg-accent/40 hover:text-foreground data-[state=open]:border-foreground/30 data-[state=open]:bg-accent/40",
720
723
  variant === "icon"
721
724
  ? "flex h-8 w-8 items-center justify-center"
722
- : "flex h-9 w-full items-center justify-between gap-2 px-3 text-start text-sm",
725
+ : variant === "select"
726
+ ? "flex h-9 w-full items-center justify-between gap-2 px-3 text-start text-sm"
727
+ : null,
723
728
  )}
724
729
  >
725
730
  <span className="flex min-w-0 items-center gap-2">
@@ -740,12 +745,12 @@ export function LanguagePicker({
740
745
  </PopoverPrimitive.Trigger>
741
746
  <PopoverPrimitive.Portal>
742
747
  <PopoverPrimitive.Content
743
- align={variant === "icon" ? "end" : "start"}
748
+ align={variant === "select" ? "start" : "end"}
744
749
  sideOffset={6}
745
750
  role="menu"
746
751
  className={cn(
747
752
  "z-[9999] max-h-[min(20rem,var(--radix-popover-content-available-height))] overflow-y-auto rounded-lg border border-border bg-popover p-1 text-popover-foreground shadow-lg outline-none will-change-[transform,opacity] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:duration-100 data-[state=open]:duration-150 data-[state=closed]:ease-in data-[state=open]:ease-out data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
748
- variant === "icon"
753
+ variant === "icon" || variant === "ghost-icon"
749
754
  ? "min-w-56"
750
755
  : "w-[min(20rem,calc(100vw-2rem))] min-w-[var(--radix-popover-trigger-width)]",
751
756
  )}
@@ -49,6 +49,7 @@ export interface SSEEvent {
49
49
  /** Server-assigned call identifier emitted on tool_start / tool_done events. */
50
50
  id?: string;
51
51
  label?: string;
52
+ progressBytes?: number;
52
53
  input?: Record<string, string>;
53
54
  result?: string;
54
55
  isError?: boolean;
@@ -144,9 +145,58 @@ export class AgentAutoContinueSignal extends Error {
144
145
  }
145
146
 
146
147
  export const SSE_NO_PROGRESS_TIMEOUT_MS = 75_000;
148
+ export const SSE_ACTION_PREPARATION_STALL_TIMEOUT_MS = 90_000;
147
149
 
148
150
  type ActivityTrailEntry = AgentActivityTrailEntry;
149
151
 
152
+ type PreparingActionState = {
153
+ tool?: string;
154
+ startedAt?: number;
155
+ /**
156
+ * Timestamp of the last real streaming progress for the in-preparation tool
157
+ * input. The server emits a throttled `activity` heartbeat per
158
+ * `tool-input-delta`, so while the model is actively streaming a (possibly
159
+ * very large) tool argument this keeps advancing. The stall guard measures
160
+ * silence from HERE — not from `startedAt` — so a legitimately large, still-
161
+ * streaming input is never aborted; only genuine silence (keepalive-only, no
162
+ * further deltas) can trip it.
163
+ */
164
+ lastProgressAt?: number;
165
+ };
166
+
167
+ function formatProgressBytes(bytes: number): string {
168
+ if (bytes < 1024) return `${bytes} B`;
169
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
170
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
171
+ }
172
+
173
+ function activityProgressBytes(ev: SSEEvent): number | undefined {
174
+ return typeof ev.progressBytes === "number" &&
175
+ Number.isFinite(ev.progressBytes) &&
176
+ ev.progressBytes >= 0
177
+ ? Math.floor(ev.progressBytes)
178
+ : undefined;
179
+ }
180
+
181
+ function isPreparingActionActivity(ev: SSEEvent): boolean {
182
+ if (ev.type !== "activity") return false;
183
+ const label = (ev.label ?? "").trim().toLowerCase();
184
+ return label.startsWith("preparing ") && label.includes(" action");
185
+ }
186
+
187
+ function baseActivityLabel(ev: SSEEvent, tool?: string): string {
188
+ return humanizeToolLabelText(ev.label ?? "Working", tool);
189
+ }
190
+
191
+ function visibleActivityLabel(ev: SSEEvent, tool?: string): string {
192
+ const label = baseActivityLabel(ev, tool);
193
+ const progressBytes = activityProgressBytes(ev);
194
+ if (progressBytes === undefined || !isPreparingActionActivity(ev)) {
195
+ return label;
196
+ }
197
+ return `${label} (${formatProgressBytes(progressBytes)} streamed)`;
198
+ }
199
+
150
200
  function findPendingToolCallIndex(
151
201
  content: ContentPart[],
152
202
  toolName: string,
@@ -199,6 +249,53 @@ function appendActivityTrail(
199
249
  }
200
250
  }
201
251
 
252
+ function updatePreparingActionState(
253
+ state: PreparingActionState,
254
+ ev: SSEEvent,
255
+ now: number,
256
+ ) {
257
+ if (ev.type === "activity" && isPreparingActionActivity(ev)) {
258
+ const tool = ev.tool?.trim() || undefined;
259
+ if (!tool) return;
260
+ if (state.tool !== tool || state.startedAt === undefined) {
261
+ state.tool = tool;
262
+ state.startedAt = now;
263
+ }
264
+ // Every tool-input activity is a throttled proof the model is still
265
+ // streaming this action's argument. Treat it as progress so large inputs
266
+ // that take many seconds/minutes to stream are NOT flagged as stalled.
267
+ state.lastProgressAt = now;
268
+ return;
269
+ }
270
+
271
+ if (
272
+ ev.type === "clear" ||
273
+ ev.type === "text" ||
274
+ ev.type === "tool_start" ||
275
+ ev.type === "tool_done" ||
276
+ ev.type === "done" ||
277
+ ev.type === "error" ||
278
+ ev.type === "missing_api_key"
279
+ ) {
280
+ state.tool = undefined;
281
+ state.startedAt = undefined;
282
+ state.lastProgressAt = undefined;
283
+ }
284
+ }
285
+
286
+ function hasStalledPreparingAction(state: PreparingActionState, now: number) {
287
+ // Fire only when a tool input has gone SILENT — no further streaming deltas
288
+ // for the whole window — never merely because a large input has been
289
+ // streaming for a long time. `lastProgressAt` advances on every delta
290
+ // heartbeat, so an actively-streaming large output keeps resetting this and
291
+ // survives; a genuinely stuck prep (keepalive-only, no deltas) trips it.
292
+ return (
293
+ state.tool !== undefined &&
294
+ state.lastProgressAt !== undefined &&
295
+ now - state.lastProgressAt >= SSE_ACTION_PREPARATION_STALL_TIMEOUT_MS
296
+ );
297
+ }
298
+
202
299
  async function readChunkWithProgressTimeout(
203
300
  reader: ReadableStreamDefaultReader<Uint8Array>,
204
301
  lastMeaningfulEventAt: number,
@@ -491,7 +588,7 @@ export function processEvent(
491
588
 
492
589
  if (ev.type === "activity") {
493
590
  const tool = ev.tool?.trim() || undefined;
494
- const label = humanizeToolLabelText(ev.label ?? "Working", tool);
591
+ const label = visibleActivityLabel(ev, tool);
495
592
  if (typeof window !== "undefined") {
496
593
  window.dispatchEvent(
497
594
  new CustomEvent("agent-chat:activity", {
@@ -941,6 +1038,7 @@ export async function* readSSEStream(
941
1038
  let buf = "";
942
1039
  let lastMeaningfulEventAt = Date.now();
943
1040
  const activityTrail: ActivityTrailEntry[] = [];
1041
+ const preparingActionState: PreparingActionState = {};
944
1042
  const processEventState: ProcessEventState = {
945
1043
  completedToolsAfterLastAssistantText: new Set(),
946
1044
  };
@@ -1000,7 +1098,9 @@ export async function* readSSEStream(
1000
1098
  continue;
1001
1099
  }
1002
1100
  sawDataEvent = true;
1003
- lastMeaningfulEventAt = Date.now();
1101
+ const now = Date.now();
1102
+ lastMeaningfulEventAt = now;
1103
+ updatePreparingActionState(preparingActionState, ev, now);
1004
1104
 
1005
1105
  // Track sequence number for reconnection
1006
1106
  if (ev.seq !== undefined && onSeq) {
@@ -1012,7 +1112,7 @@ export async function* readSSEStream(
1012
1112
  } else if (ev.type === "activity") {
1013
1113
  const tool = ev.tool?.trim() || undefined;
1014
1114
  appendActivityTrail(activityTrail, {
1015
- label: humanizeToolLabelText(ev.label ?? "Working", tool),
1115
+ label: baseActivityLabel(ev, tool),
1016
1116
  ...(tool ? { tool } : {}),
1017
1117
  });
1018
1118
  } else if (ev.type === "tool_start") {
@@ -1039,6 +1139,12 @@ export async function* readSSEStream(
1039
1139
  );
1040
1140
 
1041
1141
  if (result) yield withStreamMetadata(result);
1142
+ if (hasStalledPreparingAction(preparingActionState, Date.now())) {
1143
+ throw new AgentAutoContinueSignal({
1144
+ reason: "no_progress",
1145
+ activityTrail: [...activityTrail],
1146
+ });
1147
+ }
1042
1148
  if (action === "auto_continue") {
1043
1149
  throw new AgentAutoContinueSignal(
1044
1150
  autoContinue
@@ -1099,6 +1205,8 @@ export async function readSSEStreamRaw(
1099
1205
  const decoder = new TextDecoder();
1100
1206
  let buf = "";
1101
1207
  let lastMeaningfulEventAt = Date.now();
1208
+ const activityTrail: ActivityTrailEntry[] = [];
1209
+ const preparingActionState: PreparingActionState = {};
1102
1210
  const processEventState: ProcessEventState = {
1103
1211
  completedToolsAfterLastAssistantText: new Set(),
1104
1212
  };
@@ -1134,12 +1242,37 @@ export async function readSSEStreamRaw(
1134
1242
  continue;
1135
1243
  }
1136
1244
  sawDataEvent = true;
1137
- lastMeaningfulEventAt = Date.now();
1245
+ const now = Date.now();
1246
+ lastMeaningfulEventAt = now;
1247
+ updatePreparingActionState(preparingActionState, ev, now);
1138
1248
 
1139
1249
  if (ev.seq !== undefined && onSeq) {
1140
1250
  onSeq(ev.seq);
1141
1251
  }
1142
1252
 
1253
+ if (ev.type === "clear") {
1254
+ activityTrail.length = 0;
1255
+ } else if (ev.type === "activity") {
1256
+ const tool = ev.tool?.trim() || undefined;
1257
+ appendActivityTrail(activityTrail, {
1258
+ label: baseActivityLabel(ev, tool),
1259
+ ...(tool ? { tool } : {}),
1260
+ });
1261
+ } else if (ev.type === "tool_start") {
1262
+ const tool = ev.tool ?? "unknown";
1263
+ appendActivityTrail(activityTrail, {
1264
+ label: runningToolLabel(tool),
1265
+ tool,
1266
+ });
1267
+ } else if (ev.type === "tool_done") {
1268
+ const tool = ev.tool ?? "unknown";
1269
+ for (let i = activityTrail.length - 1; i >= 0; i--) {
1270
+ if (activityTrail[i]?.tool === tool) {
1271
+ activityTrail.splice(i, 1);
1272
+ }
1273
+ }
1274
+ }
1275
+
1143
1276
  const { action, autoContinue } = processEvent(
1144
1277
  ev,
1145
1278
  content,
@@ -1159,9 +1292,18 @@ export async function readSSEStreamRaw(
1159
1292
  if (action === "auto_continue") {
1160
1293
  onUpdate([...content]);
1161
1294
  throw new AgentAutoContinueSignal(
1162
- autoContinue ?? { reason: "stream_ended" },
1295
+ autoContinue
1296
+ ? { ...autoContinue, activityTrail: [...activityTrail] }
1297
+ : { reason: "stream_ended", activityTrail: [...activityTrail] },
1163
1298
  );
1164
1299
  }
1300
+ if (hasStalledPreparingAction(preparingActionState, Date.now())) {
1301
+ onUpdate([...content]);
1302
+ throw new AgentAutoContinueSignal({
1303
+ reason: "no_progress",
1304
+ activityTrail: [...activityTrail],
1305
+ });
1306
+ }
1165
1307
  if (
1166
1308
  action === "done" ||
1167
1309
  action === "error" ||
@@ -1,6 +1,14 @@
1
+ const TOOL_DISPLAY_NAMES: Record<string, string> = {
2
+ "delete-file": "delete screen",
3
+ "get-design-snapshot": "get screen snapshot",
4
+ "edit-design": "edit screen",
5
+ };
6
+
1
7
  export function humanizeToolName(toolName: string | undefined): string {
2
8
  const raw = (toolName ?? "").trim();
3
9
  if (!raw) return "tool";
10
+ const displayName = TOOL_DISPLAY_NAMES[raw];
11
+ if (displayName) return displayName;
4
12
 
5
13
  let name = raw;
6
14
  if (name.startsWith("mcp__")) {
@@ -27,6 +27,15 @@ function makeBody(bytes: Uint8Array, mimeType: string): BodyInit {
27
27
  : (bytes as unknown as BodyInit);
28
28
  }
29
29
 
30
+ function shouldUseSignedUrlUpload(
31
+ bytes: Uint8Array,
32
+ mimeType: string,
33
+ ): boolean {
34
+ return (
35
+ bytes.byteLength > LARGE_FILE_THRESHOLD_BYTES || /^video\//i.test(mimeType)
36
+ );
37
+ }
38
+
30
39
  function fetchWithTimeout(url: string, init: RequestInit): Promise<Response> {
31
40
  const controller = new AbortController();
32
41
  const timer = setTimeout(() => controller.abort(), UPLOAD_TIMEOUT_MS);
@@ -35,6 +44,11 @@ function fetchWithTimeout(url: string, init: RequestInit): Promise<Response> {
35
44
  );
36
45
  }
37
46
 
47
+ function setSkipCompressionQueryParams(url: URL): void {
48
+ url.searchParams.set("skipCompressionWait", "true");
49
+ url.searchParams.set("skipCompression", "true");
50
+ }
51
+
38
52
  async function assertOk(res: Response, label: string): Promise<void> {
39
53
  if (!res.ok) {
40
54
  const body = await res.text().catch(() => "");
@@ -86,6 +100,7 @@ async function uploadLargeFileViaSignedUrl(
86
100
  privateKey,
87
101
  assetId,
88
102
  input.filename,
103
+ { skipCompressionWait: input.skipCompressionWait },
89
104
  );
90
105
  console.log(`[builder-upload] done [${assetId}]: ${url}`);
91
106
  return { url, id, provider: "builder" };
@@ -139,19 +154,21 @@ async function completeBuilderUpload(
139
154
  privateKey: string,
140
155
  assetId: string,
141
156
  filename: string | undefined,
157
+ options?: { skipCompressionWait?: boolean },
142
158
  ): Promise<{ url: string; id?: string }> {
143
159
  const host = builderUploadHost();
144
- const res = await fetchWithTimeout(
145
- new URL("/api/v1/upload/complete", host).toString(),
146
- {
147
- method: "POST",
148
- headers: {
149
- Authorization: `Bearer ${privateKey}`,
150
- "Content-Type": "application/json",
151
- },
152
- body: JSON.stringify({ assetId, name: filename }),
160
+ const url = new URL("/api/v1/upload/complete", host);
161
+ if (options?.skipCompressionWait) {
162
+ setSkipCompressionQueryParams(url);
163
+ }
164
+ const res = await fetchWithTimeout(url.toString(), {
165
+ method: "POST",
166
+ headers: {
167
+ Authorization: `Bearer ${privateKey}`,
168
+ "Content-Type": "application/json",
153
169
  },
154
- );
170
+ body: JSON.stringify({ assetId, name: filename }),
171
+ });
155
172
  await assertOk(res, "Builder.io upload complete failed");
156
173
  const json = (await res.json()) as { url?: string; id?: string };
157
174
  if (!json.url) throw new Error("Builder.io upload/complete returned no URL");
@@ -204,7 +221,8 @@ export const builderFileUploadProvider: FileUploadProvider = {
204
221
  id: "builder",
205
222
  name: "Builder.io",
206
223
  isConfigured: () => !!process.env.BUILDER_PRIVATE_KEY,
207
- upload: async ({ data, filename, mimeType }: FileUploadInput) => {
224
+ upload: async (input: FileUploadInput) => {
225
+ const { data, filename, mimeType } = input;
208
226
  const { resolveBuilderPrivateKey } =
209
227
  await import("../server/credential-provider.js");
210
228
  const privateKey = await resolveBuilderPrivateKey();
@@ -226,9 +244,9 @@ export const builderFileUploadProvider: FileUploadProvider = {
226
244
  data instanceof Uint8Array ? data : new Uint8Array(data as any);
227
245
  const mb = (bytes.byteLength / (1024 * 1024)).toFixed(1);
228
246
 
229
- if (bytes.byteLength > LARGE_FILE_THRESHOLD_BYTES) {
247
+ if (shouldUseSignedUrlUpload(bytes, bareMimeType)) {
230
248
  return uploadLargeFileViaSignedUrl(
231
- { data, filename, mimeType },
249
+ input,
232
250
  privateKey,
233
251
  bareMimeType,
234
252
  bytes,
@@ -241,6 +259,9 @@ export const builderFileUploadProvider: FileUploadProvider = {
241
259
 
242
260
  const url = new URL("/api/v1/upload", builderUploadHost());
243
261
  if (filename) url.searchParams.set("name", filename);
262
+ if (input.skipCompressionWait) {
263
+ setSkipCompressionQueryParams(url);
264
+ }
244
265
 
245
266
  const response = await uploadSmallFile(url, {
246
267
  method: "POST",
@@ -366,7 +387,7 @@ export const builderFileUploadProvider: FileUploadProvider = {
366
387
  : new Error("GCS PUT failed after retries");
367
388
  },
368
389
 
369
- async completeSession(session, filename) {
390
+ async completeSession(session, filename, options) {
370
391
  const { resolveBuilderPrivateKey } =
371
392
  await import("../server/credential-provider.js");
372
393
  const privateKey = await resolveBuilderPrivateKey();
@@ -378,6 +399,11 @@ export const builderFileUploadProvider: FileUploadProvider = {
378
399
  privateKey,
379
400
  assetId,
380
401
  filename,
402
+ {
403
+ skipCompressionWait:
404
+ options?.skipCompressionWait ||
405
+ session.meta.skipCompressionWait === true,
406
+ },
381
407
  );
382
408
  console.log(`[builder-resumable] upload complete: ${url}`);
383
409
  return url;
@@ -16,6 +16,8 @@ export interface FileUploadInput {
16
16
  mimeType?: string;
17
17
  /** Optional owner email for per-user scoping in fallback storage. */
18
18
  ownerEmail?: string;
19
+ /** Builder.io upload hint: return after asset registration instead of waiting for server-side compression. */
20
+ skipCompressionWait?: boolean;
19
21
  }
20
22
 
21
23
  export interface FileUploadResult {
@@ -77,6 +79,7 @@ export interface FileUploadProvider {
77
79
  completeSession(
78
80
  session: ResumableUploadSession,
79
81
  filename: string,
82
+ options?: { skipCompressionWait?: boolean },
80
83
  ): Promise<string>;
81
84
  };
82
85
  }
@@ -150,11 +150,11 @@ async function processClaimedContinuation(
150
150
  return;
151
151
  }
152
152
 
153
- const client = new A2AClient(
154
- continuation.agentUrl,
155
- await signContinuationToken(continuation),
156
- { requestTimeoutMs: POLL_REQUEST_TIMEOUT_MS },
157
- );
153
+ const auth = await signContinuationToken(continuation);
154
+ const client = new A2AClient(continuation.agentUrl, auth.apiKey, {
155
+ requestTimeoutMs: POLL_REQUEST_TIMEOUT_MS,
156
+ ...(auth.apiKeyFallbacks ? { fallbackApiKeys: auth.apiKeyFallbacks } : {}),
157
+ });
158
158
  const deadline = Date.now() + PROCESSOR_WAIT_MS;
159
159
  let task: Task | null = null;
160
160
 
@@ -438,28 +438,35 @@ function sanitizeFailureReason(reason: string): string {
438
438
 
439
439
  async function signContinuationToken(
440
440
  continuation: A2AContinuation,
441
- ): Promise<string | undefined> {
441
+ ): Promise<{ apiKey?: string; apiKeyFallbacks?: string[] }> {
442
442
  if (continuation.a2aAuthToken === "") {
443
- return undefined;
443
+ return {};
444
444
  }
445
445
 
446
446
  const storedToken = continuation.a2aAuthToken;
447
- if (storedToken && !isLikelyJwt(storedToken)) return storedToken;
448
-
449
- const freshToken = await signFreshContinuationToken(continuation);
450
- if (freshToken) return freshToken;
451
- if (!storedToken) return undefined;
447
+ if (storedToken && !isLikelyJwt(storedToken)) return { apiKey: storedToken };
448
+
449
+ const freshTokens = await signFreshContinuationTokens(continuation);
450
+ if (freshTokens.length > 0) {
451
+ return {
452
+ apiKey: freshTokens[0],
453
+ ...(freshTokens.length > 1
454
+ ? { apiKeyFallbacks: freshTokens.slice(1) }
455
+ : {}),
456
+ };
457
+ }
458
+ if (!storedToken) return {};
452
459
 
453
460
  // Older continuations may have persisted the initial short-lived JWT. Avoid
454
461
  // replaying it forever after expiry; opaque legacy bearer keys can still be
455
462
  // reused because we cannot re-mint those.
456
- if (isLikelyJwt(storedToken)) return undefined;
457
- return storedToken;
463
+ if (isLikelyJwt(storedToken)) return {};
464
+ return { apiKey: storedToken };
458
465
  }
459
466
 
460
- async function signFreshContinuationToken(
467
+ async function signFreshContinuationTokens(
461
468
  continuation: A2AContinuation,
462
- ): Promise<string | undefined> {
469
+ ): Promise<string[]> {
463
470
  let orgDomain: string | undefined;
464
471
  let orgSecret: string | undefined;
465
472
  if (continuation.orgId) {
@@ -472,17 +479,35 @@ async function signFreshContinuationToken(
472
479
  }
473
480
 
474
481
  if (!continuation.ownerEmail || !(orgSecret || process.env.A2A_SECRET)) {
475
- return undefined;
482
+ return [];
476
483
  }
477
484
 
478
- try {
479
- return await signA2AToken(continuation.ownerEmail, orgDomain, orgSecret, {
480
- expiresIn: "30m",
481
- preferGlobalSecret: !orgSecret,
482
- });
483
- } catch {
484
- return undefined;
485
+ const tokens: string[] = [];
486
+ const add = (token: string | undefined) => {
487
+ if (token && !tokens.includes(token)) tokens.push(token);
488
+ };
489
+
490
+ if (process.env.A2A_SECRET?.trim()) {
491
+ try {
492
+ add(
493
+ await signA2AToken(continuation.ownerEmail, orgDomain, orgSecret, {
494
+ expiresIn: "30m",
495
+ preferGlobalSecret: true,
496
+ }),
497
+ );
498
+ } catch {}
499
+ }
500
+ if (orgSecret) {
501
+ try {
502
+ add(
503
+ await signA2AToken(continuation.ownerEmail, orgDomain, orgSecret, {
504
+ expiresIn: "30m",
505
+ preferGlobalSecret: false,
506
+ }),
507
+ );
508
+ } catch {}
485
509
  }
510
+ return tokens;
486
511
  }
487
512
 
488
513
  function isLikelyJwt(token: string): boolean {
@@ -260,6 +260,9 @@ async function createA2AClientForAskApp(origin: string): Promise<{
260
260
  return {
261
261
  client: new A2AClient(origin, auth.apiKey, {
262
262
  requestTimeoutMs: ASK_APP_A2A_REQUEST_TIMEOUT_MS,
263
+ ...(auth.apiKeyFallbacks
264
+ ? { fallbackApiKeys: auth.apiKeyFallbacks }
265
+ : {}),
263
266
  }),
264
267
  metadata,
265
268
  };