@aexhq/sdk 0.33.1 → 0.35.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 (81) hide show
  1. package/README.md +19 -27
  2. package/dist/_contracts/operations.d.ts +2 -54
  3. package/dist/_contracts/operations.js +2 -87
  4. package/dist/_contracts/run-config.d.ts +19 -13
  5. package/dist/_contracts/run-config.js +6 -33
  6. package/dist/_contracts/run-unit.d.ts +1 -33
  7. package/dist/_contracts/run-unit.js +2 -21
  8. package/dist/_contracts/runtime-sizes.d.ts +2 -2
  9. package/dist/_contracts/runtime-sizes.js +2 -2
  10. package/dist/_contracts/status.d.ts +2 -2
  11. package/dist/_contracts/status.js +3 -0
  12. package/dist/_contracts/submission.d.ts +80 -41
  13. package/dist/_contracts/submission.js +114 -52
  14. package/dist/agents-md.d.ts +5 -5
  15. package/dist/agents-md.js +7 -7
  16. package/dist/agents-md.js.map +1 -1
  17. package/dist/asset-upload.d.ts +4 -4
  18. package/dist/asset-upload.js +4 -4
  19. package/dist/bundle.d.ts +2 -2
  20. package/dist/bundle.js +2 -2
  21. package/dist/cli.mjs +369 -12918
  22. package/dist/cli.mjs.sha256 +1 -1
  23. package/dist/client.d.ts +234 -383
  24. package/dist/client.js +436 -648
  25. package/dist/client.js.map +1 -1
  26. package/dist/data-tools.d.ts +25 -22
  27. package/dist/data-tools.js +75 -62
  28. package/dist/data-tools.js.map +1 -1
  29. package/dist/fetch-archive.js +16 -16
  30. package/dist/fetch-archive.js.map +1 -1
  31. package/dist/file.d.ts +5 -5
  32. package/dist/file.js +7 -7
  33. package/dist/file.js.map +1 -1
  34. package/dist/index.d.ts +11 -9
  35. package/dist/index.js +20 -13
  36. package/dist/index.js.map +1 -1
  37. package/dist/mcp-server.d.ts +4 -4
  38. package/dist/mcp-server.js +4 -4
  39. package/dist/proxy-endpoint.d.ts +4 -4
  40. package/dist/proxy-endpoint.js +1 -1
  41. package/dist/retry.d.ts +162 -0
  42. package/dist/retry.js +320 -0
  43. package/dist/retry.js.map +1 -0
  44. package/dist/secret.d.ts +8 -8
  45. package/dist/secret.js +8 -8
  46. package/dist/secret.js.map +1 -1
  47. package/dist/skill-tool.d.ts +102 -0
  48. package/dist/skill-tool.js +190 -0
  49. package/dist/skill-tool.js.map +1 -0
  50. package/dist/tool.d.ts +1 -1
  51. package/dist/tool.js +3 -3
  52. package/dist/tool.js.map +1 -1
  53. package/dist/version.d.ts +1 -1
  54. package/dist/version.js +1 -1
  55. package/docs/cleanup.md +3 -3
  56. package/docs/concepts/agent-tools.md +6 -25
  57. package/docs/concepts/composition.md +15 -12
  58. package/docs/concepts/providers-and-runtimes.md +3 -3
  59. package/docs/concepts/runs.md +27 -22
  60. package/docs/credentials.md +52 -84
  61. package/docs/defaults.md +6 -6
  62. package/docs/events.md +65 -44
  63. package/docs/limits-and-quotas.md +3 -4
  64. package/docs/mcp.md +3 -3
  65. package/docs/networking.md +8 -8
  66. package/docs/outputs.md +44 -40
  67. package/docs/provider-runtime-capabilities.md +1 -1
  68. package/docs/public-surface.json +2 -2
  69. package/docs/quickstart.md +20 -10
  70. package/docs/retries.md +129 -0
  71. package/docs/run-config.md +12 -14
  72. package/docs/run-record.md +8 -8
  73. package/docs/secrets.md +16 -26
  74. package/docs/skills.md +55 -110
  75. package/docs/vision-skills.md +29 -40
  76. package/examples/chat-corpus.ts +8 -9
  77. package/examples/feature-tour.ts +301 -0
  78. package/package.json +1 -1
  79. package/dist/skill.d.ts +0 -149
  80. package/dist/skill.js +0 -198
  81. package/dist/skill.js.map +0 -1
