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 +9 -1
- package/index.js +20 -4
- package/package.json +2 -1
- package/reset-retry.js +47 -0
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.
|
|
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":
|
|
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":
|
|
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.
|
|
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
|
+
}
|