@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +16 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/caller-auth.ts +37 -8
- package/corpus/core/src/a2a/client.ts +178 -67
- package/corpus/core/src/agent/production-agent.ts +19 -3
- package/corpus/core/src/agent/types.ts +1 -1
- package/corpus/core/src/cli/pr-visual-recap-workflow.ts +1 -1
- package/corpus/core/src/cli/recap.ts +43 -34
- package/corpus/core/src/client/AssistantChat.tsx +27 -4
- package/corpus/core/src/client/PoweredByBadge.tsx +70 -17
- package/corpus/core/src/client/agent-chat-adapter.ts +5 -1
- package/corpus/core/src/client/guided-questions.tsx +11 -6
- package/corpus/core/src/client/i18n.tsx +10 -5
- package/corpus/core/src/client/sse-event-processor.ts +147 -5
- package/corpus/core/src/client/tool-display.ts +8 -0
- package/corpus/core/src/file-upload/builder.ts +40 -14
- package/corpus/core/src/file-upload/types.ts +3 -0
- package/corpus/core/src/integrations/a2a-continuation-processor.ts +49 -24
- package/corpus/core/src/mcp/builtin-tools.ts +3 -0
- package/corpus/core/src/mcp/org-directory.ts +66 -20
- package/corpus/core/src/notifications/channels.ts +235 -38
- package/corpus/core/src/scripts/call-agent.ts +7 -2
- package/corpus/core/src/server/analytics.ts +32 -6
- package/corpus/core/src/server/ssr-handler.ts +31 -12
- package/corpus/templates/analytics/AGENTS.md +21 -0
- package/corpus/templates/analytics/README.md +35 -0
- package/corpus/templates/analytics/actions/delete-analytics-alert-rule.ts +23 -0
- package/corpus/templates/analytics/actions/list-analytics-alert-rules.ts +20 -0
- package/corpus/templates/analytics/actions/run-analytics-alerts.ts +23 -0
- package/corpus/templates/analytics/actions/save-analytics-alert-rule.ts +69 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +161 -12
- package/corpus/templates/analytics/changelog/2026-07-01-analytics-can-alert-when-first-party-events-spike.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-01-session-replays-load-large-production-recordings-from-scoped.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-01-session-replays-play-back-in-production-instead-of-showing.md +6 -0
- package/corpus/templates/analytics/netlify.toml +3 -0
- package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +109 -0
- package/corpus/templates/analytics/server/db/schema.ts +55 -0
- package/corpus/templates/analytics/server/handlers/session-replay.ts +5 -2
- package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +71 -0
- package/corpus/templates/analytics/server/lib/analytics-alerts.ts +734 -0
- package/corpus/templates/analytics/server/lib/session-replay.ts +10 -3
- package/corpus/templates/analytics/server/plugins/analytics-alert-jobs.ts +54 -0
- package/corpus/templates/analytics/server/plugins/db.ts +100 -0
- package/corpus/templates/analytics/server/routes/api/analytics-alerts/run.post.ts +60 -0
- package/corpus/templates/assets/README.md +30 -8
- package/corpus/templates/brain/README.md +20 -552
- package/corpus/templates/calendar/README.md +22 -28
- package/corpus/templates/calendar/app/pages/BookingPage.tsx +14 -8
- package/corpus/templates/calendar/changelog/2026-07-01-meeting-invite-pages-have-cleaner-branding-a-black-dark-mo.md +6 -0
- package/corpus/templates/chat/README.md +32 -0
- package/corpus/templates/clips/README.md +37 -0
- package/corpus/templates/clips/actions/create-recording.ts +7 -2
- package/corpus/templates/clips/actions/finalize-recording.ts +11 -0
- package/corpus/templates/clips/actions/get-recording-player-data.ts +4 -2
- package/corpus/templates/clips/actions/import-loom-recording.ts +1 -0
- package/corpus/templates/clips/actions/lib/loom-video.ts +16 -0
- package/corpus/templates/clips/app/components/library/library-grid.tsx +18 -7
- package/corpus/templates/clips/app/components/library/recording-card.tsx +42 -5
- package/corpus/templates/clips/app/components/player/scrubber.tsx +2 -2
- package/corpus/templates/clips/app/components/recorder/countdown-overlay.tsx +4 -3
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +39 -12
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +106 -2
- package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +1 -22
- package/corpus/templates/clips/app/routes/record.tsx +48 -8
- package/corpus/templates/clips/changelog/2026-07-01-clip-uploads-and-loom-imports-finish-sooner-by-skipping-the-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-clip-uploads-and-playback-recover-more-reliably-from-storage.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-desktop-recording-avoids-unsupported-browser-tab-capture.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-embedded-player-progress-is-easier-to-see-on-dark-videos.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-loom-imports-show-clear-guidance-when-loom-does-not-provide-a-downloadable-video.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-move-to-folder-now-opens-folder-choices-directly-instead-of.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-preserve-in-flight-streaming-uploads-when-the-streaming-kill.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-recording-controls-are-cleaner-and-the-countdown-uses-shadow.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-recordings-now-recover-when-final-upload-status-gets-lost.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
- package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +7 -224
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +64 -6
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +87 -5
- package/corpus/templates/clips/server/lib/streaming-upload-mode.ts +7 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +51 -1
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +4 -1
- package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +96 -1
- package/corpus/templates/clips/shared/finalize-recovery.ts +242 -0
- package/corpus/templates/content/AGENTS.md +7 -3
- package/corpus/templates/content/README.md +22 -34
- package/corpus/templates/content/actions/list-trashed-content-databases.ts +4 -0
- package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +58 -6
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +257 -24
- package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +41 -15
- package/corpus/templates/content/app/i18n/zh-TW.ts +25 -7
- package/corpus/templates/content/app/i18n-data.ts +278 -40
- package/corpus/templates/content/changelog/2026-06-30-database-once-again-creates-an-inline-database-in-the-curren.md +6 -0
- package/corpus/templates/content/shared/api.ts +1 -0
- package/corpus/templates/design/AGENTS.md +7 -6
- package/corpus/templates/design/README.md +33 -0
- package/corpus/templates/design/actions/delete-file.ts +86 -10
- package/corpus/templates/design/actions/get-design-snapshot.ts +13 -0
- package/corpus/templates/design/actions/present-design-variants.ts +11 -13
- package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
- package/corpus/templates/design/app/pages/DesignEditor.tsx +2 -2
- package/corpus/templates/design/changelog/2026-07-01-design-recovers-sooner-when-an-agent-gets-stuck-preparing-a-.md +6 -0
- package/corpus/templates/dispatch/README.md +34 -0
- package/corpus/templates/forms/README.md +34 -0
- package/corpus/templates/macros/README.md +28 -0
- package/corpus/templates/mail/README.md +35 -0
- package/corpus/templates/plan/README.md +30 -118
- package/corpus/templates/plan/app/global.css +1 -1
- package/corpus/templates/slides/README.md +34 -0
- package/corpus/templates/videos/README.md +22 -201
- package/dist/a2a/caller-auth.d.ts +1 -0
- package/dist/a2a/caller-auth.d.ts.map +1 -1
- package/dist/a2a/caller-auth.js +34 -8
- package/dist/a2a/caller-auth.js.map +1 -1
- package/dist/a2a/client.d.ts +4 -0
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +148 -58
- package/dist/a2a/client.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +19 -4
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/types.d.ts +1 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.js +1 -1
- package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
- package/dist/cli/recap.d.ts +3 -3
- package/dist/cli/recap.d.ts.map +1 -1
- package/dist/cli/recap.js +41 -34
- package/dist/cli/recap.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +28 -4
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/PoweredByBadge.d.ts +9 -3
- package/dist/client/PoweredByBadge.d.ts.map +1 -1
- package/dist/client/PoweredByBadge.js +47 -15
- package/dist/client/PoweredByBadge.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +5 -1
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/guided-questions.d.ts.map +1 -1
- package/dist/client/guided-questions.js +10 -6
- package/dist/client/guided-questions.js.map +1 -1
- package/dist/client/i18n.d.ts +1 -1
- package/dist/client/i18n.d.ts.map +1 -1
- package/dist/client/i18n.js +6 -2
- package/dist/client/i18n.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts +2 -0
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +121 -5
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/client/tool-display.d.ts.map +1 -1
- package/dist/client/tool-display.js +8 -0
- package/dist/client/tool-display.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/builder.d.ts.map +1 -1
- package/dist/file-upload/builder.js +26 -8
- package/dist/file-upload/builder.js.map +1 -1
- package/dist/file-upload/types.d.ts +5 -1
- package/dist/file-upload/types.d.ts.map +1 -1
- package/dist/file-upload/types.js.map +1 -1
- package/dist/integrations/a2a-continuation-processor.js +43 -18
- package/dist/integrations/a2a-continuation-processor.js.map +1 -1
- package/dist/mcp/builtin-tools.d.ts.map +1 -1
- package/dist/mcp/builtin-tools.js +3 -0
- package/dist/mcp/builtin-tools.js.map +1 -1
- package/dist/mcp/org-directory.d.ts.map +1 -1
- package/dist/mcp/org-directory.js +58 -20
- package/dist/mcp/org-directory.js.map +1 -1
- package/dist/notifications/channels.d.ts +10 -0
- package/dist/notifications/channels.d.ts.map +1 -1
- package/dist/notifications/channels.js +172 -20
- package/dist/notifications/channels.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/scripts/call-agent.d.ts.map +1 -1
- package/dist/scripts/call-agent.js +2 -2
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/analytics.d.ts +16 -0
- package/dist/server/analytics.d.ts.map +1 -1
- package/dist/server/analytics.js +30 -7
- package/dist/server/analytics.js.map +1 -1
- package/dist/server/ssr-handler.d.ts.map +1 -1
- package/dist/server/ssr-handler.js +28 -12
- package/dist/server/ssr-handler.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- 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" ||
|
|
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
|
|
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
|
-
:
|
|
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 === "
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
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 (
|
|
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
|
|
247
|
+
if (shouldUseSignedUrlUpload(bytes, bareMimeType)) {
|
|
230
248
|
return uploadLargeFileViaSignedUrl(
|
|
231
|
-
|
|
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
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
{
|
|
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
|
|
441
|
+
): Promise<{ apiKey?: string; apiKeyFallbacks?: string[] }> {
|
|
442
442
|
if (continuation.a2aAuthToken === "") {
|
|
443
|
-
return
|
|
443
|
+
return {};
|
|
444
444
|
}
|
|
445
445
|
|
|
446
446
|
const storedToken = continuation.a2aAuthToken;
|
|
447
|
-
if (storedToken && !isLikelyJwt(storedToken)) return storedToken;
|
|
448
|
-
|
|
449
|
-
const
|
|
450
|
-
if (
|
|
451
|
-
|
|
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
|
|
457
|
-
return storedToken;
|
|
463
|
+
if (isLikelyJwt(storedToken)) return {};
|
|
464
|
+
return { apiKey: storedToken };
|
|
458
465
|
}
|
|
459
466
|
|
|
460
|
-
async function
|
|
467
|
+
async function signFreshContinuationTokens(
|
|
461
468
|
continuation: A2AContinuation,
|
|
462
|
-
): Promise<string
|
|
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
|
|
482
|
+
return [];
|
|
476
483
|
}
|
|
477
484
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
};
|