package/dist/client.js CHANGED
@@ -1,11 +1,12 @@
1
- import { AexError, DEFAULT_RUN_PROVIDER, HttpClient, RunConfigValidationError, RunStateError, SecretString, customName, isRunSettled, operations, providersForModel, streamCoordinatorEvents, summarizeRunTrace, textOf, parseRunLimits, BUILTIN_TOOL_NAMES, TERMINAL_RUN_STATUSES } from "./_contracts/index.js";
1
+ import { AexError, DEFAULT_RUN_PROVIDER, HttpClient, RunConfigValidationError, RunStateError, SecretString, customName, isRunSettled, operations, providersForModel, streamCoordinatorEvents, decodeAssistantText, 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";
5
5
  import { McpServer } from "./mcp-server.js";
6
6
  import { splitProxyEndpoints } from "./proxy-endpoint.js";
7
+ import { AexRateLimitError, isThrottleFault, parseProviderFault, withRetry } from "./retry.js";
7
8
  import { splitSecretEnv } from "./secret.js";
8
- import { Skill } from "./skill.js";
9
+ import { SkillTool } from "./skill-tool.js";
9
10
  import { Tool } from "./tool.js";
10
11
  export class SessionTurnStream {
11
12
  #run;
@@ -28,7 +29,6 @@ export class SessionTurnStream {
28
29
  return this.#done;
29
30
  }
30
31
  }
31
- export const ChatTurnStream = SessionTurnStream;
32
32
  const internalSessionSenders = new WeakMap();
33
33
  function sendSessionInternal(session, input, options = {}) {
34
34
  const sender = internalSessionSenders.get(session);
@@ -39,10 +39,14 @@ function sendSessionInternal(session, input, options = {}) {
39
39
  }
40
40
  export class SessionHandle {
41
41
  #http;
42
+ #fetch;
42
43
  #session;
43
- constructor(http, session) {
44
+ /** The last message sent on this handle, for {@link SessionHandle.replayLast}. */
45
+ #lastSend;
46
+ constructor(http, session, fetch) {
44
47
  this.#http = http;
45
48
  this.#session = session;
49
+ this.#fetch = fetch;
46
50
  internalSessionSenders.set(this, (input, options = {}) => new SessionTurnStream(() => this.#send(input, options)));
47
51
  }
48
52
  get id() {
@@ -55,8 +59,28 @@ export class SessionHandle {
55
59
  assertNoSessionSendSignal(options, "SessionHandle.send");
56
60
  return sendSessionInternal(this, input, options);
57
61
  }
62
+ /**
63
+ * Re-send the last message on this session — the clean way to retry a turn a
64
+ * throttle or transient failure interrupted. By default it REUSES the previous
65
+ * message's idempotency key, so if the original turn actually landed
66
+ * server-side the replay de-duplicates instead of creating a second billable
67
+ * turn; pass a fresh `idempotencyKey` to force a brand-new turn.
68
+ */
69
+ replayLast(options = {}) {
70
+ assertNoSessionSendSignal(options, "SessionHandle.replayLast");
71
+ const last = this.#lastSend;
72
+ if (last === undefined) {
73
+ throw new RunStateError("SessionHandle.replayLast: no message has been sent on this session yet");
74
+ }
75
+ return sendSessionInternal(this, last.input, {
76
+ ...options,
77
+ idempotencyKey: options.idempotencyKey ?? last.idempotencyKey
78
+ });
79
+ }
58
80
  async *#send(input, options) {
59
- const accepted = await operations.sendSessionMessage(this.#http, this.id, { input }, { idempotencyKey: options.idempotencyKey ?? generateIdempotencyKey() });
81
+ const idempotencyKey = options.idempotencyKey ?? generateIdempotencyKey();
82
+ this.#lastSend = { input, idempotencyKey };
83
+ const accepted = await operations.sendSessionMessage(this.#http, this.id, { input }, { idempotencyKey });
60
84
  this.#session = accepted.session;
61
85
  const turn = accepted.turn;
62
86
  const events = [];
@@ -102,28 +126,128 @@ export class SessionHandle {
102
126
  this.#session = accepted.session;
103
127
  }
104
128
  }
105
- listEvents() {
106
- return operations.listSessionEvents(this.#http, this.id);
129
+ /**
130
+ * Accessor for the session's decoded assistant messages (buffered output
131
+ * mode: one entry per assistant message). `list()` returns them oldest-first;
132
+ * `last()`/`first()` return a single entry or `undefined` when empty.
133
+ */
134
+ messages() {
135
+ const http = this.#http;
136
+ const id = this.id;
137
+ const list = async () => decodeAssistantText((await operations.listSessionEvents(http, id)));
138
+ return {
139
+ list,
140
+ last: async () => (await list()).at(-1),
141
+ first: async () => (await list())[0]
142
+ };
107
143
  }
108
- listOutputs(query) {
109
- return operations.listSessionOutputs(this.#http, this.id, query);
144
+ /**
145
+ * Accessor for the session's event stream: the buffered `SessionEvent`
146
+ * snapshots (`list`/`last`/`first`), the polling `RunEvent` iterator
147
+ * (`stream`), the live coordinator envelope iterator (`streamEnvelopes`), and
148
+ * the events-namespace archive (`archiveLink`/`download`).
149
+ */
150
+ events() {
151
+ const http = this.#http;
152
+ const id = this.id;
153
+ const list = () => operations.listSessionEvents(http, id);
154
+ return {
155
+ list,
156
+ last: async () => (await list()).at(-1),
157
+ first: async () => (await list())[0],
158
+ stream: (options) => streamSessionEventsPolling(http, id, options ?? {}),
159
+ streamEnvelopes: (options) => streamSessionEnvelopes(http, id, options ?? {}),
160
+ archiveLink: (options) => operations.eventArchiveLink(http, id, options),
161
+ download: async (options) => writeOptionalFile(await operations.downloadEvents(http, id), options?.to)
162
+ };
163
+ }
164
+ /**
165
+ * Accessor for the session's captured output files: `list`/`last`/`first`
166
+ * enumerate them; `read` streams one as capped text; `find`/`findOne`/`link`/
167
+ * `fetch` locate and resolve them; `download` fetches the outputs-namespace
168
+ * zip (no selector) or one file's raw bytes (with selector).
169
+ */
170
+ outputs() {
171
+ return sessionOutputs(this.#http, this.id, this.#fetch);
172
+ }
173
+ /**
174
+ * Accessor for the session's webhook delivery ledger: `list()` returns the
175
+ * delivery attempts; `redeliver(id)` re-sends the frozen payload under the
176
+ * same `webhook-id` so the consumer dedupes.
177
+ */
178
+ webhooks() {
179
+ const http = this.#http;
180
+ const id = this.id;
181
+ return {
182
+ list: () => operations.getRunWebhookDeliveries(http, id),
183
+ redeliver: (deliveryId) => operations.redeliverRunWebhook(http, id, deliveryId)
184
+ };
185
+ }
186
+ /** Re-read the session record from the server and store it as the current record. */
187
+ async refresh() {
188
+ this.#session = await operations.getSession(this.#http, this.id);
189
+ return this.#session;
190
+ }
191
+ /**
192
+ * Poll the session record until it reaches a parked/terminal status (idle,
193
+ * suspended, error, or any terminal run status). Throws if `timeoutMs`
194
+ * elapses first. Updates the stored record.
195
+ */
196
+ async wait(options = {}) {
197
+ const intervalMs = options.intervalMs ?? 1_500;
198
+ const timeoutMs = options.timeoutMs;
199
+ const signal = options.signal;
200
+ const deadline = typeof timeoutMs === "number" ? Date.now() + timeoutMs : Number.POSITIVE_INFINITY;
201
+ while (!signal?.aborted) {
202
+ const session = await operations.getSession(this.#http, this.id);
203
+ this.#session = session;
204
+ if (isSessionParked(session.status))
205
+ return session;
206
+ if (Date.now() >= deadline) {
207
+ throw new Error(`SessionHandle.wait: timeout after ${timeoutMs}ms`);
208
+ }
209
+ await sleep(intervalMs, signal);
210
+ }
211
+ throw new Error("SessionHandle.wait: aborted");
212
+ }
213
+ /**
214
+ * Fetch the self-contained `RunUnit` for this session: parsed submission,
215
+ * attempts, indexed events, outputs, capture failures, proxy-call audit, and
216
+ * resolved skills. Use this when you need fields beyond the session record.
217
+ */
218
+ unit() {
219
+ return operations.getRunUnit(this.#http, this.id);
220
+ }
221
+ /**
222
+ * Download EVERYTHING public about this session as one zip, assembled
223
+ * client-side from the public read endpoints. Organised into `metadata/`,
224
+ * `events/`, and `outputs/` folders, plus a `manifest.json`. Pass `to` to
225
+ * also write the bytes to a file path while still returning them.
226
+ */
227
+ async download(options) {
228
+ return writeOptionalFile(await operations.download(this.#http, this.id), options?.to);
229
+ }
230
+ /** Download only the session record (the `metadata` namespace) as a zip. */
231
+ async downloadMetadata(options) {
232
+ return writeOptionalFile(await operations.downloadMetadata(this.#http, this.id), options?.to);
110
233
  }
111
234
  }
112
- export const ChatSession = SessionHandle;
113
235
  export class SessionClient {
114
236
  #http;
237
+ #fetch;
115
238
  #buildCreateRequest;
116
- constructor(http, buildCreateRequest) {
239
+ constructor(http, buildCreateRequest, fetch) {
117
240
  this.#http = http;
118
241
  this.#buildCreateRequest = buildCreateRequest;
242
+ this.#fetch = fetch;
119
243
  }
120
244
  async create(options) {
121
245
  const request = await this.#buildCreateRequest(options);
122
246
  const session = await operations.createSession(this.#http, request, { idempotencyKey: options.idempotencyKey ?? generateIdempotencyKey() });
123
- return new SessionHandle(this.#http, session);
247
+ return new SessionHandle(this.#http, session, this.#fetch);
124
248
  }
125
249
  async open(sessionId) {
126
- return new SessionHandle(this.#http, await operations.getSession(this.#http, sessionId));
250
+ return new SessionHandle(this.#http, await operations.getSession(this.#http, sessionId), this.#fetch);
127
251
  }
128
252
  get(sessionId) {
129
253
  return operations.getSession(this.#http, sessionId);
@@ -131,14 +255,86 @@ export class SessionClient {
131
255
  list(query) {
132
256
  return operations.listSessions(this.#http, query);
133
257
  }
258
+ /**
259
+ * Accessor over one session's captured output files, addressed by id without
260
+ * opening a handle. Returns the SAME rich {@link SessionOutputs} surface as
261
+ * `session.outputs()` — `aex.sessions.outputs(id).list()` /
262
+ * `.read(selector)` / `.download()` / … — so the workspace client and the
263
+ * live handle share one accessor convention.
264
+ */
265
+ outputs(sessionId) {
266
+ return sessionOutputs(this.#http, sessionId, this.#fetch);
267
+ }
268
+ /**
269
+ * Find output files across sessions by filename / extension / content type.
270
+ * Returns lean REFERENCE hits (never bytes; fetch content with `readOutput`).
271
+ * Scope the search to a corpus with `query.runIds` (a session-id allow-list);
272
+ * omit it to scan every session in the workspace. Composed client-side (per-
273
+ * session `listSessionOutputs` + the contracts output filter), bounded by
274
+ * `query.limit` (default 100).
275
+ */
276
+ async searchOutputs(query = {}) {
277
+ const sessionIds = query.runIds ?? (await this.#allSessionIds());
278
+ const limit = query.limit ?? 100;
279
+ // Translate the search query to an OutputQuery so the contracts output
280
+ // filter does the matching — no re-derived filter logic here.
281
+ const outputQuery = {
282
+ ...(query.filename ? { filename: new RegExp(escapeRegExp(query.filename), "i") } : {}),
283
+ ...(query.extension ? { extension: query.extension } : {}),
284
+ ...(query.contentType ? { contentType: query.contentType } : {})
285
+ };
286
+ const hasFilter = Object.keys(outputQuery).length > 0;
287
+ const hits = [];
288
+ for (const sessionId of sessionIds) {
289
+ const outputs = hasFilter
290
+ ? await operations.listSessionOutputs(this.#http, sessionId, outputQuery)
291
+ : await operations.listSessionOutputs(this.#http, sessionId);
292
+ for (const o of outputs) {
293
+ hits.push({
294
+ runId: sessionId,
295
+ outputId: o.id,
296
+ ...(o.filename !== undefined ? { filename: o.filename } : {}),
297
+ ...(o.sizeBytes !== undefined ? { sizeBytes: o.sizeBytes } : {}),
298
+ ...(o.contentType !== undefined ? { contentType: o.contentType } : {})
299
+ });
300
+ if (hits.length >= limit)
301
+ return { hits };
302
+ }
303
+ }
304
+ return { hits };
305
+ }
306
+ /** Enumerate every session id in the workspace by paging `listSessions`. */
307
+ async #allSessionIds() {
308
+ const ids = [];
309
+ const seenCursors = new Set();
310
+ let cursor;
311
+ do {
312
+ if (cursor !== undefined) {
313
+ if (seenCursors.has(cursor)) {
314
+ throw new Error("Aex.sessions.searchOutputs: listSessions returned a repeated cursor");
315
+ }
316
+ seenCursors.add(cursor);
317
+ }
318
+ const page = await operations.listSessions(this.#http, cursor ? { cursor } : {});
319
+ for (const session of page.sessions)
320
+ ids.push(session.id);
321
+ cursor = page.nextCursor;
322
+ } while (cursor);
323
+ return ids;
324
+ }
134
325
  async run(options) {
135
326
  const { message, deleteAfter, messageIdempotencyKey, stream, ...createOptions } = options;
136
327
  assertNoLegacySessionFields(options, "Aex.sessions.run");
137
328
  const input = normaliseSessionInput(message, "Aex.sessions.run", "message");
138
- const session = await this.create(createOptions);
329
+ // Derive the message key from the create key (like the CLI) so a retried run
330
+ // with the same `idempotencyKey` de-duplicates BOTH the create and the
331
+ // billable turn — never a duplicate billable run.
332
+ const createKey = createOptions.idempotencyKey ?? generateIdempotencyKey();
333
+ const messageKey = messageIdempotencyKey ?? deriveMessageKey(createKey);
334
+ const session = await this.create({ ...createOptions, idempotencyKey: createKey });
139
335
  const result = await session.send(input, {
140
336
  ...(stream ?? {}),
141
- idempotencyKey: messageIdempotencyKey ?? generateIdempotencyKey()
337
+ idempotencyKey: messageKey
142
338
  }).done();
143
339
  if (deleteAfter) {
144
340
  await session.delete();
@@ -146,7 +342,6 @@ export class SessionClient {
146
342
  return result;
147
343
  }
148
344
  }
149
- export const ChatClient = SessionClient;
150
345
  async function* streamSessionTurnEvents(http, sessionId, turn, options) {
151
346
  const first = await operations.getSessionCoordinatorTicket(http, sessionId);
152
347
  yield* streamCoordinatorEvents({
@@ -160,6 +355,98 @@ async function* streamSessionTurnEvents(http, sessionId, turn, options) {
160
355
  ...(options.pingIntervalMs !== undefined ? { pingIntervalMs: options.pingIntervalMs } : {})
161
356
  });
162
357
  }
358
+ /**
359
+ * Poll the session's `RunEvent` snapshots until the session parks, the signal
360
+ * aborts, or the caller breaks the iterator, deduping by event id. Module-level
361
+ * so `SessionHandle.events()` can hand it to its accessor object literal.
362
+ */
363
+ async function* streamSessionEventsPolling(http, id, options) {
364
+ if (options.signal?.aborted)
365
+ return;
366
+ const seenIds = new Set();
367
+ const intervalMs = options.intervalMs ?? 1_000;
368
+ const signal = options.signal;
369
+ while (!signal?.aborted) {
370
+ const events = await operations.listRunEvents(http, id);
371
+ for (const event of events) {
372
+ if (!seenIds.has(event.id)) {
373
+ seenIds.add(event.id);
374
+ yield event;
375
+ }
376
+ }
377
+ const session = await operations.getSession(http, id);
378
+ if (isSessionParked(session.status))
379
+ return;
380
+ // `sleep` rejects on abort — treat that as a graceful stop.
381
+ try {
382
+ await sleep(intervalMs, signal);
383
+ }
384
+ catch {
385
+ return;
386
+ }
387
+ }
388
+ }
389
+ /**
390
+ * Stream the unified {@link AexEvent} envelope live over the session's
391
+ * coordinator WebSocket. The ticket is re-minted on each (re)connect so a long
392
+ * session never outlives it. Module-level so `SessionHandle.events()` can hand
393
+ * it to its accessor object literal.
394
+ */
395
+ async function* streamSessionEnvelopes(http, id, options) {
396
+ const first = await operations.getSessionCoordinatorTicket(http, id);
397
+ yield* streamCoordinatorEvents({
398
+ wsUrl: first.wsUrl,
399
+ from: options.from ?? 0,
400
+ fetchTicket: async () => (await operations.getSessionCoordinatorTicket(http, id)).ticket,
401
+ // settleConsistent ends the stream on the post-mirror barrier instead of
402
+ // the earlier RUN_FINISHED UX signal.
403
+ ...(options.settleConsistent ? { isTerminal: isRunSettled } : {}),
404
+ ...(options.signal ? { signal: options.signal } : {})
405
+ });
406
+ }
407
+ /**
408
+ * Download captured deliverables. No selector → the full outputs namespace as a
409
+ * zip; a selector → one file's raw bytes. Module-level so
410
+ * `SessionHandle.outputs()` can hand it to its accessor object literal.
411
+ */
412
+ async function downloadSessionOutput(http, id, selector, options) {
413
+ let bytes;
414
+ if (selector === undefined) {
415
+ bytes = await operations.downloadOutputs(http, id);
416
+ }
417
+ else {
418
+ const output = isOutputPathSelector(selector)
419
+ ? resolveOutputFileSelector(await operations.listOutputs(http, id), selector, id)
420
+ : resolveOutputFileSelector([], selector, id);
421
+ const { response } = await http.download(`/api/runs/${encodeURIComponent(id)}/outputs/${encodeURIComponent(output.id)}/download`);
422
+ bytes = new Uint8Array(await response.arrayBuffer());
423
+ }
424
+ return writeOptionalFile(bytes, options?.to);
425
+ }
426
+ /**
427
+ * Build the outputs accessor for a session id. Shared by
428
+ * `SessionHandle.outputs()` (bound to the live handle) and
429
+ * `SessionClient.outputs(id)` (addressed by id without opening a handle), so both
430
+ * expose the identical rich {@link SessionOutputs} surface — one accessor
431
+ * convention, one implementation.
432
+ */
433
+ function sessionOutputs(http, id, fetchLike) {
434
+ const list = (query) => operations.listSessionOutputs(http, id, query);
435
+ return {
436
+ list,
437
+ last: async () => (await list()).at(-1),
438
+ first: async () => (await list())[0],
439
+ read: (selector, options) => operations.readOutputText(http, id, selector, options),
440
+ find: (query) => operations.findOutputs(http, id, query),
441
+ findOne: (query) => operations.findOutput(http, id, query),
442
+ link: (selectorOrQuery, options) => operations.outputLink(http, id, selectorOrQuery, options),
443
+ fetch: async (selectorOrQuery, options) => {
444
+ const link = await operations.outputLink(http, id, selectorOrQuery, options);
445
+ return (fetchLike ?? globalThis.fetch)(link.url);
446
+ },
447
+ download: (selector, options) => downloadSessionOutput(http, id, selector, options)
448
+ };
449
+ }
163
450
  function isSessionTurnTerminalEvent(event, turnSeq) {
164
451
  const name = customName(event);
165
452
  if (name !== "aex.session.idle" &&
@@ -200,61 +487,12 @@ function withTerminalSessionStatus(session, terminalStatus) {
200
487
  }
201
488
  return { ...session, status: terminalStatus };
202
489
  }
203
- /**
204
- * Workspace skill admin operations exposed under `client.skills`.
205
- *
206
- * New run submissions usually use `Skill.fromFiles(...)` or
207
- * `Skill.fromPath(...)` directly inside `submit`; the SDK materializes
208
- * those bytes to the hosted asset store before the run lands. This namespace is the read/delete
209
- * surface for workspace skill records and the internal transport used by the
210
- * legacy CLI upload command.
211
- */
212
- export class SkillsClient {
213
- #http;
214
- constructor(http) {
215
- this.#http = http;
216
- }
217
- list() {
218
- return operations.listSkills(this.#http);
219
- }
220
- get(skillId) {
221
- return operations.getSkill(this.#http, skillId);
222
- }
223
- delete(skillId) {
224
- return operations.deleteSkill(this.#http, skillId);
225
- }
226
- /**
227
- * Lookup a live workspace skill by `(name, contentHash)`.
228
- *
229
- * Returns the matching `Skill` record or `null` when no live row
230
- * carries that hash. The `contentHash` is the wire format
231
- * `sha256:<hex>` returned by `hashSkillBundle` (and stored verbatim
232
- * on every skill row). The hash space is unique enough that one
233
- * row at most can match, so this is a single keyed lookup.
234
- *
235
- * Consumers can call this directly when they already have a hash in hand
236
- * and want to know whether the skill is already persisted.
237
- */
238
- findByHash(args) {
239
- return operations.findSkillByHash(this.#http, args);
240
- }
241
- /**
242
- * Lookup a live workspace skill by `name`. Returns the matching
243
- * `Skill` record or `null` when no live row carries that name.
244
- * Implemented as a list-and-filter against the existing `/api/skills`
245
- * endpoint — typical workspace skill counts are small enough that
246
- * the cost is negligible.
247
- */
248
- findByName(name) {
249
- return operations.findSkillByName(this.#http, name);
250
- }
251
- }
252
490
  /**
253
491
  * Workspace AgentsMd admin operations exposed under `client.agentsMd`.
254
492
  *
255
- * New run submissions usually use `AgentsMd.fromContent(...)` or
256
- * `AgentsMd.fromPath(...)` directly inside `submit`; the SDK
257
- * materializes those bytes to the hosted asset store before the run lands. This namespace is
493
+ * New sessions usually use `AgentsMd.fromContent(...)` or
494
+ * `AgentsMd.fromPath(...)` directly on `openSession` / `run`; the SDK
495
+ * materializes those bytes to the hosted asset store before the session starts. This namespace is
258
496
  * the read/delete surface for persisted AgentsMd records.
259
497
  */
260
498
  export class AgentsMdClient {
@@ -275,9 +513,9 @@ export class AgentsMdClient {
275
513
  /**
276
514
  * Workspace File admin operations exposed under `client.files`.
277
515
  *
278
- * New run submissions usually use `File.fromPath(...)` or
279
- * `File.fromBytes(...)` directly inside `submit`; the SDK materializes
280
- * those bytes to the hosted asset store before the run lands. This namespace is the read/delete
516
+ * New sessions usually use `File.fromPath(...)` or
517
+ * `File.fromBytes(...)` directly on `openSession` / `run`; the SDK materializes
518
+ * those bytes to the hosted asset store before the session starts. This namespace is the read/delete
281
519
  * surface for persisted file records.
282
520
  */
283
521
  export class FilesClient {
@@ -297,17 +535,16 @@ export class FilesClient {
297
535
  }
298
536
  /**
299
537
  * Workspace secret management exposed under `client.secrets`, mirroring
300
- * `client.skills` / `client.files`.
538
+ * `client.agentsMd` / `client.files`.
301
539
  *
302
- * Lifecycle parity with assets/skills: a `Secret.value(...)` is per-run and
540
+ * Lifecycle parity with assets: a `Secret.value(...)` is per-run and
303
541
  * gone at terminal; `set` (or promoting an ephemeral via `secret.upload`)
304
542
  * persists a named, searchable workspace secret you can `get` (metadata),
305
- * `get_value` (audited value), `rotate`, `list`, and `delete`. The identity is the
306
- * `name`; the value rotates under that stable name.
543
+ * `rotate`, `list`, and `delete`. The identity is the `name`; the value rotates
544
+ * under that stable name.
307
545
  *
308
- * Values are write-only: `set`/`rotate` send the value in the request BODY (never
309
- * the URL); `get`/`list` return metadata only; `get_value` is the explicit audited
310
- * value read.
546
+ * Values are write-only through the public SDK: `set`/`rotate` send the value in
547
+ * the request BODY (never the URL); `get`/`list` return metadata only.
311
548
  */
312
549
  export class SecretsClient {
313
550
  #http;
@@ -326,10 +563,6 @@ export class SecretsClient {
326
563
  get(name) {
327
564
  return operations.getSecret(this.#http, name);
328
565
  }
329
- /** Audited value read — the preferred path that returns a workspace secret value. */
330
- get_value(name) {
331
- return operations.getSecretValue(this.#http, name);
332
- }
333
566
  /** Replace the value of an existing workspace secret; bumps its version. */
334
567
  rotate(args) {
335
568
  return operations.rotateSecret(this.#http, { name: args.name, value: unwrapSecretValue(args.value) });
@@ -370,20 +603,24 @@ export class AgentExecutor {
370
603
  #http;
371
604
  /** The same fetch the HttpClient uses, threaded into `_uploadAsset`. */
372
605
  #fetch;
373
- skills;
374
606
  agentsMd;
375
607
  files;
376
608
  secrets;
377
609
  sessions;
378
- chat;
379
610
  constructor(options) {
380
611
  if (!options.apiToken) {
381
612
  throw new Error("AgentExecutor: apiToken is required");
382
613
  }
614
+ // Wrap the transport fetch (the caller's override, or global `fetch`) with
615
+ // the bounded-retry layer so every BFF request gets default resilience.
616
+ // The raw `#fetch` below stays unwrapped for the direct-to-storage asset PUT
617
+ // and presigned output GETs, which target object storage, not the API plane.
618
+ const baseFetch = options.fetch ?? ((input, init) => fetch(input, init));
619
+ const retryingFetch = withRetry(baseFetch, options.retry);
383
620
  this.#http = new HttpClient({
384
621
  ...(options.baseUrl ? { baseUrl: options.baseUrl } : {}),
385
622
  apiToken: options.apiToken,
386
- ...(options.fetch ? { fetch: options.fetch } : {}),
623
+ fetch: retryingFetch,
387
624
  // Opt-in local diagnostics: emit a redacted per-request trace to
388
625
  // stderr. Uploads nothing. A caller wanting a custom sink can pass
389
626
  // a function instead of `true`.
@@ -392,12 +629,10 @@ export class AgentExecutor {
392
629
  : {})
393
630
  });
394
631
  this.#fetch = options.fetch;
395
- this.skills = new SkillsClient(this.#http);
396
632
  this.agentsMd = new AgentsMdClient(this.#http);
397
633
  this.files = new FilesClient(this.#http);
398
634
  this.secrets = new SecretsClient(this.#http);
399
- this.chat = new ChatClient(this.#http, (options) => this.#buildSessionCreateRequest(options));
400
- this.sessions = this.chat;
635
+ this.sessions = new SessionClient(this.#http, (options) => this.#buildSessionCreateRequest(options), this.#fetch);
401
636
  }
402
637
  /**
403
638
  * Internal: satisfies the `SecretUploader` surface so a
@@ -410,9 +645,10 @@ export class AgentExecutor {
410
645
  }
411
646
  /**
412
647
  * Internal: materialize raw bytes to the content-addressable asset store
413
- * (`/assets/presign` → PUT → `/assets/finalize`). Used by `Skill.upload(this)`
414
- * to pre-upload a draft skill bundle so a later run carries only a plain
415
- * `kind:"asset"` ref. NOT part of the public API.
648
+ * (`/assets/presign` → PUT → `/assets/finalize`). Used by the session-create
649
+ * prepare step to upload draft skill-tool / tool / agentsMd / file bundles so
650
+ * the wire submission carries only plain `kind:"asset"` / `kind:"skill"` refs.
651
+ * NOT part of the public API.
416
652
  */
417
653
  async _uploadAsset(args) {
418
654
  return uploadAsset({
@@ -444,10 +680,15 @@ export class AgentExecutor {
444
680
  ...(opts.idleTimeoutMs !== undefined ? { idleTimeoutMs: opts.idleTimeoutMs } : {}),
445
681
  ...(opts.pingIntervalMs !== undefined ? { pingIntervalMs: opts.pingIntervalMs } : {})
446
682
  };
447
- const session = await this.sessions.create(createOptions);
683
+ // Derive the message key from the create key (like the CLI) so a retried
684
+ // run with the same `idempotencyKey` de-duplicates BOTH the create and the
685
+ // billable turn server-side — never a duplicate billable run (sdk-dx-3).
686
+ const createKey = createOptions.idempotencyKey ?? generateIdempotencyKey();
687
+ const messageKey = messageIdempotencyKey ?? deriveMessageKey(createKey);
688
+ const session = await this.sessions.create({ ...createOptions, idempotencyKey: createKey });
448
689
  const turnResult = await sendSessionInternal(session, input, {
449
690
  ...streamOptions,
450
- idempotencyKey: messageIdempotencyKey ?? generateIdempotencyKey()
691
+ idempotencyKey: messageKey
451
692
  }).done();
452
693
  if (deleteAfter) {
453
694
  await session.delete();
@@ -476,6 +717,19 @@ export class AgentExecutor {
476
717
  ...(!ok && errorMessage ? { error: errorMessage } : {})
477
718
  };
478
719
  if (opts.throwOnFailure && !ok) {
720
+ // A turn that failed because the upstream provider throttled us surfaces
721
+ // as a structured, non-leaky AexRateLimitError carrying the provider
722
+ // fault, so callers can branch on `isRateLimited(err)` and replay.
723
+ const throttle = throttleFromSession(turnResult.session);
724
+ if (throttle) {
725
+ throw new AexRateLimitError({
726
+ status: throttle.status ?? 429,
727
+ attempts: 1,
728
+ source: "provider",
729
+ providerFault: throttle,
730
+ ...(throttle.retryAfterMs !== undefined ? { retryAfterMs: throttle.retryAfterMs } : {})
731
+ });
732
+ }
479
733
  throw new RunStateError(`AgentExecutor.run: session ${runId} ended ${turnResult.status}${errorMessage ? `: ${errorMessage}` : ""}`, { runId, status: turnResult.status });
480
734
  }
481
735
  return result;
@@ -484,183 +738,11 @@ export class AgentExecutor {
484
738
  scopedSignal?.clear();
485
739
  }
486
740
  }
487
- /**
488
- * Explicit, discoverable alias for {@link run}: open a one-shot session turn
489
- * and collect the full {@link RunResult} in one call.
490
- */
491
- runAndCollect(options, opts) {
492
- return this.run(options, opts);
493
- }
494
741
  openSession(optionsOrId) {
495
742
  return typeof optionsOrId === "string"
496
743
  ? this.sessions.open(optionsOrId)
497
744
  : this.sessions.create(optionsOrId);
498
745
  }
499
- /**
500
- * Poll `listEvents` until the snapshot is settle-bracketed — both a
501
- * RUN_STARTED and a terminal (RUN_FINISHED / RUN_ERROR) event present — then
502
- * return it. The runner emits the terminal AG-UI event BEFORE the platform
503
- * commits the record, and the `listEvents` snapshot can lag the terminal
504
- * record by a beat; this closes that race so the decoded trace/text/outputs
505
- * are complete. Bounded so an older runtime that never emits one of the
506
- * brackets still returns the best snapshot available.
507
- */
508
- async #collectSettledEvents(runId, signal) {
509
- const intervalMs = 500;
510
- const maxAttempts = 20;
511
- let latest = [];
512
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
513
- if (signal?.aborted)
514
- return latest;
515
- latest = await this.listEvents(runId);
516
- const hasStart = latest.some((event) => event.type === "RUN_STARTED");
517
- const hasTerminal = latest.some((event) => event.type === "RUN_FINISHED" || event.type === "RUN_ERROR");
518
- if (hasStart && hasTerminal)
519
- return latest;
520
- try {
521
- await sleep(intervalMs, signal);
522
- }
523
- catch {
524
- return latest;
525
- }
526
- }
527
- return latest;
528
- }
529
- /**
530
- * Submit a run and return its run id immediately. Use that id with
531
- * `wait`, `stream`, `outputs`, `download`, `cancel`, or `delete`.
532
- *
533
- * The SDK splits `mcpServers[i].headers` into `secrets.mcpServers`
534
- * and `proxyEndpoints[i]` auth values into `secrets.proxyEndpointAuth`
535
- * before sending so credentials never enter the hashed submission or
536
- * the run snapshot.
537
- *
538
- * Unstaged inline skills / agentsMd / files (`Skill.fromFiles` /
539
- * `Skill.fromPath` / `AgentsMd.fromContent` / `File.fromBytes` without a
540
- * prior `.upload`) are auto-uploaded to the content-addressable asset
541
- * store (`/assets/presign` → PUT → `/assets/finalize`) before `POST /runs`,
542
- * deduped by content hash, and referenced in the submission as plain
543
- * `{ kind:"asset" }` refs — identical to a pre-staged `.upload(client)`.
544
- */
545
- async submit(options) {
546
- if (!options || typeof options !== "object") {
547
- throw new RunConfigValidationError("AgentExecutor.submit: options is required");
548
- }
549
- assertNoRemovedSubmitFields(options, "AgentExecutor.submit");
550
- // A model maps to one or more upstream providers (see MODEL_PROVIDER_IDS).
551
- // `providersForModel` returns the supported providers in priority order, or
552
- // `[]` for an unknown model string (the model check below then rejects it).
553
- // An explicit provider is allowed but must be one that serves the model;
554
- // when omitted the model's default (first-listed) provider is used.
555
- const supportedProviders = providersForModel(options.model);
556
- if (options.provider &&
557
- supportedProviders.length > 0 &&
558
- !supportedProviders.includes(options.provider)) {
559
- throw new RunConfigValidationError(`AgentExecutor.submit: provider ${JSON.stringify(options.provider)} is not available for ` +
560
- `model ${JSON.stringify(options.model)} (supported: ${supportedProviders.join(", ")})`);
561
- }
562
- const provider = options.provider ?? supportedProviders[0] ?? DEFAULT_RUN_PROVIDER;
563
- validateSubmitCredentials(options, provider, "AgentExecutor.submit");
564
- if (typeof options.model !== "string" || !options.model) {
565
- throw new RunConfigValidationError("AgentExecutor.submit: model is required");
566
- }
567
- const prompt = normalisePrompt(options.prompt, "AgentExecutor.submit", "prompt");
568
- const { endpoints: proxyEndpointDeclarations, auth: proxyEndpointAuthFromInstances } = splitProxyEndpoints(options.proxyEndpoints ?? []);
569
- const mergedProxyAuth = mergeProxyEndpointAuth(proxyEndpointAuthFromInstances, options.secrets?.proxyEndpointAuth ?? []);
570
- // Split secretEnv into value-free declarations (hashed submission) and
571
- // ephemeral values (vaulted secrets channel), mirroring the proxy split.
572
- const { declarations: secretEnvDeclarations, values: envSecretValues } = splitSecretEnv(options.secretEnv);
573
- // Validate the per-run limits override with the SAME parser the server runs
574
- // (shape + positivity + allow-list), failing fast before any asset upload.
575
- // Normalizes an all-absent override (e.g. `{}`) away.
576
- let limits;
577
- try {
578
- limits = parseRunLimits(options.limits);
579
- }
580
- catch (err) {
581
- throw new AexError("RUN_CONFIG_INVALID", `AgentExecutor.submit: ${err instanceof Error ? err.message : String(err)}`);
582
- }
583
- // Walk Skill / Tool / AgentsMd / File instances. Inline drafts are eagerly
584
- // uploaded to the content-addressable asset store here (before POST /runs)
585
- // and referenced as plain `kind:"asset"` refs. Already-materialized asset
586
- // refs pass through unchanged.
587
- const uploader = (args) => this._uploadAsset(args);
588
- const preparedSkills = await prepareSkills(options.skills ?? [], uploader);
589
- const preparedTools = await prepareTools(options.tools ?? [], uploader);
590
- const preparedAgentsMd = await prepareAgentsMd(options.agentsMd ?? [], uploader);
591
- const preparedFiles = await prepareFiles(options.files ?? [], uploader);
592
- const { submissionMcpServers, mergedMcpSecrets } = mergeMcpServers(options.mcpServers ?? [], options.secrets?.mcpServers ?? []);
593
- const outputCapture = outputsForWire(options.outputs);
594
- const submission = {
595
- model: options.model,
596
- ...(options.system ? { system: options.system } : {}),
597
- prompt,
598
- skills: preparedSkills,
599
- // The wire `tools` is the union: builtin name strings (cherry-picks)
600
- // followed by the custom tool bundle refs. The shared parser splits them
601
- // back into `tools` (custom) + `builtinTools` (names). The cast
602
- // acknowledges the SDK is producing pre-parse wire input here, same as
603
- // `mcpServers` / `environment` below.
604
- tools: [...preparedTools.builtinNames, ...preparedTools.refs],
605
- agentsMd: preparedAgentsMd,
606
- files: preparedFiles,
607
- // submissionMcpServers may contain workspace refs of the shape
608
- // {kind:"workspace", id:"mcp_..."}. The BFF runs
609
- // `resolveWorkspaceMcpRefsInSubmission` BEFORE the shared parser
610
- // and replaces them with the resolved {name, url}, so by the
611
- // time anything reads PlatformSubmission post-parse the
612
- // shape matches McpServerRef. The cast acknowledges that the
613
- // SDK is producing pre-resolution wire input here.
614
- mcpServers: submissionMcpServers,
615
- ...(Object.keys(secretEnvDeclarations).length > 0 ? { secretEnv: secretEnvDeclarations } : {}),
616
- // `options.environment.packages` carry the customer wire shape
617
- // (`{name:"pip:pandas"}`); the shared parser resolves the ecosystem
618
- // prefix into PlatformPackage. The cast acknowledges the SDK is
619
- // producing pre-parse wire input here, same as `mcpServers` above.
620
- ...(options.environment
621
- ? { environment: options.environment }
622
- : {}),
623
- ...(options.metadata ? { metadata: options.metadata } : {}),
624
- ...(outputCapture ? { outputs: outputCapture } : {}),
625
- // Pass-through the builtin-tool toggle verbatim (omitted ⇒ default ON).
626
- ...(options.includeBuiltinTools !== undefined
627
- ? { includeBuiltinTools: options.includeBuiltinTools }
628
- : {}),
629
- ...(options.outputMode !== undefined ? { outputMode: options.outputMode } : {})
630
- };
631
- const secrets = {
632
- ...options.secrets,
633
- ...(mergedMcpSecrets.length > 0 ? { mcpServers: mergedMcpSecrets } : {}),
634
- ...(mergedProxyAuth.length > 0 ? { proxyEndpointAuth: mergedProxyAuth } : {}),
635
- ...(Object.keys(envSecretValues).length > 0 ? { envSecrets: envSecretValues } : {})
636
- };
637
- const request = {
638
- idempotencyKey: options.idempotencyKey ?? generateIdempotencyKey(),
639
- // Always include `provider` on the wire so dashboard / proxy
640
- // tooling never has to second-guess what the runtime saw. The
641
- // shared parser still defaults to `anthropic` when callers omit
642
- // the field entirely, but the SDK has resolved it by here.
643
- provider,
644
- submission,
645
- ...(options.runtimeSize ? { runtimeSize: options.runtimeSize } : {}),
646
- ...(options.timeout ? { timeout: options.timeout } : {}),
647
- ...(options.parentRunId ? { parentRunId: options.parentRunId } : {}),
648
- // Operational/delivery concern — sibling of idempotencyKey, NOT part of
649
- // the hashed brief. The idempotency key here is randomly generated, so
650
- // including the field has no effect on dedup.
651
- ...(options.webhook ? { webhook: options.webhook } : {}),
652
- // Per-run lineage-limit override — a top-level operational dial (sibling
653
- // of parentRunId), NOT part of the hashed submission. Validated + normalized
654
- // above; the server re-clamps against the workspace + platform ceilings.
655
- ...(limits ? { limits } : {}),
656
- secrets,
657
- ...(proxyEndpointDeclarations.length > 0
658
- ? { proxyEndpoints: proxyEndpointDeclarations }
659
- : {})
660
- };
661
- const run = await operations.submitRun(this.#http, request);
662
- return getSubmittedRunId(run);
663
- }
664
746
  async #buildSessionCreateRequest(options) {
665
747
  if (!options || typeof options !== "object") {
666
748
  throw new RunConfigValidationError("Aex.openSession: options is required");
@@ -691,7 +773,6 @@ export class AgentExecutor {
691
773
  throw new AexError("RUN_CONFIG_INVALID", `Aex.openSession: ${err instanceof Error ? err.message : String(err)}`);
692
774
  }
693
775
  const uploader = (args) => this._uploadAsset(args);
694
- const preparedSkills = await prepareSkills(options.skills ?? [], uploader);
695
776
  const preparedTools = await prepareTools(options.tools ?? [], uploader);
696
777
  const preparedAgentsMd = await prepareAgentsMd(options.agentsMd ?? [], uploader);
697
778
  const preparedFiles = await prepareFiles(options.files ?? [], uploader);
@@ -701,8 +782,13 @@ export class AgentExecutor {
701
782
  const submission = {
702
783
  model: options.model,
703
784
  ...(options.system ? { system: options.system } : {}),
704
- skills: preparedSkills,
705
- tools: [...preparedTools.builtinNames, ...preparedTools.refs],
785
+ // Builtin name strings + custom tool refs + skill-tool refs all ride the
786
+ // one `tools` union; the BFF parser splits them back apart by kind.
787
+ tools: [
788
+ ...preparedTools.builtinNames,
789
+ ...preparedTools.refs,
790
+ ...preparedTools.skillToolRefs
791
+ ],
706
792
  agentsMd: preparedAgentsMd,
707
793
  files: preparedFiles,
708
794
  mcpServers: submissionMcpServers,
@@ -729,275 +815,15 @@ export class AgentExecutor {
729
815
  ...(options.overrides?.timeout ? { timeout: options.overrides.timeout } : {}),
730
816
  ...(limits ? { limits } : {}),
731
817
  retention,
818
+ // Operational/delivery concern — sibling of secrets, NOT part of the
819
+ // hashed submission. Delivered at the settle-consistent barrier.
820
+ ...(options.webhook ? { webhook: options.webhook } : {}),
732
821
  secrets,
733
822
  ...(proxyEndpointDeclarations.length > 0
734
823
  ? { proxyEndpoints: proxyEndpointDeclarations }
735
824
  : {})
736
825
  };
737
826
  }
738
- getRun(runId) {
739
- return operations.getRun(this.#http, runId);
740
- }
741
- /** Short alias for `getRun`. */
742
- get(runId) {
743
- return this.getRun(runId);
744
- }
745
- /**
746
- * List the runs in this workspace, most-recent first, one page at a time.
747
- * The workspace is derived server-side from the API token, so this only ever
748
- * enumerates your own runs. Pass `query.cursor` (from a prior page's
749
- * `nextCursor`) to page; omit it for the first page. Returns public-safe
750
- * {@link RunSummary} rows — the full submission stays behind `getRunUnit`.
751
- *
752
- * This is the workspace-wide discovery entry point: combine it with
753
- * `listOutputs` / `readOutputText` to reach any run's deliverables.
754
- */
755
- listRuns(query) {
756
- return operations.listRuns(this.#http, query);
757
- }
758
- /**
759
- * Find output files across runs by filename / extension / content type.
760
- * Returns lean REFERENCE hits (runId, outputId, filename, size, content type)
761
- * — never bytes; fetch content with {@link readOutputText}. Scope the search
762
- * to a corpus with `query.runIds`; omit it to scan the whole workspace (needs
763
- * the owner-gated `listRuns`). Composed client-side for the MVP (per-run
764
- * `listOutputs` + the contracts output filter), so it works against any
765
- * already-terminal run with no new server endpoint. Bounded by
766
- * `query.limit` (default 100) — for very large corpora prefer the deferred
767
- * server-side index.
768
- */
769
- async searchOutputs(query = {}) {
770
- const runIds = query.runIds ?? (await this.#allWorkspaceRunIds());
771
- const limit = query.limit ?? 100;
772
- // Translate the search query to an OutputQuery so the contracts output
773
- // filter (classifyOutput / basename match / contentType wildcard) does the
774
- // matching — no re-derived filter logic here.
775
- const outputQuery = {
776
- ...(query.filename ? { filename: new RegExp(escapeRegExp(query.filename), "i") } : {}),
777
- ...(query.extension ? { extension: query.extension } : {}),
778
- ...(query.contentType ? { contentType: query.contentType } : {})
779
- };
780
- const hasFilter = Object.keys(outputQuery).length > 0;
781
- const hits = [];
782
- for (const runId of runIds) {
783
- const outputs = hasFilter
784
- ? await this.listOutputs(runId, outputQuery)
785
- : await this.listOutputs(runId);
786
- for (const o of outputs) {
787
- hits.push({
788
- runId,
789
- outputId: o.id,
790
- ...(o.filename !== undefined ? { filename: o.filename } : {}),
791
- ...(o.sizeBytes !== undefined ? { sizeBytes: o.sizeBytes } : {}),
792
- ...(o.contentType !== undefined ? { contentType: o.contentType } : {})
793
- });
794
- if (hits.length >= limit)
795
- return { hits };
796
- }
797
- }
798
- return { hits };
799
- }
800
- /** Enumerate every run id in the workspace by paging `listRuns`. */
801
- async #allWorkspaceRunIds() {
802
- const ids = [];
803
- let cursor;
804
- do {
805
- const page = await this.listRuns(cursor ? { cursor } : {});
806
- for (const run of page.runs)
807
- ids.push(run.id);
808
- cursor = page.nextCursor;
809
- } while (cursor);
810
- return ids;
811
- }
812
- /**
813
- * Fetch the self-contained `RunUnit`: parsed submission inputs,
814
- * attempts, indexed events (inline + cursor for the tail), raw
815
- * provider-event Storage manifest, outputs, capture failures,
816
- * proxy-call audit, pinned workspace skills, provider skills,
817
- * inline skills. Backed by the same endpoint as `getRun` but
818
- * typed against the full wire shape — use this when you need
819
- * fields beyond `{id, status, timestamps, usage}`.
820
- */
821
- getRunUnit(runId) {
822
- return operations.getRunUnit(this.#http, runId);
823
- }
824
- /** Short alias for `getRunUnit`. */
825
- getUnit(runId) {
826
- return this.getRunUnit(runId);
827
- }
828
- listEvents(runId) {
829
- return operations.listRunEvents(this.#http, runId);
830
- }
831
- /** Short alias for `listEvents`. */
832
- events(runId) {
833
- return this.listEvents(runId);
834
- }
835
- /**
836
- * Yield run events (the `RunEvent` snapshot shape) as they arrive, by
837
- * polling the coordinator-backed `/events` endpoint until the run reaches
838
- * a terminal state, the signal aborts, or the caller breaks the iterator.
839
- *
840
- * For the live, low-latency envelope stream prefer {@link streamEnvelopes}
841
- * (coordinator WebSocket). This polling form stays for consumers that want
842
- * the loose `RunEvent` shape without a WS.
843
- */
844
- async *streamEvents(runId, options = {}) {
845
- if (options.signal?.aborted)
846
- return;
847
- yield* this.#streamEventsPolling(runId, { ...options, seenIds: new Set() });
848
- }
849
- /** Short alias for `streamEvents`. */
850
- stream(runId, options) {
851
- return this.streamEvents(runId, options);
852
- }
853
- /**
854
- * Stream the unified {@link AexEvent} envelope live over the coordinator
855
- * WebSocket. The hosted API's ticket broker authorizes the connection (workspace
856
- * token → short-lived coordinator ticket); the shared client replays from
857
- * the cursor, tails live, and resumes exactly-once across reconnects. The
858
- * ticket is re-minted on each (re)connect so a long run never outlives it.
859
- */
860
- async *streamEnvelopes(runId, options = {}) {
861
- const first = await operations.getCoordinatorTicket(this.#http, runId);
862
- yield* streamCoordinatorEvents({
863
- wsUrl: first.wsUrl,
864
- from: options.from ?? 0,
865
- fetchTicket: async () => (await operations.getCoordinatorTicket(this.#http, runId)).ticket,
866
- // settleConsistent ends the stream on the post-mirror barrier instead of the
867
- // earlier RUN_FINISHED UX signal, so "stream ended" ⇒ getRun is terminal.
868
- ...(options.settleConsistent ? { isTerminal: isRunSettled } : {}),
869
- ...(options.signal ? { signal: options.signal } : {})
870
- });
871
- }
872
- async *#streamEventsPolling(runId, options) {
873
- const intervalMs = options.intervalMs ?? 1_000;
874
- const signal = options.signal;
875
- while (!signal?.aborted) {
876
- const events = await this.listEvents(runId);
877
- for (const event of events) {
878
- if (!options.seenIds.has(event.id)) {
879
- options.seenIds.add(event.id);
880
- yield event;
881
- }
882
- }
883
- const run = await this.getRun(runId);
884
- if (isTerminal(run.status))
885
- return;
886
- // `sleep` rejects on abort — treat that as a graceful stop.
887
- try {
888
- await sleep(intervalMs, signal);
889
- }
890
- catch {
891
- return;
892
- }
893
- }
894
- }
895
- /**
896
- * Poll the run record until it reaches a terminal status (succeeded,
897
- * failed, terminated). Throws if `timeoutMs` elapses first.
898
- */
899
- async waitForRun(runId, options = {}) {
900
- const intervalMs = options.intervalMs ?? 1_500;
901
- const timeoutMs = options.timeoutMs;
902
- const signal = options.signal;
903
- const deadline = typeof timeoutMs === "number" ? Date.now() + timeoutMs : Number.POSITIVE_INFINITY;
904
- while (!signal?.aborted) {
905
- const run = await this.getRun(runId);
906
- if (isTerminal(run.status))
907
- return run;
908
- if (Date.now() >= deadline) {
909
- throw new Error(`AgentExecutor.waitForRun: timeout after ${timeoutMs}ms`);
910
- }
911
- await sleep(intervalMs, signal);
912
- }
913
- throw new Error("AgentExecutor.waitForRun: aborted");
914
- }
915
- /** Short alias for `waitForRun`. */
916
- wait(runId, options) {
917
- return this.waitForRun(runId, options);
918
- }
919
- listOutputs(runId, query) {
920
- return operations.listOutputs(this.#http, runId, query);
921
- }
922
- /** Short alias for `listOutputs`. */
923
- outputs(runId, query) {
924
- return this.listOutputs(runId, query);
925
- }
926
- findOutputs(runId, query) {
927
- return operations.findOutputs(this.#http, runId, query);
928
- }
929
- findOutput(runId, query) {
930
- return operations.findOutput(this.#http, runId, query);
931
- }
932
- outputLink(runId, selectorOrQuery, options) {
933
- return operations.outputLink(this.#http, runId, selectorOrQuery, options);
934
- }
935
- createOutputLink(runId, selectorOrQuery, options) {
936
- return this.outputLink(runId, selectorOrQuery, options);
937
- }
938
- async fetchOutput(runId, selectorOrQuery, options) {
939
- const link = await this.outputLink(runId, selectorOrQuery, options);
940
- return (this.#fetch ?? fetch)(link.url);
941
- }
942
- /**
943
- * Read ONE output file as byte-capped, decoded UTF-8 text. Streams the file and
944
- * STOPS at `options.maxBytes` (default 50 KB, ceiling 10 MB), so a huge
945
- * deliverable never fully buffers — ideal for handing a run's output to an LLM
946
- * tool. Check `result.truncated` before treating the text as complete; pass
947
- * `options.grep` to keep only matching lines. Select by `{ path }` or `{ id }`,
948
- * same as `downloadOutput`.
949
- */
950
- readOutputText(runId, selector, options) {
951
- return operations.readOutputText(this.#http, runId, selector, options);
952
- }
953
- eventArchiveLink(runId, options) {
954
- return operations.eventArchiveLink(this.#http, runId, options);
955
- }
956
- async downloadOutput(runId, selectorOrOptions, options) {
957
- const hasSelector = selectorOrOptions !== undefined && !isOutputDownloadOptionsOnly(selectorOrOptions);
958
- const selector = hasSelector ? selectorOrOptions : undefined;
959
- const to = hasSelector ? options?.to : selectorOrOptions?.to ?? options?.to;
960
- let bytes;
961
- if (selector === undefined) {
962
- bytes = await operations.downloadOutputs(this.#http, runId);
963
- }
964
- else {
965
- const output = isOutputPathSelector(selector)
966
- ? resolveOutputFileSelector(await operations.listOutputs(this.#http, runId), selector, runId)
967
- : resolveOutputFileSelector([], selector, runId);
968
- const { response } = await this.#http.download(`/api/runs/${encodeURIComponent(runId)}/outputs/${encodeURIComponent(output.id)}/download`);
969
- bytes = new Uint8Array(await response.arrayBuffer());
970
- }
971
- return writeOptionalFile(bytes, to);
972
- }
973
- cancelRun(runId) {
974
- return operations.cancelRun(this.#http, runId);
975
- }
976
- /** Short alias for `cancelRun`. */
977
- cancel(runId) {
978
- return this.cancelRun(runId);
979
- }
980
- deleteRun(runId) {
981
- return operations.deleteRun(this.#http, runId);
982
- }
983
- /** Short alias for `deleteRun`. */
984
- delete(runId) {
985
- return this.deleteRun(runId);
986
- }
987
- /**
988
- * List a run's webhook delivery attempts (the per-run delivery ledger).
989
- * Empty when the run carried no `webhook` or has not yet terminated.
990
- */
991
- getRunWebhookDeliveries(runId) {
992
- return operations.getRunWebhookDeliveries(this.#http, runId);
993
- }
994
- /**
995
- * Manually re-trigger a run's webhook delivery: re-sends the frozen payload
996
- * with the SAME `webhook-id` so the consumer dedupes.
997
- */
998
- redeliverRunWebhook(runId, deliveryId) {
999
- return operations.redeliverRunWebhook(this.#http, runId, deliveryId);
1000
- }
1001
827
  /**
1002
828
  * Delete a workspace asset blob from the shared content-addressed store
1003
829
  * (`assets/<workspaceId>/<hash>`). Accepts `sha256:<hex>` or a bare
@@ -1009,29 +835,6 @@ export class AgentExecutor {
1009
835
  whoami() {
1010
836
  return operations.whoami(this.#http);
1011
837
  }
1012
- /**
1013
- * Download EVERYTHING public about a run as one zip, assembled client-side
1014
- * from the public read endpoints (`getRun` + `listEvents` +
1015
- * `listOutputs` + per-output `/download`). Organised into the namespace
1016
- * folders `metadata/`, `events/`, and `outputs/`, plus a `manifest.json`.
1017
- * Pass `to` to also write the
1018
- * bytes to a file path while still returning the bytes.
1019
- */
1020
- async download(runId, options) {
1021
- return writeOptionalFile(await operations.download(this.#http, runId), options?.to);
1022
- }
1023
- /** Download only the run's deliverables (the `outputs` namespace) as a zip. */
1024
- async downloadOutputs(runId, options) {
1025
- return writeOptionalFile(await operations.downloadOutputs(this.#http, runId), options?.to);
1026
- }
1027
- /** Download only the indexed event archive (the `events` namespace) as a zip. */
1028
- async downloadEvents(runId, options) {
1029
- return writeOptionalFile(await operations.downloadEvents(this.#http, runId), options?.to);
1030
- }
1031
- /** Download only the run record (the `metadata` namespace) as a zip. */
1032
- async downloadMetadata(runId, options) {
1033
- return writeOptionalFile(await operations.downloadMetadata(this.#http, runId), options?.to);
1034
- }
1035
838
  }
1036
839
  /** Canonical SDK client name. `AgentExecutor` remains as a compatibility alias. */
1037
840
  export class Aex extends AgentExecutor {
@@ -1043,6 +846,19 @@ const TERMINAL_STATUSES = new Set(TERMINAL_RUN_STATUSES);
1043
846
  function isTerminal(status) {
1044
847
  return typeof status === "string" && TERMINAL_STATUSES.has(status);
1045
848
  }
849
+ /**
850
+ * A session is "parked" once it stops making progress: it reached one of the
851
+ * turn-terminal statuses (`idle` / `suspended` / `error`) or a terminal run
852
+ * status. `SessionHandle.wait` / `streamEvents` stop here.
853
+ */
854
+ function isSessionParked(status) {
855
+ return (status === "idle" ||
856
+ status === "suspended" ||
857
+ status === "error" ||
858
+ status === "deleted" ||
859
+ status === "expired" ||
860
+ isTerminal(status));
861
+ }
1046
862
  function sessionToRun(session) {
1047
863
  const id = session.sessionId ?? session.id;
1048
864
  return {
@@ -1075,13 +891,6 @@ function escapeRegExp(input) {
1075
891
  function isOutputPathSelector(selector) {
1076
892
  return Boolean(selector && typeof selector === "object" && "path" in selector);
1077
893
  }
1078
- function isOutputDownloadOptionsOnly(value) {
1079
- return Boolean(value &&
1080
- typeof value === "object" &&
1081
- "to" in value &&
1082
- !("id" in value) &&
1083
- !("path" in value));
1084
- }
1085
894
  function resolveOutputFileSelector(outputs, selector, runId) {
1086
895
  if (isOutputPathSelector(selector)) {
1087
896
  const target = normalizeOutputLookupPath(selector.path);
@@ -1148,22 +957,39 @@ function generateIdempotencyKey() {
1148
957
  return cryptoObj.randomUUID();
1149
958
  return `idem-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
1150
959
  }
1151
- function normalisePrompt(input, surface = "AgentExecutor.submit", field = "prompt") {
1152
- if (typeof input === "string") {
1153
- if (!input) {
1154
- throw new RunConfigValidationError(`${surface}: ${field} must be a non-empty string`);
1155
- }
1156
- return [input];
1157
- }
1158
- if (!Array.isArray(input) || input.length === 0) {
1159
- throw new RunConfigValidationError(`${surface}: ${field} must be a non-empty string or string array`);
960
+ /**
961
+ * Derive the message idempotency key from the session-create key. Mirrors the
962
+ * CLI (`<createKey>:message`) so a retried `run` / `sessions.run` that reuses
963
+ * one `idempotencyKey` de-duplicates BOTH the create and the billable turn.
964
+ */
965
+ function deriveMessageKey(createKey) {
966
+ return `${createKey}:message`;
967
+ }
968
+ /**
969
+ * Extract a throttle-class {@link ProviderFault} from a failed session record.
970
+ * Reads a structured `providerFault` / `error` field first (the shape the
971
+ * runtime is expected to emit on a throttled turn), then falls back to a
972
+ * heuristic scan of `errorMessage`. Returns `undefined` when the failure is not
973
+ * a throttle.
974
+ */
975
+ function throttleFromSession(session) {
976
+ const fault = parseProviderFault(session.providerFault) ??
977
+ parseProviderFault(session.error) ??
978
+ faultFromErrorMessage(typeof session.errorMessage === "string" ? session.errorMessage : undefined);
979
+ return fault && isThrottleFault(fault) ? fault : undefined;
980
+ }
981
+ /** Last-resort throttle detection from a free-text run error message. */
982
+ function faultFromErrorMessage(message) {
983
+ if (message === undefined || message.length === 0)
984
+ return undefined;
985
+ const lower = message.toLowerCase();
986
+ if (/\b429\b|rate.?limit|too many requests/.test(lower)) {
987
+ return { kind: "rate_limit", message };
1160
988
  }
1161
- for (const segment of input) {
1162
- if (typeof segment !== "string" || !segment) {
1163
- throw new RunConfigValidationError(`${surface}: ${field} segments must be non-empty strings`);
1164
- }
989
+ if (/\b529\b|overloaded/.test(lower)) {
990
+ return { kind: "overloaded", message };
1165
991
  }
1166
- return [...input];
992
+ return undefined;
1167
993
  }
1168
994
  function normaliseSessionInput(input, surface, field) {
1169
995
  if (typeof input === "string") {
@@ -1182,18 +1008,6 @@ function normaliseSessionInput(input, surface, field) {
1182
1008
  }
1183
1009
  return [...input];
1184
1010
  }
1185
- function assertNoRemovedSubmitFields(options, surface, extraFields = []) {
1186
- const record = options;
1187
- for (const field of ["credentialMode", "runtime", "region", "apiKey", "credentials", "postHook", ...extraFields]) {
1188
- if (Object.prototype.hasOwnProperty.call(record, field)) {
1189
- throw new RunConfigValidationError(`${surface}: ${field} is not a supported option; use the managed path with secrets.apiKeys[provider].`);
1190
- }
1191
- }
1192
- const secrets = record.secrets;
1193
- if (secrets && typeof secrets === "object" && !Array.isArray(secrets) && Object.prototype.hasOwnProperty.call(secrets, "apiKey")) {
1194
- throw new RunConfigValidationError(`${surface}: secrets.apiKey is not supported; use secrets.apiKeys[provider].`);
1195
- }
1196
- }
1197
1011
  function assertNoLegacySessionFields(options, surface) {
1198
1012
  const record = options;
1199
1013
  const messages = {
@@ -1204,14 +1018,14 @@ function assertNoLegacySessionFields(options, surface) {
1204
1018
  idleTtl: "use overrides.idleTtl.",
1205
1019
  retention: "use overrides.idleTtl.",
1206
1020
  secretEnv: "use environment.secrets.",
1021
+ skills: "skills are now tools; build one with Tools.fromSkillDir/fromSkillUrl and pass it in tools.",
1207
1022
  secrets: "use top-level apiKeys for provider keys and environment.secrets for run secrets.",
1208
1023
  runtimeSize: "use runtime.",
1209
1024
  parentRunId: "subagents are session-internal; parentRunId is not part of the session API.",
1210
1025
  limits: "use overrides.",
1211
1026
  timeout: "use overrides.timeout.",
1212
1027
  signal: "use session.cancel() / session.suspend() for remote control.",
1213
- postHook: "send a follow-up validation message when the session returns idle.",
1214
- webhook: "send a follow-up validation message instead of a submit webhook."
1028
+ postHook: "send a follow-up validation message when the session returns idle."
1215
1029
  };
1216
1030
  for (const [field, message] of Object.entries(messages)) {
1217
1031
  if (Object.prototype.hasOwnProperty.call(record, field)) {
@@ -1232,15 +1046,6 @@ function assertNoSessionSendSignal(options, surface) {
1232
1046
  throw new RunConfigValidationError(`${surface}: signal is not a supported option; use session.cancel() / session.suspend() for remote control.`);
1233
1047
  }
1234
1048
  }
1235
- function validateSubmitCredentials(options, provider, surface) {
1236
- if (options.parentRunId) {
1237
- return;
1238
- }
1239
- const key = options.secrets?.apiKeys?.[provider];
1240
- if (typeof key !== "string" || key.length === 0) {
1241
- throw new RunConfigValidationError(`${surface}: a provider API key is required — pass secrets.apiKeys[${JSON.stringify(provider)}].`);
1242
- }
1243
- }
1244
1049
  function validateApiKeys(apiKeys, provider, surface) {
1245
1050
  const key = apiKeys?.[provider];
1246
1051
  if (typeof key !== "string" || key.length === 0) {
@@ -1304,42 +1109,17 @@ async function resolveAssetId(entry, bundle, uploader) {
1304
1109
  entry._rememberAsset(uploaded.assetId);
1305
1110
  return uploaded.assetId;
1306
1111
  }
1307
- /** Walk Skill[], eagerly upload drafts as assets, and return plain asset refs. */
1308
- async function prepareSkills(skills, uploader) {
1309
- const refs = [];
1310
- for (let i = 0; i < skills.length; i++) {
1311
- const entry = skills[i];
1312
- if (!(entry instanceof Skill)) {
1313
- throw new RunConfigValidationError(`AgentExecutor.submit: skills[${i}] must be a Skill instance`);
1314
- }
1315
- const ref = entry.ref;
1316
- if (ref.kind === "draft") {
1317
- const bundle = entry._takeDraftBundle();
1318
- if (!bundle) {
1319
- throw new RunConfigValidationError(`AgentExecutor.submit: skills[${i}] is draft but has no bytes`);
1320
- }
1321
- const assetId = await resolveAssetId(entry, bundle, uploader);
1322
- refs.push({
1323
- kind: "asset",
1324
- assetId,
1325
- name: bundle.name
1326
- });
1327
- continue;
1328
- }
1329
- // Already-materialized asset ref.
1330
- refs.push(ref);
1331
- }
1332
- return refs;
1333
- }
1334
1112
  /**
1335
1113
  * Split the `tools` union into custom tool refs (drafts eagerly uploaded as
1336
- * assets) and builtin tool-name references (bare strings, validated against the
1337
- * closed {@link BUILTIN_TOOL_NAMES} set). Builtin names are deduped, in input
1338
- * order; the two groups are recombined on the wire (builtins first) by the
1339
- * caller.
1114
+ * assets), skill-tool refs (skill bundles eagerly uploaded as assets), and
1115
+ * builtin tool-name references (bare strings, validated against the closed
1116
+ * {@link BUILTIN_TOOL_NAMES} set). Builtin names are deduped, in input order;
1117
+ * the three groups are recombined on the wire by the caller (the BFF parser
1118
+ * splits them back apart by kind).
1340
1119
  */
1341
1120
  async function prepareTools(tools, uploader) {
1342
1121
  const refs = [];
1122
+ const skillToolRefs = [];
1343
1123
  const seenBuiltins = new Set();
1344
1124
  const builtinNames = [];
1345
1125
  for (let i = 0; i < tools.length; i++) {
@@ -1347,8 +1127,8 @@ async function prepareTools(tools, uploader) {
1347
1127
  // A bare string is a builtin tool reference.
1348
1128
  if (typeof entry === "string") {
1349
1129
  if (!BUILTIN_TOOL_NAMES.includes(entry)) {
1350
- throw new RunConfigValidationError(`AgentExecutor.submit: tools[${i}] (${JSON.stringify(entry)}) is not a builtin tool name; ` +
1351
- `expected a Tool instance or one of: ${BUILTIN_TOOL_NAMES.join(", ")}`);
1130
+ throw new RunConfigValidationError(`aex: tools[${i}] (${JSON.stringify(entry)}) is not a builtin tool name; ` +
1131
+ `expected a Tool, a SkillTool, or one of: ${BUILTIN_TOOL_NAMES.join(", ")}`);
1352
1132
  }
1353
1133
  if (!seenBuiltins.has(entry)) {
1354
1134
  seenBuiltins.add(entry);
@@ -1356,14 +1136,29 @@ async function prepareTools(tools, uploader) {
1356
1136
  }
1357
1137
  continue;
1358
1138
  }
1139
+ // A skill-tool: upload its bundle (if a draft) and emit a `kind:"skill"` ref.
1140
+ if (entry instanceof SkillTool) {
1141
+ const ref = entry.ref;
1142
+ if (ref.kind === "draft") {
1143
+ const bundle = entry._takeDraftBundle();
1144
+ if (!bundle) {
1145
+ throw new RunConfigValidationError(`aex: tools[${i}] is a draft skill-tool but has no bytes`);
1146
+ }
1147
+ const assetId = await resolveAssetId(entry, bundle, uploader);
1148
+ skillToolRefs.push({ kind: "skill", assetId, name: bundle.name, description: bundle.description });
1149
+ continue;
1150
+ }
1151
+ skillToolRefs.push(ref);
1152
+ continue;
1153
+ }
1359
1154
  if (!(entry instanceof Tool)) {
1360
- throw new RunConfigValidationError(`AgentExecutor.submit: tools[${i}] must be a Tool instance or a builtin tool name`);
1155
+ throw new RunConfigValidationError(`aex: tools[${i}] must be a Tool, a SkillTool, or a builtin tool name`);
1361
1156
  }
1362
1157
  const ref = entry.ref;
1363
1158
  if (ref.kind === "draft") {
1364
1159
  const bundle = entry._takeDraftBundle();
1365
1160
  if (!bundle) {
1366
- throw new RunConfigValidationError(`AgentExecutor.submit: tools[${i}] is draft but has no bytes`);
1161
+ throw new RunConfigValidationError(`aex: tools[${i}] is draft but has no bytes`);
1367
1162
  }
1368
1163
  const assetId = await resolveAssetId(entry, bundle, uploader);
1369
1164
  refs.push({ ...bundle.ref, assetId });
@@ -1371,7 +1166,7 @@ async function prepareTools(tools, uploader) {
1371
1166
  }
1372
1167
  refs.push(ref);
1373
1168
  }
1374
- return { refs, builtinNames };
1169
+ return { refs, skillToolRefs, builtinNames };
1375
1170
  }
1376
1171
  /** Walk AgentsMd[], eagerly upload drafts as assets, and return plain asset refs. */
1377
1172
  async function prepareAgentsMd(agentsMds, uploader) {
@@ -1379,13 +1174,13 @@ async function prepareAgentsMd(agentsMds, uploader) {
1379
1174
  for (let i = 0; i < agentsMds.length; i++) {
1380
1175
  const entry = agentsMds[i];
1381
1176
  if (!(entry instanceof AgentsMd)) {
1382
- throw new RunConfigValidationError(`AgentExecutor.submit: agentsMd[${i}] must be an AgentsMd instance`);
1177
+ throw new RunConfigValidationError(`aex: agentsMd[${i}] must be an AgentsMd instance`);
1383
1178
  }
1384
1179
  const ref = entry.ref;
1385
1180
  if (ref.kind === "draft") {
1386
1181
  const bundle = entry._takeDraftBundle();
1387
1182
  if (!bundle) {
1388
- throw new RunConfigValidationError(`AgentExecutor.submit: agentsMd[${i}] is draft but has no bytes`);
1183
+ throw new RunConfigValidationError(`aex: agentsMd[${i}] is draft but has no bytes`);
1389
1184
  }
1390
1185
  const assetId = await resolveAssetId(entry, bundle, uploader);
1391
1186
  refs.push({
@@ -1405,13 +1200,13 @@ async function prepareFiles(files, uploader) {
1405
1200
  for (let i = 0; i < files.length; i++) {
1406
1201
  const entry = files[i];
1407
1202
  if (!(entry instanceof File)) {
1408
- throw new RunConfigValidationError(`AgentExecutor.submit: files[${i}] must be a File instance`);
1203
+ throw new RunConfigValidationError(`aex: files[${i}] must be a File instance`);
1409
1204
  }
1410
1205
  const ref = entry.ref;
1411
1206
  if (ref.kind === "draft") {
1412
1207
  const bundle = entry._takeDraftBundle();
1413
1208
  if (!bundle) {
1414
- throw new RunConfigValidationError(`AgentExecutor.submit: files[${i}] is draft but has no bytes`);
1209
+ throw new RunConfigValidationError(`aex: files[${i}] is draft but has no bytes`);
1415
1210
  }
1416
1211
  const assetId = await resolveAssetId(entry, bundle, uploader);
1417
1212
  refs.push({
@@ -1426,13 +1221,6 @@ async function prepareFiles(files, uploader) {
1426
1221
  }
1427
1222
  return refs;
1428
1223
  }
1429
- function getSubmittedRunId(response) {
1430
- const id = response.id ?? response.runId;
1431
- if (typeof id !== "string" || id.length === 0) {
1432
- throw new RunStateError("AgentExecutor.submit: submit response did not include a run id");
1433
- }
1434
- return id;
1435
- }
1436
1224
  function mergeMcpServers(inputs, explicitSecrets) {
1437
1225
  const submissionMcpServers = [];
1438
1226
  const secretByName = new Map();
@@ -1442,14 +1230,14 @@ function mergeMcpServers(inputs, explicitSecrets) {
1442
1230
  for (let i = 0; i < inputs.length; i++) {
1443
1231
  const entry = inputs[i];
1444
1232
  if (!(entry instanceof McpServer)) {
1445
- throw new RunConfigValidationError(`AgentExecutor.submit: mcpServers[${i}] must be an McpServer instance`);
1233
+ throw new RunConfigValidationError(`aex: mcpServers[${i}] must be an McpServer instance`);
1446
1234
  }
1447
1235
  submissionMcpServers.push(entry.toSubmissionEntry());
1448
1236
  const secret = entry.toSecretEntry();
1449
1237
  if (secret) {
1450
1238
  const existing = secretByName.get(secret.name);
1451
1239
  if (existing && existing.url !== secret.url) {
1452
- throw new RunConfigValidationError(`AgentExecutor.submit: mcpServers[${i}].url conflicts with secrets.mcpServers["${secret.name}"]`);
1240
+ throw new RunConfigValidationError(`aex: mcpServers[${i}].url conflicts with secrets.mcpServers["${secret.name}"]`);
1453
1241
  }
1454
1242
  secretByName.set(secret.name, secret);
1455
1243
  }
@@ -1477,7 +1265,7 @@ function mergeProxyEndpointAuth(fromInstances, fromExplicitSecrets) {
1477
1265
  for (const entry of fromInstances) {
1478
1266
  const existing = byName.get(entry.name);
1479
1267
  if (existing && existing.value.type !== entry.value.type) {
1480
- throw new RunConfigValidationError(`AgentExecutor.submit: proxyEndpoint "${entry.name}" auth type conflicts ` +
1268
+ throw new RunConfigValidationError(`aex: proxyEndpoint "${entry.name}" auth type conflicts ` +
1481
1269
  `with secrets.proxyEndpointAuth (instance=${entry.value.type}, secrets=${existing.value.type})`);
1482
1270
  }
1483
1271
  byName.set(entry.name, entry);