@agent-cards/commerce-sdk 0.11.0 → 0.13.0

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.d.ts CHANGED
@@ -91,6 +91,8 @@ export interface BuyRequest {
91
91
  confirm?: string | true;
92
92
  /** Continue the shopper's newest open conversation instead of starting one. */
93
93
  resume?: true;
94
+ /** The conversation a confirm of a cart hash belongs to, in place of `resume`. */
95
+ conversation_id?: string;
94
96
  /** Drop the cart on the table and start a new order. */
95
97
  new_order?: true;
96
98
  /**
@@ -117,8 +119,23 @@ export interface BuyStore {
117
119
  export interface BuyCartLine {
118
120
  name: string;
119
121
  qty: number;
122
+ /** One unit's price, as the store lists it. */
120
123
  priceCents?: number;
124
+ /**
125
+ * What the line costs: its price with every add-on, times its quantity, before the store's own fees.
126
+ * The lines' totals add up to the cart's `subtotalCents`. Null when its price is unknown.
127
+ */
128
+ lineTotalCents: number | null;
129
+ /** The choices made on the line, by the store's names ("Carnitas", "Extra Guacamole"). Empty when none. */
130
+ options: string[];
131
+ /** The variant the line is, by the store's name ("Birthday (Medium)"), or null when the store names none. */
132
+ variant: string | null;
121
133
  product_id: string | null;
134
+ /**
135
+ * The id of the product card this line is, as `BuyProduct.ref` names it: a shown product whose `ref`
136
+ * (else `id`) equals it is in the cart. Null from an older API.
137
+ */
138
+ ref: string | null;
122
139
  /** The product's page at its store, as an http or https address, or null when Agentcard sent none. */
123
140
  url: string | null;
124
141
  }
