@aexhq/sdk 0.40.16 → 0.41.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 +25 -25
- package/dist/_contracts/api-key.d.ts +6 -4
- package/dist/_contracts/api-key.js +20 -7
- package/dist/_contracts/connection-ticket.d.ts +5 -5
- package/dist/_contracts/connection-ticket.js +7 -7
- package/dist/_contracts/error-codes.d.ts +1 -1
- package/dist/_contracts/error-codes.js +4 -4
- package/dist/_contracts/error-factory.js +1 -1
- package/dist/_contracts/event-envelope.d.ts +41 -41
- package/dist/_contracts/event-envelope.js +49 -49
- package/dist/_contracts/event-stream-client.d.ts +9 -9
- package/dist/_contracts/event-stream-client.js +5 -5
- package/dist/_contracts/event-view.d.ts +6 -6
- package/dist/_contracts/event-view.js +12 -12
- package/dist/_contracts/http.js +1 -1
- package/dist/_contracts/index.d.ts +10 -10
- package/dist/_contracts/index.js +9 -9
- package/dist/_contracts/models.d.ts +17 -17
- package/dist/_contracts/models.js +14 -14
- package/dist/_contracts/operations.d.ts +79 -82
- package/dist/_contracts/operations.js +233 -243
- package/dist/_contracts/provider-support.d.ts +2 -2
- package/dist/_contracts/runner-event.d.ts +8 -8
- package/dist/_contracts/runner-event.js +9 -9
- package/dist/_contracts/runtime-manifest.d.ts +12 -12
- package/dist/_contracts/runtime-manifest.js +1 -1
- package/dist/_contracts/runtime-sizes.d.ts +12 -12
- package/dist/_contracts/runtime-sizes.js +18 -18
- package/dist/_contracts/runtime-types.d.ts +117 -117
- package/dist/_contracts/runtime-types.js +2 -2
- package/dist/_contracts/sdk-errors.d.ts +3 -3
- package/dist/_contracts/sdk-errors.js +4 -4
- package/dist/_contracts/sdk-secrets.js +10 -10
- package/dist/_contracts/session-artifacts.d.ts +151 -0
- package/dist/_contracts/session-artifacts.js +234 -0
- package/dist/_contracts/{run-config.d.ts → session-config.d.ts} +29 -29
- package/dist/_contracts/{run-config.js → session-config.js} +37 -37
- package/dist/_contracts/session-cost.d.ts +140 -0
- package/dist/_contracts/{run-cost.js → session-cost.js} +94 -94
- package/dist/_contracts/{run-custody.d.ts → session-custody.d.ts} +16 -16
- package/dist/_contracts/{run-custody.js → session-custody.js} +42 -42
- package/dist/_contracts/session-record.d.ts +112 -0
- package/dist/_contracts/{run-record.js → session-record.js} +23 -23
- package/dist/_contracts/session-retention.d.ts +209 -0
- package/dist/_contracts/{run-retention.js → session-retention.js} +116 -116
- package/dist/_contracts/{run-unit.d.ts → session-unit.d.ts} +38 -38
- package/dist/_contracts/{run-unit.js → session-unit.js} +33 -33
- package/dist/_contracts/side-effect-audit.d.ts +14 -14
- package/dist/_contracts/side-effect-audit.js +38 -38
- package/dist/_contracts/sse.d.ts +1 -1
- package/dist/_contracts/sse.js +0 -0
- package/dist/_contracts/stable.d.ts +1 -1
- package/dist/_contracts/stable.js +1 -1
- package/dist/_contracts/status.d.ts +18 -18
- package/dist/_contracts/status.js +19 -19
- package/dist/_contracts/submission.d.ts +101 -99
- package/dist/_contracts/submission.js +96 -96
- package/dist/_contracts/{run-trace.d.ts → turn-trace.d.ts} +1 -1
- package/dist/_contracts/webhook-verify.d.ts +1 -1
- package/dist/_contracts/webhook-verify.js +1 -1
- package/dist/agents-md.d.ts +4 -4
- package/dist/agents-md.js +5 -5
- package/dist/agents-md.js.map +1 -1
- package/dist/bundle.d.ts +1 -1
- package/dist/bundle.js +1 -1
- package/dist/bundle.js.map +1 -1
- package/dist/cli.mjs +578 -621
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +141 -155
- package/dist/client.js +236 -245
- package/dist/client.js.map +1 -1
- package/dist/fetch-archive.js +3 -3
- package/dist/file.d.ts +7 -7
- package/dist/file.js +8 -8
- package/dist/file.js.map +1 -1
- package/dist/index.d.ts +10 -10
- package/dist/index.js +8 -8
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.d.ts +1 -1
- package/dist/retry.d.ts +1 -1
- package/dist/secret.d.ts +6 -6
- package/dist/secret.js +6 -6
- package/dist/secret.js.map +1 -1
- package/dist/skill.d.ts +2 -2
- package/dist/skill.js +3 -3
- package/dist/skill.js.map +1 -1
- package/dist/tool.js +2 -2
- package/dist/tool.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/docs/authentication.md +11 -11
- package/docs/billing.md +3 -3
- package/docs/cleanup.md +5 -5
- package/docs/concepts/agent-tools.md +5 -5
- package/docs/concepts/composition.md +3 -3
- package/docs/concepts/providers-and-runtimes.md +3 -3
- package/docs/concepts/{runs.md → sessions.md} +8 -8
- package/docs/concepts/subagents.md +16 -16
- package/docs/credentials.md +7 -7
- package/docs/defaults.md +14 -14
- package/docs/errors.md +10 -10
- package/docs/events.md +26 -26
- package/docs/files.md +267 -0
- package/docs/limits-and-quotas.md +35 -35
- package/docs/limits.md +6 -6
- package/docs/mcp.md +3 -3
- package/docs/networking.md +24 -24
- package/docs/provider-runtime-capabilities.md +1 -1
- package/docs/public-surface.json +6 -6
- package/docs/quickstart.md +13 -13
- package/docs/release.md +5 -5
- package/docs/retries.md +3 -3
- package/docs/secrets.md +16 -16
- package/docs/session-config.md +58 -0
- package/docs/{run-record.md → session-record.md} +16 -16
- package/docs/skills.md +9 -9
- package/docs/vision-skills.md +7 -7
- package/docs/webhooks.md +11 -11
- package/examples/feature-tour.ts +17 -17
- package/examples/spike-settle-latency.ts +11 -11
- package/package.json +1 -1
- package/dist/_contracts/run-artifacts.d.ts +0 -49
- package/dist/_contracts/run-artifacts.js +0 -102
- package/dist/_contracts/run-cost.d.ts +0 -140
- package/dist/_contracts/run-record.d.ts +0 -116
- package/dist/_contracts/run-retention.d.ts +0 -209
- package/docs/outputs.md +0 -267
- package/docs/run-config.md +0 -58
- /package/dist/_contracts/{run-trace.js → turn-trace.js} +0 -0
package/dist/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AexApiError, CredentialValidationError, HttpClient, PLANE_BASE_URLS,
|
|
1
|
+
import { AexApiError, CredentialValidationError, HttpClient, PLANE_BASE_URLS, SessionConfigValidationError, SessionStateError, SecretString, asAexEventView, customName, isSessionParked as isSessionParkedEvent, isSessionSettled, assertStreamableOutputMode, operations, parseApiKey, resolveModelProvider, streamCoordinatorEvents, usageFromProviderUsage, parseSessionLimits, parseRuntimeSize, parseSessionTimeout, parseSessionWebhook, BUILTIN_TOOL_NAMES, SKILLS_MAX, TERMINAL_SESSION_CONTROL_STATUSES } from "./_contracts/index.js";
|
|
2
2
|
import { AgentsMd } from "./agents-md.js";
|
|
3
3
|
import { uploadAsset, uploadAssetMultipart } from "./asset-upload.js";
|
|
4
4
|
import { File } from "./file.js";
|
|
@@ -8,12 +8,12 @@ import { splitSecretEnv } from "./secret.js";
|
|
|
8
8
|
import { Skill } from "./skill.js";
|
|
9
9
|
import { Tool } from "./tool.js";
|
|
10
10
|
export class SessionTurnStream {
|
|
11
|
-
#
|
|
11
|
+
#stream;
|
|
12
12
|
#generator;
|
|
13
13
|
#outcome;
|
|
14
14
|
#done;
|
|
15
|
-
constructor(
|
|
16
|
-
this.#
|
|
15
|
+
constructor(stream) {
|
|
16
|
+
this.#stream = stream;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
19
|
* ONE underlying send per turn: iterating the stream and calling `done()`
|
|
@@ -22,7 +22,7 @@ export class SessionTurnStream {
|
|
|
22
22
|
* message as a second billable turn (or 409 `session_busy` mid-turn).
|
|
23
23
|
*/
|
|
24
24
|
#shared() {
|
|
25
|
-
this.#generator ??= this.#capture(this.#
|
|
25
|
+
this.#generator ??= this.#capture(this.#stream());
|
|
26
26
|
return this.#generator;
|
|
27
27
|
}
|
|
28
28
|
async *#capture(generator) {
|
|
@@ -114,7 +114,7 @@ export class SessionHandle {
|
|
|
114
114
|
assertNoSessionSendSignal(options, "SessionHandle.replayLast");
|
|
115
115
|
const last = this.#lastSend;
|
|
116
116
|
if (last === undefined) {
|
|
117
|
-
throw new
|
|
117
|
+
throw new SessionStateError("SessionHandle.replayLast: no message has been sent on this session yet");
|
|
118
118
|
}
|
|
119
119
|
return sendSessionInternal(this, last.input, {
|
|
120
120
|
...options,
|
|
@@ -144,9 +144,9 @@ export class SessionHandle {
|
|
|
144
144
|
? readSession
|
|
145
145
|
: (await settledSessionRecord(this.#http, this.id, readSession, options.signal)) ?? readSession;
|
|
146
146
|
this.#session = withTerminalSessionStatus(settled, read);
|
|
147
|
-
const
|
|
147
|
+
const files = await operations.listSessionFiles(this.#http, this.id).catch(() => []);
|
|
148
148
|
const messages = projectAssistantMessages(events);
|
|
149
|
-
return settledTurnResult(this.id, this.#session, turn, events,
|
|
149
|
+
return settledTurnResult(this.id, this.#session, turn, events, files, messages, read);
|
|
150
150
|
}
|
|
151
151
|
/**
|
|
152
152
|
* POST the next turn, reconciling the settle-lag race. A turn stream ends on
|
|
@@ -157,7 +157,7 @@ export class SessionHandle {
|
|
|
157
157
|
* `running`, then retry the SAME idempotent POST (a replay de-duplicates, so
|
|
158
158
|
* this never creates a second billable turn). A session that stays busy past
|
|
159
159
|
* {@link SESSION_BUSY_RECONCILE_DEADLINE_MS} is a genuinely in-flight turn: we
|
|
160
|
-
* surface a clear, bounded {@link
|
|
160
|
+
* surface a clear, bounded {@link SessionStateError} rather than masking it or
|
|
161
161
|
* waiting forever. Any non-`running` busy status (suspended / cancelling /
|
|
162
162
|
* deleted) is a real rejection and passes straight through.
|
|
163
163
|
*/
|
|
@@ -172,7 +172,7 @@ export class SessionHandle {
|
|
|
172
172
|
throw err;
|
|
173
173
|
if (Date.now() >= deadline) {
|
|
174
174
|
const apiErr = err instanceof AexApiError ? err : undefined;
|
|
175
|
-
throw new
|
|
175
|
+
throw new SessionStateError(`SessionHandle.send: session ${this.id} was still running ${SESSION_BUSY_RECONCILE_DEADLINE_MS}ms after the previous turn parked — a turn is still in flight`, {
|
|
176
176
|
sessionId: this.id,
|
|
177
177
|
sessionStatus: "running",
|
|
178
178
|
...(apiErr !== undefined ? { httpStatus: apiErr.status, apiCode: apiErr.apiCode, requestId: apiErr.requestId } : {}),
|
|
@@ -251,14 +251,14 @@ export class SessionHandle {
|
|
|
251
251
|
return accepted;
|
|
252
252
|
}
|
|
253
253
|
/**
|
|
254
|
-
* Enumerate this
|
|
255
|
-
* a {@link
|
|
254
|
+
* Enumerate this session's subagent CHILD sessions (`GET /sessions/:id/children`). Each is
|
|
255
|
+
* a {@link ChildSessionHandle} backed by the RUN facade (getSessionRecord/events/files) —
|
|
256
256
|
* NOT `openSession` — so every child the platform hands you is resolvable, with
|
|
257
|
-
* its lineage (`
|
|
257
|
+
* its lineage (`parentSessionId`/`depth`) and terminal outcome exposed.
|
|
258
258
|
*/
|
|
259
259
|
async children() {
|
|
260
|
-
const refs = await operations.
|
|
261
|
-
return refs.map((ref) => new
|
|
260
|
+
const refs = await operations.listSessionChildren(this.#http, this.id);
|
|
261
|
+
return refs.map((ref) => new ChildSessionHandle(this.#http, ref, this.#fetch));
|
|
262
262
|
}
|
|
263
263
|
/**
|
|
264
264
|
* Accessor for the session's assistant messages. `all()` returns them
|
|
@@ -316,13 +316,13 @@ export class SessionHandle {
|
|
|
316
316
|
};
|
|
317
317
|
}
|
|
318
318
|
/**
|
|
319
|
-
* Accessor for the session's captured
|
|
319
|
+
* Accessor for the session's captured files: `list`/`last`/`first`
|
|
320
320
|
* enumerate them; `read` streams one as capped text; `find`/`findOne`/`link`/
|
|
321
|
-
* `fetch` locate and resolve them; `download` fetches the
|
|
321
|
+
* `fetch` locate and resolve them; `download` fetches the files-namespace
|
|
322
322
|
* zip (no selector) or one file's raw bytes (with selector).
|
|
323
323
|
*/
|
|
324
|
-
|
|
325
|
-
return
|
|
324
|
+
files() {
|
|
325
|
+
return sessionFiles(this.#http, this.id, this.#fetch);
|
|
326
326
|
}
|
|
327
327
|
/**
|
|
328
328
|
* Accessor for the session's webhook delivery ledger: `list()` returns the
|
|
@@ -333,8 +333,8 @@ export class SessionHandle {
|
|
|
333
333
|
const http = this.#http;
|
|
334
334
|
const id = this.id;
|
|
335
335
|
return {
|
|
336
|
-
list: () => operations.
|
|
337
|
-
redeliver: (deliveryId) => operations.
|
|
336
|
+
list: () => operations.getSessionWebhookDeliveries(http, id),
|
|
337
|
+
redeliver: (deliveryId) => operations.redeliverSessionWebhook(http, id, deliveryId)
|
|
338
338
|
};
|
|
339
339
|
}
|
|
340
340
|
/** Re-read the session record from the server and store it as the current record. */
|
|
@@ -344,7 +344,7 @@ export class SessionHandle {
|
|
|
344
344
|
}
|
|
345
345
|
/**
|
|
346
346
|
* Poll the session record until it reaches a parked/terminal status (idle,
|
|
347
|
-
* suspended, error, or any terminal
|
|
347
|
+
* suspended, error, or any terminal session status). Throws if `timeoutMs`
|
|
348
348
|
* elapses first. Updates the stored record.
|
|
349
349
|
*/
|
|
350
350
|
async wait(options = {}) {
|
|
@@ -365,23 +365,23 @@ export class SessionHandle {
|
|
|
365
365
|
throw new Error("SessionHandle.wait: aborted");
|
|
366
366
|
}
|
|
367
367
|
/**
|
|
368
|
-
* Fetch the self-contained `
|
|
369
|
-
* attempts, indexed events,
|
|
368
|
+
* Fetch the self-contained `SessionUnit` for this session: parsed submission,
|
|
369
|
+
* attempts, indexed events, session files, capture failures, proxy-call audit, and
|
|
370
370
|
* resolved skills. Use this when you need fields beyond the session record.
|
|
371
371
|
*
|
|
372
372
|
* On the managed plane this is a LEAN summary — the aggregate collections
|
|
373
|
-
* (`attempts` / `events.entries` / `
|
|
374
|
-
* empty. For authoritative per-
|
|
373
|
+
* (`attempts` / `events.entries` / `sessionFiles` / `rawEventPages`) default to
|
|
374
|
+
* empty. For authoritative per-session data use `files()` / `events()` /
|
|
375
375
|
* `messages()`. The returned shape is always type-valid (never `undefined`
|
|
376
376
|
* where the type promises an array/page), so array/page access is safe.
|
|
377
377
|
*/
|
|
378
378
|
unit() {
|
|
379
|
-
return operations.
|
|
379
|
+
return operations.getSessionUnit(this.#http, this.id);
|
|
380
380
|
}
|
|
381
381
|
/**
|
|
382
382
|
* Download EVERYTHING public about this session as one zip, assembled
|
|
383
383
|
* client-side from the public read endpoints. Organised into `metadata/`,
|
|
384
|
-
* `events/`, and `
|
|
384
|
+
* `events/`, and `files/` folders, plus a `manifest.json`. Pass `to` to
|
|
385
385
|
* also write the bytes to a file path while still returning them.
|
|
386
386
|
*/
|
|
387
387
|
async download(options) {
|
|
@@ -419,22 +419,22 @@ export class SessionClient {
|
|
|
419
419
|
return operations.listSessions(this.#http, query);
|
|
420
420
|
}
|
|
421
421
|
/**
|
|
422
|
-
* Accessor over one session's captured
|
|
423
|
-
* opening a handle. Returns the SAME rich {@link
|
|
424
|
-
* `session.
|
|
422
|
+
* Accessor over one session's captured files, addressed by id without
|
|
423
|
+
* opening a handle. Returns the SAME rich {@link SessionFiles} surface as
|
|
424
|
+
* `session.files()` — `aex.sessions.files(id).list()` /
|
|
425
425
|
* `.read(selector)` / `.download()` / … — so the workspace client and the
|
|
426
426
|
* live handle share one accessor convention.
|
|
427
427
|
*/
|
|
428
|
-
|
|
429
|
-
return
|
|
428
|
+
files(sessionId) {
|
|
429
|
+
return sessionFiles(this.#http, sessionId, this.#fetch);
|
|
430
430
|
}
|
|
431
|
-
async
|
|
431
|
+
async start(options) {
|
|
432
432
|
const { message, deleteAfter, messageIdempotencyKey, stream, ...createOptions } = options;
|
|
433
|
-
assertNoLegacySessionFields(options, "Aex.sessions.
|
|
434
|
-
const input = normaliseSessionInput(message, "Aex.sessions.
|
|
435
|
-
// Derive the message key from the create key (like the CLI) so a retried
|
|
433
|
+
assertNoLegacySessionFields(options, "Aex.sessions.start");
|
|
434
|
+
const input = normaliseSessionInput(message, "Aex.sessions.start", "message");
|
|
435
|
+
// Derive the message key from the create key (like the CLI) so a retried session
|
|
436
436
|
// with the same `idempotencyKey` de-duplicates BOTH the create and the
|
|
437
|
-
// billable turn — never a duplicate billable
|
|
437
|
+
// billable turn — never a duplicate billable session turn.
|
|
438
438
|
const createKey = operations.resolveIdempotencyKey(createOptions.idempotencyKey);
|
|
439
439
|
const messageKey = messageIdempotencyKey !== undefined ? operations.resolveIdempotencyKey(messageIdempotencyKey) : deriveMessageKey(createKey);
|
|
440
440
|
const session = await this.create({ ...createOptions, idempotencyKey: createKey });
|
|
@@ -449,38 +449,38 @@ export class SessionClient {
|
|
|
449
449
|
}
|
|
450
450
|
}
|
|
451
451
|
/**
|
|
452
|
-
* Cross-
|
|
453
|
-
* `
|
|
454
|
-
* `query.
|
|
452
|
+
* Cross-session captured-file search (`aex.files.search`). Composed client-side (per-session
|
|
453
|
+
* `listSessionFiles` + the contracts file filter): scope a corpus with
|
|
454
|
+
* `query.sessionIds`, or omit it to scan every session in the workspace. Metadata-only
|
|
455
455
|
* (reference hits, no bytes); a content-shaped query throws a typed
|
|
456
456
|
* "content search unsupported".
|
|
457
457
|
*/
|
|
458
|
-
|
|
458
|
+
class SessionFilesSearchClient {
|
|
459
459
|
#http;
|
|
460
460
|
constructor(http) {
|
|
461
461
|
this.#http = http;
|
|
462
462
|
}
|
|
463
463
|
async search(query = {}) {
|
|
464
|
-
|
|
465
|
-
// Dedup the caller-supplied allow-list so a
|
|
464
|
+
assertMetadataOnlyFileSearch(query, "aex.files.search");
|
|
465
|
+
// Dedup the caller-supplied allow-list so a session repeated in `sessionIds` (from
|
|
466
466
|
// concatenating corpora) isn't scanned twice and doesn't inflate hit count.
|
|
467
|
-
const unscoped = query.
|
|
467
|
+
const unscoped = query.sessionIds === undefined;
|
|
468
468
|
const limit = query.limit ?? 100;
|
|
469
469
|
if (unscoped)
|
|
470
470
|
return this.#searchWorkspace(query, limit);
|
|
471
|
-
const
|
|
471
|
+
const sessionIds = [...new Set(query.sessionIds)];
|
|
472
472
|
const hits = [];
|
|
473
|
-
for (const
|
|
474
|
-
let
|
|
473
|
+
for (const sessionId of sessionIds) {
|
|
474
|
+
let files;
|
|
475
475
|
try {
|
|
476
|
-
|
|
476
|
+
files = await searchSessionFiles(this.#http, sessionId, query);
|
|
477
477
|
}
|
|
478
478
|
catch (err) {
|
|
479
|
-
if (unscoped &&
|
|
479
|
+
if (unscoped && isMissingFilesSession(err))
|
|
480
480
|
continue;
|
|
481
481
|
throw err;
|
|
482
482
|
}
|
|
483
|
-
for (const hit of
|
|
483
|
+
for (const hit of fileHits(sessionId, files, limit - hits.length)) {
|
|
484
484
|
hits.push(hit);
|
|
485
485
|
if (hits.length >= limit)
|
|
486
486
|
return { hits };
|
|
@@ -496,7 +496,7 @@ export class OutputsClient {
|
|
|
496
496
|
do {
|
|
497
497
|
if (cursor !== undefined) {
|
|
498
498
|
if (seenCursors.has(cursor)) {
|
|
499
|
-
throw new Error("aex.
|
|
499
|
+
throw new Error("aex.files.search: listSessions returned a repeated cursor");
|
|
500
500
|
}
|
|
501
501
|
seenCursors.add(cursor);
|
|
502
502
|
}
|
|
@@ -505,16 +505,16 @@ export class OutputsClient {
|
|
|
505
505
|
...(cursor ? { cursor } : {})
|
|
506
506
|
});
|
|
507
507
|
for (const session of page.sessions) {
|
|
508
|
-
let
|
|
508
|
+
let files;
|
|
509
509
|
try {
|
|
510
|
-
|
|
510
|
+
files = await searchSessionFiles(this.#http, session.id, query);
|
|
511
511
|
}
|
|
512
512
|
catch (err) {
|
|
513
|
-
if (
|
|
513
|
+
if (isMissingFilesSession(err))
|
|
514
514
|
continue;
|
|
515
515
|
throw err;
|
|
516
516
|
}
|
|
517
|
-
for (const hit of
|
|
517
|
+
for (const hit of fileHits(session.id, files, limit - hits.length)) {
|
|
518
518
|
hits.push(hit);
|
|
519
519
|
if (hits.length >= limit)
|
|
520
520
|
return { hits };
|
|
@@ -526,13 +526,13 @@ export class OutputsClient {
|
|
|
526
526
|
}
|
|
527
527
|
}
|
|
528
528
|
/**
|
|
529
|
-
* A first-class, lineage-discoverable SUBAGENT
|
|
530
|
-
* `session.children()
|
|
531
|
-
* (
|
|
532
|
-
* is resolvable through this handle; its lineage (`
|
|
529
|
+
* A first-class, lineage-discoverable SUBAGENT child session — handed out by
|
|
530
|
+
* `session.children()`, backed by the session-record facade
|
|
531
|
+
* (getSessionRecord/events/files), NOT `openSession`. Every child the platform hands you
|
|
532
|
+
* is resolvable through this handle; its lineage (`parentSessionId`/`depth`) and
|
|
533
533
|
* terminal outcome (`status`) are first-class.
|
|
534
534
|
*/
|
|
535
|
-
export class
|
|
535
|
+
export class ChildSessionHandle {
|
|
536
536
|
#http;
|
|
537
537
|
#fetch;
|
|
538
538
|
#ref;
|
|
@@ -544,81 +544,70 @@ export class ChildRunHandle {
|
|
|
544
544
|
get id() {
|
|
545
545
|
return this.#ref.id;
|
|
546
546
|
}
|
|
547
|
-
get
|
|
548
|
-
return this.#ref.
|
|
547
|
+
get parentSessionId() {
|
|
548
|
+
return this.#ref.parentSessionId;
|
|
549
549
|
}
|
|
550
550
|
get depth() {
|
|
551
551
|
return this.#ref.depth;
|
|
552
552
|
}
|
|
553
|
-
/** The child's
|
|
553
|
+
/** The child's session status (a real session terminal outcome once settled). */
|
|
554
554
|
get status() {
|
|
555
555
|
return this.#ref.status;
|
|
556
556
|
}
|
|
557
557
|
get ref() {
|
|
558
558
|
return this.#ref;
|
|
559
559
|
}
|
|
560
|
-
/** Re-read the child
|
|
560
|
+
/** Re-read the child session record (status, lineage, costTelemetry). */
|
|
561
561
|
get() {
|
|
562
|
-
return operations.
|
|
562
|
+
return operations.getSessionRecord(this.#http, this.id);
|
|
563
563
|
}
|
|
564
|
-
/** The child's events over the
|
|
564
|
+
/** The child's events over the session-record facade (`/sessions/:id/events`). */
|
|
565
565
|
events() {
|
|
566
|
-
return
|
|
566
|
+
return childSessionEventsAccessor(this.#http, this.id);
|
|
567
567
|
}
|
|
568
|
-
/** The child's captured
|
|
569
|
-
|
|
570
|
-
return
|
|
568
|
+
/** The child's captured files over the session-record facade (`/sessions/:id/files`). */
|
|
569
|
+
files() {
|
|
570
|
+
return sessionFiles(this.#http, this.id, this.#fetch);
|
|
571
571
|
}
|
|
572
572
|
/** This child's own subagent children (recursive lineage). */
|
|
573
573
|
async children() {
|
|
574
|
-
const refs = await operations.
|
|
575
|
-
return refs.map((ref) => new
|
|
574
|
+
const refs = await operations.listSessionChildren(this.#http, this.id);
|
|
575
|
+
return refs.map((ref) => new ChildSessionHandle(this.#http, ref, this.#fetch));
|
|
576
576
|
}
|
|
577
|
-
/** Cancel the child
|
|
577
|
+
/** Cancel the child session (session record facade `POST /sessions/:id/cancel`). */
|
|
578
578
|
cancel() {
|
|
579
|
-
return operations.
|
|
579
|
+
return operations.cancelSession(this.#http, this.id).then(() => undefined);
|
|
580
580
|
}
|
|
581
581
|
}
|
|
582
|
-
/**
|
|
583
|
-
function
|
|
582
|
+
/** SessionRecord-facade events accessor (list + polling stream) — used by {@link ChildSessionHandle}. */
|
|
583
|
+
function childSessionEventsAccessor(http, id) {
|
|
584
584
|
return {
|
|
585
|
-
list: async () => (await operations.
|
|
586
|
-
stream: (options) =>
|
|
587
|
-
};
|
|
588
|
-
}
|
|
589
|
-
/** Run-facade outputs accessor — used by {@link ChildRunHandle}. */
|
|
590
|
-
function runOutputsAccessor(http, id, fetchLike) {
|
|
591
|
-
void fetchLike;
|
|
592
|
-
return {
|
|
593
|
-
list: (query) => operations.listOutputs(http, id, query),
|
|
594
|
-
find: (query) => operations.findOutputs(http, id, query),
|
|
595
|
-
findOne: (query) => operations.findOutput(http, id, query),
|
|
596
|
-
read: (selector, options) => operations.readOutputText(http, id, selector, options),
|
|
597
|
-
link: (selectorOrQuery, options) => operations.outputLink(http, id, selectorOrQuery, options),
|
|
598
|
-
download: (selector, options) => downloadSessionOutput(http, id, selector, options)
|
|
585
|
+
list: async () => (await operations.listSessionRecordEvents(http, id)).map(asAexEventView),
|
|
586
|
+
stream: (options) => streamChildSessionEventsPolling(http, id, options ?? {})
|
|
599
587
|
};
|
|
600
588
|
}
|
|
589
|
+
/** SessionRecord-facade files accessor — used by {@link ChildSessionHandle}. */
|
|
601
590
|
/**
|
|
602
|
-
* Poll a
|
|
603
|
-
* the signal aborts, or the caller breaks the iterator. Uses `
|
|
604
|
-
* `getSession`) so it resolves for a child
|
|
591
|
+
* Poll a child session's events (via the session record facade) until it reaches a terminal status,
|
|
592
|
+
* the signal aborts, or the caller breaks the iterator. Uses `getSessionRecord` (not
|
|
593
|
+
* `getSession`) so it resolves for a child session that has no session facade.
|
|
605
594
|
*/
|
|
606
|
-
async function*
|
|
595
|
+
async function* streamChildSessionEventsPolling(http, id, options) {
|
|
607
596
|
if (options.signal?.aborted)
|
|
608
597
|
return;
|
|
609
598
|
const seenIds = new Set();
|
|
610
599
|
const intervalMs = options.intervalMs ?? 1_000;
|
|
611
600
|
const signal = options.signal;
|
|
612
601
|
while (!signal?.aborted) {
|
|
613
|
-
const events = await operations.
|
|
602
|
+
const events = await operations.listSessionRecordEvents(http, id);
|
|
614
603
|
for (const event of events) {
|
|
615
604
|
if (!seenIds.has(event.id)) {
|
|
616
605
|
seenIds.add(event.id);
|
|
617
606
|
yield asAexEventView(event);
|
|
618
607
|
}
|
|
619
608
|
}
|
|
620
|
-
const
|
|
621
|
-
if (isSessionParked(
|
|
609
|
+
const session = await operations.getSessionRecord(http, id);
|
|
610
|
+
if (isSessionParked(session.status))
|
|
622
611
|
return;
|
|
623
612
|
try {
|
|
624
613
|
await sleep(intervalMs, signal);
|
|
@@ -696,7 +685,7 @@ async function* streamSessionEventsPolling(http, id, options) {
|
|
|
696
685
|
const intervalMs = options.intervalMs ?? 1_000;
|
|
697
686
|
const signal = options.signal;
|
|
698
687
|
while (!signal?.aborted) {
|
|
699
|
-
const events = await operations.
|
|
688
|
+
const events = await operations.listSessionRecordEvents(http, id);
|
|
700
689
|
for (const event of events) {
|
|
701
690
|
if (!seenIds.has(event.id)) {
|
|
702
691
|
seenIds.add(event.id);
|
|
@@ -728,8 +717,8 @@ async function* streamSessionEnvelopes(http, id, options) {
|
|
|
728
717
|
from: options.from ?? 0,
|
|
729
718
|
fetchTicket: async () => (await operations.getSessionCoordinatorTicket(http, id)).ticket,
|
|
730
719
|
// settleConsistent ends the stream on the post-mirror barrier instead of
|
|
731
|
-
// the earlier
|
|
732
|
-
isTerminal: options.settleConsistent ?
|
|
720
|
+
// the earlier TURN_FINISHED UX signal.
|
|
721
|
+
isTerminal: options.settleConsistent ? isSessionSettled : isSessionEnvelopeTerminal,
|
|
733
722
|
...(options.signal ? { signal: options.signal } : {}),
|
|
734
723
|
...(options.idleTimeoutMs !== undefined ? { idleTimeoutMs: options.idleTimeoutMs } : {}),
|
|
735
724
|
...(options.pingIntervalMs !== undefined ? { pingIntervalMs: options.pingIntervalMs } : {}),
|
|
@@ -740,93 +729,93 @@ async function* streamSessionEnvelopes(http, id, options) {
|
|
|
740
729
|
}
|
|
741
730
|
}
|
|
742
731
|
function isSessionEnvelopeTerminal(event) {
|
|
743
|
-
return event.type === "
|
|
732
|
+
return event.type === "TURN_FINISHED" || event.type === "TURN_ERROR" || isSessionParkedEvent(event);
|
|
744
733
|
}
|
|
745
734
|
/**
|
|
746
|
-
* Download captured
|
|
735
|
+
* Download captured files. No selector → the full files namespace as a
|
|
747
736
|
* zip; a selector → one file's raw bytes. Module-level so
|
|
748
|
-
* `SessionHandle.
|
|
737
|
+
* `SessionHandle.files()` can hand it to its accessor object literal.
|
|
749
738
|
*/
|
|
750
|
-
async function
|
|
751
|
-
// One selector-resolution path: the contracts `
|
|
739
|
+
async function downloadSessionFile(http, id, selector, options) {
|
|
740
|
+
// One selector-resolution path: the contracts `downloadSessionFile` lists-if-path
|
|
752
741
|
// then downloads, throwing with PUBLIC verb names — no duplicated resolver.
|
|
753
742
|
const transferOptions = options?.timeoutMs === undefined ? undefined : { timeoutMs: options.timeoutMs };
|
|
754
743
|
const bytes = selector === undefined
|
|
755
|
-
? await operations.
|
|
756
|
-
: (await operations.
|
|
744
|
+
? await operations.downloadSessionFiles(http, id, transferOptions)
|
|
745
|
+
: (await operations.downloadSessionFile(http, id, selector, transferOptions)).bytes;
|
|
757
746
|
return writeOptionalFile(bytes, options?.to);
|
|
758
747
|
}
|
|
759
748
|
/**
|
|
760
|
-
* Build the
|
|
761
|
-
* `SessionHandle.
|
|
762
|
-
* `SessionClient.
|
|
763
|
-
* expose the identical rich {@link
|
|
749
|
+
* Build the files accessor for a session id. Shared by
|
|
750
|
+
* `SessionHandle.files()` (bound to the live handle) and
|
|
751
|
+
* `SessionClient.files(id)` (addressed by id without opening a handle), so both
|
|
752
|
+
* expose the identical rich {@link SessionFiles} surface — one accessor
|
|
764
753
|
* convention, one implementation.
|
|
765
754
|
*/
|
|
766
|
-
function
|
|
767
|
-
const list = (query) => operations.
|
|
755
|
+
function sessionFiles(http, id, fetchLike) {
|
|
756
|
+
const list = (query) => operations.listSessionFiles(http, id, query);
|
|
768
757
|
return {
|
|
769
758
|
list,
|
|
770
759
|
last: async () => (await list()).at(-1),
|
|
771
760
|
first: async () => (await list())[0],
|
|
772
|
-
read: (selector, options) => operations.
|
|
773
|
-
find: (query) => operations.
|
|
774
|
-
findOne: (query) => operations.
|
|
761
|
+
read: (selector, options) => operations.readSessionFileText(http, id, selector, options),
|
|
762
|
+
find: (query) => operations.findSessionFiles(http, id, query),
|
|
763
|
+
findOne: (query) => operations.findSessionFile(http, id, query),
|
|
775
764
|
search: async (query = {}) => {
|
|
776
|
-
|
|
777
|
-
const
|
|
778
|
-
return { hits:
|
|
765
|
+
assertMetadataOnlyFileSearch(query, "files().search");
|
|
766
|
+
const files = await searchSessionFiles(http, id, query);
|
|
767
|
+
return { hits: fileHits(id, files, query.limit ?? 100) };
|
|
779
768
|
},
|
|
780
|
-
link: (selectorOrQuery, options) => operations.
|
|
769
|
+
link: (selectorOrQuery, options) => operations.sessionFileLink(http, id, selectorOrQuery, options),
|
|
781
770
|
fetch: async (selectorOrQuery, options) => {
|
|
782
|
-
const link = await operations.
|
|
771
|
+
const link = await operations.sessionFileLink(http, id, selectorOrQuery, options);
|
|
783
772
|
return (fetchLike ?? globalThis.fetch)(link.url);
|
|
784
773
|
},
|
|
785
|
-
download: (selector, options) =>
|
|
774
|
+
download: (selector, options) => downloadSessionFile(http, id, selector, options)
|
|
786
775
|
};
|
|
787
776
|
}
|
|
788
777
|
/**
|
|
789
|
-
* List one
|
|
778
|
+
* List one session's files matching a metadata search: `extension`/`contentType`
|
|
790
779
|
* via the contracts filter, and `filename` via {@link operations.toFilenameMatcher}
|
|
791
780
|
* — a case-insensitive SUBSTRING for a string, `.test` for a RegExp (RegExp-safe;
|
|
792
781
|
* no `escapeRegExp` footgun on a reused pattern).
|
|
793
782
|
*/
|
|
794
|
-
async function
|
|
783
|
+
async function searchSessionFiles(http, sessionId, query) {
|
|
795
784
|
const listQuery = {
|
|
796
785
|
...(query.extension !== undefined ? { extension: query.extension } : {}),
|
|
797
786
|
...(query.contentType !== undefined ? { contentType: query.contentType } : {})
|
|
798
787
|
};
|
|
799
|
-
const
|
|
788
|
+
const files = await operations.listSessionFiles(http, sessionId, Object.keys(listQuery).length > 0 ? listQuery : undefined);
|
|
800
789
|
if (query.filename === undefined)
|
|
801
|
-
return
|
|
790
|
+
return files;
|
|
802
791
|
const match = operations.toFilenameMatcher(query.filename);
|
|
803
|
-
return
|
|
792
|
+
return files.filter((file) => typeof file.filename === "string" && match(file.filename));
|
|
804
793
|
}
|
|
805
794
|
/**
|
|
806
795
|
* Fail-fast on a CONTENT-shaped search: the search surface is metadata-only, so
|
|
807
796
|
* a `content`/`text`/`query` needle throws a typed error rather than silently
|
|
808
|
-
* returning 0 hits (which reads as "no matches" for a query that was never
|
|
797
|
+
* returning 0 hits (which reads as "no matches" for a query that was never executed).
|
|
809
798
|
*/
|
|
810
|
-
function
|
|
799
|
+
function assertMetadataOnlyFileSearch(query, surface) {
|
|
811
800
|
for (const key of ["content", "text", "query", "grep", "body"]) {
|
|
812
801
|
if (Object.prototype.hasOwnProperty.call(query, key)) {
|
|
813
|
-
throw new
|
|
802
|
+
throw new SessionConfigValidationError(`${surface}: content search is not supported — search matches on filename/extension/contentType metadata only`, { field: key });
|
|
814
803
|
}
|
|
815
804
|
}
|
|
816
805
|
}
|
|
817
|
-
/** Project a
|
|
818
|
-
function
|
|
806
|
+
/** Project a session's files to reference-only {@link SessionFileSearchHit}s, capped. */
|
|
807
|
+
function fileHits(sessionId, files, limit) {
|
|
819
808
|
const cap = Math.max(0, Math.floor(limit));
|
|
820
809
|
if (cap === 0)
|
|
821
810
|
return [];
|
|
822
811
|
const hits = [];
|
|
823
|
-
for (const
|
|
812
|
+
for (const file of files) {
|
|
824
813
|
hits.push({
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
...(
|
|
828
|
-
...(
|
|
829
|
-
...(
|
|
814
|
+
sessionId,
|
|
815
|
+
fileId: file.id,
|
|
816
|
+
...(file.filename !== undefined ? { filename: file.filename } : {}),
|
|
817
|
+
...(file.sizeBytes !== undefined ? { sizeBytes: file.sizeBytes } : {}),
|
|
818
|
+
...(file.contentType !== undefined ? { contentType: file.contentType } : {})
|
|
830
819
|
});
|
|
831
820
|
if (hits.length >= cap)
|
|
832
821
|
break;
|
|
@@ -846,7 +835,7 @@ function messageFromWire(message) {
|
|
|
846
835
|
function isMissingMessagesEndpoint(err) {
|
|
847
836
|
return err instanceof AexApiError && (err.status === 404 || err.status === 405 || err.status === 501);
|
|
848
837
|
}
|
|
849
|
-
function
|
|
838
|
+
function isMissingFilesSession(err) {
|
|
850
839
|
return err instanceof AexApiError && err.status === 404;
|
|
851
840
|
}
|
|
852
841
|
function projectAssistantMessages(events) {
|
|
@@ -893,7 +882,7 @@ function projectAssistantMessages(events) {
|
|
|
893
882
|
function assistantTextFromEvents(events) {
|
|
894
883
|
return assistantTextEntriesFromEvents(events).map((entry) => entry.text).join("");
|
|
895
884
|
}
|
|
896
|
-
function
|
|
885
|
+
function turnTraceFromEvents(events) {
|
|
897
886
|
return {
|
|
898
887
|
toolCalls: toolCallsFromEvents(events),
|
|
899
888
|
usage: usageFromEvents(events),
|
|
@@ -1059,7 +1048,7 @@ function carriedOutcome(event) {
|
|
|
1059
1048
|
: undefined;
|
|
1060
1049
|
}
|
|
1061
1050
|
function isSessionTurnTerminalEvent(event, turnSeq) {
|
|
1062
|
-
if (event.type === "
|
|
1051
|
+
if (event.type === "TURN_FINISHED" || event.type === "TURN_ERROR") {
|
|
1063
1052
|
return true;
|
|
1064
1053
|
}
|
|
1065
1054
|
const name = customName(event);
|
|
@@ -1076,7 +1065,7 @@ function isSessionTurnTerminalEvent(event, turnSeq) {
|
|
|
1076
1065
|
/**
|
|
1077
1066
|
* Read the CARRIED terminal outcome/park from the turn's terminal event — never
|
|
1078
1067
|
* a lossy re-derivation. Prefers an explicit `data.value.outcome`, then the
|
|
1079
|
-
* `aex.session.<name>` name, then
|
|
1068
|
+
* `aex.session.<name>` name, then TURN_ERROR→failed / TURN_FINISHED→succeeded.
|
|
1080
1069
|
*/
|
|
1081
1070
|
function terminalSessionStatusFromEvents(events, turnSeq) {
|
|
1082
1071
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
@@ -1086,9 +1075,9 @@ function terminalSessionStatusFromEvents(events, turnSeq) {
|
|
|
1086
1075
|
const carried = carriedOutcome(event);
|
|
1087
1076
|
if (carried !== undefined)
|
|
1088
1077
|
return carried;
|
|
1089
|
-
if (event.type === "
|
|
1078
|
+
if (event.type === "TURN_ERROR")
|
|
1090
1079
|
return "failed";
|
|
1091
|
-
if (event.type === "
|
|
1080
|
+
if (event.type === "TURN_FINISHED")
|
|
1092
1081
|
return "succeeded";
|
|
1093
1082
|
const name = customName(event);
|
|
1094
1083
|
if (name !== null && name in SESSION_TERMINAL_EVENT_READS)
|
|
@@ -1135,15 +1124,15 @@ function providerUsageOf(record) {
|
|
|
1135
1124
|
return telemetry?.providerUsage;
|
|
1136
1125
|
}
|
|
1137
1126
|
/**
|
|
1138
|
-
* The IMMEDIATE authoritative failure text — the terminal `
|
|
1127
|
+
* The IMMEDIATE authoritative failure text — the terminal `TURN_ERROR` event's
|
|
1139
1128
|
* `data.failureMessage`. `result.error` reads this FIRST so a failed (e.g.
|
|
1140
|
-
* bad-BYOK)
|
|
1129
|
+
* bad-BYOK) session's error is never empty even before the settle-lagged
|
|
1141
1130
|
* `sessionRecord.errorMessage` lands.
|
|
1142
1131
|
*/
|
|
1143
1132
|
function failureFromEvents(events) {
|
|
1144
1133
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
1145
1134
|
const event = events[i];
|
|
1146
|
-
if (event.type !== "
|
|
1135
|
+
if (event.type !== "TURN_ERROR")
|
|
1147
1136
|
continue;
|
|
1148
1137
|
const data = asRecord(event.data);
|
|
1149
1138
|
for (const key of ["failureMessage", "message", "error"]) {
|
|
@@ -1180,10 +1169,10 @@ function outcomeFromEvents(events) {
|
|
|
1180
1169
|
}
|
|
1181
1170
|
/**
|
|
1182
1171
|
* Build the ONE unified settled turn result (shared by `session.send().done()`
|
|
1183
|
-
* and `Aex.
|
|
1172
|
+
* and `Aex.start`): the terminal outcome `status`, `ok`, `costUsd` (>= 0),
|
|
1184
1173
|
* `usage` (from `costTelemetry.providerUsage`), and event-first `error`.
|
|
1185
1174
|
*/
|
|
1186
|
-
function settledTurnResult(sessionId, session, turn, events,
|
|
1175
|
+
function settledTurnResult(sessionId, session, turn, events, files, messages, read) {
|
|
1187
1176
|
const effectiveRead = read ?? sessionRecordRead(session);
|
|
1188
1177
|
const status = readToOutcome(effectiveRead);
|
|
1189
1178
|
const ok = isTerminalReadOk(effectiveRead);
|
|
@@ -1203,7 +1192,7 @@ function settledTurnResult(sessionId, session, turn, events, outputs, messages,
|
|
|
1203
1192
|
...(error !== undefined ? { error } : {}),
|
|
1204
1193
|
text: assistantTextFromEvents(events),
|
|
1205
1194
|
events,
|
|
1206
|
-
|
|
1195
|
+
files,
|
|
1207
1196
|
messages,
|
|
1208
1197
|
...(outcome !== undefined ? { outcome } : {})
|
|
1209
1198
|
};
|
|
@@ -1241,8 +1230,10 @@ export class AgentsMdClient {
|
|
|
1241
1230
|
*/
|
|
1242
1231
|
export class FilesClient {
|
|
1243
1232
|
#http;
|
|
1233
|
+
#sessionSearch;
|
|
1244
1234
|
constructor(http) {
|
|
1245
1235
|
this.#http = http;
|
|
1236
|
+
this.#sessionSearch = new SessionFilesSearchClient(http);
|
|
1246
1237
|
}
|
|
1247
1238
|
list() {
|
|
1248
1239
|
return operations.listFiles(this.#http);
|
|
@@ -1253,6 +1244,10 @@ export class FilesClient {
|
|
|
1253
1244
|
delete(fileId) {
|
|
1254
1245
|
return operations.deleteFile(this.#http, fileId);
|
|
1255
1246
|
}
|
|
1247
|
+
/** Cross-session captured-file metadata search. */
|
|
1248
|
+
search(query) {
|
|
1249
|
+
return this.#sessionSearch.search(query);
|
|
1250
|
+
}
|
|
1256
1251
|
}
|
|
1257
1252
|
/**
|
|
1258
1253
|
* Workspace skill registry operations exposed under `client.skills`.
|
|
@@ -1280,7 +1275,7 @@ export class SkillsClient {
|
|
|
1280
1275
|
* Workspace secret management exposed under `client.secrets`, mirroring
|
|
1281
1276
|
* `client.agentsMd` / `client.files`.
|
|
1282
1277
|
*
|
|
1283
|
-
* Lifecycle parity with assets: a `Secret.value(...)` is per-
|
|
1278
|
+
* Lifecycle parity with assets: a `Secret.value(...)` is per-session and
|
|
1284
1279
|
* gone at terminal; `set` (or promoting an ephemeral via `secret.upload`)
|
|
1285
1280
|
* persists a named, searchable workspace secret you can `get` (metadata),
|
|
1286
1281
|
* `rotate`, `list`, and `delete`. The identity is the `name`; the value rotates
|
|
@@ -1335,7 +1330,7 @@ function unwrapSecretValue(value) {
|
|
|
1335
1330
|
* Unified user-facing client for the aex platform. The same class
|
|
1336
1331
|
* powers the published `@aexhq/sdk` SDK and (under the hood) every host-side
|
|
1337
1332
|
* subcommand of the in-container `aex` CLI. All operations talk to
|
|
1338
|
-
* the dashboard BFF and operate on durable
|
|
1333
|
+
* the dashboard BFF and operate on durable session records.
|
|
1339
1334
|
*
|
|
1340
1335
|
* The SDK never asks the caller for a workspace id — workspace identity
|
|
1341
1336
|
* is derived server-side from the API key on every request. Use
|
|
@@ -1351,8 +1346,6 @@ export class Aex {
|
|
|
1351
1346
|
skills;
|
|
1352
1347
|
secrets;
|
|
1353
1348
|
sessions;
|
|
1354
|
-
/** Cross-run output search (`aex.outputs.search(...)`). */
|
|
1355
|
-
outputs;
|
|
1356
1349
|
constructor(options, overrides = {}) {
|
|
1357
1350
|
const resolved = typeof options === "string" ? { ...overrides, apiKey: options } : options;
|
|
1358
1351
|
const apiKey = resolved.apiKey;
|
|
@@ -1390,7 +1383,6 @@ export class Aex {
|
|
|
1390
1383
|
this.skills = new SkillsClient(this.#http);
|
|
1391
1384
|
this.secrets = new SecretsClient(this.#http);
|
|
1392
1385
|
this.sessions = new SessionClient(this.#http, (options) => this.#buildSessionCreateRequest(options), this.#fetch);
|
|
1393
|
-
this.outputs = new OutputsClient(this.#http);
|
|
1394
1386
|
}
|
|
1395
1387
|
/**
|
|
1396
1388
|
* Internal: satisfies the `SecretUploader` surface so a
|
|
@@ -1436,7 +1428,7 @@ export class Aex {
|
|
|
1436
1428
|
/**
|
|
1437
1429
|
* Internal: upsert already-uploaded skill metadata into the workspace registry.
|
|
1438
1430
|
* The bytes are staged through `_uploadAsset`; this call binds the content hash
|
|
1439
|
-
* to a mutable workspace skill name before the
|
|
1431
|
+
* to a mutable workspace skill name before the session references that name.
|
|
1440
1432
|
*/
|
|
1441
1433
|
async _upsertSkill(args) {
|
|
1442
1434
|
return operations.upsertSkill(this.#http, args);
|
|
@@ -1445,34 +1437,34 @@ export class Aex {
|
|
|
1445
1437
|
* Convenience one-shot on top of the canonical session API:
|
|
1446
1438
|
* open a session, send `message` as the first turn, stream until the session
|
|
1447
1439
|
* parks (`idle` / `suspended` / `error`), then return the collected text,
|
|
1448
|
-
* events,
|
|
1449
|
-
* so callers can resume later with `openSession(
|
|
1440
|
+
* events, files, and session record. The returned `sessionId` is the session id,
|
|
1441
|
+
* so callers can resume later with `openSession(sessionId)`.
|
|
1450
1442
|
*/
|
|
1451
|
-
async
|
|
1443
|
+
async start(options, opts = {}) {
|
|
1452
1444
|
const scopedSignal = scopedAbortSignal(opts.timeoutMs);
|
|
1453
1445
|
try {
|
|
1454
1446
|
const { message, deleteAfter, messageIdempotencyKey, stream, ...createOptions } = options;
|
|
1455
|
-
assertNoLegacySessionFields(options, "Aex.
|
|
1456
|
-
const input = normaliseSessionInput(message, "Aex.
|
|
1457
|
-
assertNoSessionSendSignal(stream, "Aex.
|
|
1447
|
+
assertNoLegacySessionFields(options, "Aex.start");
|
|
1448
|
+
const input = normaliseSessionInput(message, "Aex.start", "message");
|
|
1449
|
+
assertNoSessionSendSignal(stream, "Aex.start stream");
|
|
1458
1450
|
const sendOptions = {
|
|
1459
1451
|
...(stream ?? {}),
|
|
1460
1452
|
...(scopedSignal?.signal ? { signal: scopedSignal.signal } : {}),
|
|
1461
1453
|
...(opts.webSocketFactory ? { webSocketFactory: opts.webSocketFactory } : {}),
|
|
1462
1454
|
...(opts.idleTimeoutMs !== undefined ? { idleTimeoutMs: opts.idleTimeoutMs } : {}),
|
|
1463
1455
|
...(opts.pingIntervalMs !== undefined ? { pingIntervalMs: opts.pingIntervalMs } : {}),
|
|
1464
|
-
//
|
|
1456
|
+
// start()/done() await settle by DEFAULT; opt out with `await: 'park'`.
|
|
1465
1457
|
...(opts.await !== undefined ? { await: opts.await } : {})
|
|
1466
1458
|
};
|
|
1467
1459
|
// Derive the message key from the create key (like the CLI) so a retried
|
|
1468
|
-
//
|
|
1469
|
-
// billable turn server-side — never a duplicate billable
|
|
1460
|
+
// session with the same `idempotencyKey` de-duplicates BOTH the create and the
|
|
1461
|
+
// billable turn server-side — never a duplicate billable session turn (sdk-dx-3).
|
|
1470
1462
|
const createKey = operations.resolveIdempotencyKey(createOptions.idempotencyKey);
|
|
1471
1463
|
const messageKey = messageIdempotencyKey !== undefined ? operations.resolveIdempotencyKey(messageIdempotencyKey) : deriveMessageKey(createKey);
|
|
1472
1464
|
const session = await this.sessions.create({ ...createOptions, idempotencyKey: createKey });
|
|
1473
1465
|
// ONE terminal boundary: `done()` awaits settle by default, so the turn
|
|
1474
|
-
// result already carries the terminal outcome + cost + usage. `
|
|
1475
|
-
// reshapes it — `done()` == `
|
|
1466
|
+
// result already carries the terminal outcome + cost + usage. `start()` just
|
|
1467
|
+
// reshapes it — `done()` == `start()` (WS3).
|
|
1476
1468
|
let turnResult;
|
|
1477
1469
|
try {
|
|
1478
1470
|
turnResult = await sendSessionInternal(session, input, { ...sendOptions, idempotencyKey: messageKey }).done();
|
|
@@ -1481,24 +1473,23 @@ export class Aex {
|
|
|
1481
1473
|
if (scopedSignal?.signal.aborted) {
|
|
1482
1474
|
// The client-side wait budget (opts.timeoutMs) expired. Parity with
|
|
1483
1475
|
// SessionHandle.wait(): THROW rather than a misleading silent result;
|
|
1484
|
-
// the
|
|
1485
|
-
throw new
|
|
1476
|
+
// the session continues server-side.
|
|
1477
|
+
throw new SessionStateError(`Aex.start: timed out after ${opts.timeoutMs}ms waiting for session ${session.id} to park; the session ` +
|
|
1486
1478
|
`continues server-side — cancel via session.cancel() or resume with openSession(${JSON.stringify(session.id)})`);
|
|
1487
1479
|
}
|
|
1488
1480
|
throw err;
|
|
1489
1481
|
}
|
|
1490
|
-
const
|
|
1482
|
+
const sessionId = turnResult.sessionId;
|
|
1491
1483
|
if (deleteAfter) {
|
|
1492
1484
|
await session.delete();
|
|
1493
1485
|
}
|
|
1494
1486
|
const sessionRecord = turnResult.session;
|
|
1495
|
-
const
|
|
1496
|
-
const trace =
|
|
1487
|
+
const record = sessionToSessionRecord(sessionRecord);
|
|
1488
|
+
const trace = turnTraceFromEvents(turnResult.events);
|
|
1497
1489
|
const outcome = turnResult.outcome;
|
|
1498
1490
|
const result = {
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
sessionId: runId,
|
|
1491
|
+
sessionId,
|
|
1492
|
+
record,
|
|
1502
1493
|
session: sessionRecord,
|
|
1503
1494
|
turn: turnResult.turn,
|
|
1504
1495
|
status: turnResult.status,
|
|
@@ -1509,7 +1500,7 @@ export class Aex {
|
|
|
1509
1500
|
messages: turnResult.messages,
|
|
1510
1501
|
events: turnResult.events,
|
|
1511
1502
|
trace,
|
|
1512
|
-
|
|
1503
|
+
files: turnResult.files,
|
|
1513
1504
|
...(turnResult.error !== undefined ? { error: turnResult.error } : {}),
|
|
1514
1505
|
...(outcome !== undefined ? { outcome } : {})
|
|
1515
1506
|
};
|
|
@@ -1527,7 +1518,7 @@ export class Aex {
|
|
|
1527
1518
|
...(throttle.retryAfterMs !== undefined ? { retryAfterMs: throttle.retryAfterMs } : {})
|
|
1528
1519
|
});
|
|
1529
1520
|
}
|
|
1530
|
-
throw new
|
|
1521
|
+
throw new SessionStateError(`Aex.start: session ${sessionId} ended ${turnResult.status}${turnResult.error ? `: ${turnResult.error}` : ""}`, { sessionId, status: turnResult.status });
|
|
1531
1522
|
}
|
|
1532
1523
|
return result;
|
|
1533
1524
|
}
|
|
@@ -1537,37 +1528,37 @@ export class Aex {
|
|
|
1537
1528
|
}
|
|
1538
1529
|
/**
|
|
1539
1530
|
* Fire-and-forget: create the session and POST its first turn WITHOUT awaiting
|
|
1540
|
-
* settle (the honest counterpart to await-settle `
|
|
1541
|
-
* `
|
|
1542
|
-
* a `webhook`, the event stream, or `openSession(
|
|
1531
|
+
* settle (the honest counterpart to await-settle `start()`). Resolves with the
|
|
1532
|
+
* `sessionId` + a resumable {@link SessionHandle} immediately; observe the session via
|
|
1533
|
+
* a `webhook`, the event stream, or `openSession(sessionId)`.
|
|
1543
1534
|
*/
|
|
1544
1535
|
async submit(options) {
|
|
1545
1536
|
const { message, deleteAfter: _deleteAfter, messageIdempotencyKey, stream: _stream, ...createOptions } = options;
|
|
1546
1537
|
assertNoLegacySessionFields(options, "Aex.submit");
|
|
1547
1538
|
const input = normaliseSessionInput(message, "Aex.submit", "message");
|
|
1548
1539
|
const request = await this.#buildSessionCreateRequest(createOptions);
|
|
1549
|
-
const {
|
|
1540
|
+
const { sessionId, session } = await operations.submit(this.#http, { ...request, input }, {
|
|
1550
1541
|
idempotencyKey: operations.resolveIdempotencyKey(createOptions.idempotencyKey),
|
|
1551
1542
|
...(messageIdempotencyKey !== undefined ? { messageIdempotencyKey } : {})
|
|
1552
1543
|
});
|
|
1553
|
-
return {
|
|
1544
|
+
return { sessionId, session: new SessionHandle(this.#http, session, this.#fetch) };
|
|
1554
1545
|
}
|
|
1555
1546
|
/**
|
|
1556
|
-
*
|
|
1547
|
+
* SessionRecord a batch of one-shot items with a bounded worker pool, returning every
|
|
1557
1548
|
* item's settled result PLUS a REAL cost/usage rollup. The rollup is honest
|
|
1558
|
-
* because each `
|
|
1549
|
+
* because each `start()` awaits settle, so each item's `costUsd`/`usage` is
|
|
1559
1550
|
* populated — a failed item lands in `failed[]`, never a silent `$0` success.
|
|
1560
1551
|
* Concurrency is clamped below the workspace tier cap.
|
|
1561
1552
|
*/
|
|
1562
1553
|
async batch(items, options = {}) {
|
|
1563
1554
|
if (!Array.isArray(items)) {
|
|
1564
|
-
throw new
|
|
1555
|
+
throw new SessionConfigValidationError("Aex.batch: items must be an array of session options");
|
|
1565
1556
|
}
|
|
1566
1557
|
const concurrency = Math.min(Math.max(1, Math.floor(options.concurrency ?? DEFAULT_BATCH_CONCURRENCY)), BATCH_MAX_CONCURRENCY);
|
|
1567
1558
|
const results = (await mapWithConcurrency(items, concurrency, async (item) => {
|
|
1568
|
-
const result = await this.
|
|
1559
|
+
const result = await this.start(item);
|
|
1569
1560
|
return {
|
|
1570
|
-
|
|
1561
|
+
sessionId: result.sessionId,
|
|
1571
1562
|
status: result.status,
|
|
1572
1563
|
ok: result.ok,
|
|
1573
1564
|
costUsd: result.costUsd,
|
|
@@ -1585,15 +1576,15 @@ export class Aex {
|
|
|
1585
1576
|
}
|
|
1586
1577
|
async #buildSessionCreateRequest(options) {
|
|
1587
1578
|
if (!options || typeof options !== "object") {
|
|
1588
|
-
throw new
|
|
1579
|
+
throw new SessionConfigValidationError("Aex.openSession: options is required");
|
|
1589
1580
|
}
|
|
1590
1581
|
assertNoLegacySessionFields(options, "Aex.openSession");
|
|
1591
|
-
// `message` belongs to the one-shot surfaces (`run` / `sessions.
|
|
1582
|
+
// `message` belongs to the one-shot surfaces (`run` / `sessions.start`), which
|
|
1592
1583
|
// strip it before creating the session. Passing it here used to be SILENTLY
|
|
1593
1584
|
// dropped — the session was created empty, idled from birth, and auto-
|
|
1594
1585
|
// suspended at the idle TTL without ever running a turn.
|
|
1595
1586
|
if (Object.prototype.hasOwnProperty.call(options, "message")) {
|
|
1596
|
-
throw new
|
|
1587
|
+
throw new SessionConfigValidationError("Aex.openSession: message is not a supported option; sessions are created without a first " +
|
|
1597
1588
|
"message — send it with session.send(...), or use run({ message }) for a one-shot.");
|
|
1598
1589
|
}
|
|
1599
1590
|
// Model is REQUIRED and checked BEFORE the provider key, so omitting `model`
|
|
@@ -1634,12 +1625,12 @@ export class Aex {
|
|
|
1634
1625
|
// server).
|
|
1635
1626
|
try {
|
|
1636
1627
|
parseRuntimeSize(options.runtime);
|
|
1637
|
-
|
|
1628
|
+
parseSessionTimeout(options.overrides?.timeout);
|
|
1638
1629
|
if (options.webhook !== undefined)
|
|
1639
|
-
|
|
1630
|
+
parseSessionWebhook(options.webhook);
|
|
1640
1631
|
}
|
|
1641
1632
|
catch (err) {
|
|
1642
|
-
throw new
|
|
1633
|
+
throw new SessionConfigValidationError(`Aex.openSession: ${err instanceof Error ? err.message : String(err)}`);
|
|
1643
1634
|
}
|
|
1644
1635
|
const { declarations: secretEnvDeclarations, values: envSecretValues } = splitSecretEnv(options.environment?.secrets);
|
|
1645
1636
|
let limits;
|
|
@@ -1649,11 +1640,11 @@ export class Aex {
|
|
|
1649
1640
|
if (options.overrides?.maxTurns !== undefined)
|
|
1650
1641
|
limitsInput.maxTurns = options.overrides.maxTurns;
|
|
1651
1642
|
try {
|
|
1652
|
-
limits =
|
|
1643
|
+
limits = parseSessionLimits(Object.keys(limitsInput).length > 0 ? limitsInput : undefined);
|
|
1653
1644
|
}
|
|
1654
1645
|
catch (err) {
|
|
1655
1646
|
// One `configError` factory for every client-side validation throw, so
|
|
1656
|
-
// maxSpendUsd/maxTurns are `
|
|
1647
|
+
// maxSpendUsd/maxTurns are `SessionConfigValidationError` (not a base AexError).
|
|
1657
1648
|
throw configError("Aex.openSession", "limits", err instanceof Error ? err.message : String(err));
|
|
1658
1649
|
}
|
|
1659
1650
|
const uploader = (args) => this._uploadAsset(args);
|
|
@@ -1667,7 +1658,7 @@ export class Aex {
|
|
|
1667
1658
|
prepareFiles(options.files ?? [], uploader, streamUploader)
|
|
1668
1659
|
]);
|
|
1669
1660
|
const { submissionMcpServers, mergedMcpSecrets } = mergeMcpServers(options.mcpServers ?? [], []);
|
|
1670
|
-
const
|
|
1661
|
+
const fileCapture = fileCaptureForWire(options.fileCapture);
|
|
1671
1662
|
const environment = sessionEnvironmentForWire(options.environment);
|
|
1672
1663
|
const submission = {
|
|
1673
1664
|
model: options.model,
|
|
@@ -1685,7 +1676,7 @@ export class Aex {
|
|
|
1685
1676
|
...(Object.keys(secretEnvDeclarations).length > 0 ? { secretEnv: secretEnvDeclarations } : {}),
|
|
1686
1677
|
...(environment ? { environment: environment } : {}),
|
|
1687
1678
|
...(options.metadata ? { metadata: options.metadata } : {}),
|
|
1688
|
-
...(
|
|
1679
|
+
...(fileCapture ? { fileCapture } : {}),
|
|
1689
1680
|
...(options.includeBuiltinTools !== undefined
|
|
1690
1681
|
? { includeBuiltinTools: options.includeBuiltinTools }
|
|
1691
1682
|
: {}),
|
|
@@ -1715,7 +1706,7 @@ export class Aex {
|
|
|
1715
1706
|
/**
|
|
1716
1707
|
* Delete a workspace asset blob from the shared content-addressed store
|
|
1717
1708
|
* (`assets/<workspaceId>/<hash>`). Accepts `sha256:<hex>` or a bare
|
|
1718
|
-
* 64-hex digest.
|
|
1709
|
+
* 64-hex digest. Sessions that already snapshotted the asset are unaffected.
|
|
1719
1710
|
*/
|
|
1720
1711
|
deleteWorkspaceAsset(hash) {
|
|
1721
1712
|
return operations.deleteWorkspaceAsset(this.#http, hash);
|
|
@@ -1790,14 +1781,14 @@ function resolveBaseUrlForKey(apiKey, baseUrl) {
|
|
|
1790
1781
|
}
|
|
1791
1782
|
return baseUrl;
|
|
1792
1783
|
}
|
|
1793
|
-
// `
|
|
1784
|
+
// `SessionRecord.status` is a loose `string` on the wire shape, so we membership-test
|
|
1794
1785
|
// against the canonical terminal set rather than re-deriving one (which is how
|
|
1795
1786
|
// `timed_out` got dropped from the old hardcoded list).
|
|
1796
|
-
const TERMINAL_STATUSES = new Set(
|
|
1787
|
+
const TERMINAL_STATUSES = new Set(TERMINAL_SESSION_CONTROL_STATUSES);
|
|
1797
1788
|
function isTerminal(status) {
|
|
1798
1789
|
return typeof status === "string" && TERMINAL_STATUSES.has(status);
|
|
1799
1790
|
}
|
|
1800
|
-
/** How long `Aex.
|
|
1791
|
+
/** How long `Aex.start` waits for the settle write after the park event (ms). */
|
|
1801
1792
|
const SETTLE_POLL_DEADLINE_MS = 60_000;
|
|
1802
1793
|
/** Interval between settle-poll reads (ms). */
|
|
1803
1794
|
const SETTLE_POLL_INTERVAL_MS = 750;
|
|
@@ -1875,7 +1866,7 @@ function isSessionParked(status) {
|
|
|
1875
1866
|
status === "expired" ||
|
|
1876
1867
|
isTerminal(status));
|
|
1877
1868
|
}
|
|
1878
|
-
function
|
|
1869
|
+
function sessionToSessionRecord(session) {
|
|
1879
1870
|
const id = session.sessionId ?? session.id;
|
|
1880
1871
|
return {
|
|
1881
1872
|
id,
|
|
@@ -1927,15 +1918,15 @@ function sleep(ms, signal) {
|
|
|
1927
1918
|
}
|
|
1928
1919
|
/**
|
|
1929
1920
|
* ONE factory for every client-side validation throw: a
|
|
1930
|
-
* {@link
|
|
1921
|
+
* {@link SessionConfigValidationError} carrying structured `details: { field, value? }`
|
|
1931
1922
|
* so callers branch on `err.details.field` instead of string-parsing the message.
|
|
1932
1923
|
*/
|
|
1933
1924
|
function configError(surface, field, message, value) {
|
|
1934
|
-
return new
|
|
1925
|
+
return new SessionConfigValidationError(`${surface}: ${message}`, value === undefined ? { field } : { field, value });
|
|
1935
1926
|
}
|
|
1936
1927
|
/**
|
|
1937
1928
|
* Derive the message idempotency key from the session-create key. Mirrors the
|
|
1938
|
-
* CLI (`<createKey>:message`) so a retried `run` / `sessions.
|
|
1929
|
+
* CLI (`<createKey>:message`) so a retried `run` / `sessions.start` that reuses
|
|
1939
1930
|
* one `idempotencyKey` de-duplicates BOTH the create and the billable turn.
|
|
1940
1931
|
*/
|
|
1941
1932
|
function deriveMessageKey(createKey) {
|
|
@@ -1954,7 +1945,7 @@ function throttleFromSession(session) {
|
|
|
1954
1945
|
faultFromErrorMessage(typeof session.errorMessage === "string" ? session.errorMessage : undefined);
|
|
1955
1946
|
return fault && isThrottleFault(fault) ? fault : undefined;
|
|
1956
1947
|
}
|
|
1957
|
-
/** Last-resort throttle detection from a free-text
|
|
1948
|
+
/** Last-resort throttle detection from a free-text turn error message. */
|
|
1958
1949
|
function faultFromErrorMessage(message) {
|
|
1959
1950
|
if (message === undefined || message.length === 0)
|
|
1960
1951
|
return undefined;
|
|
@@ -1970,16 +1961,16 @@ function faultFromErrorMessage(message) {
|
|
|
1970
1961
|
function normaliseSessionInput(input, surface, field) {
|
|
1971
1962
|
if (typeof input === "string") {
|
|
1972
1963
|
if (!input) {
|
|
1973
|
-
throw new
|
|
1964
|
+
throw new SessionConfigValidationError(`${surface}: ${field} must be a non-empty string`);
|
|
1974
1965
|
}
|
|
1975
1966
|
return input;
|
|
1976
1967
|
}
|
|
1977
1968
|
if (!Array.isArray(input) || input.length === 0) {
|
|
1978
|
-
throw new
|
|
1969
|
+
throw new SessionConfigValidationError(`${surface}: ${field} must be a non-empty string or string array`);
|
|
1979
1970
|
}
|
|
1980
1971
|
for (const segment of input) {
|
|
1981
1972
|
if (typeof segment !== "string" || !segment) {
|
|
1982
|
-
throw new
|
|
1973
|
+
throw new SessionConfigValidationError(`${surface}: ${field} segments must be non-empty strings`);
|
|
1983
1974
|
}
|
|
1984
1975
|
}
|
|
1985
1976
|
return [...input];
|
|
@@ -1989,66 +1980,66 @@ function assertNoLegacySessionFields(options, surface) {
|
|
|
1989
1980
|
const removedProxyField = ["proxy", "Endpoints"].join("");
|
|
1990
1981
|
const messages = {
|
|
1991
1982
|
input: "send user messages with session.send(...) or use run({ message }).",
|
|
1992
|
-
prompt: "use message for one-shot
|
|
1983
|
+
prompt: "use message for one-shot session input or session.send(...) for follow-up messages.",
|
|
1993
1984
|
instructions: "use system.",
|
|
1994
1985
|
idleSuspendAfter: "use overrides.idleTtl.",
|
|
1995
1986
|
idleTtl: "use overrides.idleTtl.",
|
|
1996
1987
|
retention: "use overrides.idleTtl.",
|
|
1997
1988
|
secretEnv: "use environment.secrets.",
|
|
1998
|
-
secrets: "use top-level apiKeys for provider keys and environment.secrets for
|
|
1989
|
+
secrets: "use top-level apiKeys for provider keys and environment.secrets for session secrets.",
|
|
1999
1990
|
runtimeSize: "use runtime.",
|
|
2000
1991
|
limits: "use overrides.",
|
|
2001
1992
|
timeout: "use overrides.timeout.",
|
|
2002
|
-
|
|
1993
|
+
parentSessionId: "subagent lineage is assigned by the platform.",
|
|
2003
1994
|
signal: "use session.cancel() / session.suspend() for remote control.",
|
|
2004
1995
|
postHook: "send a follow-up validation message when the session returns idle.",
|
|
2005
1996
|
[removedProxyField]: "proxy endpoints are not part of the public SDK session API."
|
|
2006
1997
|
};
|
|
2007
1998
|
for (const [field, message] of Object.entries(messages)) {
|
|
2008
1999
|
if (Object.prototype.hasOwnProperty.call(record, field)) {
|
|
2009
|
-
throw new
|
|
2000
|
+
throw new SessionConfigValidationError(`${surface}: ${field} is not a supported option; ${message}`);
|
|
2010
2001
|
}
|
|
2011
2002
|
}
|
|
2012
2003
|
const overrides = record.overrides;
|
|
2013
2004
|
if (overrides && typeof overrides === "object" && !Array.isArray(overrides)) {
|
|
2014
2005
|
const overrideRecord = overrides;
|
|
2015
2006
|
if (Object.prototype.hasOwnProperty.call(overrideRecord, "idleSuspendAfter")) {
|
|
2016
|
-
throw new
|
|
2007
|
+
throw new SessionConfigValidationError(`${surface}: overrides.idleSuspendAfter is not a supported option; use overrides.idleTtl.`);
|
|
2017
2008
|
}
|
|
2018
2009
|
}
|
|
2019
2010
|
}
|
|
2020
2011
|
function assertNoSessionSendSignal(options, surface) {
|
|
2021
2012
|
const record = options;
|
|
2022
2013
|
if (record && typeof record === "object" && Object.prototype.hasOwnProperty.call(record, "signal")) {
|
|
2023
|
-
throw new
|
|
2014
|
+
throw new SessionConfigValidationError(`${surface}: signal is not a supported option; use session.cancel() / session.suspend() for remote control.`);
|
|
2024
2015
|
}
|
|
2025
2016
|
}
|
|
2026
2017
|
function validateApiKeys(apiKeys, provider, surface) {
|
|
2027
2018
|
const key = apiKeys?.[provider];
|
|
2028
2019
|
if (typeof key !== "string" || key.length === 0) {
|
|
2029
|
-
throw new
|
|
2020
|
+
throw new SessionConfigValidationError(`${surface}: a provider API key is required — pass apiKeys[${JSON.stringify(provider)}].`);
|
|
2030
2021
|
}
|
|
2031
2022
|
}
|
|
2032
|
-
function
|
|
2033
|
-
if (
|
|
2023
|
+
function fileCaptureForWire(fileCapture) {
|
|
2024
|
+
if (fileCapture === undefined) {
|
|
2034
2025
|
return undefined;
|
|
2035
2026
|
}
|
|
2036
|
-
const allowedDirs =
|
|
2037
|
-
const deniedDirs =
|
|
2038
|
-
const hasNumericOverride =
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2027
|
+
const allowedDirs = fileCapture.allowedDirs?.filter((dir) => dir.length > 0);
|
|
2028
|
+
const deniedDirs = fileCapture.deniedDirs?.filter((dir) => dir.length > 0);
|
|
2029
|
+
const hasNumericOverride = fileCapture.captureTimeoutMs !== undefined ||
|
|
2030
|
+
fileCapture.maxFileBytes !== undefined ||
|
|
2031
|
+
fileCapture.maxTotalBytes !== undefined ||
|
|
2032
|
+
fileCapture.maxFiles !== undefined;
|
|
2042
2033
|
if ((allowedDirs?.length ?? 0) === 0 && (deniedDirs?.length ?? 0) === 0 && !hasNumericOverride) {
|
|
2043
2034
|
return undefined;
|
|
2044
2035
|
}
|
|
2045
2036
|
return {
|
|
2046
2037
|
...(allowedDirs && allowedDirs.length > 0 ? { allowedDirs } : {}),
|
|
2047
2038
|
...(deniedDirs && deniedDirs.length > 0 ? { deniedDirs } : {}),
|
|
2048
|
-
...(
|
|
2049
|
-
...(
|
|
2050
|
-
...(
|
|
2051
|
-
...(
|
|
2039
|
+
...(fileCapture.captureTimeoutMs !== undefined ? { captureTimeoutMs: fileCapture.captureTimeoutMs } : {}),
|
|
2040
|
+
...(fileCapture.maxFileBytes !== undefined ? { maxFileBytes: fileCapture.maxFileBytes } : {}),
|
|
2041
|
+
...(fileCapture.maxTotalBytes !== undefined ? { maxTotalBytes: fileCapture.maxTotalBytes } : {}),
|
|
2042
|
+
...(fileCapture.maxFiles !== undefined ? { maxFiles: fileCapture.maxFiles } : {})
|
|
2052
2043
|
};
|
|
2053
2044
|
}
|
|
2054
2045
|
const DEFAULT_SESSION_IDLE_TTL = "3m";
|
|
@@ -2121,7 +2112,7 @@ async function prepareTools(tools, uploader) {
|
|
|
2121
2112
|
// A bare string is a builtin tool reference.
|
|
2122
2113
|
if (typeof entry === "string") {
|
|
2123
2114
|
if (!BUILTIN_TOOL_NAMES.includes(entry)) {
|
|
2124
|
-
throw new
|
|
2115
|
+
throw new SessionConfigValidationError(`aex: tools[${i}] (${JSON.stringify(entry)}) is not a builtin tool name; ` +
|
|
2125
2116
|
`expected a Tool or one of: ${BUILTIN_TOOL_NAMES.join(", ")}`);
|
|
2126
2117
|
}
|
|
2127
2118
|
return { kind: "builtin", name: entry };
|
|
@@ -2129,15 +2120,15 @@ async function prepareTools(tools, uploader) {
|
|
|
2129
2120
|
if (!(entry instanceof Tool)) {
|
|
2130
2121
|
const maybeEntry = entry;
|
|
2131
2122
|
if (maybeEntry instanceof Skill) {
|
|
2132
|
-
throw new
|
|
2123
|
+
throw new SessionConfigValidationError(`aex: tools[${i}] is a Skill; pass skills via the top-level skills option`);
|
|
2133
2124
|
}
|
|
2134
|
-
throw new
|
|
2125
|
+
throw new SessionConfigValidationError(`aex: tools[${i}] must be a Tool or a builtin tool name`);
|
|
2135
2126
|
}
|
|
2136
2127
|
const ref = entry.ref;
|
|
2137
2128
|
if (ref.kind === "draft") {
|
|
2138
2129
|
const bundle = entry._takeDraftBundle();
|
|
2139
2130
|
if (!bundle) {
|
|
2140
|
-
throw new
|
|
2131
|
+
throw new SessionConfigValidationError(`aex: tools[${i}] is draft but has no bytes`);
|
|
2141
2132
|
}
|
|
2142
2133
|
const assetId = await resolveAssetId(entry, bundle, uploader);
|
|
2143
2134
|
return { kind: "ref", ref: { ...bundle.ref, assetId } };
|
|
@@ -2163,21 +2154,21 @@ async function prepareTools(tools, uploader) {
|
|
|
2163
2154
|
/**
|
|
2164
2155
|
* Upload/upsert workspace skills and return public name-only refs. Draft skills
|
|
2165
2156
|
* auto-upsert (bytes → asset store, then registry PUT) by name; already-uploaded
|
|
2166
|
-
* skills pass through. Duplicate names are pre-checked (uploads
|
|
2167
|
-
* so the dedup cannot be a
|
|
2157
|
+
* skills pass through. Duplicate names are pre-checked (uploads session in parallel,
|
|
2158
|
+
* so the dedup cannot be a sessionning set inside the map).
|
|
2168
2159
|
*/
|
|
2169
2160
|
async function prepareSkills(skills, uploader) {
|
|
2170
2161
|
if (skills.length > SKILLS_MAX) {
|
|
2171
|
-
throw new
|
|
2162
|
+
throw new SessionConfigValidationError(`aex: skills exceeds the ${SKILLS_MAX}-skill limit (got ${skills.length})`);
|
|
2172
2163
|
}
|
|
2173
2164
|
const seen = new Set();
|
|
2174
2165
|
for (let i = 0; i < skills.length; i++) {
|
|
2175
2166
|
const entry = skills[i];
|
|
2176
2167
|
if (!(entry instanceof Skill)) {
|
|
2177
|
-
throw new
|
|
2168
|
+
throw new SessionConfigValidationError(`aex: skills[${i}] must be a Skill (Skill.fromDir / fromUrl / fromFiles / fromContent / fromBytes)`);
|
|
2178
2169
|
}
|
|
2179
2170
|
if (seen.has(entry.name)) {
|
|
2180
|
-
throw new
|
|
2171
|
+
throw new SessionConfigValidationError(`aex: skills duplicate name: ${entry.name}`);
|
|
2181
2172
|
}
|
|
2182
2173
|
seen.add(entry.name);
|
|
2183
2174
|
}
|
|
@@ -2185,7 +2176,7 @@ async function prepareSkills(skills, uploader) {
|
|
|
2185
2176
|
const uploaded = entry.isDraft ? await entry.upload(uploader) : entry;
|
|
2186
2177
|
const ref = uploaded.ref;
|
|
2187
2178
|
if (ref.kind !== "skill") {
|
|
2188
|
-
throw new
|
|
2179
|
+
throw new SessionConfigValidationError(`aex: skills[${i}] did not resolve to a workspace skill ref`);
|
|
2189
2180
|
}
|
|
2190
2181
|
return ref;
|
|
2191
2182
|
});
|
|
@@ -2194,13 +2185,13 @@ async function prepareSkills(skills, uploader) {
|
|
|
2194
2185
|
async function prepareAgentsMd(agentsMds, uploader) {
|
|
2195
2186
|
return mapWithConcurrency(agentsMds, UPLOAD_CONCURRENCY, async (entry, i) => {
|
|
2196
2187
|
if (!(entry instanceof AgentsMd)) {
|
|
2197
|
-
throw new
|
|
2188
|
+
throw new SessionConfigValidationError(`aex: agentsMd[${i}] must be an AgentsMd instance`);
|
|
2198
2189
|
}
|
|
2199
2190
|
const ref = entry.ref;
|
|
2200
2191
|
if (ref.kind === "draft") {
|
|
2201
2192
|
const bundle = entry._takeDraftBundle();
|
|
2202
2193
|
if (!bundle) {
|
|
2203
|
-
throw new
|
|
2194
|
+
throw new SessionConfigValidationError(`aex: agentsMd[${i}] is draft but has no bytes`);
|
|
2204
2195
|
}
|
|
2205
2196
|
const assetId = await resolveAssetId(entry, bundle, uploader);
|
|
2206
2197
|
return { kind: "asset", assetId, name: bundle.name };
|
|
@@ -2212,7 +2203,7 @@ async function prepareAgentsMd(agentsMds, uploader) {
|
|
|
2212
2203
|
async function prepareFiles(files, uploader, streamUploader) {
|
|
2213
2204
|
return mapWithConcurrency(files, UPLOAD_CONCURRENCY, async (entry, i) => {
|
|
2214
2205
|
if (!(entry instanceof File)) {
|
|
2215
|
-
throw new
|
|
2206
|
+
throw new SessionConfigValidationError(`aex: files[${i}] must be a File instance`);
|
|
2216
2207
|
}
|
|
2217
2208
|
const ref = entry.ref;
|
|
2218
2209
|
if (ref.kind === "draft") {
|
|
@@ -2229,7 +2220,7 @@ async function prepareFiles(files, uploader, streamUploader) {
|
|
|
2229
2220
|
}
|
|
2230
2221
|
const bundle = entry._takeDraftBundle();
|
|
2231
2222
|
if (!bundle) {
|
|
2232
|
-
throw new
|
|
2223
|
+
throw new SessionConfigValidationError(`aex: files[${i}] is draft but has no bytes`);
|
|
2233
2224
|
}
|
|
2234
2225
|
const assetId = await resolveAssetId(entry, bundle, uploader);
|
|
2235
2226
|
return { kind: "asset", assetId, name: bundle.name, mountPath: bundle.mountPath };
|
|
@@ -2246,14 +2237,14 @@ function mergeMcpServers(inputs, explicitSecrets) {
|
|
|
2246
2237
|
for (let i = 0; i < inputs.length; i++) {
|
|
2247
2238
|
const entry = inputs[i];
|
|
2248
2239
|
if (!(entry instanceof McpServer)) {
|
|
2249
|
-
throw new
|
|
2240
|
+
throw new SessionConfigValidationError(`aex: mcpServers[${i}] must be an McpServer instance`);
|
|
2250
2241
|
}
|
|
2251
2242
|
submissionMcpServers.push(entry.toSubmissionEntry());
|
|
2252
2243
|
const secret = entry.toSecretEntry();
|
|
2253
2244
|
if (secret) {
|
|
2254
2245
|
const existing = secretByName.get(secret.name);
|
|
2255
2246
|
if (existing && existing.url !== secret.url) {
|
|
2256
|
-
throw new
|
|
2247
|
+
throw new SessionConfigValidationError(`aex: mcpServers[${i}].url conflicts with secrets.mcpServers["${secret.name}"]`);
|
|
2257
2248
|
}
|
|
2258
2249
|
secretByName.set(secret.name, secret);
|
|
2259
2250
|
}
|