@aexhq/sdk 0.40.9 → 0.40.11
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/http.d.ts +11 -0
- package/dist/_contracts/http.js +119 -39
- package/dist/_contracts/operations.d.ts +2 -2
- package/dist/_contracts/operations.js +20 -4
- package/dist/cli.mjs +134 -38
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/docs/outputs.md +4 -4
- package/package.json +1 -1
|
@@ -18,6 +18,17 @@ export interface HttpClientOptions {
|
|
|
18
18
|
readonly fetch?: FetchLike;
|
|
19
19
|
/** When set, every request emits a redacted one-line trace here. */
|
|
20
20
|
readonly debug?: DebugSink;
|
|
21
|
+
/**
|
|
22
|
+
* Retry transient transport failures for idempotent GET/HEAD requests.
|
|
23
|
+
* Disabled by default; host CLIs enable this so a single dropped API
|
|
24
|
+
* connection does not fail read-only commands.
|
|
25
|
+
*/
|
|
26
|
+
readonly retryTransientGets?: boolean | TransientGetRetryOptions;
|
|
27
|
+
}
|
|
28
|
+
export interface TransientGetRetryOptions {
|
|
29
|
+
readonly maxAttempts?: number;
|
|
30
|
+
readonly baseDelayMs?: number;
|
|
31
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
21
32
|
}
|
|
22
33
|
/**
|
|
23
34
|
* Thin transport used by every BFF-bound operation. The SDK class and
|
package/dist/_contracts/http.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AexError, AexNetworkError, redactUrl } from "./sdk-errors.js";
|
|
1
|
+
import { AexError, AexNetworkError, extractErrorCode, redactUrl } from "./sdk-errors.js";
|
|
2
2
|
import { apiErrorFromResponse } from "./error-factory.js";
|
|
3
3
|
import { AEX_DEFAULT_BASE_URL } from "./stable.js";
|
|
4
4
|
/**
|
|
@@ -12,6 +12,7 @@ export class HttpClient {
|
|
|
12
12
|
#apiKey;
|
|
13
13
|
#fetch;
|
|
14
14
|
#debug;
|
|
15
|
+
#retryTransientGets;
|
|
15
16
|
constructor(options) {
|
|
16
17
|
if (!options.apiKey) {
|
|
17
18
|
throw new Error("HttpClient: apiKey is required");
|
|
@@ -28,6 +29,7 @@ export class HttpClient {
|
|
|
28
29
|
this.#apiKey = options.apiKey;
|
|
29
30
|
this.#fetch = options.fetch ?? fetch;
|
|
30
31
|
this.#debug = options.debug;
|
|
32
|
+
this.#retryTransientGets = resolveTransientGetRetry(options.retryTransientGets);
|
|
31
33
|
}
|
|
32
34
|
/** Emit a redacted round-trip trace (no auth header, body, or query). */
|
|
33
35
|
#trace(method, url, status, startedMs) {
|
|
@@ -52,25 +54,34 @@ export class HttpClient {
|
|
|
52
54
|
headers["content-type"] = "application/json";
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
57
|
+
const method = methodOf(init.method);
|
|
58
|
+
const retry = retryForMethod(method, this.#retryTransientGets);
|
|
59
|
+
const requestStartedMs = Date.now();
|
|
60
|
+
for (let attempt = 1; attempt <= retry.maxAttempts; attempt += 1) {
|
|
61
|
+
const startedMs = Date.now();
|
|
62
|
+
try {
|
|
63
|
+
const response = await this.#fetch(url, { ...init, headers });
|
|
64
|
+
this.#trace(method, url, response.status, startedMs);
|
|
65
|
+
const body = await readJson(response);
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
const errorBody = withResponseRequestId(body, response.headers);
|
|
68
|
+
throw apiErrorFromResponse({
|
|
69
|
+
status: response.status,
|
|
70
|
+
body: errorBody,
|
|
71
|
+
message: extractErrorMessage(errorBody)
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return body;
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
if (shouldRetryTransientRead(err, retry, attempt)) {
|
|
78
|
+
await sleepBeforeRetry(this.#debug, method, url, err, retry, attempt);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
throw toNetworkError(method, url, err, retry.maxAttempts > 1 ? attempt : undefined, Date.now() - requestStartedMs);
|
|
82
|
+
}
|
|
72
83
|
}
|
|
73
|
-
|
|
84
|
+
throw new Error("HttpClient.request retry loop exhausted");
|
|
74
85
|
}
|
|
75
86
|
async download(path, init = {}, query = {}) {
|
|
76
87
|
const url = new URL(path.replace(/^\//, ""), this.#baseUrl);
|
|
@@ -81,26 +92,93 @@ export class HttpClient {
|
|
|
81
92
|
authorization: `Bearer ${this.#apiKey}`,
|
|
82
93
|
...normalizeHeaders(init.headers)
|
|
83
94
|
};
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
95
|
+
const method = methodOf(init.method);
|
|
96
|
+
const retry = retryForMethod(method, this.#retryTransientGets);
|
|
97
|
+
const requestStartedMs = Date.now();
|
|
98
|
+
for (let attempt = 1; attempt <= retry.maxAttempts; attempt += 1) {
|
|
99
|
+
const startedMs = Date.now();
|
|
100
|
+
try {
|
|
101
|
+
const response = await this.#fetch(url, { ...init, headers });
|
|
102
|
+
this.#trace(method, url, response.status, startedMs);
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
const body = await readJson(response);
|
|
105
|
+
const errorBody = withResponseRequestId(body, response.headers);
|
|
106
|
+
throw apiErrorFromResponse({
|
|
107
|
+
status: response.status,
|
|
108
|
+
body: errorBody,
|
|
109
|
+
message: extractErrorMessage(errorBody)
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return { response };
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
if (shouldRetryTransientRead(err, retry, attempt)) {
|
|
116
|
+
await sleepBeforeRetry(this.#debug, method, url, err, retry, attempt);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
throw toNetworkError(method, url, err, retry.maxAttempts > 1 ? attempt : undefined, Date.now() - requestStartedMs);
|
|
120
|
+
}
|
|
101
121
|
}
|
|
102
|
-
|
|
122
|
+
throw new Error("HttpClient.download retry loop exhausted");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const DEFAULT_TRANSIENT_GET_RETRY = {
|
|
126
|
+
maxAttempts: 3,
|
|
127
|
+
baseDelayMs: 250,
|
|
128
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
129
|
+
};
|
|
130
|
+
const TRANSIENT_READ_CODES = new Set([
|
|
131
|
+
"ConnectionRefused",
|
|
132
|
+
"ECONNRESET",
|
|
133
|
+
"ECONNREFUSED",
|
|
134
|
+
"ETIMEDOUT",
|
|
135
|
+
"EAI_AGAIN",
|
|
136
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
137
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
138
|
+
"UND_ERR_SOCKET",
|
|
139
|
+
"UND_ERR_BODY_TIMEOUT"
|
|
140
|
+
]);
|
|
141
|
+
function resolveTransientGetRetry(retry) {
|
|
142
|
+
if (!retry)
|
|
143
|
+
return null;
|
|
144
|
+
if (retry === true)
|
|
145
|
+
return DEFAULT_TRANSIENT_GET_RETRY;
|
|
146
|
+
return {
|
|
147
|
+
maxAttempts: Math.max(1, retry.maxAttempts ?? DEFAULT_TRANSIENT_GET_RETRY.maxAttempts),
|
|
148
|
+
baseDelayMs: Math.max(0, retry.baseDelayMs ?? DEFAULT_TRANSIENT_GET_RETRY.baseDelayMs),
|
|
149
|
+
sleep: retry.sleep ?? DEFAULT_TRANSIENT_GET_RETRY.sleep
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function methodOf(method) {
|
|
153
|
+
return (method ?? "GET").toUpperCase();
|
|
154
|
+
}
|
|
155
|
+
function retryForMethod(method, retry) {
|
|
156
|
+
if (!retry || (method !== "GET" && method !== "HEAD")) {
|
|
157
|
+
return { ...DEFAULT_TRANSIENT_GET_RETRY, maxAttempts: 1 };
|
|
103
158
|
}
|
|
159
|
+
return retry;
|
|
160
|
+
}
|
|
161
|
+
function shouldRetryTransientRead(err, retry, attempt) {
|
|
162
|
+
return attempt < retry.maxAttempts && transientReadErrorCode(err) !== undefined;
|
|
163
|
+
}
|
|
164
|
+
async function sleepBeforeRetry(debug, method, url, err, retry, attempt) {
|
|
165
|
+
const delayMs = retry.baseDelayMs * attempt;
|
|
166
|
+
debug?.(`[aex] ${method} ${url.pathname} transient ${transientReadErrorCode(err) ?? "network"} ` +
|
|
167
|
+
`attempt ${attempt}/${retry.maxAttempts}; retrying in ${delayMs}ms`);
|
|
168
|
+
await retry.sleep(delayMs);
|
|
169
|
+
}
|
|
170
|
+
function transientReadErrorCode(err) {
|
|
171
|
+
if (err instanceof AexError)
|
|
172
|
+
return undefined;
|
|
173
|
+
if (err?.name === "AbortError")
|
|
174
|
+
return undefined;
|
|
175
|
+
const code = extractErrorCode(err);
|
|
176
|
+
if (code && TRANSIENT_READ_CODES.has(code))
|
|
177
|
+
return code;
|
|
178
|
+
const text = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
179
|
+
if (/fetch failed|socket hang up|other side closed|terminated|network.*reset/i.test(text))
|
|
180
|
+
return "fetch";
|
|
181
|
+
return undefined;
|
|
104
182
|
}
|
|
105
183
|
/**
|
|
106
184
|
* Wrap a fetch rejection into an {@link AexNetworkError} carrying the
|
|
@@ -108,7 +186,7 @@ export class HttpClient {
|
|
|
108
186
|
* already-structured aex errors (e.g. a retry layer's AexRateLimitError or
|
|
109
187
|
* AexNetworkError) pass through untouched.
|
|
110
188
|
*/
|
|
111
|
-
function toNetworkError(method, url, err) {
|
|
189
|
+
function toNetworkError(method, url, err, attempts, elapsedMs) {
|
|
112
190
|
if (err instanceof AexError)
|
|
113
191
|
return err;
|
|
114
192
|
// `DOMException` is not an `Error` subclass on every runtime, so match aborts by name.
|
|
@@ -118,7 +196,9 @@ function toNetworkError(method, url, err) {
|
|
|
118
196
|
method: (method ?? "GET").toUpperCase(),
|
|
119
197
|
host: url.host,
|
|
120
198
|
path: url.pathname,
|
|
121
|
-
cause: err
|
|
199
|
+
cause: err,
|
|
200
|
+
...(attempts !== undefined ? { attempts } : {}),
|
|
201
|
+
...(elapsedMs !== undefined ? { elapsedMs } : {})
|
|
122
202
|
});
|
|
123
203
|
}
|
|
124
204
|
function normalizeHeaders(headers) {
|
|
@@ -224,12 +224,12 @@ export declare function download(http: HttpClient, runId: string): Promise<Uint8
|
|
|
224
224
|
export declare function downloadOutputs(http: HttpClient, runId: string, options?: OutputTransferOptions): Promise<Uint8Array>;
|
|
225
225
|
/**
|
|
226
226
|
* Download only the event archive (the `events` namespace). Always includes
|
|
227
|
-
* typed `events.jsonl`.
|
|
227
|
+
* typed `events.jsonl` plus `manifest.json`.
|
|
228
228
|
*/
|
|
229
229
|
export declare function downloadEvents(http: HttpClient, runId: string): Promise<Uint8Array>;
|
|
230
230
|
/**
|
|
231
231
|
* Download only the run record (the `metadata` namespace) as a zip
|
|
232
|
-
* containing `run.json`.
|
|
232
|
+
* containing `run.json` plus `manifest.json`.
|
|
233
233
|
*/
|
|
234
234
|
export declare function downloadMetadata(http: HttpClient, runId: string): Promise<Uint8Array>;
|
|
235
235
|
export declare function submitRun(http: HttpClient, request: PlatformRunSubmissionInput): Promise<Run>;
|
|
@@ -980,19 +980,35 @@ export async function downloadOutputs(http, runId, options) {
|
|
|
980
980
|
}
|
|
981
981
|
/**
|
|
982
982
|
* Download only the event archive (the `events` namespace). Always includes
|
|
983
|
-
* typed `events.jsonl`.
|
|
983
|
+
* typed `events.jsonl` plus `manifest.json`.
|
|
984
984
|
*/
|
|
985
985
|
export async function downloadEvents(http, runId) {
|
|
986
986
|
const events = await listRunEvents(http, runId);
|
|
987
|
-
return zipEntries([
|
|
987
|
+
return zipEntries([
|
|
988
|
+
jsonlEntry("events.jsonl", events),
|
|
989
|
+
jsonEntry("manifest.json", {
|
|
990
|
+
runId,
|
|
991
|
+
namespace: "events",
|
|
992
|
+
files: [{ path: "events.jsonl", role: "typed_events", status: "present", recordCount: events.length }],
|
|
993
|
+
errors: []
|
|
994
|
+
})
|
|
995
|
+
]);
|
|
988
996
|
}
|
|
989
997
|
/**
|
|
990
998
|
* Download only the run record (the `metadata` namespace) as a zip
|
|
991
|
-
* containing `run.json`.
|
|
999
|
+
* containing `run.json` plus `manifest.json`.
|
|
992
1000
|
*/
|
|
993
1001
|
export async function downloadMetadata(http, runId) {
|
|
994
1002
|
const run = await getRun(http, runId);
|
|
995
|
-
return zipEntries([
|
|
1003
|
+
return zipEntries([
|
|
1004
|
+
jsonEntry("run.json", run),
|
|
1005
|
+
jsonEntry("manifest.json", {
|
|
1006
|
+
runId,
|
|
1007
|
+
namespace: "metadata",
|
|
1008
|
+
files: [{ path: "run.json", role: "run_metadata", status: "present" }],
|
|
1009
|
+
errors: []
|
|
1010
|
+
})
|
|
1011
|
+
]);
|
|
996
1012
|
}
|
|
997
1013
|
function zipEntries(entries) {
|
|
998
1014
|
assertRunRecordArchivePublicSafeV1(entries);
|
package/dist/cli.mjs
CHANGED
|
@@ -2104,6 +2104,7 @@ var HttpClient = class {
|
|
|
2104
2104
|
#apiKey;
|
|
2105
2105
|
#fetch;
|
|
2106
2106
|
#debug;
|
|
2107
|
+
#retryTransientGets;
|
|
2107
2108
|
constructor(options) {
|
|
2108
2109
|
if (!options.apiKey) {
|
|
2109
2110
|
throw new Error("HttpClient: apiKey is required");
|
|
@@ -2118,6 +2119,7 @@ var HttpClient = class {
|
|
|
2118
2119
|
this.#apiKey = options.apiKey;
|
|
2119
2120
|
this.#fetch = options.fetch ?? fetch;
|
|
2120
2121
|
this.#debug = options.debug;
|
|
2122
|
+
this.#retryTransientGets = resolveTransientGetRetry(options.retryTransientGets);
|
|
2121
2123
|
}
|
|
2122
2124
|
/** Emit a redacted round-trip trace (no auth header, body, or query). */
|
|
2123
2125
|
#trace(method, url, status, startedMs) {
|
|
@@ -2138,24 +2140,33 @@ var HttpClient = class {
|
|
|
2138
2140
|
headers["content-type"] = "application/json";
|
|
2139
2141
|
}
|
|
2140
2142
|
}
|
|
2141
|
-
const
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2143
|
+
const method = methodOf(init.method);
|
|
2144
|
+
const retry = retryForMethod(method, this.#retryTransientGets);
|
|
2145
|
+
const requestStartedMs = Date.now();
|
|
2146
|
+
for (let attempt = 1; attempt <= retry.maxAttempts; attempt += 1) {
|
|
2147
|
+
const startedMs = Date.now();
|
|
2148
|
+
try {
|
|
2149
|
+
const response = await this.#fetch(url, { ...init, headers });
|
|
2150
|
+
this.#trace(method, url, response.status, startedMs);
|
|
2151
|
+
const body = await readJson(response);
|
|
2152
|
+
if (!response.ok) {
|
|
2153
|
+
const errorBody = withResponseRequestId(body, response.headers);
|
|
2154
|
+
throw apiErrorFromResponse({
|
|
2155
|
+
status: response.status,
|
|
2156
|
+
body: errorBody,
|
|
2157
|
+
message: extractErrorMessage(errorBody)
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
return body;
|
|
2161
|
+
} catch (err2) {
|
|
2162
|
+
if (shouldRetryTransientRead(err2, retry, attempt)) {
|
|
2163
|
+
await sleepBeforeRetry(this.#debug, method, url, err2, retry, attempt);
|
|
2164
|
+
continue;
|
|
2165
|
+
}
|
|
2166
|
+
throw toNetworkError(method, url, err2, retry.maxAttempts > 1 ? attempt : void 0, Date.now() - requestStartedMs);
|
|
2167
|
+
}
|
|
2157
2168
|
}
|
|
2158
|
-
|
|
2169
|
+
throw new Error("HttpClient.request retry loop exhausted");
|
|
2159
2170
|
}
|
|
2160
2171
|
async download(path, init = {}, query = {}) {
|
|
2161
2172
|
const url = new URL(path.replace(/^\//, ""), this.#baseUrl);
|
|
@@ -2166,27 +2177,93 @@ var HttpClient = class {
|
|
|
2166
2177
|
authorization: `Bearer ${this.#apiKey}`,
|
|
2167
2178
|
...normalizeHeaders(init.headers)
|
|
2168
2179
|
};
|
|
2169
|
-
const
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2180
|
+
const method = methodOf(init.method);
|
|
2181
|
+
const retry = retryForMethod(method, this.#retryTransientGets);
|
|
2182
|
+
const requestStartedMs = Date.now();
|
|
2183
|
+
for (let attempt = 1; attempt <= retry.maxAttempts; attempt += 1) {
|
|
2184
|
+
const startedMs = Date.now();
|
|
2185
|
+
try {
|
|
2186
|
+
const response = await this.#fetch(url, { ...init, headers });
|
|
2187
|
+
this.#trace(method, url, response.status, startedMs);
|
|
2188
|
+
if (!response.ok) {
|
|
2189
|
+
const body = await readJson(response);
|
|
2190
|
+
const errorBody = withResponseRequestId(body, response.headers);
|
|
2191
|
+
throw apiErrorFromResponse({
|
|
2192
|
+
status: response.status,
|
|
2193
|
+
body: errorBody,
|
|
2194
|
+
message: extractErrorMessage(errorBody)
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2197
|
+
return { response };
|
|
2198
|
+
} catch (err2) {
|
|
2199
|
+
if (shouldRetryTransientRead(err2, retry, attempt)) {
|
|
2200
|
+
await sleepBeforeRetry(this.#debug, method, url, err2, retry, attempt);
|
|
2201
|
+
continue;
|
|
2202
|
+
}
|
|
2203
|
+
throw toNetworkError(method, url, err2, retry.maxAttempts > 1 ? attempt : void 0, Date.now() - requestStartedMs);
|
|
2204
|
+
}
|
|
2185
2205
|
}
|
|
2186
|
-
|
|
2206
|
+
throw new Error("HttpClient.download retry loop exhausted");
|
|
2187
2207
|
}
|
|
2188
2208
|
};
|
|
2189
|
-
|
|
2209
|
+
var DEFAULT_TRANSIENT_GET_RETRY = {
|
|
2210
|
+
maxAttempts: 3,
|
|
2211
|
+
baseDelayMs: 250,
|
|
2212
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
2213
|
+
};
|
|
2214
|
+
var TRANSIENT_READ_CODES = /* @__PURE__ */ new Set([
|
|
2215
|
+
"ConnectionRefused",
|
|
2216
|
+
"ECONNRESET",
|
|
2217
|
+
"ECONNREFUSED",
|
|
2218
|
+
"ETIMEDOUT",
|
|
2219
|
+
"EAI_AGAIN",
|
|
2220
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
2221
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
2222
|
+
"UND_ERR_SOCKET",
|
|
2223
|
+
"UND_ERR_BODY_TIMEOUT"
|
|
2224
|
+
]);
|
|
2225
|
+
function resolveTransientGetRetry(retry) {
|
|
2226
|
+
if (!retry)
|
|
2227
|
+
return null;
|
|
2228
|
+
if (retry === true)
|
|
2229
|
+
return DEFAULT_TRANSIENT_GET_RETRY;
|
|
2230
|
+
return {
|
|
2231
|
+
maxAttempts: Math.max(1, retry.maxAttempts ?? DEFAULT_TRANSIENT_GET_RETRY.maxAttempts),
|
|
2232
|
+
baseDelayMs: Math.max(0, retry.baseDelayMs ?? DEFAULT_TRANSIENT_GET_RETRY.baseDelayMs),
|
|
2233
|
+
sleep: retry.sleep ?? DEFAULT_TRANSIENT_GET_RETRY.sleep
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
function methodOf(method) {
|
|
2237
|
+
return (method ?? "GET").toUpperCase();
|
|
2238
|
+
}
|
|
2239
|
+
function retryForMethod(method, retry) {
|
|
2240
|
+
if (!retry || method !== "GET" && method !== "HEAD") {
|
|
2241
|
+
return { ...DEFAULT_TRANSIENT_GET_RETRY, maxAttempts: 1 };
|
|
2242
|
+
}
|
|
2243
|
+
return retry;
|
|
2244
|
+
}
|
|
2245
|
+
function shouldRetryTransientRead(err2, retry, attempt) {
|
|
2246
|
+
return attempt < retry.maxAttempts && transientReadErrorCode(err2) !== void 0;
|
|
2247
|
+
}
|
|
2248
|
+
async function sleepBeforeRetry(debug2, method, url, err2, retry, attempt) {
|
|
2249
|
+
const delayMs = retry.baseDelayMs * attempt;
|
|
2250
|
+
debug2?.(`[aex] ${method} ${url.pathname} transient ${transientReadErrorCode(err2) ?? "network"} attempt ${attempt}/${retry.maxAttempts}; retrying in ${delayMs}ms`);
|
|
2251
|
+
await retry.sleep(delayMs);
|
|
2252
|
+
}
|
|
2253
|
+
function transientReadErrorCode(err2) {
|
|
2254
|
+
if (err2 instanceof AexError)
|
|
2255
|
+
return void 0;
|
|
2256
|
+
if (err2?.name === "AbortError")
|
|
2257
|
+
return void 0;
|
|
2258
|
+
const code = extractErrorCode(err2);
|
|
2259
|
+
if (code && TRANSIENT_READ_CODES.has(code))
|
|
2260
|
+
return code;
|
|
2261
|
+
const text = err2 instanceof Error ? `${err2.name}: ${err2.message}` : String(err2);
|
|
2262
|
+
if (/fetch failed|socket hang up|other side closed|terminated|network.*reset/i.test(text))
|
|
2263
|
+
return "fetch";
|
|
2264
|
+
return void 0;
|
|
2265
|
+
}
|
|
2266
|
+
function toNetworkError(method, url, err2, attempts, elapsedMs) {
|
|
2190
2267
|
if (err2 instanceof AexError)
|
|
2191
2268
|
return err2;
|
|
2192
2269
|
if (err2?.name === "AbortError")
|
|
@@ -2195,7 +2272,9 @@ function toNetworkError(method, url, err2) {
|
|
|
2195
2272
|
method: (method ?? "GET").toUpperCase(),
|
|
2196
2273
|
host: url.host,
|
|
2197
2274
|
path: url.pathname,
|
|
2198
|
-
cause: err2
|
|
2275
|
+
cause: err2,
|
|
2276
|
+
...attempts !== void 0 ? { attempts } : {},
|
|
2277
|
+
...elapsedMs !== void 0 ? { elapsedMs } : {}
|
|
2199
2278
|
});
|
|
2200
2279
|
}
|
|
2201
2280
|
function normalizeHeaders(headers) {
|
|
@@ -3816,11 +3895,27 @@ async function downloadOutputs(http, runId, options) {
|
|
|
3816
3895
|
}
|
|
3817
3896
|
async function downloadEvents(http, runId) {
|
|
3818
3897
|
const events = await listRunEvents(http, runId);
|
|
3819
|
-
return zipEntries([
|
|
3898
|
+
return zipEntries([
|
|
3899
|
+
jsonlEntry("events.jsonl", events),
|
|
3900
|
+
jsonEntry("manifest.json", {
|
|
3901
|
+
runId,
|
|
3902
|
+
namespace: "events",
|
|
3903
|
+
files: [{ path: "events.jsonl", role: "typed_events", status: "present", recordCount: events.length }],
|
|
3904
|
+
errors: []
|
|
3905
|
+
})
|
|
3906
|
+
]);
|
|
3820
3907
|
}
|
|
3821
3908
|
async function downloadMetadata(http, runId) {
|
|
3822
3909
|
const run = await getRun(http, runId);
|
|
3823
|
-
return zipEntries([
|
|
3910
|
+
return zipEntries([
|
|
3911
|
+
jsonEntry("run.json", run),
|
|
3912
|
+
jsonEntry("manifest.json", {
|
|
3913
|
+
runId,
|
|
3914
|
+
namespace: "metadata",
|
|
3915
|
+
files: [{ path: "run.json", role: "run_metadata", status: "present" }],
|
|
3916
|
+
errors: []
|
|
3917
|
+
})
|
|
3918
|
+
]);
|
|
3824
3919
|
}
|
|
3825
3920
|
function zipEntries(entries) {
|
|
3826
3921
|
assertRunRecordArchivePublicSafeV1(entries);
|
|
@@ -4186,6 +4281,7 @@ function makeHttpClient(io2, flags) {
|
|
|
4186
4281
|
return new HttpClient({
|
|
4187
4282
|
baseUrl: flags.aexUrl,
|
|
4188
4283
|
apiKey: flags.apiKey,
|
|
4284
|
+
retryTransientGets: true,
|
|
4189
4285
|
fetch: io2.fetchImpl,
|
|
4190
4286
|
// `--debug`: route the transport's redacted per-request traces to stderr.
|
|
4191
4287
|
...flags.debug ? { debug: (line) => io2.stderr(`${line}
|
package/dist/cli.mjs.sha256
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
383e87cd6909a7a5eb73b90a77c12b6adf78491678cdc14612d5cc878971458e cli.mjs
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/dist/version.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,SAAS,CAAC"}
|
package/docs/outputs.md
CHANGED
|
@@ -38,8 +38,8 @@ A session's downloadable content is organised into three logical namespaces, eac
|
|
|
38
38
|
| Namespace | What it holds | Verb | CLI |
|
|
39
39
|
| --- | --- | --- | --- |
|
|
40
40
|
| `outputs` | The session's real deliverables. | `session.outputs().download()` | `download <id> --only outputs` |
|
|
41
|
-
| `events` | Typed event-channel records (`events.jsonl`). | `session.events().download()` | `download <id> --only events` |
|
|
42
|
-
| `metadata` | The session record (`run.json`). | `session.downloadMetadata()` | `download <id> --only metadata` |
|
|
41
|
+
| `events` | Typed event-channel records (`events.jsonl`) plus a namespace manifest. | `session.events().download()` | `download <id> --only events` |
|
|
42
|
+
| `metadata` | The session record (`run.json`) plus a namespace manifest. | `session.downloadMetadata()` | `download <id> --only metadata` |
|
|
43
43
|
|
|
44
44
|
Platform diagnostics are stored outside the public archive under `runs/<runId>/internal/logs/` for internal/admin access only. They are not exposed by the SDK download helpers or the public CLI.
|
|
45
45
|
|
|
@@ -66,7 +66,7 @@ manifest.json # RunRecordManifestV1
|
|
|
66
66
|
| `outputs[]` | `{ id, filename, sizeBytes?, contentType? }` — one row per file successfully written under `outputs/`. |
|
|
67
67
|
| `errors[]` | `{ namespace, id, filename, message }` — per-artifact byte fetches that failed during assembly. Best-effort: a failure records an entry here and is skipped from the tree rather than aborting the whole zip. |
|
|
68
68
|
|
|
69
|
-
The single-namespace verbs return the same per-file bytes at the zip root (e.g. `session.outputs().download()` -> `report.txt` +
|
|
69
|
+
The single-namespace verbs return the same per-file bytes at the zip root (e.g. `session.outputs().download()` -> `report.txt` + `manifest.json`; `session.events().download()` -> `events.jsonl` + `manifest.json`; `session.downloadMetadata()` -> `run.json` + `manifest.json`).
|
|
70
70
|
|
|
71
71
|
## Downloading one output
|
|
72
72
|
|
|
@@ -253,7 +253,7 @@ Capture notes:
|
|
|
253
253
|
|
|
254
254
|
## Runs without explicit `outputs.allowedDirs`
|
|
255
255
|
|
|
256
|
-
Metadata still gets the full treatment. aex captures every regular file the run created or modified outside mandatory platform excludes. A run that produces no files still returns a zip with `run.json`, `events.jsonl`, and
|
|
256
|
+
Metadata still gets the full treatment. aex captures every regular file the run created or modified outside mandatory platform excludes. A run that produces no files still returns a whole-session zip with `metadata/run.json`, `events/events.jsonl`, and `manifest.json` (manifest `outputs: []`).
|
|
257
257
|
|
|
258
258
|
## Mid-session download semantics
|
|
259
259
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aexhq/sdk",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.11",
|
|
4
4
|
"description": "TypeScript SDK for running autonomous agent sessions across providers (Anthropic, OpenAI, DeepSeek, Gemini, Mistral) behind one interface.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|