@@ -172,9 +189,16 @@ export interface BuyCatalogItem {
172
189
  ref: string | null;
173
190
  name: string | null;
174
191
  priceCents: number | null;
192
+ /** True when `priceCents` is a "from" price: the lowest price of a product priced by a required choice, such as a burger's patty count or a drink's size. */
193
+ price_from?: true;
175
194
  /** The ISO 4217 code the store prices the product in, or null when Agentcard sent none. */
176
195
  currency: string | null;
177
196
  image_url: string | null;
197
+ /**
198
+ * Every photo of the product Agentcard sent, `image_url` first, each once, at most eight. One entry when the
199
+ * product has one photo, and none when it has none. An older API sends one photo at most.
200
+ */
201
+ image_urls: string[];
178
202
  store: BuyStore | null;
179
203
  /** The product's page at its store, as an http or https address, or null when Agentcard sent none. */
180
204
  url: string | null;
@@ -209,7 +233,7 @@ export interface BuyStoreChoice {
209
233
  eta_minutes: number | null;
210
234
  image_url: string | null;
211
235
  }
212
- /** The stores an answer's reply presents to choose from, at most three, in the order the reply names them. */
236
+ /** The stores an answer's reply presents to choose from, at most eight, in the order the reply names them. */
213
237
  export interface BuyStores {
214
238
  merchant: string;
215
239
  merchant_name: string;
@@ -236,6 +260,15 @@ export interface BuyPaymentSource {
236
260
  * not know reads as null.
237
261
  */
238
262
  export type BuyNextAction = 'shipping_details' | 'guest_details' | 'store_login' | 'add_card' | 'confirm' | 'verification_code' | 'approve';
263
+ /**
264
+ * A cart the store's checkout is still building after an answer went out, at `store`, since `started_at`: its cart and
265
+ * total reach the shopper as a message of their own (purchase.message `cart_ready`), and only the shopper's yes to that
266
+ * total confirms it.
267
+ */
268
+ export interface BuyCartBuilding {
269
+ store: string;
270
+ started_at: string;
271
+ }
239
272
  /** One turn's answer, every field read for its type; a field an older API does not send reads as null. */
240
273
  export interface BuyTurn {
241
274
  conversation_id: string;
@@ -273,6 +306,8 @@ export interface BuyTurn {
273
306
  stores: BuyStores | null;
274
307
  /** The store this turn's `pick.store_id` named, or an answer such as `the second one` picked; null without one. */
275
308
  store_pick: BuyStorePick | null;
309
+ /** The cart the store's checkout is still building after this answer; null when none is, and on an older API. */
310
+ cart_building: BuyCartBuilding | null;
276
311
  /** The answer as Agentcard sent it. */
277
312
  raw: Record<string, unknown>;
278
313
  }
@@ -307,6 +342,8 @@ export interface BuyConversation {
307
342
  conversation_id: string;
308
343
  status: 'open' | 'closed';
309
344
  turn_in_progress: boolean;
345
+ /** The cart the store's checkout is still building on this conversation; null when none is, and on an older API. */
346
+ cart_building: BuyCartBuilding | null;
310
347
  last_checkout: BuyLastCheckout | null;
311
348
  orders: BuyOrder[];
312
349
  carts: BuyCart[];
@@ -342,6 +379,8 @@ export declare class AgentcardApiError extends Error {
342
379
  }
343
380
  /** A turn as Agentcard answers it. */
344
381
  export declare function turnOf(body: Record<string, unknown>): BuyTurn;
382
+ /** A cart being built, as an answer, a refusal or the conversation read names it. */
383
+ export declare function cartBuildingOf(value: unknown): BuyCartBuilding | null;
345
384
  /** The client every shopper's buy tool shares. Build one per set of credentials and keep it. */
346
385
  export declare class AgentcardClient {
347
386
  readonly baseUrl: string;
@@ -394,6 +433,17 @@ export declare class AgentcardClient {
394
433
  * shopper: the listener and the buy tool share one client in one process.
395
434
  */
396
435
  delivered(externalUserId: string, message: ShopperMessage): void;
436
+ /**
437
+ * Notes that your agent answers `message` with a turn of its own (`buy.nextStep`) instead of posting
438
+ * its `text`, so the shopper never reads that text: `delivered` skips it from now on, and no later
439
+ * `buy.turn` hands it to the model as words the shopper read. `buy.nextStep` calls it.
440
+ */
441
+ answered(externalUserId: string, message: ShopperMessage): void;
442
+ /**
443
+ * Takes back `answered` for a message your agent's own turn could not answer (`BuyToolTurn.release` calls it): once you
444
+ * post its text after all, `delivered` keeps it, and a later `buy.turn` hands it to the model as words the shopper read.
445
+ */
446
+ unanswered(externalUserId: string, message: ShopperMessage): void;
397
447
  /**
398
448
  * The messages kept for this shopper, oldest first: each one that reached their chat and that no
399
449
  * history `turn.messages` read has held yet. `buy.turn` reads them when it opens.
package/dist/client.js CHANGED
@@ -76,6 +76,30 @@ function pageUrl(v) {
76
76
  return null;
77
77
  }
78
78
  }
79
+ /** The most photos one product carries. */
80
+ const IMAGES_MAX = 8;
81
+ /**
82
+ * A product's photos: `main` first, then each https address in `list`, in its order, each once, at most
83
+ * IMAGES_MAX. A value that is no https address, or that names a user before its host, is left out.
84
+ */
85
+ function imagesOf(main, list) {
86
+ // The main photo passes the same check as the others: an https address without credentials. It stays as `imageUrl` spells
87
+ // it, so the list still starts with that address, and a later copy of it is skipped by its normalized form.
88
+ const mainPage = pageUrl(main);
89
+ const ok = !!main && !!mainPage && mainPage.startsWith('https:');
90
+ const out = ok ? [main] : [];
91
+ const seen = new Set(ok ? [mainPage] : []);
92
+ for (const v of Array.isArray(list) ? list : []) {
93
+ if (out.length >= IMAGES_MAX)
94
+ break;
95
+ const page = pageUrl(v);
96
+ if (page && page.startsWith('https:') && !seen.has(page)) {
97
+ seen.add(page);
98
+ out.push(page);
99
+ }
100
+ }
101
+ return out;
102
+ }
79
103
  function storeOf(v) {
80
104
  if (!isRecord(v) || !str(v.id))
81
105
  return null;
@@ -94,13 +118,22 @@ function cartOf(v) {
94
118
  merchant: v.merchant,
95
119
  merchant_name: str(v.merchant_name) ?? v.merchant,
96
120
  store: storeOf(v.store),
97
- items: Array.isArray(v.items) ? v.items.filter(isRecord).map((i) => ({
98
- name: str(i.name) ?? '',
99
- qty: num(i.qty) ?? 1,
100
- ...(num(i.priceCents) !== null ? { priceCents: i.priceCents } : {}),
101
- product_id: str(i.product_id),
102
- url: pageUrl(i.url),
103
- })) : [],
121
+ items: Array.isArray(v.items) ? v.items.filter(isRecord).map((i) => {
122
+ const qty = num(i.qty) ?? 1;
123
+ const price = num(i.priceCents);
124
+ return {
125
+ name: str(i.name) ?? '',
126
+ qty,
127
+ ...(price !== null ? { priceCents: price } : {}),
128
+ // An older API sends no line total: the line's price times its quantity is it.
129
+ lineTotalCents: num(i.lineTotalCents) ?? (price !== null ? price * qty : null),
130
+ options: Array.isArray(i.options) ? i.options.filter((o) => typeof o === 'string' && o.trim() !== '') : [],
131
+ variant: str(i.variant),
132
+ product_id: str(i.product_id),
133
+ ref: str(i.ref),
134
+ url: pageUrl(i.url),
135
+ };
136
+ }) : [],
104
137
  subtotalCents: num(v.subtotalCents),
105
138
  serviceFeesCents: num(v.serviceFeesCents) ?? 0,
106
139
  merchantFeeLines: feeLinesOf(v.merchantFeeLines),
@@ -138,8 +171,10 @@ function catalogOf(v) {
138
171
  ref: str(i.ref),
139
172
  name: str(i.name),
140
173
  priceCents: num(i.priceCents),
174
+ ...(i.price_from === true && num(i.priceCents) !== null ? { price_from: true } : {}),
141
175
  currency: str(i.currency),
142
176
  image_url: str(i.image_url),
177
+ image_urls: imagesOf(str(i.image_url), i.image_urls),
143
178
  ...(flight ? { flight } : {}),
144
179
  store: storeOf(i.store),
145
180
  url: pageUrl(i.url),
@@ -194,15 +229,21 @@ export function turnOf(body) {
194
229
  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,
195
230
  stores: storesOf(body.stores),
196
231
  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,
232
+ cart_building: cartBuildingOf(body.cart_building),
197
233
  raw: body,
198
234
  };
199
235
  }
236
+ /** A cart being built, as an answer, a refusal or the conversation read names it. */
237
+ export function cartBuildingOf(value) {
238
+ return isRecord(value) && str(value.store) ? { store: value.store, started_at: str(value.started_at) ?? '' } : null;
239
+ }
200
240
  function conversationOf(body) {
201
241
  const last = isRecord(body.last_checkout) ? body.last_checkout : null;
202
242
  return {
203
243
  conversation_id: str(body.conversation_id) ?? '',
204
244
  status: body.status === 'closed' ? 'closed' : 'open',
205
245
  turn_in_progress: body.turn_in_progress === true,
246
+ cart_building: cartBuildingOf(body.cart_building),
206
247
  last_checkout: last ? {
207
248
  at: str(last.at) ?? '',
208
249
  merchant: str(last.merchant) ?? '',
@@ -516,6 +557,37 @@ export class AgentcardClient {
516
557
  if (this.inbox.size > DELIVERED_SHOPPERS)
517
558
  this.inbox.delete(this.inbox.keys().next().value);
518
559
  }
560
+ /**
561
+ * Notes that your agent answers `message` with a turn of its own (`buy.nextStep`) instead of posting
562
+ * its `text`, so the shopper never reads that text: `delivered` skips it from now on, and no later
563
+ * `buy.turn` hands it to the model as words the shopper read. `buy.nextStep` calls it.
564
+ */
565
+ answered(externalUserId, message) {
566
+ if (!str(externalUserId))
567
+ throw new TypeError('Pass externalUserId: your own id for the shopper the message is for.');
568
+ if (!isRecord(message))
569
+ throw new TypeError('Pass the message onMessage got.');
570
+ if (str(message.eventId)) {
571
+ this.deliveredIds.add(message.eventId);
572
+ if (this.deliveredIds.size > DELIVERED_IDS_KEPT)
573
+ this.deliveredIds.delete(this.deliveredIds.values().next().value);
574
+ }
575
+ const kept = this.inbox.get(externalUserId);
576
+ if (kept)
577
+ this.forgetMessages(externalUserId, kept.filter((m) => m === message || (!!message.eventId && m.eventId === message.eventId)));
578
+ }
579
+ /**
580
+ * Takes back `answered` for a message your agent's own turn could not answer (`BuyToolTurn.release` calls it): once you
581
+ * post its text after all, `delivered` keeps it, and a later `buy.turn` hands it to the model as words the shopper read.
582
+ */
583
+ unanswered(externalUserId, message) {
584
+ if (!str(externalUserId))
585
+ throw new TypeError('Pass externalUserId: your own id for the shopper the message is for.');
586
+ if (!isRecord(message))
587
+ throw new TypeError('Pass the message onMessage got.');
588
+ if (str(message.eventId))
589
+ this.deliveredIds.delete(message.eventId);
590
+ }
519
591
  /**
520
592
  * The messages kept for this shopper, oldest first: each one that reached their chat and that no
521
593
  * history `turn.messages` read has held yet. `buy.turn` reads them when it opens.
@@ -10,9 +10,11 @@ export const BUY_TOOL_NAME = 'buy';
10
10
  export const BUY_TOOL_DESCRIPTION = [
11
11
  'Buy what this shopper asks for at real stores.',
12
12
  "Put the shopper's own words in `ask`.",
13
+ 'When the shopper sends a link to a product, such as a buy.trychannel3.com link, put the whole link in `ask` exactly as they sent it, query string included, with their words beside it: Agentcard buys that exact product from the link, so never describe the link in words instead.',
13
14
  'Tell the shopper what `reply` says, in your own voice, except the products and stores it lists that reach the shopper as cards.',
14
15
  "When `shopper_already_told_looking` is true, the shopper already knows you are working on their request, from a reply or a 👀 on their message, so never acknowledge it again: open with the answer or the question.",
15
16
  'Show the shopper the cart before you confirm it.',
17
+ 'When `cart_building` is set, the store is still building that cart: tell the shopper in one short line that its total is on its way, never ask them to confirm it, and confirm it only after they say yes to the total in the message Agentcard sends next.',
16
18
  'Ask the shopper for exactly one thing per message: the step `next_action` names, even when `reply` lists more. Never mention a later step, and never ask for the next one until the shopper has done this one.',
17
19
  'When `next_action` is null and `reply` is about something other than a cart, ask for no checkout step: a cart from an earlier request gets one short line at most, after the answer to what the shopper just asked, and never a question about confirming or placing it.',
18
20
  'Set `confirm` to true only when the shopper just said yes to the cart in your last result, asks to check out or pay for a cart that waits, says they added the card for a cart that waits, says they approved, or asks for the approval link again.',
@@ -21,23 +23,43 @@ export const BUY_TOOL_DESCRIPTION = [
21
23
  'Each link reaches the shopper on its own.',
22
24
  'When `link_sent` is true, tell the shopper a link is waiting for them.',
23
25
  'Product links reach the shopper on their own, as cards that show each product\'s name, price and picture. When `product_links_sent` is above zero, the shopper already sees those products, so never list them again, by name, price or number, even when `reply` lists them: write one short line, such as which one they want, plus only what the cards do not show, such as why one is the pick. When the shopper asks for links, tell them the links are there; never write or invent a URL.',
26
+ "A product's `price` that reads `from $10.69` is the lowest price it comes at: a size or another choice the shopper makes sets the rest. Say it as `from $10.69`, and never call a price a placeholder or say it shows $0.",
27
+ 'A fact the cards do not show belongs to one product, such as a table time, a class time or where a place is, so name that product with it, by its `name` ("Delfina has 7:45"), never "one of these", "the other two" or "one of the spots i sent": the shopper cannot tell which card that means.',
24
28
  'A `placing` status means the order is going through, so never ask for another approval.',
25
29
  'Tell the shopper they need not reply only when `shopper_need_not_reply` is true.',
26
30
  'After `needs_card` with a cart, Agentcard keeps the cart, so set `confirm` to true once the shopper says they added the card.',
27
31
  "After `needs_card` with no cart, call this tool again with the shopper's request once they say they added a card.",
28
- 'Set `new_order` to true only when the shopper drops the cart.',
32
+ 'Set `new_order` to true only when the shopper drops the cart. When `carts` holds more than one cart and the shopper drops one of them, keep `new_order` false and say in `ask` which cart goes: the other carts stay.',
33
+ 'The shopper can buy from more than one store at once: each store keeps its own cart in `carts`, every cart is its own order, and the shopper confirms each one on its own. Never tell the shopper an order can only come from one store.',
29
34
  "Never tell the shopper that you or Agentcard can't see their name, phone number or address: Agentcard may use the ones saved on their Agentcard account for an order, and `reply` says when it does.",
30
35
  "When the shopper picks one of the `products` in your last result, by its name, its place in the list or a tap that names it, set `product_id` to that product's `product_id`, so Agentcard adds exactly that product; otherwise set it to null.",
36
+ "When the shopper's words fit more than one of the `products`, such as \"both\" or \"add dune\" while you or `reply` still asks which of two editions, set `product_id` to null and pass their words in `ask`: Agentcard adds what they settled and asks about the rest. Never add every product their words could mean.",
31
37
  "When the shopper picks one of the `stores` in your last result, by its name, its place in the list or a tap that names it, set `store_id` to that store's `store_id`, so Agentcard orders from exactly that store; otherwise set it to null. Never set both `product_id` and `store_id`.",
32
38
  'Store links reach the shopper on their own, as cards that show each store\'s name and delivery time. When `store_links_sent` is above zero, never list those stores again: ask which one in one short line.',
33
39
  'When `store_links_sent` is 0 and you offer the `stores`, name each one by its `name`, never only "their links are there".',
34
40
  'When `pick.in_cart` is false, the product the shopper picked is not in the cart: tell the shopper what `reply` says, and never ask them to confirm the cart.',
35
41
  "Talk to the shopper only about their order, in their terms: what is in the cart, what was found and what they can do next. Leave out how it was done, even when `reply` says it: never mention this tool, `reply`, a field, a system, \"my end\" or what you can or can't see, the searches run, the stores checked or not, a timeout, account setup, or a link that did not go out. When parts of the result disagree, say what you are sure of and ask, never which part said what.",
42
+ 'Never tell the shopper what has not happened yet or where things stand inside, such as "nothing\'s been charged yet", "your order isn\'t placed yet" or "the address isn\'t locked in", even when `reply` says it: ask for what is needed instead, such as "should it ship to 1 ferry plz?".',
43
+ 'When `next_action` is `store_login`, `add_card` or `approve`, the message that asks for it is one short line and nothing else, even when `reply` says more, such as "sign in to doordash to place your order", "add your card to place the order" or "approve the payment to place your order (up to $19.02)": no recap of the cart, the address or the price, nothing about tax, delivery, shipping, a hold or what was charged, no word on when the link runs out, and never a request to tell you once it is done. On `approve`, put the cart\'s `max_charge` in the parentheses when it is set, and no other amount. Only when the shopper\'s last message picked what went in the cart does a line about the cart come first, as its own message; after an address, a phone number, a yes or a saved card, the ask is the whole reply.',
44
+ 'When `next_action` is `confirm`, ask in one short message: the items, one total, and to say yes to place it, such as "1 carolina black socks pack from walmart, about $9.57. say yes to place it". Leave out the fees one by one, the hold, and the address unless the shopper just changed it.',
45
+ 'Every line you write reaches the shopper, a line before you call this tool too: write to them as "you", never about "the shopper" or "the user", and never say what you are about to do with this tool, such as "I\'ll add them now".',
36
46
  'Say as fact only what the result shows: never promise what a store will do later, such as confirm a size when the order goes through.',
37
47
  'Never tell the shopper to remove, delete or replace a card or to change a setting, even when `reply` says to.',
38
48
  'Name each link by what it is for, from `next_action`: the link to add a card, to sign in to the store, or to approve the payment.',
39
49
  'Within a step, ask for one detail per message, such as the delivery address before the name for the label.',
40
50
  'Write every sentence in your own style, casing included, even where `reply` is written another way.',
51
+ "When the shopper asks what you can do or which stores you shop at, answer without calling this tool and with no line saying you are looking: one message that names at most four stores plus the kinds of things you can do, such as food delivery and restaurant reservations, then one that asks what they need. Every shopper can buy from Amazon, Walmart, Target and Best Buy, so name those, and no other store unless a result showed it.",
52
+ 'Each paragraph you write reaches the shopper as its own message: give each one job and keep it under about 20 words. On a cart turn, write the confirmation and the one next step, such as "added a little cheeseburger from five guys, about $12.17 before delivery and tax" and then "next, sign in to doordash with the link so i can place it": one total, never a price breakdown your app\'s cart already shows (an app that shows no cart lists each line with its price) and never a fee by its name, rate or formula, such as an order fee, card processing or 2.9% + $0.30, even when `reply` or `fee_lines` names it, unless the shopper asks what the fees are, no promise to show the final total later, and nothing about passkeys, master passwords or Agentcard not seeing the card number unless the shopper asks.',
53
+ 'Never listing products again is for offering choices: when you confirm what is in the cart, name each line by its item, such as "1 Dawn Ultra dish soap, 38 oz", never "the one you picked, shown in the card".',
54
+ 'Say how long a link works only when the shopper asks, with `link_expires_in`, such as "it works for about 24 hours", never as a raw date and time or in UTC, such as `approval_expires_at` holds; when `link_expires_in` is null, say nothing about when it runs out.',
55
+ 'Never call a product the shopper was sent "the card", in any language ("la tarjeta", "o cartão"): to the shopper a card is how they pay. Call it by its name, or "the one i sent".',
56
+ 'When the cart has `total_leaves_out`, its `total` is before those: say so, such as "about $18.41 before tax and shipping", and never say it is with or includes tax, shipping or delivery.',
57
+ 'When the shopper gave a budget, compare it with the cart\'s estimated `total`, never with `max_charge`: `max_charge` is a temporary hold, and what the order does not use is released. Warn about the budget only when `total`, or a shipping cost the result shows, is above it.',
58
+ "Never say how or on whose account an order is placed, and never send the shopper to their Agentcard dashboard, settings or account or ask them to link an account there, even when `reply` says to. When they ask for something an order can't do, such as Prime shipping, say plainly that it doesn't apply and what that means for the order, such as \"prime shipping doesn't apply on these orders, so amazon may add shipping on carts under about $35\".",
59
+ 'Say what was found, never how searching works, even when `reply` says it: never how much of a store\'s stock shows, what a page or a link showed, or whether a store picked something. Say the outcome and the next step, such as "i couldn\'t find it at best buy. want me to check amazon?". Only when the shopper asks what the search covers, answer in one plain line.',
60
+ 'Agentcard charges nothing for a reservation, but the restaurant may take a card on its own page: say what it takes only from the cart\'s item line, which names a "card hold", a "deposit" or "prepaid" when there is one, the same way every time, and never give a reservation a most-you-could-be-charged amount.',
61
+ 'Ask the shopper to "let me know", never "text me", unless your app is a text-message chat.',
62
+ 'Name only the stores and products that reach the shopper as cards in this result, never others in passing, such as "five guys and mcdonald\'s deliver there too", even when `reply` names them: to offer more, ask "want other options?". This is for what you suggest: when you show the cart or ask for a yes to it, name what is in it.',
41
63
  ].join(' ');
42
64
  /** A fresh copy of the tool's definition, so a caller that edits one never changes another. */
43
65
  export function buyToolDefinition() {
@@ -52,7 +74,7 @@ export function buyToolDefinition() {
52
74
  type: 'boolean',
53
75
  description: 'True only when the shopper just said yes to the cart in your last result, asks to check out or pay for a cart that waits, says they added the card for a cart that waits, says they approved, or asks for the approval link again. False otherwise.',
54
76
  },
55
- new_order: { type: 'boolean', description: 'True only when the shopper drops the cart to start a new order. False otherwise.' },
77
+ new_order: { type: 'boolean', description: 'True only when the shopper drops the cart to start a new order. False when they drop one of several carts, and otherwise.' },
56
78
  product_id: {
57
79
  type: ['string', 'null'],
58
80
  description: "The `product_id` of the product the shopper picked from the `products` in your last result. Null when they picked none.",
package/dist/index.d.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  export { verifyAgentcardWebhook, AgentcardSignatureError, type AgentcardSignatureReason, type AgentcardEvent, type VerifyOptions, } from './verify.js';
2
2
  export { readPurchaseMessage, type PurchaseMessage, type PurchaseMessageKind, type PurchaseFacts, } from './message.js';
3
- export { readShopperMessage, isStale, SHOPPER_EVENT_TYPES, type ShopperMessage, type ShopperMessageKind, type ShopperFacts, type ShopperEventType, type OrderStatus, } from './shopper-message.js';
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 { agentcardWebhook, purchaseAgentWebhook, type AgentcardWebhookOptions, type AgentcardWebhookHandler, type PurchaseAgentWebhookOptions, type WebhookFetchRequest, type WebhookRequest, type WebhookResponse, } from './handler.js';
5
- export { AgentcardClient, AgentcardApiError, BUY_TURN_TIMEOUT_MS, DEFAULT_BASE_URL, type AgentcardClientOptions, type FetchLike, type FetchLikeResponse, type BuyProgress, type BuyProgressStage, type BuyCatalogUpdate, type BuyStreamHandlers, type Shopper, type BuyRequest, type BuyDeliveryAddress, type BuyTurn, type BuyStatus, type BuyCart, type BuyCartLine, type BuyCartFeeLine, type BuyStore, type BuyCatalog, type BuyCatalogItem, type BuyFlightItinerary, type BuyPaymentSource, type BuyConversation, type BuyLastCheckout, type BuyOrder, type VaultLink, type BuyNextAction, type BuyPick, type BuyStoreChoice, type BuyStorePick, type BuyStores, } from './client.js';
5
+ export { AgentcardClient, AgentcardApiError, BUY_TURN_TIMEOUT_MS, DEFAULT_BASE_URL, type AgentcardClientOptions, type FetchLike, type FetchLikeResponse, type BuyProgress, type BuyProgressStage, type BuyCatalogUpdate, type BuyStreamHandlers, type Shopper, 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 BuyLastCheckout, type BuyOrder, type VaultLink, type BuyNextAction, type BuyPick, type BuyStoreChoice, type BuyStorePick, type BuyStores, } from './client.js';
6
6
  export { BUY_TOOL_NAME, BUY_TOOL_DESCRIPTION, buyToolDefinition, parseBuyToolInput, type BuyToolDefinition, type BuyToolInput, type BuyToolInputSchema, type BuyToolInputCheck, type BuyToolStandardSchema, type OpenAIBuyTool, } from './definition.js';
7
7
  export { toMessages, messageStream, typingDelay, type MessagePace, type MessageStream, type MessageStreamOptions, } from './messages.js';
8
- export { buyTool, type BuyTool, type BuyToolOptions, type BuyToolResult, type BuyToolStatus, type BuyToolLink, type BuyToolProduct, type BuyToolStore, type BuyToolTurn, type BuyToolTurnItem, type BuyToolTurnOptions, type BuyToolTurnMessage, type BuyAckMode, } from './buy-tool.js';
9
- export { instantAck, instantReaction, ackFor, ACK_REACTION, type AckForOptions } from './ack.js';
10
- export { forModel, forModelError, stripLinks, type BuyToolModelView, type BuyToolModelCart, type BuyToolModelProduct, type BuyToolModelStore, } from './model-view.js';
8
+ export { buyTool, nextStepPrompt, type BuyTool, type BuyToolOptions, type BuyToolResult, type BuyToolStatus, type BuyToolLink, type BuyToolProduct, type BuyToolProductAction, productAction, type BuyToolStore, type BuyToolTurn, type BuyToolTurnItem, type BuyToolTurnOptions, type BuyToolTurnMessage, type BuyAckMode, } from './buy-tool.js';
9
+ export { instantAck, instantReaction, ackFor, keepsYes, ACK_REACTION, type AckForOptions } from './ack.js';
10
+ export { forModel, forModelError, replyAsWritten, stripLinks, type BuyToolModelView, type BuyToolModelCart, type BuyToolModelProduct, type BuyToolModelStore, } from './model-view.js';
11
11
  export { toAnthropicTool, toOpenRouterTool, type AnthropicBuyTool, type OpenRouterToolkit, type ZodLike, } from './adapters.js';
12
12
  export { listenToAgentcard, type AgentcardListener, type ListenOptions, type ListenCursorStore, type StreamFetch, } from './listen.js';
13
+ export { ACTION_LINK_ENDINGS, ACTION_LINK_KINDS, actionLinkId, actionLinkOutcome, isActionLink, type ActionLinkKind, type ActionLinkMessage, type ActionLinkOutcome, } from './action-links.js';
13
14
  export { AGENTCARD_EVENT_TYPES, type AgentcardEventType, } from './events.js';
package/dist/index.js CHANGED
@@ -1,13 +1,14 @@
1
1
  export { verifyAgentcardWebhook, AgentcardSignatureError, } from './verify.js';
2
2
  export { readPurchaseMessage, } from './message.js';
3
- export { readShopperMessage, isStale, SHOPPER_EVENT_TYPES, } from './shopper-message.js';
3
+ export { readShopperMessage, isStale, SHOPPER_EVENT_TYPES, NEXT_STEP_KINDS, } from './shopper-message.js';
4
4
  export { agentcardWebhook, purchaseAgentWebhook, } from './handler.js';
5
5
  export { AgentcardClient, AgentcardApiError, BUY_TURN_TIMEOUT_MS, DEFAULT_BASE_URL, } from './client.js';
6
6
  export { BUY_TOOL_NAME, BUY_TOOL_DESCRIPTION, buyToolDefinition, parseBuyToolInput, } from './definition.js';
7
7
  export { toMessages, messageStream, typingDelay, } from './messages.js';
8
- export { buyTool, } from './buy-tool.js';
9
- export { instantAck, instantReaction, ackFor, ACK_REACTION } from './ack.js';
10
- export { forModel, forModelError, stripLinks, } from './model-view.js';
8
+ export { buyTool, nextStepPrompt, productAction, } from './buy-tool.js';
9
+ export { instantAck, instantReaction, ackFor, keepsYes, ACK_REACTION } from './ack.js';
10
+ export { forModel, forModelError, replyAsWritten, stripLinks, } from './model-view.js';
11
11
  export { toAnthropicTool, toOpenRouterTool, } from './adapters.js';
12
12
  export { listenToAgentcard, } from './listen.js';
13
+ export { ACTION_LINK_ENDINGS, ACTION_LINK_KINDS, actionLinkId, actionLinkOutcome, isActionLink, } from './action-links.js';
13
14
  export { AGENTCARD_EVENT_TYPES, } from './events.js';
package/dist/message.d.ts CHANGED
@@ -21,8 +21,11 @@ import { type AgentcardEvent } from './verify.js';
21
21
  * - `store_connected`: the shopper signed in to their own account at the store from the sign-in link your app sent, so the order can go through on it.
22
22
  * - `store_connect_failed`: the shopper's sign-in to the store did not go through, and the link no longer works.
23
23
  * - `store_connect_expired`: the sign-in link expired before the shopper finished signing in.
24
+ * - `cart_ready`: the store's checkout built the cart an answer named as still being built (`cart_building`), at the store's own total. Nothing is paid until the shopper says yes to it.
25
+ * - `cart_needs_input`: that checkout asks the shopper something first, such as a size, a delivery address, a sign-in to the store or an approval of the most the order can cost.
26
+ * - `cart_failed`: that checkout built no cart, and nothing was ordered or charged.
24
27
  */
25
- export type PurchaseMessageKind = 'order_placed' | 'order_failed' | 'order_not_placed_yet' | 'order_unconfirmed' | 'approval_expiring' | 'approval_expired' | 'approval_declined' | 'payment_declined' | 'card_link_unfinished' | 'card_link_expired' | 'cart_idle' | 'card_added' | 'store_connected' | 'store_connect_failed' | 'store_connect_expired';
28
+ export type PurchaseMessageKind = 'order_placed' | 'order_failed' | 'order_not_placed_yet' | 'order_unconfirmed' | 'approval_expiring' | 'approval_expired' | 'approval_declined' | 'payment_declined' | 'card_link_unfinished' | 'card_link_expired' | 'cart_idle' | 'card_added' | 'store_connected' | 'store_connect_failed' | 'store_connect_expired' | 'cart_ready' | 'cart_needs_input' | 'cart_failed';
26
29
  /** The message's values one by one, for a company that writes its own copy. A fact that does not apply is absent. */
27
30
  export interface PurchaseFacts {
28
31
  merchantName?: string;
@@ -16,17 +16,17 @@
16
16
  export declare function toMessages(text: string): string[];
17
17
  /**
18
18
  * How long someone texting takes to type `message`, in milliseconds: 300, plus 12 a character, at most
19
- * 1,500. `messageStream` and `buy.turn` wait this long between two messages by default, so a model that
20
- * writes three paragraphs in 100 ms still delivers them one at a time.
19
+ * 1,500. With `pace: true`, `messageStream` and `buy.turn` wait this long between two messages, so a
20
+ * model that writes three paragraphs in 100 ms still delivers them one at a time.
21
21
  *
22
22
  * ```ts
23
23
  * typingDelay('which one do you want?'); // 564
24
24
  * ```
25
25
  */
26
26
  export declare function typingDelay(message: string): number;
27
- /** How messages are spaced: `true` by `typingDelay`, `false` not at all, or a function that returns the pause for a message. */
27
+ /** How messages are spaced: `false` (the default) not at all, `true` by `typingDelay`, or a function that returns the pause for a message. */
28
28
  export type MessagePace = boolean | ((message: string) => number);
29
- /** The pause for each message `pace` asks for, or null for none. Throws a TypeError for a pace that cannot work. */
29
+ /** The pause for each message `pace` asks for, or null for none, the default. Throws a TypeError for a pace that cannot work. */
30
30
  export declare function paceOf(pace: unknown): ((message: string) => number) | null;
31
31
  /** A typing hook that never throws: one that fails costs nothing but the indicator. */
32
32
  export declare function typingHook(onTyping: unknown): (typing: boolean) => void;
@@ -49,11 +49,11 @@ export interface MessageStreamOptions {
49
49
  */
50
50
  onMessage: (message: string) => void | Promise<void>;
51
51
  /**
52
- * Spaces the messages like someone texting. Each message after the first waits until `typingDelay`
53
- * of it has passed since the one before it went out, so a model that writes three paragraphs in
54
- * 100 ms still delivers them one at a time. Time the model spent writing counts toward the pause, so
55
- * a slow model gets no added wait. Default `true`. `false` hands each message over the moment it is
56
- * written, and a function returns the pause in milliseconds for a message.
52
+ * How the messages are spaced. Default `false`: each message goes out the moment it is written.
53
+ * `true` spaces them like someone texting: each message after the first waits until `typingDelay` of
54
+ * it has passed since the one before it went out, so a model that writes three paragraphs in 100 ms
55
+ * still delivers them one at a time, and time the model spent writing counts toward the pause. A
56
+ * function returns the pause in milliseconds for a message.
57
57
  */
58
58
  pace?: MessagePace;
59
59
  /**
package/dist/messages.js CHANGED
@@ -122,8 +122,8 @@ const TYPING_MS_PER_CHARACTER = 12;
122
122
  const TYPING_MAX_MS = 1_500;
123
123
  /**
124
124
  * How long someone texting takes to type `message`, in milliseconds: 300, plus 12 a character, at most
125
- * 1,500. `messageStream` and `buy.turn` wait this long between two messages by default, so a model that
126
- * writes three paragraphs in 100 ms still delivers them one at a time.
125
+ * 1,500. With `pace: true`, `messageStream` and `buy.turn` wait this long between two messages, so a
126
+ * model that writes three paragraphs in 100 ms still delivers them one at a time.
127
127
  *
128
128
  * ```ts
129
129
  * typingDelay('which one do you want?'); // 564
@@ -132,12 +132,12 @@ const TYPING_MAX_MS = 1_500;
132
132
  export function typingDelay(message) {
133
133
  return Math.min(TYPING_MAX_MS, TYPING_BASE_MS + TYPING_MS_PER_CHARACTER * [...String(message ?? '')].length);
134
134
  }
135
- /** The pause for each message `pace` asks for, or null for none. Throws a TypeError for a pace that cannot work. */
135
+ /** The pause for each message `pace` asks for, or null for none, the default. Throws a TypeError for a pace that cannot work. */
136
136
  export function paceOf(pace) {
137
- if (pace === undefined || pace === true)
138
- return typingDelay;
139
- if (pace === false)
137
+ if (pace === undefined || pace === false)
140
138
  return null;
139
+ if (pace === true)
140
+ return typingDelay;
141
141
  if (typeof pace !== 'function')
142
142
  throw new TypeError('Pass pace as true, false, or a function that returns the pause before a message in milliseconds.');
143
143
  return (message) => {
@@ -11,10 +11,19 @@ import type { BuyNextAction } from './client.js';
11
11
  import type { BuyToolProduct, BuyToolResult, BuyToolStatus, BuyToolStore } from './buy-tool.js';
12
12
  export interface BuyToolModelCart {
13
13
  store: string;
14
+ /**
15
+ * `price` is one unit's; `line_total` is what the line costs with its add-ons and quantity, and the
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.
18
+ */
14
19
  items: Array<{
20
+ product_id: string | null;
15
21
  name: string;
16
22
  qty: number;
17
23
  price: string | null;
24
+ line_total: string | null;
25
+ options: string[];
26
+ variant: string | null;
18
27
  }>;
19
28
  fees: string | null;
20
29
  /** The store's own fee and tax lines inside `fees`, by the store's labels. Empty when it itemized none. */
@@ -26,6 +35,8 @@ export interface BuyToolModelCart {
26
35
  total: string;
27
36
  /** True when the store has not priced its fees and tax yet: `total` is an estimate, never the final amount. */
28
37
  total_is_estimate: boolean;
38
+ /** What an estimated `total` leaves out, in words the model can repeat; null when the store priced the cart. */
39
+ total_leaves_out: string | null;
29
40
  /** 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. */
30
41
  max_charge: string | null;
31
42
  }
@@ -53,6 +64,13 @@ export interface BuyToolModelView {
53
64
  status: BuyToolStatus;
54
65
  /** The one thing to ask the shopper for in this message, or null when nothing waits on them. Never ask for a later step. */
55
66
  next_action: BuyNextAction | null;
67
+ /**
68
+ * The store whose checkout is still building the cart this answer named, absent when none is: its cart and total reach the
69
+ * shopper in a message from Agentcard, and only the shopper's yes to that total confirms it.
70
+ */
71
+ cart_building?: {
72
+ store: string;
73
+ };
56
74
  /** Agentcard's answer for the shopper, with every link taken out. */
57
75
  reply: string;
58
76
  /** The newest cart on the table. */
@@ -61,6 +79,13 @@ export interface BuyToolModelView {
61
79
  carts: BuyToolModelCart[];
62
80
  products: BuyToolModelProduct[];
63
81
  approval_expires_at: string | null;
82
+ /**
83
+ * How long the newest link this answer sent (Vault, approval or store login) still works, in words the
84
+ * shopper reads, such as `about 24 hours` or `about 15 minutes`; `no longer` once it ran out; null when
85
+ * Agentcard named no expiry. Read when the answer came back, so the model never reckons it from a timestamp.
86
+ */
87
+ link_expires_in: string | null;
88
+ /** True when the shopper need not reply to this answer, as when Agentcard places the order on its own. False beside a step `next_action` names, whose ask is one short line. */
64
89
  shopper_need_not_reply: boolean;
65
90
  /** True when the shopper already got a message this turn saying their request is being worked on, so the reply opens with no acknowledgement of its own. */
66
91
  shopper_already_told_looking: boolean;
@@ -99,6 +124,15 @@ export declare function stripLinks(text: string): string;
99
124
  * decimal point, so 1331 is $13.31 in USD, CA$13.31 in CAD and ¥1,331 in JPY. An amount whose currency
100
125
  * Agentcard did not name shows with no symbol, because a dollar sign on it could be wrong.
101
126
  */
127
+ /**
128
+ * A product's price as the shopper reads it: `$12.99`, or `from $10.69` for a product priced by a
129
+ * required choice (a size, a patty count), whose base price is often $0. Null when Agentcard named none.
130
+ */
131
+ export declare function productPrice(product: {
132
+ priceCents: number | null;
133
+ currency: string | null;
134
+ priceFrom?: true;
135
+ }): string | null;
102
136
  export declare function money(minor: number | null | undefined, currency: string | null | undefined): string | null;
103
137
  /**
104
138
  * The products the model reads: the first twenty that keep a name and a store
@@ -129,6 +163,8 @@ export declare function cardKey(product: Pick<BuyToolProduct, 'id' | 'ref' | 'ur
129
163
  * taps the flight they want (2026-10-20: three flights listed in text, and no card to tap).
130
164
  */
131
165
  export declare function productCardKey(product: Pick<BuyToolProduct, 'id' | 'ref' | 'url' | 'flight'>): string | null;
166
+ /** How long until `iso`, from `now`, as the shopper reads it: `about 24 hours`, `about 15 minutes`, `no longer`, or null. */
167
+ export declare function expiresIn(iso: string | null | undefined, now?: number): string | null;
132
168
  /**
133
169
  * The model's view of one answer: the status, Agentcard's reply, the newest
134
170
  * cart and every cart on the table, the products by id, name and price, and
@@ -136,6 +172,19 @@ export declare function productCardKey(product: Pick<BuyToolProduct, 'id' | 'ref
136
172
  * reads as `needs_input`.
137
173
  */
138
174
  export declare function forModel(result: BuyToolResult): BuyToolModelView;
175
+ /**
176
+ * Agentcard's reply, ready for the shopper as written, when the answer only asks them something: a
177
+ * `needs_input` answer with a reply, such as a product list, a size or a store to pick, or a cart's
178
+ * total and the ask for their yes. The reply comes with every link taken out, as the model reads it.
179
+ * Null for any other answer, which your model puts in its own words, and for a line the tool wrote
180
+ * itself for your model, such as the note that the cart changed or a `nextStep` turn's ask for the
181
+ * shopper's yes: only the reply Agentcard sent, as `envelope` holds it, is written for the shopper.
182
+ *
183
+ * ```ts
184
+ * replyAsWritten(result); // 'found two at Amazon. want the first one?'
185
+ * ```
186
+ */
187
+ export declare function replyAsWritten(result: BuyToolResult): string | null;
139
188
  /**
140
189
  * A thrown error as the model may read it: the same message with every link
141
190
  * taken out, and the original error on `cause` for your logs. A TypeError