@aexhq/sdk 0.13.9 → 0.14.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/README.md +4 -3
- package/dist/_contracts/internal.d.ts +1 -0
- package/dist/_contracts/internal.js +8 -0
- package/dist/_contracts/provider-support.d.ts +12 -6
- package/dist/_contracts/provider-support.js +25 -8
- package/dist/_contracts/run-config.js +5 -3
- package/dist/_contracts/submission.d.ts +37 -28
- package/dist/_contracts/submission.js +33 -70
- package/dist/cli.mjs +26 -12
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +29 -19
- package/dist/client.js +73 -600
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/skill.d.ts +32 -2
- package/dist/skill.js +31 -2
- package/dist/skill.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/cleanup.md +1 -1
- package/docs/credentials.md +7 -11
- package/docs/events.md +1 -1
- package/docs/outputs.md +1 -1
- package/docs/provider-runtime-capabilities.md +7 -7
- package/docs/quickstart.md +4 -4
- package/docs/release.md +18 -12
- package/docs/run-config.md +1 -1
- package/docs/skills.md +29 -9
- package/package.json +2 -2
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,
|
|
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`
|
|
178
|
-
*
|
|
179
|
-
* deliberate deprecation pass (it still threads into the CLI
|
|
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` /
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
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") {
|
|
@@ -238,20 +249,11 @@ export class AgentExecutor {
|
|
|
238
249
|
if (!options.secrets) {
|
|
239
250
|
throw new Error("AgentExecutor.submitRun: secrets is required");
|
|
240
251
|
}
|
|
241
|
-
// The
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (!providerSecret?.apiKey) {
|
|
247
|
-
throw new Error(`AgentExecutor.submitRun: secrets.${provider}.apiKey is required`);
|
|
248
|
-
}
|
|
249
|
-
for (const other of ["anthropic", "deepseek", "openai", "gemini", "mistral"]) {
|
|
250
|
-
if (other === provider)
|
|
251
|
-
continue;
|
|
252
|
-
if (options.secrets[other] !== undefined) {
|
|
253
|
-
throw new Error(`AgentExecutor.submitRun: secrets.${other} is not allowed when provider is ${provider}`);
|
|
254
|
-
}
|
|
252
|
+
// The BYOK provider key (for the selected `provider`) is required. The
|
|
253
|
+
// shared parser re-runs this check on the server; failing early here
|
|
254
|
+
// gives the caller a synchronous error before any network call.
|
|
255
|
+
if (typeof options.secrets.apiKey !== "string" || !options.secrets.apiKey) {
|
|
256
|
+
throw new Error("AgentExecutor.submitRun: secrets.apiKey is required");
|
|
255
257
|
}
|
|
256
258
|
if (typeof options.model !== "string" || !options.model) {
|
|
257
259
|
throw new Error("AgentExecutor.submitRun: model is required");
|
|
@@ -259,26 +261,30 @@ export class AgentExecutor {
|
|
|
259
261
|
const prompt = normalisePrompt(options.prompt);
|
|
260
262
|
const { endpoints: proxyEndpointDeclarations, auth: proxyEndpointAuthFromInstances } = splitProxyEndpoints(options.proxyEndpoints ?? []);
|
|
261
263
|
const mergedProxyAuth = mergeProxyEndpointAuth(proxyEndpointAuthFromInstances, options.secrets.proxyEndpointAuth ?? []);
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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);
|
|
274
280
|
const { submissionMcpServers, mergedMcpSecrets } = mergeMcpServers(options.mcpServers ?? [], options.secrets.mcpServers ?? []);
|
|
275
281
|
const submission = {
|
|
276
282
|
model: options.model,
|
|
277
283
|
...(options.system ? { system: options.system } : {}),
|
|
278
284
|
prompt,
|
|
279
|
-
skills: preparedSkills
|
|
280
|
-
agentsMd: preparedAgentsMd
|
|
281
|
-
files: preparedFiles
|
|
285
|
+
skills: preparedSkills,
|
|
286
|
+
agentsMd: preparedAgentsMd,
|
|
287
|
+
files: preparedFiles,
|
|
282
288
|
// submissionMcpServers may contain workspace refs of the shape
|
|
283
289
|
// {kind:"workspace", id:"mcp_..."}. The BFF runs
|
|
284
290
|
// `resolveWorkspaceMcpRefsInSubmission` BEFORE the shared parser
|
|
@@ -324,30 +330,8 @@ export class AgentExecutor {
|
|
|
324
330
|
? { proxyEndpoints: proxyEndpointDeclarations }
|
|
325
331
|
: {})
|
|
326
332
|
};
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
throw new AexError("RUNTIME_UNSUPPORTED", `AgentExecutor.submitRun: runtime must be one of: ${RUNTIME_KINDS.join(", ")} ` +
|
|
330
|
-
`(got ${JSON.stringify(options.runtime)})`);
|
|
331
|
-
}
|
|
332
|
-
const submitRequest = directInputs.length > 0
|
|
333
|
-
? {
|
|
334
|
-
...request,
|
|
335
|
-
bootstrapMode: "direct",
|
|
336
|
-
directInputs: directInputs.map(({ bytes: _bytes, ...descriptor }) => descriptor)
|
|
337
|
-
}
|
|
338
|
-
: request;
|
|
339
|
-
const run = await operations.submitRun(this.#http, submitRequest);
|
|
340
|
-
const runId = getSubmittedRunId(run);
|
|
341
|
-
if (directInputs.length > 0) {
|
|
342
|
-
await completeDirectBootstrap({
|
|
343
|
-
response: run,
|
|
344
|
-
directInputs,
|
|
345
|
-
hasRunAcceptedBootstrap: async () => hasRunAcceptedBootstrap(await operations.getRun(this.#http, runId)),
|
|
346
|
-
...(options.signal ? { signal: options.signal } : {}),
|
|
347
|
-
...(this.#fetch ? { fetch: this.#fetch } : {})
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
return runId;
|
|
333
|
+
const run = await operations.submitRun(this.#http, request);
|
|
334
|
+
return getSubmittedRunId(run);
|
|
351
335
|
}
|
|
352
336
|
getRun(runId) {
|
|
353
337
|
return operations.getRun(this.#http, runId);
|
|
@@ -540,10 +524,6 @@ export class AgentExecutor {
|
|
|
540
524
|
// against the canonical terminal set rather than re-deriving one (which is how
|
|
541
525
|
// `timed_out` got dropped from the old hardcoded list).
|
|
542
526
|
const TERMINAL_STATUSES = new Set(TERMINAL_RUN_STATUSES);
|
|
543
|
-
const DIRECT_BOOTSTRAP_RETRY_STATUSES = new Set([408, 425, 429, 502, 503, 504]);
|
|
544
|
-
const DIRECT_BOOTSTRAP_INITIAL_BACKOFF_MS = 100;
|
|
545
|
-
const DIRECT_BOOTSTRAP_MAX_BACKOFF_MS = 1_000;
|
|
546
|
-
const DIRECT_BOOTSTRAP_ATTEMPT_TIMEOUT_MS = 10_000;
|
|
547
527
|
function isTerminal(status) {
|
|
548
528
|
return typeof status === "string" && TERMINAL_STATUSES.has(status);
|
|
549
529
|
}
|
|
@@ -640,10 +620,9 @@ function normalisePrompt(input) {
|
|
|
640
620
|
}
|
|
641
621
|
return [...input];
|
|
642
622
|
}
|
|
643
|
-
/** Walk Skill[]
|
|
644
|
-
function prepareSkills(skills) {
|
|
623
|
+
/** Walk Skill[], eagerly upload drafts as assets, and return plain asset refs. */
|
|
624
|
+
async function prepareSkills(skills, uploader) {
|
|
645
625
|
const refs = [];
|
|
646
|
-
const directInputs = [];
|
|
647
626
|
for (let i = 0; i < skills.length; i++) {
|
|
648
627
|
const entry = skills[i];
|
|
649
628
|
if (!(entry instanceof Skill)) {
|
|
@@ -658,18 +637,14 @@ function prepareSkills(skills) {
|
|
|
658
637
|
if (!bundle) {
|
|
659
638
|
throw new Error(`AgentExecutor.submitRun: skills[${i}] is draft but has no bytes`);
|
|
660
639
|
}
|
|
661
|
-
const
|
|
662
|
-
role: "skill",
|
|
663
|
-
index: i,
|
|
664
|
-
name: bundle.name,
|
|
665
|
-
contentHash: bundle.contentHash,
|
|
640
|
+
const uploaded = await uploader({
|
|
666
641
|
bytes: bundle.bytes,
|
|
642
|
+
hash: bundle.contentHash,
|
|
667
643
|
contentType: "application/zip"
|
|
668
644
|
});
|
|
669
|
-
directInputs.push(input);
|
|
670
645
|
refs.push({
|
|
671
646
|
kind: "asset",
|
|
672
|
-
assetId:
|
|
647
|
+
assetId: uploaded.assetId,
|
|
673
648
|
name: bundle.name
|
|
674
649
|
});
|
|
675
650
|
continue;
|
|
@@ -677,12 +652,11 @@ function prepareSkills(skills) {
|
|
|
677
652
|
// Already-materialized asset ref.
|
|
678
653
|
refs.push(ref);
|
|
679
654
|
}
|
|
680
|
-
return
|
|
655
|
+
return refs;
|
|
681
656
|
}
|
|
682
|
-
/** Walk AgentsMd[]
|
|
683
|
-
function prepareAgentsMd(agentsMds) {
|
|
657
|
+
/** Walk AgentsMd[], eagerly upload drafts as assets, and return plain asset refs. */
|
|
658
|
+
async function prepareAgentsMd(agentsMds, uploader) {
|
|
684
659
|
const refs = [];
|
|
685
|
-
const directInputs = [];
|
|
686
660
|
for (let i = 0; i < agentsMds.length; i++) {
|
|
687
661
|
const entry = agentsMds[i];
|
|
688
662
|
if (!(entry instanceof AgentsMd)) {
|
|
@@ -697,30 +671,25 @@ function prepareAgentsMd(agentsMds) {
|
|
|
697
671
|
if (!bundle) {
|
|
698
672
|
throw new Error(`AgentExecutor.submitRun: agentsMd[${i}] is draft but has no bytes`);
|
|
699
673
|
}
|
|
700
|
-
const
|
|
701
|
-
role: "agentsMd",
|
|
702
|
-
index: i,
|
|
703
|
-
name: bundle.name,
|
|
704
|
-
contentHash: bundle.contentHash,
|
|
674
|
+
const uploaded = await uploader({
|
|
705
675
|
bytes: bundle.bytes,
|
|
676
|
+
hash: bundle.contentHash,
|
|
706
677
|
contentType: "application/zip"
|
|
707
678
|
});
|
|
708
|
-
directInputs.push(input);
|
|
709
679
|
refs.push({
|
|
710
680
|
kind: "asset",
|
|
711
|
-
assetId:
|
|
681
|
+
assetId: uploaded.assetId,
|
|
712
682
|
name: bundle.name
|
|
713
683
|
});
|
|
714
684
|
continue;
|
|
715
685
|
}
|
|
716
686
|
refs.push(ref);
|
|
717
687
|
}
|
|
718
|
-
return
|
|
688
|
+
return refs;
|
|
719
689
|
}
|
|
720
|
-
/** Walk File[]
|
|
721
|
-
function prepareFiles(files) {
|
|
690
|
+
/** Walk File[], eagerly upload drafts as assets, and return plain asset refs. */
|
|
691
|
+
async function prepareFiles(files, uploader) {
|
|
722
692
|
const refs = [];
|
|
723
|
-
const directInputs = [];
|
|
724
693
|
for (let i = 0; i < files.length; i++) {
|
|
725
694
|
const entry = files[i];
|
|
726
695
|
if (!(entry instanceof File)) {
|
|
@@ -735,53 +704,28 @@ function prepareFiles(files) {
|
|
|
735
704
|
if (!bundle) {
|
|
736
705
|
throw new Error(`AgentExecutor.submitRun: files[${i}] is draft but has no bytes`);
|
|
737
706
|
}
|
|
738
|
-
const
|
|
739
|
-
role: "file",
|
|
740
|
-
index: i,
|
|
741
|
-
name: bundle.name,
|
|
742
|
-
contentHash: bundle.contentHash,
|
|
707
|
+
const uploaded = await uploader({
|
|
743
708
|
bytes: bundle.bytes,
|
|
744
|
-
|
|
745
|
-
|
|
709
|
+
hash: bundle.contentHash,
|
|
710
|
+
contentType: "application/zip"
|
|
746
711
|
});
|
|
747
|
-
directInputs.push(input);
|
|
748
712
|
refs.push(bundle.mountPath !== undefined
|
|
749
713
|
? {
|
|
750
714
|
kind: "asset",
|
|
751
|
-
assetId:
|
|
715
|
+
assetId: uploaded.assetId,
|
|
752
716
|
name: bundle.name,
|
|
753
717
|
mountPath: bundle.mountPath
|
|
754
718
|
}
|
|
755
719
|
: {
|
|
756
720
|
kind: "asset",
|
|
757
|
-
assetId:
|
|
721
|
+
assetId: uploaded.assetId,
|
|
758
722
|
name: bundle.name
|
|
759
723
|
});
|
|
760
724
|
continue;
|
|
761
725
|
}
|
|
762
726
|
refs.push(ref);
|
|
763
727
|
}
|
|
764
|
-
return
|
|
765
|
-
}
|
|
766
|
-
function directInputFor(args) {
|
|
767
|
-
const sha256 = args.contentHash.startsWith("sha256:")
|
|
768
|
-
? args.contentHash
|
|
769
|
-
: `sha256:${args.contentHash}`;
|
|
770
|
-
const hashHex = sha256.slice("sha256:".length);
|
|
771
|
-
if (!/^[0-9a-f]{64}$/.test(hashHex)) {
|
|
772
|
-
throw new Error(`AgentExecutor.submitRun: ${args.role}[${args.index}] content hash must be sha256:<64-hex>`);
|
|
773
|
-
}
|
|
774
|
-
return {
|
|
775
|
-
inputId: `input_${args.role}_${args.index}_${hashHex}`,
|
|
776
|
-
role: args.role,
|
|
777
|
-
assetId: `asset_${hashHex}`,
|
|
778
|
-
name: args.name,
|
|
779
|
-
sha256,
|
|
780
|
-
sizeBytes: args.bytes.byteLength,
|
|
781
|
-
contentType: args.contentType,
|
|
782
|
-
...(args.mountPath ? { mountPath: args.mountPath } : {}),
|
|
783
|
-
bytes: args.bytes
|
|
784
|
-
};
|
|
728
|
+
return refs;
|
|
785
729
|
}
|
|
786
730
|
function getSubmittedRunId(response) {
|
|
787
731
|
const id = response.id ?? response.runId;
|
|
@@ -790,477 +734,6 @@ function getSubmittedRunId(response) {
|
|
|
790
734
|
}
|
|
791
735
|
return id;
|
|
792
736
|
}
|
|
793
|
-
async function completeDirectBootstrap(args) {
|
|
794
|
-
const token = args.response.bootstrapToken;
|
|
795
|
-
const statusUrl = args.response.bootstrapStatusUrl;
|
|
796
|
-
if (typeof token !== "string" || token.length === 0 || typeof statusUrl !== "string" || statusUrl.length === 0) {
|
|
797
|
-
return;
|
|
798
|
-
}
|
|
799
|
-
const fetchImpl = args.fetch ?? globalThis.fetch.bind(globalThis);
|
|
800
|
-
const useNodeTransport = args.fetch === undefined;
|
|
801
|
-
const deadline = bootstrapDeadline(args.response.bootstrapExpiresAt);
|
|
802
|
-
let target;
|
|
803
|
-
try {
|
|
804
|
-
target = resolveBootstrapReady(args.response) ?? await pollBootstrapReady({
|
|
805
|
-
fetchImpl,
|
|
806
|
-
statusUrl,
|
|
807
|
-
token,
|
|
808
|
-
deadline,
|
|
809
|
-
...(args.signal ? { signal: args.signal } : {})
|
|
810
|
-
});
|
|
811
|
-
for (const input of args.directInputs) {
|
|
812
|
-
await uploadDirectInput({
|
|
813
|
-
fetchImpl,
|
|
814
|
-
target,
|
|
815
|
-
token,
|
|
816
|
-
input,
|
|
817
|
-
deadline,
|
|
818
|
-
useNodeTransport,
|
|
819
|
-
...(args.signal ? { signal: args.signal } : {})
|
|
820
|
-
});
|
|
821
|
-
}
|
|
822
|
-
await commitDirectInputs({
|
|
823
|
-
fetchImpl,
|
|
824
|
-
target,
|
|
825
|
-
token,
|
|
826
|
-
inputs: args.directInputs,
|
|
827
|
-
deadline,
|
|
828
|
-
useNodeTransport,
|
|
829
|
-
...(args.hasRunAcceptedBootstrap ? { hasRunAcceptedBootstrap: args.hasRunAcceptedBootstrap } : {}),
|
|
830
|
-
...(args.signal ? { signal: args.signal } : {})
|
|
831
|
-
});
|
|
832
|
-
}
|
|
833
|
-
catch (err) {
|
|
834
|
-
await abortDirectBootstrap({
|
|
835
|
-
fetchImpl,
|
|
836
|
-
token,
|
|
837
|
-
statusUrl,
|
|
838
|
-
...(target ? { target } : {}),
|
|
839
|
-
...(args.signal ? { signal: args.signal } : {})
|
|
840
|
-
}).catch(() => undefined);
|
|
841
|
-
throw err;
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
async function pollBootstrapReady(args) {
|
|
845
|
-
while (!args.signal?.aborted) {
|
|
846
|
-
if (Date.now() >= args.deadline) {
|
|
847
|
-
throw new Error("AgentExecutor.submitRun: bootstrap target did not become ready before it expired");
|
|
848
|
-
}
|
|
849
|
-
const res = await args.fetchImpl(args.statusUrl, {
|
|
850
|
-
method: "GET",
|
|
851
|
-
headers: { authorization: `Bearer ${args.token}`, accept: "application/json" },
|
|
852
|
-
...(args.signal ? { signal: args.signal } : {})
|
|
853
|
-
});
|
|
854
|
-
if (res.ok) {
|
|
855
|
-
const body = await res.json();
|
|
856
|
-
const ready = resolveBootstrapReady(body);
|
|
857
|
-
if (ready)
|
|
858
|
-
return ready;
|
|
859
|
-
}
|
|
860
|
-
else if (![202, 404, 425].includes(res.status)) {
|
|
861
|
-
const detail = await res.text().catch(() => "");
|
|
862
|
-
throw new Error(`AgentExecutor.submitRun: bootstrap status failed with ${res.status}` +
|
|
863
|
-
(detail ? `: ${detail.slice(0, 300)}` : ""));
|
|
864
|
-
}
|
|
865
|
-
await sleep(250, args.signal);
|
|
866
|
-
}
|
|
867
|
-
throw new Error("AgentExecutor.submitRun: aborted");
|
|
868
|
-
}
|
|
869
|
-
function resolveBootstrapReady(value) {
|
|
870
|
-
if (!value || typeof value !== "object")
|
|
871
|
-
return undefined;
|
|
872
|
-
const record = value;
|
|
873
|
-
const base = typeof record.uploadBaseUrl === "string"
|
|
874
|
-
? record.uploadBaseUrl
|
|
875
|
-
: typeof record.bootstrapUploadBaseUrl === "string"
|
|
876
|
-
? record.bootstrapUploadBaseUrl
|
|
877
|
-
: undefined;
|
|
878
|
-
if (!base)
|
|
879
|
-
return undefined;
|
|
880
|
-
const routingHeaders = isStringRecord(record.routingHeaders) ? record.routingHeaders : undefined;
|
|
881
|
-
return {
|
|
882
|
-
uploadBaseUrl: stripTrailingSlash(base),
|
|
883
|
-
...(routingHeaders ? { routingHeaders } : {}),
|
|
884
|
-
...(typeof record.abortUrl === "string" ? { abortUrl: record.abortUrl } : {}),
|
|
885
|
-
...(typeof record.commitUrl === "string" ? { commitUrl: record.commitUrl } : {})
|
|
886
|
-
};
|
|
887
|
-
}
|
|
888
|
-
async function uploadDirectInput(args) {
|
|
889
|
-
let backoffMs = DIRECT_BOOTSTRAP_INITIAL_BACKOFF_MS;
|
|
890
|
-
let lastError;
|
|
891
|
-
const uploadUrl = `${args.target.uploadBaseUrl}/inputs/${encodeURIComponent(args.input.inputId)}`;
|
|
892
|
-
while (!args.signal?.aborted) {
|
|
893
|
-
if (Date.now() >= args.deadline) {
|
|
894
|
-
throw lastError ?? new Error("AgentExecutor.submitRun: bootstrap input upload did not complete before it expired");
|
|
895
|
-
}
|
|
896
|
-
let res;
|
|
897
|
-
try {
|
|
898
|
-
res = await directBootstrapFetch({
|
|
899
|
-
fetchImpl: args.fetchImpl,
|
|
900
|
-
url: uploadUrl,
|
|
901
|
-
deadline: args.deadline,
|
|
902
|
-
operation: `input upload for ${args.input.inputId}`,
|
|
903
|
-
useNodeTransport: args.useNodeTransport,
|
|
904
|
-
...(args.signal ? { signal: args.signal } : {}),
|
|
905
|
-
init: {
|
|
906
|
-
method: "PUT",
|
|
907
|
-
headers: {
|
|
908
|
-
authorization: `Bearer ${args.token}`,
|
|
909
|
-
"content-type": args.input.contentType,
|
|
910
|
-
"x-aex-input-sha256": args.input.sha256,
|
|
911
|
-
"x-aex-input-size": String(args.input.sizeBytes),
|
|
912
|
-
...(args.target.routingHeaders ?? {})
|
|
913
|
-
},
|
|
914
|
-
body: args.input.bytes
|
|
915
|
-
}
|
|
916
|
-
});
|
|
917
|
-
}
|
|
918
|
-
catch (err) {
|
|
919
|
-
if (args.signal?.aborted)
|
|
920
|
-
throw err;
|
|
921
|
-
lastError = bootstrapNetworkError(`input upload for ${args.input.inputId}`, err);
|
|
922
|
-
backoffMs = await waitForBootstrapRetry(backoffMs, args.deadline, args.signal, lastError);
|
|
923
|
-
continue;
|
|
924
|
-
}
|
|
925
|
-
if (res.ok)
|
|
926
|
-
return;
|
|
927
|
-
const detail = await res.text().catch(() => "");
|
|
928
|
-
const err = new Error(`AgentExecutor.submitRun: bootstrap input upload failed for ${args.input.inputId} ` +
|
|
929
|
-
`(status ${res.status})${detail ? `: ${detail.slice(0, 300)}` : ""}`);
|
|
930
|
-
if (!DIRECT_BOOTSTRAP_RETRY_STATUSES.has(res.status)) {
|
|
931
|
-
throw err;
|
|
932
|
-
}
|
|
933
|
-
lastError = err;
|
|
934
|
-
backoffMs = await waitForBootstrapRetry(backoffMs, args.deadline, args.signal, lastError);
|
|
935
|
-
}
|
|
936
|
-
throw lastError ?? new Error("AgentExecutor.submitRun: aborted");
|
|
937
|
-
}
|
|
938
|
-
async function waitForBootstrapRetry(backoffMs, deadline, signal, lastError) {
|
|
939
|
-
const remainingMs = deadline - Date.now();
|
|
940
|
-
if (remainingMs <= 0)
|
|
941
|
-
throw lastError;
|
|
942
|
-
await sleep(Math.min(backoffMs, remainingMs), signal);
|
|
943
|
-
return Math.min(backoffMs * 2, DIRECT_BOOTSTRAP_MAX_BACKOFF_MS);
|
|
944
|
-
}
|
|
945
|
-
async function directBootstrapFetch(args) {
|
|
946
|
-
const remainingMs = args.deadline - Date.now();
|
|
947
|
-
if (remainingMs <= 0) {
|
|
948
|
-
throw new Error(`AgentExecutor.submitRun: bootstrap ${args.operation} did not complete before it expired`);
|
|
949
|
-
}
|
|
950
|
-
const timeoutMs = Math.min(DIRECT_BOOTSTRAP_ATTEMPT_TIMEOUT_MS, remainingMs);
|
|
951
|
-
if (args.useNodeTransport) {
|
|
952
|
-
return nodeDirectBootstrapFetch({
|
|
953
|
-
url: args.url,
|
|
954
|
-
init: args.init,
|
|
955
|
-
timeoutMs,
|
|
956
|
-
operation: args.operation,
|
|
957
|
-
...(args.hasRunAcceptedBootstrap ? { hasRunAcceptedBootstrap: args.hasRunAcceptedBootstrap } : {}),
|
|
958
|
-
...(args.signal ? { signal: args.signal } : {})
|
|
959
|
-
});
|
|
960
|
-
}
|
|
961
|
-
const controller = new AbortController();
|
|
962
|
-
const onAbort = () => controller.abort();
|
|
963
|
-
args.signal?.addEventListener("abort", onAbort, { once: true });
|
|
964
|
-
const fetchPromise = args.fetchImpl(args.url, { ...args.init, signal: controller.signal });
|
|
965
|
-
fetchPromise.catch(() => undefined);
|
|
966
|
-
let timer;
|
|
967
|
-
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
968
|
-
timer = setTimeout(() => {
|
|
969
|
-
controller.abort();
|
|
970
|
-
reject(new Error(`AgentExecutor.submitRun: bootstrap ${args.operation} timed out after ${timeoutMs}ms`));
|
|
971
|
-
}, timeoutMs);
|
|
972
|
-
});
|
|
973
|
-
try {
|
|
974
|
-
return await Promise.race([fetchPromise, timeoutPromise]);
|
|
975
|
-
}
|
|
976
|
-
catch (err) {
|
|
977
|
-
if (args.signal?.aborted)
|
|
978
|
-
throw err;
|
|
979
|
-
throw err;
|
|
980
|
-
}
|
|
981
|
-
finally {
|
|
982
|
-
clearTimeout(timer);
|
|
983
|
-
args.signal?.removeEventListener("abort", onAbort);
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
async function nodeDirectBootstrapFetch(args) {
|
|
987
|
-
const url = new URL(args.url);
|
|
988
|
-
const requestImpl = url.protocol === "http:" ? httpRequest : url.protocol === "https:" ? httpsRequest : undefined;
|
|
989
|
-
if (!requestImpl) {
|
|
990
|
-
throw new Error(`AgentExecutor.submitRun: bootstrap ${args.operation} URL must use http or https`);
|
|
991
|
-
}
|
|
992
|
-
return await new Promise((resolve, reject) => {
|
|
993
|
-
let settled = false;
|
|
994
|
-
let timer;
|
|
995
|
-
let activeResponse;
|
|
996
|
-
let acceptedPollStarted = false;
|
|
997
|
-
const requestHeaders = normalizeBootstrapRequestHeaders(args.init.headers);
|
|
998
|
-
if (!hasHeader(requestHeaders, "connection")) {
|
|
999
|
-
requestHeaders.connection = "close";
|
|
1000
|
-
}
|
|
1001
|
-
const request = requestImpl(url, {
|
|
1002
|
-
method: args.init.method ?? "GET",
|
|
1003
|
-
headers: requestHeaders,
|
|
1004
|
-
agent: false
|
|
1005
|
-
}, (res) => {
|
|
1006
|
-
activeResponse = res;
|
|
1007
|
-
const chunks = [];
|
|
1008
|
-
res.on("data", (chunk) => {
|
|
1009
|
-
chunks.push(typeof chunk === "string" ? new TextEncoder().encode(chunk) : new Uint8Array(chunk));
|
|
1010
|
-
});
|
|
1011
|
-
res.on("end", () => {
|
|
1012
|
-
if (settled)
|
|
1013
|
-
return;
|
|
1014
|
-
settled = true;
|
|
1015
|
-
cleanup();
|
|
1016
|
-
activeResponse = undefined;
|
|
1017
|
-
request.destroy();
|
|
1018
|
-
resolve(new Response(concatUint8Arrays(chunks), {
|
|
1019
|
-
status: res.statusCode ?? 599,
|
|
1020
|
-
statusText: res.statusMessage ?? "",
|
|
1021
|
-
headers: normalizeBootstrapResponseHeaders(res.headers)
|
|
1022
|
-
}));
|
|
1023
|
-
});
|
|
1024
|
-
res.on("error", fail);
|
|
1025
|
-
});
|
|
1026
|
-
function cleanup() {
|
|
1027
|
-
if (timer)
|
|
1028
|
-
clearTimeout(timer);
|
|
1029
|
-
args.signal?.removeEventListener("abort", onAbort);
|
|
1030
|
-
}
|
|
1031
|
-
function fail(err) {
|
|
1032
|
-
if (settled)
|
|
1033
|
-
return;
|
|
1034
|
-
settled = true;
|
|
1035
|
-
cleanup();
|
|
1036
|
-
activeResponse?.destroy(err);
|
|
1037
|
-
request.destroy(err);
|
|
1038
|
-
reject(err);
|
|
1039
|
-
}
|
|
1040
|
-
function timeoutError() {
|
|
1041
|
-
return new Error(`AgentExecutor.submitRun: bootstrap ${args.operation} timed out after ${args.timeoutMs}ms`);
|
|
1042
|
-
}
|
|
1043
|
-
function onAbort() {
|
|
1044
|
-
fail(new Error(`AgentExecutor.submitRun: bootstrap ${args.operation} aborted`));
|
|
1045
|
-
}
|
|
1046
|
-
function startAcceptedPoll() {
|
|
1047
|
-
if (!args.hasRunAcceptedBootstrap || acceptedPollStarted)
|
|
1048
|
-
return;
|
|
1049
|
-
acceptedPollStarted = true;
|
|
1050
|
-
void pollAcceptedBootstrapAfterRequestFinish({
|
|
1051
|
-
check: args.hasRunAcceptedBootstrap,
|
|
1052
|
-
isSettled: () => settled,
|
|
1053
|
-
...(args.signal ? { signal: args.signal } : {})
|
|
1054
|
-
})
|
|
1055
|
-
.then((accepted) => {
|
|
1056
|
-
if (!accepted || settled)
|
|
1057
|
-
return;
|
|
1058
|
-
settled = true;
|
|
1059
|
-
cleanup();
|
|
1060
|
-
activeResponse?.destroy();
|
|
1061
|
-
request.destroy();
|
|
1062
|
-
resolve(new Response(JSON.stringify({ ok: true }), {
|
|
1063
|
-
status: 200,
|
|
1064
|
-
headers: { "content-type": "application/json" }
|
|
1065
|
-
}));
|
|
1066
|
-
})
|
|
1067
|
-
.catch(fail);
|
|
1068
|
-
}
|
|
1069
|
-
timer = setTimeout(() => fail(timeoutError()), args.timeoutMs);
|
|
1070
|
-
request.setTimeout(args.timeoutMs, () => fail(timeoutError()));
|
|
1071
|
-
request.on("error", fail);
|
|
1072
|
-
request.on("finish", startAcceptedPoll);
|
|
1073
|
-
args.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1074
|
-
try {
|
|
1075
|
-
const body = normalizeBootstrapRequestBody(args.init.body);
|
|
1076
|
-
if (body !== undefined)
|
|
1077
|
-
request.write(body);
|
|
1078
|
-
request.end();
|
|
1079
|
-
startAcceptedPoll();
|
|
1080
|
-
}
|
|
1081
|
-
catch (err) {
|
|
1082
|
-
fail(err instanceof Error ? err : new Error(String(err)));
|
|
1083
|
-
}
|
|
1084
|
-
});
|
|
1085
|
-
}
|
|
1086
|
-
async function pollAcceptedBootstrapAfterRequestFinish(args) {
|
|
1087
|
-
while (!args.isSettled()) {
|
|
1088
|
-
if (await checkRunAcceptedBootstrap(args.check))
|
|
1089
|
-
return true;
|
|
1090
|
-
await sleep(250, args.signal);
|
|
1091
|
-
}
|
|
1092
|
-
return false;
|
|
1093
|
-
}
|
|
1094
|
-
function normalizeBootstrapRequestHeaders(headers) {
|
|
1095
|
-
const result = {};
|
|
1096
|
-
if (!headers)
|
|
1097
|
-
return result;
|
|
1098
|
-
if (headers instanceof Headers) {
|
|
1099
|
-
headers.forEach((value, key) => {
|
|
1100
|
-
result[key] = value;
|
|
1101
|
-
});
|
|
1102
|
-
return result;
|
|
1103
|
-
}
|
|
1104
|
-
if (Array.isArray(headers)) {
|
|
1105
|
-
for (const [key, value] of headers)
|
|
1106
|
-
result[key] = value;
|
|
1107
|
-
return result;
|
|
1108
|
-
}
|
|
1109
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
1110
|
-
if (value !== undefined)
|
|
1111
|
-
result[key] = String(value);
|
|
1112
|
-
}
|
|
1113
|
-
return result;
|
|
1114
|
-
}
|
|
1115
|
-
function hasHeader(headers, name) {
|
|
1116
|
-
const normalized = name.toLowerCase();
|
|
1117
|
-
return Object.keys(headers).some((key) => key.toLowerCase() === normalized);
|
|
1118
|
-
}
|
|
1119
|
-
function normalizeBootstrapResponseHeaders(headers) {
|
|
1120
|
-
const result = new Headers();
|
|
1121
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
1122
|
-
if (value === undefined)
|
|
1123
|
-
continue;
|
|
1124
|
-
if (Array.isArray(value)) {
|
|
1125
|
-
for (const item of value)
|
|
1126
|
-
result.append(key, item);
|
|
1127
|
-
}
|
|
1128
|
-
else {
|
|
1129
|
-
result.set(key, value);
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
return result;
|
|
1133
|
-
}
|
|
1134
|
-
function normalizeBootstrapRequestBody(body) {
|
|
1135
|
-
if (body === null || body === undefined)
|
|
1136
|
-
return undefined;
|
|
1137
|
-
if (typeof body === "string")
|
|
1138
|
-
return body;
|
|
1139
|
-
if (body instanceof Uint8Array)
|
|
1140
|
-
return body;
|
|
1141
|
-
if (body instanceof ArrayBuffer)
|
|
1142
|
-
return new Uint8Array(body);
|
|
1143
|
-
if (ArrayBuffer.isView(body))
|
|
1144
|
-
return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
|
|
1145
|
-
if (body instanceof URLSearchParams)
|
|
1146
|
-
return body.toString();
|
|
1147
|
-
throw new Error("AgentExecutor.submitRun: unsupported bootstrap request body type");
|
|
1148
|
-
}
|
|
1149
|
-
function concatUint8Arrays(chunks) {
|
|
1150
|
-
const total = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
|
|
1151
|
-
const result = new Uint8Array(total);
|
|
1152
|
-
let offset = 0;
|
|
1153
|
-
for (const chunk of chunks) {
|
|
1154
|
-
result.set(chunk, offset);
|
|
1155
|
-
offset += chunk.byteLength;
|
|
1156
|
-
}
|
|
1157
|
-
return result;
|
|
1158
|
-
}
|
|
1159
|
-
function bootstrapNetworkError(operation, value) {
|
|
1160
|
-
const message = value instanceof Error ? value.message : String(value);
|
|
1161
|
-
return new Error(`AgentExecutor.submitRun: bootstrap ${operation} failed: ${message}`);
|
|
1162
|
-
}
|
|
1163
|
-
async function commitDirectInputs(args) {
|
|
1164
|
-
const commitUrl = args.target.commitUrl ?? `${args.target.uploadBaseUrl}/commit`;
|
|
1165
|
-
let backoffMs = DIRECT_BOOTSTRAP_INITIAL_BACKOFF_MS;
|
|
1166
|
-
let lastError;
|
|
1167
|
-
while (!args.signal?.aborted) {
|
|
1168
|
-
if (Date.now() >= args.deadline) {
|
|
1169
|
-
throw lastError ?? new Error("AgentExecutor.submitRun: bootstrap commit did not complete before it expired");
|
|
1170
|
-
}
|
|
1171
|
-
let res;
|
|
1172
|
-
try {
|
|
1173
|
-
res = await directBootstrapFetch({
|
|
1174
|
-
fetchImpl: args.fetchImpl,
|
|
1175
|
-
url: commitUrl,
|
|
1176
|
-
deadline: args.deadline,
|
|
1177
|
-
operation: "commit",
|
|
1178
|
-
useNodeTransport: args.useNodeTransport,
|
|
1179
|
-
...(args.hasRunAcceptedBootstrap ? { hasRunAcceptedBootstrap: args.hasRunAcceptedBootstrap } : {}),
|
|
1180
|
-
...(args.signal ? { signal: args.signal } : {}),
|
|
1181
|
-
init: {
|
|
1182
|
-
method: "POST",
|
|
1183
|
-
headers: {
|
|
1184
|
-
authorization: `Bearer ${args.token}`,
|
|
1185
|
-
"content-type": "application/json",
|
|
1186
|
-
accept: "application/json",
|
|
1187
|
-
...(args.target.routingHeaders ?? {})
|
|
1188
|
-
},
|
|
1189
|
-
body: JSON.stringify({
|
|
1190
|
-
inputs: args.inputs.map((input) => ({
|
|
1191
|
-
inputId: input.inputId,
|
|
1192
|
-
sha256: input.sha256,
|
|
1193
|
-
sizeBytes: input.sizeBytes
|
|
1194
|
-
}))
|
|
1195
|
-
})
|
|
1196
|
-
}
|
|
1197
|
-
});
|
|
1198
|
-
}
|
|
1199
|
-
catch (err) {
|
|
1200
|
-
if (args.signal?.aborted)
|
|
1201
|
-
throw err;
|
|
1202
|
-
lastError = bootstrapNetworkError("commit", err);
|
|
1203
|
-
if (await checkRunAcceptedBootstrap(args.hasRunAcceptedBootstrap))
|
|
1204
|
-
return;
|
|
1205
|
-
backoffMs = await waitForBootstrapRetry(backoffMs, args.deadline, args.signal, lastError);
|
|
1206
|
-
continue;
|
|
1207
|
-
}
|
|
1208
|
-
if (res.ok)
|
|
1209
|
-
return;
|
|
1210
|
-
const detail = await res.text().catch(() => "");
|
|
1211
|
-
const err = new Error(`AgentExecutor.submitRun: bootstrap commit failed with ${res.status}` +
|
|
1212
|
-
(detail ? `: ${detail.slice(0, 300)}` : ""));
|
|
1213
|
-
if (!DIRECT_BOOTSTRAP_RETRY_STATUSES.has(res.status)) {
|
|
1214
|
-
throw err;
|
|
1215
|
-
}
|
|
1216
|
-
lastError = err;
|
|
1217
|
-
if (await checkRunAcceptedBootstrap(args.hasRunAcceptedBootstrap))
|
|
1218
|
-
return;
|
|
1219
|
-
backoffMs = await waitForBootstrapRetry(backoffMs, args.deadline, args.signal, lastError);
|
|
1220
|
-
}
|
|
1221
|
-
throw lastError ?? new Error("AgentExecutor.submitRun: aborted");
|
|
1222
|
-
}
|
|
1223
|
-
async function checkRunAcceptedBootstrap(check) {
|
|
1224
|
-
if (!check)
|
|
1225
|
-
return false;
|
|
1226
|
-
return await check().catch(() => false);
|
|
1227
|
-
}
|
|
1228
|
-
function hasRunAcceptedBootstrap(run) {
|
|
1229
|
-
if (run.status === "succeeded" || run.status === "cancelled" || run.status === "timed_out")
|
|
1230
|
-
return true;
|
|
1231
|
-
if (run.status === "failed")
|
|
1232
|
-
return run.terminalAt !== undefined && run.terminalAt !== null;
|
|
1233
|
-
return run.status === "running" || run.status === "provider_running";
|
|
1234
|
-
}
|
|
1235
|
-
async function abortDirectBootstrap(args) {
|
|
1236
|
-
const abortUrl = args.target?.abortUrl ?? `${args.statusUrl.replace(/\/status$/, "")}/abort`;
|
|
1237
|
-
await args.fetchImpl(abortUrl, {
|
|
1238
|
-
method: "POST",
|
|
1239
|
-
headers: {
|
|
1240
|
-
authorization: `Bearer ${args.token}`,
|
|
1241
|
-
accept: "application/json",
|
|
1242
|
-
...(args.target?.routingHeaders ?? {})
|
|
1243
|
-
},
|
|
1244
|
-
...(args.signal ? { signal: args.signal } : {})
|
|
1245
|
-
});
|
|
1246
|
-
}
|
|
1247
|
-
function bootstrapDeadline(expiresAt) {
|
|
1248
|
-
if (typeof expiresAt === "string") {
|
|
1249
|
-
const parsed = Date.parse(expiresAt);
|
|
1250
|
-
if (Number.isFinite(parsed))
|
|
1251
|
-
return parsed;
|
|
1252
|
-
}
|
|
1253
|
-
return Date.now() + 60_000;
|
|
1254
|
-
}
|
|
1255
|
-
function isStringRecord(value) {
|
|
1256
|
-
return Boolean(value &&
|
|
1257
|
-
typeof value === "object" &&
|
|
1258
|
-
!Array.isArray(value) &&
|
|
1259
|
-
Object.values(value).every((entry) => typeof entry === "string"));
|
|
1260
|
-
}
|
|
1261
|
-
function stripTrailingSlash(s) {
|
|
1262
|
-
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
1263
|
-
}
|
|
1264
737
|
function mergeMcpServers(inputs, explicitSecrets) {
|
|
1265
738
|
const submissionMcpServers = [];
|
|
1266
739
|
const secretByName = new Map();
|