@aexhq/sdk 0.13.6 → 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 +15 -11
  30. package/dist/_contracts/submission.js +18 -14
  31. package/dist/cli.mjs +127 -127
  32. package/dist/cli.mjs.sha256 +1 -1
  33. package/dist/client.d.ts +14 -58
  34. package/dist/client.js +304 -168
  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
@@ -292,7 +294,8 @@ export class AexClient {
292
294
  // Pass-through `builtins` verbatim — including an empty array,
293
295
  // which is the "disable all builtins" signal. Distinguish from
294
296
  // omitted (default applies) via `!== undefined`.
295
- ...(options.builtins !== undefined ? { builtins: options.builtins } : {})
297
+ ...(options.builtins !== undefined ? { builtins: options.builtins } : {}),
298
+ ...(options.outputMode !== undefined ? { outputMode: options.outputMode } : {})
296
299
  };
297
300
  const secrets = {
298
301
  ...options.secrets,
@@ -321,13 +324,27 @@ export class AexClient {
321
324
  };
322
325
  if (options.runtime !== undefined &&
323
326
  !RUNTIME_KINDS.includes(options.runtime)) {
324
- 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(", ")} ` +
325
328
  `(got ${JSON.stringify(options.runtime)})`);
326
329
  }
327
- // All inline refs were materialized above, so submitRun is
328
- // always a plain JSON post. The multipart code path is gone.
329
- const run = await operations.submitRun(this.#http, request);
330
- 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;
331
348
  }
332
349
  getRun(runId) {
333
350
  return operations.getRun(this.#http, runId);
@@ -430,11 +447,11 @@ export class AexClient {
430
447
  if (isTerminal(run.status))
431
448
  return run;
432
449
  if (Date.now() >= deadline) {
433
- throw new Error(`AexClient.waitForRun: timeout after ${timeoutMs}ms`);
450
+ throw new Error(`AgentExecutor.waitForRun: timeout after ${timeoutMs}ms`);
434
451
  }
435
452
  await sleep(intervalMs, signal);
436
453
  }
437
- throw new Error("AexClient.waitForRun: aborted");
454
+ throw new Error("AgentExecutor.waitForRun: aborted");
438
455
  }
439
456
  /** Short alias for `waitForRun`. */
440
457
  wait(runId, options) {
@@ -467,57 +484,6 @@ export class AexClient {
467
484
  }
468
485
  return writeOptionalFile(bytes, to);
469
486
  }
470
- /**
471
- * Bundle the per-run debug artifacts aex captures automatically:
472
- *
473
- * - `runtime/{stdout,stderr,args}.log` — runtime process diagnostics.
474
- * - `host/...` — managed host logs when the platform includes them.
475
- * These all live in the run's `logs` namespace (`runs/<id>/logs/`).
476
- * Each is downloaded through the gated `/logs/:id/download` endpoint,
477
- * decoded as UTF-8 text when the content type looks textual, and
478
- * surfaced as raw bytes (base64) otherwise. The call is best-effort: a
479
- * download failure for one file does not block the others; the failing
480
- * entry lands in `errors` with the underlying message.
481
- *
482
- * Use this when a run failed or behaved oddly and you want all the
483
- * post-mortem material in one round-trip — no need to wire
484
- * `listOutputs` + `createOutputLink` by hand.
485
- */
486
- async getRunDebugLogs(runId) {
487
- // The `logs` namespace IS the diagnostics surface — everything it
488
- // lists is a debug artifact, so no client-side prefix filter.
489
- const matches = await operations.listLogs(this.#http, runId);
490
- const logs = [];
491
- const errors = [];
492
- for (const out of matches) {
493
- const filename = out.filename ?? "(unnamed)";
494
- try {
495
- const { response } = await this.#http.download(`/api/runs/${runId}/logs/${out.id}/download`);
496
- const buf = await response.arrayBuffer();
497
- const bytes = new Uint8Array(buf);
498
- const contentType = out.contentType ?? "application/octet-stream";
499
- const isText = /^(text\/|application\/json)/.test(contentType);
500
- const bytesBase64 = bytesToBase64(bytes);
501
- logs.push({
502
- filename,
503
- sizeBytes: out.sizeBytes ?? bytes.byteLength,
504
- contentType,
505
- createdAt: out.createdAt ?? new Date(0).toISOString(),
506
- ...(isText ? { text: new TextDecoder().decode(bytes) } : {}),
507
- bytesBase64
508
- });
509
- }
510
- catch (err) {
511
- const message = err instanceof Error ? err.message : String(err);
512
- errors.push({ filename, message });
513
- }
514
- }
515
- return { runId, logs, errors };
516
- }
517
- /** Short alias for `getRunDebugLogs`. */
518
- debugLogs(runId) {
519
- return this.getRunDebugLogs(runId);
520
- }
521
487
  cancelRun(runId) {
522
488
  return operations.cancelRun(this.#http, runId);
523
489
  }
@@ -544,12 +510,11 @@ export class AexClient {
544
510
  return operations.whoami(this.#http);
545
511
  }
546
512
  /**
547
- * Download EVERYTHING about a run as one zip, assembled client-side
513
+ * Download EVERYTHING public about a run as one zip, assembled client-side
548
514
  * from the public read endpoints (`getRun` + `listEvents` +
549
- * `listOutputs` + per-output `/download`). Organised into the four
550
- * namespace folders: `metadata/`, `events/`, `outputs/` (deliverables),
551
- * `logs/` (`runtime/`, `host/`, `provider-proxy/`, `control-plane/`
552
- * 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
553
518
  * bytes to a file path while still returning the bytes.
554
519
  */
555
520
  async download(runId, options) {
@@ -559,10 +524,6 @@ export class AexClient {
559
524
  async downloadOutputs(runId, options) {
560
525
  return writeOptionalFile(await operations.downloadOutputs(this.#http, runId), options?.to);
561
526
  }
562
- /** Download only the platform diagnostics (the `logs` namespace) as a zip. */
563
- async downloadLogs(runId, options) {
564
- return writeOptionalFile(await operations.downloadLogs(this.#http, runId), options?.to);
565
- }
566
527
  /** Download only the indexed event archive (the `events` namespace) as a zip. */
567
528
  async downloadEvents(runId, options) {
568
529
  return writeOptionalFile(await operations.downloadEvents(this.#http, runId), options?.to);
@@ -593,7 +554,7 @@ function resolveOutputFileSelector(outputs, selector, runId) {
593
554
  if (isOutputPathSelector(selector)) {
594
555
  const target = normalizeOutputLookupPath(selector.path);
595
556
  if (!target) {
596
- throw new RunStateError("AexClient.downloadOutput: output path must be non-empty", {
557
+ throw new RunStateError("AgentExecutor.downloadOutput: output path must be non-empty", {
597
558
  runId,
598
559
  path: selector.path
599
560
  });
@@ -610,15 +571,15 @@ function resolveOutputFileSelector(outputs, selector, runId) {
610
571
  if (matches.length === 1)
611
572
  return matches[0];
612
573
  if (matches.length > 1) {
613
- 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) });
614
575
  }
615
- 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`, {
616
577
  runId,
617
578
  path: selector.path
618
579
  });
