@aexhq/sdk 0.13.7 → 0.13.9
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 +14 -14
- package/dist/_contracts/connection-ticket.d.ts +8 -7
- package/dist/_contracts/connection-ticket.js +20 -14
- package/dist/_contracts/event-envelope.d.ts +17 -18
- package/dist/_contracts/event-envelope.js +10 -11
- package/dist/_contracts/managed-key.d.ts +27 -1
- package/dist/_contracts/managed-key.js +75 -4
- package/dist/_contracts/operations.d.ts +9 -20
- package/dist/_contracts/operations.js +33 -82
- package/dist/_contracts/proxy-protocol.d.ts +35 -2
- package/dist/_contracts/proxy-protocol.js +34 -1
- package/dist/_contracts/run-artifacts.d.ts +12 -10
- package/dist/_contracts/run-artifacts.js +13 -11
- package/dist/_contracts/run-config.d.ts +7 -0
- package/dist/_contracts/run-config.js +93 -24
- package/dist/_contracts/run-custody.d.ts +3 -3
- package/dist/_contracts/run-custody.js +5 -5
- package/dist/_contracts/run-record.d.ts +5 -17
- package/dist/_contracts/run-record.js +4 -15
- package/dist/_contracts/run-retention.d.ts +2 -2
- package/dist/_contracts/run-retention.js +3 -3
- package/dist/_contracts/run-unit.d.ts +4 -5
- package/dist/_contracts/runner-event.d.ts +7 -8
- package/dist/_contracts/runner-event.js +7 -8
- package/dist/_contracts/side-effect-audit.d.ts +2 -2
- package/dist/_contracts/side-effect-audit.js +3 -3
- package/dist/_contracts/stable.d.ts +1 -1
- package/dist/_contracts/stable.js +1 -1
- package/dist/_contracts/submission.d.ts +5 -6
- package/dist/_contracts/submission.js +1 -1
- package/dist/cli.mjs +127 -127
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +7 -57
- package/dist/client.js +624 -167
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/cleanup.md +4 -4
- package/docs/credentials.md +5 -5
- package/docs/events.md +5 -5
- package/docs/outputs.md +23 -25
- package/docs/product-boundaries.md +5 -5
- package/docs/provider-runtime-capabilities.md +1 -1
- package/docs/quickstart.md +12 -12
- package/docs/run-config.md +1 -1
- package/docs/run-record.md +6 -9
- package/docs/skills.md +23 -25
- package/package.json +2 -2
package/dist/client.js
CHANGED
|
@@ -1,5 +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 {
|
|
2
|
+
import { request as httpRequest } from "node:http";
|
|
3
|
+
import { request as httpsRequest } from "node:https";
|
|
3
4
|
import { AgentsMd } from "./agents-md.js";
|
|
4
5
|
import { File } from "./file.js";
|
|
5
6
|
import { McpServer } from "./mcp-server.js";
|
|
@@ -139,20 +140,16 @@ export class FilesClient {
|
|
|
139
140
|
* `client.whoami()` if you want to introspect which workspace the
|
|
140
141
|
* token resolves to.
|
|
141
142
|
*/
|
|
142
|
-
export class
|
|
143
|
+
export class AgentExecutor {
|
|
143
144
|
#http;
|
|
144
|
-
/**
|
|
145
|
-
* The same fetch the HttpClient uses, kept so the asset materializer can
|
|
146
|
-
* PUT bytes DIRECTLY to the presigned upload URL (a non-aex origin) with the
|
|
147
|
-
* caller's fetch (tests inject one; prod uses the global).
|
|
148
|
-
*/
|
|
145
|
+
/** The same fetch the HttpClient uses, kept for direct bootstrap uploads. */
|
|
149
146
|
#fetch;
|
|
150
147
|
skills;
|
|
151
148
|
agentsMd;
|
|
152
149
|
files;
|
|
153
150
|
constructor(options) {
|
|
154
151
|
if (!options.apiToken) {
|
|
155
|
-
throw new Error("
|
|
152
|
+
throw new Error("AgentExecutor: apiToken is required");
|
|
156
153
|
}
|
|
157
154
|
this.#http = new HttpClient({
|
|
158
155
|
...(options.baseUrl ? { baseUrl: options.baseUrl } : {}),
|
|
@@ -231,15 +228,15 @@ export class AexClient {
|
|
|
231
228
|
*/
|
|
232
229
|
async submitRun(options) {
|
|
233
230
|
if (!options || typeof options !== "object") {
|
|
234
|
-
throw new Error("
|
|
231
|
+
throw new Error("AgentExecutor.submitRun: options is required");
|
|
235
232
|
}
|
|
236
233
|
const provider = options.provider ?? DEFAULT_RUN_PROVIDER;
|
|
237
234
|
const credentialMode = parseCredentialMode(options.credentialMode);
|
|
238
235
|
if (credentialMode === "managed") {
|
|
239
|
-
throw new AexError("CREDENTIAL_INVALID", "
|
|
236
|
+
throw new AexError("CREDENTIAL_INVALID", "AgentExecutor.submitRun: credentialMode \"managed\" is not available without a private managed-key implementation");
|
|
240
237
|
}
|
|
241
238
|
if (!options.secrets) {
|
|
242
|
-
throw new Error("
|
|
239
|
+
throw new Error("AgentExecutor.submitRun: secrets is required");
|
|
243
240
|
}
|
|
244
241
|
// The matching provider's apiKey is required; every OTHER provider's
|
|
245
242
|
// secret block must be absent. The shared parser re-runs this check
|
|
@@ -247,34 +244,41 @@ export class AexClient {
|
|
|
247
244
|
// error before any network call.
|
|
248
245
|
const providerSecret = options.secrets[provider];
|
|
249
246
|
if (!providerSecret?.apiKey) {
|
|
250
|
-
throw new Error(`
|
|
247
|
+
throw new Error(`AgentExecutor.submitRun: secrets.${provider}.apiKey is required`);
|
|
251
248
|
}
|
|
252
249
|
for (const other of ["anthropic", "deepseek", "openai", "gemini", "mistral"]) {
|
|
253
250
|
if (other === provider)
|
|
254
251
|
continue;
|
|
255
252
|
if (options.secrets[other] !== undefined) {
|
|
256
|
-
throw new Error(`
|
|
253
|
+
throw new Error(`AgentExecutor.submitRun: secrets.${other} is not allowed when provider is ${provider}`);
|
|
257
254
|
}
|
|
258
255
|
}
|
|
259
256
|
if (typeof options.model !== "string" || !options.model) {
|
|
260
|
-
throw new Error("
|
|
257
|
+
throw new Error("AgentExecutor.submitRun: model is required");
|
|
261
258
|
}
|
|
262
259
|
const prompt = normalisePrompt(options.prompt);
|
|
263
260
|
const { endpoints: proxyEndpointDeclarations, auth: proxyEndpointAuthFromInstances } = splitProxyEndpoints(options.proxyEndpoints ?? []);
|
|
264
261
|
const mergedProxyAuth = mergeProxyEndpointAuth(proxyEndpointAuthFromInstances, options.secrets.proxyEndpointAuth ?? []);
|
|
265
|
-
// Walk Skill / AgentsMd / File instances
|
|
266
|
-
// the submit
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const
|
|
262
|
+
// Walk Skill / AgentsMd / File instances. Drafts are declared as direct
|
|
263
|
+
// inputs on the submit request, then uploaded to the run bootstrap target
|
|
264
|
+
// after the control plane accepts the run. Already-materialized asset refs
|
|
265
|
+
// still pass through unchanged.
|
|
266
|
+
const preparedSkills = prepareSkills(options.skills ?? []);
|
|
267
|
+
const preparedAgentsMd = prepareAgentsMd(options.agentsMd ?? []);
|
|
268
|
+
const preparedFiles = prepareFiles(options.files ?? []);
|
|
269
|
+
const directInputs = [
|
|
270
|
+
...preparedSkills.directInputs,
|
|
271
|
+
...preparedAgentsMd.directInputs,
|
|
272
|
+
...preparedFiles.directInputs
|
|
273
|
+
];
|
|
270
274
|
const { submissionMcpServers, mergedMcpSecrets } = mergeMcpServers(options.mcpServers ?? [], options.secrets.mcpServers ?? []);
|
|
271
275
|
const submission = {
|
|
272
276
|
model: options.model,
|
|
273
277
|
...(options.system ? { system: options.system } : {}),
|
|
274
278
|
prompt,
|
|
275
|
-
skills:
|
|
276
|
-
agentsMd:
|
|
277
|
-
files:
|
|
279
|
+
skills: preparedSkills.refs,
|
|
280
|
+
agentsMd: preparedAgentsMd.refs,
|
|
281
|
+
files: preparedFiles.refs,
|
|
278
282
|
// submissionMcpServers may contain workspace refs of the shape
|
|
279
283
|
// {kind:"workspace", id:"mcp_..."}. The BFF runs
|
|
280
284
|
// `resolveWorkspaceMcpRefsInSubmission` BEFORE the shared parser
|
|
@@ -322,13 +326,28 @@ export class AexClient {
|
|
|
322
326
|
};
|
|
323
327
|
if (options.runtime !== undefined &&
|
|
324
328
|
!RUNTIME_KINDS.includes(options.runtime)) {
|
|
325
|
-
throw new AexError("RUNTIME_UNSUPPORTED", `
|
|
329
|
+
throw new AexError("RUNTIME_UNSUPPORTED", `AgentExecutor.submitRun: runtime must be one of: ${RUNTIME_KINDS.join(", ")} ` +
|
|
326
330
|
`(got ${JSON.stringify(options.runtime)})`);
|
|
327
331
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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;
|
|
332
351
|
}
|
|
333
352
|
getRun(runId) {
|
|
334
353
|
return operations.getRun(this.#http, runId);
|
|
@@ -431,11 +450,11 @@ export class AexClient {
|
|
|
431
450
|
if (isTerminal(run.status))
|
|
432
451
|
return run;
|
|
433
452
|
if (Date.now() >= deadline) {
|
|
434
|
-
throw new Error(`
|
|
453
|
+
throw new Error(`AgentExecutor.waitForRun: timeout after ${timeoutMs}ms`);
|
|
435
454
|
}
|
|
436
455
|
await sleep(intervalMs, signal);
|
|
437
456
|
}
|
|
438
|
-
throw new Error("
|
|
457
|
+
throw new Error("AgentExecutor.waitForRun: aborted");
|
|
439
458
|
}
|
|
440
459
|
/** Short alias for `waitForRun`. */
|
|
441
460
|
wait(runId, options) {
|
|
@@ -468,57 +487,6 @@ export class AexClient {
|
|
|
468
487
|
}
|
|
469
488
|
return writeOptionalFile(bytes, to);
|
|
470
489
|
}
|
|
471
|
-
/**
|
|
472
|
-
* Bundle the per-run debug artifacts aex captures automatically:
|
|
473
|
-
*
|
|
474
|
-
* - `runtime/{stdout,stderr,args}.log` — runtime process diagnostics.
|
|
475
|
-
* - `host/...` — managed host logs when the platform includes them.
|
|
476
|
-
* These all live in the run's `logs` namespace (`runs/<id>/logs/`).
|
|
477
|
-
* Each is downloaded through the gated `/logs/:id/download` endpoint,
|
|
478
|
-
* decoded as UTF-8 text when the content type looks textual, and
|
|
479
|
-
* surfaced as raw bytes (base64) otherwise. The call is best-effort: a
|
|
480
|
-
* download failure for one file does not block the others; the failing
|
|
481
|
-
* entry lands in `errors` with the underlying message.
|
|
482
|
-
*
|
|
483
|
-
* Use this when a run failed or behaved oddly and you want all the
|
|
484
|
-
* post-mortem material in one round-trip — no need to wire
|
|
485
|
-
* `listOutputs` + `createOutputLink` by hand.
|
|
486
|
-
*/
|
|
487
|
-
async getRunDebugLogs(runId) {
|
|
488
|
-
// The `logs` namespace IS the diagnostics surface — everything it
|
|
489
|
-
// lists is a debug artifact, so no client-side prefix filter.
|
|
490
|
-
const matches = await operations.listLogs(this.#http, runId);
|
|
491
|
-
const logs = [];
|
|
492
|
-
const errors = [];
|
|
493
|
-
for (const out of matches) {
|
|
494
|
-
const filename = out.filename ?? "(unnamed)";
|
|
495
|
-
try {
|
|
496
|
-
const { response } = await this.#http.download(`/api/runs/${runId}/logs/${out.id}/download`);
|
|
497
|
-
const buf = await response.arrayBuffer();
|
|
498
|
-
const bytes = new Uint8Array(buf);
|
|
499
|
-
const contentType = out.contentType ?? "application/octet-stream";
|
|
500
|
-
const isText = /^(text\/|application\/json)/.test(contentType);
|
|
501
|
-
const bytesBase64 = bytesToBase64(bytes);
|
|
502
|
-
logs.push({
|
|
503
|
-
filename,
|
|
504
|
-
sizeBytes: out.sizeBytes ?? bytes.byteLength,
|
|
505
|
-
contentType,
|
|
506
|
-
createdAt: out.createdAt ?? new Date(0).toISOString(),
|
|
507
|
-
...(isText ? { text: new TextDecoder().decode(bytes) } : {}),
|
|
508
|
-
bytesBase64
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
catch (err) {
|
|
512
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
513
|
-
errors.push({ filename, message });
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
return { runId, logs, errors };
|
|
517
|
-
}
|
|
518
|
-
/** Short alias for `getRunDebugLogs`. */
|
|
519
|
-
debugLogs(runId) {
|
|
520
|
-
return this.getRunDebugLogs(runId);
|
|
521
|
-
}
|
|
522
490
|
cancelRun(runId) {
|
|
523
491
|
return operations.cancelRun(this.#http, runId);
|
|
524
492
|
}
|
|
@@ -545,12 +513,11 @@ export class AexClient {
|
|
|
545
513
|
return operations.whoami(this.#http);
|
|
546
514
|
}
|
|
547
515
|
/**
|
|
548
|
-
* Download EVERYTHING about a run as one zip, assembled client-side
|
|
516
|
+
* Download EVERYTHING public about a run as one zip, assembled client-side
|
|
549
517
|
* from the public read endpoints (`getRun` + `listEvents` +
|
|
550
|
-
* `listOutputs` + per-output `/download`). Organised into the
|
|
551
|
-
*
|
|
552
|
-
*
|
|
553
|
-
* diagnostics), plus a `manifest.json`. Pass `to` to also write the
|
|
518
|
+
* `listOutputs` + per-output `/download`). Organised into the namespace
|
|
519
|
+
* folders `metadata/`, `events/`, and `outputs/`, plus a `manifest.json`.
|
|
520
|
+
* Pass `to` to also write the
|
|
554
521
|
* bytes to a file path while still returning the bytes.
|
|
555
522
|
*/
|
|
556
523
|
async download(runId, options) {
|
|
@@ -560,10 +527,6 @@ export class AexClient {
|
|
|
560
527
|
async downloadOutputs(runId, options) {
|
|
561
528
|
return writeOptionalFile(await operations.downloadOutputs(this.#http, runId), options?.to);
|
|
562
529
|
}
|
|
563
|
-
/** Download only the platform diagnostics (the `logs` namespace) as a zip. */
|
|
564
|
-
async downloadLogs(runId, options) {
|
|
565
|
-
return writeOptionalFile(await operations.downloadLogs(this.#http, runId), options?.to);
|
|
566
|
-
}
|
|
567
530
|
/** Download only the indexed event archive (the `events` namespace) as a zip. */
|
|
568
531
|
async downloadEvents(runId, options) {
|
|
569
532
|
return writeOptionalFile(await operations.downloadEvents(this.#http, runId), options?.to);
|
|
@@ -577,6 +540,10 @@ export class AexClient {
|
|
|
577
540
|
// against the canonical terminal set rather than re-deriving one (which is how
|
|
578
541
|
// `timed_out` got dropped from the old hardcoded list).
|
|
579
542
|
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;
|
|
580
547
|
function isTerminal(status) {
|
|
581
548
|
return typeof status === "string" && TERMINAL_STATUSES.has(status);
|
|
582
549
|
}
|
|
@@ -594,7 +561,7 @@ function resolveOutputFileSelector(outputs, selector, runId) {
|
|
|
594
561
|
if (isOutputPathSelector(selector)) {
|
|
595
562
|
const target = normalizeOutputLookupPath(selector.path);
|
|
596
563
|
if (!target) {
|
|
597
|
-
throw new RunStateError("
|
|
564
|
+
throw new RunStateError("AgentExecutor.downloadOutput: output path must be non-empty", {
|
|
598
565
|
runId,
|
|
599
566
|
path: selector.path
|
|
600
567
|
});
|
|
@@ -611,15 +578,15 @@ function resolveOutputFileSelector(outputs, selector, runId) {
|
|
|
611
578
|
if (matches.length === 1)
|
|
612
579
|
return matches[0];
|
|
613
580
|
if (matches.length > 1) {
|
|
614
|
-
throw new RunStateError(`
|
|
581
|
+
throw new RunStateError(`AgentExecutor.downloadOutput: output path "${selector.path}" matched multiple files`, { runId, path: selector.path, matches: matches.map((output) => output.filename ?? output.id) });
|
|
615
582
|
}
|
|
616
|
-
throw new RunStateError(`
|
|
583
|
+
throw new RunStateError(`AgentExecutor.downloadOutput: output path "${selector.path}" was not found`, {
|
|
617
584
|
runId,
|
|
618
585
|
path: selector.path
|
|
619
586
|
});
|
|
620
587
|
}
|
|
621
588
|
if (typeof selector.id !== "string" || selector.id.length === 0) {
|
|
622
|
-
throw new RunStateError("
|
|
589
|
+
throw new RunStateError("AgentExecutor.downloadOutput: selector must include an output id or path", { runId });
|
|
623
590
|
}
|
|
624
591
|
return { ...selector, id: selector.id };
|
|
625
592
|
}
|
|
@@ -650,23 +617,6 @@ function sleep(ms, signal) {
|
|
|
650
617
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
651
618
|
});
|
|
652
619
|
}
|
|
653
|
-
/**
|
|
654
|
-
* Encode a byte array as base64. Uses Node's `Buffer` when available
|
|
655
|
-
* (the SDK ships as a Node tarball; this is the hot path), and falls
|
|
656
|
-
* back to `btoa` for browser or edge runtimes that pull the SDK in
|
|
657
|
-
* without Buffer.
|
|
658
|
-
*/
|
|
659
|
-
function bytesToBase64(bytes) {
|
|
660
|
-
const BufferCtor = globalThis.Buffer;
|
|
661
|
-
if (BufferCtor) {
|
|
662
|
-
return BufferCtor.from(bytes).toString("base64");
|
|
663
|
-
}
|
|
664
|
-
let binary = "";
|
|
665
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
666
|
-
binary += String.fromCharCode(bytes[i]);
|
|
667
|
-
}
|
|
668
|
-
return globalThis.btoa(binary);
|
|
669
|
-
}
|
|
670
620
|
function generateIdempotencyKey() {
|
|
671
621
|
const cryptoObj = globalThis.crypto;
|
|
672
622
|
if (cryptoObj?.randomUUID)
|
|
@@ -676,133 +626,640 @@ function generateIdempotencyKey() {
|
|
|
676
626
|
function normalisePrompt(input) {
|
|
677
627
|
if (typeof input === "string") {
|
|
678
628
|
if (!input) {
|
|
679
|
-
throw new Error("
|
|
629
|
+
throw new Error("AgentExecutor.submitRun: prompt must be a non-empty string");
|
|
680
630
|
}
|
|
681
631
|
return [input];
|
|
682
632
|
}
|
|
683
633
|
if (!Array.isArray(input) || input.length === 0) {
|
|
684
|
-
throw new Error("
|
|
634
|
+
throw new Error("AgentExecutor.submitRun: prompt must be a non-empty string or string array");
|
|
685
635
|
}
|
|
686
636
|
for (const segment of input) {
|
|
687
637
|
if (typeof segment !== "string" || !segment) {
|
|
688
|
-
throw new Error("
|
|
638
|
+
throw new Error("AgentExecutor.submitRun: prompt segments must be non-empty strings");
|
|
689
639
|
}
|
|
690
640
|
}
|
|
691
641
|
return [...input];
|
|
692
642
|
}
|
|
693
|
-
/**
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
* inline refs assigned positional slot ids (`transient-0`, …).
|
|
698
|
-
* - `inlineBundles[]` — the bytes for each inline skill,
|
|
699
|
-
* parallel-indexed by slot.
|
|
700
|
-
*
|
|
701
|
-
* Throws on consumed Skills (the user reused a draft after a prior
|
|
702
|
-
* `submitRun` call) so that mistake is loud, not silent.
|
|
703
|
-
*/
|
|
704
|
-
/**
|
|
705
|
-
* Walk the user-provided Skill[], materialize every draft to assets, and return
|
|
706
|
-
* the wire-shape refs.
|
|
707
|
-
*/
|
|
708
|
-
async function materializeSkills(http, skills, fetch) {
|
|
709
|
-
const out = [];
|
|
643
|
+
/** Walk Skill[] and turn drafts into direct-bootstrap descriptors. */
|
|
644
|
+
function prepareSkills(skills) {
|
|
645
|
+
const refs = [];
|
|
646
|
+
const directInputs = [];
|
|
710
647
|
for (let i = 0; i < skills.length; i++) {
|
|
711
648
|
const entry = skills[i];
|
|
712
649
|
if (!(entry instanceof Skill)) {
|
|
713
|
-
throw new Error(`
|
|
650
|
+
throw new Error(`AgentExecutor.submitRun: skills[${i}] must be a Skill instance`);
|
|
714
651
|
}
|
|
715
652
|
if (entry.isConsumed) {
|
|
716
|
-
throw new Error(`
|
|
653
|
+
throw new Error(`AgentExecutor.submitRun: skills[${i}] was already consumed by a prior submitRun`);
|
|
717
654
|
}
|
|
718
655
|
const ref = entry.ref;
|
|
719
656
|
if (ref.kind === "draft") {
|
|
720
657
|
const bundle = entry._takeDraftBundle();
|
|
721
658
|
if (!bundle) {
|
|
722
|
-
throw new Error(`
|
|
659
|
+
throw new Error(`AgentExecutor.submitRun: skills[${i}] is draft but has no bytes`);
|
|
723
660
|
}
|
|
724
|
-
const
|
|
725
|
-
|
|
661
|
+
const input = directInputFor({
|
|
662
|
+
role: "skill",
|
|
663
|
+
index: i,
|
|
664
|
+
name: bundle.name,
|
|
665
|
+
contentHash: bundle.contentHash,
|
|
726
666
|
bytes: bundle.bytes,
|
|
727
|
-
|
|
728
|
-
...(fetch ? { fetch } : {})
|
|
667
|
+
contentType: "application/zip"
|
|
729
668
|
});
|
|
730
|
-
|
|
669
|
+
directInputs.push(input);
|
|
670
|
+
refs.push({
|
|
731
671
|
kind: "asset",
|
|
732
|
-
assetId:
|
|
672
|
+
assetId: input.assetId,
|
|
733
673
|
name: bundle.name
|
|
734
674
|
});
|
|
735
675
|
continue;
|
|
736
676
|
}
|
|
737
677
|
// Already-materialized asset ref.
|
|
738
|
-
|
|
678
|
+
refs.push(ref);
|
|
739
679
|
}
|
|
740
|
-
return
|
|
680
|
+
return { refs, directInputs };
|
|
741
681
|
}
|
|
742
|
-
/**
|
|
743
|
-
|
|
744
|
-
const
|
|
682
|
+
/** Walk AgentsMd[] and turn drafts into direct-bootstrap descriptors. */
|
|
683
|
+
function prepareAgentsMd(agentsMds) {
|
|
684
|
+
const refs = [];
|
|
685
|
+
const directInputs = [];
|
|
745
686
|
for (let i = 0; i < agentsMds.length; i++) {
|
|
746
687
|
const entry = agentsMds[i];
|
|
747
688
|
if (!(entry instanceof AgentsMd)) {
|
|
748
|
-
throw new Error(`
|
|
689
|
+
throw new Error(`AgentExecutor.submitRun: agentsMd[${i}] must be an AgentsMd instance`);
|
|
749
690
|
}
|
|
750
691
|
if (entry.isConsumed) {
|
|
751
|
-
throw new Error(`
|
|
692
|
+
throw new Error(`AgentExecutor.submitRun: agentsMd[${i}] was already consumed by a prior submitRun`);
|
|
752
693
|
}
|
|
753
694
|
const ref = entry.ref;
|
|
754
695
|
if (ref.kind === "draft") {
|
|
755
696
|
const bundle = entry._takeDraftBundle();
|
|
756
697
|
if (!bundle) {
|
|
757
|
-
throw new Error(`
|
|
698
|
+
throw new Error(`AgentExecutor.submitRun: agentsMd[${i}] is draft but has no bytes`);
|
|
758
699
|
}
|
|
759
|
-
const
|
|
760
|
-
|
|
700
|
+
const input = directInputFor({
|
|
701
|
+
role: "agentsMd",
|
|
702
|
+
index: i,
|
|
703
|
+
name: bundle.name,
|
|
704
|
+
contentHash: bundle.contentHash,
|
|
705
|
+
bytes: bundle.bytes,
|
|
706
|
+
contentType: "application/zip"
|
|
707
|
+
});
|
|
708
|
+
directInputs.push(input);
|
|
709
|
+
refs.push({
|
|
761
710
|
kind: "asset",
|
|
762
|
-
assetId:
|
|
711
|
+
assetId: input.assetId,
|
|
763
712
|
name: bundle.name
|
|
764
713
|
});
|
|
765
714
|
continue;
|
|
766
715
|
}
|
|
767
|
-
|
|
716
|
+
refs.push(ref);
|
|
768
717
|
}
|
|
769
|
-
return
|
|
718
|
+
return { refs, directInputs };
|
|
770
719
|
}
|
|
771
|
-
/**
|
|
772
|
-
|
|
773
|
-
const
|
|
720
|
+
/** Walk File[] and turn drafts into direct-bootstrap descriptors. */
|
|
721
|
+
function prepareFiles(files) {
|
|
722
|
+
const refs = [];
|
|
723
|
+
const directInputs = [];
|
|
774
724
|
for (let i = 0; i < files.length; i++) {
|
|
775
725
|
const entry = files[i];
|
|
776
726
|
if (!(entry instanceof File)) {
|
|
777
|
-
throw new Error(`
|
|
727
|
+
throw new Error(`AgentExecutor.submitRun: files[${i}] must be a File instance`);
|
|
778
728
|
}
|
|
779
729
|
if (entry.isConsumed) {
|
|
780
|
-
throw new Error(`
|
|
730
|
+
throw new Error(`AgentExecutor.submitRun: files[${i}] was already consumed by a prior submitRun`);
|
|
781
731
|
}
|
|
782
732
|
const ref = entry.ref;
|
|
783
733
|
if (ref.kind === "draft") {
|
|
784
734
|
const bundle = entry._takeDraftBundle();
|
|
785
735
|
if (!bundle) {
|
|
786
|
-
throw new Error(`
|
|
736
|
+
throw new Error(`AgentExecutor.submitRun: files[${i}] is draft but has no bytes`);
|
|
787
737
|
}
|
|
788
|
-
const
|
|
789
|
-
|
|
738
|
+
const input = directInputFor({
|
|
739
|
+
role: "file",
|
|
740
|
+
index: i,
|
|
741
|
+
name: bundle.name,
|
|
742
|
+
contentHash: bundle.contentHash,
|
|
743
|
+
bytes: bundle.bytes,
|
|
744
|
+
contentType: "application/zip",
|
|
745
|
+
...(bundle.mountPath ? { mountPath: bundle.mountPath } : {})
|
|
746
|
+
});
|
|
747
|
+
directInputs.push(input);
|
|
748
|
+
refs.push(bundle.mountPath !== undefined
|
|
790
749
|
? {
|
|
791
750
|
kind: "asset",
|
|
792
|
-
assetId:
|
|
751
|
+
assetId: input.assetId,
|
|
793
752
|
name: bundle.name,
|
|
794
753
|
mountPath: bundle.mountPath
|
|
795
754
|
}
|
|
796
755
|
: {
|
|
797
756
|
kind: "asset",
|
|
798
|
-
assetId:
|
|
757
|
+
assetId: input.assetId,
|
|
799
758
|
name: bundle.name
|
|
800
759
|
});
|
|
801
760
|
continue;
|
|
802
761
|
}
|
|
803
|
-
|
|
762
|
+
refs.push(ref);
|
|
763
|
+
}
|
|
764
|
+
return { refs, directInputs };
|
|
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
|
+
};
|
|
785
|
+
}
|
|
786
|
+
function getSubmittedRunId(response) {
|
|
787
|
+
const id = response.id ?? response.runId;
|
|
788
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
789
|
+
throw new Error("AgentExecutor.submitRun: submit response did not include a run id");
|
|
790
|
+
}
|
|
791
|
+
return id;
|
|
792
|
+
}
|
|
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);
|
|
804
935
|
}
|
|
805
|
-
|
|
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;
|
|
806
1263
|
}
|
|
807
1264
|
function mergeMcpServers(inputs, explicitSecrets) {
|
|
808
1265
|
const submissionMcpServers = [];
|
|
@@ -813,14 +1270,14 @@ function mergeMcpServers(inputs, explicitSecrets) {
|
|
|
813
1270
|
for (let i = 0; i < inputs.length; i++) {
|
|
814
1271
|
const entry = inputs[i];
|
|
815
1272
|
if (!(entry instanceof McpServer)) {
|
|
816
|
-
throw new Error(`
|
|
1273
|
+
throw new Error(`AgentExecutor.submitRun: mcpServers[${i}] must be an McpServer instance`);
|
|
817
1274
|
}
|
|
818
1275
|
submissionMcpServers.push(entry.toSubmissionEntry());
|
|
819
1276
|
const secret = entry.toSecretEntry();
|
|
820
1277
|
if (secret) {
|
|
821
1278
|
const existing = secretByName.get(secret.name);
|
|
822
1279
|
if (existing && existing.url !== secret.url) {
|
|
823
|
-
throw new Error(`
|
|
1280
|
+
throw new Error(`AgentExecutor.submitRun: mcpServers[${i}].url conflicts with secrets.mcpServers["${secret.name}"]`);
|
|
824
1281
|
}
|
|
825
1282
|
secretByName.set(secret.name, secret);
|
|
826
1283
|
}
|
|
@@ -848,7 +1305,7 @@ function mergeProxyEndpointAuth(fromInstances, fromExplicitSecrets) {
|
|
|
848
1305
|
for (const entry of fromInstances) {
|
|
849
1306
|
const existing = byName.get(entry.name);
|
|
850
1307
|
if (existing && existing.value.type !== entry.value.type) {
|
|
851
|
-
throw new Error(`
|
|
1308
|
+
throw new Error(`AgentExecutor.submitRun: proxyEndpoint "${entry.name}" auth type conflicts ` +
|
|
852
1309
|
`with secrets.proxyEndpointAuth (instance=${entry.value.type}, secrets=${existing.value.type})`);
|
|
853
1310
|
}
|
|
854
1311
|
byName.set(entry.name, entry);
|