@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +8 -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/PoweredByBadge.tsx +70 -17
- package/corpus/core/src/client/agent-chat-adapter.ts +5 -1
- package/corpus/core/src/client/i18n.tsx +10 -5
- package/corpus/core/src/client/sse-event-processor.ts +147 -5
- package/corpus/core/src/file-upload/builder.ts +35 -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/scripts/call-agent.ts +7 -2
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +161 -12
- package/corpus/templates/analytics/changelog/2026-07-01-session-replays-load-large-production-recordings-from-scoped.md +6 -0
- 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/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/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 +65 -1
- 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-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 +14 -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/design/changelog/2026-07-01-design-recovers-sooner-when-an-agent-gets-stuck-preparing-a-.md +6 -0
- package/corpus/templates/plan/app/global.css +1 -1
- 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/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/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/collab/routes.d.ts +1 -1
- package/dist/file-upload/builder.d.ts.map +1 -1
- package/dist/file-upload/builder.js +22 -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/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/package.json +1 -1
|
@@ -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" ||
|
|
@@ -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);
|
|
@@ -86,6 +95,7 @@ async function uploadLargeFileViaSignedUrl(
|
|
|
86
95
|
privateKey,
|
|
87
96
|
assetId,
|
|
88
97
|
input.filename,
|
|
98
|
+
{ skipCompressionWait: input.skipCompressionWait },
|
|
89
99
|
);
|
|
90
100
|
console.log(`[builder-upload] done [${assetId}]: ${url}`);
|
|
91
101
|
return { url, id, provider: "builder" };
|
|
@@ -139,19 +149,21 @@ async function completeBuilderUpload(
|
|
|
139
149
|
privateKey: string,
|
|
140
150
|
assetId: string,
|
|
141
151
|
filename: string | undefined,
|
|
152
|
+
options?: { skipCompressionWait?: boolean },
|
|
142
153
|
): Promise<{ url: string; id?: string }> {
|
|
143
154
|
const host = builderUploadHost();
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
155
|
+
const url = new URL("/api/v1/upload/complete", host);
|
|
156
|
+
if (options?.skipCompressionWait) {
|
|
157
|
+
url.searchParams.set("skipCompressionWait", "true");
|
|
158
|
+
}
|
|
159
|
+
const res = await fetchWithTimeout(url.toString(), {
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers: {
|
|
162
|
+
Authorization: `Bearer ${privateKey}`,
|
|
163
|
+
"Content-Type": "application/json",
|
|
153
164
|
},
|
|
154
|
-
|
|
165
|
+
body: JSON.stringify({ assetId, name: filename }),
|
|
166
|
+
});
|
|
155
167
|
await assertOk(res, "Builder.io upload complete failed");
|
|
156
168
|
const json = (await res.json()) as { url?: string; id?: string };
|
|
157
169
|
if (!json.url) throw new Error("Builder.io upload/complete returned no URL");
|
|
@@ -204,7 +216,8 @@ export const builderFileUploadProvider: FileUploadProvider = {
|
|
|
204
216
|
id: "builder",
|
|
205
217
|
name: "Builder.io",
|
|
206
218
|
isConfigured: () => !!process.env.BUILDER_PRIVATE_KEY,
|
|
207
|
-
upload: async (
|
|
219
|
+
upload: async (input: FileUploadInput) => {
|
|
220
|
+
const { data, filename, mimeType } = input;
|
|
208
221
|
const { resolveBuilderPrivateKey } =
|
|
209
222
|
await import("../server/credential-provider.js");
|
|
210
223
|
const privateKey = await resolveBuilderPrivateKey();
|
|
@@ -226,9 +239,9 @@ export const builderFileUploadProvider: FileUploadProvider = {
|
|
|
226
239
|
data instanceof Uint8Array ? data : new Uint8Array(data as any);
|
|
227
240
|
const mb = (bytes.byteLength / (1024 * 1024)).toFixed(1);
|
|
228
241
|
|
|
229
|
-
if (bytes
|
|
242
|
+
if (shouldUseSignedUrlUpload(bytes, bareMimeType)) {
|
|
230
243
|
return uploadLargeFileViaSignedUrl(
|
|
231
|
-
|
|
244
|
+
input,
|
|
232
245
|
privateKey,
|
|
233
246
|
bareMimeType,
|
|
234
247
|
bytes,
|
|
@@ -241,6 +254,9 @@ export const builderFileUploadProvider: FileUploadProvider = {
|
|
|
241
254
|
|
|
242
255
|
const url = new URL("/api/v1/upload", builderUploadHost());
|
|
243
256
|
if (filename) url.searchParams.set("name", filename);
|
|
257
|
+
if (input.skipCompressionWait) {
|
|
258
|
+
url.searchParams.set("skipCompressionWait", "true");
|
|
259
|
+
}
|
|
244
260
|
|
|
245
261
|
const response = await uploadSmallFile(url, {
|
|
246
262
|
method: "POST",
|
|
@@ -366,7 +382,7 @@ export const builderFileUploadProvider: FileUploadProvider = {
|
|
|
366
382
|
: new Error("GCS PUT failed after retries");
|
|
367
383
|
},
|
|
368
384
|
|
|
369
|
-
async completeSession(session, filename) {
|
|
385
|
+
async completeSession(session, filename, options) {
|
|
370
386
|
const { resolveBuilderPrivateKey } =
|
|
371
387
|
await import("../server/credential-provider.js");
|
|
372
388
|
const privateKey = await resolveBuilderPrivateKey();
|
|
@@ -378,6 +394,11 @@ export const builderFileUploadProvider: FileUploadProvider = {
|
|
|
378
394
|
privateKey,
|
|
379
395
|
assetId,
|
|
380
396
|
filename,
|
|
397
|
+
{
|
|
398
|
+
skipCompressionWait:
|
|
399
|
+
options?.skipCompressionWait ||
|
|
400
|
+
session.meta.skipCompressionWait === true,
|
|
401
|
+
},
|
|
381
402
|
);
|
|
382
403
|
console.log(`[builder-resumable] upload complete: ${url}`);
|
|
383
404
|
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
|
};
|
|
@@ -146,6 +146,15 @@ function scopedCacheKey(
|
|
|
146
146
|
].join("|");
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
function authTokenAttempts(auth: {
|
|
150
|
+
apiKey?: string;
|
|
151
|
+
apiKeyFallbacks?: string[];
|
|
152
|
+
}): string[] {
|
|
153
|
+
return [auth.apiKey, ...(auth.apiKeyFallbacks ?? [])].filter(
|
|
154
|
+
(token): token is string => typeof token === "string" && token.length > 0,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
149
158
|
function serviceScopedCacheKey(origin: string, orgId: string): string {
|
|
150
159
|
return [origin, `service-org:${orgId}`].join("|");
|
|
151
160
|
}
|
|
@@ -200,7 +209,8 @@ export async function fetchOrgApps(opts?: {
|
|
|
200
209
|
const auth = serviceOrgId
|
|
201
210
|
? await resolveOrgDirectoryServiceAuth(serviceOrgId)
|
|
202
211
|
: await resolveOrgDirectoryCallerAuth();
|
|
203
|
-
|
|
212
|
+
const attempts = authTokenAttempts(auth);
|
|
213
|
+
if (attempts.length === 0) {
|
|
204
214
|
// No signed token available (no A2A secret / no caller identity) — the
|
|
205
215
|
// directory requires the org bearer, so degrade silently to local-only.
|
|
206
216
|
return [];
|
|
@@ -215,19 +225,23 @@ export async function fetchOrgApps(opts?: {
|
|
|
215
225
|
}
|
|
216
226
|
}
|
|
217
227
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
228
|
+
for (let i = 0; i < attempts.length; i++) {
|
|
229
|
+
const res = await fetch(`${origin}/_agent-native/org/apps`, {
|
|
230
|
+
method: "GET",
|
|
231
|
+
headers: {
|
|
232
|
+
Authorization: `Bearer ${attempts[i]}`,
|
|
233
|
+
Accept: "application/json",
|
|
234
|
+
},
|
|
235
|
+
signal: AbortSignal.timeout(4000),
|
|
236
|
+
});
|
|
237
|
+
if (res.ok) {
|
|
238
|
+
const json = (await res.json()) as { apps?: unknown };
|
|
239
|
+
const list = Array.isArray(json?.apps) ? json.apps : [];
|
|
240
|
+
apps = list.map(normalizeApp).filter((a): a is OrgApp => a !== null);
|
|
241
|
+
ttl = SUCCESS_TTL_MS;
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
if (res.status !== 401 || i >= attempts.length - 1) break;
|
|
231
245
|
}
|
|
232
246
|
// Non-2xx ⇒ leave apps=[] with the short EMPTY_TTL (silent degrade).
|
|
233
247
|
} catch {
|
|
@@ -252,6 +266,7 @@ export function _resetOrgDirectoryCache(): void {
|
|
|
252
266
|
|
|
253
267
|
async function resolveOrgDirectoryCallerAuth(): Promise<{
|
|
254
268
|
apiKey?: string;
|
|
269
|
+
apiKeyFallbacks?: string[];
|
|
255
270
|
userEmail?: string;
|
|
256
271
|
orgId?: string;
|
|
257
272
|
orgDomain?: string;
|
|
@@ -266,6 +281,7 @@ async function resolveOrgDirectoryCallerAuth(): Promise<{
|
|
|
266
281
|
|
|
267
282
|
async function resolveOrgDirectoryServiceAuth(orgId: string): Promise<{
|
|
268
283
|
apiKey?: string;
|
|
284
|
+
apiKeyFallbacks?: string[];
|
|
269
285
|
userEmail?: string;
|
|
270
286
|
orgId?: string;
|
|
271
287
|
orgDomain?: string;
|
|
@@ -285,12 +301,42 @@ async function resolveOrgDirectoryServiceAuth(orgId: string): Promise<{
|
|
|
285
301
|
import("./connect-store.js"),
|
|
286
302
|
]);
|
|
287
303
|
const userEmail = serviceIdentityEmail("mcp-client", trimmedOrgId);
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
|
|
304
|
+
const apiKeyAttempts: string[] = [];
|
|
305
|
+
const addApiKeyAttempt = (token: string | undefined) => {
|
|
306
|
+
if (!token || apiKeyAttempts.includes(token)) return;
|
|
307
|
+
apiKeyAttempts.push(token);
|
|
308
|
+
};
|
|
309
|
+
if (process.env.A2A_SECRET?.trim()) {
|
|
310
|
+
try {
|
|
311
|
+
addApiKeyAttempt(
|
|
312
|
+
await signA2AToken(userEmail, orgDomain, orgSecret, {
|
|
313
|
+
expiresIn: "5m",
|
|
314
|
+
preferGlobalSecret: true,
|
|
315
|
+
extraClaims: { org_id: trimmedOrgId },
|
|
316
|
+
}),
|
|
317
|
+
);
|
|
318
|
+
} catch {}
|
|
319
|
+
}
|
|
320
|
+
if (orgSecret) {
|
|
321
|
+
try {
|
|
322
|
+
addApiKeyAttempt(
|
|
323
|
+
await signA2AToken(userEmail, orgDomain, orgSecret, {
|
|
324
|
+
expiresIn: "5m",
|
|
325
|
+
preferGlobalSecret: false,
|
|
326
|
+
extraClaims: { org_id: trimmedOrgId },
|
|
327
|
+
}),
|
|
328
|
+
);
|
|
329
|
+
} catch {}
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
apiKey: apiKeyAttempts[0],
|
|
333
|
+
...(apiKeyAttempts.length > 1
|
|
334
|
+
? { apiKeyFallbacks: apiKeyAttempts.slice(1) }
|
|
335
|
+
: {}),
|
|
336
|
+
userEmail,
|
|
337
|
+
orgId: trimmedOrgId,
|
|
338
|
+
orgDomain,
|
|
339
|
+
};
|
|
294
340
|
} catch {
|
|
295
341
|
return { orgId: trimmedOrgId, orgDomain };
|
|
296
342
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
A2ATaskTimeoutError,
|
|
5
|
+
callAgent,
|
|
6
|
+
shouldPreferGlobalA2ASecret,
|
|
7
|
+
signA2AToken,
|
|
8
|
+
} from "../a2a/client.js";
|
|
4
9
|
import {
|
|
5
10
|
formatLlmCredentialErrorMessage,
|
|
6
11
|
isLlmCredentialError,
|
|
@@ -162,7 +167,7 @@ export async function run(
|
|
|
162
167
|
callerOrgSecret,
|
|
163
168
|
{
|
|
164
169
|
expiresIn: INTEGRATION_A2A_TOKEN_TTL,
|
|
165
|
-
preferGlobalSecret:
|
|
170
|
+
preferGlobalSecret: shouldPreferGlobalA2ASecret(callerOrgSecret),
|
|
166
171
|
},
|
|
167
172
|
);
|
|
168
173
|
} catch {}
|