@aexhq/sdk 0.28.1 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +80 -7
  2. package/dist/_contracts/event-guards.d.ts +67 -0
  3. package/dist/_contracts/event-guards.js +36 -0
  4. package/dist/_contracts/index.d.ts +2 -0
  5. package/dist/_contracts/index.js +6 -0
  6. package/dist/_contracts/run-config.d.ts +3 -3
  7. package/dist/_contracts/run-config.js +2 -2
  8. package/dist/_contracts/run-trace.d.ts +7 -0
  9. package/dist/_contracts/run-trace.js +9 -0
  10. package/dist/_contracts/runtime-sizes.d.ts +25 -79
  11. package/dist/_contracts/runtime-sizes.js +18 -39
  12. package/dist/_contracts/runtime-types.d.ts +31 -0
  13. package/dist/_contracts/submission.d.ts +40 -17
  14. package/dist/_contracts/submission.js +45 -18
  15. package/dist/agents-md.d.ts +4 -1
  16. package/dist/agents-md.js +10 -9
  17. package/dist/agents-md.js.map +1 -1
  18. package/dist/cli.mjs +17823 -3963
  19. package/dist/cli.mjs.sha256 +1 -1
  20. package/dist/client.d.ts +96 -17
  21. package/dist/client.js +238 -79
  22. package/dist/client.js.map +1 -1
  23. package/dist/data-tools.d.ts +23 -0
  24. package/dist/data-tools.js +102 -13
  25. package/dist/data-tools.js.map +1 -1
  26. package/dist/file.d.ts +4 -1
  27. package/dist/file.js +10 -9
  28. package/dist/file.js.map +1 -1
  29. package/dist/index.d.ts +9 -8
  30. package/dist/index.js +9 -6
  31. package/dist/index.js.map +1 -1
  32. package/dist/skill.d.ts +8 -6
  33. package/dist/skill.js +14 -14
  34. package/dist/skill.js.map +1 -1
  35. package/dist/tool.d.ts +4 -1
  36. package/dist/tool.js +10 -8
  37. package/dist/tool.js.map +1 -1
  38. package/dist/version.d.ts +1 -1
  39. package/dist/version.js +1 -1
  40. package/docs/concepts/agent-tools.md +7 -3
  41. package/docs/concepts/runs.md +1 -1
  42. package/docs/defaults.md +3 -2
  43. package/docs/events.md +32 -9
  44. package/docs/limits-and-quotas.md +2 -2
  45. package/docs/networking.md +141 -0
  46. package/docs/quickstart.md +19 -10
  47. package/docs/run-config.md +2 -2
  48. package/examples/chat-corpus.ts +85 -0
  49. package/package.json +2 -2
package/dist/client.js CHANGED
@@ -1,4 +1,4 @@
1
- import { AexError, DEFAULT_CREDENTIAL_MODE, DEFAULT_RUN_PROVIDER, HttpClient, RUN_REGIONS, RUNTIME_KINDS, RunStateError, SecretString, isRunSettled, operations, parseCredentialMode, providersForModel, streamCoordinatorEvents, parseRunLimits, BUILTIN_TOOL_NAMES, TERMINAL_RUN_STATUSES } from "./_contracts/index.js";
1
+ import { AexError, DEFAULT_CREDENTIAL_MODE, DEFAULT_RUN_PROVIDER, HttpClient, REGIONS, RUNTIME_KINDS, RunConfigValidationError, RunStateError, SecretString, isRunSettled, operations, parseCredentialMode, providersForModel, streamCoordinatorEvents, summarizeRunTrace, textOf, parseRunLimits, BUILTIN_TOOL_NAMES, TERMINAL_RUN_STATUSES } from "./_contracts/index.js";
2
2
  import { AgentsMd } from "./agents-md.js";
3
3
  import { uploadAsset } from "./asset-upload.js";
4
4
  import { File } from "./file.js";
@@ -286,17 +286,86 @@ export class AgentExecutor {
286
286
  });
287
287
  }
