@aexhq/sdk 0.31.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/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/models.d.ts +0 -76
- package/dist/_contracts/models.js +0 -20
- package/dist/_contracts/operations.d.ts +15 -18
- package/dist/_contracts/operations.js +81 -40
- 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/run-unit.js +4 -4
- package/dist/_contracts/runtime-types.d.ts +86 -2
- 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 +118 -135
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +118 -72
- package/dist/client.js +427 -96
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/cleanup.md +2 -2
- package/docs/credentials.md +3 -3
- package/docs/defaults.md +0 -2
- package/docs/events.md +32 -13
- package/docs/limits.md +4 -3
- package/docs/outputs.md +2 -2
- package/docs/provider-runtime-capabilities.md +2 -2
- package/docs/public-surface.json +15 -10
- package/docs/quickstart.md +38 -12
- package/docs/run-config.md +3 -8
- package/docs/secrets.md +2 -2
- package/docs/skills.md +4 -4
- package/docs/vision-skills.md +2 -2
- 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,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
|
*
|
|
@@ -62,8 +227,7 @@ export class SkillsClient {
|
|
|
62
227
|
* New run submissions usually use `AgentsMd.fromContent(...)` or
|
|
63
228
|
* `AgentsMd.fromPath(...)` directly inside `submit`; the SDK
|
|
64
229
|
* materializes those bytes to the hosted asset store before the run lands. This namespace is
|
|
65
|
-
* the read/delete surface for persisted AgentsMd records
|
|
66
|
-
* upload transport retained for legacy callers.
|
|
230
|
+
* the read/delete surface for persisted AgentsMd records.
|
|
67
231
|
*/
|
|
68
232
|
export class AgentsMdClient {
|
|
69
233
|
#http;
|
|
@@ -79,13 +243,6 @@ export class AgentsMdClient {
|
|
|
79
243
|
delete(agentsMdId) {
|
|
80
244
|
return operations.deleteAgentsMd(this.#http, agentsMdId);
|
|
81
245
|
}
|
|
82
|
-
/**
|
|
83
|
-
* Internal: post an AgentsMd markdown string to the BFF.
|
|
84
|
-
* NOT part of the public API.
|
|
85
|
-
*/
|
|
86
|
-
async _uploadAgentsMd(args) {
|
|
87
|
-
return operations.createAgentsMd(this.#http, args);
|
|
88
|
-
}
|
|
89
246
|
}
|
|
90
247
|
/**
|
|
91
248
|
* Workspace File admin operations exposed under `client.files`.
|
|
@@ -93,8 +250,7 @@ export class AgentsMdClient {
|
|
|
93
250
|
* New run submissions usually use `File.fromPath(...)` or
|
|
94
251
|
* `File.fromBytes(...)` directly inside `submit`; the SDK materializes
|
|
95
252
|
* those bytes to the hosted asset store before the run lands. This namespace is the read/delete
|
|
96
|
-
* surface for persisted file records
|
|
97
|
-
* retained for legacy callers.
|
|
253
|
+
* surface for persisted file records.
|
|
98
254
|
*/
|
|
99
255
|
export class FilesClient {
|
|
100
256
|
#http;
|
|
@@ -110,13 +266,6 @@ export class FilesClient {
|
|
|
110
266
|
delete(fileId) {
|
|
111
267
|
return operations.deleteFile(this.#http, fileId);
|
|
112
268
|
}
|
|
113
|
-
/**
|
|
114
|
-
* Internal: post a pre-bundled file zip to the BFF.
|
|
115
|
-
* NOT part of the public API.
|
|
116
|
-
*/
|
|
117
|
-
async _uploadFile(args) {
|
|
118
|
-
return operations.createFile(this.#http, args);
|
|
119
|
-
}
|
|
120
269
|
}
|
|
121
270
|
/**
|
|
122
271
|
* Workspace secret management exposed under `client.secrets`, mirroring
|
|
@@ -197,6 +346,8 @@ export class AgentExecutor {
|
|
|
197
346
|
agentsMd;
|
|
198
347
|
files;
|
|
199
348
|
secrets;
|
|
349
|
+
sessions;
|
|
350
|
+
chat;
|
|
200
351
|
constructor(options) {
|
|
201
352
|
if (!options.apiToken) {
|
|
202
353
|
throw new Error("AgentExecutor: apiToken is required");
|
|
@@ -217,22 +368,8 @@ export class AgentExecutor {
|
|
|
217
368
|
this.agentsMd = new AgentsMdClient(this.#http);
|
|
218
369
|
this.files = new FilesClient(this.#http);
|
|
219
370
|
this.secrets = new SecretsClient(this.#http);
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
* Internal: an `AgentsMd.upload(this)` shortcut that bypasses
|
|
223
|
-
* `client.agentsMd` indirection. Forwarded to
|
|
224
|
-
* `AgentsMdClient._uploadAgentsMd`. NOT part of the public API.
|
|
225
|
-
*/
|
|
226
|
-
async _uploadAgentsMd(args) {
|
|
227
|
-
return this.agentsMd._uploadAgentsMd(args);
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* Internal: a `File.upload(this)` shortcut that bypasses
|
|
231
|
-
* `client.files` indirection. Forwarded to
|
|
232
|
-
* `FilesClient._uploadFile`. NOT part of the public API.
|
|
233
|
-
*/
|
|
234
|
-
async _uploadFile(args) {
|
|
235
|
-
return this.files._uploadFile(args);
|
|
371
|
+
this.chat = new ChatClient(this.#http, (options) => this.#buildSessionCreateRequest(options));
|
|
372
|
+
this.sessions = this.chat;
|
|
236
373
|
}
|
|
237
374
|
/**
|
|
238
375
|
* Internal: satisfies the `SecretUploader` surface so a
|
|
@@ -259,57 +396,78 @@ export class AgentExecutor {
|
|
|
259
396
|
});
|
|
260
397
|
}
|
|
261
398
|
/**
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
* (RUN_STARTED + a terminal event present) → `listOutputs` → decode the trace
|
|
268
|
-
* and assistant text. On resolve, `getRun`/`listOutputs` are guaranteed
|
|
269
|
-
* consistent.
|
|
270
|
-
*
|
|
271
|
-
* Uses polling (portable across backends), NOT the coordinator WebSocket. By
|
|
272
|
-
* default a failed run resolves with `ok: false` and a populated `error`; pass
|
|
273
|
-
* `{ throwOnFailure: true }` to throw instead. For live events prefer `submit`
|
|
274
|
-
* + `streamEnvelopes(runId, { settleConsistent: true })`.
|
|
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)`.
|
|
275
404
|
*/
|
|
276
405
|
async run(options, opts = {}) {
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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();
|
|
303
457
|
}
|
|
304
|
-
return result;
|
|
305
458
|
}
|
|
306
459
|
/**
|
|
307
|
-
* Explicit, discoverable alias for {@link run}:
|
|
308
|
-
* 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.
|
|
309
462
|
*/
|
|
310
463
|
runAndCollect(options, opts) {
|
|
311
464
|
return this.run(options, opts);
|
|
312
465
|
}
|
|
466
|
+
openSession(optionsOrId) {
|
|
467
|
+
return typeof optionsOrId === "string"
|
|
468
|
+
? this.sessions.open(optionsOrId)
|
|
469
|
+
: this.sessions.create(optionsOrId);
|
|
470
|
+
}
|
|
313
471
|
/**
|
|
314
472
|
* Poll `listEvents` until the snapshot is settle-bracketed — both a
|
|
315
473
|
* RUN_STARTED and a terminal (RUN_FINISHED / RUN_ERROR) event present — then
|
|
@@ -360,7 +518,7 @@ export class AgentExecutor {
|
|
|
360
518
|
if (!options || typeof options !== "object") {
|
|
361
519
|
throw new RunConfigValidationError("AgentExecutor.submit: options is required");
|
|
362
520
|
}
|
|
363
|
-
assertNoRemovedSubmitFields(options);
|
|
521
|
+
assertNoRemovedSubmitFields(options, "AgentExecutor.submit");
|
|
364
522
|
// A model maps to one or more upstream providers (see MODEL_PROVIDER_IDS).
|
|
365
523
|
// `providersForModel` returns the supported providers in priority order, or
|
|
366
524
|
// `[]` for an unknown model string (the model check below then rejects it).
|
|
@@ -374,11 +532,11 @@ export class AgentExecutor {
|
|
|
374
532
|
`model ${JSON.stringify(options.model)} (supported: ${supportedProviders.join(", ")})`);
|
|
375
533
|
}
|
|
376
534
|
const provider = options.provider ?? supportedProviders[0] ?? DEFAULT_RUN_PROVIDER;
|
|
377
|
-
validateSubmitCredentials(options, provider);
|
|
535
|
+
validateSubmitCredentials(options, provider, "AgentExecutor.submit");
|
|
378
536
|
if (typeof options.model !== "string" || !options.model) {
|
|
379
537
|
throw new RunConfigValidationError("AgentExecutor.submit: model is required");
|
|
380
538
|
}
|
|
381
|
-
const prompt = normalisePrompt(options.prompt);
|
|
539
|
+
const prompt = normalisePrompt(options.prompt, "AgentExecutor.submit", "prompt");
|
|
382
540
|
const { endpoints: proxyEndpointDeclarations, auth: proxyEndpointAuthFromInstances } = splitProxyEndpoints(options.proxyEndpoints ?? []);
|
|
383
541
|
const mergedProxyAuth = mergeProxyEndpointAuth(proxyEndpointAuthFromInstances, options.secrets?.proxyEndpointAuth ?? []);
|
|
384
542
|
// Split secretEnv into value-free declarations (hashed submission) and
|
|
@@ -448,7 +606,6 @@ export class AgentExecutor {
|
|
|
448
606
|
...(mergedProxyAuth.length > 0 ? { proxyEndpointAuth: mergedProxyAuth } : {}),
|
|
449
607
|
...(Object.keys(envSecretValues).length > 0 ? { envSecrets: envSecretValues } : {})
|
|
450
608
|
};
|
|
451
|
-
const postHook = postHookForWire(options.postHook);
|
|
452
609
|
const request = {
|
|
453
610
|
idempotencyKey: options.idempotencyKey ?? generateIdempotencyKey(),
|
|
454
611
|
// Always include `provider` on the wire so dashboard / proxy
|
|
@@ -459,7 +616,6 @@ export class AgentExecutor {
|
|
|
459
616
|
submission,
|
|
460
617
|
...(options.runtimeSize ? { runtimeSize: options.runtimeSize } : {}),
|
|
461
618
|
...(options.timeout ? { timeout: options.timeout } : {}),
|
|
462
|
-
...(postHook ? { postHook } : {}),
|
|
463
619
|
...(options.parentRunId ? { parentRunId: options.parentRunId } : {}),
|
|
464
620
|
// Operational/delivery concern — sibling of idempotencyKey, NOT part of
|
|
465
621
|
// the hashed brief. The idempotency key here is randomly generated, so
|
|
@@ -477,6 +633,80 @@ export class AgentExecutor {
|
|
|
477
633
|
const run = await operations.submitRun(this.#http, request);
|
|
478
634
|
return getSubmittedRunId(run);
|
|
479
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
|
+
}
|
|
480
710
|
getRun(runId) {
|
|
481
711
|
return operations.getRun(this.#http, runId);
|
|
482
712
|
}
|
|
@@ -775,6 +1005,9 @@ export class AgentExecutor {
|
|
|
775
1005
|
return writeOptionalFile(await operations.downloadMetadata(this.#http, runId), options?.to);
|
|
776
1006
|
}
|
|
777
1007
|
}
|
|
1008
|
+
/** Canonical SDK client name. `AgentExecutor` remains as a compatibility alias. */
|
|
1009
|
+
export class Aex extends AgentExecutor {
|
|
1010
|
+
}
|
|
778
1011
|
// `Run.status` is a loose `string` on the wire shape, so we membership-test
|
|
779
1012
|
// against the canonical terminal set rather than re-deriving one (which is how
|
|
780
1013
|
// `timed_out` got dropped from the old hardcoded list).
|
|
@@ -782,6 +1015,31 @@ const TERMINAL_STATUSES = new Set(TERMINAL_RUN_STATUSES);
|
|
|
782
1015
|
function isTerminal(status) {
|
|
783
1016
|
return typeof status === "string" && TERMINAL_STATUSES.has(status);
|
|
784
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
|
+
}
|
|
785
1043
|
/** Escape a literal string for safe interpolation into a RegExp. */
|
|
786
1044
|
function escapeRegExp(input) {
|
|
787
1045
|
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -862,49 +1120,104 @@ function generateIdempotencyKey() {
|
|
|
862
1120
|
return cryptoObj.randomUUID();
|
|
863
1121
|
return `idem-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
864
1122
|
}
|
|
865
|
-
function normalisePrompt(input) {
|
|
1123
|
+
function normalisePrompt(input, surface = "AgentExecutor.submit", field = "prompt") {
|
|
866
1124
|
if (typeof input === "string") {
|
|
867
1125
|
if (!input) {
|
|
868
|
-
throw new RunConfigValidationError(
|
|
1126
|
+
throw new RunConfigValidationError(`${surface}: ${field} must be a non-empty string`);
|
|
869
1127
|
}
|
|
870
1128
|
return [input];
|
|
871
1129
|
}
|
|
872
1130
|
if (!Array.isArray(input) || input.length === 0) {
|
|
873
|
-
throw new RunConfigValidationError(
|
|
1131
|
+
throw new RunConfigValidationError(`${surface}: ${field} must be a non-empty string or string array`);
|
|
874
1132
|
}
|
|
875
1133
|
for (const segment of input) {
|
|
876
1134
|
if (typeof segment !== "string" || !segment) {
|
|
877
|
-
throw new RunConfigValidationError(
|
|
1135
|
+
throw new RunConfigValidationError(`${surface}: ${field} segments must be non-empty strings`);
|
|
878
1136
|
}
|
|
879
1137
|
}
|
|
880
1138
|
return [...input];
|
|
881
1139
|
}
|
|
882
|
-
function
|
|
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 = []) {
|
|
883
1158
|
const record = options;
|
|
884
|
-
for (const field of ["credentialMode", "runtime", "region", "apiKey", "credentials"]) {
|
|
1159
|
+
for (const field of ["credentialMode", "runtime", "region", "apiKey", "credentials", "postHook", ...extraFields]) {
|
|
885
1160
|
if (Object.prototype.hasOwnProperty.call(record, field)) {
|
|
886
|
-
throw new RunConfigValidationError(
|
|
1161
|
+
throw new RunConfigValidationError(`${surface}: ${field} is not a supported option; use the managed path with secrets.apiKeys[provider].`);
|
|
887
1162
|
}
|
|
888
1163
|
}
|
|
889
1164
|
const secrets = record.secrets;
|
|
890
1165
|
if (secrets && typeof secrets === "object" && !Array.isArray(secrets) && Object.prototype.hasOwnProperty.call(secrets, "apiKey")) {
|
|
891
|
-
throw new RunConfigValidationError(
|
|
1166
|
+
throw new RunConfigValidationError(`${surface}: secrets.apiKey is not supported; use secrets.apiKeys[provider].`);
|
|
892
1167
|
}
|
|
893
1168
|
}
|
|
894
|
-
function
|
|
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) {
|
|
895
1208
|
if (options.parentRunId) {
|
|
896
1209
|
return;
|
|
897
1210
|
}
|
|
898
1211
|
const key = options.secrets?.apiKeys?.[provider];
|
|
899
1212
|
if (typeof key !== "string" || key.length === 0) {
|
|
900
|
-
throw new RunConfigValidationError(
|
|
1213
|
+
throw new RunConfigValidationError(`${surface}: a provider API key is required — pass secrets.apiKeys[${JSON.stringify(provider)}].`);
|
|
901
1214
|
}
|
|
902
1215
|
}
|
|
903
|
-
function
|
|
904
|
-
|
|
905
|
-
|
|
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)}].`);
|
|
906
1220
|
}
|
|
907
|
-
return input;
|
|
908
1221
|
}
|
|
909
1222
|
function outputsForWire(outputs) {
|
|
910
1223
|
if (outputs === undefined) {
|
|
@@ -928,6 +1241,24 @@ function outputsForWire(outputs) {
|
|
|
928
1241
|
...(outputs.maxFiles !== undefined ? { maxFiles: outputs.maxFiles } : {})
|
|
929
1242
|
};
|
|
930
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
|
+
}
|
|
931
1262
|
/**
|
|
932
1263
|
* Resolve a draft's asset id: reuse the cached id from a prior submit, otherwise
|
|
933
1264
|
* upload the bytes and cache the result on the instance.
|