@aexhq/sdk 0.29.0 → 0.31.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 (90) hide show
  1. package/README.md +95 -8
  2. package/dist/_contracts/connection-ticket.d.ts +1 -1
  3. package/dist/_contracts/connection-ticket.js +1 -1
  4. package/dist/_contracts/event-envelope.d.ts +5 -8
  5. package/dist/_contracts/event-envelope.js +5 -6
  6. package/dist/_contracts/event-guards.d.ts +67 -0
  7. package/dist/_contracts/event-guards.js +36 -0
  8. package/dist/_contracts/event-stream-client.d.ts +1 -1
  9. package/dist/_contracts/http.js +1 -1
  10. package/dist/_contracts/index.d.ts +2 -0
  11. package/dist/_contracts/index.js +6 -0
  12. package/dist/_contracts/operations.d.ts +2 -47
  13. package/dist/_contracts/operations.js +7 -112
  14. package/dist/_contracts/provider-support.d.ts +48 -138
  15. package/dist/_contracts/provider-support.js +10 -41
  16. package/dist/_contracts/proxy-protocol.d.ts +7 -7
  17. package/dist/_contracts/proxy-protocol.js +8 -8
  18. package/dist/_contracts/run-config.d.ts +7 -20
  19. package/dist/_contracts/run-config.js +8 -46
  20. package/dist/_contracts/run-cost.d.ts +1 -5
  21. package/dist/_contracts/run-cost.js +0 -8
  22. package/dist/_contracts/run-custody.d.ts +4 -6
  23. package/dist/_contracts/run-custody.js +0 -8
  24. package/dist/_contracts/run-trace.d.ts +7 -0
  25. package/dist/_contracts/run-trace.js +9 -0
  26. package/dist/_contracts/run-unit.d.ts +1 -1
  27. package/dist/_contracts/run-unit.js +2 -2
  28. package/dist/_contracts/runner-event.d.ts +1 -1
  29. package/dist/_contracts/runner-event.js +1 -1
  30. package/dist/_contracts/runtime-manifest.d.ts +13 -26
  31. package/dist/_contracts/runtime-manifest.js +6 -35
  32. package/dist/_contracts/runtime-types.d.ts +32 -1
  33. package/dist/_contracts/sdk-secrets.js +4 -4
  34. package/dist/_contracts/side-effect-audit.d.ts +2 -4
  35. package/dist/_contracts/side-effect-audit.js +2 -4
  36. package/dist/_contracts/status.d.ts +1 -1
  37. package/dist/_contracts/status.js +1 -1
  38. package/dist/_contracts/submission.d.ts +19 -126
  39. package/dist/_contracts/submission.js +31 -185
  40. package/dist/_contracts/webhook-verify.d.ts +1 -1
  41. package/dist/_contracts/webhook-verify.js +1 -1
  42. package/dist/agents-md.d.ts +4 -1
  43. package/dist/agents-md.js +10 -9
  44. package/dist/agents-md.js.map +1 -1
  45. package/dist/asset-upload.d.ts +4 -10
  46. package/dist/asset-upload.js +4 -47
  47. package/dist/asset-upload.js.map +1 -1
  48. package/dist/cli.mjs +17647 -3950
  49. package/dist/cli.mjs.sha256 +1 -1
  50. package/dist/client.d.ts +79 -61
  51. package/dist/client.js +207 -125
  52. package/dist/client.js.map +1 -1
  53. package/dist/data-tools.d.ts +23 -0
  54. package/dist/data-tools.js +102 -13
  55. package/dist/data-tools.js.map +1 -1
  56. package/dist/file.d.ts +4 -1
  57. package/dist/file.js +10 -9
  58. package/dist/file.js.map +1 -1
  59. package/dist/index.d.ts +9 -8
  60. package/dist/index.js +10 -8
  61. package/dist/index.js.map +1 -1
  62. package/dist/skill.d.ts +9 -7
  63. package/dist/skill.js +15 -15
  64. package/dist/skill.js.map +1 -1
  65. package/dist/tool.d.ts +4 -1
  66. package/dist/tool.js +10 -8
  67. package/dist/tool.js.map +1 -1
  68. package/dist/version.d.ts +1 -1
  69. package/dist/version.js +1 -1
  70. package/docs/cleanup.md +2 -2
  71. package/docs/concepts/agent-tools.md +9 -5
  72. package/docs/concepts/composition.md +1 -1
  73. package/docs/concepts/providers-and-runtimes.md +2 -4
  74. package/docs/concepts/runs.md +3 -6
  75. package/docs/credentials.md +2 -5
  76. package/docs/defaults.md +22 -22
  77. package/docs/events.md +32 -9
  78. package/docs/limits-and-quotas.md +40 -40
  79. package/docs/limits.md +1 -1
  80. package/docs/networking.md +141 -0
  81. package/docs/outputs.md +1 -1
  82. package/docs/provider-runtime-capabilities.md +36 -64
  83. package/docs/public-surface.json +2 -3
  84. package/docs/quickstart.md +32 -11
  85. package/docs/run-config.md +3 -4
  86. package/docs/secrets.md +7 -5
  87. package/docs/skills.md +4 -12
  88. package/docs/vision-skills.md +1 -1
  89. package/examples/chat-corpus.ts +85 -0
  90. package/package.json +2 -2
