@aexhq/sdk 0.39.0 → 0.40.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +22 -16
  2. package/dist/_contracts/api-key.d.ts +49 -0
  3. package/dist/_contracts/api-key.js +87 -0
  4. package/dist/_contracts/bundle-manifest.d.ts +86 -0
  5. package/dist/_contracts/bundle-manifest.js +157 -0
  6. package/dist/_contracts/error-codes.d.ts +26 -0
  7. package/dist/_contracts/error-codes.js +79 -0
  8. package/dist/_contracts/error-factory.d.ts +32 -0
  9. package/dist/_contracts/error-factory.js +142 -0
  10. package/dist/_contracts/event-envelope.d.ts +33 -10
  11. package/dist/_contracts/event-envelope.js +46 -10
  12. package/dist/_contracts/event-view.d.ts +123 -0
  13. package/dist/_contracts/event-view.js +120 -0
  14. package/dist/_contracts/http.js +12 -3
  15. package/dist/_contracts/index.d.ts +8 -4
  16. package/dist/_contracts/index.js +11 -7
  17. package/dist/_contracts/models.d.ts +15 -0
  18. package/dist/_contracts/models.js +33 -0
  19. package/dist/_contracts/operations.d.ts +53 -2
  20. package/dist/_contracts/operations.js +119 -7
  21. package/dist/_contracts/run-record.d.ts +3 -2
  22. package/dist/_contracts/runtime-types.d.ts +126 -36
  23. package/dist/_contracts/runtime-types.js +33 -1
  24. package/dist/_contracts/sdk-errors.d.ts +61 -2
  25. package/dist/_contracts/sdk-errors.js +83 -3
  26. package/dist/_contracts/sdk-secrets.js +31 -6
  27. package/dist/_contracts/stable.d.ts +14 -0
  28. package/dist/_contracts/stable.js +14 -0
  29. package/dist/_contracts/status.d.ts +28 -1
  30. package/dist/_contracts/status.js +48 -3
  31. package/dist/_contracts/submission.d.ts +79 -2
  32. package/dist/_contracts/submission.js +148 -5
  33. package/dist/_contracts/suggest.d.ts +15 -0
  34. package/dist/_contracts/suggest.js +54 -0
  35. package/dist/asset-upload.d.ts +26 -0
  36. package/dist/asset-upload.js +218 -3
  37. package/dist/asset-upload.js.map +1 -1
  38. package/dist/bundle.d.ts +14 -3
  39. package/dist/bundle.js +34 -14
  40. package/dist/bundle.js.map +1 -1
  41. package/dist/canonical-zip.d.ts +68 -0
  42. package/dist/canonical-zip.js +307 -0
  43. package/dist/canonical-zip.js.map +1 -0
  44. package/dist/cli.mjs +1923 -338
  45. package/dist/cli.mjs.sha256 +1 -1
  46. package/dist/client.d.ts +217 -58
  47. package/dist/client.js +749 -261
  48. package/dist/client.js.map +1 -1
  49. package/dist/file.d.ts +33 -6
  50. package/dist/file.js +120 -54
  51. package/dist/file.js.map +1 -1
  52. package/dist/index.d.ts +17 -8
  53. package/dist/index.js +26 -9
  54. package/dist/index.js.map +1 -1
  55. package/dist/node-fs.d.ts +26 -9
  56. package/dist/node-fs.js +13 -38
  57. package/dist/node-fs.js.map +1 -1
  58. package/dist/node-walk.d.ts +69 -0
  59. package/dist/node-walk.js +146 -0
  60. package/dist/node-walk.js.map +1 -0
  61. package/dist/retry.d.ts +9 -13
  62. package/dist/retry.js +18 -17
  63. package/dist/retry.js.map +1 -1
  64. package/dist/skill.d.ts +16 -4
  65. package/dist/skill.js +18 -9
  66. package/dist/skill.js.map +1 -1
  67. package/dist/tool.d.ts +14 -2
  68. package/dist/tool.js +33 -7
  69. package/dist/tool.js.map +1 -1
  70. package/dist/version.d.ts +1 -1
  71. package/dist/version.js +1 -1
  72. package/docs/authentication.md +29 -5
  73. package/docs/billing.md +6 -0
  74. package/docs/concepts/agent-tools.md +11 -0
  75. package/docs/defaults.md +1 -0
  76. package/docs/errors.md +64 -4
  77. package/docs/events.md +84 -49
  78. package/docs/limits-and-quotas.md +24 -0
  79. package/docs/networking.md +7 -1
  80. package/docs/outputs.md +36 -7
  81. package/docs/quickstart.md +17 -7
  82. package/docs/run-config.md +3 -3
  83. package/docs/secrets.md +14 -0
  84. package/examples/feature-tour.ts +4 -6
  85. package/examples/spike-settle-latency.ts +125 -0
  86. package/package.json +4 -3
  87. package/dist/_contracts/event-guards.d.ts +0 -67
  88. package/dist/_contracts/event-guards.js +0 -36
