@agent-cards/commerce-sdk 0.11.0 → 0.14.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.
@@ -10,9 +10,12 @@ 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.",
16
+ 'When `shopper_already_read_reply` is true, the shopper already read `reply` as Agentcard wrote it, so never say it again in any words: answer with only what `reply` leaves out, in one short line, or with nothing.',
15
17
  'Show the shopper the cart before you confirm it.',
18
+ '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
19
  '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
20
  '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
21
  '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 +24,43 @@ export const BUY_TOOL_DESCRIPTION = [
21
24
  'Each link reaches the shopper on its own.',
22
25
  'When `link_sent` is true, tell the shopper a link is waiting for them.',
23
26
  '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.',
27
+ "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.",
28
+ '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
29
  'A `placing` status means the order is going through, so never ask for another approval.',
25
30
  'Tell the shopper they need not reply only when `shopper_need_not_reply` is true.',
26
31
  'After `needs_card` with a cart, Agentcard keeps the cart, so set `confirm` to true once the shopper says they added the card.',
27
32
  "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.',
33
+ '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.',
34
+ '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
35
  "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
36
  "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.",
37
+ "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
38
  "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
39
  '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
40
  'When `store_links_sent` is 0 and you offer the `stores`, name each one by its `name`, never only "their links are there".',
34
41
  '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
42
  "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.",
43
+ '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?".',
44
+ '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.',
45
+ '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.',
46
+ '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
47
  '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
48
  'Never tell the shopper to remove, delete or replace a card or to change a setting, even when `reply` says to.',
38
49
  '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
50
  'Within a step, ask for one detail per message, such as the delivery address before the name for the label.',
40
51
  'Write every sentence in your own style, casing included, even where `reply` is written another way.',
52
+ "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.",
53
+ '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.',
54
+ '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".',
55
+ '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.',
56
+ '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".',
57
+ '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.',
58
+ '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.',
59
+ "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\".",
60
+ '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.',
61
+ '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.',
62
+ 'Ask the shopper to "let me know", never "text me", unless your app is a text-message chat.',
63
+ '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
64
  ].join(' ');
42
65
  /** A fresh copy of the tool's definition, so a caller that edits one never changes another. */
