@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.
Files changed (51) hide show
  1. package/README.md +14 -14
  2. package/dist/_contracts/connection-ticket.d.ts +8 -7
  3. package/dist/_contracts/connection-ticket.js +20 -14
  4. package/dist/_contracts/event-envelope.d.ts +17 -18
  5. package/dist/_contracts/event-envelope.js +10 -11
  6. package/dist/_contracts/managed-key.d.ts +27 -1
  7. package/dist/_contracts/managed-key.js +75 -4
  8. package/dist/_contracts/operations.d.ts +9 -20
  9. package/dist/_contracts/operations.js +33 -82
  10. package/dist/_contracts/proxy-protocol.d.ts +35 -2
  11. package/dist/_contracts/proxy-protocol.js +34 -1
  12. package/dist/_contracts/run-artifacts.d.ts +12 -10
  13. package/dist/_contracts/run-artifacts.js +13 -11
  14. package/dist/_contracts/run-config.d.ts +7 -0
  15. package/dist/_contracts/run-config.js +93 -24
  16. package/dist/_contracts/run-custody.d.ts +3 -3
  17. package/dist/_contracts/run-custody.js +5 -5
  18. package/dist/_contracts/run-record.d.ts +5 -17
  19. package/dist/_contracts/run-record.js +4 -15
  20. package/dist/_contracts/run-retention.d.ts +2 -2
  21. package/dist/_contracts/run-retention.js +3 -3
  22. package/dist/_contracts/run-unit.d.ts +4 -5
  23. package/dist/_contracts/runner-event.d.ts +7 -8
  24. package/dist/_contracts/runner-event.js +7 -8
  25. package/dist/_contracts/side-effect-audit.d.ts +2 -2
  26. package/dist/_contracts/side-effect-audit.js +3 -3
  27. package/dist/_contracts/stable.d.ts +1 -1
  28. package/dist/_contracts/stable.js +1 -1
  29. package/dist/_contracts/submission.d.ts +5 -6
  30. package/dist/_contracts/submission.js +1 -1
  31. package/dist/cli.mjs +127 -127
  32. package/dist/cli.mjs.sha256 +1 -1
  33. package/dist/client.d.ts +7 -57
  34. package/dist/client.js +624 -167
  35. package/dist/client.js.map +1 -1
  36. package/dist/index.d.ts +3 -3
  37. package/dist/index.js +2 -2
  38. package/dist/index.js.map +1 -1
  39. package/dist/version.d.ts +1 -1
  40. package/dist/version.js +1 -1
  41. package/docs/cleanup.md +4 -4
  42. package/docs/credentials.md +5 -5
  43. package/docs/events.md +5 -5
  44. package/docs/outputs.md +23 -25
  45. package/docs/product-boundaries.md +5 -5
  46. package/docs/provider-runtime-capabilities.md +1 -1
  47. package/docs/quickstart.md +12 -12
  48. package/docs/run-config.md +1 -1
  49. package/docs/run-record.md +6 -9
  50. package/docs/skills.md +23 -25
  51. 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 { uploadAsset } from "./asset-upload.js";
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 AexClient {
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("AexClient: apiToken is required");
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("AexClient.submitRun: options is required");
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", "AexClient.submitRun: credentialMode \"managed\" is not available without a private managed-key implementation");
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("AexClient.submitRun: secrets is required");
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(`AexClient.submitRun: secrets.${provider}.apiKey is required`);
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(`AexClient.submitRun: secrets.${other} is not allowed when provider is ${provider}`);
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("AexClient.submitRun: model is required");
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 and materialize every draft before
266
- // the submit round-trip. The wire shape carries only kind:"asset" refs.
267
- const assetSkills = await materializeSkills(this.#http, options.skills ?? [], this.#fetch);
268
- const assetAgentsMd = await materializeAgentsMd(this.#http, options.agentsMd ?? [], this.#fetch);
269
- const assetFiles = await materializeFiles(this.#http, options.files ?? [], this.#fetch);
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: assetSkills,
276
- agentsMd: assetAgentsMd,
277
- files: assetFiles,
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", `AexClient.submitRun: runtime must be one of: ${RUNTIME_KINDS.join(", ")} ` +
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
- // All inline refs were materialized above, so submitRun is
329
- // always a plain JSON post. The multipart code path is gone.
330
- const run = await operations.submitRun(this.#http, request);
331
- return run.id;
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(`AexClient.waitForRun: timeout after ${timeoutMs}ms`);
453
+ throw new Error(`AgentExecutor.waitForRun: timeout after ${timeoutMs}ms`);
435
454
  }
436
455
  await sleep(intervalMs, signal);
437
456
  }
438
- throw new Error("AexClient.waitForRun: aborted");
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 four
551
- * namespace folders: `metadata/`, `events/`, `outputs/` (deliverables),
552
- * `logs/` (`runtime/`, `host/`, `provider-proxy/`, `control-plane/`
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("AexClient.downloadOutput: output path must be non-empty", {
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(`AexClient.downloadOutput: output path "${selector.path}" matched multiple files`, { runId, path: selector.path, matches: matches.map((output) => output.filename ?? output.id) });
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(`AexClient.downloadOutput: output path "${selector.path}" was not found`, {
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("AexClient.downloadOutput: selector must include an output id or path", { runId });
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("AexClient.submitRun: prompt must be a non-empty string");
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("AexClient.submitRun: prompt must be a non-empty string or string array");
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("AexClient.submitRun: prompt segments must be non-empty strings");
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
- * Walk the user-provided `Skill[]`, validating each instance and
695
- * producing:
696
- * - `skillRefs[]` — the wire entries for `submission.skills[]`, with
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(`AexClient.submitRun: skills[${i}] must be a Skill instance`);
650
+ throw new Error(`AgentExecutor.submitRun: skills[${i}] must be a Skill instance`);
714
651
  }
715
652
  if (entry.isConsumed) {
716
- throw new Error(`AexClient.submitRun: skills[${i}] was already consumed by a prior submitRun`);
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(`AexClient.submitRun: skills[${i}] is draft but has no bytes`);
659
+ throw new Error(`AgentExecutor.submitRun: skills[${i}] is draft but has no bytes`);
723
660
  }
724
- const uploaded = await uploadAsset({
725
- http,
661
+ const input = directInputFor({
662
+ role: "skill",
663
+ index: i,
664
+ name: bundle.name,
665
+ contentHash: bundle.contentHash,
726
666
  bytes: bundle.bytes,
727
- hash: bundle.contentHash,
728
- ...(fetch ? { fetch } : {})
667
+ contentType: "application/zip"
729
668
  });
730
- out.push({
669
+ directInputs.push(input);
670
+ refs.push({
731
671
  kind: "asset",
732
- assetId: uploaded.assetId,
672
+ assetId: input.assetId,
733
673
  name: bundle.name
734
674
  });
735
675
  continue;
736
676
  }
737
677
  // Already-materialized asset ref.
738
- out.push(ref);
678
+ refs.push(ref);
739
679
  }
740
- return out;
680
+ return { refs, directInputs };
741
681
  }
742
- /** Materialize draft AgentsMd[] to assets; pass-through any already-materialized refs. */
743
- async function materializeAgentsMd(http, agentsMds, fetch) {
744
- const out = [];
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(`AexClient.submitRun: agentsMd[${i}] must be an AgentsMd instance`);
689
+ throw new Error(`AgentExecutor.submitRun: agentsMd[${i}] must be an AgentsMd instance`);
749
690
  }
750
691
  if (entry.isConsumed) {
751
- throw new Error(`AexClient.submitRun: agentsMd[${i}] was already consumed by a prior submitRun`);
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(`AexClient.submitRun: agentsMd[${i}] is draft but has no bytes`);
698
+ throw new Error(`AgentExecutor.submitRun: agentsMd[${i}] is draft but has no bytes`);
758
699
  }
759
- const uploaded = await uploadAsset({ http, bytes: bundle.bytes, hash: bundle.contentHash, ...(fetch ? { fetch } : {}) });
760
- out.push({
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: uploaded.assetId,
711
+ assetId: input.assetId,
763
712
  name: bundle.name
764
713
  });
765
714
  continue;
766
715
  }
767
- out.push(ref);
716
+ refs.push(ref);
768
717
  }
769
- return out;
718
+ return { refs, directInputs };
770
719
  }
771
- /** Materialize draft File[] to assets; pass-through any already-materialized refs. */
772
- async function materializeFiles(http, files, fetch) {
773
- const out = [];
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(`AexClient.submitRun: files[${i}] must be a File instance`);
727
+ throw new Error(`AgentExecutor.submitRun: files[${i}] must be a File instance`);
778
728
  }
779
729
  if (entry.isConsumed) {
780
- throw new Error(`AexClient.submitRun: files[${i}] was already consumed by a prior submitRun`);
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(`AexClient.submitRun: files[${i}] is draft but has no bytes`);
736
+ throw new Error(`AgentExecutor.submitRun: files[${i}] is draft but has no bytes`);
787
737
  }
788
- const uploaded = await uploadAsset({ http, bytes: bundle.bytes, hash: bundle.contentHash, ...(fetch ? { fetch } : {}) });
789
- out.push(bundle.mountPath !== undefined
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: uploaded.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: uploaded.assetId,
757
+ assetId: input.assetId,
799
758
  name: bundle.name
800
759
  });
801
760
  continue;
802
761
  }
803
- out.push(ref);
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
- return out;
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(`AexClient.submitRun: mcpServers[${i}] must be an McpServer instance`);
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(`AexClient.submitRun: mcpServers[${i}].url conflicts with secrets.mcpServers["${secret.name}"]`);
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(`AexClient.submitRun: proxyEndpoint "${entry.name}" auth type conflicts ` +
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);