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