43
66
  export function buyToolDefinition() {
@@ -52,7 +75,7 @@ export function buyToolDefinition() {
52
75
  type: 'boolean',
53
76
  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
77
  },
55
- new_order: { type: 'boolean', description: 'True only when the shopper drops the cart to start a new order. False otherwise.' },
78
+ 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
79
  product_id: {
57
80
  type: ['string', 'null'],
58
81
  description: "The `product_id` of the product the shopper picked from the `products` in your last result. Null when they picked none.",
@@ -0,0 +1,50 @@
1
+ import type { BuyNextAction } from './client.js';
2
+ import type { BuyToolInput } from './definition.js';
3
+ /** Why a message goes straight to Agentcard. */
4
+ export type DirectReason = 'product_ask' | 'pick' | 'answer' | 'confirm';
5
+ /** The buy call for a message that goes straight to Agentcard, and why it does. */
6
+ export interface DirectCall {
7
+ input: BuyToolInput;
8
+ reason: DirectReason;
9
+ }
10
+ /** What Agentcard last told the shopper, which decides whether a pick or an answer goes straight. */
11
+ export interface DirectContext {
12
+ /**
13
+ * True when the shopper's last turn ended on an answer from Agentcard: its last call returned, and
14
+ * no turn since ran without one. False or left out, only a request to buy goes straight.
15
+ */
16
+ answered?: boolean;
17
+ /** The step Agentcard's last answer asked for, its `next_action`. */
18
+ nextAction?: BuyNextAction | null;
19
+ /** Agentcard's last reply, the question a short answer answers. */
20
+ asked?: string | null;
21
+ /** The products Agentcard's last answer showed, in order, with the `product_id` a pick sends. */
22
+ products?: ReadonlyArray<{
23
+ productId: string | null;
24
+ name: string;
25
+ }>;
26
+ /** The stores Agentcard's last answer showed, with the `store_id` a pick sends. */
27
+ stores?: ReadonlyArray<{
28
+ storeId: string;
29
+ name: string;
30
+ }>;
31
+ }
32
+ /**
33
+ * True when the message asks to buy or book something, read with no context: a shopping verb with what
34
+ * to get, a product link, a product at a named store, or a table. A question, small talk, a pick or an
35
+ * answer, a command to the cart, the shopper's own details and anything about the account read false.
36
+ */
37
+ export declare function isProductAsk(text: string): boolean;
38
+ /**
39
+ * The call a shopper's message makes straight to Agentcard, with no model deciding it, or null when
40
+ * your model decides: see the top of this file for what goes straight. `ask` carries the shopper's
41
+ * words as they sent them, a confirm goes in only for a plain yes to the total Agentcard asked them to
42
+ * confirm, and a pick names its `product_id` or `store_id` only when the message names one exactly.
43
+ *
44
+ * ```ts
45
+ * directCall('find me usb-c cables under $20'); // { input: { ask: 'find me usb-c cables under $20', confirm: false, ... }, reason: 'product_ask' }
46
+ * directCall('what time is it'); // null
47
+ * directCall('yes', { answered: true, nextAction: 'confirm' }); // { input: { ask: 'yes', confirm: true, ... }, reason: 'confirm' }
48
+ * ```
49
+ */
50
+ export declare function directCall(shopperText: string, context?: DirectContext): DirectCall | null;
package/dist/direct.js ADDED
@@ -0,0 +1,237 @@
1
+ /**
2
+ * The buy call a shopper's message makes with no model deciding it: `directCall` reads the message, and
3
+ * what Agentcard last asked the shopper, and returns the call your app sends straight to Agentcard, or
4
+ * null when your model decides. Only three kinds of message go straight:
5
+ *
6
+ * - a request to buy or book something: a shopping verb with what to get (`find me usb-c cables under
7
+ * $20`, `i need a gift for my dad`, `add a toothbrush from walmart to my cart`), a product link, a
8
+ * product at a named store (`aa batteries on amazon`), or a table (`a table for 2 tonight at 7`);
9
+ * - a pick of what Agentcard just showed, by a card's tap (`I'll take the KODAK AA (24 Pack) ($13.49)`,
10
+ * `Order from Shake Shack`), its place (`the first one`, `#2`, `1 and 3`) or a reference (`that one`);
11
+ * - the answer to the question Agentcard just asked: the address, name or code it asked for, a short
12
+ * answer such as `medium` or `size 9`, or a plain yes to the total it asked the shopper to confirm.
13
+ *
14
+ * Everything else goes to the model: a question, small talk, a change of mind (`no`, `wait`, `cancel`),
15
+ * a command to the cart, and anything about the account, an order or how the chat works. A pick or an
16
+ * answer goes straight only while the shopper's last turn ended on Agentcard's answer, so a reply to
17
+ * something your model said reaches your model.
18
+ *
19
+ * The rule is tuned for precision over recall: a message that reaches Agentcard by mistake gets a
20
+ * purchase reply to small talk, while one the model reads by mistake costs only the model's call. On
21
+ * the 188 labelled messages in `test/fixtures/direct-messages.json`, it sends none it should not.
22
+ */
23
+ import { ABOUT, ACKNOWLEDGED, CAPABILITY, CART_COMMAND, CHECKOUT, CHITCHAT, FILLER, asksForDetails, isNewRequest, isPickText, namesStore, plain } from './ack.js';
24
+ /** The longest message that goes straight: a long one is read by the model. */
25
+ const MAX_CHARS = 500;
26
+ /** The steps a shopper answers with their own details or a code. */
27
+ const DETAIL_STEPS = new Set(['shipping_details', 'guest_details', 'verification_code']);
28
+ /** The words of a message: plain, with punctuation, `please` and `thanks` taken out. */
29
+ function wordsOf(text) {
30
+ return plain(text).replace(/[^\p{L}\p{N}\s$'&+./#-]+/gu, ' ').replace(/[!?.,;:]+(?=\s|$)/g, ' ').replace(/[!?,;:]+/g, ' ').replace(/\b(?:please|pls|plz|thanks|thank you|thx|ty)\b/g, ' ').replace(/\s+/g, ' ').trim();
31
+ }
32
+ /** Words a message opens with before it says anything: a greeting or filler. */
33
+ const OPENING = /^(?:(?:hey+|hi+|hello+|yo|hiya|howdy|good (?:morning|afternoon|evening)|ok|okay|so|um|uh|also|and|alright|cool|great)(?: there)?\s+)+/;
34
+ /** A change of mind, a refusal or a stall: never an answer to send on. */
35
+ const TAKEBACK = /^(?:i'?ll think|let me think|thinking|i'?m thinking|let me check|i'?ll check|i'?ll let you know|no|nope|nah|not (?:yet|now|really|sure)|cancel|stop|wait|hold on|hang on|never ?mind|nvm|forget (?:it|that|about it)|start over|nothing|none|neither|later|maybe|idk|i don'?t know|dunno|not sure|hmm+|um+|uh+|lol|lmao|haha+|ok|okay|k|kk|thanks?|ty|cool|nice|great|wow|oh|ah)\b/;
36
+ /** Words that say the message is about something other than buying, wherever they sit. */
37
+ const NOT_SHOPPING = new RegExp([
38
+ 'joke', 'weather', 'forecast', 'news', 'poem', 'riddle', 'lyrics', 'translate', 'homework', 'essay', 'coding', 'python', 'javascript',
39
+ 'bug', 'error', 'crash(?:ed|es)?', 'broken', 'not working', "doesn'?t work", 'refund', 'return policy', 'password', 'log ?in', 'sign ?in',
40
+ 'account', 'api', 'support', 'customer service', 'agent ?card', 'what time', 'time is it', "today'?s date", 'human', 'real person',
41
+ 'cancel', 'track(?:ing)?', "where'?s my", 'status', 'receipt', 'invoice', 'unsubscribe', 'privacy', 'kyc', 'identity', 'verify',
42
+ 'promo', 'coupon', 'discount code', 'charged', 'charge me', 'balance', 'wallet', 'approve', 'approval', 'link',
43
+ '(?:my|a|the|new|your|payment|credit|debit|virtual|bank|visa|mastercard) card', 'card (?:number|details|on file)',
44
+ ]
45
+ .map((word) => `\\b${word}\\b`)
46
+ .join('|'));
47
+ /** A message that opens as a question: the model reads it, unless it asks Agentcard to get something (`can you find me ...`). */
48
+ const QUESTION_START = /^(?:what|whats|what'?s|where|wheres|where'?s|when|why|how|who|whose|which|is|are|was|were|do|does|did|should|shall|will|would|have|has|tell me|explain|any idea|wanna|you)\b/;
49
+ /** A shopping verb and what follows it, after any opening such as `can you`. */
50
+ const SHOP_FRAME = new RegExp('^(?:(?:can|could|would|will) (?:you|u) (?:please )?|please |i\'?d like (?:you )?to |help me )?' +
51
+ '(?:find(?: me| us)?|search(?: for)?|look(?:ing)? for|look up|show me|shop for|buy(?: me)?|order(?: me)?|purchase|get me|get us|grab(?: me)?|pick up|book(?: me)?|reserve|add|' +
52
+ "i want|i need|we need|we want|i'?d like|id like|i would like|i'?m looking for|im looking for|we'?re looking for|i'?m after|im after|" +
53
+ 'need|want|can i get|could i get|can i have|could i have|i\'?ll have|send me|get)\\s+(.+)$');
54
+ /** What a shopping verb takes that names nothing to buy: help, a person, a pronoun, the cart, a question. */
55
+ const NOT_AN_ITEM = /^(?:(?:stuff|things|anything|items|products|everything)(?: (?:on|from|at|in|here|there|online)\b.*)?$|help|some help|assistance|someone|somebody|a human|a person|an agent|to (?!(?:buy|order|get|find|book|reserve|purchase|shop|grab|try)\b)\w+|info|information|advice|more info|you|u|me|it|this|that|these|those|them|nothing|out|lost|more|again|a (?:moment|minute|sec|second|break|nap|hug|vacation|friend)|how|what|where|why|when|who|(?:the |my |our )?(?:cart|basket|order|orders|total|purchases?|history|address)|my)\b/;
56
+ /** A verb that needs an object word after it before it names a thing: `add` alone, or `get` with one word, says too little. */
57
+ const MIN_ITEM_CHARS = 3;
58
+ /** Stores a shopper names by brand, so `aa batteries amazon` reads as a product at a store. */
59
+ const STORES = "amazon|walmart|target|best ?buy|costco|home depot|lowe'?s|ebay|etsy|doordash|door dash|uber ?eats|instacart|cvs|walgreens|nike|adidas|sephora|ulta|macy'?s|nordstrom|ikea|wayfair|chewy|whole foods|trader joe'?s|safeway|kroger|old navy|uniqlo|zara|h&m|rei|staples|office depot|barnes (?:&|and) noble|petco|petsmart|aldi|publix|sam'?s club|shein|temu|newegg|lululemon|patagonia|allbirds|glossier|shake shack|chipotle|starbucks|mcdonald'?s|taco bell|five guys|souvla|opentable|resy|1-800-flowers|zappos|kohl'?s|ace hardware|hollister|abercrombie|gap|apple store|dick'?s sporting goods";
60
+ /** A store's own address, such as `livlyclothing.com`. */
61
+ const DOMAIN = '[a-z0-9][a-z0-9-]*\\.(?:com|co|net|org|shop|store|us|uk|ca|io|co\\.uk)';
62
+ /** A thing at a named store: `aa batteries from amazon`, `bows at livlyclothing.com`, `aa batteries amazon`, `tacos near me`. */
63
+ const AT_STORE = new RegExp(`^(?:a |an |some |\\d+ )?[\\p{L}\\p{N}$'&+./-]+(?: [\\p{L}\\p{N}$'&+./-]+){0,9} (?:(?:(?:from|at|on|off) (?:the )?(?:${STORES}|${DOMAIN}))(?: [\\p{L}\\p{N}$'&+./-]+){0,6}|(?:${STORES})|near me)$`, 'u');
64
+ /** Words a thing never opens with: a pronoun or a verb of the chat. */
65
+ const NOT_A_THING = /^(?:i|i'?m|im|i'?ll|ill|i'?ve|ive|we|we'?re|you|u|your|it|its|it'?s|this|that|these|those|they|he|she|my|our|let|lets|let'?s|thank|thanks|just|still|so|not|no|yes|cool|nice|great|love|like|hate|wish|hope|think|guess|really|very|too)\b/;
66
+ /** A table, or a reservation, for a number of people. */
67
+ const TABLE = /^(?:(?:a |an |book (?:a |me a |us a )?)?(?:\w+ )?)?(?:table|reservation|res|booking) for (?:\d{1,2}|one|two|three|four|five|six|seven|eight|ten|twelve)\b/;
68
+ /** A count or size a product is named by: `81 ct`, `30 rolls`, `160ct`, `6 pack`, `12 oz`. */
69
+ const COUNTED = /\b\d+(?:\.\d+)? ?(?:ct|count|pk|pack|packs|rolls?|pcs|pieces|oz|fl oz|ml|lb|lbs|kg|g|gb|tb|mah|inch|in|ft|qt|quart|gallon|gal|capsules|tablets|caplets|gels|bars|cans|bottles)\b/;
70
+ /** Words that tell of something that already happened, not something to get: `returned 3 bottles`, `the 12 pack never came`. */
71
+ const HAPPENED = /\b(?:returned|bought|ordered|got|received|have|had|has|was|were|is|are|did|never|came|arrived|missing|broke|broken|delivered|shipped|yesterday|last)\b/;
72
+ /** A purchase that already happened, which the shopper tells of rather than asks for: `returned my nike shoes yesterday`. */
73
+ const PAST = /\b(?:returned|bought|ordered|received|arrived|came|never|yesterday|last (?:week|time|month|night)|already|refund(?:ed)?|expired)\b/;
74
+ /** A correction of what came before, such as `actually i need AAA not AA`: the model reads it with what it corrects. */
75
+ const CORRECTION = /^(?:actually|instead|no|nah|nope|wait|oh wait|hmm|oops|sorry|i meant|meant)\b|\bnot\b|\binstead\b/;
76
+ /** Words that point at something only the shopper knows, such as `the usual` or `my home address`, or at a step to repeat. */
77
+ const REFERS = /\b(?:whatever|anything|up to you|you pick|you choose|your call|surprise me|dealer'?s choice|usual|same as|as before|like before|like last time|my (?:home|work|office|regular|normal)\b|code|again)\b/;
78
+ /** A complaint or a joke at a price or a store, which asks for nothing: `another $9.99 delivery fee on doordash`, `duracell is a ripoff`. */
79
+ const COMPLAINT = /\b(?:fees?|ripoff|rip off|scam|overpriced|ugh|smh|annoying|terrible|worst|sucks|hate|love that|tbh|lol|lmao|wtf|seriously|bruh)\b/;
80
+ const LINK = /\bhttps?:\/\/\S+|\bwww\.\S+/i;
81
+ /**
82
+ * True when the message asks to buy or book something, read with no context: a shopping verb with what
83
+ * to get, a product link, a product at a named store, or a table. A question, small talk, a pick or an
84
+ * answer, a command to the cart, the shopper's own details and anything about the account read false.
85
+ */
86
+ export function isProductAsk(text) {
87
+ const raw = String(text ?? '');
88
+ if (raw.length > MAX_CHARS)
89
+ return false;
90
+ const words = wordsOf(raw);
91
+ if (!/[\p{L}]/u.test(words))
92
+ return false;
93
+ const bare = words.replace(OPENING, '');
94
+ if (LINK.test(raw))
95
+ return !QUESTION_START.test(bare) && !NOT_SHOPPING.test(words.replace(/\S*https?:\/\/\S+|\S*www\.\S+/g, ''));
96
+ if (!isNewRequest(raw) || CAPABILITY.test(words) || NOT_SHOPPING.test(words) || PAST.test(words) || CORRECTION.test(words) || REFERS.test(words) || COMPLAINT.test(words))
97
+ return false;
98
+ // A question goes to the model, unless it asks Agentcard to get something: `can you find me usb-c cables?`.
99
+ const framed = SHOP_FRAME.exec(bare);
100
+ if (/\?/.test(raw) && !(framed && /^(?:can|could|would|will) (?:you|u)\b/.test(bare)))
101
+ return false;
102
+ if (QUESTION_START.test(bare))
103
+ return false;
104
+ if (framed) {
105
+ const item = framed[1].trim();
106
+ return item.length >= MIN_ITEM_CHARS && !NOT_AN_ITEM.test(item);
107
+ }
108
+ if (TABLE.test(bare))
109
+ return true;
110
+ // A product named with its count or size, such as `tide pods 81 ct` or `kirkland toilet paper 30 rolls`, with no verb.
111
+ if (COUNTED.test(bare) && !NOT_A_THING.test(bare) && !HAPPENED.test(bare) && bare.split(' ').length <= 10)
112
+ return true;
113
+ return AT_STORE.test(bare) && !NOT_A_THING.test(bare);
114
+ }
115
+ /** A plain yes: to a total, the go-ahead that confirms it. */
116
+ const YES = /^(?:y|ya|yes+|yeah|yea|yep|yup|sure|confirm(?:ed)?|sounds good|looks good|perfect|yes (?:please|place it|go ahead|do it|confirm|that'?s right|correct))$/;
117
+ function isYes(words) {
118
+ if (YES.test(words) || CHECKOUT.test(words))
119
+ return true;
120
+ // A yes, then the go-ahead in other words: `yep go ahead`, `ok do it`, `yes, place the order`, `sure, sounds good`.
121
+ const rest = words.replace(FILLER, '');
122
+ return rest !== words && (rest === '' || YES.test(rest) || CHECKOUT.test(rest));
123
+ }
124
+ /** Words that open a new request or a question rather than a short answer. */
125
+ const REQUEST_START = /^(?:find|search|look|show|get|order|buy|book|can|could|would|will|what|which|how|is|are|do|does|did|i want|i need|please)\b/;
126
+ /** Words that open an objection to giving details: the model answers it. */
127
+ const OBJECTION = /\b(?:expired|wrong|invalid|another|new code|resend|lol|don'?t|do not|won'?t|didn'?t|did not|haven'?t|not|never|no|rather not|why|what|where|how|privacy|safe|secure|instead|different|change)\b/;
128
+ /** Words that weigh a product rather than pick it: `duracell is too expensive`, `energizer instead of these`. */
129
+ const DOUBT = /\b(?:far|close|closest|open|hours|distance|minutes|rating|reviews|not|no|too|expensive|cheaper|pricey|better|worse|than|instead|vs|versus|or|compare|difference|why|isn'?t|aren'?t|don'?t|doesn'?t|hate|meh)\b/;
130
+ const ORDINALS = { first: 1, second: 2, third: 3, fourth: 4, fifth: 5, sixth: 6, seventh: 7, eighth: 8 };
131
+ /** The places in a list a message names, such as `1 and 3` or `the second one`, in order: empty when it names none. */
132
+ function placesOf(bare, count) {
133
+ const said = bare.replace(/^(?:i'?ll take|i'?ll do|i'?ll go with|go with|let'?s do|lets do|i want|give me|get me|add|the|both|option|options|number|numbers|item|#)\s*/g, ' ');
134
+ const tokens = said.match(/#?\d{1,2}(?:st|nd|rd|th)?|first|second|third|fourth|fifth|sixth|seventh|eighth|last/g) ?? [];
135
+ // The places stand alone, joined by `and` or a comma, with only `the`, `one`, `ones`, `option` or `#` around them.
136
+ const rest = said.replace(/#?\d{1,2}(?:st|nd|rd|th)?|first|second|third|fourth|fifth|sixth|seventh|eighth|last|\band\b|&|\+|,|\bthe\b|\bones?\b|\boptions?\b|\bnumbers?\b|\bitems?\b|#|\bplease\b/g, ' ').trim();
137
+ if (tokens.length === 0 || rest !== '')
138
+ return null;
139
+ const places = tokens.map((token) => (token === 'last' ? count : ORDINALS[token] ?? Number(token.replace(/\D/g, ''))));
140
+ return places.every((place) => place >= 1 && place <= count) ? places : null;
141
+ }
142
+ /** Text as lowercase words with single spaces, as the tool compares a product's name with a tap. */
143
+ function nameWords(text) {
144
+ return String(text ?? '').toLowerCase().replace(/[^\p{L}\p{N}]+/gu, ' ').trim();
145
+ }
146
+ /** The pick a message makes of what Agentcard just showed, null when it picks nothing, or false when it counts past the list. */
147
+ function pickOf(raw, bare, context) {
148
+ const products = context.products ?? [];
149
+ const stores = context.stores ?? [];
150
+ const call = (fields) => ({ input: { ask: raw, confirm: false, new_order: false, product_id: null, store_id: null, ...fields }, reason: 'pick' });
151
+ if (stores.length > 0) {
152
+ const said = ` ${nameWords(raw)} `;
153
+ // A store's card names it whole; a store named with the dishes to order from it (`five guys, bacon cheeseburger`) picks it too.
154
+ const named = stores.filter((store) => namesStore(raw, [store.name]) || (nameWords(store.name).length >= 5 && said.includes(` ${nameWords(store.name)} `)));
155
+ if (named.length === 1 && !QUESTION_START.test(bare) && !DOUBT.test(said))
156
+ return call({ store_id: named[0].storeId });
157
+ if (named.length > 0 && (QUESTION_START.test(bare) || DOUBT.test(said)))
158
+ return false;
159
+ if (named.length > 1)
160
+ return call({});
161
+ }
162
+ if (products.length > 0) {
163
+ // A card's tap names the product whole: `I'll take the KODAK AA Alkaline Batteries (24 Pack) ($13.49)`.
164
+ const said = nameWords(raw);
165
+ const named = products.filter((product) => nameWords(product.name).length >= 12 && said.includes(nameWords(product.name)));
166
+ if (named.length === 1)
167
+ return call({ product_id: named[0].productId });
168
+ if (named.length > 1)
169
+ return call({});
170
+ // A word only one product's name holds, such as its brand: `lets do the coppertop ones`, `duracell 24 count is fine`.
171
+ const words = new Set(said.split(' '));
172
+ const owners = (word) => products.filter((product) => nameWords(product.name).split(' ').includes(word));
173
+ const marked = products.filter((product) => nameWords(product.name).split(' ').some((word) => word.length >= 4 && !/^\d/.test(word) && words.has(word) && owners(word).length === 1));
174
+ if (marked.length === 1 && said.split(' ').length <= 10 && !QUESTION_START.test(bare) && !DOUBT.test(said))
175
+ return call({ product_id: marked[0].productId });
176
+ const places = placesOf(bare, products.length);
177
+ if (places)
178
+ return call(places.length === 1 ? { product_id: products[places[0] - 1].productId } : {});
179
+ // A place past the end of the list picks nothing the shopper saw.
180
+ if (placesOf(bare, Number.MAX_SAFE_INTEGER))
181
+ return false;
182
+ if (/^(?:both|all|all of them|both of them|all three|all 3)$/.test(bare))
183
+ return call({});
184
+ }
185
+ if ((products.length > 0 || stores.length > 0) && isPickText(raw))
186
+ return call({});
187
+ return null;
188
+ }
189
+ /**
190
+ * The call a shopper's message makes straight to Agentcard, with no model deciding it, or null when
191
+ * your model decides: see the top of this file for what goes straight. `ask` carries the shopper's
192
+ * words as they sent them, a confirm goes in only for a plain yes to the total Agentcard asked them to
193
+ * confirm, and a pick names its `product_id` or `store_id` only when the message names one exactly.
194
+ *
195
+ * ```ts
196
+ * directCall('find me usb-c cables under $20'); // { input: { ask: 'find me usb-c cables under $20', confirm: false, ... }, reason: 'product_ask' }
197
+ * directCall('what time is it'); // null
198
+ * directCall('yes', { answered: true, nextAction: 'confirm' }); // { input: { ask: 'yes', confirm: true, ... }, reason: 'confirm' }
199
+ * ```
200
+ */
201
+ export function directCall(shopperText, context = {}) {
202
+ const raw = String(shopperText ?? '').trim();
203
+ if (raw === '' || raw.length > MAX_CHARS)
204
+ return null;
205
+ const ctx = context ?? {};
206
+ const words = wordsOf(raw);
207
+ if (!/[\p{L}\p{N}]/u.test(words))
208
+ return null;
209
+ const bare = words.replace(OPENING, '') || words;
210
+ const plainCall = (reason, confirm = false) => ({ input: { ask: raw, confirm, new_order: false, product_id: null, store_id: null }, reason });
211
+ if (ctx.answered === true && !/\?/.test(raw)) {
212
+ const told = CHITCHAT.test(words) || ABOUT.test(words) || CART_COMMAND.test(words) || CAPABILITY.test(words);
213
+ if (ctx.nextAction === 'confirm' && isYes(words))
214
+ return plainCall('confirm', true);
215
+ // A question, a story of what happened, an objection or a change of mind is the model's to answer, whatever came before.
216
+ const aside = QUESTION_START.test(bare) || REFERS.test(words) || COMPLAINT.test(words) || PAST.test(words) || NOT_SHOPPING.test(words) || OBJECTION.test(words) || CORRECTION.test(words);
217
+ if (!told && !aside && !TAKEBACK.test(words)) {
218
+ const pick = pickOf(raw, bare, ctx);
219
+ if (pick === false)
220
+ return null;
221
+ if (pick)
222
+ return pick;
223
+ // The address, name or code Agentcard asked for: what the shopper sends is the answer, unless it objects.
224
+ const details = (typeof ctx.nextAction === 'string' && DETAIL_STEPS.has(ctx.nextAction)) || asksForDetails(ctx.asked);
225
+ if (details && !OBJECTION.test(words) && !REQUEST_START.test(bare) && !QUESTION_START.test(bare) && !PAST.test(words) && !/\b(?:same as|on file|you have|the usual|ur|your)\b/.test(words) && !/^(?:resend|send|again|still)\b/.test(bare) && !NOT_SHOPPING.test(words) && words.split(' ').length <= 40)
226
+ return plainCall('answer');
227
+ // A short answer to Agentcard's question, such as `medium` after `what size?`, or a yes to `want me to check amazon?`.
228
+ if (/\?/.test(String(ctx.asked ?? '')) && ctx.nextAction !== 'confirm' && !NOT_SHOPPING.test(words)) {
229
+ if (isYes(words))
230
+ return plainCall('answer');
231
+ if (!ACKNOWLEDGED.test(words) && words.split(' ').length <= 4 && !REQUEST_START.test(bare))
232
+ return plainCall('answer');
233
+ }
234
+ }
235
+ }
236
+ return isProductAsk(raw) ? plainCall('product_ask') : null;
237
+ }
package/dist/events.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * name. The stream (`agentcard.listen`) and a webhook endpoint carry the same types, with the same
4
4
  * envelope and the same ids. A test holds this list to the backend's catalog.
5
5
  */
6
- export declare const AGENTCARD_EVENT_TYPES: readonly ["card.created", "card.updated", "card.closed", "cardholder.created", "cardholder.updated", "transaction.authorized", "transaction.declined", "transaction.cleared", "transaction.voided", "balance.low", "cardholder_onboarding_session.completed", "platform_connect_session.completed", "merchant.connected", "order.placed", "order.confirmed", "order.failed", "order.updated", "purchase.message", "connection.created", "connected_card.updated", "vault.card_stored", "vault.session_linked", "vault.session_card_picked", "vault.payment_permission.updated", "action.confirmed", "action.done", "checkout_authorization.approved", "checkout_authorization.submitted", "checkout_authorization.declined", "checkout_authorization.cancelled", "checkout_authorization.expired", "checkout_authorization.duplicate_charge", "checkout_authorization.refused", "checkout_authorization.watched", "checkout_authorization.amount_mismatch", "checkout_authorization.settled", "checkout_authorization.settlement_unconfirmed", "checkout_authorization.outcome_reported", "card_flow.started", "card_flow.failed", "transfer.approved", "transfer.initiated", "transfer.completed", "transfer.failed", "transfer.released", "wallet.funded", "user_wallet.funding_detected", "user_wallet.funded", "wallet.balance.low", "recovery.requested", "recovery.completed", "recovery.rejected", "wallet.withdrawal.initiated", "wallet.withdrawal.completed", "wallet.withdrawal.failed", "identity.verification.updated", "approval.requested", "reward.earned", "reward.reversed", "wallet_link.opened"];
6
+ export declare const AGENTCARD_EVENT_TYPES: readonly ['card.created', 'card.updated', 'card.closed', 'cardholder.created', 'cardholder.updated', 'transaction.authorized', 'transaction.declined', 'transaction.cleared', 'transaction.voided', 'balance.low', 'cardholder_onboarding_session.completed', 'platform_connect_session.completed', 'merchant.connected', 'order.placed', 'order.confirmed', 'order.failed', 'order.updated', 'purchase.message', 'connection.created', 'connected_card.updated', 'vault.card_stored', 'vault.session_linked', 'vault.session_card_picked', 'vault.payment_permission.updated', 'action.confirmed', 'action.done', 'checkout_authorization.approved', 'checkout_authorization.submitted', 'checkout_authorization.declined', 'checkout_authorization.cancelled', 'checkout_authorization.expired', 'checkout_authorization.duplicate_charge', 'checkout_authorization.refused', 'checkout_authorization.watched', 'checkout_authorization.amount_mismatch', 'checkout_authorization.settled', 'checkout_authorization.settlement_unconfirmed', 'checkout_authorization.outcome_reported', 'card_flow.started', 'card_flow.failed', 'transfer.approved', 'transfer.initiated', 'transfer.completed', 'transfer.failed', 'transfer.released', 'wallet.funded', 'user_wallet.funding_detected', 'user_wallet.funded', 'wallet.balance.low', 'recovery.requested', 'recovery.completed', 'recovery.rejected', 'wallet.withdrawal.initiated', 'wallet.withdrawal.completed', 'wallet.withdrawal.failed', 'identity.verification.updated', 'approval.requested', 'reward.earned', 'reward.reversed', 'wallet_link.opened'];
7
7
  /** One event type Agentcard sends. A type Agentcard adds later still reads as its own name. */
8
8
  export type AgentcardEventType = (typeof AGENTCARD_EVENT_TYPES)[number];
9
9
  /** Whether a filter entry names something Agentcard sends: an event type, a family such as `order.*`, or `*`. */
package/dist/index.d.ts CHANGED
@@ -1,13 +1,15 @@
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 AgentcardTraceOptions, type OtelApi, type TraceContext, type FetchLike, type FetchLikeResponse, type BuyProgress, type BuyProgressStage, type BuyCatalogUpdate, type BuyFoundUpdate, type BuyTurnStatus, type BuySegmentUpdate, type BuyCartUpdate, type BuyDeltaUpdate, 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 BuyToolFound, 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 { directCall, isProductAsk, type DirectCall, type DirectContext, type DirectReason } from './direct.js';
11
+ export { forModel, forModelError, replyAsWritten, withoutCardList, stripLinks, type BuyToolModelView, type BuyToolModelCart, type BuyToolModelProduct, type BuyToolModelStore, } from './model-view.js';
11
12
  export { toAnthropicTool, toOpenRouterTool, type AnthropicBuyTool, type OpenRouterToolkit, type ZodLike, } from './adapters.js';
12
13
  export { listenToAgentcard, type AgentcardListener, type ListenOptions, type ListenCursorStore, type StreamFetch, } from './listen.js';
14
+ export { ACTION_LINK_ENDINGS, ACTION_LINK_KINDS, actionLinkId, actionLinkOutcome, isActionLink, type ActionLinkKind, type ActionLinkMessage, type ActionLinkOutcome, } from './action-links.js';
13
15
  export { AGENTCARD_EVENT_TYPES, type AgentcardEventType, } from './events.js';
package/dist/index.js CHANGED
@@ -1,13 +1,15 @@
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 { directCall, isProductAsk } from './direct.js';
11
+ export { forModel, forModelError, replyAsWritten, withoutCardList, stripLinks, } from './model-view.js';
11
12
  export { toAnthropicTool, toOpenRouterTool, } from './adapters.js';
12
13
  export { listenToAgentcard, } from './listen.js';
14
+ export { ACTION_LINK_ENDINGS, ACTION_LINK_KINDS, actionLinkId, actionLinkOutcome, isActionLink, } from './action-links.js';
13
15
  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) => {