288
288
  /**
289
- * Submit a run and wait until its RECORD is terminal the settle-consistent
290
- * "do it and give me the result" primitive. Returns the final `Run`, so on
291
- * resolve a subsequent `getRun`/`listOutputs` is guaranteed consistent (it
292
- * polls `getRun` via {@link waitForRun}, NOT the RUN_FINISHED event, which the
293
- * runner emits before the platform commits the record). For long-running flows
294
- * that need live events, prefer `submit` + `streamEnvelopes(runId, {
295
- * settleConsistent: true })`, or `submit` + `stream(runId)` + `wait(runId)`.
289
+ * Submit a run, wait until its RECORD is terminal, and collect the full
290
+ * {@link RunResult} — the settle-consistent "do it and give me the result"
291
+ * primitive. Folds the poll loop every consumer hand-rolled into one call:
292
+ * submit {@link waitForRun} (polls `getRun`, NOT the earlier RUN_FINISHED
293
+ * event) poll `listEvents` until the snapshot is settle-bracketed
294
+ * (RUN_STARTED + a terminal event present) → `listOutputs` decode the trace
295
+ * and assistant text. On resolve, `getRun`/`listOutputs` are guaranteed
296
+ * consistent.
297
+ *
298
+ * Uses polling (portable across backends), NOT the coordinator WebSocket. By
299
+ * default a failed run resolves with `ok: false` and a populated `error`; pass
300
+ * `{ throwOnFailure: true }` to throw instead. For live events prefer `submit`
301
+ * + `streamEnvelopes(runId, { settleConsistent: true })`.
296
302
  */
297
- async run(options) {
303
+ async run(options, opts = {}) {
304
+ const signal = opts.signal ?? options.signal;
298
305
  const runId = await this.submit(options);
299
- return this.waitForRun(runId, options.signal ? { signal: options.signal } : {});
306
+ const run = await this.waitForRun(runId, {
307
+ ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
308
+ ...(signal ? { signal } : {})
309
+ });
310
+ const events = await this.#collectSettledEvents(runId, signal);
311
+ const outputs = await this.listOutputs(runId);
312
+ const ok = run.status === "succeeded";
313
+ const costUsd = run.costTelemetry?.billedCostUsd;
314
+ const errorMessage = typeof run.errorMessage === "string" && run.errorMessage ? run.errorMessage : undefined;
315
+ const result = {
316
+ runId,
317
+ run,
318
+ status: run.status,
319
+ ok,
320
+ text: textOf(events),
321
+ events,
322
+ trace: summarizeRunTrace(events),
323
+ outputs,
324
+ ...(run.usage ? { usage: run.usage } : {}),
325
+ ...(typeof costUsd === "number" ? { costUsd } : {}),
326
+ ...(!ok && errorMessage ? { error: errorMessage } : {})
327
+ };
328
+ if (opts.throwOnFailure && !ok) {
329
+ throw new RunStateError(`AgentExecutor.run: run ${runId} ended ${run.status}${errorMessage ? `: ${errorMessage}` : ""}`, { runId, status: run.status });
330
+ }
331
+ return result;
332
+ }
333
+ /**
334
+ * Explicit, discoverable alias for {@link run}: submit, wait, and collect the
335
+ * full {@link RunResult} in one call.
336
+ */
337
+ runAndCollect(options, opts) {
338
+ return this.run(options, opts);
339
+ }
340
+ /**
341
+ * Poll `listEvents` until the snapshot is settle-bracketed — both a
342
+ * RUN_STARTED and a terminal (RUN_FINISHED / RUN_ERROR) event present — then
343
+ * return it. The runner emits the terminal AG-UI event BEFORE the platform
344
+ * commits the record, and the `listEvents` snapshot can lag the terminal
345
+ * record by a beat; this closes that race so the decoded trace/text/outputs
346
+ * are complete. Bounded so an older runtime that never emits one of the
347
+ * brackets still returns the best snapshot available.
348
+ */
349
+ async #collectSettledEvents(runId, signal) {
350
+ const intervalMs = 500;
351
+ const maxAttempts = 20;
352
+ let latest = [];
353
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
354
+ if (signal?.aborted)
355
+ return latest;
356
+ latest = await this.listEvents(runId);
357
+ const hasStart = latest.some((event) => event.type === "RUN_STARTED");
358
+ const hasTerminal = latest.some((event) => event.type === "RUN_FINISHED" || event.type === "RUN_ERROR");
359
+ if (hasStart && hasTerminal)
360
+ return latest;
361
+ try {
362
+ await sleep(intervalMs, signal);
363
+ }
364
+ catch {
365
+ return latest;
366
+ }
367
+ }
368
+ return latest;
300
369
  }
