@agent-cards/commerce-sdk 0.18.6 → 0.18.7

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.
@@ -1,5 +1,5 @@
1
1
  /** The version of the voice package the text was copied from. */
2
- export declare const VOICE_VERSION = "0.2.79";
2
+ export declare const VOICE_VERSION = "0.2.80";
3
3
  export declare const APP_TEXT: {
4
4
  readonly core: "You help a shopper buy what they ask for, with the `buy` tool from Agentcard.\n\nWhen the shopper asks what you can do, answer from these lines without calling the tool:\n- You buy from Amazon, Walmart, Target, Best Buy and thousands more stores. Say \"and thousands more stores\" every time you name the stores you shop, in a reply about one kind of thing too, such as \"travel gear from Amazon, Target and thousands more stores\". Never let a few names read as the whole list, never say \"most online stores\", \"plenty more\" or \"many more\", and never give a number of stores.\n- You plan a whole trip: the flights, a hotel and a rental car together. When they ask what you can do for travel, lead with the whole trip, then name the parts they can book on their own.\n- You order food delivery and book restaurant tables.\n\nHow you use the buy tool:\n- Call it for anything the shopper wants to find, buy, book or order, a trip with no dates or airport yet included: the tool asks for what it still needs.\n- Never say whether you can buy at a store the shopper names, either way. Only the tool knows, so say you will look and call it.\n- Never write a link. Each link reaches the shopper on its own, so when `link_sent` is true, tell them a link is waiting for them.\n- Tell the shopper no reply is needed only when `shopper_need_not_reply` is true.\n- Ask for one thing per message, the step `next_action` names, and leave every later step for a later message.\n- Never tell the shopper you can't show Markdown, a table or other formatting, and never say you keep to plain text.";
5
5
  readonly markdown: "The app shows your words as Markdown. When the shopper asks for a table, a list or bold, write it in Markdown. Use a list or a table where it reads better than a sentence, and keep each message short all the same.";
@@ -1,6 +1,6 @@
1
1
  // GENERATED by scripts/app-instructions.mjs from apps/agent-voice/kb/app.md. Do not edit: edit kb/app.md.
2
2
  /** The version of the voice package the text was copied from. */
3
- export const VOICE_VERSION = "0.2.79";
3
+ export const VOICE_VERSION = "0.2.80";
4
4
  export const APP_TEXT = {
5
5
  "core": "You help a shopper buy what they ask for, with the `buy` tool from Agentcard.\n\nWhen the shopper asks what you can do, answer from these lines without calling the tool:\n- You buy from Amazon, Walmart, Target, Best Buy and thousands more stores. Say \"and thousands more stores\" every time you name the stores you shop, in a reply about one kind of thing too, such as \"travel gear from Amazon, Target and thousands more stores\". Never let a few names read as the whole list, never say \"most online stores\", \"plenty more\" or \"many more\", and never give a number of stores.\n- You plan a whole trip: the flights, a hotel and a rental car together. When they ask what you can do for travel, lead with the whole trip, then name the parts they can book on their own.\n- You order food delivery and book restaurant tables.\n\nHow you use the buy tool:\n- Call it for anything the shopper wants to find, buy, book or order, a trip with no dates or airport yet included: the tool asks for what it still needs.\n- Never say whether you can buy at a store the shopper names, either way. Only the tool knows, so say you will look and call it.\n- Never write a link. Each link reaches the shopper on its own, so when `link_sent` is true, tell them a link is waiting for them.\n- Tell the shopper no reply is needed only when `shopper_need_not_reply` is true.\n- Ask for one thing per message, the step `next_action` names, and leave every later step for a later message.\n- Never tell the shopper you can't show Markdown, a table or other formatting, and never say you keep to plain text.",
6
6
  "markdown": "The app shows your words as Markdown. When the shopper asks for a table, a list or bold, write it in Markdown. Use a list or a table where it reads better than a sentence, and keep each message short all the same.",
@@ -32,7 +32,9 @@
32
32
  * the messages like someone texting.
33
33
  */
34
34
  import type { BuyPlan } from './plan.js';
35
- import { AgentcardClient, type BuyCart, type BuyCartBuilding, type BuyChoice, type BuyFlightItinerary, type BuyDeliveryAddress, type BuyNextAction, type BuyPick, type BuyProgress, type BuyTurnStatus, type BuyAvailability, type BuyStorePick, type BuyTurn, type FetchLike } from './client.js';
35
+ import type { BuyMerchantStatusUpdate, BuySearchStatus } from './search-status.js';
36
+ import type { BuyFulfilledBy } from './fulfilled-by.js';
37
+ import { AgentcardClient, type BuyCart, type BuyCartBuilding, type BuyChoice, type BuyCodeRequest, type BuyFlightItinerary, type BuyDeliveryAddress, type BuyNextAction, type BuyPick, type BuyProgress, type BuyTurnStatus, type BuyAvailability, type BuyStorePick, type BuyTurn, type FetchLike } from './client.js';
36
38
  import { type BuyToolDefinition, type BuyToolInput, type BuyToolInputSchema, type BuyToolStandardSchema, type OpenAIBuyTool } from './definition.js';
37
39
  import { type DirectCall } from './direct.js';
38
40
  import { type BuyToolModelView } from './model-view.js';
@@ -90,6 +92,11 @@ export interface BuyToolLink {
90
92
  storeDomain?: string | null;
91
93
  /** On a product link: the product's name. */
92
94
  title?: string;
95
+ /**
96
+ * On a product link: who sells or books the product, where that is not the store it names, such as a ticket's
97
+ * `{ name: 'StubHub', kind: 'marketplace', line: 'via StubHub' }`. Show `line` quietly under the price. Absent when none.
98
+ */
99
+ fulfilledBy?: BuyFulfilledBy;
93
100
  /**
94
101
  * On a product link: what taking the product does, so a button can say it. `reserve` is a table the
95
102
  * shopper reserves, `book` a flight, room, class or ticket they book, and `buy` anything they buy.
@@ -177,6 +184,8 @@ export interface BuyToolProduct {
177
184
  linkSent: boolean;
178
185
  /** For a place to book: whether it has room at the asked time. Absent on a product with no such question. */
179
186
  availability?: BuyAvailability;
187
+ /** Who sells or books it, where that is not the store it names (`via StubHub`); absent when Agentcard named none. */
188
+ fulfilledBy?: BuyFulfilledBy;
180
189
  }
181
190
  /** One answer of the buy tool, whole, for your code. Hand it to `forModel` before a model reads it. */
182
191
  export interface BuyToolResult {
@@ -234,6 +243,8 @@ export interface BuyToolResult {
234
243
  cart_building?: BuyCartBuilding | null;
235
244
  /** The checkout plan the answer started or moved, such as a trip's flight, hotel and car with each one's state; null when none. */
236
245
  plan?: BuyPlan | null;
246
+ /** Every store the answer's searches asked and where each stands (`searchAt` narrows it to one store); null when it searched none. */
247
+ search?: BuySearchStatus | null;
237
248
  /**
238
249
  * True when this call sent Agentcard a confirm: the shopper's yes, or the yes the tool carried past a step the shopper
239
250
  * finished outside the chat. Absent when the call sent the shopper's words or read the conversation.
@@ -258,6 +269,8 @@ export interface BuyToolResult {
258
269
  * values one per line under the question. Absent or null when the reply asks none.
259
270
  */
260
271
  choice?: BuyChoice | null;
272
+ /** The wallet's code a store checkout waits for, so an app can show a code box beside the reply; absent or null when none. */
273
+ code_request?: BuyCodeRequest | null;
261
274
  /** Agentcard's answer as it came: the purchase answer, or the body of the refusal. */
262
275
  envelope: Record<string, unknown>;
263
276
  }
@@ -389,6 +402,11 @@ export interface BuyToolOptions extends BuyStoreWait {
389
402
  * throws costs nothing but the call to it.
390
403
  */
391
404
  onAvailability?: (update: BuyToolAvailability) => void | Promise<void>;
405
+ /**
406
+ * Hears each store a call's searches ask, the moment its status changes (`BuyMerchantStatusUpdate`): `searching` when its
407
+ * search starts, then what it found. One that throws costs nothing but the call to it.
408
+ */
409
+ onMerchantStatus?: (update: BuyMerchantStatusUpdate) => void | Promise<void>;
392
410
  /**
393
411
  * Hears the products a call's search found, the moment the search answers and before Agentcard writes its reply,
394
412
  * with the conversation, when the search ran (`asOf`, as the answer's `catalog.as_of` names it) and the store's name.
@@ -616,6 +634,8 @@ export interface BuyToolTurnOptions extends BuyStoreWait {
616
634
  onFound?: BuyToolOptions['onFound'];
617
635
  /** Hears whether the places this turn's cards show have room, as the tool's `onAvailability` does. */
618
636
  onAvailability?: BuyToolOptions['onAvailability'];
637
+ /** Hears each store this turn's searches ask, as the tool's `onMerchantStatus` does. */
638
+ onMerchantStatus?: BuyToolOptions['onMerchantStatus'];
619
639
  /** Hears the products each search of this turn's calls found, the moment it answers, as the tool's `onProducts` does. */
620
640
  onProducts?: BuyToolOptions['onProducts'];
621
641
  /**
package/dist/buy-tool.js CHANGED
@@ -242,7 +242,7 @@ export function productsOf(turn) {
242
242
  /** A product's card as `sendLink` hears it: the id the model reads and a pick takes, never the store page, which is `url`. A flight option has no page at a store: its card carries no url, and its productId picks it. */
243
243
  function productLink(product) {
244
244
  const productId = pickId(product);
245
- return { kind: 'product', url: product.url || null, title: product.name, price: productPrice(product), ...(productId ? { productId } : {}), action: product.action ?? 'buy', imageUrl: product.imageUrl, images: product.images ?? (product.imageUrl ? [product.imageUrl] : []), store: product.store ?? null, storeDomain: product.storeDomain ?? null, expiresAt: null, ...(product.availability ? { availability: product.availability } : {}) };
245
+ return { kind: 'product', url: product.url || null, title: product.name, price: productPrice(product), ...(productId ? { productId } : {}), action: product.action ?? 'buy', imageUrl: product.imageUrl, images: product.images ?? (product.imageUrl ? [product.imageUrl] : []), store: product.store ?? null, storeDomain: product.storeDomain ?? null, expiresAt: null, ...(product.availability ? { availability: product.availability } : {}), ...(product.fulfilledBy ? { fulfilledBy: product.fulfilledBy } : {}) };
246
246
  }
247
247
  /** A host name: dot-separated labels of letters, digits and hyphens, ending in a letters-only top-level label. */
248
248
  const STORE_HOST = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
@@ -278,6 +278,7 @@ function productsOfCatalog(catalog) {
278
278
  action: productAction(catalog.merchant),
279
279
  linkSent: false,
280
280
  ...(item.availability ? { availability: item.availability } : {}),
281
+ ...(item.fulfilled_by ? { fulfilledBy: item.fulfilled_by } : {}),
281
282
  }));
282
283
  }
283
284
  /** The stores an answer presents, as the tool reads them. */
@@ -322,7 +323,9 @@ function answerOf(turn) {
322
323
  hashes: hashesOf(turn.cart, turn.carts),
323
324
  cart_building: turn.cart_building,
324
325
  choice: turn.choice ?? null,
326
+ code_request: turn.code_request ?? null,
325
327
  plan: turn.plan ?? null,
328
+ search: turn.search ?? null,
326
329
  envelope: turn.raw,
327
330
  };
328
331
  }