619
580
  }
620
581
  if (typeof selector.id !== "string" || selector.id.length === 0) {
621
- 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 });
622
583
  }
623
584
  return { ...selector, id: selector.id };
624
585
  }
@@ -649,23 +610,6 @@ function sleep(ms, signal) {
649
610
  signal?.addEventListener("abort", onAbort, { once: true });
650
611
  });
651
612
  }
652
- /**
653
- * Encode a byte array as base64. Uses Node's `Buffer` when available
654
- * (the SDK ships as a Node tarball; this is the hot path), and falls
655
- * back to `btoa` for browser or edge runtimes that pull the SDK in
656
- * without Buffer.
657
- */
658
- function bytesToBase64(bytes) {
659
- const BufferCtor = globalThis.Buffer;
660
- if (BufferCtor) {
661
- return BufferCtor.from(bytes).toString("base64");
662
- }
663
- let binary = "";
664
- for (let i = 0; i < bytes.length; i++) {
665
- binary += String.fromCharCode(bytes[i]);
666
- }
667
- return globalThis.btoa(binary);
668
- }
669
613
  function generateIdempotencyKey() {
670
614
  const cryptoObj = globalThis.crypto;
671
615
  if (cryptoObj?.randomUUID)
@@ -675,133 +619,325 @@ function generateIdempotencyKey() {
675
619
  function normalisePrompt(input) {
676
620
  if (typeof input === "string") {
677
621
  if (!input) {
678
- 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");
679
623
  }
680
624
  return [input];
681
625
  }
682
626
  if (!Array.isArray(input) || input.length === 0) {
683
- 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");
684
628
  }
685
629
  for (const segment of input) {
686
630
  if (typeof segment !== "string" || !segment) {
687
- 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");
688
632
  }
689
633
  }
690
634
  return [...input];
691
635
  }
692
- /**
693
- * Walk the user-provided `Skill[]`, validating each instance and
694
- * producing:
695
- * - `skillRefs[]` — the wire entries for `submission.skills[]`, with
696
- * inline refs assigned positional slot ids (`transient-0`, …).
697
- * - `inlineBundles[]` — the bytes for each inline skill,
698
- * parallel-indexed by slot.
699
- *
700
- * Throws on consumed Skills (the user reused a draft after a prior
701
- * `submitRun` call) so that mistake is loud, not silent.
702
- */
703
- /**
704
- * Walk the user-provided Skill[], materialize every draft to assets, and return
705
- * the wire-shape refs.
706
- */
707
- async function materializeSkills(http, skills, fetch) {
708
- const out = [];
636
+ /** Walk Skill[] and turn drafts into direct-bootstrap descriptors. */
637
+ function prepareSkills(skills) {
638
+ const refs = [];
639
+ const directInputs = [];
709
640
  for (let i = 0; i < skills.length; i++) {
710
641
  const entry = skills[i];
711
642
  if (!(entry instanceof Skill)) {
712
- 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`);
713
644
  }
714
645
  if (entry.isConsumed) {
715
- 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`);
716
647
  }
717
648
  const ref = entry.ref;
718
649
  if (ref.kind === "draft") {
719
650
  const bundle = entry._takeDraftBundle();
720
651
  if (!bundle) {
721
- 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`);
722
653
  }
723
- const uploaded = await uploadAsset({
724
- http,
654
+ const input = directInputFor({
655
+ role: "skill",
656
+ index: i,
657
+ name: bundle.name,
658
+ contentHash: bundle.contentHash,
725
659
  bytes: bundle.bytes,
726
- hash: bundle.contentHash,
727
- ...(fetch ? { fetch } : {})
660
+ contentType: "application/zip"
728
661
  });
729
- out.push({
662
+ directInputs.push(input);
663
+ refs.push({
730
664
  kind: "asset",
731
- assetId: uploaded.assetId,
665
+ assetId: input.assetId,
732
666
  name: bundle.name
733
667
  });
734
668
  continue;
735
669
  }
736
670
  // Already-materialized asset ref.
737
- out.push(ref);
671
+ refs.push(ref);
738
672
  }
739
- return out;
673
+ return { refs, directInputs };
740
674
  }
741
- /** Materialize draft AgentsMd[] to assets; pass-through any already-materialized refs. */
742
- async function materializeAgentsMd(http, agentsMds, fetch) {
743
- const out = [];
675
+ /** Walk AgentsMd[] and turn drafts into direct-bootstrap descriptors. */
676
+ function prepareAgentsMd(agentsMds) {
677
+ const refs = [];
678
+ const directInputs = [];
744
679
  for (let i = 0; i < agentsMds.length; i++) {
745
680
  const entry = agentsMds[i];
746
681
  if (!(entry instanceof AgentsMd)) {
747
- 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`);
748
683
  }
749
684
  if (entry.isConsumed) {
750
- 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`);
751
686
  }
752
687
  const ref = entry.ref;
753
688
  if (ref.kind === "draft") {
754
689
  const bundle = entry._takeDraftBundle();
755
690
  if (!bundle) {
756
- 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`);
757
692
  }
758
- const uploaded = await uploadAsset({ http, bytes: bundle.bytes, hash: bundle.contentHash, ...(fetch ? { fetch } : {}) });
759
- 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({
760
703
  kind: "asset",
761
- assetId: uploaded.assetId,
704
+ assetId: input.assetId,
762
705
  name: bundle.name
763
706
  });
764
707
  continue;
765
708
  }
766
- out.push(ref);
709
+ refs.push(ref);
767
710
  }
768
- return out;
711
+ return { refs, directInputs };
769
712
  }
