@aexhq/sdk 0.32.0 → 0.33.1
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/README.md +29 -31
- package/dist/_contracts/event-stream-client.d.ts +2 -2
- package/dist/_contracts/event-stream-client.js +3 -3
- package/dist/_contracts/index.d.ts +0 -1
- package/dist/_contracts/index.js +0 -1
- package/dist/_contracts/operations.d.ts +15 -1
- package/dist/_contracts/operations.js +79 -0
- package/dist/_contracts/post-hook.d.ts +4 -4
- package/dist/_contracts/post-hook.js +1 -1
- package/dist/_contracts/run-config.d.ts +0 -4
- package/dist/_contracts/run-config.js +0 -7
- package/dist/_contracts/runtime-types.d.ts +86 -0
- package/dist/_contracts/status.d.ts +3 -1
- package/dist/_contracts/status.js +17 -0
- package/dist/_contracts/submission.d.ts +1 -10
- package/dist/_contracts/submission.js +0 -4
- package/dist/cli.mjs +110 -97
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +116 -34
- package/dist/client.js +453 -62
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/defaults.md +0 -2
- package/docs/events.md +32 -13
- package/docs/limits.md +4 -3
- package/docs/public-surface.json +14 -9
- package/docs/quickstart.md +36 -11
- package/docs/run-config.md +1 -6
- package/package.json +1 -1
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,199 @@ 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
|
+
const terminalStatus = terminalSessionStatusFromEvents(events, turn.turnSeq);
|
|
71
|
+
const readSession = await operations.getSession(this.#http, this.id).catch(() => this.#session);
|
|
72
|
+
this.#session = withTerminalSessionStatus(readSession, terminalStatus);
|
|
73
|
+
const outputs = await operations.listSessionOutputs(this.#http, this.id).catch(() => []);
|
|
74
|
+
return {
|
|
75
|
+
sessionId: this.id,
|
|
76
|
+
session: this.#session,
|
|
77
|
+
turn,
|
|
78
|
+
status: this.#session.status,
|
|
79
|
+
text: textOf(events),
|
|
80
|
+
events,
|
|
81
|
+
outputs
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async suspend(options = {}) {
|
|
85
|
+
const accepted = await operations.suspendSession(this.#http, this.id, options);
|
|
86
|
+
this.#session = accepted.session;
|
|
87
|
+
return accepted;
|
|
88
|
+
}
|
|
89
|
+
async cancel(options = {}) {
|
|
90
|
+
const accepted = await operations.cancelSession(this.#http, this.id, options);
|
|
91
|
+
this.#session = accepted.session;
|
|
92
|
+
return accepted;
|
|
93
|
+
}
|
|
94
|
+
async resume(options = {}) {
|
|
95
|
+
const accepted = await operations.resumeSession(this.#http, this.id, options);
|
|
96
|
+
this.#session = accepted.session;
|
|
97
|
+
return accepted;
|
|
98
|
+
}
|
|
99
|
+
async delete(options = {}) {
|
|
100
|
+
const accepted = await operations.deleteSession(this.#http, this.id, options);
|
|
101
|
+
if (accepted && typeof accepted === "object" && "session" in accepted) {
|
|
102
|
+
this.#session = accepted.session;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
listEvents() {
|
|
106
|
+
return operations.listSessionEvents(this.#http, this.id);
|
|
107
|
+
}
|
|
108
|
+
listOutputs(query) {
|
|
109
|
+
return operations.listSessionOutputs(this.#http, this.id, query);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export const ChatSession = SessionHandle;
|
|
113
|
+
export class SessionClient {
|
|
114
|
+
#http;
|
|
115
|
+
#buildCreateRequest;
|
|
116
|
+
constructor(http, buildCreateRequest) {
|
|
117
|
+
this.#http = http;
|
|
118
|
+
this.#buildCreateRequest = buildCreateRequest;
|
|
119
|
+
}
|
|
120
|
+
async create(options) {
|
|
121
|
+
const request = await this.#buildCreateRequest(options);
|
|
122
|
+
const session = await operations.createSession(this.#http, request, { idempotencyKey: options.idempotencyKey ?? generateIdempotencyKey() });
|
|
123
|
+
return new SessionHandle(this.#http, session);
|
|
124
|
+
}
|
|
125
|
+
async open(sessionId) {
|
|
126
|
+
return new SessionHandle(this.#http, await operations.getSession(this.#http, sessionId));
|
|
127
|
+
}
|
|
128
|
+
get(sessionId) {
|
|
129
|
+
return operations.getSession(this.#http, sessionId);
|
|
130
|
+
}
|
|
131
|
+
list(query) {
|
|
132
|
+
return operations.listSessions(this.#http, query);
|
|
133
|
+
}
|
|
134
|
+
async run(options) {
|
|
135
|
+
const { message, deleteAfter, messageIdempotencyKey, stream, ...createOptions } = options;
|
|
136
|
+
assertNoLegacySessionFields(options, "Aex.sessions.run");
|
|
137
|
+
const input = normaliseSessionInput(message, "Aex.sessions.run", "message");
|
|
138
|
+
const session = await this.create(createOptions);
|
|
139
|
+
const result = await session.send(input, {
|
|
140
|
+
...(stream ?? {}),
|
|
141
|
+
idempotencyKey: messageIdempotencyKey ?? generateIdempotencyKey()
|
|
142
|
+
}).done();
|
|
143
|
+
if (deleteAfter) {
|
|
144
|
+
await session.delete();
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
export const ChatClient = SessionClient;
|
|
150
|
+
async function* streamSessionTurnEvents(http, sessionId, turn, options) {
|
|
151
|
+
const first = await operations.getSessionCoordinatorTicket(http, sessionId);
|
|
152
|
+
yield* streamCoordinatorEvents({
|
|
153
|
+
wsUrl: first.wsUrl,
|
|
154
|
+
from: options.from ?? 0,
|
|
155
|
+
fetchTicket: async () => (await operations.getSessionCoordinatorTicket(http, sessionId)).ticket,
|
|
156
|
+
isTerminal: (event) => isSessionTurnTerminalEvent(event, turn.turnSeq),
|
|
157
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
158
|
+
...(options.webSocketFactory ? { webSocketFactory: options.webSocketFactory } : {}),
|
|
159
|
+
...(options.idleTimeoutMs !== undefined ? { idleTimeoutMs: options.idleTimeoutMs } : {}),
|
|
160
|
+
...(options.pingIntervalMs !== undefined ? { pingIntervalMs: options.pingIntervalMs } : {})
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
function isSessionTurnTerminalEvent(event, turnSeq) {
|
|
164
|
+
const name = customName(event);
|
|
165
|
+
if (name !== "aex.session.idle" &&
|
|
166
|
+
name !== "aex.session.suspended" &&
|
|
167
|
+
name !== "aex.session.error") {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
const value = event.data.value;
|
|
171
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
const eventTurnSeq = value.turnSeq;
|
|
175
|
+
return typeof eventTurnSeq !== "number" || eventTurnSeq === turnSeq;
|
|
176
|
+
}
|
|
177
|
+
function terminalSessionStatusFromEvents(events, turnSeq) {
|
|
178
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
179
|
+
const event = events[i];
|
|
180
|
+
if (!isSessionTurnTerminalEvent(event, turnSeq))
|
|
181
|
+
continue;
|
|
182
|
+
const name = customName(event);
|
|
183
|
+
if (name === "aex.session.idle")
|
|
184
|
+
return "idle";
|
|
185
|
+
if (name === "aex.session.suspended")
|
|
186
|
+
return "suspended";
|
|
187
|
+
if (name === "aex.session.error")
|
|
188
|
+
return "error";
|
|
189
|
+
}
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
function withTerminalSessionStatus(session, terminalStatus) {
|
|
193
|
+
if (terminalStatus === undefined || session.status === terminalStatus)
|
|
194
|
+
return session;
|
|
195
|
+
if (session.status !== "creating" &&
|
|
196
|
+
session.status !== "running" &&
|
|
197
|
+
session.status !== "suspending" &&
|
|
198
|
+
session.status !== "cancelling") {
|
|
199
|
+
return session;
|
|
200
|
+
}
|
|
201
|
+
return { ...session, status: terminalStatus };
|
|
202
|
+
}
|
|
10
203
|
/**
|
|
11
204
|
* Workspace skill admin operations exposed under `client.skills`.
|
|
12
205
|
*
|
|
@@ -181,6 +374,8 @@ export class AgentExecutor {
|
|
|
181
374
|
agentsMd;
|
|
182
375
|
files;
|
|
183
376
|
secrets;
|
|
377
|
+
sessions;
|
|
378
|
+
chat;
|
|
184
379
|
constructor(options) {
|
|
185
380
|
if (!options.apiToken) {
|
|
186
381
|
throw new Error("AgentExecutor: apiToken is required");
|
|
@@ -201,6 +396,8 @@ export class AgentExecutor {
|
|
|
201
396
|
this.agentsMd = new AgentsMdClient(this.#http);
|
|
202
397
|
this.files = new FilesClient(this.#http);
|
|
203
398
|
this.secrets = new SecretsClient(this.#http);
|
|
399
|
+
this.chat = new ChatClient(this.#http, (options) => this.#buildSessionCreateRequest(options));
|
|
400
|
+
this.sessions = this.chat;
|
|
204
401
|
}
|
|
205
402
|
/**
|
|
206
403
|
* Internal: satisfies the `SecretUploader` surface so a
|
|
@@ -227,57 +424,78 @@ export class AgentExecutor {
|
|
|
227
424
|
});
|
|
228
425
|
}
|
|
229
426
|
/**
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
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 })`.
|
|
427
|
+
* Convenience one-shot on top of the canonical session API:
|
|
428
|
+
* open a session, send `message` as the first turn, stream until the session
|
|
429
|
+
* parks (`idle` / `suspended` / `error`), then return the collected text,
|
|
430
|
+
* events, outputs, and session record. The returned `runId` is the session id,
|
|
431
|
+
* so callers can resume later with `openSession(runId)`.
|
|
243
432
|
*/
|
|
244
433
|
async run(options, opts = {}) {
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
434
|
+
const scopedSignal = scopedAbortSignal(opts.timeoutMs);
|
|
435
|
+
try {
|
|
436
|
+
const { message, deleteAfter, messageIdempotencyKey, stream, ...createOptions } = options;
|
|
437
|
+
assertNoLegacySessionFields(options, "Aex.run");
|
|
438
|
+
const input = normaliseSessionInput(message, "Aex.run", "message");
|
|
439
|
+
assertNoSessionSendSignal(stream, "Aex.run stream");
|
|
440
|
+
const streamOptions = {
|
|
441
|
+
...(stream ?? {}),
|
|
442
|
+
...(scopedSignal?.signal ? { signal: scopedSignal.signal } : {}),
|
|
443
|
+
...(opts.webSocketFactory ? { webSocketFactory: opts.webSocketFactory } : {}),
|
|
444
|
+
...(opts.idleTimeoutMs !== undefined ? { idleTimeoutMs: opts.idleTimeoutMs } : {}),
|
|
445
|
+
...(opts.pingIntervalMs !== undefined ? { pingIntervalMs: opts.pingIntervalMs } : {})
|
|
446
|
+
};
|
|
447
|
+
const session = await this.sessions.create(createOptions);
|
|
448
|
+
const turnResult = await sendSessionInternal(session, input, {
|
|
449
|
+
...streamOptions,
|
|
450
|
+
idempotencyKey: messageIdempotencyKey ?? generateIdempotencyKey()
|
|
451
|
+
}).done();
|
|
452
|
+
if (deleteAfter) {
|
|
453
|
+
await session.delete();
|
|
454
|
+
}
|
|
455
|
+
const runId = turnResult.sessionId;
|
|
456
|
+
const run = sessionToRun(turnResult.session);
|
|
457
|
+
const events = turnResult.events;
|
|
458
|
+
const outputs = turnResult.outputs;
|
|
459
|
+
const ok = turnResult.status === "idle" || turnResult.status === "suspended";
|
|
460
|
+
const costUsd = typeof turnResult.session.costUsd === "number" ? turnResult.session.costUsd : undefined;
|
|
461
|
+
const errorMessage = typeof turnResult.session.errorMessage === "string" && turnResult.session.errorMessage ? turnResult.session.errorMessage : undefined;
|
|
462
|
+
const result = {
|
|
463
|
+
runId,
|
|
464
|
+
run,
|
|
465
|
+
sessionId: runId,
|
|
466
|
+
session: turnResult.session,
|
|
467
|
+
turn: turnResult.turn,
|
|
468
|
+
status: turnResult.status,
|
|
469
|
+
ok,
|
|
470
|
+
text: turnResult.text,
|
|
471
|
+
events,
|
|
472
|
+
trace: summarizeRunTrace(events),
|
|
473
|
+
outputs,
|
|
474
|
+
...(turnResult.session.usage ? { usage: turnResult.session.usage } : {}),
|
|
475
|
+
...(typeof costUsd === "number" ? { costUsd } : {}),
|
|
476
|
+
...(!ok && errorMessage ? { error: errorMessage } : {})
|
|
477
|
+
};
|
|
478
|
+
if (opts.throwOnFailure && !ok) {
|
|
479
|
+
throw new RunStateError(`AgentExecutor.run: session ${runId} ended ${turnResult.status}${errorMessage ? `: ${errorMessage}` : ""}`, { runId, status: turnResult.status });
|
|
480
|
+
}
|
|
481
|
+
return result;
|
|
482
|
+
}
|
|
483
|
+
finally {
|
|
484
|
+
scopedSignal?.clear();
|
|
271
485
|
}
|
|
272
|
-
return result;
|
|
273
486
|
}
|
|
274
487
|
/**
|
|
275
|
-
* Explicit, discoverable alias for {@link run}:
|
|
276
|
-
* full {@link RunResult} in one call.
|
|
488
|
+
* Explicit, discoverable alias for {@link run}: open a one-shot session turn
|
|
489
|
+
* and collect the full {@link RunResult} in one call.
|
|
277
490
|
*/
|
|
278
491
|
runAndCollect(options, opts) {
|
|
279
492
|
return this.run(options, opts);
|
|
280
493
|
}
|
|
494
|
+
openSession(optionsOrId) {
|
|
495
|
+
return typeof optionsOrId === "string"
|
|
496
|
+
? this.sessions.open(optionsOrId)
|
|
497
|
+
: this.sessions.create(optionsOrId);
|
|
498
|
+
}
|
|
281
499
|
/**
|
|
282
500
|
* Poll `listEvents` until the snapshot is settle-bracketed — both a
|
|
283
501
|
* RUN_STARTED and a terminal (RUN_FINISHED / RUN_ERROR) event present — then
|
|
@@ -328,7 +546,7 @@ export class AgentExecutor {
|
|
|
328
546
|
if (!options || typeof options !== "object") {
|
|
329
547
|
throw new RunConfigValidationError("AgentExecutor.submit: options is required");
|
|
330
548
|
}
|
|
331
|
-
assertNoRemovedSubmitFields(options);
|
|
549
|
+
assertNoRemovedSubmitFields(options, "AgentExecutor.submit");
|
|
332
550
|
// A model maps to one or more upstream providers (see MODEL_PROVIDER_IDS).
|
|
333
551
|
// `providersForModel` returns the supported providers in priority order, or
|
|
334
552
|
// `[]` for an unknown model string (the model check below then rejects it).
|
|
@@ -342,11 +560,11 @@ export class AgentExecutor {
|
|
|
342
560
|
`model ${JSON.stringify(options.model)} (supported: ${supportedProviders.join(", ")})`);
|
|
343
561
|
}
|
|
344
562
|
const provider = options.provider ?? supportedProviders[0] ?? DEFAULT_RUN_PROVIDER;
|
|
345
|
-
validateSubmitCredentials(options, provider);
|
|
563
|
+
validateSubmitCredentials(options, provider, "AgentExecutor.submit");
|
|
346
564
|
if (typeof options.model !== "string" || !options.model) {
|
|
347
565
|
throw new RunConfigValidationError("AgentExecutor.submit: model is required");
|
|
348
566
|
}
|
|
349
|
-
const prompt = normalisePrompt(options.prompt);
|
|
567
|
+
const prompt = normalisePrompt(options.prompt, "AgentExecutor.submit", "prompt");
|
|
350
568
|
const { endpoints: proxyEndpointDeclarations, auth: proxyEndpointAuthFromInstances } = splitProxyEndpoints(options.proxyEndpoints ?? []);
|
|
351
569
|
const mergedProxyAuth = mergeProxyEndpointAuth(proxyEndpointAuthFromInstances, options.secrets?.proxyEndpointAuth ?? []);
|
|
352
570
|
// Split secretEnv into value-free declarations (hashed submission) and
|
|
@@ -416,7 +634,6 @@ export class AgentExecutor {
|
|
|
416
634
|
...(mergedProxyAuth.length > 0 ? { proxyEndpointAuth: mergedProxyAuth } : {}),
|
|
417
635
|
...(Object.keys(envSecretValues).length > 0 ? { envSecrets: envSecretValues } : {})
|
|
418
636
|
};
|
|
419
|
-
const postHook = postHookForWire(options.postHook);
|
|
420
637
|
const request = {
|
|
421
638
|
idempotencyKey: options.idempotencyKey ?? generateIdempotencyKey(),
|
|
422
639
|
// Always include `provider` on the wire so dashboard / proxy
|
|
@@ -427,7 +644,6 @@ export class AgentExecutor {
|
|
|
427
644
|
submission,
|
|
428
645
|
...(options.runtimeSize ? { runtimeSize: options.runtimeSize } : {}),
|
|
429
646
|
...(options.timeout ? { timeout: options.timeout } : {}),
|
|
430
|
-
...(postHook ? { postHook } : {}),
|
|
431
647
|
...(options.parentRunId ? { parentRunId: options.parentRunId } : {}),
|
|
432
648
|
// Operational/delivery concern — sibling of idempotencyKey, NOT part of
|
|
433
649
|
// the hashed brief. The idempotency key here is randomly generated, so
|
|
@@ -445,6 +661,80 @@ export class AgentExecutor {
|
|
|
445
661
|
const run = await operations.submitRun(this.#http, request);
|
|
446
662
|
return getSubmittedRunId(run);
|
|
447
663
|
}
|
|
664
|
+
async #buildSessionCreateRequest(options) {
|
|
665
|
+
if (!options || typeof options !== "object") {
|
|
666
|
+
throw new RunConfigValidationError("Aex.openSession: options is required");
|
|
667
|
+
}
|
|
668
|
+
assertNoLegacySessionFields(options, "Aex.openSession");
|
|
669
|
+
const supportedProviders = providersForModel(options.model);
|
|
670
|
+
if (options.provider &&
|
|
671
|
+
supportedProviders.length > 0 &&
|
|
672
|
+
!supportedProviders.includes(options.provider)) {
|
|
673
|
+
throw new RunConfigValidationError(`Aex.openSession: provider ${JSON.stringify(options.provider)} is not available for ` +
|
|
674
|
+
`model ${JSON.stringify(options.model)} (supported: ${supportedProviders.join(", ")})`);
|
|
675
|
+
}
|
|
676
|
+
const provider = options.provider ?? supportedProviders[0] ?? DEFAULT_RUN_PROVIDER;
|
|
677
|
+
validateApiKeys(options.apiKeys, provider, "Aex.openSession");
|
|
678
|
+
if (typeof options.model !== "string" || !options.model) {
|
|
679
|
+
throw new RunConfigValidationError("Aex.openSession: model is required");
|
|
680
|
+
}
|
|
681
|
+
const { endpoints: proxyEndpointDeclarations, auth: proxyEndpointAuthFromInstances } = splitProxyEndpoints(options.proxyEndpoints ?? []);
|
|
682
|
+
const mergedProxyAuth = mergeProxyEndpointAuth(proxyEndpointAuthFromInstances, []);
|
|
683
|
+
const { declarations: secretEnvDeclarations, values: envSecretValues } = splitSecretEnv(options.environment?.secrets);
|
|
684
|
+
let limits;
|
|
685
|
+
try {
|
|
686
|
+
limits = parseRunLimits(options.overrides?.maxSpendUsd === undefined
|
|
687
|
+
? undefined
|
|
688
|
+
: { maxSpendUsd: options.overrides.maxSpendUsd });
|
|
689
|
+
}
|
|
690
|
+
catch (err) {
|
|
691
|
+
throw new AexError("RUN_CONFIG_INVALID", `Aex.openSession: ${err instanceof Error ? err.message : String(err)}`);
|
|
692
|
+
}
|
|
693
|
+
const uploader = (args) => this._uploadAsset(args);
|
|
694
|
+
const preparedSkills = await prepareSkills(options.skills ?? [], uploader);
|
|
695
|
+
const preparedTools = await prepareTools(options.tools ?? [], uploader);
|
|
696
|
+
const preparedAgentsMd = await prepareAgentsMd(options.agentsMd ?? [], uploader);
|
|
697
|
+
const preparedFiles = await prepareFiles(options.files ?? [], uploader);
|
|
698
|
+
const { submissionMcpServers, mergedMcpSecrets } = mergeMcpServers(options.mcpServers ?? [], []);
|
|
699
|
+
const outputCapture = outputsForWire(options.outputs);
|
|
700
|
+
const environment = sessionEnvironmentForWire(options.environment);
|
|
701
|
+
const submission = {
|
|
702
|
+
model: options.model,
|
|
703
|
+
...(options.system ? { system: options.system } : {}),
|
|
704
|
+
skills: preparedSkills,
|
|
705
|
+
tools: [...preparedTools.builtinNames, ...preparedTools.refs],
|
|
706
|
+
agentsMd: preparedAgentsMd,
|
|
707
|
+
files: preparedFiles,
|
|
708
|
+
mcpServers: submissionMcpServers,
|
|
709
|
+
...(Object.keys(secretEnvDeclarations).length > 0 ? { secretEnv: secretEnvDeclarations } : {}),
|
|
710
|
+
...(environment ? { environment: environment } : {}),
|
|
711
|
+
...(options.metadata ? { metadata: options.metadata } : {}),
|
|
712
|
+
...(outputCapture ? { outputs: outputCapture } : {}),
|
|
713
|
+
...(options.includeBuiltinTools !== undefined
|
|
714
|
+
? { includeBuiltinTools: options.includeBuiltinTools }
|
|
715
|
+
: {}),
|
|
716
|
+
...(options.outputMode !== undefined ? { outputMode: options.outputMode } : {})
|
|
717
|
+
};
|
|
718
|
+
const secrets = {
|
|
719
|
+
...(options.apiKeys ? { apiKeys: options.apiKeys } : {}),
|
|
720
|
+
...(mergedMcpSecrets.length > 0 ? { mcpServers: mergedMcpSecrets } : {}),
|
|
721
|
+
...(mergedProxyAuth.length > 0 ? { proxyEndpointAuth: mergedProxyAuth } : {}),
|
|
722
|
+
...(Object.keys(envSecretValues).length > 0 ? { envSecrets: envSecretValues } : {})
|
|
723
|
+
};
|
|
724
|
+
const retention = sessionRetentionForWire(options);
|
|
725
|
+
return {
|
|
726
|
+
provider,
|
|
727
|
+
submission,
|
|
728
|
+
...(options.runtime ? { runtimeSize: options.runtime } : {}),
|
|
729
|
+
...(options.overrides?.timeout ? { timeout: options.overrides.timeout } : {}),
|
|
730
|
+
...(limits ? { limits } : {}),
|
|
731
|
+
retention,
|
|
732
|
+
secrets,
|
|
733
|
+
...(proxyEndpointDeclarations.length > 0
|
|
734
|
+
? { proxyEndpoints: proxyEndpointDeclarations }
|
|
735
|
+
: {})
|
|
736
|
+
};
|
|
737
|
+
}
|
|
448
738
|
getRun(runId) {
|
|
449
739
|
return operations.getRun(this.#http, runId);
|
|
450
740
|
}
|
|
@@ -743,6 +1033,9 @@ export class AgentExecutor {
|
|
|
743
1033
|
return writeOptionalFile(await operations.downloadMetadata(this.#http, runId), options?.to);
|
|
744
1034
|
}
|
|
745
1035
|
}
|
|
1036
|
+
/** Canonical SDK client name. `AgentExecutor` remains as a compatibility alias. */
|
|
1037
|
+
export class Aex extends AgentExecutor {
|
|
1038
|
+
}
|
|
746
1039
|
// `Run.status` is a loose `string` on the wire shape, so we membership-test
|
|
747
1040
|
// against the canonical terminal set rather than re-deriving one (which is how
|
|
748
1041
|
// `timed_out` got dropped from the old hardcoded list).
|
|
@@ -750,6 +1043,31 @@ const TERMINAL_STATUSES = new Set(TERMINAL_RUN_STATUSES);
|
|
|
750
1043
|
function isTerminal(status) {
|
|
751
1044
|
return typeof status === "string" && TERMINAL_STATUSES.has(status);
|
|
752
1045
|
}
|
|
1046
|
+
function sessionToRun(session) {
|
|
1047
|
+
const id = session.sessionId ?? session.id;
|
|
1048
|
+
return {
|
|
1049
|
+
id,
|
|
1050
|
+
status: String(session.status),
|
|
1051
|
+
...(typeof session.workspaceId === "string" ? { workspaceId: session.workspaceId } : {}),
|
|
1052
|
+
...(typeof session.createdAt === "string" ? { createdAt: session.createdAt } : {}),
|
|
1053
|
+
...(typeof session.updatedAt === "string" ? { updatedAt: session.updatedAt } : {}),
|
|
1054
|
+
...(session.errorMessage !== undefined ? { errorMessage: session.errorMessage } : {}),
|
|
1055
|
+
...(session.usage ? { usage: session.usage } : {})
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
function scopedAbortSignal(timeoutMs) {
|
|
1059
|
+
if (timeoutMs === undefined) {
|
|
1060
|
+
return undefined;
|
|
1061
|
+
}
|
|
1062
|
+
const controller = new AbortController();
|
|
1063
|
+
const timer = setTimeout(() => controller.abort(), Math.max(0, timeoutMs));
|
|
1064
|
+
return {
|
|
1065
|
+
signal: controller.signal,
|
|
1066
|
+
clear() {
|
|
1067
|
+
clearTimeout(timer);
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
753
1071
|
/** Escape a literal string for safe interpolation into a RegExp. */
|
|
754
1072
|
function escapeRegExp(input) {
|
|
755
1073
|
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -830,49 +1148,104 @@ function generateIdempotencyKey() {
|
|
|
830
1148
|
return cryptoObj.randomUUID();
|
|
831
1149
|
return `idem-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
832
1150
|
}
|
|
833
|
-
function normalisePrompt(input) {
|
|
1151
|
+
function normalisePrompt(input, surface = "AgentExecutor.submit", field = "prompt") {
|
|
834
1152
|
if (typeof input === "string") {
|
|
835
1153
|
if (!input) {
|
|
836
|
-
throw new RunConfigValidationError(
|
|
1154
|
+
throw new RunConfigValidationError(`${surface}: ${field} must be a non-empty string`);
|
|
837
1155
|
}
|
|
838
1156
|
return [input];
|
|
839
1157
|
}
|
|
840
1158
|
if (!Array.isArray(input) || input.length === 0) {
|
|
841
|
-
throw new RunConfigValidationError(
|
|
1159
|
+
throw new RunConfigValidationError(`${surface}: ${field} must be a non-empty string or string array`);
|
|
842
1160
|
}
|
|
843
1161
|
for (const segment of input) {
|
|
844
1162
|
if (typeof segment !== "string" || !segment) {
|
|
845
|
-
throw new RunConfigValidationError(
|
|
1163
|
+
throw new RunConfigValidationError(`${surface}: ${field} segments must be non-empty strings`);
|
|
846
1164
|
}
|
|
847
1165
|
}
|
|
848
1166
|
return [...input];
|
|
849
1167
|
}
|
|
850
|
-
function
|
|
1168
|
+
function normaliseSessionInput(input, surface, field) {
|
|
1169
|
+
if (typeof input === "string") {
|
|
1170
|
+
if (!input) {
|
|
1171
|
+
throw new RunConfigValidationError(`${surface}: ${field} must be a non-empty string`);
|
|
1172
|
+
}
|
|
1173
|
+
return input;
|
|
1174
|
+
}
|
|
1175
|
+
if (!Array.isArray(input) || input.length === 0) {
|
|
1176
|
+
throw new RunConfigValidationError(`${surface}: ${field} must be a non-empty string or string array`);
|
|
1177
|
+
}
|
|
1178
|
+
for (const segment of input) {
|
|
1179
|
+
if (typeof segment !== "string" || !segment) {
|
|
1180
|
+
throw new RunConfigValidationError(`${surface}: ${field} segments must be non-empty strings`);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
return [...input];
|
|
1184
|
+
}
|
|
1185
|
+
function assertNoRemovedSubmitFields(options, surface, extraFields = []) {
|
|
851
1186
|
const record = options;
|
|
852
|
-
for (const field of ["credentialMode", "runtime", "region", "apiKey", "credentials"]) {
|
|
1187
|
+
for (const field of ["credentialMode", "runtime", "region", "apiKey", "credentials", "postHook", ...extraFields]) {
|
|
853
1188
|
if (Object.prototype.hasOwnProperty.call(record, field)) {
|
|
854
|
-
throw new RunConfigValidationError(
|
|
1189
|
+
throw new RunConfigValidationError(`${surface}: ${field} is not a supported option; use the managed path with secrets.apiKeys[provider].`);
|
|
855
1190
|
}
|
|
856
1191
|
}
|
|
857
1192
|
const secrets = record.secrets;
|
|
858
1193
|
if (secrets && typeof secrets === "object" && !Array.isArray(secrets) && Object.prototype.hasOwnProperty.call(secrets, "apiKey")) {
|
|
859
|
-
throw new RunConfigValidationError(
|
|
1194
|
+
throw new RunConfigValidationError(`${surface}: secrets.apiKey is not supported; use secrets.apiKeys[provider].`);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
function assertNoLegacySessionFields(options, surface) {
|
|
1198
|
+
const record = options;
|
|
1199
|
+
const messages = {
|
|
1200
|
+
input: "send user messages with session.send(...) or use run({ message }).",
|
|
1201
|
+
prompt: "use message for one-shot run input or session.send(...) for follow-up messages.",
|
|
1202
|
+
instructions: "use system.",
|
|
1203
|
+
idleSuspendAfter: "use overrides.idleTtl.",
|
|
1204
|
+
idleTtl: "use overrides.idleTtl.",
|
|
1205
|
+
retention: "use overrides.idleTtl.",
|
|
1206
|
+
secretEnv: "use environment.secrets.",
|
|
1207
|
+
secrets: "use top-level apiKeys for provider keys and environment.secrets for run secrets.",
|
|
1208
|
+
runtimeSize: "use runtime.",
|
|
1209
|
+
parentRunId: "subagents are session-internal; parentRunId is not part of the session API.",
|
|
1210
|
+
limits: "use overrides.",
|
|
1211
|
+
timeout: "use overrides.timeout.",
|
|
1212
|
+
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."
|
|
1215
|
+
};
|
|
1216
|
+
for (const [field, message] of Object.entries(messages)) {
|
|
1217
|
+
if (Object.prototype.hasOwnProperty.call(record, field)) {
|
|
1218
|
+
throw new RunConfigValidationError(`${surface}: ${field} is not a supported option; ${message}`);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
const overrides = record.overrides;
|
|
1222
|
+
if (overrides && typeof overrides === "object" && !Array.isArray(overrides)) {
|
|
1223
|
+
const overrideRecord = overrides;
|
|
1224
|
+
if (Object.prototype.hasOwnProperty.call(overrideRecord, "idleSuspendAfter")) {
|
|
1225
|
+
throw new RunConfigValidationError(`${surface}: overrides.idleSuspendAfter is not a supported option; use overrides.idleTtl.`);
|
|
1226
|
+
}
|
|
860
1227
|
}
|
|
861
1228
|
}
|
|
862
|
-
function
|
|
1229
|
+
function assertNoSessionSendSignal(options, surface) {
|
|
1230
|
+
const record = options;
|
|
1231
|
+
if (record && typeof record === "object" && Object.prototype.hasOwnProperty.call(record, "signal")) {
|
|
1232
|
+
throw new RunConfigValidationError(`${surface}: signal is not a supported option; use session.cancel() / session.suspend() for remote control.`);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
function validateSubmitCredentials(options, provider, surface) {
|
|
863
1236
|
if (options.parentRunId) {
|
|
864
1237
|
return;
|
|
865
1238
|
}
|
|
866
1239
|
const key = options.secrets?.apiKeys?.[provider];
|
|
867
1240
|
if (typeof key !== "string" || key.length === 0) {
|
|
868
|
-
throw new RunConfigValidationError(
|
|
1241
|
+
throw new RunConfigValidationError(`${surface}: a provider API key is required — pass secrets.apiKeys[${JSON.stringify(provider)}].`);
|
|
869
1242
|
}
|
|
870
1243
|
}
|
|
871
|
-
function
|
|
872
|
-
|
|
873
|
-
|
|
1244
|
+
function validateApiKeys(apiKeys, provider, surface) {
|
|
1245
|
+
const key = apiKeys?.[provider];
|
|
1246
|
+
if (typeof key !== "string" || key.length === 0) {
|
|
1247
|
+
throw new RunConfigValidationError(`${surface}: a provider API key is required — pass apiKeys[${JSON.stringify(provider)}].`);
|
|
874
1248
|
}
|
|
875
|
-
return input;
|
|
876
1249
|
}
|
|
877
1250
|
function outputsForWire(outputs) {
|
|
878
1251
|
if (outputs === undefined) {
|
|
@@ -896,6 +1269,24 @@ function outputsForWire(outputs) {
|
|
|
896
1269
|
...(outputs.maxFiles !== undefined ? { maxFiles: outputs.maxFiles } : {})
|
|
897
1270
|
};
|
|
898
1271
|
}
|
|
1272
|
+
const DEFAULT_SESSION_IDLE_TTL = "3m";
|
|
1273
|
+
function sessionRetentionForWire(options) {
|
|
1274
|
+
return {
|
|
1275
|
+
idleTtl: options.overrides?.idleTtl ?? DEFAULT_SESSION_IDLE_TTL
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
function sessionEnvironmentForWire(environment) {
|
|
1279
|
+
if (environment === undefined) {
|
|
1280
|
+
return undefined;
|
|
1281
|
+
}
|
|
1282
|
+
const { variables, secrets: _secrets, ...rest } = environment;
|
|
1283
|
+
void _secrets;
|
|
1284
|
+
const out = {
|
|
1285
|
+
...rest,
|
|
1286
|
+
...(variables !== undefined ? { envVars: variables } : {})
|
|
1287
|
+
};
|
|
1288
|
+
return Object.keys(out).length === 0 ? undefined : out;
|
|
1289
|
+
}
|
|
899
1290
|
/**
|
|
900
1291
|
* Resolve a draft's asset id: reuse the cached id from a prior submit, otherwise
|
|
901
1292
|
* upload the bytes and cache the result on the instance.
|