@aexhq/sdk 0.46.4-canary → 0.50.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/LICENSE +201 -0
- package/NOTICE +38 -0
- package/README.md +23 -31
- package/dist/client/aex.d.ts +33 -0
- package/dist/client/aex.js +98 -0
- package/dist/client/aex.js.map +1 -0
- package/dist/client/credentials.d.ts +25 -0
- package/dist/client/credentials.js +97 -0
- package/dist/client/credentials.js.map +1 -0
- package/dist/client/routing.d.ts +7 -0
- package/dist/client/routing.js +29 -0
- package/dist/client/routing.js.map +1 -0
- package/dist/downloads/download.d.ts +25 -0
- package/dist/downloads/download.js +53 -0
- package/dist/downloads/download.js.map +1 -0
- package/dist/generated/errors.d.ts +12 -0
- package/dist/generated/errors.js +81 -0
- package/dist/generated/errors.js.map +1 -0
- package/dist/generated/resources.d.ts +730 -0
- package/dist/generated/resources.js +606 -0
- package/dist/generated/resources.js.map +1 -0
- package/dist/generated/routes.d.ts +42 -0
- package/dist/generated/routes.js +2101 -0
- package/dist/generated/routes.js.map +1 -0
- package/dist/index.d.ts +20 -50
- package/dist/index.js +11 -62
- package/dist/index.js.map +1 -1
- package/dist/observations/stream.d.ts +1 -0
- package/dist/observations/stream.js +18 -0
- package/dist/observations/stream.js.map +1 -0
- package/dist/transport/errors.d.ts +60 -0
- package/dist/transport/errors.js +107 -0
- package/dist/transport/errors.js.map +1 -0
- package/dist/transport/pagination.d.ts +8 -0
- package/dist/transport/pagination.js +34 -0
- package/dist/transport/pagination.js.map +1 -0
- package/dist/transport/retry.d.ts +21 -0
- package/dist/transport/retry.js +37 -0
- package/dist/transport/retry.js.map +1 -0
- package/dist/transport/transport.d.ts +25 -0
- package/dist/transport/transport.js +28 -0
- package/dist/transport/transport.js.map +1 -0
- package/package.json +63 -30
- package/dist/_contracts/account-operations.d.ts +0 -101
- package/dist/_contracts/account-operations.js +0 -242
- package/dist/_contracts/account-types.d.ts +0 -461
- package/dist/_contracts/account-types.js +0 -1
- package/dist/_contracts/api-key.d.ts +0 -61
- package/dist/_contracts/api-key.js +0 -101
- package/dist/_contracts/api-routes.d.ts +0 -20
- package/dist/_contracts/api-routes.js +0 -109
- package/dist/_contracts/archive-limits.d.ts +0 -3
- package/dist/_contracts/archive-limits.js +0 -23
- package/dist/_contracts/asset-authoring.d.ts +0 -22
- package/dist/_contracts/asset-authoring.js +0 -106
- package/dist/_contracts/asset-bundle.d.ts +0 -64
- package/dist/_contracts/asset-bundle.js +0 -263
- package/dist/_contracts/asset-upload-helper.d.ts +0 -31
- package/dist/_contracts/asset-upload-helper.js +0 -84
- package/dist/_contracts/billing-admission.d.ts +0 -29
- package/dist/_contracts/billing-admission.js +0 -28
- package/dist/_contracts/bundle-manifest.d.ts +0 -89
- package/dist/_contracts/bundle-manifest.js +0 -158
- package/dist/_contracts/canonical-sha256.d.ts +0 -8
- package/dist/_contracts/canonical-sha256.js +0 -8
- package/dist/_contracts/connection-ticket.d.ts +0 -22
- package/dist/_contracts/connection-ticket.js +0 -54
- package/dist/_contracts/continuation-event.d.ts +0 -31
- package/dist/_contracts/continuation-event.js +0 -6
- package/dist/_contracts/contract-parse-error.d.ts +0 -12
- package/dist/_contracts/contract-parse-error.js +0 -51
- package/dist/_contracts/error-codes.d.ts +0 -26
- package/dist/_contracts/error-codes.js +0 -116
- package/dist/_contracts/error-factory.d.ts +0 -32
- package/dist/_contracts/error-factory.js +0 -174
- package/dist/_contracts/event-envelope.d.ts +0 -471
- package/dist/_contracts/event-envelope.js +0 -501
- package/dist/_contracts/event-stream-client.d.ts +0 -122
- package/dist/_contracts/event-stream-client.js +0 -445
- package/dist/_contracts/event-view.d.ts +0 -44
- package/dist/_contracts/event-view.js +0 -69
- package/dist/_contracts/failure-class.d.ts +0 -29
- package/dist/_contracts/failure-class.js +0 -73
- package/dist/_contracts/http.d.ts +0 -135
- package/dist/_contracts/http.js +0 -434
- package/dist/_contracts/ids.d.ts +0 -66
- package/dist/_contracts/ids.js +0 -119
- package/dist/_contracts/index.d.ts +0 -42
- package/dist/_contracts/index.js +0 -52
- package/dist/_contracts/internal.d.ts +0 -55
- package/dist/_contracts/internal.js +0 -113
- package/dist/_contracts/models.d.ts +0 -30
- package/dist/_contracts/models.js +0 -28
- package/dist/_contracts/operation-core.d.ts +0 -36
- package/dist/_contracts/operation-core.js +0 -70
- package/dist/_contracts/operations.d.ts +0 -218
- package/dist/_contracts/operations.js +0 -1496
- package/dist/_contracts/otlp-projection.d.ts +0 -78
- package/dist/_contracts/otlp-projection.js +0 -171
- package/dist/_contracts/post-hook.d.ts +0 -31
- package/dist/_contracts/post-hook.js +0 -61
- package/dist/_contracts/provider-fault.d.ts +0 -34
- package/dist/_contracts/provider-fault.js +0 -68
- package/dist/_contracts/retry-core.d.ts +0 -29
- package/dist/_contracts/retry-core.js +0 -79
- package/dist/_contracts/runner-event.d.ts +0 -117
- package/dist/_contracts/runner-event.js +0 -172
- package/dist/_contracts/runtime-kind.d.ts +0 -60
- package/dist/_contracts/runtime-kind.js +0 -70
- package/dist/_contracts/runtime-manifest.d.ts +0 -121
- package/dist/_contracts/runtime-manifest.js +0 -83
- package/dist/_contracts/runtime-security-profile.d.ts +0 -26
- package/dist/_contracts/runtime-security-profile.js +0 -73
- package/dist/_contracts/runtime-sizes.d.ts +0 -104
- package/dist/_contracts/runtime-sizes.js +0 -111
- package/dist/_contracts/runtime-types.d.ts +0 -618
- package/dist/_contracts/runtime-types.js +0 -58
- package/dist/_contracts/schemas/asset-bundle.d.ts +0 -70
- package/dist/_contracts/schemas/asset-bundle.js +0 -107
- package/dist/_contracts/schemas/asset-ref.d.ts +0 -61
- package/dist/_contracts/schemas/asset-ref.js +0 -118
- package/dist/_contracts/schemas/bundle-manifest.d.ts +0 -66
- package/dist/_contracts/schemas/bundle-manifest.js +0 -77
- package/dist/_contracts/schemas/index.d.ts +0 -32
- package/dist/_contracts/schemas/index.js +0 -30
- package/dist/_contracts/schemas/mcp-server.d.ts +0 -99
- package/dist/_contracts/schemas/mcp-server.js +0 -209
- package/dist/_contracts/schemas/models.d.ts +0 -29
- package/dist/_contracts/schemas/models.js +0 -51
- package/dist/_contracts/schemas/numeric.d.ts +0 -18
- package/dist/_contracts/schemas/numeric.js +0 -28
- package/dist/_contracts/schemas/post-hook.d.ts +0 -45
- package/dist/_contracts/schemas/post-hook.js +0 -68
- package/dist/_contracts/schemas/response-assets.d.ts +0 -75
- package/dist/_contracts/schemas/response-assets.js +0 -81
- package/dist/_contracts/schemas/response-billing.d.ts +0 -208
- package/dist/_contracts/schemas/response-billing.js +0 -139
- package/dist/_contracts/schemas/response-common.d.ts +0 -132
- package/dist/_contracts/schemas/response-common.js +0 -162
- package/dist/_contracts/schemas/response-identity.d.ts +0 -648
- package/dist/_contracts/schemas/response-identity.js +0 -131
- package/dist/_contracts/schemas/response-mcp-servers.d.ts +0 -51
- package/dist/_contracts/schemas/response-mcp-servers.js +0 -32
- package/dist/_contracts/schemas/response-secrets.d.ts +0 -50
- package/dist/_contracts/schemas/response-secrets.js +0 -32
- package/dist/_contracts/schemas/response-sessions-internal.d.ts +0 -200
- package/dist/_contracts/schemas/response-sessions-internal.js +0 -142
- package/dist/_contracts/schemas/response-sessions.d.ts +0 -1598
- package/dist/_contracts/schemas/response-sessions.js +0 -377
- package/dist/_contracts/schemas/response-webhooks.d.ts +0 -76
- package/dist/_contracts/schemas/response-webhooks.js +0 -42
- package/dist/_contracts/schemas/response-workspace.d.ts +0 -225
- package/dist/_contracts/schemas/response-workspace.js +0 -99
- package/dist/_contracts/schemas/runtime-kind.d.ts +0 -31
- package/dist/_contracts/schemas/runtime-kind.js +0 -29
- package/dist/_contracts/schemas/runtime-security-profile.d.ts +0 -28
- package/dist/_contracts/schemas/runtime-security-profile.js +0 -26
- package/dist/_contracts/schemas/runtime-sizes.d.ts +0 -70
- package/dist/_contracts/schemas/runtime-sizes.js +0 -127
- package/dist/_contracts/schemas/session-limits.d.ts +0 -34
- package/dist/_contracts/schemas/session-limits.js +0 -39
- package/dist/_contracts/schemas/session-machine.d.ts +0 -23
- package/dist/_contracts/schemas/session-machine.js +0 -24
- package/dist/_contracts/schemas/session-request-config.d.ts +0 -58
- package/dist/_contracts/schemas/session-request-config.js +0 -134
- package/dist/_contracts/schemas/session-webhook.d.ts +0 -11
- package/dist/_contracts/schemas/session-webhook.js +0 -38
- package/dist/_contracts/schemas/side-effect-audit.d.ts +0 -98
- package/dist/_contracts/schemas/side-effect-audit.js +0 -102
- package/dist/_contracts/schemas/submission-assets.d.ts +0 -117
- package/dist/_contracts/schemas/submission-assets.js +0 -147
- package/dist/_contracts/schemas/submission-body.d.ts +0 -251
- package/dist/_contracts/schemas/submission-body.js +0 -378
- package/dist/_contracts/schemas/submission-environment.d.ts +0 -79
- package/dist/_contracts/schemas/submission-environment.js +0 -179
- package/dist/_contracts/schemas/submission-request.d.ts +0 -158
- package/dist/_contracts/schemas/submission-request.js +0 -49
- package/dist/_contracts/schemas/submission-secrets.d.ts +0 -47
- package/dist/_contracts/schemas/submission-secrets.js +0 -108
- package/dist/_contracts/schemas/wire.d.ts +0 -118
- package/dist/_contracts/schemas/wire.js +0 -171
- package/dist/_contracts/schemas/workspace-resources.d.ts +0 -50
- package/dist/_contracts/schemas/workspace-resources.js +0 -87
- package/dist/_contracts/sdk-errors.d.ts +0 -212
- package/dist/_contracts/sdk-errors.js +0 -313
- package/dist/_contracts/sdk-secrets.d.ts +0 -67
- package/dist/_contracts/sdk-secrets.js +0 -427
- package/dist/_contracts/session-archive.d.ts +0 -16
- package/dist/_contracts/session-archive.js +0 -92
- package/dist/_contracts/session-artifacts.d.ts +0 -189
- package/dist/_contracts/session-artifacts.js +0 -264
- package/dist/_contracts/session-config.d.ts +0 -373
- package/dist/_contracts/session-config.js +0 -562
- package/dist/_contracts/session-cost-types.d.ts +0 -211
- package/dist/_contracts/session-cost-types.js +0 -69
- package/dist/_contracts/session-cost.d.ts +0 -8
- package/dist/_contracts/session-cost.js +0 -582
- package/dist/_contracts/session-custody.d.ts +0 -165
- package/dist/_contracts/session-custody.js +0 -345
- package/dist/_contracts/session-file-query.d.ts +0 -14
- package/dist/_contracts/session-file-query.js +0 -178
- package/dist/_contracts/session-record.d.ts +0 -112
- package/dist/_contracts/session-record.js +0 -165
- package/dist/_contracts/session-retention.d.ts +0 -201
- package/dist/_contracts/session-retention.js +0 -450
- package/dist/_contracts/side-effect-audit.d.ts +0 -126
- package/dist/_contracts/side-effect-audit.js +0 -520
- package/dist/_contracts/sse.d.ts +0 -74
- package/dist/_contracts/sse.js +0 -227
- package/dist/_contracts/stable.d.ts +0 -45
- package/dist/_contracts/stable.js +0 -62
- package/dist/_contracts/status.d.ts +0 -25
- package/dist/_contracts/status.js +0 -57
- package/dist/_contracts/submission-limits.d.ts +0 -61
- package/dist/_contracts/submission-limits.js +0 -60
- package/dist/_contracts/submission.d.ts +0 -547
- package/dist/_contracts/submission.js +0 -812
- package/dist/_contracts/suggest.d.ts +0 -15
- package/dist/_contracts/suggest.js +0 -53
- package/dist/_contracts/testing/response-bindings.d.ts +0 -45
- package/dist/_contracts/testing/response-bindings.js +0 -256
- package/dist/_contracts/testing/wire-conformance-entry.d.ts +0 -10
- package/dist/_contracts/testing/wire-conformance-entry.js +0 -8
- package/dist/_contracts/testing/wire-conformance.d.ts +0 -169
- package/dist/_contracts/testing/wire-conformance.js +0 -276
- package/dist/_contracts/turn-trace.d.ts +0 -28
- package/dist/_contracts/turn-trace.js +0 -1
- package/dist/_contracts/unknown-field-error.d.ts +0 -13
- package/dist/_contracts/unknown-field-error.js +0 -21
- package/dist/_contracts/value-guards.d.ts +0 -20
- package/dist/_contracts/value-guards.js +0 -34
- package/dist/_contracts/webhook-verify.d.ts +0 -34
- package/dist/_contracts/webhook-verify.js +0 -93
- package/dist/_contracts/wire-observer.d.ts +0 -49
- package/dist/_contracts/wire-observer.js +0 -34
- package/dist/_contracts/workflow-status.d.ts +0 -7
- package/dist/_contracts/workflow-status.js +0 -43
- package/dist/_contracts/workspace-resources.d.ts +0 -98
- package/dist/_contracts/workspace-resources.js +0 -39
- package/dist/archive-limits.d.ts +0 -1
- package/dist/archive-limits.js +0 -2
- package/dist/archive-limits.js.map +0 -1
- package/dist/asset-upload.d.ts +0 -47
- package/dist/asset-upload.js +0 -269
- package/dist/asset-upload.js.map +0 -1
- package/dist/bundle.d.ts +0 -9
- package/dist/bundle.js +0 -20
- package/dist/bundle.js.map +0 -1
- package/dist/canonical-zip.d.ts +0 -68
- package/dist/canonical-zip.js +0 -355
- package/dist/canonical-zip.js.map +0 -1
- package/dist/cli.mjs +0 -12048
- package/dist/cli.mjs.sha256 +0 -1
- package/dist/client-types.d.ts +0 -192
- package/dist/client-types.js +0 -2
- package/dist/client-types.js.map +0 -1
- package/dist/client.d.ts +0 -464
- package/dist/client.js +0 -1207
- package/dist/client.js.map +0 -1
- package/dist/event-projection.d.ts +0 -22
- package/dist/event-projection.js +0 -380
- package/dist/event-projection.js.map +0 -1
- package/dist/fetch-archive.d.ts +0 -16
- package/dist/fetch-archive.js +0 -252
- package/dist/fetch-archive.js.map +0 -1
- package/dist/file.d.ts +0 -96
- package/dist/file.js +0 -272
- package/dist/file.js.map +0 -1
- package/dist/instructions.d.ts +0 -20
- package/dist/instructions.js +0 -40
- package/dist/instructions.js.map +0 -1
- package/dist/legacy-session-provider-fault.d.ts +0 -7
- package/dist/legacy-session-provider-fault.js +0 -38
- package/dist/legacy-session-provider-fault.js.map +0 -1
- package/dist/mcp-server.d.ts +0 -84
- package/dist/mcp-server.js +0 -117
- package/dist/mcp-server.js.map +0 -1
- package/dist/node-fs.d.ts +0 -29
- package/dist/node-fs.js +0 -19
- package/dist/node-fs.js.map +0 -1
- package/dist/node-walk.d.ts +0 -69
- package/dist/node-walk.js +0 -151
- package/dist/node-walk.js.map +0 -1
- package/dist/path-basename.d.ts +0 -5
- package/dist/path-basename.js +0 -9
- package/dist/path-basename.js.map +0 -1
- package/dist/retry.d.ts +0 -70
- package/dist/retry.js +0 -155
- package/dist/retry.js.map +0 -1
- package/dist/secret.d.ts +0 -65
- package/dist/secret.js +0 -110
- package/dist/secret.js.map +0 -1
- package/dist/session-validate.d.ts +0 -100
- package/dist/session-validate.js +0 -303
- package/dist/session-validate.js.map +0 -1
- package/dist/skill.d.ts +0 -99
- package/dist/skill.js +0 -169
- package/dist/skill.js.map +0 -1
- package/dist/submission-wire.d.ts +0 -13
- package/dist/submission-wire.js +0 -69
- package/dist/submission-wire.js.map +0 -1
- package/dist/tool.d.ts +0 -41
- package/dist/tool.js +0 -76
- package/dist/tool.js.map +0 -1
- package/dist/version.d.ts +0 -9
- package/dist/version.js +0 -10
- package/dist/version.js.map +0 -1
- package/docs/authentication.md +0 -125
- package/docs/billing.md +0 -164
- package/docs/cleanup.md +0 -27
- package/docs/concepts/agent-tools.md +0 -47
- package/docs/concepts/composition.md +0 -60
- package/docs/concepts/providers-and-runtimes.md +0 -121
- package/docs/concepts/sessions.md +0 -51
- package/docs/concepts/subagents.md +0 -35
- package/docs/credentials.md +0 -116
- package/docs/defaults.md +0 -51
- package/docs/errors.md +0 -258
- package/docs/events.md +0 -143
- package/docs/files.md +0 -130
- package/docs/limits-and-quotas.md +0 -114
- package/docs/limits.md +0 -51
- package/docs/mcp.md +0 -47
- package/docs/networking.md +0 -114
- package/docs/provider-runtime-capabilities.md +0 -32
- package/docs/public-surface.json +0 -73
- package/docs/quickstart.md +0 -135
- package/docs/release.md +0 -44
- package/docs/retries.md +0 -108
- package/docs/secrets.md +0 -141
- package/docs/session-config.md +0 -51
- package/docs/session-record.md +0 -58
- package/docs/skills.md +0 -65
- package/docs/telemetry.md +0 -66
- package/docs/testing.md +0 -35
- package/docs/vision-skills.md +0 -94
- package/docs/webhooks.md +0 -143
|
@@ -1,1496 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { CANONICAL_SHA256_DIGEST_PATTERN } from "./canonical-sha256.js";
|
|
3
|
-
import { isRecord, isStringLiteral } from "./value-guards.js";
|
|
4
|
-
import { AexNetworkError, SessionStateError } from "./sdk-errors.js";
|
|
5
|
-
import { buildSessionArchive, buildSessionEventsArchive, buildSessionFilesArchive, buildSessionMetadataArchive } from "./session-archive.js";
|
|
6
|
-
import { filterSessionFiles, isPathSelector, resolveSessionFileSelector } from "./session-file-query.js";
|
|
7
|
-
import { configError, idempotencyHeaders, resolveIdempotencyKey, deriveMessageIdempotencyKey } from "./operation-core.js";
|
|
8
|
-
import { BILLING_ADMISSION_STATES } from "./billing-admission.js";
|
|
9
|
-
import { RUNTIME_CAPABILITY_NAMES, SESSION_RUN_PHASES } from "./runtime-types.js";
|
|
10
|
-
import { RUNTIME_SIZES, parseRuntimeSize } from "./runtime-sizes.js";
|
|
11
|
-
import { RUNTIME_KINDS } from "./runtime-kind.js";
|
|
12
|
-
import { SESSION_STATUSES, SESSION_TERMINAL_OUTCOMES } from "./status.js";
|
|
13
|
-
import { parseProviderFault } from "./provider-fault.js";
|
|
14
|
-
export { classifySessionFile, filterSessionFiles, resolveSessionFileSelector, toFilenameMatcher } from "./session-file-query.js";
|
|
15
|
-
// The idempotency-key policy moved to the `operation-core.js` leaf so
|
|
16
|
-
// `account-operations.js` can share it without either module importing the
|
|
17
|
-
// other. Named rather than star re-exported: `configError` and
|
|
18
|
-
// `idempotencyHeaders` stay module-private, exactly as they were when they
|
|
19
|
-
// lived here.
|
|
20
|
-
export { IDEMPOTENCY_KEY_MAX_LENGTH, deriveMessageIdempotencyKey, resolveIdempotencyKey } from "./operation-core.js";
|
|
21
|
-
// Billing, workspace secrets and the control plane. Star re-exported because
|
|
22
|
-
// every name over there is a public operation this module used to declare, and
|
|
23
|
-
// `internal.ts` publishes this module as the whole `operations` namespace.
|
|
24
|
-
export * from "./account-operations.js";
|
|
25
|
-
/**
|
|
26
|
-
* The single source of truth for SDK<->BFF transport. The SDK class
|
|
27
|
-
* AND the CLI subcommands both call these functions; neither
|
|
28
|
-
* surface re-implements HTTP requests against the dashboard.
|
|
29
|
-
*
|
|
30
|
-
* Every function takes an HttpClient (so callers control auth + fetch
|
|
31
|
-
* injection) and returns parsed responses.
|
|
32
|
-
*
|
|
33
|
-
* Workspace identity is derived server-side from the API key on
|
|
34
|
-
* every request; callers do not pass `workspaceId`.
|
|
35
|
-
*/
|
|
36
|
-
const SESSION_STATUS_SET = new Set(SESSION_STATUSES);
|
|
37
|
-
const SESSION_FILE_SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
38
|
-
const SESSION_FILE_CHECKPOINT_ID_PATTERN = /^[A-Za-z0-9._-]{1,200}$/;
|
|
39
|
-
export async function createSession(http, request, options) {
|
|
40
|
-
const headers = idempotencyHeaders(options);
|
|
41
|
-
const result = await http.request("/api/sessions", {
|
|
42
|
-
method: "POST",
|
|
43
|
-
...(headers ? { headers } : {}),
|
|
44
|
-
body: JSON.stringify(request)
|
|
45
|
-
});
|
|
46
|
-
return unwrapSession(result);
|
|
47
|
-
}
|
|
48
|
-
/** Create a session and enqueue its first message without waiting for the RUN terminal. */
|
|
49
|
-
export async function createSessionWithMessage(http, request, input, options) {
|
|
50
|
-
const createKey = resolveIdempotencyKey(options?.idempotencyKey);
|
|
51
|
-
const messageKey = options?.messageIdempotencyKey !== undefined
|
|
52
|
-
? resolveIdempotencyKey(options.messageIdempotencyKey)
|
|
53
|
-
: deriveMessageIdempotencyKey(createKey);
|
|
54
|
-
if (!((typeof input === "string" && input.length > 0) ||
|
|
55
|
-
(Array.isArray(input) && input.length > 0 && input.every((part) => typeof part === "string" && part.length > 0)))) {
|
|
56
|
-
throw configError("input", "session message must be a non-empty string or string array");
|
|
57
|
-
}
|
|
58
|
-
const created = await createSession(http, request, { idempotencyKey: createKey });
|
|
59
|
-
return sendSessionMessage(http, created.id, { input }, { idempotencyKey: messageKey });
|
|
60
|
-
}
|
|
61
|
-
export async function getSession(http, sessionId) {
|
|
62
|
-
const result = await http.request(`/api/sessions/${encodeURIComponent(sessionId)}`);
|
|
63
|
-
return unwrapSession(result);
|
|
64
|
-
}
|
|
65
|
-
export async function listSessions(http, query) {
|
|
66
|
-
validateSessionListQuery(query);
|
|
67
|
-
const params = {};
|
|
68
|
-
if (query?.status !== undefined)
|
|
69
|
-
params.status = query.status;
|
|
70
|
-
if (query?.since !== undefined)
|
|
71
|
-
params.since = query.since;
|
|
72
|
-
if (query?.limit !== undefined)
|
|
73
|
-
params.limit = String(query.limit);
|
|
74
|
-
if (query?.cursor !== undefined)
|
|
75
|
-
params.cursor = query.cursor;
|
|
76
|
-
const page = await http.request("/api/sessions", {}, params);
|
|
77
|
-
if (!isRecord(page) || !Array.isArray(page.sessions)) {
|
|
78
|
-
throw new SessionStateError("sessions.list returned an invalid page: sessions must be an array");
|
|
79
|
-
}
|
|
80
|
-
const sessions = page.sessions.map((value, index) => {
|
|
81
|
-
if (!isRecord(value)) {
|
|
82
|
-
throw new SessionStateError(`sessions.list returned an invalid row at index ${index}`);
|
|
83
|
-
}
|
|
84
|
-
for (const field of ["id", "status", "createdAt", "updatedAt"]) {
|
|
85
|
-
if (typeof value[field] !== "string" || value[field].length === 0) {
|
|
86
|
-
throw new SessionStateError(`sessions.list row ${index} has an invalid ${field}`);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
if (!SESSION_STATUS_SET.has(value.status)) {
|
|
90
|
-
throw new SessionStateError(`sessions.list row ${index} has an unknown lifecycle status`);
|
|
91
|
-
}
|
|
92
|
-
assertCanonicalSessionWireFields(value, `sessions.list row ${index}`);
|
|
93
|
-
if (typeof value.acceptsMessages !== "boolean") {
|
|
94
|
-
throw new SessionStateError(`sessions.list row ${index} has an invalid acceptsMessages`);
|
|
95
|
-
}
|
|
96
|
-
if (value.costUsd !== undefined &&
|
|
97
|
-
(typeof value.costUsd !== "number" || !Number.isFinite(value.costUsd) || value.costUsd < 0)) {
|
|
98
|
-
throw new SessionStateError(`sessions.list row ${index} has an invalid costUsd`);
|
|
99
|
-
}
|
|
100
|
-
const currentRun = normalizeOptionalSessionRun(value.currentRun, value.id, `sessions.list row ${index}.currentRun`);
|
|
101
|
-
const lastRun = normalizeOptionalSessionRun(value.lastRun, value.id, `sessions.list row ${index}.lastRun`);
|
|
102
|
-
// `providerFault` is detail-only and must never leak onto SessionSummary.
|
|
103
|
-
const { providerFault: _providerFault, ...normalized } = normalizeSessionRuntime(value, `sessions.list row ${index}`);
|
|
104
|
-
return {
|
|
105
|
-
...normalized,
|
|
106
|
-
...(currentRun !== undefined ? { currentRun } : {}),
|
|
107
|
-
...(lastRun !== undefined ? { lastRun } : {})
|
|
108
|
-
};
|
|
109
|
-
});
|
|
110
|
-
if (page.nextCursor !== undefined && typeof page.nextCursor !== "string") {
|
|
111
|
-
throw new SessionStateError("sessions.list returned an invalid page: nextCursor must be a string");
|
|
112
|
-
}
|
|
113
|
-
return {
|
|
114
|
-
sessions,
|
|
115
|
-
...(typeof page.nextCursor === "string" ? { nextCursor: page.nextCursor } : {})
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
function validateSessionListQuery(query) {
|
|
119
|
-
if (query?.limit !== undefined && (!Number.isInteger(query.limit) || query.limit < 1 || query.limit > 100)) {
|
|
120
|
-
throw configError("limit", "sessions.list limit must be an integer between 1 and 100");
|
|
121
|
-
}
|
|
122
|
-
if (query?.status !== undefined && !SESSION_STATUS_SET.has(query.status)) {
|
|
123
|
-
throw configError("status", "sessions.list status must be a session lifecycle status");
|
|
124
|
-
}
|
|
125
|
-
if (query?.since !== undefined && (query.since.length === 0 || !Number.isFinite(Date.parse(query.since)))) {
|
|
126
|
-
throw configError("since", "sessions.list since must be an ISO-8601 timestamp");
|
|
127
|
-
}
|
|
128
|
-
if (query?.cursor !== undefined && query.cursor.length === 0) {
|
|
129
|
-
throw configError("cursor", "sessions.list cursor must be a non-empty opaque string");
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
export async function sendSessionMessage(http, sessionId, request, options) {
|
|
133
|
-
const headers = idempotencyHeaders(options);
|
|
134
|
-
const accepted = await http.request(`/api/sessions/${encodeURIComponent(sessionId)}/messages`, {
|
|
135
|
-
method: "POST",
|
|
136
|
-
...(headers ? { headers } : {}),
|
|
137
|
-
body: JSON.stringify(request)
|
|
138
|
-
});
|
|
139
|
-
return normalizeSessionMessageAccepted(accepted, sessionId);
|
|
140
|
-
}
|
|
141
|
-
export async function listSessionMessages(http, sessionId, query) {
|
|
142
|
-
const params = {};
|
|
143
|
-
if (query?.limit !== undefined)
|
|
144
|
-
params.limit = String(query.limit);
|
|
145
|
-
if (query?.cursor !== undefined)
|
|
146
|
-
params.cursor = query.cursor;
|
|
147
|
-
if (query?.since !== undefined)
|
|
148
|
-
params.since = query.since;
|
|
149
|
-
return http.request(`/api/sessions/${encodeURIComponent(sessionId)}/messages`, {}, params);
|
|
150
|
-
}
|
|
151
|
-
export async function suspendSession(http, sessionId) {
|
|
152
|
-
const accepted = await http.request(`/api/sessions/${encodeURIComponent(sessionId)}/suspend`, { method: "POST" });
|
|
153
|
-
return normalizeSessionAccepted(accepted, "session suspend response");
|
|
154
|
-
}
|
|
155
|
-
export async function cancelSession(http, sessionId) {
|
|
156
|
-
const accepted = await http.request(`/api/sessions/${encodeURIComponent(sessionId)}/cancel`, { method: "POST" });
|
|
157
|
-
return normalizeSessionAccepted(accepted, "session cancel response");
|
|
158
|
-
}
|
|
159
|
-
export async function resumeSession(http, sessionId) {
|
|
160
|
-
const accepted = await http.request(`/api/sessions/${encodeURIComponent(sessionId)}/resume`, { method: "POST" });
|
|
161
|
-
return normalizeSessionAccepted(accepted, "session resume response");
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Request the HITL write-gate: park the session `awaiting_approval` before its
|
|
165
|
-
* next gated action (mirrors {@link suspendSession}). Imperative counterpart to
|
|
166
|
-
* the declarative submission-time `approvalGate`.
|
|
167
|
-
*/
|
|
168
|
-
export async function requestApproval(http, sessionId) {
|
|
169
|
-
const accepted = await http.request(`/api/sessions/${encodeURIComponent(sessionId)}/request-approval`, { method: "POST" });
|
|
170
|
-
return normalizeSessionAccepted(accepted, "session approval-request response");
|
|
171
|
-
}
|
|
172
|
-
/** Approve an `awaiting_approval` session so the held turn resumes (→ running). */
|
|
173
|
-
export async function approveSession(http, sessionId) {
|
|
174
|
-
const accepted = await http.request(`/api/sessions/${encodeURIComponent(sessionId)}/approve`, { method: "POST" });
|
|
175
|
-
return normalizeSessionAccepted(accepted, "session approve response");
|
|
176
|
-
}
|
|
177
|
-
/** Deny an `awaiting_approval` session so the held turn is cancelled (→ cancelled). */
|
|
178
|
-
export async function denySession(http, sessionId) {
|
|
179
|
-
const accepted = await http.request(`/api/sessions/${encodeURIComponent(sessionId)}/deny`, { method: "POST" });
|
|
180
|
-
return normalizeSessionAccepted(accepted, "session deny response");
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* Delete a session and purge its stored footprint.
|
|
184
|
-
*
|
|
185
|
-
* Answers 200 with a BODY, not 204 — the deleted session plus the counters from
|
|
186
|
-
* a best-effort cleanup. This used to be typed `SessionStateChangeAccepted |
|
|
187
|
-
* void`, which said the body might not arrive and hid the two cleanup fields
|
|
188
|
-
* entirely; it always arrives, and `cleanupComplete: false` is the only signal
|
|
189
|
-
* that some stored objects survived the delete.
|
|
190
|
-
*/
|
|
191
|
-
export async function deleteSession(http, sessionId) {
|
|
192
|
-
const accepted = await http.request(`/api/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
|
|
193
|
-
const normalized = normalizeSessionAccepted(accepted, "session delete response");
|
|
194
|
-
if (typeof normalized.purgedSessionFileObjects !== "number") {
|
|
195
|
-
throw new SessionStateError("session delete response is missing purgedSessionFileObjects");
|
|
196
|
-
}
|
|
197
|
-
if (typeof normalized.cleanupComplete !== "boolean") {
|
|
198
|
-
throw new SessionStateError("session delete response is missing cleanupComplete");
|
|
199
|
-
}
|
|
200
|
-
return normalized;
|
|
201
|
-
}
|
|
202
|
-
export async function listSessionEvents(http, sessionId) {
|
|
203
|
-
const events = [];
|
|
204
|
-
for await (const event of iterateSessionEvents(http, sessionId))
|
|
205
|
-
events.push(event);
|
|
206
|
-
return events;
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Lazily traverse a session's durable event history. At most one bounded API
|
|
210
|
-
* page is retained by this iterator, and breaking the loop prevents later
|
|
211
|
-
* pages from being requested.
|
|
212
|
-
*/
|
|
213
|
-
export async function* iterateSessionEvents(http, sessionId, options = {}) {
|
|
214
|
-
const pageSize = options.pageSize;
|
|
215
|
-
if (pageSize !== undefined && (!Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 1000)) {
|
|
216
|
-
throw configError("pageSize", "session event pageSize must be an integer between 1 and 1000");
|
|
217
|
-
}
|
|
218
|
-
const path = `/api/sessions/${encodeURIComponent(sessionId)}/events`;
|
|
219
|
-
const seenCursors = new Set();
|
|
220
|
-
let cursor;
|
|
221
|
-
for (let pageIndex = 0; pageIndex < LIST_EVENTS_PAGE_BUDGET; pageIndex += 1) {
|
|
222
|
-
if (options.signal?.aborted)
|
|
223
|
-
return;
|
|
224
|
-
const query = {
|
|
225
|
-
...(pageSize === undefined ? {} : { limit: String(pageSize) }),
|
|
226
|
-
...(cursor === undefined ? {} : { cursor })
|
|
227
|
-
};
|
|
228
|
-
const result = await http.request(path, options.signal === undefined ? {} : { signal: options.signal }, query);
|
|
229
|
-
for (const event of result.events)
|
|
230
|
-
yield event;
|
|
231
|
-
if (result.nextCursor === undefined || result.nextCursor === null)
|
|
232
|
-
return;
|
|
233
|
-
if (typeof result.nextCursor !== "string" || result.nextCursor.length === 0) {
|
|
234
|
-
throw new SessionStateError("session events response contains an invalid nextCursor", { sessionId });
|
|
235
|
-
}
|
|
236
|
-
if (seenCursors.has(result.nextCursor)) {
|
|
237
|
-
throw new SessionStateError("session events pagination repeated a cursor", {
|
|
238
|
-
sessionId,
|
|
239
|
-
cursor: result.nextCursor
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
seenCursors.add(result.nextCursor);
|
|
243
|
-
cursor = result.nextCursor;
|
|
244
|
-
}
|
|
245
|
-
throw new SessionStateError("session events pagination exceeded its page budget", {
|
|
246
|
-
sessionId,
|
|
247
|
-
pageBudget: LIST_EVENTS_PAGE_BUDGET
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
export async function getSessionOtlpPage(http, sessionId, signal, cursor, abortSignal) {
|
|
251
|
-
const path = `/api/sessions/${encodeURIComponent(sessionId)}/otel`;
|
|
252
|
-
const { response } = await http.download(path, abortSignal === undefined
|
|
253
|
-
? { headers: { accept: "application/json" } }
|
|
254
|
-
: { headers: { accept: "application/json" }, signal: abortSignal }, { signal, ...(cursor === undefined ? {} : { cursor }) });
|
|
255
|
-
let body;
|
|
256
|
-
try {
|
|
257
|
-
body = await response.json();
|
|
258
|
-
}
|
|
259
|
-
catch (cause) {
|
|
260
|
-
throw new SessionStateError("session OTLP response is not valid JSON", { sessionId, signal }, { cause });
|
|
261
|
-
}
|
|
262
|
-
assertStandardsPureOtlpBody(body, sessionId, signal);
|
|
263
|
-
const responseCursor = response.headers.get("x-aex-next-cursor");
|
|
264
|
-
if (responseCursor !== null && responseCursor.trim().length === 0) {
|
|
265
|
-
throw new SessionStateError("session OTLP response contains an invalid x-aex-next-cursor", {
|
|
266
|
-
sessionId,
|
|
267
|
-
signal
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
return {
|
|
271
|
-
body,
|
|
272
|
-
...(responseCursor === null ? {} : { nextCursor: responseCursor })
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* Lazily traverse standards-pure OTLP pages. Pagination metadata remains in
|
|
277
|
-
* `x-aex-next-cursor`; it is never mixed into an OTLP request body.
|
|
278
|
-
*/
|
|
279
|
-
export async function* iterateSessionOtlpPages(http, sessionId, signal, options = {}) {
|
|
280
|
-
const seenCursors = new Set();
|
|
281
|
-
let cursor;
|
|
282
|
-
for (let pageIndex = 0; pageIndex < LIST_EVENTS_PAGE_BUDGET; pageIndex += 1) {
|
|
283
|
-
if (options.signal?.aborted)
|
|
284
|
-
return;
|
|
285
|
-
const page = signal === "traces"
|
|
286
|
-
? await getSessionOtlpPage(http, sessionId, "traces", cursor, options.signal)
|
|
287
|
-
: await getSessionOtlpPage(http, sessionId, "logs", cursor, options.signal);
|
|
288
|
-
if (page.nextCursor !== undefined && seenCursors.has(page.nextCursor)) {
|
|
289
|
-
throw new SessionStateError("session OTLP pagination repeated a cursor", {
|
|
290
|
-
sessionId,
|
|
291
|
-
signal,
|
|
292
|
-
cursor: page.nextCursor
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
yield page.body;
|
|
296
|
-
if (page.nextCursor === undefined)
|
|
297
|
-
return;
|
|
298
|
-
seenCursors.add(page.nextCursor);
|
|
299
|
-
cursor = page.nextCursor;
|
|
300
|
-
}
|
|
301
|
-
throw new SessionStateError("session OTLP pagination exceeded its page budget", {
|
|
302
|
-
sessionId,
|
|
303
|
-
signal,
|
|
304
|
-
pageBudget: LIST_EVENTS_PAGE_BUDGET
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
function assertStandardsPureOtlpBody(body, sessionId, signal) {
|
|
308
|
-
const root = signal === "traces" ? "resourceSpans" : "resourceLogs";
|
|
309
|
-
if (!isRecord(body) || Object.keys(body).length !== 1 || !Array.isArray(body[root])) {
|
|
310
|
-
throw new SessionStateError(`session OTLP ${signal} response must contain only ${root}`, {
|
|
311
|
-
sessionId,
|
|
312
|
-
signal
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
export async function listSessionFiles(http, sessionId, query) {
|
|
317
|
-
const requestedCheckpointId = query?.checkpointId === undefined
|
|
318
|
-
? undefined
|
|
319
|
-
: requireSessionFileCheckpointId(query.checkpointId, "files.list");
|
|
320
|
-
const result = await http.request(`/api/sessions/${encodeURIComponent(sessionId)}/files`, {}, requestedCheckpointId === undefined ? {} : { checkpointId: requestedCheckpointId });
|
|
321
|
-
if (!Array.isArray(result.files) ||
|
|
322
|
-
!result.revision ||
|
|
323
|
-
typeof result.revision.checkpointId !== "string" ||
|
|
324
|
-
!SESSION_FILE_CHECKPOINT_ID_PATTERN.test(result.revision.checkpointId) ||
|
|
325
|
-
typeof result.revision.runId !== "string" ||
|
|
326
|
-
result.revision.runId.trim().length === 0 ||
|
|
327
|
-
!Number.isSafeInteger(result.revision.turnSeq) ||
|
|
328
|
-
result.revision.turnSeq < 1 ||
|
|
329
|
-
typeof result.revision.committedAt !== "string" ||
|
|
330
|
-
!Number.isFinite(Date.parse(result.revision.committedAt)) ||
|
|
331
|
-
!Number.isSafeInteger(result.revision.throughSeq) ||
|
|
332
|
-
result.revision.throughSeq < 0) {
|
|
333
|
-
throw new SessionStateError("session files response is missing checkpoint revision metadata", { sessionId });
|
|
334
|
-
}
|
|
335
|
-
if (requestedCheckpointId !== undefined && result.revision.checkpointId !== requestedCheckpointId) {
|
|
336
|
-
throw new SessionStateError("session files response did not resolve the requested checkpoint", {
|
|
337
|
-
sessionId,
|
|
338
|
-
requestedCheckpointId,
|
|
339
|
-
resolvedCheckpointId: result.revision.checkpointId
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
for (const file of result.files) {
|
|
343
|
-
if (typeof file.id !== "string" || file.id.length === 0) {
|
|
344
|
-
throw new SessionStateError("session files response contains an invalid file id", { sessionId });
|
|
345
|
-
}
|
|
346
|
-
if (file.checkpointId !== result.revision.checkpointId) {
|
|
347
|
-
throw new SessionStateError("session file is not pinned to the response checkpoint", {
|
|
348
|
-
sessionId,
|
|
349
|
-
fileId: file.id,
|
|
350
|
-
fileCheckpointId: file.checkpointId,
|
|
351
|
-
checkpointId: result.revision.checkpointId
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
if (!Number.isSafeInteger(file.sizeBytes) || file.sizeBytes < 0) {
|
|
355
|
-
throw new SessionStateError("session file is missing a valid committed byte length", {
|
|
356
|
-
sessionId,
|
|
357
|
-
fileId: file.id,
|
|
358
|
-
sizeBytes: file.sizeBytes
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
if (!SESSION_FILE_SHA256_PATTERN.test(file.sha256)) {
|
|
362
|
-
throw new SessionStateError("session file is missing a valid committed SHA-256 digest", {
|
|
363
|
-
sessionId,
|
|
364
|
-
fileId: file.id
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
return {
|
|
369
|
-
revision: result.revision,
|
|
370
|
-
files: query === undefined ? result.files : filterSessionFiles(result.files, query)
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
export async function getSessionCoordinatorTicket(http, sessionId) {
|
|
374
|
-
return http.request(`/api/sessions/${encodeURIComponent(sessionId)}/events/ticket`, { method: "POST" });
|
|
375
|
-
}
|
|
376
|
-
// Bound the transparent pager: the read route caps each page at 1000, so this
|
|
377
|
-
// admits up to ~1e6 events before bailing — past any real session, but bounded so a
|
|
378
|
-
// server that never clears `nextCursor` can't loop forever.
|
|
379
|
-
const LIST_EVENTS_PAGE_BUDGET = 1000;
|
|
380
|
-
export async function findSessionFiles(http, sessionId, query) {
|
|
381
|
-
return (await listSessionFiles(http, sessionId, query)).files;
|
|
382
|
-
}
|
|
383
|
-
export async function findSessionFile(http, sessionId, query) {
|
|
384
|
-
const matches = await findSessionFiles(http, sessionId, query);
|
|
385
|
-
if (matches.length === 0)
|
|
386
|
-
return null;
|
|
387
|
-
if (matches.length === 1)
|
|
388
|
-
return matches[0];
|
|
389
|
-
throw new SessionStateError("files.findOne: file query matched multiple files", {
|
|
390
|
-
sessionId,
|
|
391
|
-
matches: matches.map((file) => file.filename ?? file.id)
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
export async function sessionFileLink(http, sessionId, selectorOrQuery, options) {
|
|
395
|
-
const requestedCheckpointId = options?.checkpointId === undefined
|
|
396
|
-
? undefined
|
|
397
|
-
: requireSessionFileCheckpointId(options.checkpointId, "files.link");
|
|
398
|
-
const file = await resolveSessionFileLinkTarget(http, sessionId, selectorOrQuery, requestedCheckpointId);
|
|
399
|
-
const expiresInSeconds = normalizeSessionFileLinkExpiresIn(options?.expiresIn);
|
|
400
|
-
const checkpointId = requestedCheckpointId ?? file.checkpointId;
|
|
401
|
-
// The WIRE type, not `SessionFileLink`: the server sends no absolute
|
|
402
|
-
// `expiresAt`, and asking for the client type here is what made the synthesis
|
|
403
|
-
// below look like a fallback for a field the server sometimes omits.
|
|
404
|
-
const result = await http.request(sessionFileRoute(sessionId, file.id, "link", checkpointId), {
|
|
405
|
-
method: "POST",
|
|
406
|
-
body: JSON.stringify({ expiresInSeconds })
|
|
407
|
-
});
|
|
408
|
-
const effectiveExpiresIn = result.expiresInSeconds ?? expiresInSeconds;
|
|
409
|
-
return {
|
|
410
|
-
url: result.url,
|
|
411
|
-
expiresInSeconds: effectiveExpiresIn,
|
|
412
|
-
expiresAt: syntheticExpiresAt(effectiveExpiresIn),
|
|
413
|
-
file
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
/**
|
|
417
|
-
* The hosted API returns `{ url, expiresInSeconds }` without an absolute
|
|
418
|
-
* timestamp; the documented `link.expiresAt` is synthesized client-side from
|
|
419
|
-
* the mint time so it is always present on a returned link.
|
|
420
|
-
*/
|
|
421
|
-
function syntheticExpiresAt(expiresInSeconds) {
|
|
422
|
-
return new Date(Date.now() + expiresInSeconds * 1000).toISOString();
|
|
423
|
-
}
|
|
424
|
-
export async function eventArchiveLink(http, sessionId, options) {
|
|
425
|
-
const expiresInSeconds = normalizeSessionFileLinkExpiresIn(options?.expiresIn);
|
|
426
|
-
// `EventArchiveLinkResponseSchema` — `{ url, expiresInSeconds }` and NO
|
|
427
|
-
// `file`, unlike the per-file link. Hence `SessionFileLink.file` staying
|
|
428
|
-
// optional while `expiresAt` and `expiresInSeconds` are not.
|
|
429
|
-
const result = await http.request(`/api/sessions/${encodeURIComponent(sessionId)}/events/link`, {
|
|
430
|
-
method: "POST",
|
|
431
|
-
body: JSON.stringify({ expiresInSeconds })
|
|
432
|
-
});
|
|
433
|
-
const effectiveExpiresIn = result.expiresInSeconds ?? expiresInSeconds;
|
|
434
|
-
return {
|
|
435
|
-
url: result.url,
|
|
436
|
-
expiresInSeconds: effectiveExpiresIn,
|
|
437
|
-
expiresAt: syntheticExpiresAt(effectiveExpiresIn)
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
export async function downloadSessionFile(http, sessionId, selector, options) {
|
|
441
|
-
const timeoutMs = normalizeSessionFileTransferTimeoutMs(options?.timeoutMs);
|
|
442
|
-
const requestedCheckpointId = options?.checkpointId === undefined
|
|
443
|
-
? undefined
|
|
444
|
-
: requireSessionFileCheckpointId(options.checkpointId, "files.download");
|
|
445
|
-
const file = await resolveAuthoritativeSessionFile(http, sessionId, selector, requestedCheckpointId);
|
|
446
|
-
const checkpointId = requestedCheckpointId ?? file.checkpointId;
|
|
447
|
-
const path = sessionFileRoute(sessionId, file.id, "download", checkpointId);
|
|
448
|
-
return { file, bytes: await downloadSessionFileBytesWithRetry(http, path, timeoutMs, file) };
|
|
449
|
-
}
|
|
450
|
-
/** Byte ceiling for {@link readSessionFileText} — a hard cap even if a caller asks for more. */
|
|
451
|
-
export const READ_SESSION_FILE_TEXT_MAX_BYTES = 10_000_000;
|
|
452
|
-
/** Default `maxBytes` for {@link readSessionFileText} — a chat-sized preview. */
|
|
453
|
-
export const READ_SESSION_FILE_TEXT_DEFAULT_BYTES = 50_000;
|
|
454
|
-
/** Default per-attempt timeout while fetching or reading one session file body. */
|
|
455
|
-
export const SESSION_FILE_TRANSFER_DEFAULT_TIMEOUT_MS = 30_000;
|
|
456
|
-
/** Idempotent file GETs retry once on a transfer timeout. */
|
|
457
|
-
export const SESSION_FILE_TRANSFER_ATTEMPTS = 2;
|
|
458
|
-
/**
|
|
459
|
-
* Read ONE session file as byte-capped, decoded UTF-8 text. Built for handing a session
|
|
460
|
-
* deliverable to an LLM tool: it streams the file body and STOPS at `maxBytes`, so
|
|
461
|
-
* a 200 MB artifact never fully buffers in memory or context. `truncated` is true
|
|
462
|
-
* when the file is larger than the cap. Optionally `grep` keeps only matching lines.
|
|
463
|
-
*
|
|
464
|
-
* Selector is the same `{ path }` / `{ id }` shape as `downloadSessionFile`.
|
|
465
|
-
* Both forms resolve against the authoritative checkpoint snapshot first.
|
|
466
|
-
*/
|
|
467
|
-
export async function readSessionFileText(http, sessionId, selector, options) {
|
|
468
|
-
const timeoutMs = normalizeSessionFileTransferTimeoutMs(options?.timeoutMs);
|
|
469
|
-
const maxBytes = Math.max(1, Math.min(options?.maxBytes ?? READ_SESSION_FILE_TEXT_DEFAULT_BYTES, READ_SESSION_FILE_TEXT_MAX_BYTES));
|
|
470
|
-
const requestedCheckpointId = options?.checkpointId === undefined
|
|
471
|
-
? undefined
|
|
472
|
-
: requireSessionFileCheckpointId(options.checkpointId, "files.read");
|
|
473
|
-
const file = await resolveAuthoritativeSessionFile(http, sessionId, selector, requestedCheckpointId);
|
|
474
|
-
const checkpointId = requestedCheckpointId ?? file.checkpointId;
|
|
475
|
-
const path = sessionFileRoute(sessionId, file.id, "download", checkpointId);
|
|
476
|
-
const capped = await readSessionFileTextWithRetry(http, path, maxBytes, timeoutMs, file);
|
|
477
|
-
const text = options?.grep === undefined ? capped.text : grepLines(capped.text, options.grep);
|
|
478
|
-
return { file, text, truncated: capped.truncated, totalBytes: capped.totalBytes };
|
|
479
|
-
}
|
|
480
|
-
async function downloadSessionFileBytesWithRetry(http, path, timeoutMs, file) {
|
|
481
|
-
return sessionFileTransferWithRetry(path, timeoutMs, async () => {
|
|
482
|
-
const response = await downloadSessionFileResponse(http, path, timeoutMs);
|
|
483
|
-
assertSessionFileResponseLength(file, response);
|
|
484
|
-
const bytes = await readResponseBytes(response, timeoutMs);
|
|
485
|
-
assertSessionFileIntegrity(file, bytes);
|
|
486
|
-
return bytes;
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
|
-
async function resolveAuthoritativeSessionFile(http, sessionId, selector, checkpointOverride) {
|
|
490
|
-
const selectorCheckpointId = isPathSelector(selector)
|
|
491
|
-
? undefined
|
|
492
|
-
: requireSessionFileCheckpointId(selector && typeof selector === "object" ? selector.checkpointId : undefined, "session file id selector");
|
|
493
|
-
const checkpointId = checkpointOverride === undefined
|
|
494
|
-
? selectorCheckpointId
|
|
495
|
-
: requireSessionFileCheckpointId(checkpointOverride, "session file operation");
|
|
496
|
-
const snapshot = await listSessionFiles(http, sessionId, checkpointId === undefined ? undefined : { checkpointId });
|
|
497
|
-
if (isPathSelector(selector))
|
|
498
|
-
return resolveSessionFileSelector(snapshot.files, selector, sessionId);
|
|
499
|
-
if (typeof selector.id !== "string" || selector.id.length === 0) {
|
|
500
|
-
throw new SessionStateError("files.download: selector must include a file id or path", { sessionId });
|
|
501
|
-
}
|
|
502
|
-
const file = snapshot.files.find((candidate) => candidate.id === selector.id);
|
|
503
|
-
if (file !== undefined)
|
|
504
|
-
return file;
|
|
505
|
-
throw new SessionStateError(`files.download: file id "${selector.id}" was not found in checkpoint`, {
|
|
506
|
-
sessionId,
|
|
507
|
-
fileId: selector.id,
|
|
508
|
-
checkpointId: snapshot.revision.checkpointId
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
class SessionFileIntegrityError extends SessionStateError {
|
|
512
|
-
}
|
|
513
|
-
function assertSessionFileResponseLength(file, response) {
|
|
514
|
-
const raw = response.headers.get("content-length");
|
|
515
|
-
if (raw === null)
|
|
516
|
-
return;
|
|
517
|
-
const declaredSizeBytes = /^\d+$/.test(raw) ? Number(raw) : Number.NaN;
|
|
518
|
-
if (Number.isSafeInteger(declaredSizeBytes) && declaredSizeBytes === file.sizeBytes)
|
|
519
|
-
return;
|
|
520
|
-
throw new SessionFileIntegrityError("files.download: checkpoint file integrity verification failed", {
|
|
521
|
-
fileId: file.id,
|
|
522
|
-
checkpointId: file.checkpointId,
|
|
523
|
-
expectedSizeBytes: file.sizeBytes,
|
|
524
|
-
declaredSizeBytes: raw
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
function assertSessionFileIntegrity(file, bytes) {
|
|
528
|
-
const actualSha256 = createHash("sha256").update(bytes).digest("hex");
|
|
529
|
-
if (bytes.byteLength === file.sizeBytes && actualSha256 === file.sha256)
|
|
530
|
-
return;
|
|
531
|
-
throw new SessionFileIntegrityError("files.download: checkpoint file integrity verification failed", {
|
|
532
|
-
fileId: file.id,
|
|
533
|
-
checkpointId: file.checkpointId,
|
|
534
|
-
expectedSizeBytes: file.sizeBytes,
|
|
535
|
-
actualSizeBytes: bytes.byteLength,
|
|
536
|
-
expectedSha256: file.sha256,
|
|
537
|
-
actualSha256
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
async function readSessionFileTextWithRetry(http, path, maxBytes, timeoutMs, file) {
|
|
541
|
-
return sessionFileTransferWithRetry(path, timeoutMs, async () => {
|
|
542
|
-
const response = await downloadSessionFileResponse(http, path, timeoutMs);
|
|
543
|
-
assertSessionFileResponseLength(file, response);
|
|
544
|
-
return readCappedText(response, maxBytes, timeoutMs, file);
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
async function downloadSessionFileResponse(http, path, timeoutMs) {
|
|
548
|
-
const controller = new AbortController();
|
|
549
|
-
const { response } = await withSessionFileTransferTimeout(http.download(path, { signal: controller.signal }), timeoutMs, () => controller.abort(), "download-open");
|
|
550
|
-
return response;
|
|
551
|
-
}
|
|
552
|
-
async function sessionFileTransferWithRetry(path, timeoutMs, action) {
|
|
553
|
-
const startedMs = Date.now();
|
|
554
|
-
let lastTimeout;
|
|
555
|
-
for (let attempt = 1; attempt <= SESSION_FILE_TRANSFER_ATTEMPTS; attempt += 1) {
|
|
556
|
-
try {
|
|
557
|
-
return await action();
|
|
558
|
-
}
|
|
559
|
-
catch (err) {
|
|
560
|
-
if (!(err instanceof SessionFileTransferTimeoutError))
|
|
561
|
-
throw err;
|
|
562
|
-
lastTimeout = err;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
throw new AexNetworkError({
|
|
566
|
-
method: "GET",
|
|
567
|
-
host: "",
|
|
568
|
-
path,
|
|
569
|
-
cause: lastTimeout ?? new SessionFileTransferTimeoutError("unknown", timeoutMs),
|
|
570
|
-
attempts: SESSION_FILE_TRANSFER_ATTEMPTS,
|
|
571
|
-
elapsedMs: Date.now() - startedMs
|
|
572
|
-
});
|
|
573
|
-
}
|
|
574
|
-
function sessionFileRoute(sessionId, fileId, action, checkpointId) {
|
|
575
|
-
if (!checkpointId) {
|
|
576
|
-
throw new SessionStateError("session file operations require checkpointId", { sessionId, fileId });
|
|
577
|
-
}
|
|
578
|
-
return `/api/sessions/${encodeURIComponent(sessionId)}/files/${encodeURIComponent(fileId)}/${action}` +
|
|
579
|
-
`?checkpointId=${encodeURIComponent(checkpointId)}`;
|
|
580
|
-
}
|
|
581
|
-
function requireSessionFileCheckpointId(value, context) {
|
|
582
|
-
if (typeof value !== "string" || !SESSION_FILE_CHECKPOINT_ID_PATTERN.test(value)) {
|
|
583
|
-
throw new SessionStateError(`${context}: checkpointId must match ${SESSION_FILE_CHECKPOINT_ID_PATTERN.source}`, {
|
|
584
|
-
checkpointId: value
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
return value;
|
|
588
|
-
}
|
|
589
|
-
function normalizeSessionFileTransferTimeoutMs(value) {
|
|
590
|
-
if (value === undefined)
|
|
591
|
-
return SESSION_FILE_TRANSFER_DEFAULT_TIMEOUT_MS;
|
|
592
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
593
|
-
throw configError("timeoutMs", "files.download: timeoutMs must be a positive finite number");
|
|
594
|
-
}
|
|
595
|
-
return Math.max(1, Math.floor(value));
|
|
596
|
-
}
|
|
597
|
-
class SessionFileTransferTimeoutError extends Error {
|
|
598
|
-
code = "ETIMEDOUT";
|
|
599
|
-
phase;
|
|
600
|
-
constructor(phase, timeoutMs) {
|
|
601
|
-
super(`file transfer phase=${phase} timed out after ${timeoutMs}ms`);
|
|
602
|
-
this.name = "SessionFileTransferTimeoutError";
|
|
603
|
-
this.phase = phase;
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
async function withSessionFileTransferTimeout(promise, timeoutMs, abort, phase) {
|
|
607
|
-
let timedOut = false;
|
|
608
|
-
let timeout;
|
|
609
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
610
|
-
timeout = setTimeout(() => {
|
|
611
|
-
timedOut = true;
|
|
612
|
-
reject(new SessionFileTransferTimeoutError(phase, timeoutMs));
|
|
613
|
-
queueMicrotask(() => {
|
|
614
|
-
try {
|
|
615
|
-
abort();
|
|
616
|
-
}
|
|
617
|
-
catch {
|
|
618
|
-
// Best effort: the timeout itself is the user-facing failure.
|
|
619
|
-
}
|
|
620
|
-
});
|
|
621
|
-
}, timeoutMs);
|
|
622
|
-
});
|
|
623
|
-
try {
|
|
624
|
-
return await Promise.race([promise, timeoutPromise]);
|
|
625
|
-
}
|
|
626
|
-
catch (err) {
|
|
627
|
-
if (timedOut && isAbortLikeError(err))
|
|
628
|
-
throw new SessionFileTransferTimeoutError(phase, timeoutMs);
|
|
629
|
-
throw err;
|
|
630
|
-
}
|
|
631
|
-
finally {
|
|
632
|
-
if (timeout !== undefined)
|
|
633
|
-
clearTimeout(timeout);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
function isAbortLikeError(err) {
|
|
637
|
-
const name = err?.name;
|
|
638
|
-
return name === "AbortError";
|
|
639
|
-
}
|
|
640
|
-
async function readResponseBytes(response, timeoutMs) {
|
|
641
|
-
const body = response.body;
|
|
642
|
-
if (!body) {
|
|
643
|
-
const buffer = await withSessionFileTransferTimeout(response.arrayBuffer(), timeoutMs, () => { }, "body-read");
|
|
644
|
-
return new Uint8Array(buffer);
|
|
645
|
-
}
|
|
646
|
-
const reader = body.getReader();
|
|
647
|
-
const chunks = [];
|
|
648
|
-
try {
|
|
649
|
-
while (true) {
|
|
650
|
-
const { done, value } = await withSessionFileTransferTimeout(reader.read(), timeoutMs, () => {
|
|
651
|
-
void reader.cancel().catch(() => { });
|
|
652
|
-
}, "body-read");
|
|
653
|
-
if (done)
|
|
654
|
-
break;
|
|
655
|
-
if (value && value.byteLength > 0)
|
|
656
|
-
chunks.push(value);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
finally {
|
|
660
|
-
void reader.cancel().catch(() => { });
|
|
661
|
-
}
|
|
662
|
-
return concatBytes(chunks);
|
|
663
|
-
}
|
|
664
|
-
/**
|
|
665
|
-
* Read a streamed response body up to `maxBytes` and decode as UTF-8. The
|
|
666
|
-
* checkpoint supplies the authoritative total size. A partial read cancels as
|
|
667
|
-
* soon as the retained prefix reaches the cap; a complete read reaches EOF and
|
|
668
|
-
* verifies both committed size and digest.
|
|
669
|
-
*/
|
|
670
|
-
async function readCappedText(response, maxBytes, timeoutMs, file) {
|
|
671
|
-
const decoder = new TextDecoder("utf-8");
|
|
672
|
-
const wholeFile = file.sizeBytes <= maxBytes;
|
|
673
|
-
const targetBytes = wholeFile ? file.sizeBytes : maxBytes;
|
|
674
|
-
const body = response.body;
|
|
675
|
-
if (!body) {
|
|
676
|
-
// No streaming body (some fetch polyfills) — buffer, then slice to the cap.
|
|
677
|
-
const buf = new Uint8Array(await withSessionFileTransferTimeout(response.arrayBuffer(), timeoutMs, () => { }, "body-read"));
|
|
678
|
-
if (wholeFile) {
|
|
679
|
-
assertSessionFileIntegrity(file, buf);
|
|
680
|
-
}
|
|
681
|
-
else if (buf.byteLength < targetBytes) {
|
|
682
|
-
throw new SessionFileIntegrityError("files.read: checkpoint file ended before the requested prefix", {
|
|
683
|
-
fileId: file.id,
|
|
684
|
-
checkpointId: file.checkpointId,
|
|
685
|
-
expectedPrefixBytes: targetBytes,
|
|
686
|
-
actualSizeBytes: buf.byteLength
|
|
687
|
-
});
|
|
688
|
-
}
|
|
689
|
-
return {
|
|
690
|
-
text: decoder.decode(buf.subarray(0, targetBytes)),
|
|
691
|
-
truncated: !wholeFile,
|
|
692
|
-
totalBytes: file.sizeBytes
|
|
693
|
-
};
|
|
694
|
-
}
|
|
695
|
-
const reader = body.getReader();
|
|
696
|
-
const chunks = [];
|
|
697
|
-
let retainedBytes = 0;
|
|
698
|
-
try {
|
|
699
|
-
while (wholeFile || retainedBytes < targetBytes) {
|
|
700
|
-
const { done, value } = await withSessionFileTransferTimeout(reader.read(), timeoutMs, () => {
|
|
701
|
-
void reader.cancel().catch(() => { });
|
|
702
|
-
}, "body-read");
|
|
703
|
-
if (done)
|
|
704
|
-
break;
|
|
705
|
-
if (value && value.byteLength > 0) {
|
|
706
|
-
const remainingBytes = targetBytes - retainedBytes;
|
|
707
|
-
const retainedFromChunk = Math.min(remainingBytes, value.byteLength);
|
|
708
|
-
if (retainedFromChunk > 0) {
|
|
709
|
-
chunks.push(retainedFromChunk === value.byteLength ? value : value.slice(0, retainedFromChunk));
|
|
710
|
-
retainedBytes += retainedFromChunk;
|
|
711
|
-
}
|
|
712
|
-
if (wholeFile && value.byteLength > remainingBytes) {
|
|
713
|
-
throw new SessionFileIntegrityError("files.read: checkpoint file exceeded its committed byte length", {
|
|
714
|
-
fileId: file.id,
|
|
715
|
-
checkpointId: file.checkpointId,
|
|
716
|
-
expectedSizeBytes: file.sizeBytes,
|
|
717
|
-
actualSizeBytesAtLeast: retainedBytes + value.byteLength - retainedFromChunk
|
|
718
|
-
});
|
|
719
|
-
}
|
|
720
|
-
if (!wholeFile && retainedBytes >= targetBytes)
|
|
721
|
-
break;
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
finally {
|
|
726
|
-
void reader.cancel().catch(() => { });
|
|
727
|
-
}
|
|
728
|
-
const merged = concatBytes(chunks);
|
|
729
|
-
if (wholeFile) {
|
|
730
|
-
assertSessionFileIntegrity(file, merged);
|
|
731
|
-
}
|
|
732
|
-
else if (retainedBytes < targetBytes) {
|
|
733
|
-
throw new SessionFileIntegrityError("files.read: checkpoint file ended before the requested prefix", {
|
|
734
|
-
fileId: file.id,
|
|
735
|
-
checkpointId: file.checkpointId,
|
|
736
|
-
expectedPrefixBytes: targetBytes,
|
|
737
|
-
actualSizeBytes: retainedBytes
|
|
738
|
-
});
|
|
739
|
-
}
|
|
740
|
-
return { text: decoder.decode(merged), truncated: !wholeFile, totalBytes: file.sizeBytes };
|
|
741
|
-
}
|
|
742
|
-
function concatBytes(chunks) {
|
|
743
|
-
if (chunks.length === 1)
|
|
744
|
-
return chunks[0];
|
|
745
|
-
const total = chunks.reduce((n, c) => n + c.byteLength, 0);
|
|
746
|
-
const out = new Uint8Array(total);
|
|
747
|
-
let offset = 0;
|
|
748
|
-
for (const c of chunks) {
|
|
749
|
-
out.set(c, offset);
|
|
750
|
-
offset += c.byteLength;
|
|
751
|
-
}
|
|
752
|
-
return out;
|
|
753
|
-
}
|
|
754
|
-
function grepLines(text, pattern) {
|
|
755
|
-
const test = typeof pattern === "string"
|
|
756
|
-
? (line) => line.toLowerCase().includes(pattern.toLowerCase())
|
|
757
|
-
: (line) => pattern.test(line);
|
|
758
|
-
return text
|
|
759
|
-
.split("\n")
|
|
760
|
-
.filter((line) => test(line))
|
|
761
|
-
.join("\n");
|
|
762
|
-
}
|
|
763
|
-
/**
|
|
764
|
-
* List a session's subagent child sessions (`GET /api/sessions/:id/children`). Each
|
|
765
|
-
* {@link ChildSessionRef} is a read-only lineage snapshot whose id addresses
|
|
766
|
-
* events, checkpointed files, and descendants. An empty array means the session
|
|
767
|
-
* spawned no children.
|
|
768
|
-
*/
|
|
769
|
-
export async function listSessionChildren(http, sessionId) {
|
|
770
|
-
const result = await http.request(`/api/sessions/${encodeURIComponent(sessionId)}/children`);
|
|
771
|
-
if (!isRecord(result) || !Array.isArray(result.children)) {
|
|
772
|
-
throw new SessionStateError("session children response must contain a children array");
|
|
773
|
-
}
|
|
774
|
-
return result.children.map(parseChildSessionRef);
|
|
775
|
-
}
|
|
776
|
-
function parseChildSessionRef(value, index) {
|
|
777
|
-
if (!isRecord(value)) {
|
|
778
|
-
throw new SessionStateError(`session children response has an invalid row at index ${index}`);
|
|
779
|
-
}
|
|
780
|
-
for (const field of ["id", "parentSessionId", "status", "createdAt", "updatedAt"]) {
|
|
781
|
-
if (typeof value[field] !== "string" || value[field].length === 0) {
|
|
782
|
-
throw new SessionStateError(`session children row ${index} has an invalid ${field}`);
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
if (!SESSION_STATUS_SET.has(value.status)) {
|
|
786
|
-
throw new SessionStateError(`session children row ${index} has an unknown lifecycle status`);
|
|
787
|
-
}
|
|
788
|
-
assertCanonicalSessionWireFields(value, `session children row ${index}`);
|
|
789
|
-
if (value.depth !== undefined && (!Number.isSafeInteger(value.depth) || value.depth < 1)) {
|
|
790
|
-
throw new SessionStateError(`session children row ${index} has an invalid depth`);
|
|
791
|
-
}
|
|
792
|
-
if (value.costUsd !== undefined &&
|
|
793
|
-
(typeof value.costUsd !== "number" || !Number.isFinite(value.costUsd) || value.costUsd < 0)) {
|
|
794
|
-
throw new SessionStateError(`session children row ${index} has an invalid costUsd`);
|
|
795
|
-
}
|
|
796
|
-
if (value.terminalAt !== undefined && value.terminalAt !== null && typeof value.terminalAt !== "string") {
|
|
797
|
-
throw new SessionStateError(`session children row ${index} has an invalid terminalAt`);
|
|
798
|
-
}
|
|
799
|
-
if (value.lastRun !== undefined && !isRecord(value.lastRun)) {
|
|
800
|
-
throw new SessionStateError(`session children row ${index} has an invalid lastRun`);
|
|
801
|
-
}
|
|
802
|
-
return value;
|
|
803
|
-
}
|
|
804
|
-
/**
|
|
805
|
-
* List a session's run-terminal webhook deliveries. Each finalized run has its
|
|
806
|
-
* own row; an empty array means the session has no webhook or no run has
|
|
807
|
-
* finalized yet.
|
|
808
|
-
*/
|
|
809
|
-
export async function getSessionWebhookDeliveries(http, sessionId) {
|
|
810
|
-
const result = await http.request(`/api/sessions/${encodeURIComponent(sessionId)}/webhook-deliveries`);
|
|
811
|
-
if (!isRecord(result) || !Array.isArray(result.deliveries)) {
|
|
812
|
-
throw new SessionStateError("session webhook deliveries response must contain a deliveries array");
|
|
813
|
-
}
|
|
814
|
-
return result.deliveries;
|
|
815
|
-
}
|
|
816
|
-
/**
|
|
817
|
-
* Manually re-trigger one run's webhook delivery: resets the row to `pending`
|
|
818
|
-
* and re-sends the frozen payload with the same run-scoped `webhook-id`.
|
|
819
|
-
*/
|
|
820
|
-
export async function redeliverSessionWebhook(http, sessionId, deliveryId) {
|
|
821
|
-
await http.request(`/api/sessions/${encodeURIComponent(sessionId)}/webhook-deliveries/${encodeURIComponent(deliveryId)}/redeliver`, { method: "POST" });
|
|
822
|
-
}
|
|
823
|
-
/**
|
|
824
|
-
* Delete a workspace asset cache entry. Accepts an `asset_<id>` value,
|
|
825
|
-
* `sha256:<hex>`, or a bare 64-hex digest. Workspace is derived server-side
|
|
826
|
-
* from the token; idempotent.
|
|
827
|
-
* Does NOT affect sessions that already snapshotted the asset.
|
|
828
|
-
*/
|
|
829
|
-
export async function deleteWorkspaceAsset(http, hash) {
|
|
830
|
-
const assetId = hash.startsWith("asset_")
|
|
831
|
-
? hash
|
|
832
|
-
: `asset_${hash.startsWith("sha256:") ? hash.slice("sha256:".length) : hash}`;
|
|
833
|
-
await http.request(`/api/assets/${encodeURIComponent(assetId)}`, { method: "DELETE" });
|
|
834
|
-
}
|
|
835
|
-
export async function whoami(http) {
|
|
836
|
-
return parseWhoAmI(await http.request("/api/whoami"));
|
|
837
|
-
}
|
|
838
|
-
/**
|
|
839
|
-
* Validate a CONTROL-plane account token (PAT / device session) against the
|
|
840
|
-
* dashboard BFF `GET /api/whoami`. Same endpoint as {@link whoami}, but the
|
|
841
|
-
* bearer is an account PAT (`aexu_...`) so the server answers with an
|
|
842
|
-
* `account_token` principal (no workspace, no data-plane limits) — a shape the
|
|
843
|
-
* workspace-key {@link whoami} parser rejects. Used by
|
|
844
|
-
* `aex login --api-key <account PAT>` to prove the PAT before persisting it.
|
|
845
|
-
*/
|
|
846
|
-
export async function accountWhoami(http) {
|
|
847
|
-
return parseAccountWhoAmI(await http.request("/api/whoami"));
|
|
848
|
-
}
|
|
849
|
-
function parseAccountWhoAmI(value) {
|
|
850
|
-
if (!isRecord(value)) {
|
|
851
|
-
throw new SessionStateError("account whoami response must be an object");
|
|
852
|
-
}
|
|
853
|
-
if (value.ok !== true || value.principalType !== "account_token") {
|
|
854
|
-
throw new SessionStateError("account whoami response must identify an account_token principal");
|
|
855
|
-
}
|
|
856
|
-
if (typeof value.appUserId !== "string" || value.appUserId.length === 0) {
|
|
857
|
-
throw new SessionStateError("account whoami response appUserId must be a non-empty string");
|
|
858
|
-
}
|
|
859
|
-
if (!Array.isArray(value.scopes) || !value.scopes.every((scope) => typeof scope === "string")) {
|
|
860
|
-
throw new SessionStateError("account whoami response scopes must be an array of strings");
|
|
861
|
-
}
|
|
862
|
-
return {
|
|
863
|
-
ok: true,
|
|
864
|
-
principalType: "account_token",
|
|
865
|
-
appUserId: value.appUserId,
|
|
866
|
-
scopes: value.scopes,
|
|
867
|
-
...(typeof value.orgId === "string" ? { orgId: value.orgId } : {}),
|
|
868
|
-
...(typeof value.tokenId === "string" ? { tokenId: value.tokenId } : {}),
|
|
869
|
-
...(typeof value.tokenName === "string" ? { tokenName: value.tokenName } : {}),
|
|
870
|
-
...(typeof value.tokenKind === "string" ? { tokenKind: value.tokenKind } : {})
|
|
871
|
-
};
|
|
872
|
-
}
|
|
873
|
-
function parseWhoAmI(value) {
|
|
874
|
-
if (!isRecord(value)) {
|
|
875
|
-
throw new SessionStateError("whoami response must be an object");
|
|
876
|
-
}
|
|
877
|
-
const removed = ["caps", "tokenId", "tokenName"].find((field) => Object.hasOwn(value, field));
|
|
878
|
-
if (removed !== undefined) {
|
|
879
|
-
throw new SessionStateError(`whoami response contains the removed ${removed} field`);
|
|
880
|
-
}
|
|
881
|
-
if (value.ok !== true || value.principalType !== "api_key") {
|
|
882
|
-
throw new SessionStateError("whoami response must identify an api_key principal");
|
|
883
|
-
}
|
|
884
|
-
if (typeof value.workspaceId !== "string" || value.workspaceId.length === 0) {
|
|
885
|
-
throw new SessionStateError("whoami response workspaceId must be a non-empty string");
|
|
886
|
-
}
|
|
887
|
-
if (!Array.isArray(value.scopes) || !value.scopes.every((scope) => typeof scope === "string" && scope.length > 0)) {
|
|
888
|
-
throw new SessionStateError("whoami response scopes must be an array of non-empty strings");
|
|
889
|
-
}
|
|
890
|
-
const limits = parseWhoAmILimits(value.limits);
|
|
891
|
-
const runtimeCapabilities = value.runtimeCapabilities === undefined
|
|
892
|
-
? undefined
|
|
893
|
-
: parseRuntimeCapabilities(value.runtimeCapabilities);
|
|
894
|
-
return {
|
|
895
|
-
ok: true,
|
|
896
|
-
principalType: "api_key",
|
|
897
|
-
workspaceId: value.workspaceId,
|
|
898
|
-
scopes: value.scopes,
|
|
899
|
-
limits,
|
|
900
|
-
...(runtimeCapabilities ? { runtimeCapabilities } : {})
|
|
901
|
-
};
|
|
902
|
-
}
|
|
903
|
-
const RUNTIME_KIND_SET = new Set(RUNTIME_KINDS);
|
|
904
|
-
const RUNTIME_SIZE_SET = new Set(RUNTIME_SIZES);
|
|
905
|
-
function parseRuntimeCapabilities(value) {
|
|
906
|
-
const field = "whoami response runtimeCapabilities";
|
|
907
|
-
if (!isRecord(value))
|
|
908
|
-
throw new SessionStateError(`${field} must be an object`);
|
|
909
|
-
if (value.schemaVersion !== 1)
|
|
910
|
-
throw new SessionStateError(`${field}.schemaVersion must be 1`);
|
|
911
|
-
if (typeof value.capabilityVersion !== "string" || value.capabilityVersion.length === 0) {
|
|
912
|
-
throw new SessionStateError(`${field}.capabilityVersion must be a non-empty string`);
|
|
913
|
-
}
|
|
914
|
-
if (typeof value.capabilityHash !== "string" || !CANONICAL_SHA256_DIGEST_PATTERN.test(value.capabilityHash)) {
|
|
915
|
-
throw new SessionStateError(`${field}.capabilityHash must be a canonical SHA-256 digest`);
|
|
916
|
-
}
|
|
917
|
-
if (!Array.isArray(value.availableRuntimeKinds)) {
|
|
918
|
-
throw new SessionStateError(`${field}.availableRuntimeKinds must be an array`);
|
|
919
|
-
}
|
|
920
|
-
const availableRuntimeKinds = value.availableRuntimeKinds;
|
|
921
|
-
if (!availableRuntimeKinds.every((kind) => typeof kind === "string" && RUNTIME_KIND_SET.has(kind)) ||
|
|
922
|
-
new Set(availableRuntimeKinds).size !== availableRuntimeKinds.length) {
|
|
923
|
-
throw new SessionStateError(`${field}.availableRuntimeKinds must contain unique supported runtime kinds`);
|
|
924
|
-
}
|
|
925
|
-
if (!isRecord(value.sizesByRuntimeKind)) {
|
|
926
|
-
throw new SessionStateError(`${field}.sizesByRuntimeKind must be an object`);
|
|
927
|
-
}
|
|
928
|
-
if (!isRecord(value.unavailable)) {
|
|
929
|
-
throw new SessionStateError(`${field}.unavailable must be an object`);
|
|
930
|
-
}
|
|
931
|
-
const unknownSizeKind = Object.keys(value.sizesByRuntimeKind).find((kind) => !RUNTIME_KIND_SET.has(kind));
|
|
932
|
-
const unknownUnavailableKind = Object.keys(value.unavailable).find((kind) => !RUNTIME_KIND_SET.has(kind));
|
|
933
|
-
if (unknownSizeKind || unknownUnavailableKind) {
|
|
934
|
-
throw new SessionStateError(`${field} contains an unknown runtime kind`);
|
|
935
|
-
}
|
|
936
|
-
const available = new Set(availableRuntimeKinds);
|
|
937
|
-
const sizesByRuntimeKind = {};
|
|
938
|
-
const unavailable = {};
|
|
939
|
-
for (const runtimeKind of RUNTIME_KINDS) {
|
|
940
|
-
const sizes = value.sizesByRuntimeKind[runtimeKind];
|
|
941
|
-
const reason = value.unavailable[runtimeKind];
|
|
942
|
-
if (available.has(runtimeKind)) {
|
|
943
|
-
const parsedSizes = Array.isArray(sizes)
|
|
944
|
-
? sizes.map((size) => {
|
|
945
|
-
try {
|
|
946
|
-
return parseRuntimeSizeForRead(size);
|
|
947
|
-
}
|
|
948
|
-
catch {
|
|
949
|
-
return undefined;
|
|
950
|
-
}
|
|
951
|
-
})
|
|
952
|
-
: undefined;
|
|
953
|
-
if (!Array.isArray(parsedSizes) || parsedSizes.length === 0 ||
|
|
954
|
-
!parsedSizes.every((size) => typeof size === "string" && RUNTIME_SIZE_SET.has(size)) ||
|
|
955
|
-
new Set(parsedSizes).size !== parsedSizes.length || reason !== undefined) {
|
|
956
|
-
throw new SessionStateError(`${field}.${runtimeKind} must have unique supported sizes and no unavailable reason`);
|
|
957
|
-
}
|
|
958
|
-
sizesByRuntimeKind[runtimeKind] = parsedSizes;
|
|
959
|
-
}
|
|
960
|
-
else {
|
|
961
|
-
if (sizes !== undefined || !isRecord(reason) || typeof reason.code !== "string" || reason.code.length === 0) {
|
|
962
|
-
throw new SessionStateError(`${field}.${runtimeKind} must have one unavailable reason and no sizes`);
|
|
963
|
-
}
|
|
964
|
-
unavailable[runtimeKind] = { code: reason.code };
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
return {
|
|
968
|
-
schemaVersion: 1,
|
|
969
|
-
capabilityVersion: value.capabilityVersion,
|
|
970
|
-
capabilityHash: value.capabilityHash,
|
|
971
|
-
availableRuntimeKinds,
|
|
972
|
-
sizesByRuntimeKind,
|
|
973
|
-
unavailable,
|
|
974
|
-
profilesByRuntimeKind: parseRuntimeProfiles(value.profilesByRuntimeKind)
|
|
975
|
-
};
|
|
976
|
-
}
|
|
977
|
-
const RUNTIME_CAPABILITY_STATES = new Set(["supported", "unsupported"]);
|
|
978
|
-
const TOOL_EXECUTION_DELIVERY = new Set(["at-least-once", "exactly-once"]);
|
|
979
|
-
const COLD_START_CLASSES = new Set(["warm", "cold-seconds", "cold-tens-of-seconds"]);
|
|
980
|
-
const IDLE_BILLING_CLASSES = new Set(["wall-clock", "zero"]);
|
|
981
|
-
const COMPUTE_BASES = new Set(["wall_clock", "microvm_running"]);
|
|
982
|
-
/**
|
|
983
|
-
* The profile set is TOTAL over runtime kinds and is not optional: a response that
|
|
984
|
-
* declares which runtimes exist but not what they do is exactly the gap the public
|
|
985
|
-
* parity claim used to paper over. A missing or partial set is a contract violation.
|
|
986
|
-
*/
|
|
987
|
-
function parseRuntimeProfiles(value) {
|
|
988
|
-
const field = "whoami response runtimeCapabilities.profilesByRuntimeKind";
|
|
989
|
-
if (!isRecord(value))
|
|
990
|
-
throw new SessionStateError(`${field} must be an object`);
|
|
991
|
-
const unknownKind = Object.keys(value).find((kind) => !RUNTIME_KIND_SET.has(kind));
|
|
992
|
-
if (unknownKind !== undefined)
|
|
993
|
-
throw new SessionStateError(`${field} contains an unknown runtime kind`);
|
|
994
|
-
const profiles = {};
|
|
995
|
-
for (const runtimeKind of RUNTIME_KINDS) {
|
|
996
|
-
profiles[runtimeKind] = parseRuntimeProfile(value[runtimeKind], `${field}.${runtimeKind}`, runtimeKind);
|
|
997
|
-
}
|
|
998
|
-
return profiles;
|
|
999
|
-
}
|
|
1000
|
-
function parseRuntimeProfile(value, field, runtimeKind) {
|
|
1001
|
-
if (!isRecord(value))
|
|
1002
|
-
throw new SessionStateError(`${field} must be an object`);
|
|
1003
|
-
if (value.schemaVersion !== 1)
|
|
1004
|
-
throw new SessionStateError(`${field}.schemaVersion must be 1`);
|
|
1005
|
-
if (value.runtimeKind !== runtimeKind)
|
|
1006
|
-
throw new SessionStateError(`${field}.runtimeKind must be ${runtimeKind}`);
|
|
1007
|
-
if (!isRecord(value.capabilities))
|
|
1008
|
-
throw new SessionStateError(`${field}.capabilities must be an object`);
|
|
1009
|
-
const capabilities = {};
|
|
1010
|
-
for (const capability of RUNTIME_CAPABILITY_NAMES) {
|
|
1011
|
-
const state = value.capabilities[capability];
|
|
1012
|
-
if (typeof state !== "string" || !RUNTIME_CAPABILITY_STATES.has(state)) {
|
|
1013
|
-
throw new SessionStateError(`${field}.capabilities.${capability} must be supported or unsupported`);
|
|
1014
|
-
}
|
|
1015
|
-
capabilities[capability] = state;
|
|
1016
|
-
}
|
|
1017
|
-
if (!isRecord(value.limits))
|
|
1018
|
-
throw new SessionStateError(`${field}.limits must be an object`);
|
|
1019
|
-
const limits = value.limits;
|
|
1020
|
-
for (const limit of ["maxSessionMs", "maxSingleEffectMs", "maxWorkspaceBytes", "maxConcurrentToolCalls"]) {
|
|
1021
|
-
const observed = limits[limit];
|
|
1022
|
-
if (typeof observed !== "number" || !Number.isFinite(observed) || observed <= 0) {
|
|
1023
|
-
throw new SessionStateError(`${field}.limits.${limit} must be a positive number`);
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
if (!isRecord(value.delivery))
|
|
1027
|
-
throw new SessionStateError(`${field}.delivery must be an object`);
|
|
1028
|
-
const delivery = value.delivery;
|
|
1029
|
-
for (const [key, allowed] of [
|
|
1030
|
-
["toolExecution", TOOL_EXECUTION_DELIVERY],
|
|
1031
|
-
["coldStartClass", COLD_START_CLASSES],
|
|
1032
|
-
["idleBilling", IDLE_BILLING_CLASSES]
|
|
1033
|
-
]) {
|
|
1034
|
-
const observed = delivery[key];
|
|
1035
|
-
if (typeof observed !== "string" || !allowed.has(observed)) {
|
|
1036
|
-
throw new SessionStateError(`${field}.delivery.${key} is not a recognized value`);
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
if (typeof value.computeBasis !== "string" || !COMPUTE_BASES.has(value.computeBasis)) {
|
|
1040
|
-
throw new SessionStateError(`${field}.computeBasis is not a recognized value`);
|
|
1041
|
-
}
|
|
1042
|
-
return {
|
|
1043
|
-
schemaVersion: 1,
|
|
1044
|
-
runtimeKind,
|
|
1045
|
-
capabilities,
|
|
1046
|
-
limits: {
|
|
1047
|
-
maxSessionMs: limits.maxSessionMs,
|
|
1048
|
-
maxSingleEffectMs: limits.maxSingleEffectMs,
|
|
1049
|
-
maxWorkspaceBytes: limits.maxWorkspaceBytes,
|
|
1050
|
-
maxConcurrentToolCalls: limits.maxConcurrentToolCalls
|
|
1051
|
-
},
|
|
1052
|
-
delivery: {
|
|
1053
|
-
toolExecution: delivery.toolExecution,
|
|
1054
|
-
coldStartClass: delivery.coldStartClass,
|
|
1055
|
-
idleBilling: delivery.idleBilling
|
|
1056
|
-
},
|
|
1057
|
-
computeBasis: value.computeBasis
|
|
1058
|
-
};
|
|
1059
|
-
}
|
|
1060
|
-
function parseWhoAmILimits(value) {
|
|
1061
|
-
if (!isRecord(value)) {
|
|
1062
|
-
throw new SessionStateError("whoami response limits must be an object");
|
|
1063
|
-
}
|
|
1064
|
-
const requiredNumbers = [
|
|
1065
|
-
"maxConcurrentSessions",
|
|
1066
|
-
"submitRatePerMinute",
|
|
1067
|
-
"spendCapUsd",
|
|
1068
|
-
"monthSpendUsd",
|
|
1069
|
-
"balanceUsd",
|
|
1070
|
-
"balanceGraceFloorUsd",
|
|
1071
|
-
"llmTokenAllowanceRemainingUsd"
|
|
1072
|
-
];
|
|
1073
|
-
for (const field of requiredNumbers) {
|
|
1074
|
-
if (typeof value[field] !== "number" || !Number.isFinite(value[field])) {
|
|
1075
|
-
throw new SessionStateError(`whoami response limits.${field} must be a finite number`);
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
for (const field of ["creditGateActive", "autoTopupEnabled"]) {
|
|
1079
|
-
if (typeof value[field] !== "boolean") {
|
|
1080
|
-
throw new SessionStateError(`whoami response limits.${field} must be a boolean`);
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
const paymentMethodStatus = value.paymentMethodStatus;
|
|
1084
|
-
const admissionState = value.admissionState;
|
|
1085
|
-
const accountType = value.accountType;
|
|
1086
|
-
assertOneOf(paymentMethodStatus, ["none", "active"], "limits.paymentMethodStatus");
|
|
1087
|
-
assertOneOf(admissionState, BILLING_ADMISSION_STATES, "limits.admissionState");
|
|
1088
|
-
assertOneOf(accountType, ["standard", "internal"], "limits.accountType");
|
|
1089
|
-
return {
|
|
1090
|
-
maxConcurrentSessions: value.maxConcurrentSessions,
|
|
1091
|
-
submitRatePerMinute: value.submitRatePerMinute,
|
|
1092
|
-
spendCapUsd: value.spendCapUsd,
|
|
1093
|
-
monthSpendUsd: value.monthSpendUsd,
|
|
1094
|
-
balanceUsd: value.balanceUsd,
|
|
1095
|
-
balanceGraceFloorUsd: value.balanceGraceFloorUsd,
|
|
1096
|
-
llmTokenAllowanceRemainingUsd: value.llmTokenAllowanceRemainingUsd,
|
|
1097
|
-
creditGateActive: value.creditGateActive,
|
|
1098
|
-
paymentMethodStatus,
|
|
1099
|
-
admissionState,
|
|
1100
|
-
autoTopupEnabled: value.autoTopupEnabled,
|
|
1101
|
-
accountType
|
|
1102
|
-
};
|
|
1103
|
-
}
|
|
1104
|
-
function assertOneOf(value, allowed, field) {
|
|
1105
|
-
if (!isStringLiteral(value, allowed)) {
|
|
1106
|
-
throw new SessionStateError(`whoami response ${field} is invalid`);
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
/**
|
|
1110
|
-
* Download each artifact's bytes into a zip-file map keyed by
|
|
1111
|
-
* `<zipPrefix><relative-path>`, fetched from the `files`
|
|
1112
|
-
* download route. Best-effort: a per-artifact fetch failure records an
|
|
1113
|
-
* `errors[]` entry rather than aborting the rest, so the failure is
|
|
1114
|
-
* surfaced (never silent) while a partially-available run still yields a
|
|
1115
|
-
* usable zip. A committed size or digest contradiction is terminal because
|
|
1116
|
-
* returning a partial archive would misrepresent corrupted bytes as absent.
|
|
1117
|
-
*/
|
|
1118
|
-
async function collectArtifactBytes(http, sessionId, items, zipPrefix, namespace, timeoutMs = SESSION_FILE_TRANSFER_DEFAULT_TIMEOUT_MS) {
|
|
1119
|
-
const entries = [];
|
|
1120
|
-
const captured = [];
|
|
1121
|
-
const errors = [];
|
|
1122
|
-
for (const item of items) {
|
|
1123
|
-
const rel = item.filename ?? item.id;
|
|
1124
|
-
try {
|
|
1125
|
-
const path = sessionFileRoute(sessionId, item.id, "download", item.checkpointId);
|
|
1126
|
-
entries.push({
|
|
1127
|
-
path: `${zipPrefix}${rel}`,
|
|
1128
|
-
bytes: await downloadSessionFileBytesWithRetry(http, path, timeoutMs, item),
|
|
1129
|
-
...(item.contentType !== undefined ? { contentType: item.contentType } : {}),
|
|
1130
|
-
customerContent: true
|
|
1131
|
-
});
|
|
1132
|
-
captured.push({
|
|
1133
|
-
id: item.id,
|
|
1134
|
-
filename: item.filename ?? null,
|
|
1135
|
-
...(item.sizeBytes !== undefined ? { sizeBytes: item.sizeBytes } : {}),
|
|
1136
|
-
...(item.contentType !== undefined ? { contentType: item.contentType } : {})
|
|
1137
|
-
});
|
|
1138
|
-
}
|
|
1139
|
-
catch (err) {
|
|
1140
|
-
if (err instanceof SessionFileIntegrityError)
|
|
1141
|
-
throw err;
|
|
1142
|
-
errors.push({ namespace, id: item.id, filename: item.filename ?? null, message: err.message });
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
return { entries: Object.freeze(entries), captured, errors };
|
|
1146
|
-
}
|
|
1147
|
-
export function normalizeSessionFileLinkExpiresIn(input = "1h") {
|
|
1148
|
-
if (typeof input === "number") {
|
|
1149
|
-
if (!Number.isFinite(input) || input <= 0) {
|
|
1150
|
-
throw new SessionStateError("sessionFileLink: expiresIn must be a positive number of seconds", {
|
|
1151
|
-
expiresIn: input
|
|
1152
|
-
});
|
|
1153
|
-
}
|
|
1154
|
-
return Math.floor(input);
|
|
1155
|
-
}
|
|
1156
|
-
if (input === "15m")
|
|
1157
|
-
return 15 * 60;
|
|
1158
|
-
if (input === "1h")
|
|
1159
|
-
return 60 * 60;
|
|
1160
|
-
if (input === "1d")
|
|
1161
|
-
return 24 * 60 * 60;
|
|
1162
|
-
throw new SessionStateError("sessionFileLink: expiresIn must be seconds, \"15m\", \"1h\", or \"1d\"", {
|
|
1163
|
-
expiresIn: input
|
|
1164
|
-
});
|
|
1165
|
-
}
|
|
1166
|
-
async function resolveSessionFileLinkTarget(http, sessionId, selectorOrQuery, checkpointId) {
|
|
1167
|
-
if (hasSessionFileIdProperty(selectorOrQuery)) {
|
|
1168
|
-
if (typeof selectorOrQuery.id !== "string" || selectorOrQuery.id.length === 0) {
|
|
1169
|
-
throw new SessionStateError("sessionFileLink: selector must include a file id or query", { sessionId });
|
|
1170
|
-
}
|
|
1171
|
-
return resolveAuthoritativeSessionFile(http, sessionId, selectorOrQuery, checkpointId);
|
|
1172
|
-
}
|
|
1173
|
-
if (isPathSelector(selectorOrQuery) && selectorOrQuery.match === "suffix") {
|
|
1174
|
-
const snapshot = await listSessionFiles(http, sessionId, checkpointId ? { checkpointId } : undefined);
|
|
1175
|
-
return resolveSessionFileSelector(snapshot.files, selectorOrQuery, sessionId);
|
|
1176
|
-
}
|
|
1177
|
-
const match = await findSessionFile(http, sessionId, {
|
|
1178
|
-
...selectorOrQuery,
|
|
1179
|
-
...(checkpointId ? { checkpointId } : {})
|
|
1180
|
-
});
|
|
1181
|
-
if (match)
|
|
1182
|
-
return match;
|
|
1183
|
-
throw new SessionStateError("sessionFileLink: file query matched no files", { sessionId });
|
|
1184
|
-
}
|
|
1185
|
-
function hasSessionFileIdProperty(value) {
|
|
1186
|
-
return Boolean(value &&
|
|
1187
|
-
typeof value === "object" &&
|
|
1188
|
-
"id" in value);
|
|
1189
|
-
}
|
|
1190
|
-
/**
|
|
1191
|
-
* Download EVERYTHING public about a session as one zip, organised into the three
|
|
1192
|
-
* namespace folders:
|
|
1193
|
-
*
|
|
1194
|
-
* metadata/session.json — the session record.
|
|
1195
|
-
* events/events.jsonl — typed event-channel records.
|
|
1196
|
-
* files/<rel> — the session's captured files.
|
|
1197
|
-
* manifest.json — `SessionRecordManifestV1`.
|
|
1198
|
-
*/
|
|
1199
|
-
export async function download(http, sessionId) {
|
|
1200
|
-
const [session, events, snapshot] = await Promise.all([
|
|
1201
|
-
getSession(http, sessionId),
|
|
1202
|
-
listSessionEvents(http, sessionId),
|
|
1203
|
-
listSessionFiles(http, sessionId)
|
|
1204
|
-
]);
|
|
1205
|
-
const collectedFiles = await collectArtifactBytes(http, sessionId, snapshot.files, "files/", "files");
|
|
1206
|
-
return buildSessionArchive(sessionId, session, events, collectedFiles);
|
|
1207
|
-
}
|
|
1208
|
-
/**
|
|
1209
|
-
* Download only the session's captured files (the `files` namespace). Zip
|
|
1210
|
-
* layout: `<rel>` per file plus a `manifest.json`
|
|
1211
|
-
* (`{ sessionId, namespace: "files", files[], errors[] }`).
|
|
1212
|
-
*/
|
|
1213
|
-
export async function downloadSessionFiles(http, sessionId, options) {
|
|
1214
|
-
const timeoutMs = normalizeSessionFileTransferTimeoutMs(options?.timeoutMs);
|
|
1215
|
-
const requestedCheckpointId = options?.checkpointId === undefined
|
|
1216
|
-
? undefined
|
|
1217
|
-
: requireSessionFileCheckpointId(options.checkpointId, "files.download");
|
|
1218
|
-
const snapshot = await listSessionFiles(http, sessionId, requestedCheckpointId === undefined ? undefined : { checkpointId: requestedCheckpointId });
|
|
1219
|
-
const collectedFiles = await collectArtifactBytes(http, sessionId, snapshot.files, "", "files", timeoutMs);
|
|
1220
|
-
return buildSessionFilesArchive(sessionId, collectedFiles);
|
|
1221
|
-
}
|
|
1222
|
-
/**
|
|
1223
|
-
* Download only the event archive (the `events` namespace). Always includes
|
|
1224
|
-
* typed `events.jsonl` plus `manifest.json`.
|
|
1225
|
-
*/
|
|
1226
|
-
export async function downloadEvents(http, sessionId) {
|
|
1227
|
-
const events = await listSessionEvents(http, sessionId);
|
|
1228
|
-
return buildSessionEventsArchive(sessionId, events);
|
|
1229
|
-
}
|
|
1230
|
-
/**
|
|
1231
|
-
* Download only the session record (the `metadata` namespace) as a zip
|
|
1232
|
-
* containing `session.json` plus `manifest.json`.
|
|
1233
|
-
*/
|
|
1234
|
-
export async function downloadMetadata(http, sessionId) {
|
|
1235
|
-
const session = await getSession(http, sessionId);
|
|
1236
|
-
return buildSessionMetadataArchive(sessionId, session);
|
|
1237
|
-
}
|
|
1238
|
-
export async function publishWorkspaceFile(http, input) {
|
|
1239
|
-
return publishWorkspaceResource(http, "files", input);
|
|
1240
|
-
}
|
|
1241
|
-
export async function publishWorkspaceSkill(http, input) {
|
|
1242
|
-
return publishWorkspaceResource(http, "skills", input);
|
|
1243
|
-
}
|
|
1244
|
-
export async function publishWorkspaceTool(http, input) {
|
|
1245
|
-
return publishWorkspaceResource(http, "tools", input);
|
|
1246
|
-
}
|
|
1247
|
-
export async function publishWorkspaceInstruction(http, input) {
|
|
1248
|
-
return publishWorkspaceResource(http, "instructions", input);
|
|
1249
|
-
}
|
|
1250
|
-
export function listWorkspaceFiles(http, query = {}) {
|
|
1251
|
-
return listWorkspaceResources(http, "files", query);
|
|
1252
|
-
}
|
|
1253
|
-
export function listWorkspaceSkills(http, query = {}) {
|
|
1254
|
-
return listWorkspaceResources(http, "skills", query);
|
|
1255
|
-
}
|
|
1256
|
-
export function listWorkspaceTools(http, query = {}) {
|
|
1257
|
-
return listWorkspaceResources(http, "tools", query);
|
|
1258
|
-
}
|
|
1259
|
-
export function listWorkspaceInstructions(http, query = {}) {
|
|
1260
|
-
return listWorkspaceResources(http, "instructions", query);
|
|
1261
|
-
}
|
|
1262
|
-
export function getWorkspaceFile(http, resourceId, version) {
|
|
1263
|
-
return getWorkspaceResource(http, "files", resourceId, version);
|
|
1264
|
-
}
|
|
1265
|
-
export function getWorkspaceSkill(http, resourceId, version) {
|
|
1266
|
-
return getWorkspaceResource(http, "skills", resourceId, version);
|
|
1267
|
-
}
|
|
1268
|
-
export function getWorkspaceTool(http, resourceId, version) {
|
|
1269
|
-
return getWorkspaceResource(http, "tools", resourceId, version);
|
|
1270
|
-
}
|
|
1271
|
-
export function getWorkspaceInstruction(http, resourceId, version) {
|
|
1272
|
-
return getWorkspaceResource(http, "instructions", resourceId, version);
|
|
1273
|
-
}
|
|
1274
|
-
export function deleteWorkspaceFile(http, resourceId) {
|
|
1275
|
-
return deleteWorkspaceResource(http, "files", resourceId);
|
|
1276
|
-
}
|
|
1277
|
-
export function deleteWorkspaceSkill(http, resourceId) {
|
|
1278
|
-
return deleteWorkspaceResource(http, "skills", resourceId);
|
|
1279
|
-
}
|
|
1280
|
-
export function deleteWorkspaceTool(http, resourceId) {
|
|
1281
|
-
return deleteWorkspaceResource(http, "tools", resourceId);
|
|
1282
|
-
}
|
|
1283
|
-
export function deleteWorkspaceInstruction(http, resourceId) {
|
|
1284
|
-
return deleteWorkspaceResource(http, "instructions", resourceId);
|
|
1285
|
-
}
|
|
1286
|
-
async function publishWorkspaceResource(http, kind, input) {
|
|
1287
|
-
const result = await http.request(`/api/workspace/${kind}`, {
|
|
1288
|
-
method: "POST",
|
|
1289
|
-
body: JSON.stringify(input)
|
|
1290
|
-
});
|
|
1291
|
-
return result.resource;
|
|
1292
|
-
}
|
|
1293
|
-
async function listWorkspaceResources(http, kind, query) {
|
|
1294
|
-
if (query.limit !== undefined &&
|
|
1295
|
-
(!Number.isSafeInteger(query.limit) || query.limit < 1 || query.limit > 100)) {
|
|
1296
|
-
throw new Error("workspace resource list limit must be an integer from 1 through 100");
|
|
1297
|
-
}
|
|
1298
|
-
return http.request(`/api/workspace/${kind}`, {}, {
|
|
1299
|
-
...(query.cursor !== undefined ? { cursor: query.cursor } : {}),
|
|
1300
|
-
...(query.limit !== undefined ? { limit: String(query.limit) } : {})
|
|
1301
|
-
});
|
|
1302
|
-
}
|
|
1303
|
-
async function getWorkspaceResource(http, kind, resourceId, version) {
|
|
1304
|
-
const result = await http.request(`/api/workspace/${kind}/${encodeURIComponent(resourceId)}`, {}, version === undefined ? {} : { version: String(version) });
|
|
1305
|
-
return result.resource;
|
|
1306
|
-
}
|
|
1307
|
-
async function deleteWorkspaceResource(http, kind, resourceId) {
|
|
1308
|
-
await http.request(`/api/workspace/${kind}/${encodeURIComponent(resourceId)}`, { method: "DELETE" });
|
|
1309
|
-
}
|
|
1310
|
-
function unwrapSession(result) {
|
|
1311
|
-
if (!isRecord(result) || !isRecord(result.session)) {
|
|
1312
|
-
throw new SessionStateError("session response must contain a session object");
|
|
1313
|
-
}
|
|
1314
|
-
const value = result.session;
|
|
1315
|
-
if (typeof value.id !== "string" || value.id.length === 0) {
|
|
1316
|
-
throw new SessionStateError("session response is missing its canonical id");
|
|
1317
|
-
}
|
|
1318
|
-
assertCanonicalSessionWireFields(value, "session response");
|
|
1319
|
-
if (typeof value.status !== "string" || !SESSION_STATUS_SET.has(value.status)) {
|
|
1320
|
-
throw new SessionStateError("session response has an unknown lifecycle status");
|
|
1321
|
-
}
|
|
1322
|
-
if (typeof value.acceptsMessages !== "boolean") {
|
|
1323
|
-
throw new SessionStateError("session response is missing acceptsMessages");
|
|
1324
|
-
}
|
|
1325
|
-
const currentRun = normalizeOptionalSessionRun(value.currentRun, value.id, "session response currentRun");
|
|
1326
|
-
const lastRun = normalizeOptionalSessionRun(value.lastRun, value.id, "session response lastRun");
|
|
1327
|
-
let providerFault;
|
|
1328
|
-
if (Object.hasOwn(value, "providerFault")) {
|
|
1329
|
-
try {
|
|
1330
|
-
providerFault = parseProviderFault(value.providerFault);
|
|
1331
|
-
}
|
|
1332
|
-
catch (error) {
|
|
1333
|
-
throw new SessionStateError(`session response has an invalid providerFault: ${error instanceof Error ? error.message : "invalid value"}`);
|
|
1334
|
-
}
|
|
1335
|
-
if (lastRun?.outcome !== "failed") {
|
|
1336
|
-
throw new SessionStateError("session response providerFault must belong to a failed lastRun");
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
return {
|
|
1340
|
-
...normalizeSessionRuntime(value, "session response"),
|
|
1341
|
-
...(currentRun !== undefined ? { currentRun } : {}),
|
|
1342
|
-
...(lastRun !== undefined ? { lastRun } : {}),
|
|
1343
|
-
...(providerFault !== undefined ? { providerFault } : {})
|
|
1344
|
-
};
|
|
1345
|
-
}
|
|
1346
|
-
const REMOVED_SESSION_WIRE_FIELDS = ["sessionId", "runtime", "turnSeq", "cleanupStatus"];
|
|
1347
|
-
function assertCanonicalSessionWireFields(value, context) {
|
|
1348
|
-
const removed = REMOVED_SESSION_WIRE_FIELDS.find((field) => Object.hasOwn(value, field));
|
|
1349
|
-
if (removed !== undefined) {
|
|
1350
|
-
throw new SessionStateError(`${context} contains the removed ${removed} field`);
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
function normalizeSessionAccepted(value, context) {
|
|
1354
|
-
if (!isRecord(value) || !isRecord(value.session)) {
|
|
1355
|
-
throw new SessionStateError(`${context} must contain a session object`);
|
|
1356
|
-
}
|
|
1357
|
-
return {
|
|
1358
|
-
...value,
|
|
1359
|
-
session: unwrapSession({ session: value.session })
|
|
1360
|
-
};
|
|
1361
|
-
}
|
|
1362
|
-
const SESSION_RUN_PHASE_SET = new Set(SESSION_RUN_PHASES);
|
|
1363
|
-
const SESSION_RUN_OUTCOME_SET = new Set(SESSION_TERMINAL_OUTCOMES);
|
|
1364
|
-
function normalizeSessionMessageAccepted(value, requestedSessionId) {
|
|
1365
|
-
if (!isRecord(value)) {
|
|
1366
|
-
throw new SessionStateError("session message response must be an object");
|
|
1367
|
-
}
|
|
1368
|
-
if (Object.hasOwn(value, "turn")) {
|
|
1369
|
-
throw new SessionStateError("session message response contains the removed turn field; use run");
|
|
1370
|
-
}
|
|
1371
|
-
if (!isRecord(value.session)) {
|
|
1372
|
-
throw new SessionStateError("session message response must contain a session object");
|
|
1373
|
-
}
|
|
1374
|
-
const session = unwrapSession({ session: value.session });
|
|
1375
|
-
if (session.id !== requestedSessionId) {
|
|
1376
|
-
throw new SessionStateError("session message response session.id does not match the requested session", {
|
|
1377
|
-
requestedSessionId,
|
|
1378
|
-
sessionId: session.id
|
|
1379
|
-
});
|
|
1380
|
-
}
|
|
1381
|
-
if (!isRecord(value.run)) {
|
|
1382
|
-
throw new SessionStateError("session message response must contain a run object");
|
|
1383
|
-
}
|
|
1384
|
-
const run = normalizeSessionRun(value.run, session.id, "session message response run");
|
|
1385
|
-
const eventCursor = value.eventCursor;
|
|
1386
|
-
if (eventCursor !== undefined && (!Number.isSafeInteger(eventCursor) || eventCursor < 0)) {
|
|
1387
|
-
throw new SessionStateError("session message response eventCursor must be a non-negative safe integer");
|
|
1388
|
-
}
|
|
1389
|
-
if (eventCursor !== undefined &&
|
|
1390
|
-
run.eventCursor !== undefined &&
|
|
1391
|
-
eventCursor !== run.eventCursor) {
|
|
1392
|
-
throw new SessionStateError("session message response eventCursor does not match run.eventCursor");
|
|
1393
|
-
}
|
|
1394
|
-
return {
|
|
1395
|
-
...value,
|
|
1396
|
-
session,
|
|
1397
|
-
run,
|
|
1398
|
-
...(typeof eventCursor === "number" ? { eventCursor } : {})
|
|
1399
|
-
};
|
|
1400
|
-
}
|
|
1401
|
-
function normalizeOptionalSessionRun(value, sessionId, context) {
|
|
1402
|
-
if (value === undefined)
|
|
1403
|
-
return undefined;
|
|
1404
|
-
if (!isRecord(value)) {
|
|
1405
|
-
throw new SessionStateError(`${context} must be an object`);
|
|
1406
|
-
}
|
|
1407
|
-
return normalizeSessionRun(value, sessionId, context);
|
|
1408
|
-
}
|
|
1409
|
-
function normalizeSessionRun(value, sessionId, context) {
|
|
1410
|
-
if (Object.hasOwn(value, "executionEndedAt")) {
|
|
1411
|
-
throw new SessionStateError(`${context} contains the removed executionEndedAt field`);
|
|
1412
|
-
}
|
|
1413
|
-
if (typeof value.sessionId !== "string" || value.sessionId.length === 0) {
|
|
1414
|
-
throw new SessionStateError(`${context}.sessionId must be a non-empty string`);
|
|
1415
|
-
}
|
|
1416
|
-
if (value.sessionId !== sessionId) {
|
|
1417
|
-
throw new SessionStateError(`${context}.sessionId does not match session.id`, {
|
|
1418
|
-
sessionId,
|
|
1419
|
-
runSessionId: value.sessionId
|
|
1420
|
-
});
|
|
1421
|
-
}
|
|
1422
|
-
if (typeof value.runId !== "string" || value.runId.length === 0) {
|
|
1423
|
-
throw new SessionStateError(`${context}.runId must be a non-empty string`);
|
|
1424
|
-
}
|
|
1425
|
-
if (!Number.isSafeInteger(value.turnSeq) || value.turnSeq < 1) {
|
|
1426
|
-
throw new SessionStateError(`${context}.turnSeq must be a positive safe integer`);
|
|
1427
|
-
}
|
|
1428
|
-
if (typeof value.phase !== "string" || !SESSION_RUN_PHASE_SET.has(value.phase)) {
|
|
1429
|
-
throw new SessionStateError(`${context}.phase is invalid`);
|
|
1430
|
-
}
|
|
1431
|
-
if (value.outcome !== undefined && (typeof value.outcome !== "string" || !SESSION_RUN_OUTCOME_SET.has(value.outcome))) {
|
|
1432
|
-
throw new SessionStateError(`${context}.outcome is invalid`);
|
|
1433
|
-
}
|
|
1434
|
-
for (const field of ["startedAt", "finishedAt"]) {
|
|
1435
|
-
if (value[field] !== undefined && typeof value[field] !== "string") {
|
|
1436
|
-
throw new SessionStateError(`${context}.${field} must be a string`);
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
if (value.eventCursor !== undefined && (!Number.isSafeInteger(value.eventCursor) || value.eventCursor < 0)) {
|
|
1440
|
-
throw new SessionStateError(`${context}.eventCursor must be a non-negative safe integer`);
|
|
1441
|
-
}
|
|
1442
|
-
if (value.checkpoint !== undefined && !isRecord(value.checkpoint)) {
|
|
1443
|
-
throw new SessionStateError(`${context}.checkpoint must be an object`);
|
|
1444
|
-
}
|
|
1445
|
-
return {
|
|
1446
|
-
sessionId: value.sessionId,
|
|
1447
|
-
turnSeq: value.turnSeq,
|
|
1448
|
-
runId: value.runId,
|
|
1449
|
-
phase: value.phase,
|
|
1450
|
-
...(typeof value.outcome === "string"
|
|
1451
|
-
? { outcome: value.outcome }
|
|
1452
|
-
: {}),
|
|
1453
|
-
...(typeof value.startedAt === "string" ? { startedAt: value.startedAt } : {}),
|
|
1454
|
-
...(typeof value.finishedAt === "string" ? { finishedAt: value.finishedAt } : {}),
|
|
1455
|
-
...(isRecord(value.checkpoint) ? { checkpoint: value.checkpoint } : {}),
|
|
1456
|
-
...(typeof value.eventCursor === "number" ? { eventCursor: value.eventCursor } : {})
|
|
1457
|
-
};
|
|
1458
|
-
}
|
|
1459
|
-
function normalizeSessionRuntime(value, context) {
|
|
1460
|
-
let size;
|
|
1461
|
-
try {
|
|
1462
|
-
size = parseRuntimeSizeForRead(value.runtimeSize);
|
|
1463
|
-
}
|
|
1464
|
-
catch (error) {
|
|
1465
|
-
throw new SessionStateError(`${context} has an invalid runtimeSize: ${error instanceof Error ? error.message : String(error)}`);
|
|
1466
|
-
}
|
|
1467
|
-
// Fold the flat wire fields (`runtimeKind`, `runtimeSize`) into the grouped
|
|
1468
|
-
// client shape `runtime: { kind, size }`. Tolerant on read — an unknown future
|
|
1469
|
-
// runtime kind passes through rather than throwing (forward-compat), unlike the
|
|
1470
|
-
// strict submit path.
|
|
1471
|
-
const kind = typeof value.runtimeKind === "string" ? value.runtimeKind : undefined;
|
|
1472
|
-
const runtime = kind !== undefined || size !== undefined
|
|
1473
|
-
? { ...(kind !== undefined ? { kind } : {}), ...(size !== undefined ? { size } : {}) }
|
|
1474
|
-
: undefined;
|
|
1475
|
-
const { runtimeSize: _wireSize, runtimeKind: _wireKind, ...normalized } = value;
|
|
1476
|
-
return {
|
|
1477
|
-
...normalized,
|
|
1478
|
-
...(runtime !== undefined ? { runtime } : {})
|
|
1479
|
-
};
|
|
1480
|
-
}
|
|
1481
|
-
// Session records can outlive a public runtime-size vocabulary migration. Keep
|
|
1482
|
-
// read-side normalization compatible with records emitted by the previous
|
|
1483
|
-
// pre-launch platform while retaining strict validation for new submissions.
|
|
1484
|
-
const LEGACY_RUNTIME_SIZE_ALIASES = {
|
|
1485
|
-
"shared-0.25x-1gb": "0.25cpu-1gb",
|
|
1486
|
-
"shared-0.5x-4gb": "0.5cpu-4gb",
|
|
1487
|
-
"shared-1x-6gb": "1cpu-6gb",
|
|
1488
|
-
"shared-2x-8gb": "2cpu-8gb",
|
|
1489
|
-
"shared-4x-12gb": "4cpu-12gb"
|
|
1490
|
-
};
|
|
1491
|
-
function parseRuntimeSizeForRead(input) {
|
|
1492
|
-
if (typeof input === "string" && input in LEGACY_RUNTIME_SIZE_ALIASES) {
|
|
1493
|
-
return LEGACY_RUNTIME_SIZE_ALIASES[input];
|
|
1494
|
-
}
|
|
1495
|
-
return parseRuntimeSize(input);
|
|
1496
|
-
}
|