770
- /** Materialize draft File[] to assets; pass-through any already-materialized refs. */
771
- async function materializeFiles(http, files, fetch) {
772
- const out = [];
713
+ /** Walk File[] and turn drafts into direct-bootstrap descriptors. */
714
+ function prepareFiles(files) {
715
+ const refs = [];
716
+ const directInputs = [];
773
717
  for (let i = 0; i < files.length; i++) {
774
718
  const entry = files[i];
775
719
  if (!(entry instanceof File)) {
776
- 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`);
777
721
  }
778
722
  if (entry.isConsumed) {
779
- 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`);
780
724
  }
781
725
  const ref = entry.ref;
782
726
  if (ref.kind === "draft") {
783
727
  const bundle = entry._takeDraftBundle();
784
728
  if (!bundle) {
785
- 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`);
786
730
  }
787
- const uploaded = await uploadAsset({ http, bytes: bundle.bytes, hash: bundle.contentHash, ...(fetch ? { fetch } : {}) });
788
- 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
789
742
  ? {
790
743
  kind: "asset",
791
- assetId: uploaded.assetId,
744
+ assetId: input.assetId,
792
745
  name: bundle.name,
793
746
  mountPath: bundle.mountPath
794
747
  }
795
748
  : {
796
749
  kind: "asset",
797
- assetId: uploaded.assetId,
750
+ assetId: input.assetId,
798
751
  name: bundle.name
799
752
  });
800
753
  continue;
801
754
  }
802
- 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
+ });
803
812
  }
804
- 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;
805
941
  }
806
942
  function mergeMcpServers(inputs, explicitSecrets) {
807
943
  const submissionMcpServers = [];
@@ -812,14 +948,14 @@ function mergeMcpServers(inputs, explicitSecrets) {
812
948
  for (let i = 0; i < inputs.length; i++) {
813
949
  const entry = inputs[i];
814
950
  if (!(entry instanceof McpServer)) {
815
- 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`);
816
952
  }
817
953
  submissionMcpServers.push(entry.toSubmissionEntry());
818
954
  const secret = entry.toSecretEntry();
819
955
  if (secret) {
820
956
  const existing = secretByName.get(secret.name);
821
957
  if (existing && existing.url !== secret.url) {
822
- 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}"]`);
823
959
  }
824
960
  secretByName.set(secret.name, secret);
825
961
  }
@@ -847,7 +983,7 @@ function mergeProxyEndpointAuth(fromInstances, fromExplicitSecrets) {
847
983
  for (const entry of fromInstances) {
848
984
  const existing = byName.get(entry.name);
849
985
  if (existing && existing.value.type !== entry.value.type) {
850
- throw new Error(`AexClient.submitRun: proxyEndpoint "${entry.name}" auth type conflicts ` +
986
+ throw new Error(`AgentExecutor.submitRun: proxyEndpoint "${entry.name}" auth type conflicts ` +
851
987
  `with secrets.proxyEndpointAuth (instance=${entry.value.type}, secrets=${existing.value.type})`);
852
988
  }
853
989
  byName.set(entry.name, entry);