@aexhq/sdk 0.13.7 → 0.13.8

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 +302 -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,4 @@
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";
3
2
  import { AgentsMd } from "./agents-md.js";
4
3
  import { File } from "./file.js";
5
4
  import { McpServer } from "./mcp-server.js";
@@ -139,20 +138,16 @@ export class FilesClient {
139
138
  * `client.whoami()` if you want to introspect which workspace the
140
139
  * token resolves to.
141
140
  */
142
- export class AexClient {
141
+ export class AgentExecutor {
143
142
  #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
- */
143
+ /** The same fetch the HttpClient uses, kept for direct bootstrap uploads. */
149
144
  #fetch;
150
145
  skills;
151
146
  agentsMd;
152
147
  files;
153
148
  constructor(options) {
154
149
  if (!options.apiToken) {
155
- throw new Error("AexClient: apiToken is required");
150
+ throw new Error("AgentExecutor: apiToken is required");
156
151
  }
157
152
  this.#http = new HttpClient({
158
153
  ...(options.baseUrl ? { baseUrl: options.baseUrl } : {}),
@@ -231,15 +226,15 @@ export class AexClient {
231
226
  */
232
227
  async submitRun(options) {
233
228
  if (!options || typeof options !== "object") {
234
- throw new Error("AexClient.submitRun: options is required");
229
+ throw new Error("AgentExecutor.submitRun: options is required");
235
230
  }
236
231
  const provider = options.provider ?? DEFAULT_RUN_PROVIDER;
237
232
  const credentialMode = parseCredentialMode(options.credentialMode);
238
233
  if (credentialMode === "managed") {
239
- throw new AexError("CREDENTIAL_INVALID", "AexClient.submitRun: credentialMode \"managed\" is not available without a private managed-key implementation");
234
+ throw new AexError("CREDENTIAL_INVALID", "AgentExecutor.submitRun: credentialMode \"managed\" is not available without a private managed-key implementation");
240
235
  }
241
236
  if (!options.secrets) {
242
- throw new Error("AexClient.submitRun: secrets is required");
237
+ throw new Error("AgentExecutor.submitRun: secrets is required");
243
238
  }
244
239
  // The matching provider's apiKey is required; every OTHER provider's
245
240
  // secret block must be absent. The shared parser re-runs this check
@@ -247,34 +242,41 @@ export class AexClient {
247
242
  // error before any network call.
248
243
  const providerSecret = options.secrets[provider];
249
244
  if (!providerSecret?.apiKey) {
250
- throw new Error(`AexClient.submitRun: secrets.${provider}.apiKey is required`);
245
+ throw new Error(`AgentExecutor.submitRun: secrets.${provider}.apiKey is required`);
251
246
  }
252
247
  for (const other of ["anthropic", "deepseek", "openai", "gemini", "mistral"]) {
253
248
  if (other === provider)
254
249
  continue;
255
250
  if (options.secrets[other] !== undefined) {
256
- throw new Error(`AexClient.submitRun: secrets.${other} is not allowed when provider is ${provider}`);
251
+ throw new Error(`AgentExecutor.submitRun: secrets.${other} is not allowed when provider is ${provider}`);
257
252
  }
258
253
  }
259
254
  if (typeof options.model !== "string" || !options.model) {
260
- throw new Error("AexClient.submitRun: model is required");
255
+ throw new Error("AgentExecutor.submitRun: model is required");
261
256
  }
262
257
  const prompt = normalisePrompt(options.prompt);
263
258
  const { endpoints: proxyEndpointDeclarations, auth: proxyEndpointAuthFromInstances } = splitProxyEndpoints(options.proxyEndpoints ?? []);
264
259
  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);
260
+ // Walk Skill / AgentsMd / File instances. Drafts are declared as direct
261
+ // inputs on the submit request, then uploaded to the run bootstrap target
262
+ // after the control plane accepts the run. Already-materialized asset refs
263
+ // still pass through unchanged.
264
+ const preparedSkills = prepareSkills(options.skills ?? []);
265
+ const preparedAgentsMd = prepareAgentsMd(options.agentsMd ?? []);
266
+ const preparedFiles = prepareFiles(options.files ?? []);
267
+ const directInputs = [
268
+ ...preparedSkills.directInputs,
269
+ ...preparedAgentsMd.directInputs,
270
+ ...preparedFiles.directInputs
271
+ ];
270
272
  const { submissionMcpServers, mergedMcpSecrets } = mergeMcpServers(options.mcpServers ?? [], options.secrets.mcpServers ?? []);
271
273
  const submission = {
272
274
  model: options.model,
273
275
  ...(options.system ? { system: options.system } : {}),
274
276
  prompt,
275
- skills: assetSkills,
276
- agentsMd: assetAgentsMd,
277
- files: assetFiles,
277
+ skills: preparedSkills.refs,
278
+ agentsMd: preparedAgentsMd.refs,
279
+ files: preparedFiles.refs,
278
280
  // submissionMcpServers may contain workspace refs of the shape
279
281
  // {kind:"workspace", id:"mcp_..."}. The BFF runs
280
282
  // `resolveWorkspaceMcpRefsInSubmission` BEFORE the shared parser
@@ -322,13 +324,27 @@ export class AexClient {
322
324
  };
323
325
  if (options.runtime !== undefined &&
324
326
  !RUNTIME_KINDS.includes(options.runtime)) {
325
- throw new AexError("RUNTIME_UNSUPPORTED", `AexClient.submitRun: runtime must be one of: ${RUNTIME_KINDS.join(", ")} ` +
327
+ throw new AexError("RUNTIME_UNSUPPORTED", `AgentExecutor.submitRun: runtime must be one of: ${RUNTIME_KINDS.join(", ")} ` +
326
328
  `(got ${JSON.stringify(options.runtime)})`);
327
329
  }
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;
330
+ const submitRequest = directInputs.length > 0
331
+ ? {
332
+ ...request,
333
+ bootstrapMode: "direct",
334
+ directInputs: directInputs.map(({ bytes: _bytes, ...descriptor }) => descriptor)
335
+ }
336
+ : request;
337
+ const run = await operations.submitRun(this.#http, submitRequest);
338
+ const runId = getSubmittedRunId(run);
339
+ if (directInputs.length > 0) {
340
+ await completeDirectBootstrap({
341
+ response: run,
342
+ directInputs,
343
+ ...(options.signal ? { signal: options.signal } : {}),
344
+ ...(this.#fetch ? { fetch: this.#fetch } : {})
345
+ });
346
+ }
347
+ return runId;
332
348
  }
333
349
  getRun(runId) {
334
350
  return operations.getRun(this.#http, runId);
@@ -431,11 +447,11 @@ export class AexClient {
431
447
  if (isTerminal(run.status))
432
448
  return run;
433
449
  if (Date.now() >= deadline) {
434
- throw new Error(`AexClient.waitForRun: timeout after ${timeoutMs}ms`);
450
+ throw new Error(`AgentExecutor.waitForRun: timeout after ${timeoutMs}ms`);
435
451
  }
436
452
  await sleep(intervalMs, signal);
437
453
  }
438
- throw new Error("AexClient.waitForRun: aborted");
454
+ throw new Error("AgentExecutor.waitForRun: aborted");
439
455
  }
440
456
  /** Short alias for `waitForRun`. */
441
457
  wait(runId, options) {
@@ -468,57 +484,6 @@ export class AexClient {
468
484
  }
469
485
  return writeOptionalFile(bytes, to);
470
486
  }
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
487
  cancelRun(runId) {
523
488
  return operations.cancelRun(this.#http, runId);
524
489
  }
@@ -545,12 +510,11 @@ export class AexClient {
545
510
  return operations.whoami(this.#http);
546
511
  }
547
512
  /**
548
- * Download EVERYTHING about a run as one zip, assembled client-side
513
+ * Download EVERYTHING public about a run as one zip, assembled client-side
549
514
  * 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
515
+ * `listOutputs` + per-output `/download`). Organised into the namespace
516
+ * folders `metadata/`, `events/`, and `outputs/`, plus a `manifest.json`.
517
+ * Pass `to` to also write the
554
518
  * bytes to a file path while still returning the bytes.
555
519
  */
556
520
  async download(runId, options) {
@@ -560,10 +524,6 @@ export class AexClient {
560
524
  async downloadOutputs(runId, options) {
561
525
  return writeOptionalFile(await operations.downloadOutputs(this.#http, runId), options?.to);
562
526
  }
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
527
  /** Download only the indexed event archive (the `events` namespace) as a zip. */
568
528
  async downloadEvents(runId, options) {
569
529
  return writeOptionalFile(await operations.downloadEvents(this.#http, runId), options?.to);
@@ -594,7 +554,7 @@ function resolveOutputFileSelector(outputs, selector, runId) {
594
554
  if (isOutputPathSelector(selector)) {
595
555
  const target = normalizeOutputLookupPath(selector.path);
596
556
  if (!target) {
597
- throw new RunStateError("AexClient.downloadOutput: output path must be non-empty", {
557
+ throw new RunStateError("AgentExecutor.downloadOutput: output path must be non-empty", {
598
558
  runId,
599
559
  path: selector.path
600
560
  });
@@ -611,15 +571,15 @@ function resolveOutputFileSelector(outputs, selector, runId) {
611
571
  if (matches.length === 1)
612
572
  return matches[0];
613
573
  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) });
574
+ 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
575
  }
616
- throw new RunStateError(`AexClient.downloadOutput: output path "${selector.path}" was not found`, {
576
+ throw new RunStateError(`AgentExecutor.downloadOutput: output path "${selector.path}" was not found`, {
617
577
  runId,
618
578
  path: selector.path
619
579
  });
620
580
  }
621
581
  if (typeof selector.id !== "string" || selector.id.length === 0) {
622
- throw new RunStateError("AexClient.downloadOutput: selector must include an output id or path", { runId });
582
+ throw new RunStateError("AgentExecutor.downloadOutput: selector must include an output id or path", { runId });
623
583
  }
624
584
  return { ...selector, id: selector.id };
625
585
  }
@@ -650,23 +610,6 @@ function sleep(ms, signal) {
650
610
  signal?.addEventListener("abort", onAbort, { once: true });
651
611
  });
652
612
  }
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
613
  function generateIdempotencyKey() {
671
614
  const cryptoObj = globalThis.crypto;
672
615
  if (cryptoObj?.randomUUID)
@@ -676,133 +619,325 @@ function generateIdempotencyKey() {
676
619
  function normalisePrompt(input) {
677
620
  if (typeof input === "string") {
678
621
  if (!input) {
679
- throw new Error("AexClient.submitRun: prompt must be a non-empty string");
622
+ throw new Error("AgentExecutor.submitRun: prompt must be a non-empty string");
680
623
  }
681
624
  return [input];
682
625
  }
683
626
  if (!Array.isArray(input) || input.length === 0) {
684
- throw new Error("AexClient.submitRun: prompt must be a non-empty string or string array");
627
+ throw new Error("AgentExecutor.submitRun: prompt must be a non-empty string or string array");
685
628
  }
686
629
  for (const segment of input) {
687
630
  if (typeof segment !== "string" || !segment) {
688
- throw new Error("AexClient.submitRun: prompt segments must be non-empty strings");
631
+ throw new Error("AgentExecutor.submitRun: prompt segments must be non-empty strings");
689
632
  }
690
633
  }
691
634
  return [...input];
692
635
  }
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 = [];
636
+ /** Walk Skill[] and turn drafts into direct-bootstrap descriptors. */
637
+ function prepareSkills(skills) {
638
+ const refs = [];
639
+ const directInputs = [];
710
640
  for (let i = 0; i < skills.length; i++) {
711
641
  const entry = skills[i];
712
642
  if (!(entry instanceof Skill)) {
713
- throw new Error(`AexClient.submitRun: skills[${i}] must be a Skill instance`);
643
+ throw new Error(`AgentExecutor.submitRun: skills[${i}] must be a Skill instance`);
714
644
  }
715
645
  if (entry.isConsumed) {
716
- throw new Error(`AexClient.submitRun: skills[${i}] was already consumed by a prior submitRun`);
646
+ throw new Error(`AgentExecutor.submitRun: skills[${i}] was already consumed by a prior submitRun`);
717
647
  }
718
648
  const ref = entry.ref;
719
649
  if (ref.kind === "draft") {
720
650
  const bundle = entry._takeDraftBundle();
721
651
  if (!bundle) {
722
- throw new Error(`AexClient.submitRun: skills[${i}] is draft but has no bytes`);
652
+ throw new Error(`AgentExecutor.submitRun: skills[${i}] is draft but has no bytes`);
723
653
  }
724
- const uploaded = await uploadAsset({
725
- http,
654
+ const input = directInputFor({
655
+ role: "skill",
656
+ index: i,
657
+ name: bundle.name,
658
+ contentHash: bundle.contentHash,
726
659
  bytes: bundle.bytes,
727
- hash: bundle.contentHash,
728
- ...(fetch ? { fetch } : {})
660
+ contentType: "application/zip"
729
661
  });
730
- out.push({
662
+ directInputs.push(input);
663
+ refs.push({
731
664
  kind: "asset",
732
- assetId: uploaded.assetId,
665
+ assetId: input.assetId,
733
666
  name: bundle.name
734
667
  });
735
668
  continue;
736
669
  }
737
670
  // Already-materialized asset ref.
738
- out.push(ref);
671
+ refs.push(ref);
739
672
  }
740
- return out;
673
+ return { refs, directInputs };
741
674
  }
742
- /** Materialize draft AgentsMd[] to assets; pass-through any already-materialized refs. */
743
- async function materializeAgentsMd(http, agentsMds, fetch) {
744
- const out = [];
675
+ /** Walk AgentsMd[] and turn drafts into direct-bootstrap descriptors. */
676
+ function prepareAgentsMd(agentsMds) {
677
+ const refs = [];
678
+ const directInputs = [];
745
679
  for (let i = 0; i < agentsMds.length; i++) {
746
680
  const entry = agentsMds[i];
747
681
  if (!(entry instanceof AgentsMd)) {
748
- throw new Error(`AexClient.submitRun: agentsMd[${i}] must be an AgentsMd instance`);
682
+ throw new Error(`AgentExecutor.submitRun: agentsMd[${i}] must be an AgentsMd instance`);
749
683
  }
750
684
  if (entry.isConsumed) {
751
- throw new Error(`AexClient.submitRun: agentsMd[${i}] was already consumed by a prior submitRun`);
685
+ throw new Error(`AgentExecutor.submitRun: agentsMd[${i}] was already consumed by a prior submitRun`);
752
686
  }
753
687
  const ref = entry.ref;
754
688
  if (ref.kind === "draft") {
755
689
  const bundle = entry._takeDraftBundle();
756
690
  if (!bundle) {
757
- throw new Error(`AexClient.submitRun: agentsMd[${i}] is draft but has no bytes`);
691
+ throw new Error(`AgentExecutor.submitRun: agentsMd[${i}] is draft but has no bytes`);
758
692
  }
759
- const uploaded = await uploadAsset({ http, bytes: bundle.bytes, hash: bundle.contentHash, ...(fetch ? { fetch } : {}) });
760
- out.push({
693
+ const input = directInputFor({
694
+ role: "agentsMd",
695
+ index: i,
696
+ name: bundle.name,
697
+ contentHash: bundle.contentHash,
698
+ bytes: bundle.bytes,
699
+ contentType: "application/zip"
700
+ });
701
+ directInputs.push(input);
702
+ refs.push({
761
703
  kind: "asset",
762
- assetId: uploaded.assetId,
704
+ assetId: input.assetId,
763
705
  name: bundle.name
764
706
  });
765
707
  continue;
766
708
  }
767
- out.push(ref);
709
+ refs.push(ref);
768
710
  }
769
- return out;
711
+ return { refs, directInputs };
770
712
  }
771
- /** Materialize draft File[] to assets; pass-through any already-materialized refs. */
772
- async function materializeFiles(http, files, fetch) {
773
- const out = [];
713
+ /** Walk File[] and turn drafts into direct-bootstrap descriptors. */
714
+ function prepareFiles(files) {
715
+ const refs = [];
716
+ const directInputs = [];
774
717
  for (let i = 0; i < files.length; i++) {
775
718
  const entry = files[i];
776
719
  if (!(entry instanceof File)) {
777
- throw new Error(`AexClient.submitRun: files[${i}] must be a File instance`);
720
+ throw new Error(`AgentExecutor.submitRun: files[${i}] must be a File instance`);
778
721
  }
779
722
  if (entry.isConsumed) {
780
- throw new Error(`AexClient.submitRun: files[${i}] was already consumed by a prior submitRun`);
723
+ throw new Error(`AgentExecutor.submitRun: files[${i}] was already consumed by a prior submitRun`);
781
724
  }
782
725
  const ref = entry.ref;
783
726
  if (ref.kind === "draft") {
784
727
  const bundle = entry._takeDraftBundle();
785
728
  if (!bundle) {
786
- throw new Error(`AexClient.submitRun: files[${i}] is draft but has no bytes`);
729
+ throw new Error(`AgentExecutor.submitRun: files[${i}] is draft but has no bytes`);
787
730
  }
788
- const uploaded = await uploadAsset({ http, bytes: bundle.bytes, hash: bundle.contentHash, ...(fetch ? { fetch } : {}) });
789
- out.push(bundle.mountPath !== undefined
731
+ const input = directInputFor({
732
+ role: "file",
733
+ index: i,
734
+ name: bundle.name,
735
+ contentHash: bundle.contentHash,
736
+ bytes: bundle.bytes,
737
+ contentType: "application/zip",
738
+ ...(bundle.mountPath ? { mountPath: bundle.mountPath } : {})
739
+ });
740
+ directInputs.push(input);
741
+ refs.push(bundle.mountPath !== undefined
790
742
  ? {
791
743
  kind: "asset",
792
- assetId: uploaded.assetId,
744
+ assetId: input.assetId,
793
745
  name: bundle.name,
794
746
  mountPath: bundle.mountPath
795
747
  }
796
748
  : {
797
749
  kind: "asset",
798
- assetId: uploaded.assetId,
750
+ assetId: input.assetId,
799
751
  name: bundle.name
800
752
  });
801
753
  continue;
802
754
  }
803
- out.push(ref);
755
+ refs.push(ref);
756
+ }
757
+ return { refs, directInputs };
758
+ }
759
+ function directInputFor(args) {
760
+ const sha256 = args.contentHash.startsWith("sha256:")
761
+ ? args.contentHash
762
+ : `sha256:${args.contentHash}`;
763
+ const hashHex = sha256.slice("sha256:".length);
764
+ if (!/^[0-9a-f]{64}$/.test(hashHex)) {
765
+ throw new Error(`AgentExecutor.submitRun: ${args.role}[${args.index}] content hash must be sha256:<64-hex>`);
766
+ }
767
+ return {
768
+ inputId: `input_${args.role}_${args.index}_${hashHex}`,
769
+ role: args.role,
770
+ assetId: `asset_${hashHex}`,
771
+ name: args.name,
772
+ sha256,
773
+ sizeBytes: args.bytes.byteLength,
774
+ contentType: args.contentType,
775
+ ...(args.mountPath ? { mountPath: args.mountPath } : {}),
776
+ bytes: args.bytes
777
+ };
778
+ }
779
+ function getSubmittedRunId(response) {
780
+ const id = response.id ?? response.runId;
781
+ if (typeof id !== "string" || id.length === 0) {
782
+ throw new Error("AgentExecutor.submitRun: submit response did not include a run id");
783
+ }
784
+ return id;
785
+ }
786
+ async function completeDirectBootstrap(args) {
787
+ const token = args.response.bootstrapToken;
788
+ const statusUrl = args.response.bootstrapStatusUrl;
789
+ if (typeof token !== "string" || token.length === 0 || typeof statusUrl !== "string" || statusUrl.length === 0) {
790
+ return;
791
+ }
792
+ const fetchImpl = args.fetch ?? globalThis.fetch.bind(globalThis);
793
+ let target;
794
+ try {
795
+ target = resolveBootstrapReady(args.response) ?? await pollBootstrapReady({
796
+ fetchImpl,
797
+ statusUrl,
798
+ token,
799
+ ...(args.response.bootstrapExpiresAt ? { expiresAt: args.response.bootstrapExpiresAt } : {}),
800
+ ...(args.signal ? { signal: args.signal } : {})
801
+ });
802
+ for (const input of args.directInputs) {
803
+ await uploadDirectInput({ fetchImpl, target, token, input, ...(args.signal ? { signal: args.signal } : {}) });
804
+ }
805
+ await commitDirectInputs({
806
+ fetchImpl,
807
+ target,
808
+ token,
809
+ inputs: args.directInputs,
810
+ ...(args.signal ? { signal: args.signal } : {})
811
+ });
804
812
  }
805
- return out;
813
+ catch (err) {
814
+ await abortDirectBootstrap({
815
+ fetchImpl,
816
+ token,
817
+ statusUrl,
818
+ ...(target ? { target } : {}),
819
+ ...(args.signal ? { signal: args.signal } : {})
820
+ }).catch(() => undefined);
821
+ throw err;
822
+ }
823
+ }
824
+ async function pollBootstrapReady(args) {
825
+ const deadline = bootstrapDeadline(args.expiresAt);
826
+ while (!args.signal?.aborted) {
827
+ if (Date.now() >= deadline) {
828
+ throw new Error("AgentExecutor.submitRun: bootstrap target did not become ready before it expired");
829
+ }
830
+ const res = await args.fetchImpl(args.statusUrl, {
831
+ method: "GET",
832
+ headers: { authorization: `Bearer ${args.token}`, accept: "application/json" },
833
+ ...(args.signal ? { signal: args.signal } : {})
834
+ });
835
+ if (res.ok) {
836
+ const body = await res.json();
837
+ const ready = resolveBootstrapReady(body);
838
+ if (ready)
839
+ return ready;
840
+ }
841
+ else if (![202, 404, 425].includes(res.status)) {
842
+ const detail = await res.text().catch(() => "");
843
+ throw new Error(`AgentExecutor.submitRun: bootstrap status failed with ${res.status}` +
844
+ (detail ? `: ${detail.slice(0, 300)}` : ""));
845
+ }
846
+ await sleep(250, args.signal);
847
+ }
848
+ throw new Error("AgentExecutor.submitRun: aborted");
849
+ }
850
+ function resolveBootstrapReady(value) {
851
+ if (!value || typeof value !== "object")
852
+ return undefined;
853
+ const record = value;
854
+ const base = typeof record.uploadBaseUrl === "string"
855
+ ? record.uploadBaseUrl
856
+ : typeof record.bootstrapUploadBaseUrl === "string"
857
+ ? record.bootstrapUploadBaseUrl
858
+ : undefined;
859
+ if (!base)
860
+ return undefined;
861
+ const routingHeaders = isStringRecord(record.routingHeaders) ? record.routingHeaders : undefined;
862
+ return {
863
+ uploadBaseUrl: stripTrailingSlash(base),
864
+ ...(routingHeaders ? { routingHeaders } : {}),
865
+ ...(typeof record.abortUrl === "string" ? { abortUrl: record.abortUrl } : {}),
866
+ ...(typeof record.commitUrl === "string" ? { commitUrl: record.commitUrl } : {})
867
+ };
868
+ }
869
+ async function uploadDirectInput(args) {
870
+ const res = await args.fetchImpl(`${args.target.uploadBaseUrl}/inputs/${encodeURIComponent(args.input.inputId)}`, {
871
+ method: "PUT",
872
+ headers: {
873
+ authorization: `Bearer ${args.token}`,
874
+ "content-type": args.input.contentType,
875
+ "x-aex-input-sha256": args.input.sha256,
876
+ "x-aex-input-size": String(args.input.sizeBytes),
877
+ ...(args.target.routingHeaders ?? {})
878
+ },
879
+ body: args.input.bytes,
880
+ ...(args.signal ? { signal: args.signal } : {})
881
+ });
882
+ if (!res.ok) {
883
+ const detail = await res.text().catch(() => "");
884
+ throw new Error(`AgentExecutor.submitRun: bootstrap input upload failed for ${args.input.inputId} ` +
885
+ `(status ${res.status})${detail ? `: ${detail.slice(0, 300)}` : ""}`);
886
+ }
887
+ }
888
+ async function commitDirectInputs(args) {
889
+ const commitUrl = args.target.commitUrl ?? `${args.target.uploadBaseUrl}/commit`;
890
+ const res = await args.fetchImpl(commitUrl, {
891
+ method: "POST",
892
+ headers: {
893
+ authorization: `Bearer ${args.token}`,
894
+ "content-type": "application/json",
895
+ accept: "application/json",
896
+ ...(args.target.routingHeaders ?? {})
897
+ },
898
+ body: JSON.stringify({
899
+ inputs: args.inputs.map((input) => ({
900
+ inputId: input.inputId,
901
+ sha256: input.sha256,
902
+ sizeBytes: input.sizeBytes
903
+ }))
904
+ }),
905
+ ...(args.signal ? { signal: args.signal } : {})
906
+ });
907
+ if (!res.ok) {
908
+ const detail = await res.text().catch(() => "");
909
+ throw new Error(`AgentExecutor.submitRun: bootstrap commit failed with ${res.status}` +
910
+ (detail ? `: ${detail.slice(0, 300)}` : ""));
911
+ }
912
+ }
913
+ async function abortDirectBootstrap(args) {
914
+ const abortUrl = args.target?.abortUrl ?? `${args.statusUrl.replace(/\/status$/, "")}/abort`;
915
+ await args.fetchImpl(abortUrl, {
916
+ method: "POST",
917
+ headers: {
918
+ authorization: `Bearer ${args.token}`,
919
+ accept: "application/json",
920
+ ...(args.target?.routingHeaders ?? {})
921
+ },
922
+ ...(args.signal ? { signal: args.signal } : {})
923
+ });
924
+ }
925
+ function bootstrapDeadline(expiresAt) {
926
+ if (typeof expiresAt === "string") {
927
+ const parsed = Date.parse(expiresAt);
928
+ if (Number.isFinite(parsed))
929
+ return parsed;
930
+ }
931
+ return Date.now() + 60_000;
932
+ }
933
+ function isStringRecord(value) {
934
+ return Boolean(value &&
935
+ typeof value === "object" &&
936
+ !Array.isArray(value) &&
937
+ Object.values(value).every((entry) => typeof entry === "string"));
938
+ }
939
+ function stripTrailingSlash(s) {
940
+ return s.endsWith("/") ? s.slice(0, -1) : s;
806
941
  }
807
942
  function mergeMcpServers(inputs, explicitSecrets) {
808
943
  const submissionMcpServers = [];
@@ -813,14 +948,14 @@ function mergeMcpServers(inputs, explicitSecrets) {
813
948
  for (let i = 0; i < inputs.length; i++) {
814
949
  const entry = inputs[i];
815
950
  if (!(entry instanceof McpServer)) {
816
- throw new Error(`AexClient.submitRun: mcpServers[${i}] must be an McpServer instance`);
951
+ throw new Error(`AgentExecutor.submitRun: mcpServers[${i}] must be an McpServer instance`);
817
952
  }
818
953
  submissionMcpServers.push(entry.toSubmissionEntry());
819
954
  const secret = entry.toSecretEntry();
820
955
  if (secret) {
821
956
  const existing = secretByName.get(secret.name);
822
957
  if (existing && existing.url !== secret.url) {
823
- throw new Error(`AexClient.submitRun: mcpServers[${i}].url conflicts with secrets.mcpServers["${secret.name}"]`);
958
+ throw new Error(`AgentExecutor.submitRun: mcpServers[${i}].url conflicts with secrets.mcpServers["${secret.name}"]`);
824
959
  }
825
960
  secretByName.set(secret.name, secret);
826
961
  }
@@ -848,7 +983,7 @@ function mergeProxyEndpointAuth(fromInstances, fromExplicitSecrets) {
848
983
  for (const entry of fromInstances) {
849
984
  const existing = byName.get(entry.name);
850
985
  if (existing && existing.value.type !== entry.value.type) {
851
- throw new Error(`AexClient.submitRun: proxyEndpoint "${entry.name}" auth type conflicts ` +
986
+ throw new Error(`AgentExecutor.submitRun: proxyEndpoint "${entry.name}" auth type conflicts ` +
852
987
  `with secrets.proxyEndpointAuth (instance=${entry.value.type}, secrets=${existing.value.type})`);
853
988
  }
854
989
  byName.set(entry.name, entry);