@aexhq/sdk 0.40.10 → 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.
@@ -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
@@ -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 startedMs = Date.now();
56
- let response;
57
- try {
58
- response = await this.#fetch(url, { ...init, headers });
59
- }
60
- catch (err) {
61
- throw toNetworkError(init.method, url, err);
62
- }
63
- this.#trace(init.method, url, response.status, startedMs);
64
- const body = await readJson(response);
65
- if (!response.ok) {
66
- const errorBody = withResponseRequestId(body, response.headers);
67
- throw apiErrorFromResponse({
68
- status: response.status,
69
- body: errorBody,
70
- message: extractErrorMessage(errorBody)
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
- return body;
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 startedMs = Date.now();
85
- let response;
86
- try {
87
- response = await this.#fetch(url, { ...init, headers });
88
- }
89
- catch (err) {
90
- throw toNetworkError(init.method, url, err);
91
- }
92
- this.#trace(init.method, url, response.status, startedMs);
93
- if (!response.ok) {
94
- const body = await readJson(response);
95
- const errorBody = withResponseRequestId(body, response.headers);
96
- throw apiErrorFromResponse({
97
- status: response.status,
98
- body: errorBody,
99
- message: extractErrorMessage(errorBody)
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
- return { response };
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) {
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 startedMs = Date.now();
2142
- let response;
2143
- try {
2144
- response = await this.#fetch(url, { ...init, headers });
2145
- } catch (err2) {
2146
- throw toNetworkError(init.method, url, err2);
2147
- }
2148
- this.#trace(init.method, url, response.status, startedMs);
2149
- const body = await readJson(response);
2150
- if (!response.ok) {
2151
- const errorBody = withResponseRequestId(body, response.headers);
2152
- throw apiErrorFromResponse({
2153
- status: response.status,
2154
- body: errorBody,
2155
- message: extractErrorMessage(errorBody)
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
- return body;
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 startedMs = Date.now();
2170
- let response;
2171
- try {
2172
- response = await this.#fetch(url, { ...init, headers });
2173
- } catch (err2) {
2174
- throw toNetworkError(init.method, url, err2);
2175
- }
2176
- this.#trace(init.method, url, response.status, startedMs);
2177
- if (!response.ok) {
2178
- const body = await readJson(response);
2179
- const errorBody = withResponseRequestId(body, response.headers);
2180
- throw apiErrorFromResponse({
2181
- status: response.status,
2182
- body: errorBody,
2183
- message: extractErrorMessage(errorBody)
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
- return { response };
2206
+ throw new Error("HttpClient.download retry loop exhausted");
2187
2207
  }
2188
2208
  };
2189
- function toNetworkError(method, url, err2) {
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) {
@@ -4202,6 +4281,7 @@ function makeHttpClient(io2, flags) {
4202
4281
  return new HttpClient({
4203
4282
  baseUrl: flags.aexUrl,
4204
4283
  apiKey: flags.apiKey,
4284
+ retryTransientGets: true,
4205
4285
  fetch: io2.fetchImpl,
4206
4286
  // `--debug`: route the transport's redacted per-request traces to stderr.
4207
4287
  ...flags.debug ? { debug: (line) => io2.stderr(`${line}
@@ -1 +1 @@
1
- feca3311d49affc9dd305e9c1a8da4faee1c604662ebb34abc142c8f1276635c cli.mjs
1
+ 383e87cd6909a7a5eb73b90a77c12b6adf78491678cdc14612d5cc878971458e cli.mjs
package/dist/version.d.ts CHANGED
@@ -6,4 +6,4 @@
6
6
  *
7
7
  * Used by the (future) User-Agent header on outbound SDK requests.
8
8
  */
9
- export declare const SDK_VERSION = "0.40.10";
9
+ export declare const SDK_VERSION = "0.40.11";
package/dist/version.js CHANGED
@@ -6,5 +6,5 @@
6
6
  *
7
7
  * Used by the (future) User-Agent header on outbound SDK requests.
8
8
  */
9
- export const SDK_VERSION = "0.40.10";
9
+ export const SDK_VERSION = "0.40.11";
10
10
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexhq/sdk",
3
- "version": "0.40.10",
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": {