@@ -1,3 +1,4 @@
1
+ import type { AexApiErrorCode } from "./error-codes.js";
1
2
  export type AexErrorCode = "RUN_CONFIG_INVALID" | "CREDENTIAL_INVALID" | "PROVIDER_ERROR" | "RUN_STATE_ERROR" | "CLEANUP_ERROR" | "RUNTIME_UNSUPPORTED" | "API_ERROR" | "NETWORK_ERROR";
2
3
  export declare class AexError extends Error {
3
4
  readonly code: AexErrorCode;
@@ -27,13 +28,69 @@ export declare class CleanupError extends AexError {
27
28
  }
28
29
  /**
29
30
  * Thrown by SDK and CLI operations when the dashboard BFF returns a non-2xx
30
- * response. Carries the HTTP status and parsed body for the caller to inspect.
31
+ * response. Carries the HTTP status, the redacted parsed body, the server's
32
+ * STABLE {@link AexApiErrorCode} (when present), and a `requestId` for support.
33
+ * Construct via {@link import("./error-factory.js").apiErrorFromResponse} — the
34
+ * single wire→exception mapping — which dispatches to a typed subclass
35
+ * ({@link AexAuthError} / {@link AexIdempotencyConflictError} /
36
+ * {@link AexNotFoundError} / {@link AexRateLimitError}).
31
37
  */
32
38
  export declare class AexApiError extends AexError {
33
39
  readonly status: number;
34
40
  readonly body: unknown;
35
- constructor(status: number, message: string, body: unknown);
41
+ /** The server's stable error code, when the body carried a known one. */
42
+ readonly apiCode: AexApiErrorCode | undefined;
43
+ /** Request id (body `requestId` or a response header) for support correlation. */
44
+ readonly requestId: string | undefined;
45
+ constructor(status: number, message: string, body: unknown, options?: {
46
+ readonly apiCode?: AexApiErrorCode | undefined;
47
+ readonly requestId?: string | undefined;
48
+ readonly cause?: unknown;
49
+ });
50
+ }
51
+ /** Shared construction shape for the typed {@link AexApiError} subclasses. */
52
+ export interface AexApiErrorInit {
53
+ readonly status: number;
54
+ readonly message: string;
55
+ readonly body: unknown;
56
+ readonly apiCode?: AexApiErrorCode | undefined;
57
+ readonly requestId?: string | undefined;
58
+ readonly cause?: unknown;
59
+ }
60
+ /** 401/403 auth failure (token invalid/revoked/expired, forbidden, insufficient scope). */
61
+ export declare class AexAuthError extends AexApiError {
62
+ /** The scope the endpoint required, when the server named it (insufficient_scope). */
63
+ readonly requiredScope: string | undefined;
64
+ constructor(init: AexApiErrorInit & {
65
+ readonly requiredScope?: string | undefined;
66
+ });
67
+ }
68
+ /** 409 — the idempotency key was reused with a different request body. */
69
+ export declare class AexIdempotencyConflictError extends AexApiError {
70
+ constructor(init: AexApiErrorInit);
71
+ }
72
+ /** 404 — the requested resource was not found. */
73
+ export declare class AexNotFoundError extends AexApiError {
74
+ constructor(init: AexApiErrorInit);
75
+ }
76
+ /** 429 — the workspace hit a rate/concurrency limit; retry after a backoff. */
77
+ export declare class AexRateLimitError extends AexApiError {
78
+ /** Suggested backoff (ms), when the server advertised one (Retry-After). */
79
+ readonly retryAfterMs: number | undefined;
80
+ constructor(init: AexApiErrorInit & {
81
+ readonly retryAfterMs?: number | undefined;
82
+ });
36
83
  }
84
+ /** True for a 401/403 authentication/authorization failure. */
85
+ export declare function isAuthError(err: unknown): err is AexAuthError;
86
+ /** True for a 403 whose cause is a missing scope (`insufficient_scope`). */
87
+ export declare function isInsufficientScope(err: unknown): boolean;
88
+ /** True for a 409 idempotency-key reuse conflict. */
89
+ export declare function isIdempotencyConflict(err: unknown): err is AexIdempotencyConflictError;
90
+ /** True for a 404 not-found error. */
91
+ export declare function isNotFound(err: unknown): err is AexNotFoundError;
92
+ /** True for a 429 rate/concurrency-limit error. */
93
+ export declare function isRateLimited(err: unknown): err is AexRateLimitError;
37
94
  /**
38
95
  * Thrown when a BFF-bound request fails BEFORE any HTTP response exists — DNS
39
96
  * failure, connection refused, TLS error, socket reset. Wraps the raw fetch
@@ -52,6 +109,8 @@ export declare class AexNetworkError extends AexError {
52
109
  readonly causeCode: string | undefined;
53
110
  /** Attempts made when a retry layer exhausted its budget; `1` otherwise. */
54
111
  readonly attempts: number;
112
+ /** Total elapsed time (ms) across all attempts, when the retry layer set it. */
113
+ readonly elapsedMs: number | undefined;
55
114
  constructor(args: {
56
115
  readonly method: string;
57
116
  readonly host: string;
@@ -38,17 +38,94 @@ export class CleanupError extends AexError {
38
38
  }
39
39
  /**
40
40
  * Thrown by SDK and CLI operations when the dashboard BFF returns a non-2xx
41
- * response. Carries the HTTP status and parsed body for the caller to inspect.
41
+ * response. Carries the HTTP status, the redacted parsed body, the server's
42
+ * STABLE {@link AexApiErrorCode} (when present), and a `requestId` for support.
43
+ * Construct via {@link import("./error-factory.js").apiErrorFromResponse} — the
44
+ * single wire→exception mapping — which dispatches to a typed subclass
45
+ * ({@link AexAuthError} / {@link AexIdempotencyConflictError} /
46
+ * {@link AexNotFoundError} / {@link AexRateLimitError}).
42
47
  */
43
48
  export class AexApiError extends AexError {
44
49
  status;
45
50
  body;
46
- constructor(status, message, body) {
47
- super("API_ERROR", message, body);
51
+ /** The server's stable error code, when the body carried a known one. */
52
+ apiCode;
53
+ /** Request id (body `requestId` or a response header) for support correlation. */
54
+ requestId;
55
+ constructor(status, message, body, options) {
56
+ super("API_ERROR", message, body, options?.cause === undefined ? undefined : { cause: options.cause });
48
57
  this.status = status;
49
58
  this.body = redactSecrets(body);
59
+ this.apiCode = options?.apiCode;
60
+ this.requestId = options?.requestId;
50
61
  }
51
62
  }
63
+ /** 401/403 auth failure (token invalid/revoked/expired, forbidden, insufficient scope). */
64
+ export class AexAuthError extends AexApiError {
65
+ /** The scope the endpoint required, when the server named it (insufficient_scope). */
66
+ requiredScope;
67
+ constructor(init) {
68
+ super(init.status, init.message, init.body, {
69
+ apiCode: init.apiCode,
70
+ requestId: init.requestId,
71
+ cause: init.cause
72
+ });
73
+ this.requiredScope = init.requiredScope;
74
+ }
75
+ }
76
+ /** 409 — the idempotency key was reused with a different request body. */
77
+ export class AexIdempotencyConflictError extends AexApiError {
78
+ constructor(init) {
79
+ super(init.status, init.message, init.body, {
80
+ apiCode: init.apiCode,
81
+ requestId: init.requestId,
82
+ cause: init.cause
83
+ });
84
+ }
85
+ }
86
+ /** 404 — the requested resource was not found. */
87
+ export class AexNotFoundError extends AexApiError {
88
+ constructor(init) {
89
+ super(init.status, init.message, init.body, {
90
+ apiCode: init.apiCode,
91
+ requestId: init.requestId,
92
+ cause: init.cause
93
+ });
94
+ }
95
+ }
96
+ /** 429 — the workspace hit a rate/concurrency limit; retry after a backoff. */
97
+ export class AexRateLimitError extends AexApiError {
98
+ /** Suggested backoff (ms), when the server advertised one (Retry-After). */
99
+ retryAfterMs;
100
+ constructor(init) {
101
+ super(init.status, init.message, init.body, {
102
+ apiCode: init.apiCode,
103
+ requestId: init.requestId,
104
+ cause: init.cause
105
+ });
106
+ this.retryAfterMs = init.retryAfterMs;
107
+ }
108
+ }
109
+ /** True for a 401/403 authentication/authorization failure. */
110
+ export function isAuthError(err) {
111
+ return err instanceof AexAuthError;
112
+ }
113
+ /** True for a 403 whose cause is a missing scope (`insufficient_scope`). */
114
+ export function isInsufficientScope(err) {
115
+ return err instanceof AexAuthError && err.apiCode === "insufficient_scope";
116
+ }
117
+ /** True for a 409 idempotency-key reuse conflict. */
118
+ export function isIdempotencyConflict(err) {
119
+ return err instanceof AexIdempotencyConflictError;
120
+ }
121
+ /** True for a 404 not-found error. */
122
+ export function isNotFound(err) {
123
+ return err instanceof AexNotFoundError;
124
+ }
125
+ /** True for a 429 rate/concurrency-limit error. */
126
+ export function isRateLimited(err) {
127
+ return err instanceof AexRateLimitError;
128
+ }
52
129
  /**
53
130
  * Thrown when a BFF-bound request fails BEFORE any HTTP response exists — DNS
54
131
  * failure, connection refused, TLS error, socket reset. Wraps the raw fetch
@@ -67,6 +144,8 @@ export class AexNetworkError extends AexError {
67
144
  causeCode;
68
145
  /** Attempts made when a retry layer exhausted its budget; `1` otherwise. */
69
146
  attempts;
147
+ /** Total elapsed time (ms) across all attempts, when the retry layer set it. */
148
+ elapsedMs;
70
149
  constructor(args) {
71
150
  const causeCode = extractErrorCode(args.cause);
72
151
  super("NETWORK_ERROR", networkErrorMessage(args, causeCode), { method: args.method, host: args.host, path: args.path, ...(causeCode ? { code: causeCode } : {}) }, { cause: args.cause });
@@ -75,6 +154,7 @@ export class AexNetworkError extends AexError {
75
154
  this.path = args.path;
76
155
  this.causeCode = causeCode;
77
156
  this.attempts = args.attempts ?? 1;
157
+ this.elapsedMs = args.elapsedMs;
78
158
  }
79
159
  }
80
160
  function networkErrorMessage(args, causeCode) {
@@ -194,17 +194,42 @@ export function createRedactingStream(known = []) {
194
194
  function isSecretKey(key) {
195
195
  return /(?:api[_-]?key|authorization|token|secret|password|credential)/i.test(key);
196
196
  }
197
- /** A candidate run is a high-entropy secret if it is both information-dense
198
- * AND mixes character classes (the property that separates opaque secret blobs
199
- * from single-class identifiers/paths). */
197
+ /**
198
+ * A candidate run is a high-entropy secret if it contains an opaque
199
+ * secret-shaped SEGMENT. The candidate class already excludes `_` so
200
+ * snake_case names split into sub-candidates; we ADDITIONALLY treat `-` as a
201
+ * segment boundary here, at the entropy layer, so a kebab-case human name or a
202
+ * timestamped slug is judged by its PARTS — dictionary words + a decimal number,
203
+ * each individually low-entropy — not by its (misleadingly high) whole-run
204
+ * entropy. Concretely a customer secret NAME like `spike17-race-1720000000000`
205
+ * (and its real-`Date.now()` variants, whose whole-run entropy is ~4.1) has no
206
+ * opaque segment and SURVIVES — a name is not a credential, and masking it makes
207
+ * `secret_not_found` unactionable.
208
+ *
209
+ * Splitting at the entropy layer (not in the candidate regex) is the precise,
210
+ * non-weakening choice: an OPAQUE base64url secret that merely CONTAINS a `-`
211
+ * still masks, because at least one of its `-`-segments is itself a dense,
212
+ * class-diverse run and trips the gate. A `-`-free candidate is a single
213
+ * segment, i.e. byte-identical to the pre-fix whole-run check.
214
+ */
200
215
  function looksHighEntropySecret(value) {
216
+ return value.split("-").some(isOpaqueSecretRun);
217
+ }
218
+ /**
219
+ * True when a delimiter-free run is a dense, character-class-diverse opaque
220
+ * secret: an entropy floor AND ≥2 character classes, and either a digit or an
221
+ * extreme length. The digit/length clause keeps digit-free mixed-case
222
+ * identifiers (stack-trace frames, API symbol names the diagnostic bundle
223
+ * captures) legible while real secret shapes still match. A segment below ~8
224
+ * chars cannot clear the entropy floor, so dictionary words and short slug parts
225
+ * fall out on entropy alone — no separate length gate needed here (the
226
+ * ≥24-char {@link HIGH_ENTROPY_CANDIDATE} bounds the whole candidate).
227
+ */
228
+ function isOpaqueSecretRun(value) {
201
229
  if (charClassCount(value) < MIN_CHAR_CLASSES)
202
230
  return false;
203
231
  if (shannonEntropyBits(value) < ENTROPY_BITS_PER_CHAR)
204
232
  return false;
205
- // Require a digit OR extreme length so digit-free mixed-case identifiers
206
- // (stack-trace frames, API symbol names) are not destroyed in diagnostic
207
- // output, while real secret shapes (alnum-mixed, or very long) still match.
208
233
  return /[0-9]/.test(value) || value.length >= HIGH_ENTROPY_NO_DIGIT_MIN_LEN;
209
234
  }
210
235
  /** How many of {lowercase, uppercase, digit} appear in `value` (0–3). */
@@ -22,5 +22,19 @@
22
22
  * exact default API plane.
23
23
  */
24
24
  export declare const AEX_DEFAULT_BASE_URL = "https://api.aex.dev";
25
+ /**
26
+ * Plane → API base URL. When the SDK constructor is given no explicit `baseUrl`
27
+ * it DERIVES the target from the API key's embedded plane (see
28
+ * {@link import("./api-key.js").parseApiKey}) rather than blindly defaulting to
29
+ * prod — so a dev key never silently 401s against `api.aex.dev`.
30
+ *
31
+ * `prd` is {@link AEX_DEFAULT_BASE_URL}. `dev` is `null`: the dev plane has no
32
+ * stable public hostname yet, so a dev key still requires an explicit `baseUrl`
33
+ * — but the plane-mismatch guard (dev key + prd baseUrl) still fires.
34
+ */
35
+ export declare const PLANE_BASE_URLS: {
36
+ readonly dev: null;
37
+ readonly prd: "https://api.aex.dev";
38
+ };
25
39
  export declare function stableStringify(value: unknown): string;
26
40
  export declare function sha256(value: unknown): string;
@@ -23,6 +23,20 @@ import { createHash } from "node:crypto";
23
23
  * exact default API plane.
24
24
  */
25
25
  export const AEX_DEFAULT_BASE_URL = "https://api.aex.dev";
26
+ /**
27
+ * Plane → API base URL. When the SDK constructor is given no explicit `baseUrl`
28
+ * it DERIVES the target from the API key's embedded plane (see
29
+ * {@link import("./api-key.js").parseApiKey}) rather than blindly defaulting to
30
+ * prod — so a dev key never silently 401s against `api.aex.dev`.
31
+ *
32
+ * `prd` is {@link AEX_DEFAULT_BASE_URL}. `dev` is `null`: the dev plane has no
33
+ * stable public hostname yet, so a dev key still requires an explicit `baseUrl`
34
+ * — but the plane-mismatch guard (dev key + prd baseUrl) still fires.
35
+ */
36
+ export const PLANE_BASE_URLS = {
37
+ dev: null,
38
+ prd: AEX_DEFAULT_BASE_URL
39
+ };
26
40
  export function stableStringify(value) {
27
41
  return JSON.stringify(sortValue(value));
28
42
  }
@@ -1,7 +1,34 @@
1
1
  export declare const RUN_STATUSES: readonly ["queued", "claiming", "provisioning", "session_created", "dispatched", "provider_running", "provider_idle", "provider_rescheduled", "idle", "suspending", "suspended", "deleting", "deleted", "expired", "cancelling", "capturing_outputs", "cleaning_up", "succeeded", "failed", "timed_out", "cancelled", "cleanup_failed"];
2
2
  export type RunStatus = typeof RUN_STATUSES[number];
3
- export declare const SESSION_STATUSES: readonly ["creating", "running", "idle", "suspending", "suspended", "cancelling", "deleting", "deleted", "expired", "error"];
3
+ /**
4
+ * The RESUMABLE / in-flight lifecycle half of a session's status. A session
5
+ * whose status is one of these is either transitioning or parked resumable
6
+ * (`idle`/`suspended`) — it is NOT a terminal outcome. The terminal outcome
7
+ * half is {@link SESSION_TERMINAL_OUTCOMES}, derived from the run outcome SSoT.
8
+ */
9
+ export declare const SESSION_LIFECYCLE_STATUSES: readonly ["creating", "running", "idle", "suspending", "suspended", "cancelling", "deleting", "deleted", "expired"];
10
+ export type SessionLifecycleStatus = typeof SESSION_LIFECYCLE_STATUSES[number];
11
+ /**
12
+ * The terminal OUTCOME half of a session's status — DERIVED from the run
13
+ * outcome SSoT via `satisfies readonly RunTerminalOutcome[]`, so a new run
14
+ * outcome fails to compile until it is accounted for here (and therefore in
15
+ * {@link SESSION_STATUSES}). This is the compile-time binding that stops the
16
+ * session and run terminal vocabularies from drifting: the bare session
17
+ * `error` is gone — a failed turn is `failed`, a wall-clock kill `timed_out`,
18
+ * a cancel `cancelled`, a clean finish `succeeded`.
19
+ */
20
+ export declare const SESSION_TERMINAL_OUTCOMES: readonly ["succeeded", "failed", "timed_out", "cancelled"];
21
+ export type SessionTerminalOutcome = typeof SESSION_TERMINAL_OUTCOMES[number];
22
+ /**
23
+ * The full closed set of session statuses: the resumable lifecycle half, the
24
+ * terminal-outcome half (bound to {@link RUN_TERMINAL_OUTCOMES}), and the
25
+ * first-class HITL write-gate `awaiting_approval`. Composed — never
26
+ * hand-listed — so the outcome vocabulary can only be extended at the run SSoT.
27
+ */
28
+ export declare const SESSION_STATUSES: readonly ["creating", "running", "idle", "suspending", "suspended", "cancelling", "deleting", "deleted", "expired", "succeeded", "failed", "timed_out", "cancelled", "awaiting_approval"];
4
29
  export type SessionStatus = typeof SESSION_STATUSES[number];
30
+ /** True when a session status is terminal (an outcome, or deleted/expired). */
31
+ export declare function isTerminalSessionStatus(status: SessionStatus): boolean;
5
32
  export type RunStatusKind = "active" | "terminal";
6
33
  export declare const TERMINAL_RUN_STATUSES: readonly ["succeeded", "failed", "timed_out", "cancelled", "deleted", "expired", "cleanup_failed"];
7
34
  export declare function isTerminalRunStatus(status: RunStatus): boolean;
@@ -22,7 +22,13 @@ export const RUN_STATUSES = [
22
22
  "cancelled",
23
23
  "cleanup_failed"
24
24
  ];
25
- export const SESSION_STATUSES = [
25
+ /**
26
+ * The RESUMABLE / in-flight lifecycle half of a session's status. A session
27
+ * whose status is one of these is either transitioning or parked resumable
28
+ * (`idle`/`suspended`) — it is NOT a terminal outcome. The terminal outcome
29
+ * half is {@link SESSION_TERMINAL_OUTCOMES}, derived from the run outcome SSoT.
30
+ */
31
+ export const SESSION_LIFECYCLE_STATUSES = [
26
32
  "creating",
27
33
  "running",
28
34
  "idle",
@@ -31,9 +37,48 @@ export const SESSION_STATUSES = [
31
37
  "cancelling",
32
38
  "deleting",
33
39
  "deleted",
34
- "expired",
35
- "error"
40
+ "expired"
36
41
  ];
42
+ /**
43
+ * The terminal OUTCOME half of a session's status — DERIVED from the run
44
+ * outcome SSoT via `satisfies readonly RunTerminalOutcome[]`, so a new run
45
+ * outcome fails to compile until it is accounted for here (and therefore in
46
+ * {@link SESSION_STATUSES}). This is the compile-time binding that stops the
47
+ * session and run terminal vocabularies from drifting: the bare session
48
+ * `error` is gone — a failed turn is `failed`, a wall-clock kill `timed_out`,
49
+ * a cancel `cancelled`, a clean finish `succeeded`.
50
+ */
51
+ export const SESSION_TERMINAL_OUTCOMES = [
52
+ "succeeded",
53
+ "failed",
54
+ "timed_out",
55
+ "cancelled"
56
+ ];
57
+ /**
58
+ * The full closed set of session statuses: the resumable lifecycle half, the
59
+ * terminal-outcome half (bound to {@link RUN_TERMINAL_OUTCOMES}), and the
60
+ * first-class HITL write-gate `awaiting_approval`. Composed — never
61
+ * hand-listed — so the outcome vocabulary can only be extended at the run SSoT.
62
+ */
63
+ export const SESSION_STATUSES = [
64
+ ...SESSION_LIFECYCLE_STATUSES,
65
+ ...SESSION_TERMINAL_OUTCOMES,
66
+ "awaiting_approval"
67
+ ];
68
+ /**
69
+ * The terminal session statuses: the four outcomes plus the lifecycle-terminal
70
+ * `deleted`/`expired`. `idle`/`suspended` are resumable (NOT terminal) and
71
+ * `awaiting_approval` is a held gate (NOT terminal).
72
+ */
73
+ const SESSION_TERMINAL_STATUSES = new Set([
74
+ ...SESSION_TERMINAL_OUTCOMES,
75
+ "deleted",
76
+ "expired"
77
+ ]);
78
+ /** True when a session status is terminal (an outcome, or deleted/expired). */
79
+ export function isTerminalSessionStatus(status) {
80
+ return SESSION_TERMINAL_STATUSES.has(status);
81
+ }
37
82
  export const TERMINAL_RUN_STATUSES = [
38
83
  "succeeded",
39
84
  "failed",
@@ -275,9 +275,26 @@ export interface PlatformSubmission {
275
275
  /**
276
276
  * Assistant-output granularity. `buffered` (the default) emits one event per
277
277
  * assistant message; `stream` emits the agent's per-token text deltas as they
278
- * arrive. Buffered is quieter and cheaper; stream suits live typing UIs.
278
+ * arrive, THEN a final coalesced block. Streaming is CAPABILITY-GATED: it is
279
+ * only honored for a streamable provider (see {@link STREAMABLE_SHAPES}) and a
280
+ * `stream` mode on a non-streamable provider is rejected at parse
281
+ * ({@link assertStreamableOutputMode}) — no silent downgrade.
279
282
  */
280
283
  readonly outputMode?: OutputMode;
284
+ /**
285
+ * Structured-output policy. `{ kind: 'text' }` (default) is free-form; a
286
+ * `{ kind: 'json_schema', … }` requests provider-native constrained decode.
287
+ * The run's typed outcome is then `decoded | refused` — no untyped path yields
288
+ * a hallucinated object. Fail-closed for a provider lacking the capability.
289
+ */
290
+ readonly responseFormat?: ResponseFormat;
291
+ /**
292
+ * Declarative HITL write-gate: the platform parks the session
293
+ * `awaiting_approval` BEFORE dispatching any listed tool, holding for an
294
+ * `approve()`/`deny()`. Structural — independent of model prose. Empty/absent
295
+ * ⇒ no gate.
296
+ */
297
+ readonly approvalGate?: ApprovalGate;
281
298
  /**
282
299
  * Platform-injection controls. The platform prepends a small system
283
300
  * prompt (see `platformSystemPrompt`) ahead of `system` to explain
@@ -391,6 +408,15 @@ export interface RunLimits {
391
408
  * single source of truth for that wire→boot name mapping.
392
409
  */
393
410
  readonly maxSpendUsd?: number;
411
+ /**
412
+ * Maximum number of agent ITERATIONS (turns) the run may take before the
413
+ * platform parks it terminal. A positive integer; omitted ⇒ the platform
414
+ * default (`RUN_DEFAULT_MAX_TURNS`). Previously a bare server literal absent
415
+ * from both the public contract and the limits SSoT — now a settable dial.
416
+ * Only shape/positivity are validated here; clamping to the ceiling is the
417
+ * resolver's job.
418
+ */
419
+ readonly maxTurns?: number;
394
420
  }
395
421
  /**
396
422
  * Per-run machine/capacity intent. v1 exposes only `spot`: opt the run into
@@ -504,6 +530,54 @@ export declare function parseSubmission(input: unknown, options?: {
504
530
  export declare const OUTPUT_MODES: readonly ["buffered", "stream"];
505
531
  export type OutputMode = (typeof OUTPUT_MODES)[number];
506
532
  export declare const DEFAULT_OUTPUT_MODE: OutputMode;
533
+ /**
534
+ * The provider wire-SHAPES that have a real per-token streaming producer. This
535
+ * const is the contracts-side SSoT, pinned EQUAL to the platform's shape SSoT by
536
+ * a cross-repo parity test so streaming can never be promised for a shape
537
+ * nothing feeds.
538
+ */
539
+ export declare const STREAMABLE_SHAPES: readonly ["anthropic", "openai_chat"];
540
+ export type StreamableShape = (typeof STREAMABLE_SHAPES)[number];
541
+ /** True when a provider has a streaming producer wired (a {@link STREAMABLE_SHAPES} shape). */
542
+ export declare function isStreamableProvider(provider: RunProvider): boolean;
543
+ /**
544
+ * Fail-closed streaming gate: `outputMode:'stream'` on a NON-streamable provider
545
+ * throws (a HARD reject — no silent downgrade to buffered). Called by
546
+ * {@link parseRunSubmissionRequest} once mode + provider are both known.
547
+ */
548
+ export declare function assertStreamableOutputMode(outputMode: OutputMode | undefined, provider: RunProvider): void;
549
+ /** Response-format kinds: free-form `text` (default) or provider-native `json_schema`. */
550
+ export declare const RESPONSE_FORMAT_KINDS: readonly ["text", "json_schema"];
551
+ export type ResponseFormatKind = (typeof RESPONSE_FORMAT_KINDS)[number];
552
+ /**
553
+ * Structured-output policy. `{ kind: 'text' }` is free-form; `{ kind:
554
+ * 'json_schema', schema, strict?, name? }` requests provider-native constrained
555
+ * decode against `schema` (a JSON Schema object).
556
+ */
557
+ export type ResponseFormat = {
558
+ readonly kind: "text";
559
+ } | {
560
+ readonly kind: "json_schema";
561
+ readonly schema: JsonValue;
562
+ readonly strict?: boolean;
563
+ readonly name?: string;
564
+ };
565
+ /**
566
+ * Parse the optional `submission.responseFormat`. Mirrors {@link parseOutputMode}
567
+ * / {@link OUTPUT_MODES}: absent ⇒ undefined; a bad `kind` or unknown subfield is
568
+ * rejected (fail-fast). `json_schema` requires a JSON-object `schema`.
569
+ */
570
+ export declare function parseResponseFormat(input: unknown): ResponseFormat | undefined;
571
+ /** Declarative HITL write-gate: park `awaiting_approval` before any listed tool. */
572
+ export interface ApprovalGate {
573
+ readonly tools: readonly string[];
574
+ }
575
+ /**
576
+ * Parse the optional `submission.approvalGate`. Absent / empty tool list ⇒
577
+ * undefined (no gate). Tool names are deduped; the strict allow-list mirrors the
578
+ * sibling parsers.
579
+ */
580
+ export declare function parseApprovalGate(input: unknown): ApprovalGate | undefined;
507
581
  /**
508
582
  * The CLOSED set of builtin tool NAMES the managed runtime can inject — one per
509
583
  * machine tool the hands implement. This list is the single source of truth for
@@ -515,7 +589,7 @@ export declare const DEFAULT_OUTPUT_MODE: OutputMode;
515
589
  * Order mirrors `HANDS_TOOLS`. A builtin tool reference (a bare string in
516
590
  * `submission.tools`) must be a member of this set.
517
591
  */
518
- export declare const BUILTIN_TOOL_NAMES: readonly ["bash", "read_file", "write_file", "edit_file", "grep", "glob", "head", "tail", "todo_write", "subagent", "subagent_result", "web_fetch", "web_search", "bash_output", "bash_kill", "code_execution", "wait", "git"];
592
+ export declare const BUILTIN_TOOL_NAMES: readonly ["bash", "read_file", "write_file", "edit_file", "grep", "glob", "head", "tail", "todo_write", "subagent", "subagent_result", "web_fetch", "web_search", "bash_output", "bash_kill", "code_execution", "wait", "git", "ls", "stat", "wc"];
519
593
  export type BuiltinToolName = (typeof BUILTIN_TOOL_NAMES)[number];
520
594
  /**
521
595
  * Typo-safe accessors for the closed builtin tool set: each key maps to the
@@ -545,6 +619,9 @@ export declare const BuiltinTools: {
545
619
  readonly code_execution: "code_execution";
546
620
  readonly wait: "wait";
547
621
  readonly git: "git";
622
+ readonly ls: "ls";
623
+ readonly stat: "stat";
624
+ readonly wc: "wc";
548
625
  };
549
626
  /**
550
627
  * The default builtin tool set injected when `includeBuiltinTools !== false`.