@aexhq/sdk 0.37.2 → 0.37.4
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/dist/_contracts/event-stream-client.d.ts +11 -0
- package/dist/_contracts/event-stream-client.js +45 -5
- package/dist/_contracts/http.js +79 -7
- package/dist/_contracts/operations.js +42 -3
- package/dist/_contracts/provider-support.d.ts +2 -2
- package/dist/_contracts/provider-support.js +1 -1
- package/dist/_contracts/run-unit.d.ts +1 -1
- package/dist/_contracts/run-unit.js +12 -9
- package/dist/_contracts/runtime-types.d.ts +20 -8
- package/dist/_contracts/sdk-errors.d.ts +44 -2
- package/dist/_contracts/sdk-errors.js +104 -2
- package/dist/_contracts/sdk-secrets.js +18 -1
- package/dist/asset-upload.js +85 -17
- package/dist/asset-upload.js.map +1 -1
- package/dist/cli.mjs +275 -35
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +17 -1
- package/dist/client.js +145 -15
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/retry.js +66 -6
- package/dist/retry.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/authentication.md +9 -0
- package/docs/concepts/composition.md +1 -1
- package/docs/concepts/subagents.md +6 -3
- package/docs/defaults.md +2 -2
- package/docs/errors.md +8 -4
- package/docs/limits-and-quotas.md +1 -1
- package/docs/provider-runtime-capabilities.md +3 -3
- package/package.json +1 -1
|
@@ -71,6 +71,17 @@ export interface CoordinatorStreamOptions {
|
|
|
71
71
|
* watchdog → quiet runs may reconnect).
|
|
72
72
|
*/
|
|
73
73
|
readonly pingIntervalMs?: number;
|
|
74
|
+
/**
|
|
75
|
+
* Event-quiet recheck window. A pong proves the SOCKET is alive, not the
|
|
76
|
+
* delivery pipeline behind it — a server-side subscription that died (reaped
|
|
77
|
+
* connection row, wedged fan-out) keeps answering pings while never delivering
|
|
78
|
+
* another event, so the idle watchdog alone would hang one frame short of the
|
|
79
|
+
* terminal forever. If no REAL event frame arrives within this many ms the
|
|
80
|
+
* client silently reconnects (resume from cursor) — the replay-on-connect path
|
|
81
|
+
* reads the event store directly, so a dead subscription self-heals. Default
|
|
82
|
+
* 90s. Set 0 to disable.
|
|
83
|
+
*/
|
|
84
|
+
readonly eventQuietRecheckMs?: number;
|
|
74
85
|
}
|
|
75
86
|
export declare function streamCoordinatorEvents(opts: CoordinatorStreamOptions): AsyncGenerator<AexEvent, void, void>;
|
|
76
87
|
/** Async-iterable filter — keep only events matching the predicate. */
|
|
@@ -41,6 +41,8 @@ const COORDINATOR_PING = "aex:ping";
|
|
|
41
41
|
const DEFAULT_IDLE_TIMEOUT_MS = 45_000;
|
|
42
42
|
/** Default client keep-alive ping cadence. */
|
|
43
43
|
const DEFAULT_PING_INTERVAL_MS = 15_000;
|
|
44
|
+
/** Default event-quiet recheck window (a silent reconnect, so the cost of a false positive is small). */
|
|
45
|
+
const DEFAULT_EVENT_QUIET_RECHECK_MS = 90_000;
|
|
44
46
|
export async function* streamCoordinatorEvents(opts) {
|
|
45
47
|
const makeWs = opts.webSocketFactory ?? ((url) => new WebSocket(url));
|
|
46
48
|
const isTerminal = opts.isTerminal ?? isTerminalType;
|
|
@@ -48,6 +50,7 @@ export async function* streamCoordinatorEvents(opts) {
|
|
|
48
50
|
const maxReconnects = opts.maxReconnects ?? Number.POSITIVE_INFINITY;
|
|
49
51
|
const idleTimeoutMs = opts.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
|
|
50
52
|
const pingIntervalMs = opts.pingIntervalMs ?? DEFAULT_PING_INTERVAL_MS;
|
|
53
|
+
const eventQuietRecheckMs = opts.eventQuietRecheckMs ?? DEFAULT_EVENT_QUIET_RECHECK_MS;
|
|
51
54
|
let cursor = (opts.from ?? 0) - 1;
|
|
52
55
|
let attempts = 0;
|
|
53
56
|
let done = false;
|
|
@@ -70,6 +73,7 @@ export async function* streamCoordinatorEvents(opts) {
|
|
|
70
73
|
};
|
|
71
74
|
let idleTimer = null;
|
|
72
75
|
let pingTimer = null;
|
|
76
|
+
let quietTimer = null;
|
|
73
77
|
const stopTimers = () => {
|
|
74
78
|
if (idleTimer !== null) {
|
|
75
79
|
clearTimeout(idleTimer);
|
|
@@ -79,6 +83,10 @@ export async function* streamCoordinatorEvents(opts) {
|
|
|
79
83
|
clearInterval(pingTimer);
|
|
80
84
|
pingTimer = null;
|
|
81
85
|
}
|
|
86
|
+
if (quietTimer !== null) {
|
|
87
|
+
clearTimeout(quietTimer);
|
|
88
|
+
quietTimer = null;
|
|
89
|
+
}
|
|
82
90
|
};
|
|
83
91
|
// Re-arm on every inbound frame. On expiry the socket is presumed half-open
|
|
84
92
|
// → close it and let the loop fall through to reconnect (resume from cursor).
|
|
@@ -97,6 +105,23 @@ export async function* streamCoordinatorEvents(opts) {
|
|
|
97
105
|
wake();
|
|
98
106
|
}, idleTimeoutMs);
|
|
99
107
|
};
|
|
108
|
+
// Re-arm only on REAL event frames. On expiry: silent reconnect (resume from
|
|
109
|
+
// cursor) — self-heals a dead server-side subscription a pong can't expose.
|
|
110
|
+
const armQuiet = () => {
|
|
111
|
+
if (eventQuietRecheckMs <= 0)
|
|
112
|
+
return;
|
|
113
|
+
if (quietTimer !== null)
|
|
114
|
+
clearTimeout(quietTimer);
|
|
115
|
+
quietTimer = setTimeout(() => {
|
|
116
|
+
quietTimer = null;
|
|
117
|
+
if (closed)
|
|
118
|
+
return;
|
|
119
|
+
closed = true;
|
|
120
|
+
disconnectReason = "quiet_recheck";
|
|
121
|
+
closeQuietly(ws);
|
|
122
|
+
wake();
|
|
123
|
+
}, eventQuietRecheckMs);
|
|
124
|
+
};
|
|
100
125
|
ws.addEventListener("open", () => {
|
|
101
126
|
armIdle();
|
|
102
127
|
if (pingIntervalMs > 0 && typeof ws.send === "function") {
|
|
@@ -117,9 +142,12 @@ export async function* streamCoordinatorEvents(opts) {
|
|
|
117
142
|
return;
|
|
118
143
|
try {
|
|
119
144
|
const evt = JSON.parse(data);
|
|
120
|
-
if (typeof evt.sequence === "number"
|
|
121
|
-
|
|
122
|
-
|
|
145
|
+
if (typeof evt.sequence === "number") {
|
|
146
|
+
armQuiet(); // a real event frame proves the delivery pipeline, not just the socket
|
|
147
|
+
if (evt.sequence > cursor) {
|
|
148
|
+
queue.push(evt);
|
|
149
|
+
wake();
|
|
150
|
+
}
|
|
123
151
|
}
|
|
124
152
|
}
|
|
125
153
|
catch {
|
|
@@ -152,8 +180,10 @@ export async function* streamCoordinatorEvents(opts) {
|
|
|
152
180
|
};
|
|
153
181
|
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
154
182
|
// Arm immediately: a connect that never reaches "open" (and fakes that
|
|
155
|
-
// never emit it) must still time out rather than hang forever.
|
|
183
|
+
// never emit it) must still time out rather than hang forever. The quiet
|
|
184
|
+
// recheck arms here too so a socket fed only by pongs still recycles.
|
|
156
185
|
armIdle();
|
|
186
|
+
armQuiet();
|
|
157
187
|
try {
|
|
158
188
|
while (true) {
|
|
159
189
|
while (queue.length > 0) {
|
|
@@ -177,6 +207,12 @@ export async function* streamCoordinatorEvents(opts) {
|
|
|
177
207
|
finally {
|
|
178
208
|
stopTimers();
|
|
179
209
|
opts.signal?.removeEventListener("abort", onAbort);
|
|
210
|
+
// A caller that `break`s the iterator triggers the generator's `return()`,
|
|
211
|
+
// which lands here with the socket still OPEN (the yield was suspended, no
|
|
212
|
+
// terminal/abort/transport-close ran). Close it so an early break never
|
|
213
|
+
// leaks a live WebSocket. Idempotent: the terminal/abort/reconnect paths
|
|
214
|
+
// have already closed it, and closeQuietly swallows a double close.
|
|
215
|
+
closeQuietly(ws);
|
|
180
216
|
}
|
|
181
217
|
if (done || opts.signal?.aborted)
|
|
182
218
|
return;
|
|
@@ -188,7 +224,11 @@ export async function* streamCoordinatorEvents(opts) {
|
|
|
188
224
|
console.warn(`[aex] event stream gave up after ${maxReconnects} reconnect attempt(s) (last: ${disconnectReason || "unknown"}); ended before a terminal event at seq ${cursor + 1}`);
|
|
189
225
|
return;
|
|
190
226
|
}
|
|
191
|
-
|
|
227
|
+
// The quiet recheck is a routine self-heal on a legitimately quiet stream —
|
|
228
|
+
// reconnecting silently keeps a long tool call from spamming the console.
|
|
229
|
+
if (disconnectReason !== "quiet_recheck") {
|
|
230
|
+
console.warn(`[aex] event stream disconnected (${disconnectReason || "unknown"}); reconnecting attempt ${attempts} from seq ${cursor + 1}`);
|
|
231
|
+
}
|
|
192
232
|
await sleep(reconnectDelayMs, opts.signal);
|
|
193
233
|
}
|
|
194
234
|
}
|
package/dist/_contracts/http.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AexApiError } from "./sdk-errors.js";
|
|
1
|
+
import { AexApiError, AexError, AexNetworkError, redactUrl } from "./sdk-errors.js";
|
|
2
2
|
import { AEX_DEFAULT_BASE_URL } from "./stable.js";
|
|
3
3
|
/**
|
|
4
4
|
* Thin transport used by every BFF-bound operation. The SDK class and
|
|
@@ -17,7 +17,13 @@ export class HttpClient {
|
|
|
17
17
|
}
|
|
18
18
|
const raw = options.baseUrl ?? AEX_DEFAULT_BASE_URL;
|
|
19
19
|
const normalized = raw.endsWith("/") ? raw : `${raw}/`;
|
|
20
|
-
|
|
20
|
+
try {
|
|
21
|
+
this.#baseUrl = new URL(normalized);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
throw new Error(`HttpClient: invalid aex baseUrl ${JSON.stringify(redactUrl(raw))} — ` +
|
|
25
|
+
`expected an absolute URL like "${AEX_DEFAULT_BASE_URL}"`, { cause: err });
|
|
26
|
+
}
|
|
21
27
|
this.#apiToken = options.apiToken;
|
|
22
28
|
this.#fetch = options.fetch ?? fetch;
|
|
23
29
|
this.#debug = options.debug;
|
|
@@ -46,11 +52,18 @@ export class HttpClient {
|
|
|
46
52
|
}
|
|
47
53
|
}
|
|
48
54
|
const startedMs = Date.now();
|
|
49
|
-
|
|
55
|
+
let response;
|
|
56
|
+
try {
|
|
57
|
+
response = await this.#fetch(url, { ...init, headers });
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
throw toNetworkError(init.method, url, err);
|
|
61
|
+
}
|
|
50
62
|
this.#trace(init.method, url, response.status, startedMs);
|
|
51
63
|
const body = await readJson(response);
|
|
52
64
|
if (!response.ok) {
|
|
53
|
-
|
|
65
|
+
const errorBody = withResponseRequestId(body, response.headers);
|
|
66
|
+
throw new AexApiError(response.status, extractErrorMessage(errorBody), errorBody);
|
|
54
67
|
}
|
|
55
68
|
return body;
|
|
56
69
|
}
|
|
@@ -64,15 +77,41 @@ export class HttpClient {
|
|
|
64
77
|
...normalizeHeaders(init.headers)
|
|
65
78
|
};
|
|
66
79
|
const startedMs = Date.now();
|
|
67
|
-
|
|
80
|
+
let response;
|
|
81
|
+
try {
|
|
82
|
+
response = await this.#fetch(url, { ...init, headers });
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
throw toNetworkError(init.method, url, err);
|
|
86
|
+
}
|
|
68
87
|
this.#trace(init.method, url, response.status, startedMs);
|
|
69
88
|
if (!response.ok) {
|
|
70
89
|
const body = await readJson(response);
|
|
71
|
-
|
|
90
|
+
const errorBody = withResponseRequestId(body, response.headers);
|
|
91
|
+
throw new AexApiError(response.status, extractErrorMessage(errorBody), errorBody);
|
|
72
92
|
}
|
|
73
93
|
return { response };
|
|
74
94
|
}
|
|
75
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Wrap a fetch rejection into an {@link AexNetworkError} carrying the
|
|
98
|
+
* request's method + redacted host/path. Caller-initiated aborts and
|
|
99
|
+
* already-structured aex errors (e.g. a retry layer's AexRateLimitError or
|
|
100
|
+
* AexNetworkError) pass through untouched.
|
|
101
|
+
*/
|
|
102
|
+
function toNetworkError(method, url, err) {
|
|
103
|
+
if (err instanceof AexError)
|
|
104
|
+
return err;
|
|
105
|
+
// `DOMException` is not an `Error` subclass on every runtime, so match aborts by name.
|
|
106
|
+
if (err?.name === "AbortError")
|
|
107
|
+
return err;
|
|
108
|
+
return new AexNetworkError({
|
|
109
|
+
method: (method ?? "GET").toUpperCase(),
|
|
110
|
+
host: url.host,
|
|
111
|
+
path: url.pathname,
|
|
112
|
+
cause: err
|
|
113
|
+
});
|
|
114
|
+
}
|
|
76
115
|
function normalizeHeaders(headers) {
|
|
77
116
|
if (!headers)
|
|
78
117
|
return {};
|
|
@@ -93,11 +132,44 @@ async function readJson(response) {
|
|
|
93
132
|
return { raw: text };
|
|
94
133
|
}
|
|
95
134
|
}
|
|
135
|
+
function withResponseRequestId(body, headers) {
|
|
136
|
+
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
137
|
+
return body;
|
|
138
|
+
const record = body;
|
|
139
|
+
if (typeof record.requestId === "string" && record.requestId.trim())
|
|
140
|
+
return body;
|
|
141
|
+
const requestId = responseRequestId(headers);
|
|
142
|
+
return requestId ? { ...record, requestId } : body;
|
|
143
|
+
}
|
|
144
|
+
function responseRequestId(headers) {
|
|
145
|
+
for (const name of ["x-request-id", "request-id"]) {
|
|
146
|
+
const value = headers.get(name)?.trim();
|
|
147
|
+
if (value)
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
96
152
|
function extractErrorMessage(body) {
|
|
97
153
|
if (body && typeof body === "object") {
|
|
98
154
|
const obj = body;
|
|
99
|
-
if (typeof obj.error === "string")
|
|
155
|
+
if (typeof obj.error === "string") {
|
|
156
|
+
// A 409 `session_busy` body carries the session's CURRENT status.
|
|
157
|
+
// Surface it: a send to a deleted (or cancelling/suspending) session
|
|
158
|
+
// otherwise reads as merely "busy", which is misleading for a session
|
|
159
|
+
// that will never accept a turn again.
|
|
160
|
+
const status = body.status;
|
|
161
|
+
if (obj.error === "session_busy" && typeof status === "string") {
|
|
162
|
+
return `session_busy (session status: ${status})`;
|
|
163
|
+
}
|
|
164
|
+
// Most aex API rejections are `{error: <code>, message: <human detail>}`.
|
|
165
|
+
// Keep the stable code first, but don't drop the server's actionable
|
|
166
|
+
// detail (e.g. asset_snapshot_source_missing's "upload and finalize it
|
|
167
|
+
// before referencing it").
|
|
168
|
+
if (typeof obj.message === "string" && obj.message.length > 0 && obj.message !== obj.error) {
|
|
169
|
+
return `${obj.error}: ${obj.message}`;
|
|
170
|
+
}
|
|
100
171
|
return obj.error;
|
|
172
|
+
}
|
|
101
173
|
if (obj.error && typeof obj.error === "object" && "message" in obj.error) {
|
|
102
174
|
const message = obj.error.message;
|
|
103
175
|
if (typeof message === "string")
|
|
@@ -54,7 +54,34 @@ export async function listRuns(http, query) {
|
|
|
54
54
|
params.limit = String(query.limit);
|
|
55
55
|
if (query?.cursor !== undefined)
|
|
56
56
|
params.cursor = query.cursor;
|
|
57
|
-
|
|
57
|
+
const page = await http.request("/api/runs", {}, params);
|
|
58
|
+
// Defensive contract enforcement: some deployed planes leak non-run marker
|
|
59
|
+
// rows (settle-time ledger/spendmark items) into the run-list index. Those
|
|
60
|
+
// phantoms carry only { id, createdAt } and would surface as duplicate,
|
|
61
|
+
// status-less RunSummary entries. Drop anything missing the fields
|
|
62
|
+
// RunSummary declares required, so callers can trust the published type.
|
|
63
|
+
// The same enforcement covers `costUsd`: deployed planes serve `null` for
|
|
64
|
+
// runs with no settled telemetry, but RunSummary declares `costUsd?: number`
|
|
65
|
+
// — normalize `null` to absent so typed callers never see it.
|
|
66
|
+
let changed = false;
|
|
67
|
+
const runs = [];
|
|
68
|
+
for (const run of page.runs) {
|
|
69
|
+
if (typeof run.id !== "string" ||
|
|
70
|
+
typeof run.status !== "string" ||
|
|
71
|
+
typeof run.createdAt !== "string" ||
|
|
72
|
+
typeof run.updatedAt !== "string") {
|
|
73
|
+
changed = true;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (typeof run.costUsd !== "number" && run.costUsd !== undefined) {
|
|
77
|
+
const { costUsd: _dropped, ...rest } = run;
|
|
78
|
+
runs.push(rest);
|
|
79
|
+
changed = true;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
runs.push(run);
|
|
83
|
+
}
|
|
84
|
+
return changed ? { ...page, runs } : page;
|
|
58
85
|
}
|
|
59
86
|
function idempotencyHeaders(options) {
|
|
60
87
|
return options?.idempotencyKey ? { "Idempotency-Key": options.idempotencyKey } : undefined;
|
|
@@ -197,12 +224,22 @@ export async function outputLink(http, runId, selectorOrQuery, options) {
|
|
|
197
224
|
method: "POST",
|
|
198
225
|
body: JSON.stringify({ expiresInSeconds })
|
|
199
226
|
});
|
|
227
|
+
const effectiveExpiresIn = result.expiresInSeconds ?? expiresInSeconds;
|
|
200
228
|
return {
|
|
201
229
|
...result,
|
|
202
|
-
expiresInSeconds:
|
|
230
|
+
expiresInSeconds: effectiveExpiresIn,
|
|
231
|
+
expiresAt: result.expiresAt ?? syntheticExpiresAt(effectiveExpiresIn),
|
|
203
232
|
output: result.output ?? output
|
|
204
233
|
};
|
|
205
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* The hosted API returns `{ url, expiresInSeconds }` without an absolute
|
|
237
|
+
* timestamp; the documented `link.expiresAt` is synthesized client-side from
|
|
238
|
+
* the mint time so it is always present on a returned link.
|
|
239
|
+
*/
|
|
240
|
+
function syntheticExpiresAt(expiresInSeconds) {
|
|
241
|
+
return new Date(Date.now() + expiresInSeconds * 1000).toISOString();
|
|
242
|
+
}
|
|
206
243
|
export async function createOutputLink(http, runId, selectorOrQuery, options) {
|
|
207
244
|
return outputLink(http, runId, selectorOrQuery, options);
|
|
208
245
|
}
|
|
@@ -212,9 +249,11 @@ export async function eventArchiveLink(http, runId, options) {
|
|
|
212
249
|
method: "POST",
|
|
213
250
|
body: JSON.stringify({ expiresInSeconds })
|
|
214
251
|
});
|
|
252
|
+
const effectiveExpiresIn = result.expiresInSeconds ?? expiresInSeconds;
|
|
215
253
|
return {
|
|
216
254
|
...result,
|
|
217
|
-
expiresInSeconds:
|
|
255
|
+
expiresInSeconds: effectiveExpiresIn,
|
|
256
|
+
expiresAt: result.expiresAt ?? syntheticExpiresAt(effectiveExpiresIn)
|
|
218
257
|
};
|
|
219
258
|
}
|
|
220
259
|
export function resolveOutputFileSelector(outputs, selector, runId) {
|
|
@@ -35,11 +35,11 @@ export declare const PROVIDER_PUBLIC_SUPPORT: {
|
|
|
35
35
|
readonly href: "../../../scripts/validate/capability-matrix.test.ts";
|
|
36
36
|
}, {
|
|
37
37
|
readonly label: "Installed-SDK Anthropic live user test";
|
|
38
|
-
readonly href: "../../../apps/user-tests/test/live/live-sdk-anthropic-managed.test.ts";
|
|
38
|
+
readonly href: "../../../apps/user-tests/test/live/providers/live-sdk-anthropic-managed.test.ts";
|
|
39
39
|
}];
|
|
40
40
|
readonly managedEvidence: readonly [{
|
|
41
41
|
readonly label: "Installed-SDK Anthropic live user test";
|
|
42
|
-
readonly href: "../../../apps/user-tests/test/live/live-sdk-anthropic-managed.test.ts";
|
|
42
|
+
readonly href: "../../../apps/user-tests/test/live/providers/live-sdk-anthropic-managed.test.ts";
|
|
43
43
|
}];
|
|
44
44
|
};
|
|
45
45
|
readonly deepseek: {
|
|
@@ -9,7 +9,7 @@ const COMMON_EVIDENCE = [
|
|
|
9
9
|
const ANTHROPIC_LIVE_USER_EVIDENCE = [
|
|
10
10
|
{
|
|
11
11
|
label: "Installed-SDK Anthropic live user test",
|
|
12
|
-
href: "../../../apps/user-tests/test/live/live-sdk-anthropic-managed.test.ts"
|
|
12
|
+
href: "../../../apps/user-tests/test/live/providers/live-sdk-anthropic-managed.test.ts"
|
|
13
13
|
}
|
|
14
14
|
];
|
|
15
15
|
const DEEPSEEK_LIVE_USER_EVIDENCE = [
|
|
@@ -135,7 +135,7 @@ export interface RunUnit {
|
|
|
135
135
|
* must still render *something* for a buggy historical row rather than 500ing
|
|
136
136
|
* the whole detail page.
|
|
137
137
|
*/
|
|
138
|
-
export declare function parseRunUnitSubmission(input: unknown): RunUnitSubmission;
|
|
138
|
+
export declare function parseRunUnitSubmission(input: unknown, fallbackModel?: unknown): RunUnitSubmission;
|
|
139
139
|
/**
|
|
140
140
|
* Normalize a `GET /api/runs/:runId` payload into a RunUnit whose type contract
|
|
141
141
|
* holds AT RUNTIME. The managed (AWS) plane returns a LEAN record (scalars +
|
|
@@ -34,19 +34,19 @@ import { PLATFORM_PACKAGE_ECOSYSTEMS } from "./submission.js";
|
|
|
34
34
|
* must still render *something* for a buggy historical row rather than 500ing
|
|
35
35
|
* the whole detail page.
|
|
36
36
|
*/
|
|
37
|
-
export function parseRunUnitSubmission(input) {
|
|
37
|
+
export function parseRunUnitSubmission(input, fallbackModel) {
|
|
38
38
|
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
39
|
-
return fallbackFlat();
|
|
39
|
+
return fallbackFlat(fallbackModel);
|
|
40
40
|
}
|
|
41
41
|
const value = input;
|
|
42
42
|
if (value.kind === "submission") {
|
|
43
|
-
return parseFlatProjection(value);
|
|
43
|
+
return parseFlatProjection(value, fallbackModel);
|
|
44
44
|
}
|
|
45
45
|
// Snapshot exists but does not match the flat shape — surface as an
|
|
46
46
|
// empty flat submission so consumers can still render lifecycle bits.
|
|
47
|
-
return fallbackFlat();
|
|
47
|
+
return fallbackFlat(fallbackModel);
|
|
48
48
|
}
|
|
49
|
-
function parseFlatProjection(value) {
|
|
49
|
+
function parseFlatProjection(value, fallbackModel) {
|
|
50
50
|
const submissionRaw = isRecord(value.submission) ? value.submission : {};
|
|
51
51
|
const outputsRaw = isRecord(submissionRaw.outputs) ? submissionRaw.outputs : {};
|
|
52
52
|
const allowedDirs = toOptionalStringArray(outputsRaw.allowedDirs);
|
|
@@ -56,7 +56,7 @@ function parseFlatProjection(value) {
|
|
|
56
56
|
const maxTotalBytes = toOptionalPositiveInteger(outputsRaw.maxTotalBytes);
|
|
57
57
|
const maxFiles = toOptionalPositiveInteger(outputsRaw.maxFiles);
|
|
58
58
|
const submission = {
|
|
59
|
-
model: coerceRunUnitModel(submissionRaw.model),
|
|
59
|
+
model: coerceRunUnitModel(submissionRaw.model ?? fallbackModel),
|
|
60
60
|
...(typeof submissionRaw.system === "string" ? { system: submissionRaw.system } : {}),
|
|
61
61
|
prompt: toStringArray(submissionRaw.prompt),
|
|
62
62
|
agentsMd: [],
|
|
@@ -99,11 +99,11 @@ function parseSecurityProfile(value) {
|
|
|
99
99
|
function toOptionalPositiveInteger(value) {
|
|
100
100
|
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
|
|
101
101
|
}
|
|
102
|
-
function fallbackFlat() {
|
|
102
|
+
function fallbackFlat(fallbackModel) {
|
|
103
103
|
return {
|
|
104
104
|
kind: "submission",
|
|
105
105
|
submission: {
|
|
106
|
-
model:
|
|
106
|
+
model: coerceRunUnitModel(fallbackModel),
|
|
107
107
|
prompt: [],
|
|
108
108
|
agentsMd: [],
|
|
109
109
|
files: [],
|
|
@@ -154,7 +154,10 @@ export function normalizeRunUnit(raw) {
|
|
|
154
154
|
: Array.isArray(r.attempts)
|
|
155
155
|
? r.attempts.length
|
|
156
156
|
: 0,
|
|
157
|
-
|
|
157
|
+
// Plane responses that project a flat record (no `submission` snapshot)
|
|
158
|
+
// still carry the run's `model` at the top level — prefer it over the
|
|
159
|
+
// static fallback so `unit()` never claims a model the run did not use.
|
|
160
|
+
submission: parseRunUnitSubmission(r.submission, r.model),
|
|
158
161
|
...(isRecord(r.capsSnapshot) ? { capsSnapshot: r.capsSnapshot } : {}),
|
|
159
162
|
attempts: arr(r.attempts),
|
|
160
163
|
events: {
|
|
@@ -69,6 +69,12 @@ export interface Session {
|
|
|
69
69
|
readonly usage?: UsageSummary;
|
|
70
70
|
readonly costUsd?: number;
|
|
71
71
|
readonly errorMessage?: string | null;
|
|
72
|
+
/**
|
|
73
|
+
* Settle-written failure taxonomy for an `error` session (e.g.
|
|
74
|
+
* `provider-permanent`, `budget_exhausted`) — the class a caller can branch
|
|
75
|
+
* on, complementing the human-readable `errorMessage`.
|
|
76
|
+
*/
|
|
77
|
+
readonly failureClass?: string | null;
|
|
72
78
|
readonly [key: string]: unknown;
|
|
73
79
|
}
|
|
74
80
|
export interface SessionSummary {
|
|
@@ -153,7 +159,7 @@ export interface UsageSummary {
|
|
|
153
159
|
readonly totalTokens?: number;
|
|
154
160
|
}
|
|
155
161
|
/**
|
|
156
|
-
* Filters for {@link import("./operations.js").listRuns} / `
|
|
162
|
+
* Filters for {@link import("./operations.js").listRuns} / the CLI's `aex runs`.
|
|
157
163
|
* Every field is optional; omitting all of them lists the most recent runs in the
|
|
158
164
|
* token's workspace. Workspace identity is derived server-side from the API token,
|
|
159
165
|
* so there is no `workspaceId` here — a token can only ever enumerate its own runs.
|
|
@@ -187,7 +193,7 @@ export interface RunListPage {
|
|
|
187
193
|
readonly nextCursor?: string;
|
|
188
194
|
}
|
|
189
195
|
/**
|
|
190
|
-
* Cross-run output search query (`Aex.
|
|
196
|
+
* Cross-run output search query (`Aex.sessions.searchOutputs`). Restrict to a
|
|
191
197
|
* corpus with `runIds`; filter by filename substring / extension / content type.
|
|
192
198
|
* The MVP composes this client-side (per-run `listOutputs` + filter) — a future
|
|
193
199
|
* server-side `GET /api/outputs/search` can back the same contract with a real
|
|
@@ -322,7 +328,7 @@ export interface OutputFileDownload {
|
|
|
322
328
|
readonly output: Output;
|
|
323
329
|
readonly bytes: Uint8Array;
|
|
324
330
|
}
|
|
325
|
-
/** Options for `Aex.outputs.read` / {@link import("./operations.js").readOutputText}. */
|
|
331
|
+
/** Options for `Aex.sessions.outputs(id).read` / {@link import("./operations.js").readOutputText}. */
|
|
326
332
|
export interface ReadOutputTextOptions {
|
|
327
333
|
/**
|
|
328
334
|
* Stop reading after this many bytes. Defaults to 50_000; clamped server-side
|
|
@@ -338,7 +344,7 @@ export interface ReadOutputTextOptions {
|
|
|
338
344
|
}
|
|
339
345
|
/**
|
|
340
346
|
* A byte-capped, decoded text read of one output file, as returned by
|
|
341
|
-
* `Aex.outputs.read`. Built for feeding run deliverables to an LLM
|
|
347
|
+
* `Aex.sessions.outputs(id).read`. Built for feeding run deliverables to an LLM
|
|
342
348
|
* without loading the whole (possibly very large) file into memory or context:
|
|
343
349
|
* the read streams and stops at `maxBytes`, so `text` is at most that many bytes
|
|
344
350
|
* decoded as UTF-8. Check {@link truncated} before treating `text` as complete.
|
|
@@ -368,7 +374,12 @@ export interface OutputLink {
|
|
|
368
374
|
readonly [key: string]: unknown;
|
|
369
375
|
}
|
|
370
376
|
export interface WhoAmI {
|
|
371
|
-
|
|
377
|
+
/**
|
|
378
|
+
* Kind of principal the bearer resolved to. OPTIONAL IN PRACTICE: current
|
|
379
|
+
* managed deployments do not serve it (`GET /whoami` returns only
|
|
380
|
+
* `workspaceId` + `scopes` + `limits`) — treat `undefined` as "api_token".
|
|
381
|
+
*/
|
|
382
|
+
readonly principalType?: "api_token" | "user";
|
|
372
383
|
readonly workspaceId?: string;
|
|
373
384
|
readonly tokenId?: string;
|
|
374
385
|
readonly tokenName?: string | null;
|
|
@@ -377,9 +388,10 @@ export interface WhoAmI {
|
|
|
377
388
|
* Workspace-level caps the BFF will enforce on subsequent calls.
|
|
378
389
|
* Surfaced so consumers (e.g. broll's app-side admission gate) can
|
|
379
390
|
* decide whether to keep their own gate or rely on platform headers.
|
|
380
|
-
* All fields optional — older BFFs may omit
|
|
381
|
-
*
|
|
382
|
-
*
|
|
391
|
+
* All fields optional — older BFFs may omit, and current managed
|
|
392
|
+
* deployments serve the newer {@link limits} block INSTEAD of `caps`.
|
|
393
|
+
* Numbers are concrete snapshots at the time of the `whoami` call;
|
|
394
|
+
* `null` means no app-visible cap is applied for that field.
|
|
383
395
|
*/
|
|
384
396
|
readonly caps?: {
|
|
385
397
|
/** Token-bucket cap on POST /api/runs per minute, per workspace. */
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
export type AexErrorCode = "RUN_CONFIG_INVALID" | "CREDENTIAL_INVALID" | "PROVIDER_ERROR" | "RUN_STATE_ERROR" | "CLEANUP_ERROR" | "RUNTIME_UNSUPPORTED" | "API_ERROR";
|
|
1
|
+
export type AexErrorCode = "RUN_CONFIG_INVALID" | "CREDENTIAL_INVALID" | "PROVIDER_ERROR" | "RUN_STATE_ERROR" | "CLEANUP_ERROR" | "RUNTIME_UNSUPPORTED" | "API_ERROR" | "NETWORK_ERROR";
|
|
2
2
|
export declare class AexError extends Error {
|
|
3
3
|
readonly code: AexErrorCode;
|
|
4
4
|
readonly details?: unknown;
|
|
5
|
-
constructor(code: AexErrorCode, message: string, details?: unknown
|
|
5
|
+
constructor(code: AexErrorCode, message: string, details?: unknown, options?: {
|
|
6
|
+
readonly cause?: unknown;
|
|
7
|
+
});
|
|
6
8
|
}
|
|
7
9
|
export declare class RunConfigValidationError extends AexError {
|
|
8
10
|
constructor(message: string, details?: unknown);
|
|
@@ -32,3 +34,43 @@ export declare class AexApiError extends AexError {
|
|
|
32
34
|
readonly body: unknown;
|
|
33
35
|
constructor(status: number, message: string, body: unknown);
|
|
34
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Thrown when a BFF-bound request fails BEFORE any HTTP response exists — DNS
|
|
39
|
+
* failure, connection refused, TLS error, socket reset. Wraps the raw fetch
|
|
40
|
+
* rejection (whose undici form is a bare `TypeError: fetch failed` with the
|
|
41
|
+
* useful code hidden on `cause.code`) into a message that names the request
|
|
42
|
+
* and the transport failure, e.g.
|
|
43
|
+
* `POST api.aex.dev/assets/presign failed: ECONNREFUSED (connect ECONNREFUSED 127.0.0.1:443)`.
|
|
44
|
+
* The original rejection is preserved on `cause`.
|
|
45
|
+
*/
|
|
46
|
+
export declare class AexNetworkError extends AexError {
|
|
47
|
+
readonly method: string;
|
|
48
|
+
/** Request host — never carries credentials or the query string. */
|
|
49
|
+
readonly host: string;
|
|
50
|
+
readonly path: string;
|
|
51
|
+
/** Transport failure code (e.g. `ECONNREFUSED`), when detectable. */
|
|
52
|
+
readonly causeCode: string | undefined;
|
|
53
|
+
/** Attempts made when a retry layer exhausted its budget; `1` otherwise. */
|
|
54
|
+
readonly attempts: number;
|
|
55
|
+
constructor(args: {
|
|
56
|
+
readonly method: string;
|
|
57
|
+
readonly host: string;
|
|
58
|
+
readonly path: string;
|
|
59
|
+
readonly cause: unknown;
|
|
60
|
+
/** Set by the retry layer when it gave up: appended to the message. */
|
|
61
|
+
readonly attempts?: number;
|
|
62
|
+
readonly elapsedMs?: number;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Best-effort transport error code (`ECONNREFUSED`, `ENOTFOUND`, …): checks
|
|
67
|
+
* `err.code`, then `err.cause.code` (where undici hides it), then falls back
|
|
68
|
+
* to an `E…`-shaped token in the message.
|
|
69
|
+
*/
|
|
70
|
+
export declare function extractErrorCode(err: unknown): string | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* Redact a URL down to protocol + host + path: credentials become
|
|
73
|
+
* `[redacted]@` and any query string becomes `?[redacted]` (presigned URLs
|
|
74
|
+
* carry signatures there). Tolerates unparseable input.
|
|
75
|
+
*/
|
|
76
|
+
export declare function redactUrl(url: string): string;
|