@nuanu-ai/agentify 0.2.4

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/client.js ADDED
@@ -0,0 +1,520 @@
1
+ /**
2
+ * The client a merchant's engineer holds: a catalog, an order desk, and one
3
+ * place to say what this process answers.
4
+ *
5
+ * The shape of it is the portal's, not this file's. Every method here exists
6
+ * because a page of the documentation tells a merchant to call it, with the
7
+ * arguments that page passes and answering with what that page reads — and the
8
+ * examples on those pages are compiled against these types by a test, so the
9
+ * code a merchant copies out of the documentation is code that builds.
10
+ *
11
+ * Three kinds travel one stream, so there is one way to register a handler for
12
+ * one of them — `on(kind, handler)` — and one lifecycle, `start` and `stop`,
13
+ * for the loop that carries all three. What a handler receives carries the
14
+ * calls that answer it, and that is the part worth reading before changing
15
+ * anything here, because two things that look alike are deliberately not.
16
+ *
17
+ * `order.delivered(...)`, `order.refused(...)` and `order.accepted(...)`
18
+ * build the answer and send nothing; the handler still returns it. A bot
19
+ * library lets a handler reply and forget, and forgetting there costs a
20
+ * message; forgetting here is an order nobody answered — redelivered, a
21
+ * delivery attempt spent, and in the asynchronous mode a debt to a buyer at
22
+ * the end of it. Returning the answer is what makes forgetting impossible and
23
+ * answering twice impossible.
24
+ *
25
+ * `order.deliver(...)`, `order.refuse(...)` and `order.accept(...)` are the
26
+ * other thing: they send. They are the asynchronous merchant's closure verbs,
27
+ * made hours later and from anywhere in their code, and they exist on the
28
+ * order rather than beside it so that a merchant never holds an identifier of
29
+ * ours. The same calls are on every order this client hands back — off the
30
+ * stream, off `orders.get`, off `orders.list`, and off `orders.forId` for the
31
+ * process that kept nothing but the identifier — because a process that
32
+ * restarted has no object left to hold, and that is exactly the moment
33
+ * juggling identifiers would come back.
34
+ *
35
+ * One rule runs through the whole surface and it is worth stating once. What
36
+ * the contract gives a call a failure branch for is returned, never thrown,
37
+ * and every one of those answers is the same envelope: `ok` says which it was,
38
+ * a card that was not published comes back with its findings under `problems`,
39
+ * and an order call that did not go through comes back as an error with a flag
40
+ * saying whether repeating it could help. A merchant's integration code is
41
+ * expected to read those and branch on them.
42
+ *
43
+ * What is thrown is what has no branch to be read on, and it is two kinds. A
44
+ * client built wrong — no key, an address that is not one, a handler
45
+ * registered twice — is a `TypeError` at the line that is wrong, before
46
+ * anything leaves the process. A call that went out and produced no answer of
47
+ * the kind its route promises, where the route has nowhere to put one, is a
48
+ * `AgentifyError` carrying the same code vocabulary the returned failures use.
49
+ *
50
+ * The one place that rule needed a decision rather than a reading is a
51
+ * `deliver` or a `refuse` whose answer never arrived, or arrived in words this
52
+ * package cannot read. The contract has a branch for it — an error with a flag
53
+ * saying whether calling again could change the outcome is exactly the shape
54
+ * of "the connection dropped, call again" — so it comes back through that
55
+ * branch, under a code this package produces and the gateway never sends. The
56
+ * three codes and what separates them are written down beside them below.
57
+ */
58
+ import { callRoute, REACH, whatIsKnown, } from "./transport.js";
59
+ import { droppedFieldWarning, REGISTERED_AS, reportSafely, startWorker, } from "./worker.js";
60
+ /** The word for the reporter, which nothing on the wire carries. */
61
+ const PROBLEM = "problem";
62
+ /**
63
+ * The words `on` accepts, as a map over the kinds themselves.
64
+ *
65
+ * A merchant working outside TypeScript, or one who wrote `orders`, would
66
+ * otherwise register a handler that is never called and hear nothing about it.
67
+ * Written as a mapped type so that a kind the stream grows and this file
68
+ * forgets stops it compiling.
69
+ */
70
+ const HANDLER_KINDS = Object.freeze({
71
+ order: true,
72
+ quote: true,
73
+ event: true,
74
+ problem: true,
75
+ });
76
+ /**
77
+ * Every word `on` accepts, in the order this file lists them, for the sentence
78
+ * a merchant reads when they wrote one that is not on the list.
79
+ */
80
+ const everyKind = () => Object.keys(HANDLER_KINDS).map((kind) => `'${kind}'`);
81
+ /** The words that answer something on the stream, which `problem` does not. */
82
+ const streamKinds = () => Object.values(REGISTERED_AS);
83
+ /**
84
+ * The three codes an order call comes back under when the gateway produced no
85
+ * answer this package can read.
86
+ *
87
+ * The contract's list of error codes is open exactly so that a case nobody
88
+ * anticipated reaches the merchant in its own words instead of being flattened
89
+ * into the nearest of three. These are three of those, and they are produced
90
+ * here rather than on the wire.
91
+ *
92
+ * Three and not one because the difference is the whole of what a merchant
93
+ * needs at that moment, and they are about their own books. A call that never
94
+ * arrived certainly changed nothing. A call answered in words we could not
95
+ * read reached us and may well have done its work; what is missing is what it
96
+ * said. And a call that was sent into silence — the connection broke, the
97
+ * worker was shut down mid-flight — is neither of those, and saying it did not
98
+ * arrive would be inventing the one fact the merchant came here for.
99
+ */
100
+ export const CALL_DID_NOT_REACH_US = "call_did_not_reach_us";
101
+ export const ANSWER_NOT_UNDERSTOOD = "answer_not_understood";
102
+ export const OUTCOME_UNKNOWN = "outcome_unknown";
103
+ const CODE_FOR = {
104
+ [REACH.NOT_RECEIVED]: CALL_DID_NOT_REACH_US,
105
+ [REACH.ANSWERED]: ANSWER_NOT_UNDERSTOOD,
106
+ [REACH.UNKNOWN]: OUTCOME_UNKNOWN,
107
+ };
108
+ /**
109
+ * What is thrown where a route has no failure branch to hand a merchant back.
110
+ *
111
+ * Three of this client's calls answer with a document or with nothing at all —
112
+ * publishing a card, reading one order, listing them — so a call that produced
113
+ * no document has nowhere in the contract to be returned. It is thrown, and
114
+ * this is what it is thrown as.
115
+ *
116
+ * It carries what the returned failures carry, because the two are the same
117
+ * facts about the same calls and a merchant should not have to learn a second
118
+ * vocabulary for the half of them that throws. `code` is the gateway's own word
119
+ * where the gateway refused in words we recognise, and otherwise one of the
120
+ * three this package produces — `call_did_not_reach_us`, `answer_not_understood`,
121
+ * `outcome_unknown`. `failedCall` below makes that same choice for the order
122
+ * calls, so a refusal has one name on both roads. `route` is the call it
123
+ * happened on, under the name the
124
+ * contract's own table gives it, so a `catch` can say which of three it was
125
+ * without reading the sentence.
126
+ *
127
+ * What is not this: a client built wrong. A missing key, a `baseUrl` that is
128
+ * not an address, a handler registered twice and a kind nothing delivers are
129
+ * mistakes in the merchant's own code, made before anything left the process,
130
+ * and they stay `TypeError`. A merchant catching this class is catching calls
131
+ * that went out.
132
+ */
133
+ export class AgentifyError extends Error {
134
+ /** Why the call did not go through, for the code that branches on it. */
135
+ code;
136
+ /** The call it happened on, named as the contract's route table names it. */
137
+ route;
138
+ /**
139
+ * Whether making the same call again could get past this.
140
+ *
141
+ * Here for the same reason the code is: the calls that throw and the calls
142
+ * that return are the same calls failing the same ways, and a merchant who
143
+ * had to work out from a `catch` what a returned error would have told them
144
+ * outright is being charged twice for one lesson. The gateway's own answer
145
+ * where the gateway refused in words, and true where nothing readable came
146
+ * back — which is a statement about what this package knows, not a promise
147
+ * that the call will work.
148
+ */
149
+ retryable;
150
+ constructor(code, route, message, retryable) {
151
+ super(message);
152
+ this.name = "AgentifyError";
153
+ this.code = code;
154
+ this.route = route;
155
+ this.retryable = retryable;
156
+ }
157
+ }
158
+ /**
159
+ * What an order call hands back when it produced no answer of its own.
160
+ *
161
+ * `code` is the gateway's own word where the gateway refused in words we
162
+ * recognise, and otherwise one of the three this package produces for a
163
+ * silence. The two halves of the answer have to say the same thing: a refusal
164
+ * quoted in the message and filed under `answer_not_understood` is the message
165
+ * arguing with the code, and the code is the half a program reads. It is the
166
+ * same choice `document` makes for the calls that throw, so one refusal has one
167
+ * name whether the merchant catches it or branches on it.
168
+ *
169
+ * `retryable` says whether calling again could change the outcome, and it has
170
+ * two sources because there are two situations. Where the gateway refused in
171
+ * words, the answer is the gateway's own: it is the side that knows whether
172
+ * the door is shut or was merely shut a moment ago, and this package does not
173
+ * improve on what it was told. Where no answer arrived, or none that could be
174
+ * read, there is nothing to defer to and the answer is true — none of those
175
+ * three is a state of the order, and none will still be true in a minute if a
176
+ * network settled or a proxy went away.
177
+ *
178
+ * The fallback is `true` and not `false` on purpose. It is reached only when
179
+ * this package could not read an answer, which is a statement about our
180
+ * knowledge and not about the order; saying "do not call again" there would be
181
+ * this package inventing the one fact the merchant came for.
182
+ *
183
+ * The sentence about repeating safely is added where two things hold at once:
184
+ * the contract says the call may be repeated — delivering is idempotent by the
185
+ * order's identifier, and taking an order on happens again on every
186
+ * redelivery, while refusing is documented as neither — and calling again
187
+ * could get anywhere. Offering "you may safely repeat this" under a refusal
188
+ * that will answer the same way for ever is an invitation to do something
189
+ * pointless, and it reads as encouragement.
190
+ */
191
+ const failedCall = (repeatIsSafe, failure) => {
192
+ const retryable = failure.refusal?.retryable ?? true;
193
+ return {
194
+ code: failure.refusal?.code ?? CODE_FOR[failure.reach],
195
+ message: repeatIsSafe && retryable
196
+ ? `${whatIsKnown(failure)}: ${failure.reason} — this call may be made again without doing its work twice`
197
+ : `${whatIsKnown(failure)}: ${failure.reason}`,
198
+ retryable,
199
+ };
200
+ };
201
+ /**
202
+ * The reporter a client gets when the merchant registered none.
203
+ *
204
+ * A library writing to the console is a small rudeness. A worker that stopped
205
+ * on a contract mismatch and said nothing to anybody is a merchant learning
206
+ * about it from a buyer, and between the two this is the cheaper one.
207
+ */
208
+ const reportToConsole = (problem) => {
209
+ // The exception goes out beside the sentence rather than inside it. The
210
+ // message already carries what the exception says; what it cannot carry is
211
+ // the stack, and the stack is the whole of what a merchant debugging their
212
+ // own handler is looking for.
213
+ if (problem.cause === undefined) {
214
+ console.error(`[agentify] ${problem.kind}: ${problem.message}`);
215
+ return;
216
+ }
217
+ console.error(`[agentify] ${problem.kind}: ${problem.message}`, problem.cause);
218
+ };
219
+ /**
220
+ * What a key says about the world it was issued in, read from its own prefix.
221
+ *
222
+ * ADR-0020 is what makes reading it here legitimate rather than clever: every
223
+ * key carries the environment it was issued in as its prefix, and whoever holds
224
+ * a key can read that prefix. So a worker learns which deployment it joined
225
+ * without a field on the wire, a line in a response document or a step in a
226
+ * handshake — none of which this adds.
227
+ *
228
+ * The rule's home is `packages/core/src/deployment/environment.ts`, where
229
+ * `keyPrefixFor` writes these prefixes and `environmentOfKeyPrefix` reads them.
230
+ * The two strings below are a second copy of that rule and not a use of it,
231
+ * because this package cannot depend on that module at runtime: it is private
232
+ * to the workspace and unpublished, and the dependency tree a merchant installs
233
+ * is a recorded decision (ADR-0003 §8) that does not grow to reach it.
234
+ *
235
+ * A copy is a thing that drifts, and these two must not. What holds them
236
+ * together is `which-world.test.ts`: it builds its keys out of core's own
237
+ * `keyPrefixFor` and asserts the sentences written here, so a prefix changed on
238
+ * one side and not the other fails `pnpm test` instead of going quiet in
239
+ * somebody's log. That import is dev-time and reaches nothing published — the
240
+ * tarball ships `dist`, and the build leaves the tests out. Changing either
241
+ * side means changing both in the same commit.
242
+ *
243
+ * A key matching neither prefix says nothing at all, which is the third case
244
+ * ADR-0020 names: a key issued before the prefix existed, or one that is not
245
+ * ours. There is nothing to fall back to. Guessing is wrong in both directions
246
+ * — telling a merchant their money is play when it is real, or the other way
247
+ * round — and no line at all is better than a wrong claim about whose money is
248
+ * at stake.
249
+ */
250
+ const WHAT_THE_KEY_SAYS = Object.freeze({
251
+ csk_test_: "with a test key: the money there is not real",
252
+ csk_live_: "with a live key: the money there is real",
253
+ });
254
+ const whatTheKeySays = (apiKey) => Object.entries(WHAT_THE_KEY_SAYS).find(([prefix]) => apiKey.startsWith(prefix))?.[1];
255
+ const keyOf = (apiKey) => {
256
+ if (typeof apiKey !== "string" || apiKey.trim() === "") {
257
+ throw new TypeError("createClient needs the merchant's API key, and was given none — check that the environment variable holding it is set where this process can read it");
258
+ }
259
+ return apiKey;
260
+ };
261
+ /**
262
+ * The address as given, checked as far as it can be checked without using it.
263
+ *
264
+ * A value that is not an address is refused at once: it will not become one
265
+ * later, and the line that produced it is the line to fix. An address that was
266
+ * not given at all is a different matter, and it is kept as the sentence that
267
+ * will be raised by the first call that needs it — see `baseUrl` above.
268
+ */
269
+ const addressOf = (baseUrl) => {
270
+ if (baseUrl === undefined)
271
+ return "";
272
+ let parsed;
273
+ try {
274
+ parsed = new URL(baseUrl);
275
+ }
276
+ catch {
277
+ throw new TypeError(`baseUrl is not an address: ${JSON.stringify(baseUrl)}`);
278
+ }
279
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
280
+ throw new TypeError(`baseUrl is not an http address: ${JSON.stringify(baseUrl)} — the gateway is reached over https, and over http only where the two run on one machine`);
281
+ }
282
+ return baseUrl;
283
+ };
284
+ /** Refuses to compile if a kind is added to `Handlers` and not registered here. */
285
+ const assertEveryKindIsRegistered = (kind) => {
286
+ throw new TypeError(`on() has no place to put a handler for ${JSON.stringify(kind)}`);
287
+ };
288
+ export const createClient = (options) => {
289
+ const gateway = {
290
+ apiKey: keyOf(options.apiKey),
291
+ baseUrl: addressOf(options.baseUrl),
292
+ };
293
+ const reachable = () => {
294
+ if (gateway.baseUrl === "") {
295
+ throw new TypeError("this client has no gateway address: pass baseUrl to createClient — https://test.agentify.ad while you are building, https://app.agentify.ad once you are live. There is no default on purpose: which of the two you talk to is the difference between a sale and a rehearsal, and an address this package chose for you would point somebody's production at the wrong one without a word");
296
+ }
297
+ };
298
+ /**
299
+ * The handlers, and the loop that dispatches to them.
300
+ *
301
+ * The registry belongs to the client and not to any one loop, which is what
302
+ * lets a merchant stop and start again on the handlers they already
303
+ * registered. `worker` is the loop currently running, if one is; it also
304
+ * ends on its own when the gateway turns out to speak another dialect, which
305
+ * is why every reader asks it whether it is still running rather than
306
+ * assuming that a worker that exists is a worker that polls.
307
+ */
308
+ const registry = { problem: reportToConsole };
309
+ const registered = new Set();
310
+ let worker;
311
+ /** Set while a stop is in flight, and shared by every caller of stop(). */
312
+ let stopping;
313
+ /** Whether the world this client works in has already been said. */
314
+ let worldIsSaid = false;
315
+ /**
316
+ * The one line this package writes uninvited: where this worker is pointed
317
+ * and whether the money there is real.
318
+ *
319
+ * Once per client and not once per start. The key cannot change under a
320
+ * client and neither can the address, so a second line would carry no second
321
+ * fact — and a supervisor that stops and starts a worker on a schedule would
322
+ * turn it into the kind of line people learn to scroll past. The question is
323
+ * closed even where the answer is silence: a key naming no environment is
324
+ * asked about once and never again.
325
+ *
326
+ * It goes to the console because that is where this package already writes
327
+ * when nobody gave it anywhere else to write, and at the informational level
328
+ * because that is what it is. `console.error` would put it in front of every
329
+ * log collector as a fault on a worker that has just started correctly, which
330
+ * is a false alarm and the one thing this line must never look like.
331
+ *
332
+ * It is not a `WorkerProblem`: the merchant's reporter is for what went
333
+ * wrong, and a process counting problems should not have to filter out the
334
+ * one that means everything is fine.
335
+ */
336
+ const sayWhichWorld = () => {
337
+ if (worldIsSaid)
338
+ return;
339
+ worldIsSaid = true;
340
+ const words = whatTheKeySays(gateway.apiKey);
341
+ if (words === undefined)
342
+ return;
343
+ console.info(`[agentify] worker started against ${gateway.baseUrl} ${words}`);
344
+ };
345
+ const orderCall = async (route, orderId, body) => {
346
+ reachable();
347
+ if (route === "deliver_order") {
348
+ // The road the worker's own warning does not cover, and the one a
349
+ // merchant delivering asynchronously takes. See `droppedFieldWarning`:
350
+ // the field goes no further than the gateway's parse, and without this
351
+ // the merchant's handler had no way to learn that what it sent is not
352
+ // what went out. A warning and not a refusal — the rest of the delivery
353
+ // is good, and the call is the merchant's to make.
354
+ const dropped = droppedFieldWarning(orderId, body);
355
+ if (dropped !== null)
356
+ reportSafely(registry.problem, dropped);
357
+ }
358
+ const answer = await callRoute(gateway, route, { path: { order_id: orderId }, body });
359
+ return answer.ok
360
+ ? answer.document
361
+ : { ok: false, error: failedCall(route === "deliver_order", answer.failure) };
362
+ };
363
+ const acceptCall = async (orderId, acceptance) => {
364
+ reachable();
365
+ const answer = await callRoute(gateway, "accept_order", {
366
+ path: { order_id: orderId },
367
+ body: acceptance ?? {},
368
+ });
369
+ // Accepting the same order again is ordinary: an order is taken on afresh
370
+ // on every redelivery, so a repeat does no work twice.
371
+ return answer.ok ? answer.document : { ok: false, error: failedCall(true, answer.failure) };
372
+ };
373
+ /**
374
+ * The calls one order answers to, closed over that order's identifier and
375
+ * over this client.
376
+ *
377
+ * This closure is the whole mechanism. The identifier is captured here, once,
378
+ * at the moment the order was read — off the stream or out of our record —
379
+ * and never travels back through a merchant's hands, where it could be the
380
+ * wrong string. What holds the client is the same closure: `orderCall` and
381
+ * `acceptCall` above are this client's, so an order kept in a merchant's map
382
+ * for four hours still knows which gateway to call and with whose key.
383
+ */
384
+ const callsFor = (orderId) => ({
385
+ delivered: (delivery) => ({ delivered: delivery }),
386
+ refused: (refusal) => ({ refused: refusal }),
387
+ accepted: (acceptance) => ({ accepted: acceptance ?? {} }),
388
+ deliver: (delivery) => orderCall("deliver_order", orderId, delivery),
389
+ refuse: (refusal) => orderCall("refuse_order", orderId, refusal),
390
+ accept: (acceptance) => acceptCall(orderId, acceptance),
391
+ });
392
+ const withCalls = (order) => Object.assign({}, order, callsFor(order.id));
393
+ const askedWithAnswers = (question) => Object.assign({}, question, {
394
+ available: (price, asOf) => ({
395
+ available: true,
396
+ price,
397
+ as_of: asOf ?? new Date().toISOString(),
398
+ }),
399
+ unavailable: (asOf) => ({
400
+ available: false,
401
+ as_of: asOf ?? new Date().toISOString(),
402
+ }),
403
+ });
404
+ const document = async (route, options_) => {
405
+ reachable();
406
+ const answer = await callRoute(gateway, route, options_);
407
+ if (!answer.ok) {
408
+ const { failure } = answer;
409
+ // The gateway's own word wherever it wrote one, and one of ours only
410
+ // where it did not. A refusal we recognise carries the reason a merchant
411
+ // can act on, and replacing it with "we could not read the answer" would
412
+ // throw away the one thing they came here for. The same holds for its
413
+ // answer about calling again — this is the rule `failedCall` applies for
414
+ // the calls that return, applied here so the two roads agree.
415
+ throw new AgentifyError(failure.refusal?.code ?? CODE_FOR[failure.reach], failure.route, failure.reason, failure.refusal?.retryable ?? true);
416
+ }
417
+ return answer.document;
418
+ };
419
+ return {
420
+ catalog: {
421
+ publish: (card) => document("publish_card", { body: card }),
422
+ },
423
+ orders: {
424
+ forId: (orderId) => ({ id: orderId, ...callsFor(orderId) }),
425
+ get: async (orderId) => withCalls(await document("get_order", { path: { order_id: orderId } })),
426
+ list: async (query) => {
427
+ const listed = await document("list_orders", {
428
+ // The wire carries the two words rather than a boolean, because a
429
+ // query string carries text. The merchant writes the boolean their
430
+ // language has and this is where the two meet.
431
+ query: query?.open === undefined ? {} : { open: query.open ? "true" : "false" },
432
+ });
433
+ return listed.orders.map((order) => withCalls(order));
434
+ },
435
+ },
436
+ on(kind, handler) {
437
+ if (!Object.hasOwn(HANDLER_KINDS, kind)) {
438
+ throw new TypeError(`on() was given ${JSON.stringify(kind)}, which is not a kind this stream carries: the kinds are ${everyKind().join(", ")}`);
439
+ }
440
+ if (registered.has(kind)) {
441
+ throw new TypeError(`on('${kind}') was called twice on one client: the second handler would silently replace the first, and one message goes to one handler`);
442
+ }
443
+ // Narrowed off the generic so that the switch below is exhaustive over
444
+ // the kinds rather than over whatever this call was instantiated with.
445
+ const which = kind;
446
+ switch (which) {
447
+ case "order": {
448
+ // The merchant's handler is wrapped rather than registered as it
449
+ // stands: the loop reads a plain order off the envelope, and the
450
+ // calls that close it are this client's to attach.
451
+ const answering = handler;
452
+ registry.order = (arrived) => answering(withCalls(arrived));
453
+ break;
454
+ }
455
+ case "quote": {
456
+ const answering = handler;
457
+ registry.quote = (asked) => answering(askedWithAnswers(asked));
458
+ break;
459
+ }
460
+ case "event": {
461
+ registry.event = handler;
462
+ break;
463
+ }
464
+ case "problem": {
465
+ registry.problem = handler;
466
+ break;
467
+ }
468
+ default:
469
+ return assertEveryKindIsRegistered(which);
470
+ }
471
+ // Written down only once everything that could refuse this call has
472
+ // passed, so a call that threw leaves no trace and the same call made
473
+ // again succeeds.
474
+ registered.add(which);
475
+ },
476
+ start: async () => {
477
+ reachable();
478
+ if (stopping !== undefined) {
479
+ throw new TypeError("this client is being stopped: await that stop() before starting it again, or the loop being started would be racing the one going away");
480
+ }
481
+ if (worker?.running() === true) {
482
+ throw new TypeError("this client is already started: a second loop would open a second long poll for one merchant, and an envelope handed to one of them is not handed to the other");
483
+ }
484
+ // Read off the vocabulary rather than written out, so a kind the stream
485
+ // grows is counted as an answer here without anybody remembering to add
486
+ // it — a process that answered only the new kind would otherwise be
487
+ // refused a start on the grounds that it answers nothing.
488
+ if (!streamKinds().some((kind) => registry[kind] !== undefined)) {
489
+ throw new TypeError(`this client has nothing registered to answer with: register at least one of ${streamKinds()
490
+ .map((kind) => `on('${kind}')`)
491
+ .join(", ")} before start(), or the loop would take work off the queue that nobody answers`);
492
+ }
493
+ // After every refusal above and before the loop: a start that threw
494
+ // opened nothing, and a line saying otherwise would be the first thing a
495
+ // merchant read on the way to an exception.
496
+ sayWhichWorld();
497
+ worker = startWorker(gateway, registry);
498
+ },
499
+ stop: async () => {
500
+ const owned = worker;
501
+ // A client that was never started has nothing to stop, and saying so by
502
+ // returning is what lets a shutdown routine call this unconditionally.
503
+ if (owned === undefined)
504
+ return;
505
+ // One promise for every caller — a shutdown routine and a signal handler
506
+ // both calling stop() is ordinary — and what it buys is the bookkeeping
507
+ // rather than the waiting. Each of them would wait out the same loop
508
+ // either way, because the worker's own stop() awaits the one promise the
509
+ // loop ends on. What must happen once is the clearing below: two of them
510
+ // racing it is two of them deciding whether this client still has a
511
+ // worker, and the last one to answer wins.
512
+ stopping ??= owned.stop().finally(() => {
513
+ if (worker === owned)
514
+ worker = undefined;
515
+ stopping = undefined;
516
+ });
517
+ await stopping;
518
+ },
519
+ };
520
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Which contract this package speaks, and whether it understands another.
3
+ *
4
+ * It sits in a file of its own because both ends of the package need it and
5
+ * neither should reach through the other: the worker checks the version on the
6
+ * answer to every poll, and `index.ts` publishes the same check to a merchant
7
+ * who wants to make it themselves.
8
+ */
9
+ /** The contract version this SDK talks in. */
10
+ export declare const contractVersion = "2";
11
+ /**
12
+ * Whether the SDK understands a contract of the given version. The gateway
13
+ * names its own version in the response, the merchant's worker checks it with
14
+ * this function and fails at startup rather than on the first order, where a
15
+ * divergence of dialects costs the buyer money.
16
+ */
17
+ export declare function speaksContract(version: string): boolean;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Which contract this package speaks, and whether it understands another.
3
+ *
4
+ * It sits in a file of its own because both ends of the package need it and
5
+ * neither should reach through the other: the worker checks the version on the
6
+ * answer to every poll, and `index.ts` publishes the same check to a merchant
7
+ * who wants to make it themselves.
8
+ */
9
+ import { CONTRACT_VERSION } from "@nuanu-ai/agentify-contracts";
10
+ /** The contract version this SDK talks in. */
11
+ export const contractVersion = CONTRACT_VERSION;
12
+ /**
13
+ * Whether the SDK understands a contract of the given version. The gateway
14
+ * names its own version in the response, the merchant's worker checks it with
15
+ * this function and fails at startup rather than on the first order, where a
16
+ * divergence of dialects costs the buyer money.
17
+ */
18
+ export function speaksContract(version) {
19
+ return version === CONTRACT_VERSION;
20
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The Agentify merchant SDK: what someone else's engineer installs so that
3
+ * their catalog sells to agents.
4
+ *
5
+ * There are two things in here and they are one process. `createClient` builds
6
+ * a handle on the gateway from a key and an address; through it a merchant
7
+ * says what their process answers with `on`, opens one outgoing subscription
8
+ * that carries paid orders, price questions and order events together, and
9
+ * closes orders they took on earlier off the orders themselves. Beside it,
10
+ * `checkCard` is the check a merchant runs on their own cards before
11
+ * publishing them — the same check the documentation calls
12
+ * `agentify verify`, which the published package installs as a command of
13
+ * that name. The command's own insides are not exported: what a merchant calls
14
+ * from their code is `checkCard`, and what they run from a terminal is the
15
+ * command, and neither of those needs the other's exit codes.
16
+ *
17
+ * Every call that can fail answers in one envelope — `ok` says which, and a
18
+ * failure carries one `error` with a code, a sentence, whether repeating could
19
+ * help, and the findings under `problems` where the call is refusing what it
20
+ * was handed. What throws instead is a call with no such branch, as
21
+ * `AgentifyError`, under the same codes.
22
+ *
23
+ * The runtime dependency tree is minimal and listed in full: our own
24
+ * `@nuanu-ai/agentify-contracts`, and zod underneath it, and nothing else. A merchant
25
+ * installing the SDK into their production should know exactly what arrives
26
+ * with it, rather than inherit a foreign package tree they would then be
27
+ * maintaining themselves. Every new third-party package in this tree is a
28
+ * recorded decision (ADR-0003 §8), and a test in this package holds the line.
29
+ *
30
+ * The types a merchant needs to write their own functions against — what a
31
+ * card is, what an order carries, what a handler may answer — are re-exported
32
+ * here from the contracts package, so that integration code has one import and
33
+ * not two.
34
+ */
35
+ export type { Acceptance, CallError, Card, CardInput, Delivery, Fulfillment, HandlerAnswer, Money, Order, OrderCallResponse, OrderCallResult, OrderEvent, OrderList, OrderStatus, OrderWithStatus, Problem, PublishResult, QuotePurpose, QuoteRequest, QuoteResponse, Refusal, RefusalCode, SalePrice, } from "@nuanu-ai/agentify-contracts";
36
+ export { CARD_REJECTED, ORDER_EVENT_TYPES, RECOMMENDED_REFUSAL_CODES, } from "@nuanu-ai/agentify-contracts";
37
+ export type { CardCheck } from "./check-card.js";
38
+ export { checkCard } from "./check-card.js";
39
+ export type { AgentifyClient, CatalogNamespace, ClientOptions, HandlerKind, Handlers, LiveOrder, LiveOrderWithStatus, LiveQuoteRequest, OrderCalls, OrderHandle, OrderHandler, OrdersNamespace, QuoteCalls, QuoteHandler, } from "./client.js";
40
+ export { AgentifyError, ANSWER_NOT_UNDERSTOOD, CALL_DID_NOT_REACH_US, createClient, OUTCOME_UNKNOWN, } from "./client.js";
41
+ export { contractVersion, speaksContract } from "./contract.js";
42
+ export type { Delivered, EventHandler, ProblemReporter, WorkerProblem, WorkerProblemKind, } from "./worker.js";
43
+ export { WORKER_PROBLEM_KINDS } from "./worker.js";
package/dist/index.js ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The Agentify merchant SDK: what someone else's engineer installs so that
3
+ * their catalog sells to agents.
4
+ *
5
+ * There are two things in here and they are one process. `createClient` builds
6
+ * a handle on the gateway from a key and an address; through it a merchant
7
+ * says what their process answers with `on`, opens one outgoing subscription
8
+ * that carries paid orders, price questions and order events together, and
9
+ * closes orders they took on earlier off the orders themselves. Beside it,
10
+ * `checkCard` is the check a merchant runs on their own cards before
11
+ * publishing them — the same check the documentation calls
12
+ * `agentify verify`, which the published package installs as a command of
13
+ * that name. The command's own insides are not exported: what a merchant calls
14
+ * from their code is `checkCard`, and what they run from a terminal is the
15
+ * command, and neither of those needs the other's exit codes.
16
+ *
17
+ * Every call that can fail answers in one envelope — `ok` says which, and a
18
+ * failure carries one `error` with a code, a sentence, whether repeating could
19
+ * help, and the findings under `problems` where the call is refusing what it
20
+ * was handed. What throws instead is a call with no such branch, as
21
+ * `AgentifyError`, under the same codes.
22
+ *
23
+ * The runtime dependency tree is minimal and listed in full: our own
24
+ * `@nuanu-ai/agentify-contracts`, and zod underneath it, and nothing else. A merchant
25
+ * installing the SDK into their production should know exactly what arrives
26
+ * with it, rather than inherit a foreign package tree they would then be
27
+ * maintaining themselves. Every new third-party package in this tree is a
28
+ * recorded decision (ADR-0003 §8), and a test in this package holds the line.
29
+ *
30
+ * The types a merchant needs to write their own functions against — what a
31
+ * card is, what an order carries, what a handler may answer — are re-exported
32
+ * here from the contracts package, so that integration code has one import and
33
+ * not two.
34
+ */
35
+ export { CARD_REJECTED, ORDER_EVENT_TYPES, RECOMMENDED_REFUSAL_CODES, } from "@nuanu-ai/agentify-contracts";
36
+ export { checkCard } from "./check-card.js";
37
+ export { AgentifyError, ANSWER_NOT_UNDERSTOOD, CALL_DID_NOT_REACH_US, createClient, OUTCOME_UNKNOWN, } from "./client.js";
38
+ export { contractVersion, speaksContract } from "./contract.js";
39
+ export { WORKER_PROBLEM_KINDS } from "./worker.js";