@aexhq/sdk 0.13.10 → 0.15.0

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/dist/client.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { AexError, DEFAULT_CREDENTIAL_MODE, DEFAULT_RUN_PROVIDER, HttpClient, RUNTIME_KINDS, RunStateError, operations, parseCredentialMode, streamCoordinatorEvents, TERMINAL_RUN_STATUSES } from "./_contracts/index.js";
2
- import { request as httpRequest } from "node:http";
3
- import { request as httpsRequest } from "node:https";
4
2
  import { AgentsMd } from "./agents-md.js";
3
+ import { uploadAsset } from "./asset-upload.js";
5
4
  import { File } from "./file.js";
6
5
  import { McpServer } from "./mcp-server.js";
7
6
  import { splitProxyEndpoints } from "./proxy-endpoint.js";
@@ -142,7 +141,7 @@ export class FilesClient {
142
141
  */
143
142
  export class AgentExecutor {
144
143
  #http;
145
- /** The same fetch the HttpClient uses, kept for direct bootstrap uploads. */
144
+ /** The same fetch the HttpClient uses, threaded into `_uploadAsset`. */
146
145
  #fetch;
147
146
  skills;
148
147
  agentsMd;
@@ -174,10 +173,10 @@ export class AgentExecutor {
174
173
  * NOTE (tech-debt): this is part of the legacy workspace-skill upload
175
174
  * surface (`SkillsClient` + `operations.createSkillBundle` + the TUS
176
175
  * chunked path in asset-upload.ts). The live submit path materializes
177
- * inline skills via `uploadAsset` instead; `Skill` no longer
178
- * exposes `.upload()`/`.fromId()`. This surface is retained pending a
179
- * deliberate deprecation pass (it still threads into the CLI host
180
- * commands), tracked in the remediation plan as item 4a.
176
+ * inline skills via `uploadAsset` instead; `Skill.upload(client)`
177
+ * pre-stages a draft explicitly for reuse. This surface is retained
178
+ * pending a deliberate deprecation pass (it still threads into the CLI
179
+ * host commands), tracked in the remediation plan as item 4a.
181
180
  */
182
181
  async _uploadSkillBundle(args) {
183
182
  return this.skills._uploadSkillBundle(args);
@@ -198,6 +197,21 @@ export class AgentExecutor {
198
197
  async _uploadFile(args) {
199
198
  return this.files._uploadFile(args);
200
199
  }
200
+ /**
201
+ * Internal: materialize raw bytes to the content-addressable asset store
202
+ * (`/assets/presign` → PUT → `/assets/finalize`). Used by `Skill.upload(this)`
203
+ * to pre-upload a draft skill bundle so a later run carries only a plain
204
+ * `kind:"asset"` ref. NOT part of the public API.
205
+ */
206
+ async _uploadAsset(args) {
207
+ return uploadAsset({
208
+ http: this.#http,
209
+ bytes: args.bytes,
210
+ hash: args.hash,
211
+ ...(args.contentType ? { contentType: args.contentType } : {}),
212
+ ...(this.#fetch ? { fetch: this.#fetch } : {})
213
+ });
214
+ }
201
215
  /**
202
216
  * Submit a run and wait for it to reach a terminal state. Returns the
203
217
  * final `Run` record. For long-running flows, prefer `submitRun` +
@@ -216,15 +230,12 @@ export class AgentExecutor {
216
230
  * before sending so credentials never enter the hashed submission or
217
231
  * the run snapshot.
218
232
  *
219
- * Unstaged inline skills (`Skill.fromFiles` / `Skill.fromPath`
220
- * without a prior `.upload`) are accepted: the SDK switches to a
221
- * multipart body that carries the canonical zip bytes alongside the
222
- * JSON submission. The dashboard BFF ingests each one through the
223
- * standard workspace-skill upload pipeline (dedup by content hash;
224
- * upload via the existing two-phase pending ready flow) and
225
- * rewrites the run's `skills[]` to reference the resulting `skl_*`
226
- * ids. The bytes persist on aex; the user can browse and
227
- * download the resulting workspace skill from the dashboard.
233
+ * Unstaged inline skills / agentsMd / files (`Skill.fromFiles` /
234
+ * `Skill.fromPath` / `AgentsMd.fromContent` / `File.fromBytes` without a
235
+ * prior `.upload`) are auto-uploaded to the content-addressable asset
236
+ * store (`/assets/presign` PUT `/assets/finalize`) before `POST /runs`,
237
+ * deduped by content hash, and referenced in the submission as plain
238
+ * `{ kind:"asset" }` refs identical to a pre-staged `.upload(client)`.
228
239
  */
229
240
  async submitRun(options) {
230
241
  if (!options || typeof options !== "object") {
@@ -250,26 +261,30 @@ export class AgentExecutor {
250
261
  const prompt = normalisePrompt(options.prompt);
251
262
  const { endpoints: proxyEndpointDeclarations, auth: proxyEndpointAuthFromInstances } = splitProxyEndpoints(options.proxyEndpoints ?? []);
252
263
  const mergedProxyAuth = mergeProxyEndpointAuth(proxyEndpointAuthFromInstances, options.secrets.proxyEndpointAuth ?? []);
253
- // Walk Skill / AgentsMd / File instances. Drafts are declared as direct
254
- // inputs on the submit request, then uploaded to the run bootstrap target
255
- // after the control plane accepts the run. Already-materialized asset refs
256
- // still pass through unchanged.
257
- const preparedSkills = prepareSkills(options.skills ?? []);
258
- const preparedAgentsMd = prepareAgentsMd(options.agentsMd ?? []);
259
- const preparedFiles = prepareFiles(options.files ?? []);
260
- const directInputs = [
261
- ...preparedSkills.directInputs,
262
- ...preparedAgentsMd.directInputs,
263
- ...preparedFiles.directInputs
264
- ];
264
+ // Validate the runtime selector before any network I/O inline drafts
265
+ // are uploaded below, so an invalid runtime must reject first rather than
266
+ // leak an asset upload.
267
+ if (options.runtime !== undefined &&
268
+ !RUNTIME_KINDS.includes(options.runtime)) {
269
+ throw new AexError("RUNTIME_UNSUPPORTED", `AgentExecutor.submitRun: runtime must be one of: ${RUNTIME_KINDS.join(", ")} ` +
270
+ `(got ${JSON.stringify(options.runtime)})`);
271
+ }
272
+ // Walk Skill / AgentsMd / File instances. Inline drafts are eagerly
273
+ // uploaded to the content-addressable asset store here (before POST /runs)
274
+ // and referenced as plain `kind:"asset"` refs. Already-materialized asset
275
+ // refs pass through unchanged.
276
+ const uploader = (args) => this._uploadAsset(args);
277
+ const preparedSkills = await prepareSkills(options.skills ?? [], uploader);
278
+ const preparedAgentsMd = await prepareAgentsMd(options.agentsMd ?? [], uploader);
279
+ const preparedFiles = await prepareFiles(options.files ?? [], uploader);
265
280
  const { submissionMcpServers, mergedMcpSecrets } = mergeMcpServers(options.mcpServers ?? [], options.secrets.mcpServers ?? []);
266
281
  const submission = {
267
282
  model: options.model,
268
283
  ...(options.system ? { system: options.system } : {}),
269
284
  prompt,
270
- skills: preparedSkills.refs,
271
- agentsMd: preparedAgentsMd.refs,
272
- files: preparedFiles.refs,
285
+ skills: preparedSkills,
286
+ agentsMd: preparedAgentsMd,
287
+ files: preparedFiles,
273
288
  // submissionMcpServers may contain workspace refs of the shape
274
289
  // {kind:"workspace", id:"mcp_..."}. The BFF runs
275
290
  // `resolveWorkspaceMcpRefsInSubmission` BEFORE the shared parser
@@ -315,30 +330,8 @@ export class AgentExecutor {
315
330
  ? { proxyEndpoints: proxyEndpointDeclarations }
316
331
  : {})
317
332
  };
318
- if (options.runtime !== undefined &&
319
- !RUNTIME_KINDS.includes(options.runtime)) {
320
- throw new AexError("RUNTIME_UNSUPPORTED", `AgentExecutor.submitRun: runtime must be one of: ${RUNTIME_KINDS.join(", ")} ` +
321
- `(got ${JSON.stringify(options.runtime)})`);
322
- }
323
- const submitRequest = directInputs.length > 0
324
- ? {
325
- ...request,
326
- bootstrapMode: "direct",
327
- directInputs: directInputs.map(({ bytes: _bytes, ...descriptor }) => descriptor)
328
- }
329
- : request;
330
- const run = await operations.submitRun(this.#http, submitRequest);
331
- const runId = getSubmittedRunId(run);
332
- if (directInputs.length > 0) {
333
- await completeDirectBootstrap({
334
- response: run,
335
- directInputs,
336
- hasRunAcceptedBootstrap: async () => hasRunAcceptedBootstrap(await operations.getRun(this.#http, runId)),
337
- ...(options.signal ? { signal: options.signal } : {}),
338
- ...(this.#fetch ? { fetch: this.#fetch } : {})
339
- });
340
- }
341
- return runId;
333
+ const run = await operations.submitRun(this.#http, request);
334
+ return getSubmittedRunId(run);
342
335
  }
343
336
  getRun(runId) {
344
337
  return operations.getRun(this.#http, runId);
@@ -531,10 +524,6 @@ export class AgentExecutor {
531
524
  // against the canonical terminal set rather than re-deriving one (which is how
532
525
  // `timed_out` got dropped from the old hardcoded list).
533
526
  const TERMINAL_STATUSES = new Set(TERMINAL_RUN_STATUSES);
534
- const DIRECT_BOOTSTRAP_RETRY_STATUSES = new Set([408, 425, 429, 502, 503, 504]);
535
- const DIRECT_BOOTSTRAP_INITIAL_BACKOFF_MS = 100;
536
- const DIRECT_BOOTSTRAP_MAX_BACKOFF_MS = 1_000;
537
- const DIRECT_BOOTSTRAP_ATTEMPT_TIMEOUT_MS = 10_000;
538
527
  function isTerminal(status) {
539
528
  return typeof status === "string" && TERMINAL_STATUSES.has(status);
540
529
  }
@@ -631,10 +620,9 @@ function normalisePrompt(input) {
631
620
  }
632
621
  return [...input];
633
622
  }
634
- /** Walk Skill[] and turn drafts into direct-bootstrap descriptors. */
635
- function prepareSkills(skills) {
623
+ /** Walk Skill[], eagerly upload drafts as assets, and return plain asset refs. */
624
+ async function prepareSkills(skills, uploader) {
636
625
  const refs = [];
637
- const directInputs = [];
638
626
  for (let i = 0; i < skills.length; i++) {
639
627
  const entry = skills[i];
640
628
  if (!(entry instanceof Skill)) {
@@ -649,18 +637,14 @@ function prepareSkills(skills) {
649
637
  if (!bundle) {
650
638
  throw new Error(`AgentExecutor.submitRun: skills[${i}] is draft but has no bytes`);
651
639
  }
652
- const input = directInputFor({
653
- role: "skill",
654
- index: i,
655
- name: bundle.name,
656
- contentHash: bundle.contentHash,
640
+ const uploaded = await uploader({
657
641
  bytes: bundle.bytes,
642
+ hash: bundle.contentHash,
658
643
  contentType: "application/zip"
659
644
  });
660
- directInputs.push(input);
661
645
  refs.push({
662
646
  kind: "asset",
663
- assetId: input.assetId,
647
+ assetId: uploaded.assetId,
664
648
  name: bundle.name
665
649
  });
666
650
  continue;
@@ -668,12 +652,11 @@ function prepareSkills(skills) {
668
652
  // Already-materialized asset ref.
669
653
  refs.push(ref);
670
654
  }
671
- return { refs, directInputs };
655
+ return refs;
672
656
  }
673
- /** Walk AgentsMd[] and turn drafts into direct-bootstrap descriptors. */
674
- function prepareAgentsMd(agentsMds) {
657
+ /** Walk AgentsMd[], eagerly upload drafts as assets, and return plain asset refs. */
658
+ async function prepareAgentsMd(agentsMds, uploader) {
675
659
  const refs = [];
676
- const directInputs = [];
677
660
  for (let i = 0; i < agentsMds.length; i++) {
678
661
  const entry = agentsMds[i];
679
662
  if (!(entry instanceof AgentsMd)) {
@@ -688,30 +671,25 @@ function prepareAgentsMd(agentsMds) {
688
671
  if (!bundle) {
689
672
  throw new Error(`AgentExecutor.submitRun: agentsMd[${i}] is draft but has no bytes`);
690
673
  }
691
- const input = directInputFor({
692
- role: "agentsMd",
693
- index: i,
694
- name: bundle.name,
695
- contentHash: bundle.contentHash,
674
+ const uploaded = await uploader({
696
675
  bytes: bundle.bytes,
676
+ hash: bundle.contentHash,
697
677
  contentType: "application/zip"
698
678
  });
699
- directInputs.push(input);
700
679
  refs.push({
701
680
  kind: "asset",
702
- assetId: input.assetId,
681
+ assetId: uploaded.assetId,
703
682
  name: bundle.name
704
683
  });
705
684
  continue;
706
685
  }
707
686
  refs.push(ref);
708
687
  }
709
- return { refs, directInputs };
688
+ return refs;
710
689
  }
711
- /** Walk File[] and turn drafts into direct-bootstrap descriptors. */
712
- function prepareFiles(files) {
690
+ /** Walk File[], eagerly upload drafts as assets, and return plain asset refs. */
691
+ async function prepareFiles(files, uploader) {
713
692
  const refs = [];
714
- const directInputs = [];
715
693
  for (let i = 0; i < files.length; i++) {
716
694
  const entry = files[i];
717
695
  if (!(entry instanceof File)) {
@@ -726,53 +704,28 @@ function prepareFiles(files) {
726
704
  if (!bundle) {
727
705
  throw new Error(`AgentExecutor.submitRun: files[${i}] is draft but has no bytes`);
728
706
  }
729
- const input = directInputFor({
730
- role: "file",
731
- index: i,
732
- name: bundle.name,
733
- contentHash: bundle.contentHash,
707
+ const uploaded = await uploader({
734
708
  bytes: bundle.bytes,
735
- contentType: "application/zip",
736
- ...(bundle.mountPath ? { mountPath: bundle.mountPath } : {})
709
+ hash: bundle.contentHash,
710
+ contentType: "application/zip"
737
711
  });
738
- directInputs.push(input);
739
712
  refs.push(bundle.mountPath !== undefined
740
713
  ? {
741
714
  kind: "asset",
742
- assetId: input.assetId,
715
+ assetId: uploaded.assetId,
743
716
  name: bundle.name,
744
717
  mountPath: bundle.mountPath
745
718
  }
746
719
  : {
747
720
  kind: "asset",
748
- assetId: input.assetId,
721
+ assetId: uploaded.assetId,
749
722
  name: bundle.name
750
723
  });
751
724
  continue;
752
725
  }
753
726
  refs.push(ref);
754
727
  }
755
- return { refs, directInputs };
756
- }
757
- function directInputFor(args) {
758
- const sha256 = args.contentHash.startsWith("sha256:")
759
- ? args.contentHash
760
- : `sha256:${args.contentHash}`;
761
- const hashHex = sha256.slice("sha256:".length);
762
- if (!/^[0-9a-f]{64}$/.test(hashHex)) {
763
- throw new Error(`AgentExecutor.submitRun: ${args.role}[${args.index}] content hash must be sha256:<64-hex>`);
764
- }
765
- return {
766
- inputId: `input_${args.role}_${args.index}_${hashHex}`,
767
- role: args.role,
768
- assetId: `asset_${hashHex}`,
769
- name: args.name,
770
- sha256,
771
- sizeBytes: args.bytes.byteLength,
772
- contentType: args.contentType,
773
- ...(args.mountPath ? { mountPath: args.mountPath } : {}),
774
- bytes: args.bytes
775
- };
728
+ return refs;
776
729
  }
777
730
  function getSubmittedRunId(response) {
778
731
  const id = response.id ?? response.runId;
@@ -781,477 +734,6 @@ function getSubmittedRunId(response) {
781
734
  }
782
735
  return id;
783
736
  }
784
- async function completeDirectBootstrap(args) {
785
- const token = args.response.bootstrapToken;
786
- const statusUrl = args.response.bootstrapStatusUrl;
787
- if (typeof token !== "string" || token.length === 0 || typeof statusUrl !== "string" || statusUrl.length === 0) {
788
- return;
789
- }
790
- const fetchImpl = args.fetch ?? globalThis.fetch.bind(globalThis);
791
- const useNodeTransport = args.fetch === undefined;
792
- const deadline = bootstrapDeadline(args.response.bootstrapExpiresAt);
793
- let target;
794
- try {
795
- target = resolveBootstrapReady(args.response) ?? await pollBootstrapReady({
796
- fetchImpl,
797
- statusUrl,
798
- token,
799
- deadline,
800
- ...(args.signal ? { signal: args.signal } : {})
801
- });
802
- for (const input of args.directInputs) {
803
- await uploadDirectInput({
804
- fetchImpl,
805
- target,
806
- token,
807
- input,
808
- deadline,
809
- useNodeTransport,
810
- ...(args.signal ? { signal: args.signal } : {})
811
- });
812
- }
813
- await commitDirectInputs({
814
- fetchImpl,
815
- target,
816
- token,
817
- inputs: args.directInputs,
818
- deadline,
819
- useNodeTransport,
820
- ...(args.hasRunAcceptedBootstrap ? { hasRunAcceptedBootstrap: args.hasRunAcceptedBootstrap } : {}),
821
- ...(args.signal ? { signal: args.signal } : {})
822
- });
823
- }
824
- catch (err) {
825
- await abortDirectBootstrap({
826
- fetchImpl,
827
- token,
828
- statusUrl,
829
- ...(target ? { target } : {}),
830
- ...(args.signal ? { signal: args.signal } : {})
831
- }).catch(() => undefined);
832
- throw err;
833
- }
834
- }
835
- async function pollBootstrapReady(args) {
836
- while (!args.signal?.aborted) {
837
- if (Date.now() >= args.deadline) {
838
- throw new Error("AgentExecutor.submitRun: bootstrap target did not become ready before it expired");
839
- }
840
- const res = await args.fetchImpl(args.statusUrl, {
841
- method: "GET",
842
- headers: { authorization: `Bearer ${args.token}`, accept: "application/json" },
843
- ...(args.signal ? { signal: args.signal } : {})
844
- });
845
- if (res.ok) {
846
- const body = await res.json();
847
- const ready = resolveBootstrapReady(body);
848
- if (ready)
849
- return ready;
850
- }
851
- else if (![202, 404, 425].includes(res.status)) {
852
- const detail = await res.text().catch(() => "");
853
- throw new Error(`AgentExecutor.submitRun: bootstrap status failed with ${res.status}` +
854
- (detail ? `: ${detail.slice(0, 300)}` : ""));
855
- }
856
- await sleep(250, args.signal);
857
- }
858
- throw new Error("AgentExecutor.submitRun: aborted");
859
- }
860
- function resolveBootstrapReady(value) {
861
- if (!value || typeof value !== "object")
862
- return undefined;
863
- const record = value;
864
- const base = typeof record.uploadBaseUrl === "string"
865
- ? record.uploadBaseUrl
866
- : typeof record.bootstrapUploadBaseUrl === "string"
867
- ? record.bootstrapUploadBaseUrl
868
- : undefined;
869
- if (!base)
870
- return undefined;
871
- const routingHeaders = isStringRecord(record.routingHeaders) ? record.routingHeaders : undefined;
872
- return {
873
- uploadBaseUrl: stripTrailingSlash(base),
874
- ...(routingHeaders ? { routingHeaders } : {}),
875
- ...(typeof record.abortUrl === "string" ? { abortUrl: record.abortUrl } : {}),
876
- ...(typeof record.commitUrl === "string" ? { commitUrl: record.commitUrl } : {})
877
- };
878
- }
879
- async function uploadDirectInput(args) {
880
- let backoffMs = DIRECT_BOOTSTRAP_INITIAL_BACKOFF_MS;
881
- let lastError;
882
- const uploadUrl = `${args.target.uploadBaseUrl}/inputs/${encodeURIComponent(args.input.inputId)}`;
883
- while (!args.signal?.aborted) {
884
- if (Date.now() >= args.deadline) {
885
- throw lastError ?? new Error("AgentExecutor.submitRun: bootstrap input upload did not complete before it expired");
886
- }
887
- let res;
888
- try {
889
- res = await directBootstrapFetch({
890
- fetchImpl: args.fetchImpl,
891
- url: uploadUrl,
892
- deadline: args.deadline,
893
- operation: `input upload for ${args.input.inputId}`,
894
- useNodeTransport: args.useNodeTransport,
895
- ...(args.signal ? { signal: args.signal } : {}),
896
- init: {
897
- method: "PUT",
898
- headers: {
899
- authorization: `Bearer ${args.token}`,
900
- "content-type": args.input.contentType,
901
- "x-aex-input-sha256": args.input.sha256,
902
- "x-aex-input-size": String(args.input.sizeBytes),
903
- ...(args.target.routingHeaders ?? {})
904
- },
905
- body: args.input.bytes
906
- }
907
- });
908
- }
909
- catch (err) {
910
- if (args.signal?.aborted)
911
- throw err;
912
- lastError = bootstrapNetworkError(`input upload for ${args.input.inputId}`, err);
913
- backoffMs = await waitForBootstrapRetry(backoffMs, args.deadline, args.signal, lastError);
914
- continue;
915
- }
916
- if (res.ok)
917
- return;
918
- const detail = await res.text().catch(() => "");
919
- const err = new Error(`AgentExecutor.submitRun: bootstrap input upload failed for ${args.input.inputId} ` +
920
- `(status ${res.status})${detail ? `: ${detail.slice(0, 300)}` : ""}`);
921
- if (!DIRECT_BOOTSTRAP_RETRY_STATUSES.has(res.status)) {
922
- throw err;
923
- }
924
- lastError = err;
925
- backoffMs = await waitForBootstrapRetry(backoffMs, args.deadline, args.signal, lastError);
926
- }
927
- throw lastError ?? new Error("AgentExecutor.submitRun: aborted");
928
- }
929
- async function waitForBootstrapRetry(backoffMs, deadline, signal, lastError) {
930
- const remainingMs = deadline - Date.now();
931
- if (remainingMs <= 0)
932
- throw lastError;
933
- await sleep(Math.min(backoffMs, remainingMs), signal);
934
- return Math.min(backoffMs * 2, DIRECT_BOOTSTRAP_MAX_BACKOFF_MS);
935
- }
936
- async function directBootstrapFetch(args) {
937
- const remainingMs = args.deadline - Date.now();
938
- if (remainingMs <= 0) {
939
- throw new Error(`AgentExecutor.submitRun: bootstrap ${args.operation} did not complete before it expired`);
940
- }
941
- const timeoutMs = Math.min(DIRECT_BOOTSTRAP_ATTEMPT_TIMEOUT_MS, remainingMs);
942
- if (args.useNodeTransport) {
943
- return nodeDirectBootstrapFetch({
944
- url: args.url,
945
- init: args.init,
946
- timeoutMs,
947
- operation: args.operation,
948
- ...(args.hasRunAcceptedBootstrap ? { hasRunAcceptedBootstrap: args.hasRunAcceptedBootstrap } : {}),
949
- ...(args.signal ? { signal: args.signal } : {})
950
- });
951
- }
952
- const controller = new AbortController();
953
- const onAbort = () => controller.abort();
954
- args.signal?.addEventListener("abort", onAbort, { once: true });
955
- const fetchPromise = args.fetchImpl(args.url, { ...args.init, signal: controller.signal });
956
- fetchPromise.catch(() => undefined);
957
- let timer;
958
- const timeoutPromise = new Promise((_resolve, reject) => {
959
- timer = setTimeout(() => {
960
- controller.abort();
961
- reject(new Error(`AgentExecutor.submitRun: bootstrap ${args.operation} timed out after ${timeoutMs}ms`));
962
- }, timeoutMs);
963
- });
964
- try {
965
- return await Promise.race([fetchPromise, timeoutPromise]);
966
- }
967
- catch (err) {
968
- if (args.signal?.aborted)
969
- throw err;
970
- throw err;
971
- }
972
- finally {
973
- clearTimeout(timer);
974
- args.signal?.removeEventListener("abort", onAbort);
975
- }
976
- }
977
- async function nodeDirectBootstrapFetch(args) {
978
- const url = new URL(args.url);
979
- const requestImpl = url.protocol === "http:" ? httpRequest : url.protocol === "https:" ? httpsRequest : undefined;
980
- if (!requestImpl) {
981
- throw new Error(`AgentExecutor.submitRun: bootstrap ${args.operation} URL must use http or https`);
982
- }
983
- return await new Promise((resolve, reject) => {
984
- let settled = false;
985
- let timer;
986
- let activeResponse;
987
- let acceptedPollStarted = false;
988
- const requestHeaders = normalizeBootstrapRequestHeaders(args.init.headers);
989
- if (!hasHeader(requestHeaders, "connection")) {
990
- requestHeaders.connection = "close";
991
- }
992
- const request = requestImpl(url, {
993
- method: args.init.method ?? "GET",
994
- headers: requestHeaders,
995
- agent: false
996
- }, (res) => {
997
- activeResponse = res;
998
- const chunks = [];
999
- res.on("data", (chunk) => {
1000
- chunks.push(typeof chunk === "string" ? new TextEncoder().encode(chunk) : new Uint8Array(chunk));
1001
- });
1002
- res.on("end", () => {
1003
- if (settled)
1004
- return;
1005
- settled = true;
1006
- cleanup();
1007
- activeResponse = undefined;
1008
- request.destroy();
1009
- resolve(new Response(concatUint8Arrays(chunks), {
1010
- status: res.statusCode ?? 599,
1011
- statusText: res.statusMessage ?? "",
1012
- headers: normalizeBootstrapResponseHeaders(res.headers)
1013
- }));
1014
- });
1015
- res.on("error", fail);
1016
- });
1017
- function cleanup() {
1018
- if (timer)
1019
- clearTimeout(timer);
1020
- args.signal?.removeEventListener("abort", onAbort);
1021
- }
1022
- function fail(err) {
1023
- if (settled)
1024
- return;
1025
- settled = true;
1026
- cleanup();
1027
- activeResponse?.destroy(err);
1028
- request.destroy(err);
1029
- reject(err);
1030
- }
1031
- function timeoutError() {
1032
- return new Error(`AgentExecutor.submitRun: bootstrap ${args.operation} timed out after ${args.timeoutMs}ms`);
1033
- }
1034
- function onAbort() {
1035
- fail(new Error(`AgentExecutor.submitRun: bootstrap ${args.operation} aborted`));
1036
- }
1037
- function startAcceptedPoll() {
1038
- if (!args.hasRunAcceptedBootstrap || acceptedPollStarted)
1039
- return;
1040
- acceptedPollStarted = true;
1041
- void pollAcceptedBootstrapAfterRequestFinish({
1042
- check: args.hasRunAcceptedBootstrap,
1043
- isSettled: () => settled,
1044
- ...(args.signal ? { signal: args.signal } : {})
1045
- })
1046
- .then((accepted) => {
1047
- if (!accepted || settled)
1048
- return;
1049
- settled = true;
1050
- cleanup();
1051
- activeResponse?.destroy();
1052
- request.destroy();
1053
- resolve(new Response(JSON.stringify({ ok: true }), {
1054
- status: 200,
1055
- headers: { "content-type": "application/json" }
1056
- }));
1057
- })
1058
- .catch(fail);
1059
- }
1060
- timer = setTimeout(() => fail(timeoutError()), args.timeoutMs);
1061
- request.setTimeout(args.timeoutMs, () => fail(timeoutError()));
1062
- request.on("error", fail);
1063
- request.on("finish", startAcceptedPoll);
1064
- args.signal?.addEventListener("abort", onAbort, { once: true });
1065
- try {
1066
- const body = normalizeBootstrapRequestBody(args.init.body);
1067
- if (body !== undefined)
1068
- request.write(body);
1069
- request.end();
1070
- startAcceptedPoll();
1071
- }
1072
- catch (err) {
1073
- fail(err instanceof Error ? err : new Error(String(err)));
1074
- }
1075
- });
1076
- }
1077
- async function pollAcceptedBootstrapAfterRequestFinish(args) {
1078
- while (!args.isSettled()) {
1079
- if (await checkRunAcceptedBootstrap(args.check))
1080
- return true;
1081
- await sleep(250, args.signal);
1082
- }
1083
- return false;
1084
- }
1085
- function normalizeBootstrapRequestHeaders(headers) {
1086
- const result = {};
1087
- if (!headers)
1088
- return result;
1089
- if (headers instanceof Headers) {
1090
- headers.forEach((value, key) => {
1091
- result[key] = value;
1092
- });
1093
- return result;
1094
- }
1095
- if (Array.isArray(headers)) {
1096
- for (const [key, value] of headers)
1097
- result[key] = value;
1098
- return result;
1099
- }
1100
- for (const [key, value] of Object.entries(headers)) {
1101
- if (value !== undefined)
1102
- result[key] = String(value);
1103
- }
1104
- return result;
1105
- }
1106
- function hasHeader(headers, name) {
1107
- const normalized = name.toLowerCase();
1108
- return Object.keys(headers).some((key) => key.toLowerCase() === normalized);
1109
- }
1110
- function normalizeBootstrapResponseHeaders(headers) {
1111
- const result = new Headers();
1112
- for (const [key, value] of Object.entries(headers)) {
1113
- if (value === undefined)
1114
- continue;
1115
- if (Array.isArray(value)) {
1116
- for (const item of value)
1117
- result.append(key, item);
1118
- }
1119
- else {
1120
- result.set(key, value);
1121
- }
1122
- }
1123
- return result;
1124
- }
1125
- function normalizeBootstrapRequestBody(body) {
1126
- if (body === null || body === undefined)
1127
- return undefined;
1128
- if (typeof body === "string")
1129
- return body;
1130
- if (body instanceof Uint8Array)
1131
- return body;
1132
- if (body instanceof ArrayBuffer)
1133
- return new Uint8Array(body);
1134
- if (ArrayBuffer.isView(body))
1135
- return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
1136
- if (body instanceof URLSearchParams)
1137
- return body.toString();
1138
- throw new Error("AgentExecutor.submitRun: unsupported bootstrap request body type");
1139
- }
1140
- function concatUint8Arrays(chunks) {
1141
- const total = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
1142
- const result = new Uint8Array(total);
1143
- let offset = 0;
1144
- for (const chunk of chunks) {
1145
- result.set(chunk, offset);
1146
- offset += chunk.byteLength;
1147
- }
1148
- return result;
1149
- }
1150
- function bootstrapNetworkError(operation, value) {
1151
- const message = value instanceof Error ? value.message : String(value);
1152
- return new Error(`AgentExecutor.submitRun: bootstrap ${operation} failed: ${message}`);
1153
- }
1154
- async function commitDirectInputs(args) {
1155
- const commitUrl = args.target.commitUrl ?? `${args.target.uploadBaseUrl}/commit`;
1156
- let backoffMs = DIRECT_BOOTSTRAP_INITIAL_BACKOFF_MS;
1157
- let lastError;
1158
- while (!args.signal?.aborted) {
1159
- if (Date.now() >= args.deadline) {
1160
- throw lastError ?? new Error("AgentExecutor.submitRun: bootstrap commit did not complete before it expired");
1161
- }
1162
- let res;
1163
- try {
1164
- res = await directBootstrapFetch({
1165
- fetchImpl: args.fetchImpl,
1166
- url: commitUrl,
1167
- deadline: args.deadline,
1168
- operation: "commit",
1169
- useNodeTransport: args.useNodeTransport,
1170
- ...(args.hasRunAcceptedBootstrap ? { hasRunAcceptedBootstrap: args.hasRunAcceptedBootstrap } : {}),
1171
- ...(args.signal ? { signal: args.signal } : {}),
1172
- init: {
1173
- method: "POST",
1174
- headers: {
1175
- authorization: `Bearer ${args.token}`,
1176
- "content-type": "application/json",
1177
- accept: "application/json",
1178
- ...(args.target.routingHeaders ?? {})
1179
- },
1180
- body: JSON.stringify({
1181
- inputs: args.inputs.map((input) => ({
1182
- inputId: input.inputId,
1183
- sha256: input.sha256,
1184
- sizeBytes: input.sizeBytes
1185
- }))
1186
- })
1187
- }
1188
- });
1189
- }
1190
- catch (err) {
1191
- if (args.signal?.aborted)
1192
- throw err;
1193
- lastError = bootstrapNetworkError("commit", err);
1194
- if (await checkRunAcceptedBootstrap(args.hasRunAcceptedBootstrap))
1195
- return;
1196
- backoffMs = await waitForBootstrapRetry(backoffMs, args.deadline, args.signal, lastError);
1197
- continue;
1198
- }
1199
- if (res.ok)
1200
- return;
1201
- const detail = await res.text().catch(() => "");
1202
- const err = new Error(`AgentExecutor.submitRun: bootstrap commit failed with ${res.status}` +
1203
- (detail ? `: ${detail.slice(0, 300)}` : ""));
1204
- if (!DIRECT_BOOTSTRAP_RETRY_STATUSES.has(res.status)) {
1205
- throw err;
1206
- }
1207
- lastError = err;
1208
- if (await checkRunAcceptedBootstrap(args.hasRunAcceptedBootstrap))
1209
- return;
1210
- backoffMs = await waitForBootstrapRetry(backoffMs, args.deadline, args.signal, lastError);
1211
- }
1212
- throw lastError ?? new Error("AgentExecutor.submitRun: aborted");
1213
- }
1214
- async function checkRunAcceptedBootstrap(check) {
1215
- if (!check)
1216
- return false;
1217
- return await check().catch(() => false);
1218
- }
1219
- function hasRunAcceptedBootstrap(run) {
1220
- if (run.status === "succeeded" || run.status === "cancelled" || run.status === "timed_out")
1221
- return true;
1222
- if (run.status === "failed")
1223
- return run.terminalAt !== undefined && run.terminalAt !== null;
1224
- return run.status === "running" || run.status === "provider_running";
1225
- }
1226
- async function abortDirectBootstrap(args) {
1227
- const abortUrl = args.target?.abortUrl ?? `${args.statusUrl.replace(/\/status$/, "")}/abort`;
1228
- await args.fetchImpl(abortUrl, {
1229
- method: "POST",
1230
- headers: {
1231
- authorization: `Bearer ${args.token}`,
1232
- accept: "application/json",
1233
- ...(args.target?.routingHeaders ?? {})
1234
- },
1235
- ...(args.signal ? { signal: args.signal } : {})
1236
- });
1237
- }
1238
- function bootstrapDeadline(expiresAt) {
1239
- if (typeof expiresAt === "string") {
1240
- const parsed = Date.parse(expiresAt);
1241
- if (Number.isFinite(parsed))
1242
- return parsed;
1243
- }
1244
- return Date.now() + 60_000;
1245
- }
1246
- function isStringRecord(value) {
1247
- return Boolean(value &&
1248
- typeof value === "object" &&
1249
- !Array.isArray(value) &&
1250
- Object.values(value).every((entry) => typeof entry === "string"));
1251
- }
1252
- function stripTrailingSlash(s) {
1253
- return s.endsWith("/") ? s.slice(0, -1) : s;
1254
- }
1255
737
  function mergeMcpServers(inputs, explicitSecrets) {
1256
738
  const submissionMcpServers = [];
1257
739
  const secretByName = new Map();