301
370
  /**
302
371
  * Submit a run and return its run id immediately. Use that id with
@@ -316,7 +385,7 @@ export class AgentExecutor {
316
385
  */
317
386
  async submit(options) {
318
387
  if (!options || typeof options !== "object") {
319
- throw new Error("AgentExecutor.submit: options is required");
388
+ throw new RunConfigValidationError("AgentExecutor.submit: options is required");
320
389
  }
321
390
  // A model maps to one or more upstream providers (see MODEL_PROVIDER_IDS).
322
391
  // `providersForModel` returns the supported providers in priority order, or
@@ -327,27 +396,23 @@ export class AgentExecutor {
327
396
  if (options.provider &&
328
397
  supportedProviders.length > 0 &&
329
398
  !supportedProviders.includes(options.provider)) {
330
- throw new Error(`AgentExecutor.submit: provider ${JSON.stringify(options.provider)} is not available for ` +
399
+ throw new RunConfigValidationError(`AgentExecutor.submit: provider ${JSON.stringify(options.provider)} is not available for ` +
331
400
  `model ${JSON.stringify(options.model)} (supported: ${supportedProviders.join(", ")})`);
332
401
  }
333
402
  const provider = options.provider ?? supportedProviders[0] ?? DEFAULT_RUN_PROVIDER;
334
403
  const credentialMode = parseCredentialMode(options.credentialMode);
335
- if (!options.secrets) {
336
- throw new Error("AgentExecutor.submit: secrets is required");
337
- }
338
- // The BYOK provider key (for the selected `provider`) is required. The
339
- // shared parser re-runs this check on the server; failing early here
340
- // gives the caller a synchronous error before any network call.
341
- const selectedProviderKey = options.secrets.apiKeys?.[provider] ?? options.secrets.apiKey;
342
- if (typeof selectedProviderKey !== "string" || !selectedProviderKey) {
343
- throw new Error("AgentExecutor.submit: secrets.apiKey is required");
344
- }
404
+ // Resolve the BYOK key(s) across the top-level sugar (`apiKey`,
405
+ // `credentials`) and the advanced `secrets` bundle, folding everything into a
406
+ // single `apiKeys` map (the wire shape the platform reads). Validates the
407
+ // selected provider's key is present and that the sources don't disagree,
408
+ // failing synchronously before any network call.
409
+ const { foldedApiKeys } = resolveSubmitCredentials(options, provider);
345
410
  if (typeof options.model !== "string" || !options.model) {
346
- throw new Error("AgentExecutor.submit: model is required");
411
+ throw new RunConfigValidationError("AgentExecutor.submit: model is required");
347
412
  }
348
413
  const prompt = normalisePrompt(options.prompt);
349
414
  const { endpoints: proxyEndpointDeclarations, auth: proxyEndpointAuthFromInstances } = splitProxyEndpoints(options.proxyEndpoints ?? []);
350
- const mergedProxyAuth = mergeProxyEndpointAuth(proxyEndpointAuthFromInstances, options.secrets.proxyEndpointAuth ?? []);
415
+ const mergedProxyAuth = mergeProxyEndpointAuth(proxyEndpointAuthFromInstances, options.secrets?.proxyEndpointAuth ?? []);
351
416
  // Split secretEnv into value-free declarations (hashed submission) and
352
417
  // ephemeral values (vaulted secrets channel), mirroring the proxy split.
353
418
  const { declarations: secretEnvDeclarations, values: envSecretValues } = splitSecretEnv(options.secretEnv);
@@ -360,8 +425,8 @@ export class AgentExecutor {
360
425
  `(got ${JSON.stringify(options.runtime)})`);
361
426
  }
362
427
  if (options.region !== undefined &&
363
- !RUN_REGIONS.includes(options.region)) {
364
- throw new AexError("RUN_CONFIG_INVALID", `AgentExecutor.submit: region must be one of: ${RUN_REGIONS.join(", ")} ` +
428
+ !REGIONS.includes(options.region)) {
429
+ throw new AexError("RUN_CONFIG_INVALID", `AgentExecutor.submit: region must be one of: ${REGIONS.join(", ")} ` +
365
430
  `(got ${JSON.stringify(options.region)})`);
366
431
  }
367
432
  // Validate the per-run limits override with the SAME parser the server runs
@@ -383,7 +448,7 @@ export class AgentExecutor {
383
448
  const preparedTools = await prepareTools(options.tools ?? [], uploader);
384
449
  const preparedAgentsMd = await prepareAgentsMd(options.agentsMd ?? [], uploader);
385
450
  const preparedFiles = await prepareFiles(options.files ?? [], uploader);
386
- const { submissionMcpServers, mergedMcpSecrets } = mergeMcpServers(options.mcpServers ?? [], options.secrets.mcpServers ?? []);
451
+ const { submissionMcpServers, mergedMcpSecrets } = mergeMcpServers(options.mcpServers ?? [], options.secrets?.mcpServers ?? []);
387
452
  const outputCapture = outputsForWire(options.outputs);
388
453
  const submission = {
389
454
  model: options.model,
@@ -424,6 +489,9 @@ export class AgentExecutor {
424
489
  };
425
490
  const secrets = {
426
491
  ...options.secrets,
492
+ // Folded BYOK keys (sugar + secrets.apiKeys) override; omitted entirely
493
+ // when empty so a pure legacy `secrets.apiKey` submission stays unchanged.
494
+ ...(Object.keys(foldedApiKeys).length > 0 ? { apiKeys: foldedApiKeys } : {}),
427
495
  ...(mergedMcpSecrets.length > 0 ? { mcpServers: mergedMcpSecrets } : {}),
428
496
  ...(mergedProxyAuth.length > 0 ? { proxyEndpointAuth: mergedProxyAuth } : {}),
429
497
  ...(Object.keys(envSecretValues).length > 0 ? { envSecrets: envSecretValues } : {})
@@ -483,6 +551,60 @@ export class AgentExecutor {
483
551
  listRuns(query) {
484
552
  return operations.listRuns(this.#http, query);
485
553
  }
554
+ /**
555
+ * Find output files across runs by filename / extension / content type.
556
+ * Returns lean REFERENCE hits (runId, outputId, filename, size, content type)
557
+ * — never bytes; fetch content with {@link readOutputText}. Scope the search
558
+ * to a corpus with `query.runIds`; omit it to scan the whole workspace (needs
559
+ * the owner-gated `listRuns`). Composed client-side for the MVP (per-run
560
+ * `listOutputs` + the contracts output filter), so it works against any
561
+ * already-terminal run with no new server endpoint. Bounded by
562
+ * `query.limit` (default 100) — for very large corpora prefer the deferred
563
+ * server-side index.
564
+ */
565
+ async searchOutputs(query = {}) {
566
+ const runIds = query.runIds ?? (await this.#allWorkspaceRunIds());
567
+ const limit = query.limit ?? 100;
568
+ // Translate the search query to an OutputQuery so the contracts output
569
+ // filter (classifyOutput / basename match / contentType wildcard) does the
570
+ // matching — no re-derived filter logic here.
571
+ const outputQuery = {
572
+ ...(query.filename ? { filename: new RegExp(escapeRegExp(query.filename), "i") } : {}),
573
+ ...(query.extension ? { extension: query.extension } : {}),
574
+ ...(query.contentType ? { contentType: query.contentType } : {})
575
+ };
576
+ const hasFilter = Object.keys(outputQuery).length > 0;
577
+ const hits = [];
578
+ for (const runId of runIds) {
579
+ const outputs = hasFilter
580
+ ? await this.listOutputs(runId, outputQuery)
581
+ : await this.listOutputs(runId);
582
+ for (const o of outputs) {
583
+ hits.push({
584
+ runId,
585
+ outputId: o.id,
586
+ ...(o.filename !== undefined ? { filename: o.filename } : {}),
587
+ ...(o.sizeBytes !== undefined ? { sizeBytes: o.sizeBytes } : {}),
588
+ ...(o.contentType !== undefined ? { contentType: o.contentType } : {})
589
+ });
590
+ if (hits.length >= limit)
591
+ return { hits };
592
+ }
593
+ }
594
+ return { hits };
595
+ }
596
+ /** Enumerate every run id in the workspace by paging `listRuns`. */
597
+ async #allWorkspaceRunIds() {
598
+ const ids = [];
599
+ let cursor;
600
+ do {
601
+ const page = await this.listRuns(cursor ? { cursor } : {});
602
+ for (const run of page.runs)
603
+ ids.push(run.id);
604
+ cursor = page.nextCursor;
605
+ } while (cursor);
606
+ return ids;
607
+ }
486
608
  /**
487
609
  * Fetch the self-contained `RunUnit`: parsed submission inputs,
488
610
  * attempts, indexed events (inline + cursor for the tail), raw
@@ -714,6 +836,10 @@ const TERMINAL_STATUSES = new Set(TERMINAL_RUN_STATUSES);
714
836
  function isTerminal(status) {
715
837
  return typeof status === "string" && TERMINAL_STATUSES.has(status);
716
838
  }
839
+ /** Escape a literal string for safe interpolation into a RegExp. */
840
+ function escapeRegExp(input) {
841
+ return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
842
+ }
717
843
  function isOutputPathSelector(selector) {
718
844
  return Boolean(selector && typeof selector === "object" && "path" in selector);
719
845
  }
@@ -793,20 +919,64 @@ function generateIdempotencyKey() {
793
919
  function normalisePrompt(input) {
794
920
  if (typeof input === "string") {
795
921
  if (!input) {
796
- throw new Error("AgentExecutor.submit: prompt must be a non-empty string");
922
+ throw new RunConfigValidationError("AgentExecutor.submit: prompt must be a non-empty string");
797
923
  }
798
924
  return [input];
799
925
  }
800
926
  if (!Array.isArray(input) || input.length === 0) {
801
- throw new Error("AgentExecutor.submit: prompt must be a non-empty string or string array");
927
+ throw new RunConfigValidationError("AgentExecutor.submit: prompt must be a non-empty string or string array");
802
928
  }
803
929
  for (const segment of input) {
804
930
  if (typeof segment !== "string" || !segment) {
805
- throw new Error("AgentExecutor.submit: prompt segments must be non-empty strings");
931
+ throw new RunConfigValidationError("AgentExecutor.submit: prompt segments must be non-empty strings");
806
932
  }
807
933
  }
808
934
  return [...input];
809
935
  }
936
+ /**
937
+ * Resolve the BYOK provider key(s) for a submission across the top-level sugar
938
+ * (`apiKey`, `credentials`) and the advanced `secrets` bundle, folding them into
939
+ * one `apiKeys` map (the wire shape the platform reads). Per-provider precedence:
940
+ * `secrets.apiKeys[p] ?? secrets.apiKey ?? credentials[p] ?? apiKey`. Legacy
941
+ * `secrets.apiKey` is left in place (not folded) so a pure-legacy submission's
942
+ * wire shape is unchanged; the platform falls back to it for the selected
943
+ * provider. Throws synchronously when the selected provider has no key, or when
944
+ * the sources name DIFFERENT keys for it (a call-site mistake).
945
+ */
946
+ function resolveSubmitCredentials(options, provider) {
947
+ const secrets = options.secrets;
948
+ const selectedCandidates = [
949
+ secrets?.apiKeys?.[provider],
950
+ secrets?.apiKey,
951
+ options.credentials?.[provider],
952
+ options.apiKey
953
+ ].filter((value) => typeof value === "string" && value.length > 0);
954
+ if (new Set(selectedCandidates).size > 1) {
955
+ throw new RunConfigValidationError(`AgentExecutor.submit: conflicting API keys for provider ${JSON.stringify(provider)} ` +
956
+ "(secrets.apiKeys / secrets.apiKey / credentials / apiKey disagree). Supply exactly one.");
957
+ }
958
+ if (selectedCandidates.length === 0) {
959
+ throw new RunConfigValidationError("AgentExecutor.submit: a provider API key is required — pass `apiKey`, " +
960
+ "`credentials[provider]`, `secrets.apiKey`, or `secrets.apiKeys[provider]`.");
961
+ }
962
+ // Fold in precedence order (lowest first; later writes win):
963
+ // apiKey (selected provider) < credentials < secrets.apiKeys.
964
+ const foldedApiKeys = {};
965
+ if (typeof options.apiKey === "string" && options.apiKey.length > 0) {
966
+ foldedApiKeys[provider] = options.apiKey;
967
+ }
968
+ for (const [p, key] of Object.entries(options.credentials ?? {})) {
969
+ if (typeof key === "string" && key.length > 0) {
970
+ foldedApiKeys[p] = key;
971
+ }
972
+ }
973
+ for (const [p, key] of Object.entries(secrets?.apiKeys ?? {})) {
974
+ if (typeof key === "string" && key.length > 0) {
975
+ foldedApiKeys[p] = key;
976
+ }
977
+ }
978
+ return { foldedApiKeys };
979
+ }
810
980
  function postHookForWire(input) {
811
981
  if (input === undefined || typeof input.command !== "string" || input.command.trim().length === 0) {
812
982
  return undefined;
@@ -835,31 +1005,41 @@ function outputsForWire(outputs) {
835
1005
  ...(outputs.maxFiles !== undefined ? { maxFiles: outputs.maxFiles } : {})
836
1006
  };
837
1007
  }
1008
+ /**
1009
+ * Resolve a draft's asset id: reuse the cached id from a prior submit, otherwise
1010
+ * upload the bytes and cache the result on the instance.
1011
+ */
1012
+ async function resolveAssetId(entry, bundle, uploader) {
1013
+ const cached = entry._cachedAssetId;
1014
+ if (cached !== undefined) {
1015
+ return cached;
1016
+ }
1017
+ const uploaded = await uploader({
1018
+ bytes: bundle.bytes,
1019
+ hash: bundle.contentHash,
1020
+ contentType: "application/zip"
1021
+ });
1022
+ entry._rememberAsset(uploaded.assetId);
1023
+ return uploaded.assetId;
1024
+ }
838
1025
  /** Walk Skill[], eagerly upload drafts as assets, and return plain asset refs. */
839
1026
  async function prepareSkills(skills, uploader) {
840
1027
  const refs = [];
841
1028
  for (let i = 0; i < skills.length; i++) {
842
1029
  const entry = skills[i];
843
1030
  if (!(entry instanceof Skill)) {
844
- throw new Error(`AgentExecutor.submit: skills[${i}] must be a Skill instance`);
845
- }
846
- if (entry.isConsumed) {
847
- throw new Error(`AgentExecutor.submit: skills[${i}] was already consumed by a prior submit`);
1031
+ throw new RunConfigValidationError(`AgentExecutor.submit: skills[${i}] must be a Skill instance`);
848
1032
  }
849
1033
  const ref = entry.ref;
850
1034
  if (ref.kind === "draft") {
851
1035
  const bundle = entry._takeDraftBundle();
852
1036
  if (!bundle) {
853
- throw new Error(`AgentExecutor.submit: skills[${i}] is draft but has no bytes`);
1037
+ throw new RunConfigValidationError(`AgentExecutor.submit: skills[${i}] is draft but has no bytes`);
854
1038
  }
855
- const uploaded = await uploader({
856
- bytes: bundle.bytes,
857
- hash: bundle.contentHash,
858
- contentType: "application/zip"
859
- });
1039
+ const assetId = await resolveAssetId(entry, bundle, uploader);
860
1040
  refs.push({
861
1041
  kind: "asset",
862
- assetId: uploaded.assetId,
1042
+ assetId,
863
1043
  name: bundle.name
864
1044
  });
865
1045
  continue;
@@ -885,7 +1065,7 @@ async function prepareTools(tools, uploader) {
885
1065
  // A bare string is a builtin tool reference.
886
1066
  if (typeof entry === "string") {
887
1067
  if (!BUILTIN_TOOL_NAMES.includes(entry)) {
888
- throw new Error(`AgentExecutor.submit: tools[${i}] (${JSON.stringify(entry)}) is not a builtin tool name; ` +
1068
+ throw new RunConfigValidationError(`AgentExecutor.submit: tools[${i}] (${JSON.stringify(entry)}) is not a builtin tool name; ` +
889
1069
  `expected a Tool instance or one of: ${BUILTIN_TOOL_NAMES.join(", ")}`);
890
1070
  }
891
1071
  if (!seenBuiltins.has(entry)) {
@@ -895,23 +1075,16 @@ async function prepareTools(tools, uploader) {
895
1075
  continue;
896
1076
  }
897
1077
  if (!(entry instanceof Tool)) {
898
- throw new Error(`AgentExecutor.submit: tools[${i}] must be a Tool instance or a builtin tool name`);
899
- }
900
- if (entry.isConsumed) {
901
- throw new Error(`AgentExecutor.submit: tools[${i}] was already consumed by a prior submit`);
1078
+ throw new RunConfigValidationError(`AgentExecutor.submit: tools[${i}] must be a Tool instance or a builtin tool name`);
902
1079
  }
903
1080
  const ref = entry.ref;
904
1081
  if (ref.kind === "draft") {
905
1082
  const bundle = entry._takeDraftBundle();
906
1083
  if (!bundle) {
907
- throw new Error(`AgentExecutor.submit: tools[${i}] is draft but has no bytes`);
1084
+ throw new RunConfigValidationError(`AgentExecutor.submit: tools[${i}] is draft but has no bytes`);
908
1085
  }
909
- const uploaded = await uploader({
910
- bytes: bundle.bytes,
911
- hash: bundle.contentHash,
912
- contentType: "application/zip"
913
- });
914
- refs.push({ ...bundle.ref, assetId: uploaded.assetId });
1086
+ const assetId = await resolveAssetId(entry, bundle, uploader);
1087
+ refs.push({ ...bundle.ref, assetId });
915
1088
  continue;
916
1089
  }
917
1090
  refs.push(ref);
@@ -924,25 +1097,18 @@ async function prepareAgentsMd(agentsMds, uploader) {
924
1097
  for (let i = 0; i < agentsMds.length; i++) {
925
1098
  const entry = agentsMds[i];
926
1099
  if (!(entry instanceof AgentsMd)) {
927
- throw new Error(`AgentExecutor.submit: agentsMd[${i}] must be an AgentsMd instance`);
928
- }
929
- if (entry.isConsumed) {
930
- throw new Error(`AgentExecutor.submit: agentsMd[${i}] was already consumed by a prior submit`);
1100
+ throw new RunConfigValidationError(`AgentExecutor.submit: agentsMd[${i}] must be an AgentsMd instance`);
931
1101
  }
932
1102
  const ref = entry.ref;
933
1103
  if (ref.kind === "draft") {
934
1104
  const bundle = entry._takeDraftBundle();
935
1105
  if (!bundle) {
936
- throw new Error(`AgentExecutor.submit: agentsMd[${i}] is draft but has no bytes`);
1106
+ throw new RunConfigValidationError(`AgentExecutor.submit: agentsMd[${i}] is draft but has no bytes`);
937
1107
  }
938
- const uploaded = await uploader({
939
- bytes: bundle.bytes,
940
- hash: bundle.contentHash,
941
- contentType: "application/zip"
942
- });
1108
+ const assetId = await resolveAssetId(entry, bundle, uploader);
943
1109
  refs.push({
944
1110
  kind: "asset",
945
- assetId: uploaded.assetId,
1111
+ assetId,
946
1112
  name: bundle.name
947
1113
  });
948
1114
  continue;
@@ -957,25 +1123,18 @@ async function prepareFiles(files, uploader) {
957
1123
  for (let i = 0; i < files.length; i++) {
958
1124
  const entry = files[i];
959
1125
  if (!(entry instanceof File)) {
960
- throw new Error(`AgentExecutor.submit: files[${i}] must be a File instance`);
961
- }
962
- if (entry.isConsumed) {
963
- throw new Error(`AgentExecutor.submit: files[${i}] was already consumed by a prior submit`);
1126
+ throw new RunConfigValidationError(`AgentExecutor.submit: files[${i}] must be a File instance`);
964
1127
  }
965
1128
  const ref = entry.ref;
966
1129
  if (ref.kind === "draft") {
967
1130
  const bundle = entry._takeDraftBundle();
968
1131
  if (!bundle) {
969
- throw new Error(`AgentExecutor.submit: files[${i}] is draft but has no bytes`);
1132
+ throw new RunConfigValidationError(`AgentExecutor.submit: files[${i}] is draft but has no bytes`);
970
1133
  }
971
- const uploaded = await uploader({
972
- bytes: bundle.bytes,
973
- hash: bundle.contentHash,
974
- contentType: "application/zip"
975
- });
1134
+ const assetId = await resolveAssetId(entry, bundle, uploader);
976
1135
  refs.push({
977
1136
  kind: "asset",
978
- assetId: uploaded.assetId,
1137
+ assetId,
979
1138
  name: bundle.name,
980
1139
  mountPath: bundle.mountPath
981
1140
  });
@@ -988,7 +1147,7 @@ async function prepareFiles(files, uploader) {
988
1147
  function getSubmittedRunId(response) {
989
1148
  const id = response.id ?? response.runId;
990
1149
  if (typeof id !== "string" || id.length === 0) {
991
- throw new Error("AgentExecutor.submit: submit response did not include a run id");
1150
+ throw new RunStateError("AgentExecutor.submit: submit response did not include a run id");
992
1151
  }
993
1152
  return id;
994
1153
  }
@@ -1001,14 +1160,14 @@ function mergeMcpServers(inputs, explicitSecrets) {
1001
1160
  for (let i = 0; i < inputs.length; i++) {
1002
1161
  const entry = inputs[i];
1003
1162
  if (!(entry instanceof McpServer)) {
1004
- throw new Error(`AgentExecutor.submit: mcpServers[${i}] must be an McpServer instance`);
1163
+ throw new RunConfigValidationError(`AgentExecutor.submit: mcpServers[${i}] must be an McpServer instance`);
1005
1164
  }
1006
1165
  submissionMcpServers.push(entry.toSubmissionEntry());
1007
1166
  const secret = entry.toSecretEntry();
1008
1167
  if (secret) {
1009
1168
  const existing = secretByName.get(secret.name);
1010
1169
  if (existing && existing.url !== secret.url) {
1011
- throw new Error(`AgentExecutor.submit: mcpServers[${i}].url conflicts with secrets.mcpServers["${secret.name}"]`);
1170
+ throw new RunConfigValidationError(`AgentExecutor.submit: mcpServers[${i}].url conflicts with secrets.mcpServers["${secret.name}"]`);
1012
1171
  }
1013
1172
  secretByName.set(secret.name, secret);
1014
1173
  }
@@ -1036,7 +1195,7 @@ function mergeProxyEndpointAuth(fromInstances, fromExplicitSecrets) {
1036
1195
  for (const entry of fromInstances) {
1037
1196
  const existing = byName.get(entry.name);
1038
1197
  if (existing && existing.value.type !== entry.value.type) {
1039
- throw new Error(`AgentExecutor.submit: proxyEndpoint "${entry.name}" auth type conflicts ` +
1198
+ throw new RunConfigValidationError(`AgentExecutor.submit: proxyEndpoint "${entry.name}" auth type conflicts ` +
1040
1199
  `with secrets.proxyEndpointAuth (instance=${entry.value.type}, secrets=${existing.value.type})`);
1041
1200
  }
1042
1201
  byName.set(entry.name, entry);