agent402-client 0.8.20 → 0.8.22

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 CHANGED
@@ -166,6 +166,14 @@ What the server does with it depends on how the call was paid:
166
166
  route with the same body, from its first answer inside the replay window,
167
167
  with no second settlement, and marks it `X-Replayed-Answer: 1` (identity-bound
168
168
  routes, routes with a lasting effect and streamed calls excepted).
169
+ - **A connection reset before any answer:** the SDK's own requests (proof-of-work,
170
+ credits, discovery) are resent once when the connection failed before the
171
+ server answered, for example on a kept-alive socket the edge had already
172
+ closed (`retryResets: false` turns this off). Your `payFetch` is not wrapped,
173
+ because a retry over a payment wrapper would sign a fresh authorization. To get
174
+ the same resend on the wallet path, put it under the wrapper:
175
+ `wrapFetchWithPayment(resetRetryFetch(distrustFetch()), client)`. The resend
176
+ carries the same signed header, and one authorization settles at most once.
169
177
  - **Circle Gateway batched option:** a payment the server has not had confirmed
170
178
  by Gateway yet is answered `402` with reason `settlement-pending`; the SDK
171
179
  resends the same signed header after the server's `Retry-After` (never a new
@@ -269,7 +277,7 @@ that runs as one.
269
277
 
270
278
  | Method | What |
271
279
  |---|---|
