@aexhq/sdk 0.32.0 → 0.33.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.
package/dist/client.js CHANGED
@@ -1,4 +1,4 @@
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";
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";
2
2
  import { AgentsMd } from "./agents-md.js";
3
3
  import { uploadAsset } from "./asset-upload.js";
4
4
  import { File } from "./file.js";
@@ -7,6 +7,171 @@ import { splitProxyEndpoints } from "./proxy-endpoint.js";
7
7
  import { splitSecretEnv } from "./secret.js";
8
8
  import { Skill } from "./skill.js";
9
9
  import { Tool } from "./tool.js";
10
+ export class SessionTurnStream {
11
+ #run;
12
+ #done;
13
+ constructor(run) {
14
+ this.#run = run;
15
+ }
16
+ [Symbol.asyncIterator]() {
17
+ return this.#run();
18
+ }
19
+ done() {
20
+ this.#done ??= (async () => {
21
+ const iterator = this.#run();
22
+ let next = await iterator.next();
23
+ while (!next.done) {
24
+ next = await iterator.next();
25
+ }
26
+ return next.value;
27
+ })();
28
+ return this.#done;
29
+ }
30
+ }
31
+ export const ChatTurnStream = SessionTurnStream;
32
+ const internalSessionSenders = new WeakMap();
33
+ function sendSessionInternal(session, input, options = {}) {
34
+ const sender = internalSessionSenders.get(session);
35
+ if (sender === undefined) {
36
+ throw new Error("Aex: invalid session handle");
37
+ }
38
+ return sender(normaliseSessionInput(input, "SessionHandle.send", "input"), options);
39
+ }
40
+ export class SessionHandle {
41
+ #http;
42
+ #session;
43
+ constructor(http, session) {
44
+ this.#http = http;
45
+ this.#session = session;
46
+ internalSessionSenders.set(this, (input, options = {}) => new SessionTurnStream(() => this.#send(input, options)));
47
+ }
48
+ get id() {
49
+ return this.#session.sessionId ?? this.#session.id;
50
+ }
51
+ get record() {
52
+ return this.#session;
53
+ }
54
+ send(input, options = {}) {
55
+ assertNoSessionSendSignal(options, "SessionHandle.send");
56
+ return sendSessionInternal(this, input, options);
57
+ }
58
+ async *#send(input, options) {
59
+ const accepted = await operations.sendSessionMessage(this.#http, this.id, { input }, { idempotencyKey: options.idempotencyKey ?? generateIdempotencyKey() });
60
+ this.#session = accepted.session;
61
+ const turn = accepted.turn;
62
+ const events = [];
63
+ for await (const event of streamSessionTurnEvents(this.#http, this.id, turn, {
64
+ ...options,
65
+ from: options.from ?? accepted.eventCursor ?? turn.eventCursor ?? 0
66
+ })) {
67
+ events.push(event);
68
+ yield event;
69
+ }
70
+ this.#session = await operations.getSession(this.#http, this.id).catch(() => this.#session);
71
+ const outputs = await operations.listSessionOutputs(this.#http, this.id).catch(() => []);
72
+ return {
73
+ sessionId: this.id,
74
+ session: this.#session,
75
+ turn,
76
+ status: this.#session.status,
77
+ text: textOf(events),
78
+ events,
79
+ outputs
80
+ };
81
+ }
82
+ async suspend(options = {}) {
83
+ const accepted = await operations.suspendSession(this.#http, this.id, options);
84
+ this.#session = accepted.session;
85
+ return accepted;
86
+ }
87
+ async cancel(options = {}) {
88
+ const accepted = await operations.cancelSession(this.#http, this.id, options);
89
+ this.#session = accepted.session;
90
+ return accepted;
91
+ }
92
+ async resume(options = {}) {
93
+ const accepted = await operations.resumeSession(this.#http, this.id, options);
94
+ this.#session = accepted.session;
95
+ return accepted;
96
+ }
97
+ async delete(options = {}) {
98
+ const accepted = await operations.deleteSession(this.#http, this.id, options);
99
+ if (accepted && typeof accepted === "object" && "session" in accepted) {
100
+ this.#session = accepted.session;
101
+ }
102
+ }
103
+ listEvents() {
104
+ return operations.listSessionEvents(this.#http, this.id);
105
+ }
106
+ listOutputs(query) {
107
+ return operations.listSessionOutputs(this.#http, this.id, query);
108
+ }
109
+ }
110
+ export const ChatSession = SessionHandle;
111
+ export class SessionClient {
112
+ #http;
113
+ #buildCreateRequest;
114
+ constructor(http, buildCreateRequest) {
115
+ this.#http = http;
116
+ this.#buildCreateRequest = buildCreateRequest;
117
+ }
118
+ async create(options) {
119
+ const request = await this.#buildCreateRequest(options);
120
+ const session = await operations.createSession(this.#http, request, { idempotencyKey: options.idempotencyKey ?? generateIdempotencyKey() });
121
+ return new SessionHandle(this.#http, session);
122
+ }
123
+ async open(sessionId) {
124
+ return new SessionHandle(this.#http, await operations.getSession(this.#http, sessionId));
125
+ }
126
+ get(sessionId) {
127
+ return operations.getSession(this.#http, sessionId);
128
+ }
129
+ list(query) {
130
+ return operations.listSessions(this.#http, query);
131
+ }
132
+ async run(options) {
133
+ const { message, deleteAfter, messageIdempotencyKey, stream, ...createOptions } = options;
134
+ assertNoLegacySessionFields(options, "Aex.sessions.run");
135
+ const input = normaliseSessionInput(message, "Aex.sessions.run", "message");
136
+ const session = await this.create(createOptions);
137
+ const result = await session.send(input, {
138
+ ...(stream ?? {}),
139
+ idempotencyKey: messageIdempotencyKey ?? generateIdempotencyKey()
140
+ }).done();
141
+ if (deleteAfter) {
142
+ await session.delete();
143
+ }
144
+ return result;
145
+ }
146
+ }
147
+ export const ChatClient = SessionClient;
148
+ async function* streamSessionTurnEvents(http, sessionId, turn, options) {
149
+ const first = await operations.getSessionCoordinatorTicket(http, sessionId);
150
+ yield* streamCoordinatorEvents({
151
+ wsUrl: first.wsUrl,
152
+ from: options.from ?? 0,
153
+ fetchTicket: async () => (await operations.getSessionCoordinatorTicket(http, sessionId)).ticket,
154
+ isTerminal: (event) => isSessionTurnTerminalEvent(event, turn.turnSeq),
155
+ ...(options.signal ? { signal: options.signal } : {}),
156
+ ...(options.webSocketFactory ? { webSocketFactory: options.webSocketFactory } : {}),
157
+ ...(options.idleTimeoutMs !== undefined ? { idleTimeoutMs: options.idleTimeoutMs } : {}),
158
+ ...(options.pingIntervalMs !== undefined ? { pingIntervalMs: options.pingIntervalMs } : {})
159
+ });
160
+ }
161
+ function isSessionTurnTerminalEvent(event, turnSeq) {
162
+ const name = customName(event);
163
+ if (name !== "aex.session.idle" &&
164
+ name !== "aex.session.suspended" &&
165
+ name !== "aex.session.error") {
166
+ return false;
167
+ }
168
+ const value = event.data.value;
169
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
170
+ return true;
171
+ }
172
+ const eventTurnSeq = value.turnSeq;
173
+ return typeof eventTurnSeq !== "number" || eventTurnSeq === turnSeq;
174
+ }
10
175
  /**
11
176
  * Workspace skill admin operations exposed under `client.skills`.
12
177
  *
@@ -181,6 +346,8 @@ export class AgentExecutor {
181
346
  agentsMd;
182
347
  files;
183
348
  secrets;
349
+ sessions;
350
+ chat;
184
351
  constructor(options) {
185
352
  if (!options.apiToken) {
186
353
  throw new Error("AgentExecutor: apiToken is required");
@@ -201,6 +368,8 @@ export class AgentExecutor {
201
368
  this.agentsMd = new AgentsMdClient(this.#http);
202
369
  this.files = new FilesClient(this.#http);
203
370
  this.secrets = new SecretsClient(this.#http);
371
+ this.chat = new ChatClient(this.#http, (options) => this.#buildSessionCreateRequest(options));
372
+ this.sessions = this.chat;
204
373
  }
205
374
  /**
206
375
  * Internal: satisfies the `SecretUploader` surface so a
@@ -227,57 +396,78 @@ export class AgentExecutor {
227
396
  });
228
397
  }
229
398
  /**
230
- * Submit a run, wait until its RECORD is terminal, and collect the full
231
- * {@link RunResult} the settle-consistent "do it and give me the result"
232
- * primitive. Folds the poll loop every consumer hand-rolled into one call:
233
- * submit {@link waitForRun} (polls `getRun`, NOT the earlier RUN_FINISHED
234
- * event) poll `listEvents` until the snapshot is settle-bracketed
235
- * (RUN_STARTED + a terminal event present) → `listOutputs` → decode the trace
236
- * and assistant text. On resolve, `getRun`/`listOutputs` are guaranteed
237
- * consistent.
238
- *
239
- * Uses polling (portable across backends), NOT the coordinator WebSocket. By
240
- * default a failed run resolves with `ok: false` and a populated `error`; pass
241
- * `{ throwOnFailure: true }` to throw instead. For live events prefer `submit`
242
- * + `streamEnvelopes(runId, { settleConsistent: true })`.
399
+ * Convenience one-shot on top of the canonical session API:
400
+ * open a session, send `message` as the first turn, stream until the session
401
+ * parks (`idle` / `suspended` / `error`), then return the collected text,
402
+ * events, outputs, and session record. The returned `runId` is the session id,
403
+ * so callers can resume later with `openSession(runId)`.
243
404
  */
244
405
  async run(options, opts = {}) {
245
- const signal = opts.signal ?? options.signal;
246
- const runId = await this.submit(options);
247
- const run = await this.waitForRun(runId, {
248
- ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
249
- ...(signal ? { signal } : {})
250
- });
251
- const events = await this.#collectSettledEvents(runId, signal);
252
- const outputs = await this.listOutputs(runId);
253
- const ok = run.status === "succeeded";
254
- const costUsd = run.costTelemetry?.billedCostUsd;
255
- const errorMessage = typeof run.errorMessage === "string" && run.errorMessage ? run.errorMessage : undefined;
256
- const result = {
257
- runId,
258
- run,
259
- status: run.status,
260
- ok,
261
- text: textOf(events),
262
- events,
263
- trace: summarizeRunTrace(events),
264
- outputs,
265
- ...(run.usage ? { usage: run.usage } : {}),
266
- ...(typeof costUsd === "number" ? { costUsd } : {}),
267
- ...(!ok && errorMessage ? { error: errorMessage } : {})
268
- };
269
- if (opts.throwOnFailure && !ok) {
270
- throw new RunStateError(`AgentExecutor.run: run ${runId} ended ${run.status}${errorMessage ? `: ${errorMessage}` : ""}`, { runId, status: run.status });
406
+ const scopedSignal = scopedAbortSignal(opts.timeoutMs);
407
+ try {
408
+ const { message, deleteAfter, messageIdempotencyKey, stream, ...createOptions } = options;
409
+ assertNoLegacySessionFields(options, "Aex.run");
410
+ const input = normaliseSessionInput(message, "Aex.run", "message");
411
+ assertNoSessionSendSignal(stream, "Aex.run stream");
412
+ const streamOptions = {
413
+ ...(stream ?? {}),
414
+ ...(scopedSignal?.signal ? { signal: scopedSignal.signal } : {}),
415
+ ...(opts.webSocketFactory ? { webSocketFactory: opts.webSocketFactory } : {}),
416
+ ...(opts.idleTimeoutMs !== undefined ? { idleTimeoutMs: opts.idleTimeoutMs } : {}),
417
+ ...(opts.pingIntervalMs !== undefined ? { pingIntervalMs: opts.pingIntervalMs } : {})
418
+ };
419
+ const session = await this.sessions.create(createOptions);
420
+ const turnResult = await sendSessionInternal(session, input, {
421
+ ...streamOptions,
422
+ idempotencyKey: messageIdempotencyKey ?? generateIdempotencyKey()
423
+ }).done();
424
+ if (deleteAfter) {
425
+ await session.delete();
426
+ }
427
+ const runId = turnResult.sessionId;
428
+ const run = sessionToRun(turnResult.session);
429
+ const events = turnResult.events;
430
+ const outputs = turnResult.outputs;
431
+ const ok = turnResult.status === "idle" || turnResult.status === "suspended";
432
+ const costUsd = typeof turnResult.session.costUsd === "number" ? turnResult.session.costUsd : undefined;
433
+ const errorMessage = typeof turnResult.session.errorMessage === "string" && turnResult.session.errorMessage ? turnResult.session.errorMessage : undefined;
434
+ const result = {
435
+ runId,
436
+ run,
437
+ sessionId: runId,
438
+ session: turnResult.session,
439
+ turn: turnResult.turn,
440
+ status: turnResult.status,
441
+ ok,
442
+ text: turnResult.text,
443
+ events,
444
+ trace: summarizeRunTrace(events),
445
+ outputs,
446
+ ...(turnResult.session.usage ? { usage: turnResult.session.usage } : {}),
447
+ ...(typeof costUsd === "number" ? { costUsd } : {}),
448
+ ...(!ok && errorMessage ? { error: errorMessage } : {})
449
+ };
450
+ if (opts.throwOnFailure && !ok) {
451
+ throw new RunStateError(`AgentExecutor.run: session ${runId} ended ${turnResult.status}${errorMessage ? `: ${errorMessage}` : ""}`, { runId, status: turnResult.status });
452
+ }
453
+ return result;
454
+ }
455
+ finally {
456
+ scopedSignal?.clear();
271
457
  }
272
- return result;
273
458
  }
274
459
  /**
275
- * Explicit, discoverable alias for {@link run}: submit, wait, and collect the
276
- * full {@link RunResult} in one call.
460
+ * Explicit, discoverable alias for {@link run}: open a one-shot session turn
461
+ * and collect the full {@link RunResult} in one call.
277
462
  */
278
463
  runAndCollect(options, opts) {
279
464
  return this.run(options, opts);
280
465
  }
466
+ openSession(optionsOrId) {
467
+ return typeof optionsOrId === "string"
468
+ ? this.sessions.open(optionsOrId)
469
+ : this.sessions.create(optionsOrId);
470
+ }
281
471
  /**
282
472
  * Poll `listEvents` until the snapshot is settle-bracketed — both a
283
473
  * RUN_STARTED and a terminal (RUN_FINISHED / RUN_ERROR) event present — then
@@ -328,7 +518,7 @@ export class AgentExecutor {
328
518
  if (!options || typeof options !== "object") {
329
519
  throw new RunConfigValidationError("AgentExecutor.submit: options is required");
330
520
  }
331
- assertNoRemovedSubmitFields(options);
521
+ assertNoRemovedSubmitFields(options, "AgentExecutor.submit");
332
522
  // A model maps to one or more upstream providers (see MODEL_PROVIDER_IDS).
333
523
  // `providersForModel` returns the supported providers in priority order, or
334
524
  // `[]` for an unknown model string (the model check below then rejects it).
@@ -342,11 +532,11 @@ export class AgentExecutor {
342
532
  `model ${JSON.stringify(options.model)} (supported: ${supportedProviders.join(", ")})`);
343
533
  }
344
534
  const provider = options.provider ?? supportedProviders[0] ?? DEFAULT_RUN_PROVIDER;
345
- validateSubmitCredentials(options, provider);
535
+ validateSubmitCredentials(options, provider, "AgentExecutor.submit");
346
536
  if (typeof options.model !== "string" || !options.model) {
347
537
  throw new RunConfigValidationError("AgentExecutor.submit: model is required");
348
538
  }
349
- const prompt = normalisePrompt(options.prompt);
539
+ const prompt = normalisePrompt(options.prompt, "AgentExecutor.submit", "prompt");
350
540
  const { endpoints: proxyEndpointDeclarations, auth: proxyEndpointAuthFromInstances } = splitProxyEndpoints(options.proxyEndpoints ?? []);
351
541
  const mergedProxyAuth = mergeProxyEndpointAuth(proxyEndpointAuthFromInstances, options.secrets?.proxyEndpointAuth ?? []);
352
542
  // Split secretEnv into value-free declarations (hashed submission) and
@@ -416,7 +606,6 @@ export class AgentExecutor {
416
606
  ...(mergedProxyAuth.length > 0 ? { proxyEndpointAuth: mergedProxyAuth } : {}),
417
607
  ...(Object.keys(envSecretValues).length > 0 ? { envSecrets: envSecretValues } : {})
418
608
  };
419
- const postHook = postHookForWire(options.postHook);
420
609
  const request = {
421
610
  idempotencyKey: options.idempotencyKey ?? generateIdempotencyKey(),
422
611
  // Always include `provider` on the wire so dashboard / proxy
@@ -427,7 +616,6 @@ export class AgentExecutor {
427
616
  submission,
428
617
  ...(options.runtimeSize ? { runtimeSize: options.runtimeSize } : {}),
429
618
  ...(options.timeout ? { timeout: options.timeout } : {}),
430
- ...(postHook ? { postHook } : {}),
431
619
  ...(options.parentRunId ? { parentRunId: options.parentRunId } : {}),
432
620
  // Operational/delivery concern — sibling of idempotencyKey, NOT part of
433
621
  // the hashed brief. The idempotency key here is randomly generated, so
@@ -445,6 +633,80 @@ export class AgentExecutor {
445
633
  const run = await operations.submitRun(this.#http, request);
446
634
  return getSubmittedRunId(run);
447
635
  }
636
+ async #buildSessionCreateRequest(options) {
637
+ if (!options || typeof options !== "object") {
638
+ throw new RunConfigValidationError("Aex.openSession: options is required");
639
+ }
640
+ assertNoLegacySessionFields(options, "Aex.openSession");
641
+ const supportedProviders = providersForModel(options.model);
642
+ if (options.provider &&
643
+ supportedProviders.length > 0 &&
644
+ !supportedProviders.includes(options.provider)) {
645
+ throw new RunConfigValidationError(`Aex.openSession: provider ${JSON.stringify(options.provider)} is not available for ` +
646
+ `model ${JSON.stringify(options.model)} (supported: ${supportedProviders.join(", ")})`);
647
+ }
648
+ const provider = options.provider ?? supportedProviders[0] ?? DEFAULT_RUN_PROVIDER;
649
+ validateApiKeys(options.apiKeys, provider, "Aex.openSession");
650
+ if (typeof options.model !== "string" || !options.model) {
651
+ throw new RunConfigValidationError("Aex.openSession: model is required");
652
+ }
653
+ const { endpoints: proxyEndpointDeclarations, auth: proxyEndpointAuthFromInstances } = splitProxyEndpoints(options.proxyEndpoints ?? []);
654
+ const mergedProxyAuth = mergeProxyEndpointAuth(proxyEndpointAuthFromInstances, []);
655
+ const { declarations: secretEnvDeclarations, values: envSecretValues } = splitSecretEnv(options.environment?.secrets);
656
+ let limits;
657
+ try {
658
+ limits = parseRunLimits(options.overrides?.maxSpendUsd === undefined
659
+ ? undefined
660
+ : { maxSpendUsd: options.overrides.maxSpendUsd });
661
+ }
662
+ catch (err) {
663
+ throw new AexError("RUN_CONFIG_INVALID", `Aex.openSession: ${err instanceof Error ? err.message : String(err)}`);
664
+ }
665
+ const uploader = (args) => this._uploadAsset(args);
666
+ const preparedSkills = await prepareSkills(options.skills ?? [], uploader);
667
+ const preparedTools = await prepareTools(options.tools ?? [], uploader);
668
+ const preparedAgentsMd = await prepareAgentsMd(options.agentsMd ?? [], uploader);
669
+ const preparedFiles = await prepareFiles(options.files ?? [], uploader);
670
+ const { submissionMcpServers, mergedMcpSecrets } = mergeMcpServers(options.mcpServers ?? [], []);
671
+ const outputCapture = outputsForWire(options.outputs);
672
+ const environment = sessionEnvironmentForWire(options.environment);
673
+ const submission = {
674
+ model: options.model,
675
+ ...(options.system ? { system: options.system } : {}),
676
+ skills: preparedSkills,
677
+ tools: [...preparedTools.builtinNames, ...preparedTools.refs],
678
+ agentsMd: preparedAgentsMd,
679
+ files: preparedFiles,
680
+ mcpServers: submissionMcpServers,
681
+ ...(Object.keys(secretEnvDeclarations).length > 0 ? { secretEnv: secretEnvDeclarations } : {}),
682
+ ...(environment ? { environment: environment } : {}),
683
+ ...(options.metadata ? { metadata: options.metadata } : {}),
684
+ ...(outputCapture ? { outputs: outputCapture } : {}),
685
+ ...(options.includeBuiltinTools !== undefined
686
+ ? { includeBuiltinTools: options.includeBuiltinTools }
687
+ : {}),
688
+ ...(options.outputMode !== undefined ? { outputMode: options.outputMode } : {})
689
+ };
690
+ const secrets = {
691
+ ...(options.apiKeys ? { apiKeys: options.apiKeys } : {}),
692
+ ...(mergedMcpSecrets.length > 0 ? { mcpServers: mergedMcpSecrets } : {}),
693
+ ...(mergedProxyAuth.length > 0 ? { proxyEndpointAuth: mergedProxyAuth } : {}),
694
+ ...(Object.keys(envSecretValues).length > 0 ? { envSecrets: envSecretValues } : {})
695
+ };
696
+ const retention = sessionRetentionForWire(options);
697
+ return {
698
+ provider,
699
+ submission,
700
+ ...(options.runtime ? { runtimeSize: options.runtime } : {}),
701
+ ...(options.overrides?.timeout ? { timeout: options.overrides.timeout } : {}),
702
+ ...(limits ? { limits } : {}),
703
+ retention,
704
+ secrets,
705
+ ...(proxyEndpointDeclarations.length > 0
706
+ ? { proxyEndpoints: proxyEndpointDeclarations }
707
+ : {})
708
+ };
709
+ }
448
710
  getRun(runId) {
449
711
  return operations.getRun(this.#http, runId);
450
712
  }
@@ -743,6 +1005,9 @@ export class AgentExecutor {
743
1005
  return writeOptionalFile(await operations.downloadMetadata(this.#http, runId), options?.to);
744
1006
  }
745
1007
  }
1008
+ /** Canonical SDK client name. `AgentExecutor` remains as a compatibility alias. */
1009
+ export class Aex extends AgentExecutor {
1010
+ }
746
1011
  // `Run.status` is a loose `string` on the wire shape, so we membership-test
747
1012
  // against the canonical terminal set rather than re-deriving one (which is how
748
1013
  // `timed_out` got dropped from the old hardcoded list).
@@ -750,6 +1015,31 @@ const TERMINAL_STATUSES = new Set(TERMINAL_RUN_STATUSES);
750
1015
  function isTerminal(status) {
751
1016
  return typeof status === "string" && TERMINAL_STATUSES.has(status);
752
1017
  }
1018
+ function sessionToRun(session) {
1019
+ const id = session.sessionId ?? session.id;
1020
+ return {
1021
+ id,
1022
+ status: String(session.status),
1023
+ ...(typeof session.workspaceId === "string" ? { workspaceId: session.workspaceId } : {}),
1024
+ ...(typeof session.createdAt === "string" ? { createdAt: session.createdAt } : {}),
1025
+ ...(typeof session.updatedAt === "string" ? { updatedAt: session.updatedAt } : {}),
1026
+ ...(session.errorMessage !== undefined ? { errorMessage: session.errorMessage } : {}),
1027
+ ...(session.usage ? { usage: session.usage } : {})
1028
+ };
1029
+ }
1030
+ function scopedAbortSignal(timeoutMs) {
1031
+ if (timeoutMs === undefined) {
1032
+ return undefined;
1033
+ }
1034
+ const controller = new AbortController();
1035
+ const timer = setTimeout(() => controller.abort(), Math.max(0, timeoutMs));
1036
+ return {
1037
+ signal: controller.signal,
1038
+ clear() {
1039
+ clearTimeout(timer);
1040
+ }
1041
+ };
1042
+ }
753
1043
  /** Escape a literal string for safe interpolation into a RegExp. */
754
1044
  function escapeRegExp(input) {
755
1045
  return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -830,49 +1120,104 @@ function generateIdempotencyKey() {
830
1120
  return cryptoObj.randomUUID();
831
1121
  return `idem-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
832
1122
  }
833
- function normalisePrompt(input) {
1123
+ function normalisePrompt(input, surface = "AgentExecutor.submit", field = "prompt") {
834
1124
  if (typeof input === "string") {
835
1125
  if (!input) {
836
- throw new RunConfigValidationError("AgentExecutor.submit: prompt must be a non-empty string");
1126
+ throw new RunConfigValidationError(`${surface}: ${field} must be a non-empty string`);
837
1127
  }
838
1128
  return [input];
839
1129
  }
840
1130
  if (!Array.isArray(input) || input.length === 0) {
841
- throw new RunConfigValidationError("AgentExecutor.submit: prompt must be a non-empty string or string array");
1131
+ throw new RunConfigValidationError(`${surface}: ${field} must be a non-empty string or string array`);
842
1132
  }
843
1133
  for (const segment of input) {
844
1134
  if (typeof segment !== "string" || !segment) {
845
- throw new RunConfigValidationError("AgentExecutor.submit: prompt segments must be non-empty strings");
1135
+ throw new RunConfigValidationError(`${surface}: ${field} segments must be non-empty strings`);
846
1136
  }
847
1137
  }
848
1138
  return [...input];
849
1139
  }
850
- function assertNoRemovedSubmitFields(options) {
1140
+ function normaliseSessionInput(input, surface, field) {
1141
+ if (typeof input === "string") {
1142
+ if (!input) {
1143
+ throw new RunConfigValidationError(`${surface}: ${field} must be a non-empty string`);
1144
+ }
1145
+ return input;
1146
+ }
1147
+ if (!Array.isArray(input) || input.length === 0) {
1148
+ throw new RunConfigValidationError(`${surface}: ${field} must be a non-empty string or string array`);
1149
+ }
1150
+ for (const segment of input) {
1151
+ if (typeof segment !== "string" || !segment) {
1152
+ throw new RunConfigValidationError(`${surface}: ${field} segments must be non-empty strings`);
1153
+ }
1154
+ }
1155
+ return [...input];
1156
+ }
1157
+ function assertNoRemovedSubmitFields(options, surface, extraFields = []) {
851
1158
  const record = options;
852
- for (const field of ["credentialMode", "runtime", "region", "apiKey", "credentials"]) {
1159
+ for (const field of ["credentialMode", "runtime", "region", "apiKey", "credentials", "postHook", ...extraFields]) {
853
1160
  if (Object.prototype.hasOwnProperty.call(record, field)) {
854
- throw new RunConfigValidationError(`AgentExecutor.submit: ${field} is not a supported option; use the managed path with secrets.apiKeys[provider].`);
1161
+ throw new RunConfigValidationError(`${surface}: ${field} is not a supported option; use the managed path with secrets.apiKeys[provider].`);
855
1162
  }
856
1163
  }
857
1164
  const secrets = record.secrets;
858
1165
  if (secrets && typeof secrets === "object" && !Array.isArray(secrets) && Object.prototype.hasOwnProperty.call(secrets, "apiKey")) {
859
- throw new RunConfigValidationError("AgentExecutor.submit: secrets.apiKey is not supported; use secrets.apiKeys[provider].");
1166
+ throw new RunConfigValidationError(`${surface}: secrets.apiKey is not supported; use secrets.apiKeys[provider].`);
860
1167
  }
861
1168
  }
862
- function validateSubmitCredentials(options, provider) {
1169
+ function assertNoLegacySessionFields(options, surface) {
1170
+ const record = options;
1171
+ const messages = {
1172
+ input: "send user messages with session.send(...) or use run({ message }).",
1173
+ prompt: "use message for one-shot run input or session.send(...) for follow-up messages.",
1174
+ instructions: "use system.",
1175
+ idleSuspendAfter: "use overrides.idleTtl.",
1176
+ idleTtl: "use overrides.idleTtl.",
1177
+ retention: "use overrides.idleTtl.",
1178
+ secretEnv: "use environment.secrets.",
1179
+ secrets: "use top-level apiKeys for provider keys and environment.secrets for run secrets.",
1180
+ runtimeSize: "use runtime.",
1181
+ parentRunId: "subagents are session-internal; parentRunId is not part of the session API.",
1182
+ limits: "use overrides.",
1183
+ timeout: "use overrides.timeout.",
1184
+ signal: "use session.cancel() / session.suspend() for remote control.",
1185
+ postHook: "send a follow-up validation message when the session returns idle.",
1186
+ webhook: "send a follow-up validation message instead of a submit webhook."
1187
+ };
1188
+ for (const [field, message] of Object.entries(messages)) {
1189
+ if (Object.prototype.hasOwnProperty.call(record, field)) {
1190
+ throw new RunConfigValidationError(`${surface}: ${field} is not a supported option; ${message}`);
1191
+ }
1192
+ }
1193
+ const overrides = record.overrides;
1194
+ if (overrides && typeof overrides === "object" && !Array.isArray(overrides)) {
1195
+ const overrideRecord = overrides;
1196
+ if (Object.prototype.hasOwnProperty.call(overrideRecord, "idleSuspendAfter")) {
1197
+ throw new RunConfigValidationError(`${surface}: overrides.idleSuspendAfter is not a supported option; use overrides.idleTtl.`);
1198
+ }
1199
+ }
1200
+ }
1201
+ function assertNoSessionSendSignal(options, surface) {
1202
+ const record = options;
1203
+ if (record && typeof record === "object" && Object.prototype.hasOwnProperty.call(record, "signal")) {
1204
+ throw new RunConfigValidationError(`${surface}: signal is not a supported option; use session.cancel() / session.suspend() for remote control.`);
1205
+ }
1206
+ }
1207
+ function validateSubmitCredentials(options, provider, surface) {
863
1208
  if (options.parentRunId) {
864
1209
  return;
865
1210
  }
866
1211
  const key = options.secrets?.apiKeys?.[provider];
867
1212
  if (typeof key !== "string" || key.length === 0) {
868
- throw new RunConfigValidationError(`AgentExecutor.submit: a provider API key is required — pass secrets.apiKeys[${JSON.stringify(provider)}].`);
1213
+ throw new RunConfigValidationError(`${surface}: a provider API key is required — pass secrets.apiKeys[${JSON.stringify(provider)}].`);
869
1214
  }
870
1215
  }
871
- function postHookForWire(input) {
872
- if (input === undefined || typeof input.command !== "string" || input.command.trim().length === 0) {
873
- return undefined;
1216
+ function validateApiKeys(apiKeys, provider, surface) {
1217
+ const key = apiKeys?.[provider];
1218
+ if (typeof key !== "string" || key.length === 0) {
1219
+ throw new RunConfigValidationError(`${surface}: a provider API key is required — pass apiKeys[${JSON.stringify(provider)}].`);
874
1220
  }
875
- return input;
876
1221
  }
877
1222
  function outputsForWire(outputs) {
878
1223
  if (outputs === undefined) {
@@ -896,6 +1241,24 @@ function outputsForWire(outputs) {
896
1241
  ...(outputs.maxFiles !== undefined ? { maxFiles: outputs.maxFiles } : {})
897
1242
  };
898
1243
  }
1244
+ const DEFAULT_SESSION_IDLE_TTL = "3m";
1245
+ function sessionRetentionForWire(options) {
1246
+ return {
1247
+ idleTtl: options.overrides?.idleTtl ?? DEFAULT_SESSION_IDLE_TTL
1248
+ };
1249
+ }
1250
+ function sessionEnvironmentForWire(environment) {
1251
+ if (environment === undefined) {
1252
+ return undefined;
1253
+ }
1254
+ const { variables, secrets: _secrets, ...rest } = environment;
1255
+ void _secrets;
1256
+ const out = {
1257
+ ...rest,
1258
+ ...(variables !== undefined ? { envVars: variables } : {})
1259
+ };
1260
+ return Object.keys(out).length === 0 ? undefined : out;
1261
+ }
899
1262
  /**
900
1263
  * Resolve a draft's asset id: reuse the cached id from a prior submit, otherwise
901
1264
  * upload the bytes and cache the result on the instance.