@aexhq/sdk 0.39.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -16
- package/dist/_contracts/api-key.d.ts +49 -0
- package/dist/_contracts/api-key.js +87 -0
- package/dist/_contracts/bundle-manifest.d.ts +86 -0
- package/dist/_contracts/bundle-manifest.js +157 -0
- package/dist/_contracts/error-codes.d.ts +26 -0
- package/dist/_contracts/error-codes.js +79 -0
- package/dist/_contracts/error-factory.d.ts +32 -0
- package/dist/_contracts/error-factory.js +142 -0
- package/dist/_contracts/event-envelope.d.ts +33 -10
- package/dist/_contracts/event-envelope.js +46 -10
- package/dist/_contracts/event-view.d.ts +123 -0
- package/dist/_contracts/event-view.js +120 -0
- package/dist/_contracts/http.js +12 -3
- package/dist/_contracts/index.d.ts +8 -4
- package/dist/_contracts/index.js +11 -7
- package/dist/_contracts/models.d.ts +15 -0
- package/dist/_contracts/models.js +33 -0
- package/dist/_contracts/operations.d.ts +50 -2
- package/dist/_contracts/operations.js +100 -7
- package/dist/_contracts/run-record.d.ts +3 -2
- package/dist/_contracts/runtime-types.d.ts +126 -36
- package/dist/_contracts/runtime-types.js +33 -1
- package/dist/_contracts/sdk-errors.d.ts +61 -2
- package/dist/_contracts/sdk-errors.js +83 -3
- package/dist/_contracts/sdk-secrets.js +31 -6
- package/dist/_contracts/stable.d.ts +14 -0
- package/dist/_contracts/stable.js +14 -0
- package/dist/_contracts/status.d.ts +28 -1
- package/dist/_contracts/status.js +48 -3
- package/dist/_contracts/submission.d.ts +79 -2
- package/dist/_contracts/submission.js +148 -5
- package/dist/_contracts/suggest.d.ts +15 -0
- package/dist/_contracts/suggest.js +54 -0
- package/dist/asset-upload.d.ts +26 -0
- package/dist/asset-upload.js +218 -3
- package/dist/asset-upload.js.map +1 -1
- package/dist/bundle.d.ts +14 -3
- package/dist/bundle.js +34 -14
- package/dist/bundle.js.map +1 -1
- package/dist/canonical-zip.d.ts +68 -0
- package/dist/canonical-zip.js +307 -0
- package/dist/canonical-zip.js.map +1 -0
- package/dist/cli.mjs +1900 -340
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +217 -58
- package/dist/client.js +746 -261
- package/dist/client.js.map +1 -1
- package/dist/file.d.ts +33 -6
- package/dist/file.js +120 -54
- package/dist/file.js.map +1 -1
- package/dist/index.d.ts +17 -8
- package/dist/index.js +26 -9
- package/dist/index.js.map +1 -1
- package/dist/node-fs.d.ts +26 -9
- package/dist/node-fs.js +13 -38
- package/dist/node-fs.js.map +1 -1
- package/dist/node-walk.d.ts +69 -0
- package/dist/node-walk.js +146 -0
- package/dist/node-walk.js.map +1 -0
- package/dist/retry.d.ts +9 -13
- package/dist/retry.js +18 -17
- package/dist/retry.js.map +1 -1
- package/dist/skill.d.ts +16 -4
- package/dist/skill.js +18 -9
- package/dist/skill.js.map +1 -1
- package/dist/tool.d.ts +14 -2
- package/dist/tool.js +33 -7
- package/dist/tool.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/authentication.md +29 -5
- package/docs/billing.md +6 -0
- package/docs/concepts/agent-tools.md +11 -0
- package/docs/defaults.md +1 -0
- package/docs/errors.md +64 -4
- package/docs/events.md +84 -49
- package/docs/limits-and-quotas.md +24 -0
- package/docs/networking.md +7 -1
- package/docs/outputs.md +36 -7
- package/docs/quickstart.md +17 -7
- package/docs/run-config.md +3 -3
- package/docs/secrets.md +14 -0
- package/examples/feature-tour.ts +4 -6
- package/examples/spike-settle-latency.ts +125 -0
- package/package.json +4 -3
- package/dist/_contracts/event-guards.d.ts +0 -67
- package/dist/_contracts/event-guards.js +0 -36
|
@@ -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
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
/**
|
|
198
|
-
*
|
|
199
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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`.
|
|
@@ -565,6 +565,9 @@ export function parseRunSubmissionRequest(input, options = {}) {
|
|
|
565
565
|
trustedReparse: options.trustedReparse === true
|
|
566
566
|
});
|
|
567
567
|
assertRunModelMatchesProvider(provider, submission.model);
|
|
568
|
+
// Fail-closed streaming: `outputMode:'stream'` on a non-streamable provider is
|
|
569
|
+
// a hard reject at parse time (no silent downgrade).
|
|
570
|
+
assertStreamableOutputMode(submission.outputMode, provider);
|
|
568
571
|
crossValidateSecretEnvAndValues(submission.secretEnv, secrets.envSecrets);
|
|
569
572
|
// mcpServers names must agree across the submission half and the
|
|
570
573
|
// secrets half — every secrets.mcpServers[i].name MUST resolve to a
|
|
@@ -648,7 +651,7 @@ export function parseRunLimits(input) {
|
|
|
648
651
|
return undefined;
|
|
649
652
|
}
|
|
650
653
|
const value = requireRecord(input, "limits");
|
|
651
|
-
const allowed = new Set(["maxConcurrentChildRuns", "maxSubagentDepth", "maxSpendUsd"]);
|
|
654
|
+
const allowed = new Set(["maxConcurrentChildRuns", "maxSubagentDepth", "maxSpendUsd", "maxTurns"]);
|
|
652
655
|
for (const key of Object.keys(value)) {
|
|
653
656
|
if (!allowed.has(key)) {
|
|
654
657
|
throw new Error(`limits.${key} is not an allowed field; permitted: ${[...allowed].join(", ")}`);
|
|
@@ -660,16 +663,23 @@ export function parseRunLimits(input) {
|
|
|
660
663
|
// NUMBER, not a positive int. Clamping to the workspace/platform ceiling is the
|
|
661
664
|
// resolver's job; here we only enforce shape + positivity.
|
|
662
665
|
const maxSpendUsd = optionalPositiveNumber(value.maxSpendUsd, "limits.maxSpendUsd");
|
|
666
|
+
// maxTurns is an ITERATION count — a positive safe integer. Clamp to the ceiling
|
|
667
|
+
// is the resolver's job; here we enforce shape + positivity only.
|
|
668
|
+
const maxTurns = optionalPositiveInt(value.maxTurns, "limits.maxTurns");
|
|
663
669
|
// Collapse an all-absent override (e.g. `limits: {}`) to `undefined` so it never
|
|
664
670
|
// lands an empty object on the request — matches sibling parsers (parseRunWebhook,
|
|
665
671
|
// parseEnvironment). The resolver supplies platform defaults for absent fields.
|
|
666
|
-
if (maxConcurrentChildRuns === undefined &&
|
|
672
|
+
if (maxConcurrentChildRuns === undefined &&
|
|
673
|
+
maxSubagentDepth === undefined &&
|
|
674
|
+
maxSpendUsd === undefined &&
|
|
675
|
+
maxTurns === undefined) {
|
|
667
676
|
return undefined;
|
|
668
677
|
}
|
|
669
678
|
return {
|
|
670
679
|
...(maxConcurrentChildRuns !== undefined ? { maxConcurrentChildRuns } : {}),
|
|
671
680
|
...(maxSubagentDepth !== undefined ? { maxSubagentDepth } : {}),
|
|
672
|
-
...(maxSpendUsd !== undefined ? { maxSpendUsd } : {})
|
|
681
|
+
...(maxSpendUsd !== undefined ? { maxSpendUsd } : {}),
|
|
682
|
+
...(maxTurns !== undefined ? { maxTurns } : {})
|
|
673
683
|
};
|
|
674
684
|
}
|
|
675
685
|
/**
|
|
@@ -766,6 +776,8 @@ export function parseSubmission(input, options = {}) {
|
|
|
766
776
|
"outputs",
|
|
767
777
|
"includeBuiltinTools",
|
|
768
778
|
"outputMode",
|
|
779
|
+
"responseFormat",
|
|
780
|
+
"approvalGate",
|
|
769
781
|
"platform"
|
|
770
782
|
]);
|
|
771
783
|
for (const key of Object.keys(value)) {
|
|
@@ -789,6 +801,8 @@ export function parseSubmission(input, options = {}) {
|
|
|
789
801
|
const outputs = parseOutputs(value.outputs);
|
|
790
802
|
const includeBuiltinTools = parseIncludeBuiltinTools(value.includeBuiltinTools);
|
|
791
803
|
const outputMode = parseOutputMode(value.outputMode);
|
|
804
|
+
const responseFormat = parseResponseFormat(value.responseFormat);
|
|
805
|
+
const approvalGate = parseApprovalGate(value.approvalGate);
|
|
792
806
|
const platform = parsePlatformConfig(value.platform);
|
|
793
807
|
return {
|
|
794
808
|
model,
|
|
@@ -808,6 +822,8 @@ export function parseSubmission(input, options = {}) {
|
|
|
808
822
|
...(includeBuiltinTools !== undefined ? { includeBuiltinTools } : {}),
|
|
809
823
|
...(builtinTools.length > 0 ? { builtinTools } : {}),
|
|
810
824
|
...(outputMode !== undefined ? { outputMode } : {}),
|
|
825
|
+
...(responseFormat !== undefined ? { responseFormat } : {}),
|
|
826
|
+
...(approvalGate !== undefined ? { approvalGate } : {}),
|
|
811
827
|
...(platform ? { platform } : {})
|
|
812
828
|
};
|
|
813
829
|
}
|
|
@@ -869,6 +885,127 @@ function parseOutputMode(input) {
|
|
|
869
885
|
}
|
|
870
886
|
return input;
|
|
871
887
|
}
|
|
888
|
+
// ---------------------------------------------------------------------------
|
|
889
|
+
// Streaming capability model — WS9. `outputMode:'stream'` is capability-gated.
|
|
890
|
+
// ---------------------------------------------------------------------------
|
|
891
|
+
/**
|
|
892
|
+
* The provider wire-SHAPES that have a real per-token streaming producer. This
|
|
893
|
+
* const is the contracts-side SSoT, pinned EQUAL to the platform's shape SSoT by
|
|
894
|
+
* a cross-repo parity test so streaming can never be promised for a shape
|
|
895
|
+
* nothing feeds.
|
|
896
|
+
*/
|
|
897
|
+
export const STREAMABLE_SHAPES = ["anthropic", "openai_chat"];
|
|
898
|
+
/**
|
|
899
|
+
* Each provider's wire shape, or `null` when it has no streaming producer wired
|
|
900
|
+
* yet. `stream` output is only honored for a provider whose shape is streamable.
|
|
901
|
+
*/
|
|
902
|
+
const PROVIDER_STREAM_SHAPE = {
|
|
903
|
+
anthropic: "anthropic",
|
|
904
|
+
deepseek: "openai_chat",
|
|
905
|
+
openai: "openai_chat",
|
|
906
|
+
gemini: null,
|
|
907
|
+
mistral: "openai_chat",
|
|
908
|
+
openrouter: "openai_chat",
|
|
909
|
+
doubao: "openai_chat",
|
|
910
|
+
"doubao-cn": "openai_chat"
|
|
911
|
+
};
|
|
912
|
+
/** True when a provider has a streaming producer wired (a {@link STREAMABLE_SHAPES} shape). */
|
|
913
|
+
export function isStreamableProvider(provider) {
|
|
914
|
+
return PROVIDER_STREAM_SHAPE[provider] !== null;
|
|
915
|
+
}
|
|
916
|
+
function streamableProviders() {
|
|
917
|
+
return Object.keys(PROVIDER_STREAM_SHAPE).filter(isStreamableProvider);
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Fail-closed streaming gate: `outputMode:'stream'` on a NON-streamable provider
|
|
921
|
+
* throws (a HARD reject — no silent downgrade to buffered). Called by
|
|
922
|
+
* {@link parseRunSubmissionRequest} once mode + provider are both known.
|
|
923
|
+
*/
|
|
924
|
+
export function assertStreamableOutputMode(outputMode, provider) {
|
|
925
|
+
if (outputMode === "stream" && !isStreamableProvider(provider)) {
|
|
926
|
+
throw new Error(`submission.outputMode 'stream' is not supported for provider ${provider}; ` +
|
|
927
|
+
`streaming is available for: ${streamableProviders().join(", ")}`);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
// ---------------------------------------------------------------------------
|
|
931
|
+
// Structured-output (schema-decode) policy — WS10.
|
|
932
|
+
// ---------------------------------------------------------------------------
|
|
933
|
+
/** Response-format kinds: free-form `text` (default) or provider-native `json_schema`. */
|
|
934
|
+
export const RESPONSE_FORMAT_KINDS = ["text", "json_schema"];
|
|
935
|
+
/**
|
|
936
|
+
* Parse the optional `submission.responseFormat`. Mirrors {@link parseOutputMode}
|
|
937
|
+
* / {@link OUTPUT_MODES}: absent ⇒ undefined; a bad `kind` or unknown subfield is
|
|
938
|
+
* rejected (fail-fast). `json_schema` requires a JSON-object `schema`.
|
|
939
|
+
*/
|
|
940
|
+
export function parseResponseFormat(input) {
|
|
941
|
+
if (input === undefined || input === null)
|
|
942
|
+
return undefined;
|
|
943
|
+
const value = requireRecord(input, "submission.responseFormat");
|
|
944
|
+
const kind = value.kind;
|
|
945
|
+
if (typeof kind !== "string" || !RESPONSE_FORMAT_KINDS.includes(kind)) {
|
|
946
|
+
throw new Error(`submission.responseFormat.kind must be one of ${RESPONSE_FORMAT_KINDS.join(", ")}`);
|
|
947
|
+
}
|
|
948
|
+
if (kind === "text") {
|
|
949
|
+
for (const key of Object.keys(value)) {
|
|
950
|
+
if (key !== "kind") {
|
|
951
|
+
throw new Error(`submission.responseFormat.${key} is not allowed when kind is 'text'`);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
return { kind: "text" };
|
|
955
|
+
}
|
|
956
|
+
const allowed = new Set(["kind", "schema", "strict", "name"]);
|
|
957
|
+
for (const key of Object.keys(value)) {
|
|
958
|
+
if (!allowed.has(key)) {
|
|
959
|
+
throw new Error(`submission.responseFormat.${key} is not an allowed field; permitted: ${[...allowed].join(", ")}`);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
if (!isRecord(value.schema) || !isJsonValue(value.schema)) {
|
|
963
|
+
throw new Error("submission.responseFormat.schema must be a JSON-serializable object");
|
|
964
|
+
}
|
|
965
|
+
const schema = value.schema;
|
|
966
|
+
if (value.strict !== undefined && typeof value.strict !== "boolean") {
|
|
967
|
+
throw new Error("submission.responseFormat.strict must be a boolean");
|
|
968
|
+
}
|
|
969
|
+
const name = optionalString(value.name, "submission.responseFormat.name");
|
|
970
|
+
return {
|
|
971
|
+
kind: "json_schema",
|
|
972
|
+
schema,
|
|
973
|
+
...(value.strict !== undefined ? { strict: value.strict } : {}),
|
|
974
|
+
...(name !== undefined ? { name } : {})
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Parse the optional `submission.approvalGate`. Absent / empty tool list ⇒
|
|
979
|
+
* undefined (no gate). Tool names are deduped; the strict allow-list mirrors the
|
|
980
|
+
* sibling parsers.
|
|
981
|
+
*/
|
|
982
|
+
export function parseApprovalGate(input) {
|
|
983
|
+
if (input === undefined || input === null)
|
|
984
|
+
return undefined;
|
|
985
|
+
const value = requireRecord(input, "submission.approvalGate");
|
|
986
|
+
for (const key of Object.keys(value)) {
|
|
987
|
+
if (key !== "tools") {
|
|
988
|
+
throw new Error(`submission.approvalGate.${key} is not an allowed field; permitted: tools`);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
if (!Array.isArray(value.tools)) {
|
|
992
|
+
throw new Error("submission.approvalGate.tools must be an array of tool names");
|
|
993
|
+
}
|
|
994
|
+
const seen = new Set();
|
|
995
|
+
const tools = [];
|
|
996
|
+
value.tools.forEach((entry, index) => {
|
|
997
|
+
if (typeof entry !== "string" || entry.length === 0) {
|
|
998
|
+
throw new Error(`submission.approvalGate.tools[${index}] must be a non-empty string`);
|
|
999
|
+
}
|
|
1000
|
+
if (!seen.has(entry)) {
|
|
1001
|
+
seen.add(entry);
|
|
1002
|
+
tools.push(entry);
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
if (tools.length === 0)
|
|
1006
|
+
return undefined;
|
|
1007
|
+
return { tools };
|
|
1008
|
+
}
|
|
872
1009
|
/**
|
|
873
1010
|
* The CLOSED set of builtin tool NAMES the managed runtime can inject — one per
|
|
874
1011
|
* machine tool the hands implement. This list is the single source of truth for
|
|
@@ -898,7 +1035,10 @@ export const BUILTIN_TOOL_NAMES = [
|
|
|
898
1035
|
"bash_kill",
|
|
899
1036
|
"code_execution",
|
|
900
1037
|
"wait",
|
|
901
|
-
"git"
|
|
1038
|
+
"git",
|
|
1039
|
+
"ls",
|
|
1040
|
+
"stat",
|
|
1041
|
+
"wc"
|
|
902
1042
|
];
|
|
903
1043
|
/**
|
|
904
1044
|
* Typo-safe accessors for the closed builtin tool set: each key maps to the
|
|
@@ -927,7 +1067,10 @@ export const BuiltinTools = {
|
|
|
927
1067
|
bash_kill: "bash_kill",
|
|
928
1068
|
code_execution: "code_execution",
|
|
929
1069
|
wait: "wait",
|
|
930
|
-
git: "git"
|
|
1070
|
+
git: "git",
|
|
1071
|
+
ls: "ls",
|
|
1072
|
+
stat: "stat",
|
|
1073
|
+
wc: "wc"
|
|
931
1074
|
};
|
|
932
1075
|
/**
|
|
933
1076
|
* The default builtin tool set injected when `includeBuiltinTools !== false`.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared "did you mean?" suggester — the SSoT for near-miss enum hints across
|
|
3
|
+
* the SDK and the CLI. Previously this lived only in the CLI, so the SDK could
|
|
4
|
+
* not emit a fuzzy model/provider hint even though the docs promised one.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* The closest candidate to `input` by a unique case-insensitive prefix match,
|
|
8
|
+
* else by Levenshtein distance (≤ 2), else `undefined`. Ties break by
|
|
9
|
+
* declaration order. Used on an invalid `--model` / `--provider` /
|
|
10
|
+
* `--runtime-size` (and the SDK's unknown-model throw) to turn a flat rejection
|
|
11
|
+
* into a fix hint.
|
|
12
|
+
*/
|
|
13
|
+
export declare function suggest(input: string, candidates: readonly string[]): string | undefined;
|
|
14
|
+
/** Levenshtein edit distance between two strings (rolling two-row DP). */
|
|
15
|
+
export declare function levenshtein(a: string, b: string): number;
|