272
- | `new Agent402({ baseUrl?, fetch?, x402Client?, creditsKey?, gateway?, cache?, fetchImpl?, maxPerCallUsd?, dailyLimitUsd?, maxPerHostUsd?, payNetworks?, maxResponseBytes?, outputValidator?, respondAsync?, asyncTimeoutMs?, onProgress? })` | `fetch` is your x402- or MPP-wrapped fetch for paid tools (optional); `x402Client` is the `@x402/core` client that `fetch` wraps, so the caps are enforced on the payment that is signed (see below); `payNetworks` (for example `["base"]`, the names `withNetworkPreference` takes) tells the caps which chains a pre-wrapped `fetch` without an `x402Client` pays on, so a call is counted at the dearest option on those chains (when none of a 402's options is on them, the dearest of all counts); `creditsKey` is a prepaid credits key (`a402_...`) used for paid tools when no `fetch` is given; `gateway` (`{ signer, networks? }`) pays the Circle Gateway batched accept where a 402 offers one, from the signer's Gateway deposit (see above); `cache` (default `true`) memoizes deterministic results; the three USD caps set optional spending limits (see below); `maxResponseBytes` (default 32MB, `null` to disable) refuses an oversized response body before it is parsed; `outputValidator` binds a caller-supplied delivery check; `respondAsync` (default `"auto"`), `asyncTimeoutMs` (default 15 minutes) and `onProgress` control slow routes run as jobs (see above) |
280
+ | `new Agent402({ baseUrl?, fetch?, x402Client?, creditsKey?, gateway?, cache?, fetchImpl?, maxPerCallUsd?, dailyLimitUsd?, maxPerHostUsd?, payNetworks?, maxResponseBytes?, outputValidator?, respondAsync?, asyncTimeoutMs?, onProgress?, userAgent? })` | `fetch` is your x402- or MPP-wrapped fetch for paid tools (optional); `x402Client` is the `@x402/core` client that `fetch` wraps, so the caps are enforced on the payment that is signed (see below); `payNetworks` (for example `["base"]`, the names `withNetworkPreference` takes) tells the caps which chains a pre-wrapped `fetch` without an `x402Client` pays on, so a call is counted at the dearest option on those chains (when none of a 402's options is on them, the dearest of all counts); `creditsKey` is a prepaid credits key (`a402_...`) used for paid tools when no `fetch` is given; `gateway` (`{ signer, networks? }`) pays the Circle Gateway batched accept where a 402 offers one, from the signer's Gateway deposit (see above); `cache` (default `true`) memoizes deterministic results; the three USD caps set optional spending limits (see below); `maxResponseBytes` (default 32MB, `null` to disable) refuses an oversized response body before it is parsed; `outputValidator` binds a caller-supplied delivery check; `respondAsync` (default `"auto"`), `asyncTimeoutMs` (default 15 minutes) and `onProgress` control slow routes run as jobs (see above); `userAgent` (`"name/version"`) is a calling package's own product token, sent before `agent402-client/<version>` in the User-Agent |
273
281
  | `await a.find(task, { k = 5 })` | Resolve a plain-language task to the best-matching tools on **this host** (route, price, schema, example) |
274
282
  | `await a.route(task, { k?, include?, network? })` | Cross-seller Smart Order Router: rank eligible/routable seller rows from the host's current index (free, read-only; opaque results may include server-reported `executeVia`) |
275
283
  | `await a.findWorkflows(task, { k = 2 })` | Resolve a task to matching multi-tool workflow templates (skill packs) |
package/index.js CHANGED
@@ -23,6 +23,8 @@ import { quoteOf, guardSigning, networkIdsOf, acceptUsd } from "./known-assets.j
23
23
  export { KNOWN_ASSETS, NETWORK_NAMES, knownAsset, acceptUsd, networkIdsOf } from "./known-assets.js";
24
24
  import { distrustFetch } from "./tls-distrust.js";
25
25
  export { distrustFetch, distrustDispatcher, distrustCheckServerIdentity, DISTRUSTED_SPKI_SHA256 } from "./tls-distrust.js";
26
+ export { resetRetryFetch, isConnectionReset } from "./reset-retry.js";
27
+ import { resetRetryFetch } from "./reset-retry.js";
26
28
  import { awaitJob, shouldRequestAsync, AsyncJobPendingError, DEFAULT_ASYNC_TIMEOUT_MS } from "./async-job.js";
27
29
  export { AsyncJobPendingError } from "./async-job.js";
28
30
 
@@ -30,8 +32,13 @@ export { AsyncJobPendingError } from "./async-job.js";
30
32
  // `User-Agent: agent402-client/<version>` - a standard header, no extra
31
33
  // network calls - so a seller can attribute traffic (and settled payments)
32
34
  // to this SDK. Product token only; nothing about the caller rides along.
33
- const VERSION = "0.8.20";
35
+ const VERSION = "0.8.22";
34
36
  const USER_AGENT = `agent402-client/${VERSION}`;
37
+ // A package built on this SDK (an adapter) names itself first: its own
38
+ // product token, then this one. Anything that is not one well-formed token
39
+ // is ignored and the SDK's own token is sent alone.
40
+ const PRODUCT_TOKEN_RE = /^[A-Za-z0-9.-]{1,40}\/[A-Za-z0-9.-]{1,20}$/;
41
+ const userAgentOf = (product) => (typeof product === "string" && PRODUCT_TOKEN_RE.test(product) ? `${product} ${USER_AGENT}` : USER_AGENT);
35
42
  // 32MB: about a hundred times any realistic response from this catalog (the
36
43
  // largest is a base64 image at a few MB), so it cannot break a legitimate
37
44
  // caller, while still stopping the class it exists for. A ceiling that is off
@@ -120,8 +127,12 @@ export class Agent402 {
120
127
  * @param {typeof fetch} [opts.fetch] an x402-wrapped fetch for wallet-only tools (optional)
121
128
  * @param {boolean} [opts.cache=true] cache results in memory (deterministic tools)
122
129
  * @param {typeof fetch} [opts.fetchImpl] plain fetch (defaults to global fetch through distrustFetch)
130
+ * @param {string} [opts.userAgent] a calling package's own product token ("name/version"),
131
+ * sent before agent402-client's in the User-Agent
123
132
  * @param {boolean} [opts.tlsDistrust=true] send the default fetch through distrustFetch (refuses a TLS
124
133
  * chain through a distrusted intermediate CA); false sends it as is
134
+ * @param {boolean} [opts.retryResets=true] resend the SDK's own request once when its connection failed
135
+ * before any answer (see resetRetryFetch); false sends each once
125
136
  * @param {number} [opts.maxPerCallUsd] hard ceiling on a single paid call (USD); over → SpendingLimitError before paying
126
137
  * @param {number} [opts.dailyLimitUsd] hard ceiling on rolling-24h paid spend (USD)
127
138
  * @param {number} [opts.maxPerHostUsd] hard ceiling on rolling-24h paid spend to one seller host (USD)
@@ -156,7 +167,8 @@ export class Agent402 {
156
167
  constructor({ baseUrl = "https://agent402.tools", fetch: payFetch, cache = true, fetchImpl = undefined, tlsDistrust = true,
157
168
  maxPerCallUsd = null, dailyLimitUsd = null, maxPerHostUsd = null, creditsKey = null, x402Client = null, payNetworks = null,
158
169
  maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES, outputValidator = null,
159
- respondAsync = "auto", asyncTimeoutMs = DEFAULT_ASYNC_TIMEOUT_MS, onProgress = null, gateway = null } = {}) {
170
+ respondAsync = "auto", asyncTimeoutMs = DEFAULT_ASYNC_TIMEOUT_MS, onProgress = null, gateway = null, userAgent = null, retryResets = true } = {}) {
171
+ this.userAgent = userAgentOf(userAgent);
160
172
  this.respondAsync = respondAsync;
161
173
  this.gateway = normalizeGateway(gateway);
162
174
  this.asyncTimeoutMs = numOrNull(asyncTimeoutMs) ?? DEFAULT_ASYNC_TIMEOUT_MS;
@@ -167,11 +179,15 @@ export class Agent402 {
167
179
  if (typeof fetchImpl === "function" && tlsDistrust !== false) fetchImpl = distrustFetch(fetchImpl);
168
180
  }
169
181
  if (typeof fetchImpl !== "function") throw new Error("No fetch available - pass { fetchImpl } on Node < 18");
182
+ // The SDK's own requests resend once when the connection failed before any
183
+ // answer (a closed keep-alive socket). A caller's payFetch is not wrapped:
184
+ // retrying over a payment wrapper would sign a fresh authorization.
185
+ if (retryResets !== false) fetchImpl = resetRetryFetch(fetchImpl);
170
186
  this.baseUrl = String(baseUrl).replace(/\/$/, "");
171
187
  this.payFetch = payFetch || null;
172
188
  // Wrap the plain fetch so every request identifies the SDK (see USER_AGENT
173
189
  // above). Caller-supplied init.headers still win on a key collision.
174
- this.f = (url, init = {}) => fetchImpl(url, { ...init, headers: { "User-Agent": USER_AGENT, ...(init.headers || {}) } });
190
+ this.f = (url, init = {}) => fetchImpl(url, { ...init, headers: { "User-Agent": this.userAgent, ...(init.headers || {}) } });
175
191
  this._catalog = null;
176
192
  // The wait between shed-503 retries of a discovery read (tests replace it).
177
193
  this._sleep = sleepMs;
@@ -511,7 +527,7 @@ export class Agent402 {
511
527
  const send = async (extraHeaders = {}, useFetch = this.f, { paid = false } = {}) => {
512
528
  // UA set here too (not only in the this.f wrapper) so the x402 payFetch
513
529
  // path - the one that settles real payments - always carries it.
514
- const headers = { "User-Agent": USER_AGENT, "Idempotency-Key": idem, ...extraHeaders };
530
+ const headers = { "User-Agent": this.userAgent, "Idempotency-Key": idem, ...extraHeaders };
515
531
  let url = `${this.baseUrl}${tool.path}`;
516
532
  // A request that can carry a payment never follows a redirect: followed,
517
533
  // it would answer a 402 from another origin with a signature to that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent402-client",
3
- "version": "0.8.20",
3
+ "version": "0.8.22",
4
4
  "description": "Agent402 buyer SDK: resolve a task to a tool across 500+ pay-per-call endpoints and call it with payment handled - free via proof-of-work, or over x402 or MPP (a stock mppx fetch works) with a wallet you keep; the buy side of Agentic Finance. Caching, idempotent retries (Idempotency-Key), non-custodial, zero dependencies.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -12,6 +12,7 @@
12
12
  "async-job.js",
13
13
  "known-assets.js",
14
14
  "tls-distrust.js",
15
+ "reset-retry.js",
15
16
  "README.md"
16
17
  ],
17
18
  "engines": {
package/reset-retry.js ADDED
@@ -0,0 +1,47 @@
1
+ // resetRetryFetch(baseFetch) returns a fetch that resends a request ONCE when
2
+ // its connection failed before any response arrived: a kept-alive socket the
3
+ // server's edge already closed answers the next write with ECONNRESET (or
4
+ // undici's "other side closed"), and the request never reached the server.
5
+ //
6
+ // Only a transport error is retried, never an HTTP answer, and only for a
7
+ // body that can be sent again (none, a string, bytes, URLSearchParams, a
8
+ // Blob); a streamed body is not. The resend carries the same headers, so a
9
+ // signed payment is resent as signed: an EIP-3009 authorization's nonce can
10
+ // settle only once, and the server answers a re-sent signed payment from its
11
+ // first answer, so the resend can never pay twice. Wrap it UNDER a payment
12
+ // wrapper (wrapFetchWithPayment(resetRetryFetch(fetch), client)), never over
13
+ // one: over a wrapper, a retry would sign a fresh authorization.
14
+ const RESET_CODES = new Set(["ECONNRESET", "EPIPE", "UND_ERR_SOCKET", "UND_ERR_CLOSED", "ECONNABORTED"]);
15
+
16
+ /** Did this fetch error fail on the connection before any response? */
17
+ export function isConnectionReset(err) {
18
+ let e = err;
19
+ for (let i = 0; i < 5 && e; i++) {
20
+ if (RESET_CODES.has(e.code)) return true;
21
+ if (/other side closed|socket hang up/i.test(String(e.message || ""))) return true;
22
+ e = e.cause;
23
+ }
24
+ return false;
25
+ }
26
+
27
+ function replayableBody(body) {
28
+ if (body === undefined || body === null || typeof body === "string") return true;
29
+ if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return true;
30
+ if (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams) return true;
31
+ if (typeof Blob !== "undefined" && body instanceof Blob) return true;
32
+ return false;
33
+ }
34
+
35
+ export function resetRetryFetch(baseFetch = globalThis.fetch, { pauseMs = 300 } = {}) {
36
+ if (typeof baseFetch !== "function") throw new Error("resetRetryFetch needs a fetch function");
37
+ return async function fetchWithResetRetry(input, init) {
38
+ try {
39
+ return await baseFetch(input, init);
40
+ } catch (e) {
41
+ const isRequest = typeof Request !== "undefined" && input instanceof Request;
42
+ if (!isConnectionReset(e) || init?.signal?.aborted || isRequest || !replayableBody(init?.body)) throw e;
43
+ await new Promise((r) => setTimeout(r, pauseMs));
44
+ return baseFetch(input, init);
45
+ }
46
+ };
47
+ }