package/dist/client.js CHANGED
@@ -1,4 +1,4 @@
1
- import { AexError, DEFAULT_CREDENTIAL_MODE, DEFAULT_RUN_PROVIDER, HttpClient, 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_RUN_PROVIDER, HttpClient, RunConfigValidationError, RunStateError, SecretString, isRunSettled, operations, 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";
@@ -55,18 +55,6 @@ export class SkillsClient {
55
55
  findByName(name) {
56
56
  return operations.findSkillByName(this.#http, name);
57
57
  }
58
- /**
59
- * Internal: post a pre-bundled skill zip to the BFF. Only
60
- * `Skill.upload` calls this. NOT part of the public API.
61
- */
62
- async _uploadSkillBundle(args) {
63
- return operations.createSkillBundle(this.#http, {
64
- name: args.name,
65
- body: args.body,
66
- contentType: "application/zip",
67
- filename: `${args.name}.zip`
68
- });
69
- }
70
58
  }
71
59
  /**
72
60
  * Workspace AgentsMd admin operations exposed under `client.agentsMd`.
@@ -230,21 +218,6 @@ export class AgentExecutor {
230
218
  this.files = new FilesClient(this.#http);
231
219
  this.secrets = new SecretsClient(this.#http);
232
220
  }
233
- /**
234
- * Internal: forwards to `SkillsClient._uploadSkillBundle`. NOT part of
235
- * the public API.
236
- *
237
- * NOTE (tech-debt): this is part of the legacy workspace-skill upload
238
- * surface (`SkillsClient` + `operations.createSkillBundle` + the TUS
239
- * chunked path in asset-upload.ts). The live submit path materializes
240
- * inline skills via `uploadAsset` instead; `Skill.upload(client)`
241
- * pre-stages a draft explicitly for reuse. This surface is retained
242
- * pending a deliberate deprecation pass (it still threads into the CLI
243
- * host commands), tracked in the remediation plan as item 4a.
244
- */
245
- async _uploadSkillBundle(args) {
246
- return this.skills._uploadSkillBundle(args);
247
- }
248
221
  /**
249
222
  * Internal: an `AgentsMd.upload(this)` shortcut that bypasses
250
223
  * `client.agentsMd` indirection. Forwarded to
@@ -286,17 +259,86 @@ export class AgentExecutor {
286
259
  });
287
260
  }
288
261
  /**
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)`.
262
+ * Submit a run, wait until its RECORD is terminal, and collect the full
263
+ * {@link RunResult} — the settle-consistent "do it and give me the result"
264
+ * primitive. Folds the poll loop every consumer hand-rolled into one call:
265
+ * submit {@link waitForRun} (polls `getRun`, NOT the earlier RUN_FINISHED
266
+ * event) poll `listEvents` until the snapshot is settle-bracketed
267
+ * (RUN_STARTED + a terminal event present) → `listOutputs` decode the trace
268
+ * and assistant text. On resolve, `getRun`/`listOutputs` are guaranteed
269
+ * consistent.
270
+ *
271
+ * Uses polling (portable across backends), NOT the coordinator WebSocket. By
272
+ * default a failed run resolves with `ok: false` and a populated `error`; pass
273
+ * `{ throwOnFailure: true }` to throw instead. For live events prefer `submit`
274
+ * + `streamEnvelopes(runId, { settleConsistent: true })`.
296
275
  */
297
- async run(options) {
276
+ async run(options, opts = {}) {
277
+ const signal = opts.signal ?? options.signal;
298
278
  const runId = await this.submit(options);
299
- return this.waitForRun(runId, options.signal ? { signal: options.signal } : {});
279
+ const run = await this.waitForRun(runId, {
280
+ ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
281
+ ...(signal ? { signal } : {})
282
+ });
283
+ const events = await this.#collectSettledEvents(runId, signal);
284
+ const outputs = await this.listOutputs(runId);
285
+ const ok = run.status === "succeeded";
286
+ const costUsd = run.costTelemetry?.billedCostUsd;
287
+ const errorMessage = typeof run.errorMessage === "string" && run.errorMessage ? run.errorMessage : undefined;
288
+ const result = {
289
+ runId,
290
+ run,
291
+ status: run.status,
292
+ ok,
293
+ text: textOf(events),
294
+ events,
295
+ trace: summarizeRunTrace(events),
296
+ outputs,
297
+ ...(run.usage ? { usage: run.usage } : {}),
298
+ ...(typeof costUsd === "number" ? { costUsd } : {}),
299
+ ...(!ok && errorMessage ? { error: errorMessage } : {})
300
+ };
301
+ if (opts.throwOnFailure && !ok) {
302
+ throw new RunStateError(`AgentExecutor.run: run ${runId} ended ${run.status}${errorMessage ? `: ${errorMessage}` : ""}`, { runId, status: run.status });
303
+ }
304
+ return result;
305
+ }
306
+ /**
307
+ * Explicit, discoverable alias for {@link run}: submit, wait, and collect the
308
+ * full {@link RunResult} in one call.
309
+ */
310
+ runAndCollect(options, opts) {
311
+ return this.run(options, opts);
312
+ }
313
+ /**
314
+ * Poll `listEvents` until the snapshot is settle-bracketed — both a
315
+ * RUN_STARTED and a terminal (RUN_FINISHED / RUN_ERROR) event present — then
316
+ * return it. The runner emits the terminal AG-UI event BEFORE the platform
317
+ * commits the record, and the `listEvents` snapshot can lag the terminal
318
+ * record by a beat; this closes that race so the decoded trace/text/outputs
319
+ * are complete. Bounded so an older runtime that never emits one of the
320
+ * brackets still returns the best snapshot available.
321
+ */
322
+ async #collectSettledEvents(runId, signal) {
323
+ const intervalMs = 500;
324
+ const maxAttempts = 20;
325
+ let latest = [];
326
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
327
+ if (signal?.aborted)
328
+ return latest;
329
+ latest = await this.listEvents(runId);
330
+ const hasStart = latest.some((event) => event.type === "RUN_STARTED");
331
+ const hasTerminal = latest.some((event) => event.type === "RUN_FINISHED" || event.type === "RUN_ERROR");
332
+ if (hasStart && hasTerminal)
333
+ return latest;
334
+ try {
335
+ await sleep(intervalMs, signal);
336
+ }
337
+ catch {
338
+ return latest;
339
+ }
340
+ }
341
+ return latest;
300
342
  }
