@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/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
- * The transport is HTTP long polling against the gateway (ADR-0004): the
5
- * worker asks for the next batch, the gateway holds the request until
6
- * something arrives or its own window closes, and the answer is a batch of
7
- * envelopes or an empty one. An empty batch is the ordinary answer to a quiet
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
- * How each kind is answered is the part worth reading before changing
13
- * anything, because the three are answered in three different places and none
14
- * of them is an acknowledgement of the envelope itself.
15
- *
16
- * An order is given to the merchant's handler, and whatever the handler
17
- * returns is posted to the order's answer route — in every mode, without the
18
- * merchant asking. In the synchronous mode that return is the whole of the
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
- * The three cases are three different things to tell a merchant, and only one
62
- * of them is a promise. An answer that never left certainly leaves the order
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 has to tell the merchant about, under the names their
74
- * code can branch on.
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
- * Reporting must not be able to end the loop that reports. A merchant's
108
- * reporter is their code — a logger over a stream that closed during shutdown,
109
- * a client that was never configured — and an exception out of it would
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` and not only a real Promise: a reporter built on
130
- // another library's promise, or one from another realm, fails in exactly
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. See above.
81
+ // Nowhere to say it.
139
82
  }
140
83
  };
141
84
  /**
142
- * The one loss in this contract that nobody is told about, said out loud here
143
- * because this is the place it would reach the agent.
144
- *
145
- * A field named `__proto__` is removed while a delivery is parsed, before any
146
- * check runs — it is neither carried nor refused, and the merchant's handler
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
- * Which registration each kind on the stream is answered by, in the word a
173
- * merchant passes to `on`.
174
- *
175
- * This table is the vocabulary, and everything that has to know the stream's
176
- * kinds is derived from it rather than written out again: what a merchant may
177
- * register, what the registry holds, what `start` counts as an answer, and the
178
- * sentence that names a registration a merchant is missing. It is written over
179
- * the contract's own kinds, so a fourth kind added to the envelope — the
180
- * confirmation request, whose shape the contract does not carry yet — stops
181
- * this file compiling until it has a word of its own here. The `switch` in
182
- * `dispatch` below is the second half of that guard, and it is the one that
183
- * demands the behaviour.
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. The only thing this clock is asked
192
- // is how long something took, and a wall clock stepped backwards by an hour
193
- // — which is what a machine does when it corrects its time — would turn that
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
- * The gateway holds a poll open for its own window, so in ordinary work this
217
- * costs nothing: by the time an empty batch comes back, the floor has long
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 something the SDK could not parse.
229
- *
230
- * A gateway that added an envelope kind, or a field, answers with a document
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, per ADR-0004 §1.
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 will wait for a poll before giving up on it.
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
- * A window asked for is not a promise kept: a connection dropped silently by
261
- * something in the middle — a load balancer that recycled it, a firewall that
262
- * forgot it — leaves this side waiting on an answer that will never come, with
263
- * nothing reported and nothing retried. Without a deadline of our own the only
264
- * thing that eventually breaks that is whatever timeout the runtime happens to
265
- * default to, which is a number nobody here chose and which is measured in
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 numbers are in the sentence: a merchant reading only one of them
319
- // cannot tell which of the two ends needs upgrading.
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 when the worker is stopping, and especially then. The
331
- // handler has already run: goods may have been issued, and a merchant
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`: the same loss, on the handler's road. */
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, for the same reason an order's
421
- // answer is: an amount written as a number instead of text is the easiest
422
- // mistake there is to make here, and sent out it would come back as the
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, outside the
427
- // schema; applied here too, a zero or a missing cent is named as the
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 holds whichever of the three this was, which
454
- // is why it is not branched the way an order's consequence is. A price
455
- // that did arrive and was used produces an order before the question
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
- // Nothing here can ask for the event again — an event carries no
492
- // acknowledgement, so there is no call that says "send that one
493
- // once more". Whether the gateway sends it again anyway is the
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, and here so
516
- // that a fourth added there stops this file compiling rather than
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 goes out, so that a process which calls
523
- // start() without awaiting it and registers on the next line has that
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
- // Two ways this call can end early, and they are different: the worker
536
- // was stopped, or the answer took longer than any answer should.
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
- // A gateway of another dialect answers with a document this SDK cannot
543
- // parse, so the version it names is worth passing on — but only as a
544
- // remark, never as a verdict. Whatever this field was read out of was
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 change it — a key revoked or
554
- // never accepted is the case that matters — is still not taken as the
555
- // end: one such answer from a proxy must not stop a merchant's worker
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 (a revoked key has to be replaced); asking again after a wait in case it was not the gateway that answered`
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 as a silence. An order or a price question
586
- // left in the batch comes back on its own; an event carries no
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
- // Under its own name rather than as a failed poll, because nothing about
607
- // a poll failed: everything the loop expects to go wrong is handled
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 — stopped, or fatally — it is over, and both
619
- // this worker and whoever is holding it should agree about that.
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.0",
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.9.0"
39
+ "@nuanu-ai/agentify-contracts": "0.10.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "vitest": "^4.1.11",