@@ -767,7 +770,7 @@ export function buyTool(options) {
767
770
  if (o.stream !== undefined && o.stream !== false)
768
771
  throw new TypeError('Pass stream as false to wait for each whole answer, or leave it out.');
769
772
  const onProgress = o.onProgress;
770
- for (const key of ['onStatus', 'onFound', 'onAvailability']) {
773
+ for (const key of ['onStatus', 'onFound', 'onAvailability', 'onMerchantStatus']) {
771
774
  if (o[key] !== undefined && typeof o[key] !== 'function')
772
775
  throw new TypeError(`Pass ${key} as a function, or leave it out.`);
773
776
  }
@@ -848,6 +851,7 @@ export function buyTool(options) {
848
851
  onStatus: o.onStatus,
849
852
  onFound: o.onFound,
850
853
  onAvailability: o.onAvailability,
854
+ onMerchantStatus: o.onMerchantStatus,
851
855
  onProducts: o.onProducts,
852
856
  onText: o.onText,
853
857
  texting: textingByDefault,
@@ -1961,6 +1965,18 @@ export function buyTool(options) {
1961
1965
  },
1962
1966
  }
1963
1967
  : {}),
1968
+ ...(t.onMerchantStatus
1969
+ ? {
1970
+ onMerchantStatus: async (update) => {
1971
+ try {
1972
+ await t.onMerchantStatus(update);
1973
+ }
1974
+ catch {
1975
+ // The app's display failing never fails the turn.
1976
+ }
1977
+ },
1978
+ }
1979
+ : {}),
1964
1980
  ...(t.onText || writtenText
1965
1981
  ? {
1966
1982
  onSegment: async ({ text, conversationId }) => {
@@ -2315,7 +2331,7 @@ export function buyTool(options) {
2315
2331
  const own = options ?? {};
2316
2332
  if (typeof own !== 'object')
2317
2333
  throw new TypeError('Pass the turn options as an object, or leave them out.');
2318
- for (const key of ['sendLink', 'onWorking', 'onReaction', 'onResult', 'onProgress', 'onStatus', 'onFound', 'onAvailability', 'onItem', 'onMessage', 'onProducts', 'onText', 'onCart']) {
2334
+ for (const key of ['sendLink', 'onWorking', 'onReaction', 'onResult', 'onProgress', 'onStatus', 'onFound', 'onAvailability', 'onMerchantStatus', 'onItem', 'onMessage', 'onProducts', 'onText', 'onCart']) {
2319
2335
  if (own[key] !== undefined && typeof own[key] !== 'function')
2320
2336
  throw new TypeError(`Pass ${key} to the turn as a function, or leave it out.`);
2321
2337
  }
@@ -2399,6 +2415,7 @@ export function buyTool(options) {
2399
2415
  onStatus: own.onStatus ?? o.onStatus,
2400
2416
  onFound: own.onFound ?? o.onFound,
2401
2417
  onAvailability: own.onAvailability ?? o.onAvailability,
2418
+ onMerchantStatus: own.onMerchantStatus ?? o.onMerchantStatus,
2402
2419
  onProducts: own.onProducts ?? o.onProducts,
2403
2420
  // Agentcard's message takes its place in the turn's sequence, after the items before it, and goes to onText alone, so
2404
2421
  // an app that passes onItem too hears it once.
package/dist/client.d.ts CHANGED
@@ -9,6 +9,8 @@
9
9
  * token cache follows its client (packages/checkout/src/client.ts accessToken).
10
10
  */
11
11
  import { type BuyPlan } from './plan.js';
12
+ import { type BuyMerchantStatusUpdate, type BuySearchStatus } from './search-status.js';
13
+ import { type BuyFulfilledBy } from './fulfilled-by.js';
12
14
  import { type AgentcardListener, type ListenOptions } from './listen.js';
13
15
  import { type ShopperMessage } from './shopper-message.js';
14
16
  /** The fetch the client calls: Node 18 and later have one built in. */
@@ -162,6 +164,11 @@ export interface BuyStreamHandlers {
162
164
  onReplyWritten?: (update: {
163
165
  conversationId: string | null;
164
166
  }) => void | Promise<void>;
167
+ /**
168
+ * Each store the turn searches, the moment its status changes: `searching` when its search starts, then what it found. To
169
+ * follow one store, keep the updates `isMerchantStatusAt(update, 'walmart.com')` accepts. The answer's `search` holds every store.
170
+ */
171
+ onMerchantStatus?: (update: BuyMerchantStatusUpdate) => void | Promise<void>;
165
172
  }
166
173
  export declare const DEFAULT_BASE_URL = "https://api.agentcard.sh";
167
174
  /** The wait for one purchase turn: past Agentcard's own 110-second budget, so a slow turn answers `turn_timeout` rather than aborting here. */
@@ -346,6 +353,8 @@ export interface BuyCartLine {
346
353
  ref: string | null;
347
354
  /** The product's page at its store, as an http or https address, or null when Agentcard sent none. */
348
355
  url: string | null;
356
+ /** Who sells or books the line, where that is not the store the cart names (`via StubHub`); absent when Agentcard named none. */
357
+ fulfilled_by?: BuyFulfilledBy;
349
358
  }
350
359
  /**
351
360
  * A cart as Agentcard answers it. `totalCents` is the all-in figure the shopper sees; `hash` is what a confirm names.
@@ -356,6 +365,25 @@ export interface BuyCartFeeLine {
356
365
  label: string;
357
366
  amountCents: number;
358
367
  }
368
+ /**
369
+ * A discount or a coupon inside a cart's total, as the store labels it. `amountCents` is negative. `kind` is `discount`
370
+ * for one the store applied on its own, such as a promotion, and `coupon` for a code entered at the store's checkout,
371
+ * which `code` names; `code` is null for a discount.
372
+ */
373
+ export interface BuyCartDiscount {
374
+ label: string;
375
+ amountCents: number;
376
+ kind: 'discount' | 'coupon';
377
+ code: string | null;
378
+ }
379
+ /**
380
+ * Why a cart's card is held for `amountCents`: `flight` and `hotel` for the hold a booking places on the card, and
381
+ * `checkout_estimate` for the headroom above a store's total that covers tax and shipping and is released unused.
382
+ */
383
+ export interface BuyCartHold {
384
+ amountCents: number;
385
+ reason: 'flight' | 'hotel' | 'checkout_estimate';
386
+ }
359
387
  export interface BuyCart {
360
388
  merchant: string;
361
389
  merchant_name: string;
@@ -367,6 +395,10 @@ export interface BuyCart {
367
395
  serviceFeesStatus?: 'known' | 'pending' | 'unknown';
368
396
  /** The store's own fee and tax lines inside `serviceFeesCents`, or null when the store itemized none. */
369
397
  merchantFeeLines: BuyCartFeeLine[] | null;
398
+ /** The discounts and coupons inside `totalCents`, which `merchantFeeLines` also lists. Empty when there are none, and from an older API. */
399
+ discounts?: BuyCartDiscount[];
400
+ /** True while the store's checkout builds this cart again, so `totalCents` is not its price yet. False from an older API. */
401
+ pricing?: boolean;
370
402
  tipCents: number;
371
403
  totalCents: number;
372
404
  /**
@@ -377,6 +409,8 @@ export interface BuyCart {
377
409
  /** The ISO 4217 code the store prices the cart in, or null when Agentcard sent none. */
378
410
  currency: string | null;
379
411
  approvedCeilingCents: number | null;
412
+ /** Why the card is held for `approvedCeilingCents`, or null when nothing is held or the API is older. */
413
+ hold?: BuyCartHold | null;
380
414
  hash: string;
381
415
  }
382
416
  /** A product a search found. `priceCents` is in the minor unit of `currency`, the store's ISO 4217 code. */
@@ -413,6 +447,8 @@ export interface BuyCatalogItem {
413
447
  url: string | null;
414
448
  /** Whether a place has room at the asked time (BuyAvailability). Absent on a product with no such question. */
415
449
  availability?: BuyAvailability;
450
+ /** Who sells or books the product, where that is not the store the card names (`booked with OpenTable`); absent when Agentcard named none. */
451
+ fulfilled_by?: BuyFulfilledBy;
416
452
  }
417
453
  /**
418
454
  * The products the last search found. `presented` is true when this answer's reply presents them: the
@@ -510,6 +546,19 @@ export interface BuyPaymentSource {
510
546
  * (the code a booking texted them), then `approve` (the approval link). A value this version does
511
547
  * not know reads as null.
512
548
  */
549
+ /**
550
+ * A store checkout waiting for the one-time code Stripe Link or Shop Pay sent the shopper: which wallet, whether it was a
551
+ * text or an email, and where it went, masked to a phone's last two digits ("••21") or an email's first letter and domain
552
+ * ("j•••@example.com"). The shopper sends the code as their next message, as `next_action: 'verification_code'` says.
553
+ */
554
+ export interface BuyCodeRequest {
555
+ store: string;
556
+ wallet: 'link' | 'shop_pay';
557
+ channel: 'text' | 'email';
558
+ destination: string | null;
559
+ }
560
+ /** An answer's code request, from its `browser_checkout` when that checkout waits on a wallet's code; null otherwise. */
561
+ export declare function codeRequestOf(value: unknown): BuyCodeRequest | null;
513
562
  export type BuyNextAction = 'shipping_details' | 'guest_details' | 'store_login' | 'add_card' | 'confirm' | 'verification_code' | 'approve';
514
563
  /**
515
564
  * A cart the store's checkout is still building after an answer went out, at `store`, since `started_at`: its cart and
@@ -538,6 +587,8 @@ export interface BuyTurn {
538
587
  approval_url: string | null;
539
588
  charge_status: string | null;
540
589
  error_code: string | null;
590
+ /** The party of the purchase this answer says failed, such as StubHub for a ticket, so an app names who stopped it; null when none is named. */
591
+ fulfilled_by: BuyFulfilledBy | null;
541
592
  authorization_id: string | null;
542
593
  merchant_total_cents: number | null;
543
594
  merchant_currency: string | null;
@@ -548,6 +599,8 @@ export interface BuyTurn {
548
599
  approval_in_progress: boolean | null;
549
600
  /** The one step the shopper takes next, or null when nothing waits on them or Agentcard named none. */
550
601
  next_action: BuyNextAction | null;
602
+ /** The wallet's code a store checkout waits for, with where it went, or null when none waits (BuyCodeRequest). */
603
+ code_request: BuyCodeRequest | null;
551
604
  /** The page where the shopper signs in to a store's own account before Agentcard can buy there. */
552
605
  store_login_url: string | null;
553
606
  store_login_expires_at: string | null;
@@ -565,6 +618,8 @@ export interface BuyTurn {
565
618
  choice: BuyChoice | null;
566
619
  /** The checkout plan this answer's yes started or moved, with each item's state; null when none, and on an older API. */
567
620
  plan: BuyPlan | null;
621
+ /** Every store the turn searched and where each stands; null for a turn that searched none, and on an older API. */
622
+ search: BuySearchStatus | null;
568
623
  /** The answer as Agentcard sent it. */
569
624
  raw: Record<string, unknown>;
570
625
  }
package/dist/client.js CHANGED
@@ -9,11 +9,13 @@
9
9
  * token cache follows its client (packages/checkout/src/client.ts accessToken).
10
10
  */
11
11
  import { planOf } from './plan.js';
12
+ import { merchantStatusOf, searchOf } from './search-status.js';
12
13
  import { isRecord } from './verify.js';
14
+ import { failedPartyOf, fulfilledByField } from './fulfilled-by.js';
13
15
  import { listenToAgentcard } from './listen.js';
14
16
  import { messageInChat } from './shopper-message.js';
15
17
  /** The handlers that ask Agentcard to stream a turn. */
16
- const STREAM_HANDLERS = ['onProgress', 'onStatus', 'onFound', 'onCatalog', 'onSegment', 'onAck', 'onCart', 'onDelta', 'onAvailability', 'onReplyWritten'];
18
+ const STREAM_HANDLERS = ['onProgress', 'onStatus', 'onFound', 'onCatalog', 'onSegment', 'onAck', 'onCart', 'onDelta', 'onAvailability', 'onReplyWritten', 'onMerchantStatus'];
17
19
  const PROGRESS_STAGES = new Set(['searching', 'building_cart', 'checking_out', 'working']);
18
20
  export const DEFAULT_BASE_URL = 'https://api.agentcard.sh';
19
21
  /** The wait for one purchase turn: past Agentcard's own 110-second budget, so a slow turn answers `turn_timeout` rather than aborting here. */
@@ -59,6 +61,15 @@ const TRACEPARENT = /^[\da-f]{2}-[\da-f]{32}-[\da-f]{16}-[\da-f]{2}$/;
59
61
  // A tracestate is printable ASCII of at most 512 characters (W3C); any other value is left off, so it can never stop a purchase.
60
62
  const TRACESTATE = /^[\x20-\x7e]{1,512}$/;
61
63
  const pickTrace = (c) => typeof c.tracestate === 'string' && TRACESTATE.test(c.tracestate) ? { traceparent: c.traceparent, tracestate: c.tracestate } : { traceparent: c.traceparent };
64
+ /** An answer's code request, from its `browser_checkout` when that checkout waits on a wallet's code; null otherwise. */
65
+ export function codeRequestOf(value) {
66
+ if (!isRecord(value) || value.status !== 'needs_code' || !str(value.store) || !isRecord(value.code))
67
+ return null;
68
+ const { wallet, channel, destination } = value.code;
69
+ if ((wallet !== 'link' && wallet !== 'shop_pay') || (channel !== 'text' && channel !== 'email'))
70
+ return null;
71
+ return { store: value.store, wallet, channel, destination: str(destination) };
72
+ }
62
73
  const NEXT_ACTIONS = new Set(['shipping_details', 'guest_details', 'store_login', 'add_card', 'confirm', 'verification_code', 'approve']);
63
74
  /**
64
75
  * A refusal, or a failure to reach Agentcard. `code` is the API's own
@@ -146,6 +157,23 @@ function feeLinesOf(v) {
146
157
  const lines = v.filter(isRecord).flatMap((f) => (str(f.label) && num(f.amountCents) !== null ? [{ label: f.label, amountCents: f.amountCents }] : []));
147
158
  return lines.length ? lines : null;
148
159
  }
160
+ const HOLD_REASONS = new Set(['flight', 'hotel', 'checkout_estimate']);
161
+ function holdOf(v) {
162
+ if (!isRecord(v) || typeof v.reason !== 'string' || !HOLD_REASONS.has(v.reason))
163
+ return null;
164
+ const amount = num(v.amountCents);
165
+ return amount !== null && amount > 0 ? { amountCents: amount, reason: v.reason } : null;
166
+ }
167
+ function discountsOf(v) {
168
+ if (!Array.isArray(v))
169
+ return [];
170
+ return v.filter(isRecord).flatMap((d) => {
171
+ const amount = num(d.amountCents);
172
+ if (!str(d.label) || amount === null || !(amount < 0))
173
+ return [];
174
+ return [d.kind === 'coupon' ? { label: d.label, amountCents: amount, kind: 'coupon', code: str(d.code) } : { label: d.label, amountCents: amount, kind: 'discount', code: null }];
175
+ });
176
+ }
149
177
  function feesOf(v) {
150
178
  const amount = num(v.serviceFeesCents);
151
179
  const status = v.merchantFeesPending === true || v.serviceFeesStatus === 'pending' ? 'pending'
@@ -174,16 +202,20 @@ function cartOf(v) {
174
202
  product_id: str(i.product_id),
175
203
  ref: str(i.ref),
176
204
  url: pageUrl(i.url),
205
+ ...fulfilledByField(i),
177
206
  };
178
207
  }) : [],
179
208
  subtotalCents: num(v.subtotalCents),
180
209
  ...feesOf(v),
181
210
  merchantFeeLines: feeLinesOf(v.merchantFeeLines),
211
+ discounts: discountsOf(v.discounts),
212
+ pricing: v.pricing === true,
182
213
  tipCents: num(v.tipCents) ?? 0,
183
214
  totalCents: num(v.totalCents) ?? 0,
184
215
  totalIsEstimate: v.totalIsEstimate === true,
185
216
  currency: str(v.currency),
186
217
  approvedCeilingCents: num(v.approvedCeilingCents),
218
+ hold: holdOf(v.hold),
187
219
  hash: v.hash,
188
220
  };
189
221
  }
@@ -226,6 +258,7 @@ function catalogItemsOf(v) {
226
258
  store: storeOf(i.store),
227
259
  url: pageUrl(i.url),
228
260
  ...availabilityField(i),
261
+ ...fulfilledByField(i),
229
262
  };
230
263
  }) : [];
231
264
  }
@@ -286,6 +319,7 @@ export function turnOf(body) {
286
319
  approval_url: str(body.approval_url),
287
320
  charge_status: str(body.charge_status),
288
321
  error_code: str(body.error_code),
322
+ fulfilled_by: failedPartyOf(body.placements),
289
323
  authorization_id: str(body.authorization_id),
290
324
  merchant_total_cents: num(body.merchant_total_cents),
291
325
  merchant_currency: str(body.merchant_currency),
@@ -298,12 +332,14 @@ export function turnOf(body) {
298
332
  store_login_expires_at: str(body.store_login_expires_at),
299
333
  store_login_merchant_name: str(body.store_login_merchant_name),
300
334
  next_action: typeof body.next_action === 'string' && NEXT_ACTIONS.has(body.next_action) ? body.next_action : null,
335
+ code_request: codeRequestOf(body.browser_checkout),
301
336
  pick: isRecord(body.pick) && str(body.pick.product_id) ? { product_id: body.pick.product_id, ref: str(body.pick.ref), in_cart: body.pick.in_cart === true } : null,
302
337
  stores: storesOf(body.stores),
303
338
  store_pick: isRecord(body.store_pick) && str(body.store_pick.store_id) ? { store_id: body.store_pick.store_id, name: str(body.store_pick.name), selected: body.store_pick.selected === true } : null,
304
339
  cart_building: cartBuildingOf(body.cart_building),
305
340
  choice: choiceOf(body.choice),
306
341
  plan: planOf(body.plan),
342
+ search: searchOf(body.search),
307
343
  raw: body,
308
344
  };
309
345
  }
@@ -480,7 +516,7 @@ async function eachEvent(res, onEvent) {
480
516
  }
481
517
  /**
482
518
  * A streamed purchase turn, read to its `envelope`: the same answer a JSON turn returns, with its
483
- * status in `http_status`. Each `progress`, `status`, `delta`, `catalog`, `find`, `segment`, `reply_written`, `cart` and `venue_availability` goes to its handler
519
+ * status in `http_status`. Each `progress`, `status`, `delta`, `catalog`, `find`, `segment`, `reply_written`, `cart`, `venue_availability` and `merchant` goes to its handler
484
520
  * on the way, and any other event, such as a `ping` or a `tool`, is read past. A stream that breaks or
485
521
  * ends before its envelope, once it named the conversation, reads as a turn past its budget
486
522
  * (`turn_timeout`), so the tool reads the conversation for what the turn left instead of losing it.
@@ -574,6 +610,11 @@ async function readStreamedTurn(res, stream, timeoutMs) {
574
610
  if (items.length)
575
611
  deliver(() => stream.onAvailability({ conversationId: data.conversation_id, items }));
576
612
  }
613
+ else if (event === 'merchant' && stream.onMerchantStatus) {
614
+ const update = merchantStatusOf(data);
615
+ if (update)
616
+ deliver(() => stream.onMerchantStatus(update));
617
+ }
577
618
  else if (event === 'cart' && stream.onCart && str(data.conversation_id)) {
578
619
  const update = { conversationId: data.conversation_id, cart: cartOf(data.cart), carts: cartsOf(data.carts) };
579
620
  deliver(() => stream.onCart(update));
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Who sells or books a product, where that is not the place its card names: a ticket "via StubHub", an event's tickets
3
+ * "sold by Gomry", a table "booked with OpenTable". Agentcard sends the party on a card, a cart line and a failed
4
+ * purchase as `fulfilled_by: { name, kind }`, and this file reads it and words the one quiet line an app shows under the
5
+ * price. A shopper who reads the party tells one seller's failure from another's, and from Agentcard's.
6
+ *
7
+ * Agentcard names only a party the shopper deals with, never the service behind it, and only where the store the card
8
+ * names does not sell it itself: a retail or Shopify card carries none.
9
+ */
10
+ /** How the party relates to the product: a ticket marketplace, the seller, a table's or class's booking provider, an airline or a hotel. */
11
+ export type BuyFulfilledByKind = 'marketplace' | 'seller' | 'booking_provider' | 'airline' | 'hotel';
12
+ /** The party a shopper deals with for one card, cart line or failed purchase. `line` is the words to show, such as `via StubHub`. */
13
+ export interface BuyFulfilledBy {
14
+ name: string;
15
+ kind: BuyFulfilledByKind;
16
+ line: string;
17
+ }
18
+ /** The quiet line for a party: `via StubHub`, `sold by Gomry`, `booked with OpenTable`, `flown by Delta`. */
19
+ export declare function fulfilledByLine(party: {
20
+ name: string;
21
+ kind: BuyFulfilledByKind;
22
+ }): string;
23
+ /** `fulfilled_by` as Agentcard sent it, or null when it sent none, or a kind this version cannot word. */
24
+ export declare function fulfilledByOf(v: unknown): BuyFulfilledBy | null;
25
+ /** `{ fulfilled_by }` when Agentcard sent a party, else nothing: a card or a line with no party carries no field. */
26
+ export declare function fulfilledByField(v: Record<string, unknown>): {
27
+ fulfilled_by?: BuyFulfilledBy;
28
+ };
29
+ /**
30
+ * The party of the first purchase an answer says failed (`placements[].fulfilled_by` beside an `error_code`), or null when
31
+ * that purchase names none: the turn's `error_code` is that first failure's, so a later failed purchase's party never
32
+ * stands beside it.
33
+ */
34
+ export declare function failedPartyOf(placements: unknown): BuyFulfilledBy | null;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Who sells or books a product, where that is not the place its card names: a ticket "via StubHub", an event's tickets
3
+ * "sold by Gomry", a table "booked with OpenTable". Agentcard sends the party on a card, a cart line and a failed
4
+ * purchase as `fulfilled_by: { name, kind }`, and this file reads it and words the one quiet line an app shows under the
5
+ * price. A shopper who reads the party tells one seller's failure from another's, and from Agentcard's.
6
+ *
7
+ * Agentcard names only a party the shopper deals with, never the service behind it, and only where the store the card
8
+ * names does not sell it itself: a retail or Shopify card carries none.
9
+ */
10
+ const LEAD = {
11
+ marketplace: 'via',
12
+ seller: 'sold by',
13
+ booking_provider: 'booked with',
14
+ airline: 'flown by',
15
+ hotel: 'booked with',
16
+ };
17
+ /** The quiet line for a party: `via StubHub`, `sold by Gomry`, `booked with OpenTable`, `flown by Delta`. */
18
+ export function fulfilledByLine(party) {
19
+ return `${LEAD[party.kind]} ${party.name}`;
20
+ }
21
+ /** `fulfilled_by` as Agentcard sent it, or null when it sent none, or a kind this version cannot word. */
22
+ export function fulfilledByOf(v) {
23
+ if (typeof v !== 'object' || v === null)
24
+ return null;
25
+ const { name, kind } = v;
26
+ if (typeof name !== 'string' || !name.trim() || typeof kind !== 'string' || !Object.hasOwn(LEAD, kind))
27
+ return null;
28
+ const party = { name: name.trim(), kind: kind };
29
+ return { ...party, line: fulfilledByLine(party) };
30
+ }
31
+ /** `{ fulfilled_by }` when Agentcard sent a party, else nothing: a card or a line with no party carries no field. */
32
+ export function fulfilledByField(v) {
33
+ const party = fulfilledByOf(v.fulfilled_by);
34
+ return party ? { fulfilled_by: party } : {};
35
+ }
36
+ /**
37
+ * The party of the first purchase an answer says failed (`placements[].fulfilled_by` beside an `error_code`), or null when
38
+ * that purchase names none: the turn's `error_code` is that first failure's, so a later failed purchase's party never
39
+ * stands beside it.
40
+ */
41
+ export function failedPartyOf(placements) {
42
+ if (!Array.isArray(placements))
43
+ return null;
44
+ for (const p of placements) {
45
+ if (typeof p !== 'object' || p === null)
46
+ continue;
47
+ const { error_code: code, fulfilled_by: party } = p;
48
+ if (typeof code === 'string' && code)
49
+ return fulfilledByOf(party);
50
+ }
51
+ return null;
52
+ }
package/dist/index.d.ts CHANGED
@@ -3,7 +3,8 @@ export { readPurchaseMessage, type PurchaseMessage, type PurchaseMessageKind, ty
3
3
  export { readShopperMessage, isStale, SHOPPER_EVENT_TYPES, NEXT_STEP_KINDS, type NextStepKind, type ShopperMessage, type ShopperMessageKind, type ShopperFacts, type ShopperEventType, type OrderStatus, } from './shopper-message.js';
4
4
  export { ORDER_STEPS, updateOrder, arrivalText, type ShopperOrder, type OrderStep, type OrderPathStep, type OrderStepState, type ArrivalTextOptions, } from './order.js';
5
5
  export { agentcardWebhook, purchaseAgentWebhook, type AgentcardWebhookOptions, type AgentcardWebhookHandler, type PurchaseAgentWebhookOptions, type WebhookFetchRequest, type WebhookRequest, type WebhookResponse, } from './handler.js';
6
- export { AgentcardClient, AgentcardApiError, BUY_TURN_TIMEOUT_MS, DEFAULT_BASE_URL, type AgentcardClientOptions, type AgentcardTraceOptions, type OtelApi, type TraceContext, type FetchLike, type FetchLikeResponse, type BuyProgress, type BuyProgressStage, type BuyCatalogUpdate, type BuyAvailability, type BuyAvailabilityUpdate, type BuyFoundUpdate, type BuyTurnStatus, type BuySegmentUpdate, type BuyCartUpdate, type BuyDeltaUpdate, type BuyStreamHandlers, type Shopper, type BuyLastAnswer, type BuyRequest, type BuyDeliveryAddress, type BuyTurn, type BuyStatus, type BuyCart, type BuyCartBuilding, type BuyCartLine, type BuyCartFeeLine, type BuyStore, type BuyCatalog, type BuyCatalogItem, type BuyFlightItinerary, type BuyPaymentSource, type BuyConversation, type BuyLatestReply, type BuyLastCheckout, type BuyOrder, type VaultLink, type BuyNextAction, type BuyPick, type BuyStoreChoice, type BuyStorePick, type BuyStores, type BuyChoice, type BuyChoiceGroup, type BuyChoiceDelivery, type BuyChoiceOption, } from './client.js';
6
+ export { searchAt, isMerchantStatusAt, type BuySearchStatus, type BuyMerchantStatus, type BuyMerchantSearchStatus, type BuyMerchantStatusUpdate, } from './search-status.js';
7
+ export { AgentcardClient, AgentcardApiError, BUY_TURN_TIMEOUT_MS, DEFAULT_BASE_URL, type AgentcardClientOptions, type AgentcardTraceOptions, type OtelApi, type TraceContext, type FetchLike, type FetchLikeResponse, type BuyProgress, type BuyProgressStage, type BuyCatalogUpdate, type BuyAvailability, type BuyAvailabilityUpdate, type BuyFoundUpdate, type BuyTurnStatus, type BuySegmentUpdate, type BuyCartUpdate, type BuyDeltaUpdate, type BuyStreamHandlers, type Shopper, type BuyLastAnswer, type BuyRequest, type BuyDeliveryAddress, type BuyTurn, type BuyStatus, type BuyCart, type BuyCartBuilding, type BuyCartLine, type BuyCartFeeLine, type BuyCartDiscount, type BuyCartHold, type BuyStore, type BuyCatalog, type BuyCatalogItem, type BuyFlightItinerary, type BuyPaymentSource, type BuyConversation, type BuyLatestReply, type BuyLastCheckout, type BuyOrder, type VaultLink, type BuyNextAction, type BuyPick, type BuyStoreChoice, type BuyStorePick, type BuyStores, type BuyChoice, type BuyCodeRequest, type BuyChoiceGroup, type BuyChoiceDelivery, type BuyChoiceOption, } from './client.js';
7
8
  export { BUY_TOOL_NAME, BUY_TOOL_DESCRIPTION, buyToolDefinition, parseBuyToolInput, type BuyToolDefinition, type BuyToolInput, type BuyToolInputSchema, type BuyToolInputCheck, type BuyToolStandardSchema, type OpenAIBuyTool, } from './definition.js';
8
9
  export { toMessages, messageStream, typingDelay, type MessagePace, type MessageStream, type MessageStreamOptions, } from './messages.js';
9
10
  export { buyTool, nextStepPrompt, type BuyTool, type BuyToolOptions, type BuyToolResult, type BuyToolStatus, type BuyToolLink, type BuyToolFound, type BuyToolAvailability, type BuyToolProduct, type BuyToolProductAction, productAction, type BuyToolStore, type BuyToolTurn, type BuyToolTurnItem, type BuyToolTurnOptions, type BuyToolTurnMessage, type BuyAckMode, } from './buy-tool.js';
@@ -20,3 +21,4 @@ export { turnStream, readTurn, TURN_RECORD_TTL_SECONDS, type TurnStream, type Tu
20
21
  export { TURN_RECOVERY_ON, isTurnId, type TurnEvent, type TurnRecord, type TurnRecordStore } from './turn-record.js';
21
22
  export type { BuyLateResults, BuyStoreWait } from './store-wait.js';
22
23
  export { planOf, type BuyPlan, type BuyPlanItem, type BuyPlanItemState, type BuyPlanStatus } from './plan.js';
24
+ export { fulfilledByLine, fulfilledByOf, type BuyFulfilledBy, type BuyFulfilledByKind } from './fulfilled-by.js';
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export { readPurchaseMessage, } from './message.js';
3
3
  export { readShopperMessage, isStale, SHOPPER_EVENT_TYPES, NEXT_STEP_KINDS, } from './shopper-message.js';
4
4
  export { ORDER_STEPS, updateOrder, arrivalText, } from './order.js';
5
5
  export { agentcardWebhook, purchaseAgentWebhook, } from './handler.js';
6
+ export { searchAt, isMerchantStatusAt, } from './search-status.js';
6
7
  export { AgentcardClient, AgentcardApiError, BUY_TURN_TIMEOUT_MS, DEFAULT_BASE_URL, } from './client.js';
7
8
  export { BUY_TOOL_NAME, BUY_TOOL_DESCRIPTION, buyToolDefinition, parseBuyToolInput, } from './definition.js';
8
9
  export { toMessages, messageStream, typingDelay, } from './messages.js';
@@ -19,3 +20,4 @@ export { AGENTCARD_EVENT_TYPES, } from './events.js';
19
20
  export { turnStream, readTurn, TURN_RECORD_TTL_SECONDS } from './turn-stream.js';
20
21
  export { TURN_RECOVERY_ON, isTurnId } from './turn-record.js';
21
22
  export { planOf } from './plan.js';
23
+ export { fulfilledByLine, fulfilledByOf } from './fulfilled-by.js';
@@ -14,7 +14,8 @@ export interface BuyToolModelCart {
14
14
  /**
15
15
  * `price` is one unit's; `line_total` is what the line costs with its add-ons and quantity, and the
16
16
  * lines' totals add up before `fees`. `options` are the choices made on the line and `variant` the
17
- * variant it is. `product_id` is the id the line's product carries in `products`, or null.
17
+ * variant it is. `product_id` is the id the line's product carries in `products`, or null. `fulfilled_by` is who
18
+ * sells or books the line where that is not the store, such as `via StubHub`; absent when Agentcard named none.
18
19
  */
19
20
  items: Array<{
20
21
  product_id: string | null;
@@ -24,6 +25,7 @@ export interface BuyToolModelCart {
24
25
  line_total: string | null;
25
26
  options: string[];
26
27
  variant: string | null;
28
+ fulfilled_by?: string;
27
29
  }>;
28
30
  fees: string | null;
29
31
  fees_status?: 'known' | 'pending' | 'unknown';
@@ -32,6 +34,19 @@ export interface BuyToolModelCart {
32
34
  label: string;
33
35
  amount: string;
34
36
  }>;
37
+ /**
38
+ * The discounts and coupons inside `total`, each negative, which `fee_lines` also lists: show them as money taken off,
39
+ * never add them to `total` again. `code` names a coupon's code. Absent when the cart has none.
40
+ */
41
+ discounts?: Array<{
42
+ label: string;
43
+ amount: string;
44
+ amount_cents: number;
45
+ kind: 'discount' | 'coupon';
46
+ code: string | null;
47
+ }>;
48
+ /** True while the store's checkout prices this cart, several stores at once included, so `total` is not its price yet; absent once it is priced. */
49
+ pricing?: true;
35
50
  tip: string | null;
36
51
  total: string;
37
52
  /** True when the store has not priced its fees and tax yet: `total` is an estimate, never the final amount. */
@@ -40,6 +55,16 @@ export interface BuyToolModelCart {
40
55
  total_leaves_out: string | null;
41
56
  /** The most the shopper's card can be held for this cart: above `total` at a store that prices its final at checkout, `total` itself for a flight, which holds exactly that; else null. */
42
57
  max_charge: string | null;
58
+ /**
59
+ * Why the card is held for `max_charge`: `flight` or `hotel` for a booking's hold on the card, which the shopper sees
60
+ * on their statement, and `checkout_estimate` for the headroom over a store's total, released unused. Show a hold line
61
+ * for `flight` and `hotel` alone. Absent when nothing is held.
62
+ */
63
+ hold?: {
64
+ amount: string;
65
+ amount_cents: number;
66
+ reason: 'flight' | 'hotel' | 'checkout_estimate';
67
+ };
43
68
  }
44
69
  export interface BuyToolModelProduct {
45
70
  /** What the model passes as `product_id` when the shopper picks this product: Agentcard's ref, never a link. Null when there is none. */
@@ -164,7 +164,12 @@ export function productCardKey(product) {
164
164
  return cardKey(product);
165
165
  return product.flight || product.availability ? pickId(product) : null;
166
166
  }
167
- function cartView(cart) {
167
+ /** True when `store`, the domain an answer's `cart_building` names, is the store `cart` is at. */
168
+ function buildingAt(cart, store) {
169
+ const building = store?.trim().toLowerCase();
170
+ return !!building && [cart.merchant, cart.store?.id].some((name) => typeof name === 'string' && name.toLowerCase() === building);
171
+ }
172
+ function cartView(cart, building) {
168
173
  return {
169
174
  store: stripLinks(cart.store?.name ?? cart.merchant_name) || stripLinks(cart.merchant),
170
175
  items: cart.items.map((line) => ({
@@ -179,10 +184,14 @@ function cartView(cart) {
179
184
  line_total: money(line.lineTotalCents ?? (typeof line.priceCents === 'number' ? line.priceCents * line.qty : null), cart.currency),
180
185
  options: (line.options ?? []).map(stripLinks).filter((o) => o !== ''),
181
186
  variant: line.variant ? stripLinks(line.variant) || null : null,
187
+ ...(line.fulfilled_by ? { fulfilled_by: stripLinks(line.fulfilled_by.line) } : {}),
182
188
  })),
183
189
  fees: cart.serviceFeesCents !== null && cart.serviceFeesCents > 0 ? money(cart.serviceFeesCents, cart.currency) : null,
184
190
  fees_status: cart.serviceFeesStatus,
185
191
  fee_lines: (cart.merchantFeeLines ?? []).map((line) => ({ label: stripLinks(line.label), amount: money(line.amountCents, cart.currency) })),
192
+ ...(cart.discounts?.length ? { discounts: cart.discounts.map((d) => ({ label: stripLinks(d.label), amount: money(d.amountCents, cart.currency), amount_cents: d.amountCents, kind: d.kind, code: d.code })) } : {}),
193
+ ...(cart.pricing === true || buildingAt(cart, building) ? { pricing: true } : {}),
194
+ ...(cart.hold ? { hold: { amount: money(cart.hold.amountCents, cart.currency), amount_cents: cart.hold.amountCents, reason: cart.hold.reason } } : {}),
186
195
  tip: cart.tipCents > 0 ? money(cart.tipCents, cart.currency) : null,
187
196
  total: money(cart.totalCents, cart.currency) ?? money(0, cart.currency),
188
197
  total_is_estimate: cart.totalIsEstimate === true,
@@ -274,8 +283,8 @@ export function forModel(result) {
274
283
  groups: result.choice.groups.map((group) => ({ name: stripLinks(group.name), required: group.required, options: group.options.map((option) => `${stripLinks(option.label)}${option.price_delta_cents ? ` (+$${(option.price_delta_cents / 100).toFixed(2)})` : ''}`) })),
275
284
  } } : {}),
276
285
  reply: replyWithoutLinks(result.reply, products.some(held) || stores.some(heldStore) || result.link_sent === true),
277
- cart: result.cart ? cartView(result.cart) : null,
278
- carts: (result.carts ?? []).map(cartView),
286
+ cart: result.cart ? cartView(result.cart, result.cart_building?.store) : null,
287
+ carts: (result.carts ?? []).map((cart) => cartView(cart, result.cart_building?.store)),
279
288
  products: products.map((product) => ({ product_id: pickId(product), name: stripLinks(product.name), store: stripLinks(product.store ?? ''), price: productPrice(product), link_sent: held(product) })),
280
289
  approval_expires_at: typeof result.approval_expires_at === 'string' ? stripLinks(result.approval_expires_at) || null : null,
281
290
  link_expires_in: expiresIn(linkExpiry(result)),
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Where each store a turn searches stands: the `merchant` events of a streamed turn, one each time a store's status changes, and
3
+ * the `search` block of the answer, every store at once.
4
+ */
5
+ /**
6
+ * Where one store stands in a turn's search.
7
+ *
8
+ * - `searching`: Agentcard is still searching the store.
9
+ * - `confirmed`: the store's own site answered with products, their price and stock read live in this search.
10
+ * - `recently_confirmed`: products Agentcard already knew, their price and stock read live at the store in the last hour.
11
+ * - `confirmation_unavailable`: products found for the store, none of them read live there in this search.
12
+ * - `no_results`: the store answered, with nothing for the ask.
13
+ * - `timed_out`: the search answered without the store, which was still running.
14
+ * - `failed`: the store's search failed.
15
+ */
16
+ export type BuyMerchantSearchStatus = 'searching' | 'confirmed' | 'recently_confirmed' | 'no_results' | 'timed_out' | 'confirmation_unavailable' | 'failed';
17
+ /** One store in the answer's `search` block, as Agentcard sends it. */
18
+ export interface BuyMerchantStatus {
19
+ /** The store's id: a store Agentcard knows by name, such as `walmart`, or the store's domain, such as `livlyclothing.com`. */
20
+ merchant: string;
21
+ /** The store's name, to show the shopper: `Walmart`. */
22
+ merchant_name: string;
23
+ status: BuyMerchantSearchStatus;
24
+ /** How many products the search found at the store. */
25
+ results: number;
26
+ /** How many of them had their price and stock read live (`confirmed`) or in the last hour (`recently_confirmed`). */
27
+ confirmed: number;
28
+ /** Milliseconds from the start of the store's search to its first product; null when it found none. */
29
+ first_result_ms: number | null;
30
+ /** Milliseconds the store's search took, or has taken so far while it reads `searching`. */
31
+ ms: number;
32
+ }
33
+ /** Every store the turn searched, as the answer carries them. */
34
+ export interface BuySearchStatus {
35
+ /** The id of this turn's search. A later turn that searches again has a new one. */
36
+ search_id: string;
37
+ /** True once no store is still being searched. False when the answer left while one still ran. */
38
+ final: boolean;
39
+ /** Milliseconds from the first store's search to the last store's answer. */
40
+ elapsed_ms: number;
41
+ /** Each store, in the order its search started. */
42
+ merchants: BuyMerchantStatus[];
43
+ /** Agentcard's own product index, when the turn read it: how many products it answered and in how many milliseconds. */
44
+ index: {
45
+ results: number;
46
+ ms: number;
47
+ } | null;
48
+ }
49
+ /** One store's status the moment it changes, while the turn runs. */
50
+ export interface BuyMerchantStatusUpdate {
51
+ conversationId: string;
52
+ searchId: string;
53
+ merchant: string;
54
+ merchantName: string;
55
+ status: BuyMerchantSearchStatus;
56
+ results: number;
57
+ confirmed: number;
58
+ firstResultMs: number | null;
59
+ ms: number;
60
+ }
61
+ /** The answer's `search` block, or null when the answer carries none. */
62
+ export declare function searchOf(v: unknown): BuySearchStatus | null;
63
+ /** A `merchant` event's update, or null for one with no conversation, no search or a store this version cannot read. */
64
+ export declare function merchantStatusOf(data: Record<string, unknown>): BuyMerchantStatusUpdate | null;
65
+ /**
66
+ * The search block with only the store you name, matched by its id, its domain or its name, so a caller that searched one store
67
+ * reads that store's status: `searchAt(turn.search, 'walmart.com')`. Null when the block is null; an empty `merchants` when the
68
+ * turn did not search that store.
69
+ */
70
+ export declare function searchAt(search: BuySearchStatus | null, store: string): BuySearchStatus | null;
71
+ /** Whether a `merchant` event is about the store you name, as `searchAt` matches it. */
72
+ export declare function isMerchantStatusAt(update: BuyMerchantStatusUpdate, store: string): boolean;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Where each store a turn searches stands: the `merchant` events of a streamed turn, one each time a store's status changes, and
3
+ * the `search` block of the answer, every store at once.
4
+ */
5
+ const MERCHANT_STATUSES = new Set(['searching', 'confirmed', 'recently_confirmed', 'no_results', 'timed_out', 'confirmation_unavailable', 'failed']);
6
+ const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
7
+ const text = (v) => (typeof v === 'string' && v !== '' ? v : null);
8
+ const count = (v) => (typeof v === 'number' && Number.isFinite(v) && v >= 0 ? Math.round(v) : 0);
9
+ const msOrNull = (v) => (typeof v === 'number' && Number.isFinite(v) && v >= 0 ? Math.round(v) : null);
10
+ /** One store as Agentcard sent it, or null for a line with no store or a status this version does not know. */
11
+ function merchantOf(v) {
12
+ if (!isRecord(v))
13
+ return null;
14
+ const merchant = text(v.merchant);
15
+ if (!merchant || typeof v.status !== 'string' || !MERCHANT_STATUSES.has(v.status))
16
+ return null;
17
+ return {
18
+ merchant,
19
+ merchant_name: text(v.merchant_name) ?? merchant,
20
+ status: v.status,
21
+ results: count(v.results),
22
+ confirmed: count(v.confirmed),
23
+ first_result_ms: msOrNull(v.first_result_ms),
24
+ ms: count(v.ms),
25
+ };
26
+ }
27
+ /** The answer's `search` block, or null when the answer carries none. */
28
+ export function searchOf(v) {
29
+ if (!isRecord(v) || !text(v.search_id) || !Array.isArray(v.merchants))
30
+ return null;
31
+ const index = isRecord(v.index) ? { results: count(v.index.results), ms: count(v.index.ms) } : null;
32
+ return {
33
+ search_id: v.search_id,
34
+ final: v.final !== false,
35
+ elapsed_ms: count(v.elapsed_ms),
36
+ merchants: v.merchants.flatMap((m) => merchantOf(m) ?? []),
37
+ index,
38
+ };
39
+ }
40
+ /** A `merchant` event's update, or null for one with no conversation, no search or a store this version cannot read. */
41
+ export function merchantStatusOf(data) {
42
+ const conversationId = text(data.conversation_id);
43
+ const searchId = text(data.search_id);
44
+ const line = merchantOf(data);
45
+ if (!conversationId || !searchId || !line)
46
+ return null;
47
+ return {
48
+ conversationId, searchId, merchant: line.merchant, merchantName: line.merchant_name, status: line.status,
49
+ results: line.results, confirmed: line.confirmed, firstResultMs: line.first_result_ms, ms: line.ms,
50
+ };
51
+ }
52
+ /** The stores whose domain is not their Agentcard id with `.com` after it, as Agentcard maps them (merchant-status.ts). */
53
+ const KEY_OF_DOMAIN = { 'mcmaster.com': 'mcmastercarr', 'shop.tiktok.com': 'tiktok', 'tiktok.com': 'tiktok' };
54
+ /** A store as you may name it, and as Agentcard names it, folded to one form: `Walmart`, `walmart.com` and `walmart` are one store. */
55
+ function storeKey(store) {
56
+ const bare = store.trim().toLowerCase().replace(/^https?:\/\//, '').replace(/^www\./, '').replace(/\/.*$/, '');
57
+ if (KEY_OF_DOMAIN[bare])
58
+ return KEY_OF_DOMAIN[bare];
59
+ // An accent and an ampersand are spelled out as Agentcard's ids spell them: `Pokémon Center`, `Barnes & Noble`.
60
+ const word = (s) => s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/&/g, 'and').replace(/[^a-z0-9]/g, '');
61
+ return bare.endsWith('.com') ? word(bare.slice(0, -4)) : bare.includes('.') ? bare : word(bare.replace(/^the\s+/, ''));
62
+ }
63
+ /**
64
+ * The search block with only the store you name, matched by its id, its domain or its name, so a caller that searched one store
65
+ * reads that store's status: `searchAt(turn.search, 'walmart.com')`. Null when the block is null; an empty `merchants` when the
66
+ * turn did not search that store.
67
+ */
68
+ export function searchAt(search, store) {
69
+ if (!search)
70
+ return null;
71
+ const want = storeKey(store);
72
+ return { ...search, merchants: search.merchants.filter((m) => storeKey(m.merchant) === want || storeKey(m.merchant_name) === want) };
73
+ }
74
+ /** Whether a `merchant` event is about the store you name, as `searchAt` matches it. */
75
+ export function isMerchantStatusAt(update, store) {
76
+ const want = storeKey(store);
77
+ return storeKey(update.merchant) === want || storeKey(update.merchantName) === want;
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-cards/commerce-sdk",
3
- "version": "0.18.6",
3
+ "version": "0.18.7",
4
4
  "description": "Give your agent one tool that buys for your shoppers at real stores, and post each purchase update Agentcard sends into the shopper's chat, over a connection your server opens or a webhook endpoint.",
5
5
  "homepage": "https://opencommerce.sh",
6
6
  "type": "module",