301
343
  /**
302
344
  * Submit a run and return its run id immediately. Use that id with
@@ -316,8 +358,9 @@ export class AgentExecutor {
316
358
  */
317
359
  async submit(options) {
318
360
  if (!options || typeof options !== "object") {
319
- throw new Error("AgentExecutor.submit: options is required");
361
+ throw new RunConfigValidationError("AgentExecutor.submit: options is required");
320
362
  }
363
+ assertNoRemovedSubmitFields(options);
321
364
  // A model maps to one or more upstream providers (see MODEL_PROVIDER_IDS).
322
365
  // `providersForModel` returns the supported providers in priority order, or
323
366
  // `[]` for an unknown model string (the model check below then rejects it).
@@ -327,43 +370,20 @@ export class AgentExecutor {
327
370
  if (options.provider &&
328
371
  supportedProviders.length > 0 &&
329
372
  !supportedProviders.includes(options.provider)) {
330
- throw new Error(`AgentExecutor.submit: provider ${JSON.stringify(options.provider)} is not available for ` +
373
+ throw new RunConfigValidationError(`AgentExecutor.submit: provider ${JSON.stringify(options.provider)} is not available for ` +
331
374
  `model ${JSON.stringify(options.model)} (supported: ${supportedProviders.join(", ")})`);
332
375
  }
333
376
  const provider = options.provider ?? supportedProviders[0] ?? DEFAULT_RUN_PROVIDER;
334
- 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
- }
377
+ validateSubmitCredentials(options, provider);
345
378
  if (typeof options.model !== "string" || !options.model) {
346
- throw new Error("AgentExecutor.submit: model is required");
379
+ throw new RunConfigValidationError("AgentExecutor.submit: model is required");
347
380
  }
348
381
  const prompt = normalisePrompt(options.prompt);
349
382
  const { endpoints: proxyEndpointDeclarations, auth: proxyEndpointAuthFromInstances } = splitProxyEndpoints(options.proxyEndpoints ?? []);
350
- const mergedProxyAuth = mergeProxyEndpointAuth(proxyEndpointAuthFromInstances, options.secrets.proxyEndpointAuth ?? []);
383
+ const mergedProxyAuth = mergeProxyEndpointAuth(proxyEndpointAuthFromInstances, options.secrets?.proxyEndpointAuth ?? []);
351
384
  // Split secretEnv into value-free declarations (hashed submission) and
352
385
  // ephemeral values (vaulted secrets channel), mirroring the proxy split.
353
386
  const { declarations: secretEnvDeclarations, values: envSecretValues } = splitSecretEnv(options.secretEnv);
354
- // Validate the runtime selector before any network I/O — inline drafts
355
- // are uploaded below, so an invalid runtime must reject first rather than
356
- // leak an asset upload.
357
- if (options.runtime !== undefined &&
358
- !RUNTIME_KINDS.includes(options.runtime)) {
359
- throw new AexError("RUNTIME_UNSUPPORTED", `AgentExecutor.submit: runtime must be one of: ${RUNTIME_KINDS.join(", ")} ` +
360
- `(got ${JSON.stringify(options.runtime)})`);
361
- }
362
- if (options.region !== undefined &&
363
- !REGIONS.includes(options.region)) {
364
- throw new AexError("RUN_CONFIG_INVALID", `AgentExecutor.submit: region must be one of: ${REGIONS.join(", ")} ` +
365
- `(got ${JSON.stringify(options.region)})`);
366
- }
367
387
  // Validate the per-run limits override with the SAME parser the server runs
368
388
  // (shape + positivity + allow-list), failing fast before any asset upload.
369
389
  // Normalizes an all-absent override (e.g. `{}`) away.
@@ -383,7 +403,7 @@ export class AgentExecutor {
383
403
  const preparedTools = await prepareTools(options.tools ?? [], uploader);
384
404
  const preparedAgentsMd = await prepareAgentsMd(options.agentsMd ?? [], uploader);
385
405
  const preparedFiles = await prepareFiles(options.files ?? [], uploader);
386
- const { submissionMcpServers, mergedMcpSecrets } = mergeMcpServers(options.mcpServers ?? [], options.secrets.mcpServers ?? []);
406
+ const { submissionMcpServers, mergedMcpSecrets } = mergeMcpServers(options.mcpServers ?? [], options.secrets?.mcpServers ?? []);
387
407
  const outputCapture = outputsForWire(options.outputs);
388
408
  const submission = {
389
409
  model: options.model,
@@ -436,12 +456,6 @@ export class AgentExecutor {
436
456
  // shared parser still defaults to `anthropic` when callers omit
437
457
  // the field entirely, but the SDK has resolved it by here.
438
458
  provider,
439
- ...(credentialMode !== DEFAULT_CREDENTIAL_MODE ? { credentialMode } : {}),
440
- // `runtime` is optional on the wire — absent means let the
441
- // dispatcher auto-route. Only emit it when the caller asked for
442
- // a specific runtime so the wire shape stays minimal.
443
- ...(options.runtime ? { runtime: options.runtime } : {}),
444
- ...(options.region ? { region: options.region } : {}),
445
459
  submission,
446
460
  ...(options.runtimeSize ? { runtimeSize: options.runtimeSize } : {}),
447
461
  ...(options.timeout ? { timeout: options.timeout } : {}),
@@ -483,6 +497,60 @@ export class AgentExecutor {
483
497
  listRuns(query) {
484
498
  return operations.listRuns(this.#http, query);
485
499
  }
500
+ /**
501
+ * Find output files across runs by filename / extension / content type.
502
+ * Returns lean REFERENCE hits (runId, outputId, filename, size, content type)
503
+ * — never bytes; fetch content with {@link readOutputText}. Scope the search
504
+ * to a corpus with `query.runIds`; omit it to scan the whole workspace (needs
505
+ * the owner-gated `listRuns`). Composed client-side for the MVP (per-run
506
+ * `listOutputs` + the contracts output filter), so it works against any
507
+ * already-terminal run with no new server endpoint. Bounded by
508
+ * `query.limit` (default 100) — for very large corpora prefer the deferred
509
+ * server-side index.
510
+ */
511
+ async searchOutputs(query = {}) {
512
+ const runIds = query.runIds ?? (await this.#allWorkspaceRunIds());
513
+ const limit = query.limit ?? 100;
514
+ // Translate the search query to an OutputQuery so the contracts output
515
+ // filter (classifyOutput / basename match / contentType wildcard) does the
516
+ // matching — no re-derived filter logic here.
517
+ const outputQuery = {
518
+ ...(query.filename ? { filename: new RegExp(escapeRegExp(query.filename), "i") } : {}),
519
+ ...(query.extension ? { extension: query.extension } : {}),
520
+ ...(query.contentType ? { contentType: query.contentType } : {})
521
+ };
522
+ const hasFilter = Object.keys(outputQuery).length > 0;
523
+ const hits = [];
524
+ for (const runId of runIds) {
525
+ const outputs = hasFilter
526
+ ? await this.listOutputs(runId, outputQuery)
527
+ : await this.listOutputs(runId);
528
+ for (const o of outputs) {
529
+ hits.push({
530
+ runId,
531
+ outputId: o.id,
532
+ ...(o.filename !== undefined ? { filename: o.filename } : {}),
533
+ ...(o.sizeBytes !== undefined ? { sizeBytes: o.sizeBytes } : {}),
534
+ ...(o.contentType !== undefined ? { contentType: o.contentType } : {})
535
+ });
536
+ if (hits.length >= limit)
537
+ return { hits };
538
+ }
539
+ }
540
+ return { hits };
541
+ }
542
+ /** Enumerate every run id in the workspace by paging `listRuns`. */
543
+ async #allWorkspaceRunIds() {
544
+ const ids = [];
545
+ let cursor;
546
+ do {
547
+ const page = await this.listRuns(cursor ? { cursor } : {});
548
+ for (const run of page.runs)
549
+ ids.push(run.id);
550
+ cursor = page.nextCursor;
551
+ } while (cursor);
552
+ return ids;
553
+ }
486
554
  /**
487
555
  * Fetch the self-contained `RunUnit`: parsed submission inputs,
488
556
  * attempts, indexed events (inline + cursor for the tail), raw
@@ -526,7 +594,7 @@ export class AgentExecutor {
526
594
  }
527
595
  /**
528
596
  * Stream the unified {@link AexEvent} envelope live over the coordinator
529
- * WebSocket. The Worker's ticket broker authorizes the connection (workspace
597
+ * WebSocket. The hosted API's ticket broker authorizes the connection (workspace
530
598
  * token → short-lived coordinator ticket); the shared client replays from
531
599
  * the cursor, tails live, and resumes exactly-once across reconnects. The
532
600
  * ticket is re-minted on each (re)connect so a long run never outlives it.
@@ -714,6 +782,10 @@ const TERMINAL_STATUSES = new Set(TERMINAL_RUN_STATUSES);
714
782
  function isTerminal(status) {
715
783
  return typeof status === "string" && TERMINAL_STATUSES.has(status);
716
784
  }
785
+ /** Escape a literal string for safe interpolation into a RegExp. */
786
+ function escapeRegExp(input) {
787
+ return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
788
+ }
717
789
  function isOutputPathSelector(selector) {
718
790
  return Boolean(selector && typeof selector === "object" && "path" in selector);
719
791
  }
@@ -793,20 +865,41 @@ function generateIdempotencyKey() {
793
865
  function normalisePrompt(input) {
794
866
  if (typeof input === "string") {
795
867
  if (!input) {
796
- throw new Error("AgentExecutor.submit: prompt must be a non-empty string");
868
+ throw new RunConfigValidationError("AgentExecutor.submit: prompt must be a non-empty string");
797
869
  }
798
870
  return [input];
799
871
  }
800
872
  if (!Array.isArray(input) || input.length === 0) {
801
- throw new Error("AgentExecutor.submit: prompt must be a non-empty string or string array");
873
+ throw new RunConfigValidationError("AgentExecutor.submit: prompt must be a non-empty string or string array");
802
874
  }
803
875
  for (const segment of input) {
804
876
  if (typeof segment !== "string" || !segment) {
805
- throw new Error("AgentExecutor.submit: prompt segments must be non-empty strings");
877
+ throw new RunConfigValidationError("AgentExecutor.submit: prompt segments must be non-empty strings");
806
878
  }
807
879
  }
808
880
  return [...input];
809
881
  }
882
+ function assertNoRemovedSubmitFields(options) {
883
+ const record = options;
884
+ for (const field of ["credentialMode", "runtime", "region", "apiKey", "credentials"]) {
885
+ if (Object.prototype.hasOwnProperty.call(record, field)) {
886
+ throw new RunConfigValidationError(`AgentExecutor.submit: ${field} is not a supported option; use the managed path with secrets.apiKeys[provider].`);
887
+ }
888
+ }
889
+ const secrets = record.secrets;
890
+ if (secrets && typeof secrets === "object" && !Array.isArray(secrets) && Object.prototype.hasOwnProperty.call(secrets, "apiKey")) {
891
+ throw new RunConfigValidationError("AgentExecutor.submit: secrets.apiKey is not supported; use secrets.apiKeys[provider].");
892
+ }
893
+ }
894
+ function validateSubmitCredentials(options, provider) {
895
+ if (options.parentRunId) {
896
+ return;
897
+ }
898
+ const key = options.secrets?.apiKeys?.[provider];
899
+ if (typeof key !== "string" || key.length === 0) {
900
+ throw new RunConfigValidationError(`AgentExecutor.submit: a provider API key is required — pass secrets.apiKeys[${JSON.stringify(provider)}].`);
901
+ }
902
+ }
810
903
  function postHookForWire(input) {
811
904
  if (input === undefined || typeof input.command !== "string" || input.command.trim().length === 0) {
812
905
  return undefined;
@@ -835,31 +928,41 @@ function outputsForWire(outputs) {
835
928
  ...(outputs.maxFiles !== undefined ? { maxFiles: outputs.maxFiles } : {})
836
929
  };
837
930
  }
931
+ /**
932
+ * Resolve a draft's asset id: reuse the cached id from a prior submit, otherwise
933
+ * upload the bytes and cache the result on the instance.
934
+ */
935
+ async function resolveAssetId(entry, bundle, uploader) {
936
+ const cached = entry._cachedAssetId;
937
+ if (cached !== undefined) {
938
+ return cached;
939
+ }
940
+ const uploaded = await uploader({
941
+ bytes: bundle.bytes,
942
+ hash: bundle.contentHash,
943
+ contentType: "application/zip"
944
+ });
945
+ entry._rememberAsset(uploaded.assetId);
946
+ return uploaded.assetId;
947
+ }
838
948
  /** Walk Skill[], eagerly upload drafts as assets, and return plain asset refs. */
839
949
  async function prepareSkills(skills, uploader) {
840
950
  const refs = [];
841
951
  for (let i = 0; i < skills.length; i++) {
842
952
  const entry = skills[i];
843
953
  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`);
954
+ throw new RunConfigValidationError(`AgentExecutor.submit: skills[${i}] must be a Skill instance`);
848
955
  }
849
956
  const ref = entry.ref;
850
957
  if (ref.kind === "draft") {
851
958
  const bundle = entry._takeDraftBundle();
852
959
  if (!bundle) {
853
- throw new Error(`AgentExecutor.submit: skills[${i}] is draft but has no bytes`);
960
+ throw new RunConfigValidationError(`AgentExecutor.submit: skills[${i}] is draft but has no bytes`);
854
961
  }
855
- const uploaded = await uploader({
856
- bytes: bundle.bytes,
857
- hash: bundle.contentHash,
858
- contentType: "application/zip"
859
- });
962
+ const assetId = await resolveAssetId(entry, bundle, uploader);
860
963
  refs.push({
861
964
  kind: "asset",
862
- assetId: uploaded.assetId,
965
+ assetId,
863
966
  name: bundle.name
864
967
  });
865
968
  continue;
@@ -885,7 +988,7 @@ async function prepareTools(tools, uploader) {
885
988
  // A bare string is a builtin tool reference.
886
989
  if (typeof entry === "string") {
887
990
  if (!BUILTIN_TOOL_NAMES.includes(entry)) {
888
- throw new Error(`AgentExecutor.submit: tools[${i}] (${JSON.stringify(entry)}) is not a builtin tool name; ` +
991
+ throw new RunConfigValidationError(`AgentExecutor.submit: tools[${i}] (${JSON.stringify(entry)}) is not a builtin tool name; ` +
889
992
  `expected a Tool instance or one of: ${BUILTIN_TOOL_NAMES.join(", ")}`);
890
993
  }
891
994
  if (!seenBuiltins.has(entry)) {
@@ -895,23 +998,16 @@ async function prepareTools(tools, uploader) {
895
998
  continue;
896
999
  }
897
1000
  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`);
1001
+ throw new RunConfigValidationError(`AgentExecutor.submit: tools[${i}] must be a Tool instance or a builtin tool name`);
902
1002
  }
903
1003
  const ref = entry.ref;
904
1004
  if (ref.kind === "draft") {
905
1005
  const bundle = entry._takeDraftBundle();
906
1006
  if (!bundle) {
907
- throw new Error(`AgentExecutor.submit: tools[${i}] is draft but has no bytes`);
1007
+ throw new RunConfigValidationError(`AgentExecutor.submit: tools[${i}] is draft but has no bytes`);
908
1008
  }
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 });
1009
+ const assetId = await resolveAssetId(entry, bundle, uploader);
1010
+ refs.push({ ...bundle.ref, assetId });
915
1011
  continue;
916
1012
  }
917
1013
  refs.push(ref);
@@ -924,25 +1020,18 @@ async function prepareAgentsMd(agentsMds, uploader) {
924
1020
  for (let i = 0; i < agentsMds.length; i++) {
925
1021
  const entry = agentsMds[i];
926
1022
  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`);
1023
+ throw new RunConfigValidationError(`AgentExecutor.submit: agentsMd[${i}] must be an AgentsMd instance`);
931
1024
  }
932
1025
  const ref = entry.ref;
933
1026
  if (ref.kind === "draft") {
934
1027
  const bundle = entry._takeDraftBundle();
935
1028
  if (!bundle) {
936
- throw new Error(`AgentExecutor.submit: agentsMd[${i}] is draft but has no bytes`);
1029
+ throw new RunConfigValidationError(`AgentExecutor.submit: agentsMd[${i}] is draft but has no bytes`);
937
1030
  }
938
- const uploaded = await uploader({
939
- bytes: bundle.bytes,
940
- hash: bundle.contentHash,
941
- contentType: "application/zip"
942
- });
1031
+ const assetId = await resolveAssetId(entry, bundle, uploader);
943
1032
  refs.push({
944
1033
  kind: "asset",
945
- assetId: uploaded.assetId,
1034
+ assetId,
946
1035
  name: bundle.name
947
1036
  });
948
1037
  continue;
@@ -957,25 +1046,18 @@ async function prepareFiles(files, uploader) {
957
1046
  for (let i = 0; i < files.length; i++) {
958
1047
  const entry = files[i];
959
1048
  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`);
1049
+ throw new RunConfigValidationError(`AgentExecutor.submit: files[${i}] must be a File instance`);
964
1050
  }
965
1051
  const ref = entry.ref;
966
1052
  if (ref.kind === "draft") {
967
1053
  const bundle = entry._takeDraftBundle();
968
1054
  if (!bundle) {
969
- throw new Error(`AgentExecutor.submit: files[${i}] is draft but has no bytes`);
1055
+ throw new RunConfigValidationError(`AgentExecutor.submit: files[${i}] is draft but has no bytes`);
970
1056
  }
971
- const uploaded = await uploader({
972
- bytes: bundle.bytes,
973
- hash: bundle.contentHash,
974
- contentType: "application/zip"
975
- });
1057
+ const assetId = await resolveAssetId(entry, bundle, uploader);
976
1058
  refs.push({
977
1059
  kind: "asset",
978
- assetId: uploaded.assetId,
1060
+ assetId,
979
1061
  name: bundle.name,
980
1062
  mountPath: bundle.mountPath
981
1063
  });
@@ -988,7 +1070,7 @@ async function prepareFiles(files, uploader) {
988
1070
  function getSubmittedRunId(response) {
989
1071
  const id = response.id ?? response.runId;
990
1072
  if (typeof id !== "string" || id.length === 0) {
991
- throw new Error("AgentExecutor.submit: submit response did not include a run id");
1073
+ throw new RunStateError("AgentExecutor.submit: submit response did not include a run id");
992
1074
  }
993
1075
  return id;
994
1076
  }
@@ -1001,14 +1083,14 @@ function mergeMcpServers(inputs, explicitSecrets) {
1001
1083
  for (let i = 0; i < inputs.length; i++) {
1002
1084
  const entry = inputs[i];
1003
1085
  if (!(entry instanceof McpServer)) {
1004
- throw new Error(`AgentExecutor.submit: mcpServers[${i}] must be an McpServer instance`);
1086
+ throw new RunConfigValidationError(`AgentExecutor.submit: mcpServers[${i}] must be an McpServer instance`);
1005
1087
  }
1006
1088
  submissionMcpServers.push(entry.toSubmissionEntry());
1007
1089
  const secret = entry.toSecretEntry();
1008
1090
  if (secret) {
1009
1091
  const existing = secretByName.get(secret.name);
1010
1092
  if (existing && existing.url !== secret.url) {
1011
- throw new Error(`AgentExecutor.submit: mcpServers[${i}].url conflicts with secrets.mcpServers["${secret.name}"]`);
1093
+ throw new RunConfigValidationError(`AgentExecutor.submit: mcpServers[${i}].url conflicts with secrets.mcpServers["${secret.name}"]`);
1012
1094
  }
1013
1095
  secretByName.set(secret.name, secret);
1014
1096
  }
@@ -1036,7 +1118,7 @@ function mergeProxyEndpointAuth(fromInstances, fromExplicitSecrets) {
1036
1118
  for (const entry of fromInstances) {
1037
1119
  const existing = byName.get(entry.name);
1038
1120
  if (existing && existing.value.type !== entry.value.type) {
1039
- throw new Error(`AgentExecutor.submit: proxyEndpoint "${entry.name}" auth type conflicts ` +
1121
+ throw new RunConfigValidationError(`AgentExecutor.submit: proxyEndpoint "${entry.name}" auth type conflicts ` +
1040
1122
  `with secrets.proxyEndpointAuth (instance=${entry.value.type}, secrets=${existing.value.type})`);
1041
1123
  }
1042
1124
  byName.set(entry.name, entry);