@nuanu-ai/agentify 0.5.0 → 0.5.1
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/backoff.d.ts +17 -39
- package/dist/backoff.js +17 -39
- package/dist/check-card.d.ts +15 -24
- package/dist/check-card.js +15 -24
- package/dist/cli.d.ts +6 -16
- package/dist/cli.js +6 -16
- package/dist/client.d.ts +123 -251
- package/dist/client.js +113 -255
- package/dist/contract.d.ts +7 -10
- package/dist/contract.js +7 -10
- package/dist/index.d.ts +16 -28
- package/dist/index.js +16 -28
- package/dist/schema.d.ts +15 -37
- package/dist/schema.js +12 -30
- package/dist/transport.d.ts +43 -122
- package/dist/transport.js +53 -144
- package/dist/verify.d.ts +8 -17
- package/dist/verify.js +15 -58
- package/dist/worker.d.ts +93 -236
- package/dist/worker.js +111 -255
- package/package.json +2 -2
package/dist/worker.js
CHANGED
|
@@ -1,54 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The one loop that carries everything the merchant's process receives
|
|
2
|
+
* The one loop that carries everything the merchant's process receives, over
|
|
3
|
+
* HTTP long polling: the worker asks for the next batch, and the gateway holds
|
|
4
|
+
* the request until something arrives or its own window closes. An empty batch
|
|
5
|
+
* is the ordinary answer to a quiet window.
|
|
3
6
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* window and not a failure. There is one loop and not three, because the
|
|
9
|
-
* stream carries three kinds on one connection and the portal promises the
|
|
10
|
-
* merchant one subscription.
|
|
7
|
+
* An order goes to the merchant's handler, and what the handler returns is
|
|
8
|
+
* posted to the order's answer route in every mode. A price question goes to
|
|
9
|
+
* the price handler, and its answer to the quote route against the question's
|
|
10
|
+
* `price_id`. An event is handed over and nothing is sent back.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* merchant's answer; in the asynchronous one it is usually an acceptance, and
|
|
20
|
-
* the goods follow later through the explicit `deliver` call, which is the
|
|
21
|
-
* merchant's to make and not this loop's.
|
|
22
|
-
*
|
|
23
|
-
* A price question is given to the price handler and the answer goes to the
|
|
24
|
-
* quote route against the `price_id` the question carried.
|
|
25
|
-
*
|
|
26
|
-
* An event is handed over and nothing is sent back. An event notifies, it does
|
|
27
|
-
* not ask for work.
|
|
28
|
-
*
|
|
29
|
-
* What happens when a handler throws is the design decision the rest follows
|
|
30
|
-
* from: nothing is sent. An order nobody answered comes back on its own,
|
|
31
|
-
* because the gateway's own order machine is watching for that answer and
|
|
32
|
-
* decides there should be another attempt — that is the machine's meaning of
|
|
33
|
-
* "not delivered". A retry written on this side would be a second machine with
|
|
34
|
-
* a second opinion about how many times a merchant's handler may run, and the
|
|
35
|
-
* two would disagree the first time one of them restarted. The same holds for a
|
|
36
|
-
* handler that answers with something the contract refuses, and for an envelope
|
|
37
|
-
* that arrives with no handler registered for its kind: the merchant is told
|
|
38
|
-
* through the problem channel, and an order or a price question comes back on
|
|
39
|
-
* its own.
|
|
40
|
-
*
|
|
41
|
-
* An event is the exception and it is worth knowing about. It carries no
|
|
42
|
-
* acknowledgement, so nothing here can ask for one again and nothing on the
|
|
43
|
-
* other side is waiting for a reply to notice that it went unanswered. The
|
|
44
|
-
* contract makes no promise that one is ever sent a second time, and whether a
|
|
45
|
-
* particular gateway sends one anyway is that gateway's business. A handler
|
|
46
|
-
* that throws on an event has lost what it carried.
|
|
47
|
-
*
|
|
48
|
-
* The transport failures are the one thing this loop does retry, because there
|
|
49
|
-
* is nothing else to do with them: a poll that did not reach the gateway
|
|
50
|
-
* delivered no envelope to anybody. That retry is the backoff in
|
|
51
|
-
* `backoff.ts` and nothing more.
|
|
12
|
+
* When a handler throws, nothing is sent and nothing is retried here: an
|
|
13
|
+
* unanswered order comes back because the gateway decides on another attempt,
|
|
14
|
+
* and a second retry policy on this side would disagree with it. The same
|
|
15
|
+
* holds for an answer the contract refuses and for a kind with no handler; the
|
|
16
|
+
* merchant is told through the problem reporter. An event is not sent again,
|
|
17
|
+
* so a handler that throws on one has lost what it carried. The only retry
|
|
18
|
+
* here is of a poll that failed, with the backoff in `backoff.ts`.
|
|
52
19
|
*/
|
|
53
20
|
import { HandlerAnswerSchema, PROTOTYPE_KEY_IS_DROPPED, priceProblemsOf, QuoteResponseSchema, } from "@nuanu-ai/agentify-contracts";
|
|
54
21
|
import { retryDelayMs } from "./backoff.js";
|
|
@@ -56,26 +23,17 @@ import { contractVersion, speaksContract } from "./contract.js";
|
|
|
56
23
|
import { describeProblems, problemsOf } from "./schema.js";
|
|
57
24
|
import { callRoute, REACH, whatIsKnown } from "./transport.js";
|
|
58
25
|
/**
|
|
59
|
-
* What follows from a failed answer for the order it was about.
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* unanswered, so it comes back. An answer that reached us, or that vanished
|
|
64
|
-
* into silence, may have closed the order already — saying it will be
|
|
65
|
-
* delivered again would be promising a redelivery we have no grounds to
|
|
66
|
-
* expect, and a merchant who acted on that promise would be waiting for an
|
|
67
|
-
* order that never comes.
|
|
26
|
+
* What follows from a failed answer for the order it was about. Only an
|
|
27
|
+
* answer that never left is certain to leave the order unanswered, so only
|
|
28
|
+
* then is a redelivery promised; one that reached the gateway or vanished may
|
|
29
|
+
* already have closed the order.
|
|
68
30
|
*/
|
|
69
31
|
const andSoTheOrder = (failure) => failure.reach === REACH.NOT_RECEIVED
|
|
70
32
|
? ", so the order counts as undelivered: it is sent again while it has attempts left and its deadline allows, and otherwise ends undelivered"
|
|
71
33
|
: ", so whether the order is delivered again is not something this side can say";
|
|
72
34
|
/**
|
|
73
|
-
* The things the worker
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* They are one vocabulary rather than several channels because every one of
|
|
77
|
-
* them has the same shape of consequence — something did not get through — and
|
|
78
|
-
* a merchant who wants to count them wants to count them together.
|
|
35
|
+
* The things the worker tells the merchant about, under names their code can
|
|
36
|
+
* branch on, through the reporter registered with `on('problem')`.
|
|
79
37
|
*/
|
|
80
38
|
export const WORKER_PROBLEM_KINDS = Object.freeze({
|
|
81
39
|
/** The gateway speaks another dialect of the contract. The loop stops. */
|
|
@@ -100,62 +58,35 @@ export const WORKER_PROBLEM_KINDS = Object.freeze({
|
|
|
100
58
|
WORKER_FAILED: "worker_failed",
|
|
101
59
|
/** A delivery carried the one field name this contract removes in silence. */
|
|
102
60
|
DELIVERY_FIELD_DROPPED: "delivery_field_dropped",
|
|
61
|
+
/** An acceptance was given an argument, which it does not carry. The order is taken on. */
|
|
62
|
+
ACCEPTANCE_ARGUMENT_IGNORED: "acceptance_argument_ignored",
|
|
103
63
|
});
|
|
104
64
|
/**
|
|
105
|
-
* Calls a merchant's reporter and lets nothing it does escape.
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* otherwise unwind the worker and, from the worker's own last-resort handler,
|
|
111
|
-
* escape as an unhandled rejection and take the host process down with it.
|
|
112
|
-
* There is nowhere left to report a reporter that throws, so it is swallowed.
|
|
113
|
-
*
|
|
114
|
-
* Both ways of throwing, and the second is the one that hid. A reporter
|
|
115
|
-
* declared `void` may still be an `async` function — TypeScript allows it, and
|
|
116
|
-
* a merchant shipping problems to an HTTP logger will write one — and such a
|
|
117
|
-
* reporter does not throw, it returns a promise that rejects a turn later, past
|
|
118
|
-
* this `catch`, with nothing holding it. That is the unhandled rejection this
|
|
119
|
-
* comment claims not to allow, arriving by the door the `try` does not cover.
|
|
120
|
-
* So what comes back is settled too, and its failure dropped in the same
|
|
121
|
-
* silence and for the same reason.
|
|
122
|
-
*
|
|
123
|
-
* It is at the top of the file rather than inside the loop because the loop is
|
|
124
|
-
* not the only thing that reports: a merchant calling `deliver` on an order
|
|
125
|
-
* directly has a reporter too, and the same reasoning covers it exactly.
|
|
65
|
+
* Calls a merchant's reporter and lets nothing it does escape. A reporter that
|
|
66
|
+
* throws, or that is `async` and rejects a turn later, would otherwise end the
|
|
67
|
+
* loop or surface as an unhandled rejection and take the host process down;
|
|
68
|
+
* there is nowhere left to report it, so it is swallowed. A merchant calling
|
|
69
|
+
* `deliver` directly reports through this too.
|
|
126
70
|
*/
|
|
127
71
|
export const reportSafely = (reporter, problem) => {
|
|
128
72
|
try {
|
|
129
|
-
// Anything with a `then
|
|
130
|
-
//
|
|
131
|
-
// the same way and must be settled in exactly the same silence.
|
|
73
|
+
// Anything with a `then`, not only a real Promise: another library's
|
|
74
|
+
// promise, or one from another realm, fails the same way.
|
|
132
75
|
const reporting = reporter(problem);
|
|
133
76
|
if (typeof reporting?.then === "function") {
|
|
134
77
|
void Promise.resolve(reporting).catch(() => { });
|
|
135
78
|
}
|
|
136
79
|
}
|
|
137
80
|
catch {
|
|
138
|
-
// Nowhere to say it.
|
|
81
|
+
// Nowhere to say it.
|
|
139
82
|
}
|
|
140
83
|
};
|
|
141
84
|
/**
|
|
142
|
-
* The
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* has no way to know that what it returned is not what went out. The contracts
|
|
148
|
-
* package names the delivery as the place the loss lands in what the agent is
|
|
149
|
-
* handed — `DeliverySchema` in its `handler.ts` — and a delivery is written
|
|
150
|
-
* here, so this file says it too.
|
|
151
|
-
*
|
|
152
|
-
* No card can declare such a field, so nothing legitimate reaches here; a
|
|
153
|
-
* merchant only meets it by delivering a name nobody asked for.
|
|
154
|
-
*
|
|
155
|
-
* It is one function because there are two roads to the same gateway and the
|
|
156
|
-
* loss is the same on both: the worker answering with what a handler returned,
|
|
157
|
-
* and a merchant calling `deliver` on an order himself. Written twice, one of
|
|
158
|
-
* them would drift or, as it did, never be written at all.
|
|
85
|
+
* The warning for a delivery carrying a field named `__proto__`, which is
|
|
86
|
+
* removed while the delivery is parsed: the agent never receives it, and
|
|
87
|
+
* without this the merchant would not know. No card can declare such a field.
|
|
88
|
+
* Both roads to the gateway use it, the worker answering for a handler and a
|
|
89
|
+
* merchant calling `deliver` directly.
|
|
159
90
|
*/
|
|
160
91
|
export const droppedFieldWarning = (orderId, delivered) => {
|
|
161
92
|
if (delivered === undefined || !Object.hasOwn(delivered, PROTOTYPE_KEY_IS_DROPPED)) {
|
|
@@ -169,18 +100,23 @@ export const droppedFieldWarning = (orderId, delivered) => {
|
|
|
169
100
|
};
|
|
170
101
|
};
|
|
171
102
|
/**
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
103
|
+
* The warning reported when `accepted` or `accept` is given an argument.
|
|
104
|
+
* TypeScript refuses one, but JavaScript code can pass it, and nothing passed
|
|
105
|
+
* reaches the gateway. The order is accepted all the same, because refusing
|
|
106
|
+
* would strand an order its buyer has paid for.
|
|
107
|
+
*/
|
|
108
|
+
export const acceptanceArgumentWarning = (orderId) => ({
|
|
109
|
+
kind: WORKER_PROBLEM_KINDS.ACCEPTANCE_ARGUMENT_IGNORED,
|
|
110
|
+
fatal: false,
|
|
111
|
+
subject: orderId,
|
|
112
|
+
message: `order ${orderId} was accepted with an argument, and an acceptance carries nothing: whatever was passed was not sent (eta_seconds was removed), so call accepted() or accept() with nothing in it`,
|
|
113
|
+
});
|
|
114
|
+
/**
|
|
115
|
+
* Which registration answers each kind on the stream, by the word a merchant
|
|
116
|
+
* passes to `on`. Everything that has to know the stream's kinds derives from
|
|
117
|
+
* this table, and the `satisfies` clause stops the file compiling when the
|
|
118
|
+
* contract gains a kind this table lacks; the `switch` in `dispatch` demands
|
|
119
|
+
* the behaviour for it.
|
|
184
120
|
*/
|
|
185
121
|
export const REGISTERED_AS = Object.freeze({
|
|
186
122
|
order: "order",
|
|
@@ -188,10 +124,9 @@ export const REGISTERED_AS = Object.freeze({
|
|
|
188
124
|
order_event: "event",
|
|
189
125
|
});
|
|
190
126
|
export const systemClock = {
|
|
191
|
-
// Monotonic rather than the wall clock
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
// question into an hour-long sleep.
|
|
127
|
+
// Monotonic rather than the wall clock: this clock only measures how long
|
|
128
|
+
// something took, and a wall clock stepped back an hour would become an
|
|
129
|
+
// hour-long sleep.
|
|
195
130
|
now: () => performance.now(),
|
|
196
131
|
random: () => Math.random(),
|
|
197
132
|
sleep: (ms, signal) => new Promise((resolve) => {
|
|
@@ -211,29 +146,16 @@ export const systemClock = {
|
|
|
211
146
|
}),
|
|
212
147
|
};
|
|
213
148
|
/**
|
|
214
|
-
* The shortest a quiet poll may take before the next one.
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
* passed and the next poll goes out at once — which is what keeps a waiting
|
|
219
|
-
* agent's price question free of any polling lag (ADR-0004 §4). It exists for
|
|
220
|
-
* the gateway that is not holding requests at all, misconfigured or half
|
|
221
|
-
* broken, where a loop with no floor turns into as many requests a second as
|
|
222
|
-
* the network allows. One second is the whole of the claim: slow enough that
|
|
223
|
-
* nothing is hammered, fast enough that it is invisible against a window
|
|
224
|
-
* measured in tens of seconds.
|
|
149
|
+
* The shortest a quiet poll may take before the next one. Against a gateway
|
|
150
|
+
* that holds polls open it costs nothing, since the window is far longer; it
|
|
151
|
+
* exists so a gateway that answers at once does not get as many requests a
|
|
152
|
+
* second as the network allows.
|
|
225
153
|
*/
|
|
226
154
|
export const QUIET_POLL_FLOOR_MS = 1_000;
|
|
227
155
|
/**
|
|
228
|
-
* The contract version named inside
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
* this SDK refuses, and the version it names is the one thing still readable
|
|
232
|
-
* in what it sent. It goes into the sentence the merchant reads and no
|
|
233
|
-
* further: this body was held to no schema of ours, so a proxy's error
|
|
234
|
-
* envelope that happens to carry a field of that name must not be able to stop
|
|
235
|
-
* a worker. One field is read, and only when it is a string; anything else is
|
|
236
|
-
* a body with nothing to say about versions.
|
|
156
|
+
* The contract version named inside a body the SDK could not parse. It goes
|
|
157
|
+
* into the merchant's sentence and no further: the body was held to no schema,
|
|
158
|
+
* so a proxy's error page with a field of that name must not stop a worker.
|
|
237
159
|
*/
|
|
238
160
|
const versionSpokenIn = (body) => {
|
|
239
161
|
if (typeof body !== "object" || body === null)
|
|
@@ -246,62 +168,26 @@ const assertEveryKindIsHandled = (envelope) => {
|
|
|
246
168
|
throw new TypeError(`the worker was handed an envelope of a kind it does not know: ${JSON.stringify(envelope)}`);
|
|
247
169
|
};
|
|
248
170
|
/**
|
|
249
|
-
* How long the worker asks the gateway to hold a poll open
|
|
250
|
-
*
|
|
251
|
-
* It is a request and not an expectation. The gateway holds it for its own
|
|
252
|
-
* window at most, and the contract says as much, so a shorter answer is
|
|
253
|
-
* ordinary rather than a fault. Asking is what makes the number visible on the
|
|
254
|
-
* wire, where a gateway that wanted to answer sooner can see it.
|
|
171
|
+
* How long the worker asks the gateway to hold a poll open. The gateway holds
|
|
172
|
+
* it for its own window at most, so a shorter answer is ordinary.
|
|
255
173
|
*/
|
|
256
174
|
export const POLL_WAIT_SECONDS = 25;
|
|
257
175
|
/**
|
|
258
|
-
* How long the worker
|
|
176
|
+
* How long the worker waits for a poll before giving up on it: twice the
|
|
177
|
+
* window asked for, so a gateway that answers late is never cut off, while a
|
|
178
|
+
* connection dropped silently by something in the middle costs one window
|
|
179
|
+
* rather than the runtime's default of minutes.
|
|
259
180
|
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
* minutes against a window measured in seconds.
|
|
267
|
-
*
|
|
268
|
-
* Twice the window, so a gateway that holds the full window and answers late is
|
|
269
|
-
* never cut off, and a connection that has gone quiet costs one window of
|
|
270
|
-
* silence rather than several minutes of it.
|
|
271
|
-
*
|
|
272
|
-
* This worker never comes near it against the gateway in this repository, and
|
|
273
|
-
* that is not luck: every poll names the window it wants, and that gateway
|
|
274
|
-
* holds a poll for the smaller of the window asked for and its own, so the wait
|
|
275
|
-
* is bounded by the twenty-five seconds above however the gateway is
|
|
276
|
-
* configured. Naming the window is this side's guarantee. The deadline is for
|
|
277
|
-
* the other case — a connection that died without saying so.
|
|
278
|
-
*
|
|
279
|
-
* A second belt exists on the gateway's side, and it is not for this worker.
|
|
280
|
-
* The window is optional on the wire, and a poll that names none is held for
|
|
281
|
-
* the gateway's whole configured window instead; that window is refused above a
|
|
282
|
-
* ceiling set under this number (`WORKER_POLL_WAIT_CEILING_MS` in
|
|
283
|
-
* `apps/gateway/src/config.ts`), which is where this figure is copied. The
|
|
284
|
-
* gateway depends on the contracts and the core machine rather than on the SDK,
|
|
285
|
-
* so it cannot read it, and nothing checks that the copy still matches:
|
|
286
|
-
* changing what is written here means changing it there in the same breath.
|
|
287
|
-
*
|
|
288
|
-
* It is also the reason two poll failures can be a minute and a half apart,
|
|
289
|
-
* which matters to anybody counting them from outside. A gateway that refuses
|
|
290
|
-
* connections fails a poll at once and the backoff is the whole gap; one that
|
|
291
|
-
* accepts and never answers fails only when this deadline runs out, so the gap
|
|
292
|
-
* is this plus the backoff. `DOUBT_MS` in packages/slice/src/subscription.ts is
|
|
293
|
-
* that sum, and it is named here because raising this number silently narrows
|
|
294
|
-
* it — and a window too narrow reads the quiet between two failures as a
|
|
295
|
-
* recovery.
|
|
181
|
+
* Two copies elsewhere must move with it. `WORKER_POLL_WAIT_CEILING_MS` in
|
|
182
|
+
* `apps/gateway/src/config.ts` is set under this number, and nothing checks
|
|
183
|
+
* that it still is. `DOUBT_MS` in packages/slice/src/subscription.ts is this
|
|
184
|
+
* plus the backoff ceiling, since a gateway that accepts and never answers
|
|
185
|
+
* fails a poll only when this runs out; raise this alone and a healthcheck
|
|
186
|
+
* reads the quiet between two failures as a recovery.
|
|
296
187
|
*/
|
|
297
188
|
export const POLL_DEADLINE_MS = POLL_WAIT_SECONDS * 2 * 1_000;
|
|
298
189
|
export const startWorker = (gateway, handlers, clock = systemClock,
|
|
299
|
-
/**
|
|
300
|
-
* The deadline is a parameter for the same reason the clock is: a test
|
|
301
|
-
* cannot wait out fifty seconds to find out whether the worker gives up on a
|
|
302
|
-
* poll, and a deadline nothing exercises is a deadline nobody has seen work.
|
|
303
|
-
* It is not part of anything a merchant passes.
|
|
304
|
-
*/
|
|
190
|
+
/** A parameter so a test can see the deadline work without waiting it out. */
|
|
305
191
|
deadlineMs = POLL_DEADLINE_MS) => {
|
|
306
192
|
const controller = new AbortController();
|
|
307
193
|
let stopping = false;
|
|
@@ -315,8 +201,8 @@ deadlineMs = POLL_DEADLINE_MS) => {
|
|
|
315
201
|
report({
|
|
316
202
|
kind: WORKER_PROBLEM_KINDS.CONTRACT_VERSION_MISMATCH,
|
|
317
203
|
fatal: true,
|
|
318
|
-
// Both
|
|
319
|
-
//
|
|
204
|
+
// Both versions are in the sentence, so the merchant can tell which end
|
|
205
|
+
// needs upgrading.
|
|
320
206
|
message: `the gateway speaks contract version ${spoken} and this SDK speaks ${contractVersion}; the worker stopped rather than handle orders in a dialect it may be reading wrong`,
|
|
321
207
|
});
|
|
322
208
|
};
|
|
@@ -327,10 +213,8 @@ deadlineMs = POLL_DEADLINE_MS) => {
|
|
|
327
213
|
signal: controller.signal,
|
|
328
214
|
});
|
|
329
215
|
if (!sent.ok) {
|
|
330
|
-
// Reported even
|
|
331
|
-
//
|
|
332
|
-
// reconciling a shutdown is entitled to know that an answer for this
|
|
333
|
-
// order went nowhere.
|
|
216
|
+
// Reported even while stopping: the handler has run, goods may have been
|
|
217
|
+
// issued, and the merchant needs to know this answer went nowhere.
|
|
334
218
|
report({
|
|
335
219
|
kind: WORKER_PROBLEM_KINDS.ANSWER_FAILED,
|
|
336
220
|
fatal: false,
|
|
@@ -348,7 +232,7 @@ deadlineMs = POLL_DEADLINE_MS) => {
|
|
|
348
232
|
});
|
|
349
233
|
}
|
|
350
234
|
};
|
|
351
|
-
/** See `droppedFieldWarning
|
|
235
|
+
/** See `droppedFieldWarning`. */
|
|
352
236
|
const warnAboutTheDroppedKey = (order, answer) => {
|
|
353
237
|
const dropped = droppedFieldWarning(order.id, "delivered" in answer ? answer.delivered : undefined);
|
|
354
238
|
if (dropped !== null)
|
|
@@ -417,15 +301,12 @@ deadlineMs = POLL_DEADLINE_MS) => {
|
|
|
417
301
|
});
|
|
418
302
|
return;
|
|
419
303
|
}
|
|
420
|
-
// Held to the contract before it is sent,
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
// gateway's complaint about a document, which reads as our fault and
|
|
424
|
-
// names the merchant's field only inside a quoted blob.
|
|
304
|
+
// Held to the contract before it is sent, so that the easiest mistake, an
|
|
305
|
+
// amount written as a number, is named as the merchant's own field rather
|
|
306
|
+
// than coming back as the gateway's complaint.
|
|
425
307
|
const checked = QuoteResponseSchema.safeParse(answer);
|
|
426
|
-
// The gateway holds the price to the rule a card's price meets
|
|
427
|
-
//
|
|
428
|
-
// merchant's own field rather than coming back as a call that failed.
|
|
308
|
+
// The gateway holds the price to the rule a card's price meets; applied
|
|
309
|
+
// here too, a zero or a missing cent names the merchant's own field.
|
|
429
310
|
const problems = checked.success
|
|
430
311
|
? checked.data.available
|
|
431
312
|
? priceProblemsOf(checked.data.price)
|
|
@@ -450,11 +331,9 @@ deadlineMs = POLL_DEADLINE_MS) => {
|
|
|
450
331
|
kind: WORKER_PROBLEM_KINDS.ANSWER_FAILED,
|
|
451
332
|
fatal: false,
|
|
452
333
|
subject: question.price_id,
|
|
453
|
-
// The advice about stock
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
// expires, and a merchant releasing stock at that moment is releasing
|
|
457
|
-
// stock they have already committed to an order they hold.
|
|
334
|
+
// The advice about stock is the same whichever reach this was: a price
|
|
335
|
+
// that was used produces an order before the question expires, so
|
|
336
|
+
// stock is released only once it has expired.
|
|
458
337
|
message: `the price for question ${question.price_id} was answered and ${whatIsKnown(sent.failure)}; stock set aside under that identifier can be released once the question expires, whichever of those it was: ${sent.failure.reason}`,
|
|
459
338
|
});
|
|
460
339
|
return;
|
|
@@ -488,14 +367,9 @@ deadlineMs = POLL_DEADLINE_MS) => {
|
|
|
488
367
|
fatal: false,
|
|
489
368
|
cause,
|
|
490
369
|
subject: event.order_id,
|
|
491
|
-
//
|
|
492
|
-
//
|
|
493
|
-
//
|
|
494
|
-
// gateway's, and this SDK does not know: the contract gives every
|
|
495
|
-
// envelope an identifier and a delivery time precisely so that a
|
|
496
|
-
// repeat can be recognised, which only means something if repeats
|
|
497
|
-
// happen. So the honest thing to tell a merchant is that we cannot
|
|
498
|
-
// get it back, not that it is gone.
|
|
370
|
+
// An event is sent at most once and carries no acknowledgement, so
|
|
371
|
+
// nothing here can ask for it again: what the handler failed on is
|
|
372
|
+
// lost, and the open-orders list is where a refund owed shows up.
|
|
499
373
|
message: `the event handler threw on ${event.type} for order ${event.order_id}; nothing here can ask for that event again, and whether it is sent again is not something this SDK knows: ${String(cause)}`,
|
|
500
374
|
});
|
|
501
375
|
}
|
|
@@ -512,57 +386,44 @@ deadlineMs = POLL_DEADLINE_MS) => {
|
|
|
512
386
|
await handleEvent(envelope.payload, { id: envelope.id, sent_at: envelope.sent_at });
|
|
513
387
|
return;
|
|
514
388
|
default:
|
|
515
|
-
// Unreachable while the contract carries three kinds
|
|
516
|
-
//
|
|
517
|
-
// being dropped in silence by a switch with nothing to say about it.
|
|
389
|
+
// Unreachable while the contract carries three kinds; a fourth stops
|
|
390
|
+
// this file compiling instead of being dropped in silence.
|
|
518
391
|
return assertEveryKindIsHandled(envelope);
|
|
519
392
|
}
|
|
520
393
|
};
|
|
521
394
|
const run = async () => {
|
|
522
|
-
// One turn before the first poll
|
|
523
|
-
//
|
|
524
|
-
// registration in place before anything is asked of the gateway. A
|
|
525
|
-
// handler registered later than that is in place too — the registry is
|
|
526
|
-
// read when an envelope is dispatched, not when the loop begins — so this
|
|
527
|
-
// buys only the synchronous lines immediately after the call, which is
|
|
528
|
-
// exactly where a merchant writes them.
|
|
395
|
+
// One turn before the first poll, so that a process which calls start()
|
|
396
|
+
// without awaiting it and registers on the next line is ready first.
|
|
529
397
|
await Promise.resolve();
|
|
530
398
|
let failures = 0;
|
|
531
399
|
while (!stopping) {
|
|
532
400
|
const startedAt = clock.now();
|
|
533
401
|
const answer = await callRoute(gateway, "poll_worker", {
|
|
534
402
|
body: { wait_seconds: POLL_WAIT_SECONDS },
|
|
535
|
-
//
|
|
536
|
-
//
|
|
403
|
+
// Ends early when the worker is stopped or the answer takes longer
|
|
404
|
+
// than any answer should.
|
|
537
405
|
signal: AbortSignal.any([controller.signal, AbortSignal.timeout(deadlineMs)]),
|
|
538
406
|
});
|
|
539
407
|
if (stopping)
|
|
540
408
|
break;
|
|
541
409
|
if (!answer.ok) {
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
//
|
|
545
|
-
// not held to any schema of ours: a proxy's error envelope carrying a
|
|
546
|
-
// field of that name would otherwise stop a merchant's worker for
|
|
547
|
-
// good over something that needed waiting out. The one place a
|
|
548
|
-
// mismatch is decided is a poll answer that parsed.
|
|
410
|
+
// The version a failed body names is passed on as a remark, never as a
|
|
411
|
+
// verdict: that body was held to no schema, and only a poll answer
|
|
412
|
+
// that parsed decides a mismatch.
|
|
549
413
|
const spoken = versionSpokenIn(answer.failure.body);
|
|
550
414
|
const remark = spoken !== undefined && !speaksContract(spoken)
|
|
551
415
|
? ` — what answered names contract version ${spoken} while this SDK speaks ${contractVersion}, which may mean the gateway is of another version`
|
|
552
416
|
: "";
|
|
553
|
-
// A refusal that says waiting will not
|
|
554
|
-
//
|
|
555
|
-
//
|
|
556
|
-
// for good. But it is said for what it is, because "asking again"
|
|
557
|
-
// alone reads as something that will pass, and a revoked key takes no
|
|
558
|
-
// new orders until it is replaced.
|
|
417
|
+
// A refusal that says waiting will not help, such as a revoked key, is
|
|
418
|
+
// still not taken as the end, since one such answer may come from a
|
|
419
|
+
// proxy; but it is said for what it is.
|
|
559
420
|
const settled = answer.failure.refusal?.retryable === false;
|
|
560
421
|
failures += 1;
|
|
561
422
|
report({
|
|
562
423
|
kind: WORKER_PROBLEM_KINDS.POLL_FAILED,
|
|
563
424
|
fatal: false,
|
|
564
425
|
message: settled
|
|
565
|
-
? `${answer.failure.reason}${remark} — the gateway says asking again will not change this, and no new orders arrive until it is fixed
|
|
426
|
+
? `${answer.failure.reason}${remark} — the gateway says asking again will not change this, and no new orders arrive until what it names is fixed; asking again after a wait in case it was not the gateway that answered`
|
|
566
427
|
: `${answer.failure.reason}${remark} — asking again after a wait`,
|
|
567
428
|
});
|
|
568
429
|
await rest(retryDelayMs(failures, clock.random()));
|
|
@@ -582,11 +443,8 @@ deadlineMs = POLL_DEADLINE_MS) => {
|
|
|
582
443
|
handled += 1;
|
|
583
444
|
}
|
|
584
445
|
if (handled < batch.length) {
|
|
585
|
-
// Said rather than left
|
|
586
|
-
//
|
|
587
|
-
// acknowledgement, so nothing here can ask for that one again, and a
|
|
588
|
-
// merchant who stopped a worker mid-batch is entitled to know which
|
|
589
|
-
// kinds went unread.
|
|
446
|
+
// Said rather than left silent: an order or price question left in the
|
|
447
|
+
// batch comes back on its own, but an event does not.
|
|
590
448
|
report({
|
|
591
449
|
kind: WORKER_PROBLEM_KINDS.BATCH_ABANDONED,
|
|
592
450
|
fatal: false,
|
|
@@ -603,10 +461,8 @@ deadlineMs = POLL_DEADLINE_MS) => {
|
|
|
603
461
|
};
|
|
604
462
|
const finished = run()
|
|
605
463
|
.catch((cause) => {
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
// inside it, so reaching here means this SDK has a defect and the
|
|
609
|
-
// merchant's worker is down until their process is restarted.
|
|
464
|
+
// Reaching here means a defect in this SDK, not a failed poll: the
|
|
465
|
+
// merchant's worker is down until their process restarts.
|
|
610
466
|
report({
|
|
611
467
|
kind: WORKER_PROBLEM_KINDS.WORKER_FAILED,
|
|
612
468
|
fatal: true,
|
|
@@ -615,8 +471,8 @@ deadlineMs = POLL_DEADLINE_MS) => {
|
|
|
615
471
|
});
|
|
616
472
|
})
|
|
617
473
|
.finally(() => {
|
|
618
|
-
// However the loop ended
|
|
619
|
-
//
|
|
474
|
+
// However the loop ended, it is over, and the worker and its holder
|
|
475
|
+
// must agree.
|
|
620
476
|
ended = true;
|
|
621
477
|
stopping = true;
|
|
622
478
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuanu-ai/agentify",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Sell your existing products to AI agents: publish product cards to an x402 storefront and fill the paid orders from your own service.",
|
|
6
6
|
"keywords": [
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"access": "public"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@nuanu-ai/agentify-contracts": "0.
|
|
39
|
+
"@nuanu-ai/agentify-contracts": "0.10.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"vitest": "^4.1.11",
|