@agent-cards/commerce-sdk 0.8.0 → 0.10.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/ack.js CHANGED
@@ -7,8 +7,12 @@
7
7
  *
8
8
  * The paraphrase never carries a link, an email address, a phone number, a street address or a long
9
9
  * number: the text stops before the first one, and a request that starts with one gets the template
10
- * without a paraphrase. A reply to a question, such as `yes`, `2`, `the first one` or a code, gets no
11
- * instant reply at all, because it starts no search.
10
+ * without a paraphrase. It never reads back the shopper's own first-person words, such as `i'll take` or
11
+ * `my mom`, and it never cuts a phrase short: a subject that runs long or carries brackets gets the
12
+ * template without one. A reply to a question, such as `yes`, `2` or a code, and a pick of a product the
13
+ * shopper already saw, such as `the first one`, `I'll take the KODAK AA ($13.49)` or `#2`, gets no
14
+ * instant reply at all, because it starts no search and Agentcard answers it fast. So does a pick of a
15
+ * store the shopper saw, such as `order here`, or `Order from Tacos El Grullense` as a store card sends it.
12
16
  */
13
17
  /** Each template twice: with the paraphrase in place of `{x}`, and without one. */
14
18
  const TEMPLATES = [
@@ -19,6 +23,19 @@ const TEMPLATES = [
19
23
  ['searching for {x}, one moment', 'checking on that for you now'],
20
24
  ['checking what i can find for {x}', 'on it, give me a moment'],
21
25
  ];
26
+ /**
27
+ * What a slow call sends for a message that is no search, such as `yes` or `ok lets checkout`: words
28
+ * that say Agentcard is working and never that it is looking for anything. As many as `TEMPLATES`, so
29
+ * the same rotation never repeats one twice in a row, and none the same as a template's, so a wait and
30
+ * the next search's reply never read alike.
31
+ */
32
+ const WAITS = ['one moment', 'just a sec', 'working on it', 'on it, one sec', 'hang tight', 'one sec, working on it'];
33
+ /** Words a reply opens with before it says what it means, such as `ok` in `ok lets checkout` or `yes,` in `yes, sounds good`. */
34
+ const FILLER = /^(?:(?:ok|okay|k|kk|yes|yeah|yea|yep|yup|ya|sure|alright|all right|cool|great|perfect|nice|so|then|and|um|uh|oh|well|hmm|now)[\s,]+)+/;
35
+ /** A bare acknowledgement, such as `its ok`, `thats fine`, `sounds good` or `works for me`, after the filler comes off. */
36
+ const ACKNOWLEDGED = /^(?:ok|okay|k|kk|fine|alright|all right|cool|great|perfect|good|nice|sure|yes|yeah|yep|yup|right|correct|exactly|agreed|works|(?:that|this|it) works|(?:it|that|this)(?:'?s| is) (?:ok|okay|fine|good|great|perfect|cool|right|correct|alright|all right|all good|it)|its (?:ok|okay|fine|good|great|perfect|cool|right|correct|alright|all good)|thats (?:ok|okay|fine|good|great|perfect|cool|right|correct|alright|it|all)|sounds (?:good|great|fine|perfect|right|ok|okay)|looks (?:good|great|fine|perfect|right|ok|okay)|all good|all set|no worries|no problem|np|got it|gotcha|understood|makes sense|(?:works|fine|ok|okay|good) (?:for|by|with) me|i'?m good|im good|we'?re good)(?: (?:then|now|with me|for me|to me))?$/;
37
+ /** A step toward paying for the cart already on the table, such as `lets checkout`, `place the order` or `pay`, after the filler comes off. */
38
+ const CHECKOUT = /^(?:(?:let'?s|lets|i want to|we want to|i'?d like to|id like to|i'?m ready to|im ready to|ready to|can you|could you|can we|go ahead and|just|please)\s+)*(?:check ?out|pay|pay now|pay for (?:it|this|that|them|the order|my order|everything)|place (?:the |my |this |that |our )?order|place it|confirm(?: (?:the|my) (?:order|purchase|cart)| it)?|complete (?:the |my )?(?:order|purchase|checkout)|finish (?:the |my )?(?:order|purchase|checkout)|proceed(?: to (?:checkout|payment|pay))?|go ahead|go for it|buy (?:it|them|this|that|these|those)(?: now)?|order it|book it|submit (?:the |my )?order|send (?:the |my )?order|do it|go|ready|i'?m ready|im ready)(?: now| then| already)?$/;
22
39
  /** Whole replies that answer a question the shopper was asked, after `please` and `thanks` come off. */
23
40
  const ANSWERS = new RegExp('^(?:' +
24
41
  [
@@ -30,6 +47,24 @@ const ANSWERS = new RegExp('^(?:' +
30
47
  '(?:option|number|no|#) ?\\d{1,2}',
31
48
  ].join('|') +
32
49
  ')$');
50
+ /** A status report after a step the shopper was asked to take, such as `connected`, `logged in` or `ok i did it`. */
51
+ const STATUS = /^(?:(?:ok|okay|yes|yep|yeah|alright|cool|great|so|done|there)[\s,]+)*(?:i(?:'ve| have)? |we(?:'ve| have)? )?(?:just |already |finally )?(?:connected|linked|logged in|signed in|signed up|saved|finished|added|done|did it|did that|completed|verified|approved|paid|submitted|set (?:it )?up|all set|all good|good to go|i'?m in|we'?re in|it'?s (?:done|connected|linked|added|saved|in))(?: (?:it|that|them|this|my (?:account|card|address)|the (?:account|card|address)|now|already|too|just now|on my end))*$/;
52
+ /**
53
+ * A question about the conversation, an order or how things work, such as `how did it know to msg that
54
+ * number?`, `is it booked?` or `where is my order`: the model answers it straight away, with no search.
55
+ */
56
+ const ABOUT = /^(?:(?:ok|okay|so|wait|hmm|um|uh|and|but)[\s,]+)*(?:how (?:did|does|do|come|is|was|were|are)|why (?:did|does|do|is|was|are|were|would|didn'?t|isn'?t|won'?t)|what(?:'s| is| was) (?:my|the status|happening|going on|that|this)|what happened|where(?:'s| is| are) (?:my|the order|it)|(?:is|was|has|did|will) (?:it|my|this|that|the order|the booking|the reservation)|did (?:it|that|you) (?:go|work|book|place|send|get|cancel|charge|order|change|add|remove)|will you (?:let|tell|text|notify|email|update|remind|ping|keep (?:me|us) (?:updated|posted|informed|in the loop))|when (?:will|does|do|is|did) (?:it|my|the order|they)|how long (?:will|does|until|till))\b/;
57
+ /** A command to the cart or the order, such as `lets clear the cart`, `remove the taco` or `change the quantity to 2`. */
58
+ const CART_COMMAND = new RegExp('^(?:(?:ok|okay|so|actually|wait|and|um|uh)[\\s,]+)*(?:let\'?s |lets |can you |could you |pls |i want to |i\'?d like to |id like to |i need to |go ahead and |just )*(?:' +
59
+ [
60
+ // Verbs that never open a search: `remove fries`, `cancel order`.
61
+ 'remove|delete|cancel|undo|start over|take off|get rid of',
62
+ // Verbs that also open product names, such as `clear phone case`, `drop earrings` or `take out pizza`, count only before the cart or a reference.
63
+ '(?:clear|empty|reset) (?:out )?(?:the |my |our )?(?:cart|basket|order|everything|it|all)',
64
+ '(?:drop|swap|take out) (?:the|that|this|those|these|it|them|my|our|item|#|\\d|one|both|all|everything)',
65
+ '(?:change|update|edit|reduce|increase|lower|raise|move) (?:the |my |our )?(?:quantity|qty|amount|number|size|order|cart|address|delivery|pickup|time|date|tip|item|reservation|booking|it|that|this|to|the|my|our)',
66
+ ].join('|') +
67
+ ')\\b');
33
68
  /** Small talk that asks for nothing to be bought, after `please` and `thanks` come off. */
34
69
  const CHITCHAT = /^(?:(?:hi|hey|hello|yo|hiya|howdy|sup)(?: there| again)?|good (?:morning|afternoon|evening|night)|g(?:oo)?d ?night|how are (?:you|u)(?: doing)?(?: today)?|how'?s it going|what'?s up|whats up|wassup|who are (?:you|u)|what (?:can|do) (?:you|u) do|are (?:you|u) (?:a )?(?:bot|real|human|there)|you there|test(?:ing)?|bye|see (?:you|ya)|later|love (?:it|you|u)|awesome|amazing|wow|omg|ha(?:ha)+|lmao|:\)|ok cool|cool thanks|so much|very much|a lot)$/;
35
70
  /** Unit words a number names a product by, such as `10000 mah`, so it is never read as a zip code. */
@@ -47,6 +82,17 @@ const PERSONAL = [
47
82
  /\b(?:my name is|my name'?s|name is|i'?m called|my (?:address|number|phone|email|zip)|address is|phone is|email is|zip is|deliver(?:ed)? (?:it )?to|ship(?:ped)? (?:it )?to|send (?:it )?to|bring (?:it )?to|call me|text me)\b/,
48
83
  new RegExp(`(?<![\\w/.$-])\\d{5}(?:-\\d{4})?(?![\\w.,]\\d)(?!\\w)(?!\\s*${UNIT}\\b)`),
49
84
  ];
85
+ /**
86
+ * A message that is only a place, as a shopper answers `what address?`: a house number and a street
87
+ * with no kind, such as `1801 wedemeyer`, a room number, or a named building, such as `the ferry building`.
88
+ * A number before a unit or a plural, such as `100 envelopes` or `100 w projector`, and a year, such as
89
+ * `2024 macbook air`, name a product.
90
+ */
91
+ const PLACE_ONLY = [
92
+ new RegExp(`^(?!(?:19|20)\\d{2}\\b)\\d{2,6}\\s+(?!${UNIT}\\b)[a-z][a-z'.-]*(?:\\s+[a-z][a-z'.-]*){0,2}(?<!s)$`),
93
+ /^(?:room|rm)\s*#?\s*\d{1,5}[a-z]?$/,
94
+ /^the [a-z']+(?: [a-z']+){0,2} (?:building|tower|plaza|station|terminal|airport|center|centre|hall|campus)$/,
95
+ ];
50
96
  /** True when the text holds a phone number (10 to 15 digits, with or without separators) or a run of 12 digits or more, such as a card number. */
51
97
  function hasPersonalNumber(line) {
52
98
  if (/\d{12,}/.test(line))
@@ -58,10 +104,101 @@ function hasPersonalNumber(line) {
58
104
  }
59
105
  return false;
60
106
  }
107
+ /** Words a reply opens with before it names its pick, such as `yes,` in `yes, the first one`. */
108
+ const OPENER = /^(?:(?:yes|yeah|yep|yup|ya|ok|okay|sure|great|perfect|cool|nice|alright|hmm|um|uh|so|then|actually|and)[\s,]+)+/;
109
+ /** A frame that picks from what the shopper saw: `i'll take`, `go with`, `i want the`, `add the`. */
110
+ const PICK_FRAME = new RegExp([
111
+ "^(?:i'?ll|i will|ill|we'?ll|we will|i'?d|id|let'?s|lets|let me) (?:take|go with|go for|pick|choose)\\b",
112
+ "^(?:go|going) (?:with|for) (?:the|that|this|those|these|it|both|option|number)\\b",
113
+ '^(?:i|we) (?:choose|chose|pick|picked)\\b',
114
+ "^(?:i'?ll|i will|ill|i'?d|we'?ll) (?:get|have|buy|order|do|grab) (?:the|that|this|those|these|it|both|one|option|number)\\b",
115
+ "^(?:i want|i'?d like|id like|i'?d prefer|i prefer|we want|we'?d like|i'?ll have|we'?ll have) (?:the|that|this|those|these|it|both)\\b",
116
+ '^add (?:the|that|this|those|these|it|both|one|option|number)\\b',
117
+ '^add #',
118
+ ].join('|'));
119
+ /** A frame in front of a reference to a product, taken off before the reference is read: `can i get` in `can i get the second one`. */
120
+ const ASK_FRAME = /^(?:can i (?:get|have)|could i (?:get|have)|can you (?:get|add|order|buy)|give me|get me|gimme|i need|let me (?:get|have)|buy|order|get)\s+/;
121
+ /** A reference to a store the shopper already saw: `order here`, `let's order from here`, `this place`, `from that restaurant`. */
122
+ const STORE_REF = /^(?:(?:let'?s|lets|i'?ll|ill|we'?ll|i want to|i'?d like to|id like to) )?(?:(?:order|get it|buy|go|eat|try) )?(?:from |at |with )?(?:here|there|this (?:store|place|restaurant|spot|shop|one)|that (?:store|place|restaurant|spot|shop|one)|the (?:first|second|third|last|other) (?:store|place|restaurant|spot|shop))$/;
123
+ /**
124
+ * Words around a store's name that only pick it: `order from` in `Order from Tacos El Grullense`, as a
125
+ * store card sends it, or `let's go with` and `please`. A dish named next to the store, as in
126
+ * `order tacos from el farolito`, is a search and stays.
127
+ */
128
+ const STORE_FRAME = /^(?:(?:i'?ll|ill|i will|we'?ll|we will|let'?s|lets|let me|i want to|i'?d like to|id like to|we want to|can i|can we|could i|could we|can you|could you|i'?d|id)\s+)?(?:(?:order|get|buy|grab|try|do|pick|choose|take|use|go with|go for|go)\s+)?(?:(?:food|something|some food)\s+)?(?:(?:from|at|with)\s+)?/;
129
+ const STORE_TAIL = /\s+(?:one|place|store|restaurant|it is|then|instead|today|tonight|now)$/;
130
+ const PLACE = '(?:first|second|third|fourth|fifth|sixth|last|other|\\d{1,2}(?:st|nd|rd|th))';
131
+ /** A reference to a product the shopper already saw: `that one`, `the blue one`, `the second`, `2nd option`, `#2`, `option 3`. */
132
+ const PICK_REF = new RegExp(`^(?:(?:the|that|this) (?:[\\w'$.-]+ ){0,3}one\\b|(?:that|this|those|these|both|it)(?: ones?)?$|the ${PLACE}(?: (?:one|option|item|product|choice|result))?$|${PLACE}(?: (?:one|option|item|product|choice|result)\\b|$)|(?:option|number|item|choice|#) ?#?\\d{1,2}\\b|\\d{1,2}$)`);
133
+ /** A budget word in front of a price, so `under $13.49` stays a search. */
134
+ const BUDGET = /(?:under|below|than|max|maximum|around|about|to|budget|within|over|above|for|most)\s*$/;
135
+ /**
136
+ * True for a pick of a product the shopper already saw: a frame such as `i'll take` or `i want the`, a
137
+ * reference such as `that one`, `the second` or `#2`, or a product title with its price, as a product
138
+ * card sends it: `KODAK AA Alkaline Batteries (24 Pack) ($13.49)`. Agentcard answers a pick fast, so it
139
+ * gets no instant reply.
140
+ */
141
+ function isPick(line, words) {
142
+ if (/\(\s*\$\s*\d[\d,]*(?:\.\d{2})?\s*\)/.test(line))
143
+ return true;
144
+ const price = line.match(/\$\s*\d[\d,]*\.\d{2}\s*$/);
145
+ if (price && price.index !== undefined && !BUDGET.test(line.slice(0, price.index)))
146
+ return true;
147
+ const said = words.replace(OPENER, '');
148
+ const asked = said.replace(ASK_FRAME, '');
149
+ return PICK_FRAME.test(said) || PICK_REF.test(asked) || PICK_REF.test(asked.replace(/^(?:from|at) /, '')) || STORE_REF.test(said) || STORE_REF.test(asked);
150
+ }
151
+ /**
152
+ * The ways a message can read as a store's name: as said, with the frame that only picks a store off,
153
+ * and with a closing word such as `place` off too, so `tacos el grullense` for `Order from Tacos El
154
+ * Grullense, please`. Each comes before the next, so a store whose own name holds one of those words,
155
+ * such as `Go Fish` or `Ramen Place`, still matches whole.
156
+ */
157
+ function storeReadings(text) {
158
+ const words = plain(String(text ?? '')).replace(/\b(?:please|pls|thanks|thank you|thx|ty)\b/g, ' ').replace(/[^\p{L}\p{N}']+/gu, ' ').trim().replace(OPENER, '');
159
+ const framed = words.replace(STORE_FRAME, '');
160
+ const tidy = (reading) => reading.replace(/'/g, ' ').replace(/\s+/g, ' ').trim();
161
+ // A `the` the shopper said stays in one reading, so `order from the melt` reads two words of `The Melt - Market St`.
162
+ const readings = [words, framed, framed.replace(/^the\s+/, '')].flatMap((reading) => [reading, reading.replace(STORE_TAIL, '')]);
163
+ return [...new Set(readings.map(tidy))].filter((reading) => reading.length >= 3);
164
+ }
165
+ /**
166
+ * True when the message names one of these stores and nothing else to buy: the store's whole name, or
167
+ * two words of it in a row, such as `el grullense` for `Tacos El Grullense`, alone or after a frame such
168
+ * as `order from` or `let's go with`. One word of a name, such as `tacos`, and a dish beside the name,
169
+ * such as `order tacos from el farolito`, name no store.
170
+ */
171
+ export function namesStore(text, storeNames) {
172
+ const readings = storeReadings(text);
173
+ if (readings.length === 0)
174
+ return false;
175
+ return (storeNames ?? []).some((raw) => {
176
+ const name = String(raw ?? '').toLowerCase().replace(/[^\p{L}\p{N}]+/gu, ' ').trim();
177
+ if (name === '')
178
+ return false;
179
+ return readings.some((said) => {
180
+ if (said === name || said === name.replace(/^the /, ''))
181
+ return true;
182
+ // Part of a name counts only as two words or more the shopper said, so `burger` never picks `The Burger Joint`.
183
+ return said.includes(' ') && ` ${name} `.includes(` ${said} `);
184
+ });
185
+ });
186
+ }
187
+ /** True when the shopper's words pick a product they already saw, such as `the first one`, `2` or a card's title with its price. */
188
+ export function isPickText(text) {
189
+ const line = plain(String(text ?? ''));
190
+ return isPick(line, line.replace(/[!?.,;:]+/g, ' ').replace(/\b(?:please|pls|thanks|thank you|thx|ty)\b/g, ' ').replace(/\s+/g, ' ').trim());
191
+ }
61
192
  /** A code or a number on its own, such as `111111` or `4 2 1 9`. */
62
193
  const CODE = /^[a-z]{0,3}\d[\d ]*$/;
63
194
  /** Words that start a request and say nothing about what the shopper wants, taken off the front. */
64
- const LEAD = /^(?:hey|hi|hello|yo|ok|okay|so|also|and|um|uh|actually|just|please|pls|can you|could you|would you|will you|can i (?:get|have)|could i (?:get|have)|i want to|i want|i need to|i need|i would like|i'?d like|id like|i'?m looking for|im looking for|looking for|get|find|order|buy|book|grab|reserve|search for|look for|look up|show|me|us)\b[\s,]*/;
195
+ const LEAD = /^(?:hey|hi|hello|yo|ok|okay|so|also|and|um|uh|actually|just|please|pls|can you|could you|would you|will you|can i (?:get|have)|could i (?:get|have)|i want to|i want|i need to|i need|we want|we need|i would like|i'?d like|id like|we'?d like|i'?m looking for|im looking for|we'?re looking for|looking for|i'?m after|im after|i'?m trying to find|im trying to find|trying to find|i'?m searching for|searching for|hunting for|help me find|help me get|help me|need to|want to|need|want|get|find|order|buy|book|grab|reserve|search for|look for|look up|show|me|us)\b[\s,]*/;
196
+ /** The shopper's own first-person words, which an instant reply never reads back: `i'll take`, `my mom`. */
197
+ const FIRST_PERSON = /\b(?:i|i'm|i'll|i'd|i've|im|ive|me|my|mine|myself|we|we're|we'll|we'd|our|ours|us)\b/;
198
+ /** Words a subject that names a thing to buy never opens with: a verb of a command, or a pronoun. */
199
+ const NOT_A_THING = /^(?:let|lets|let's|clear|empty|remove|delete|cancel|change|update|edit|undo|reset|start|stop|try|use|make|do|put|take|keep|it|its|it's|this|that|these|those|they|them|you|your|he|she|his|her|to|is|are|be|not|no)\b/;
200
+ /** Characters that make a subject read as cut or as a product title rather than a request: brackets, bars and the like. */
201
+ const MESSY = /[()[\]{}<>|\\*_=~^@#]/;
65
202
  /** A request that reads as a question or a change to the purchase: the template goes out without a paraphrase. */
66
203
  const NO_PARAPHRASE = /^(?:what|where|when|why|how|who|which|is|are|was|were|do|does|did|can|could|will|would|should|has|have|change|make|swap|switch|add|remove|drop|cancel|update|use|try|send|deliver|ship|pay|charge|check|tell|give|put|set|move|keep|skip|replace|return|refund|track|reorder|pick|choose|select|go|let|help|see|compare)\b/;
67
204
  /** Where the text stops: before the first link, contact detail, address, long number or delivery clause. */
@@ -77,7 +214,10 @@ const PRIVATE = [
77
214
  ];
78
215
  /** Words a cut paraphrase never ends on. */
79
216
  const TAIL = /\s+(?:for|with|and|or|but|the|a|an|to|of|in|at|on|from|under|over|below|above|near|by|who|that|which|my|some|around|about)$/;
217
+ /** Words a paraphrase never opens with, so a reply never reads `looking for from …`. */
218
+ const LEADING_PREPOSITION = /^(?:from|at|in|on|to|for|of|with|by|near|into|onto|off|via)\b/;
80
219
  const MAX_WORDS = 8;
220
+ const MAX_WORD_LENGTH = 24;
81
221
  /** The text as one plain line: lowercase, no quotes, and no dash between words. */
82
222
  function plain(text) {
83
223
  return text.toLowerCase().replace(/[‘’]/g, "'").replace(/["“”`]/g, '').replace(/\s+[-–—]+\s+|[–—]/g, ' ').replace(/\s+/g, ' ').trim();
@@ -92,30 +232,50 @@ export function isNewRequest(text) {
92
232
  const words = line.replace(/[!?.,;:]+/g, ' ').replace(/\b(?:please|pls|thanks|thank you|thx|ty)\b/g, ' ').replace(/\s+/g, ' ').trim();
93
233
  if (!/[\p{L}\p{N}]/u.test(words))
94
234
  return false;
95
- if (hasPersonalNumber(line) || PERSONAL.some((pattern) => pattern.test(line)))
235
+ if (hasPersonalNumber(line) || PERSONAL.some((pattern) => pattern.test(line)) || PLACE_ONLY.some((pattern) => pattern.test(words)))
236
+ return false;
237
+ const bare = words.replace(FILLER, '');
238
+ if (bare === '' || ACKNOWLEDGED.test(bare) || CHECKOUT.test(bare))
96
239
  return false;
97
- return !ANSWERS.test(words) && !CODE.test(words) && !CHITCHAT.test(words);
240
+ return !ANSWERS.test(words) && !STATUS.test(words) && !ABOUT.test(words) && !CART_COMMAND.test(words) && !CODE.test(words) && !CHITCHAT.test(words) && !isPick(line, words);
98
241
  }
99
242
  /** A short paraphrase of what the shopper asks for, safe to read back to them, or null when none is. */
100
243
  export function paraphrase(text) {
101
244
  let rest = plain(String(text ?? '')).split(/[.!?;\n](?:\s|$)/)[0] ?? '';
102
245
  for (const pattern of PRIVATE) {
103
246
  const at = rest.search(pattern);
247
+ // A cut inside a word takes the rest of that word too, so no word is left half read.
104
248
  if (at >= 0)
105
- rest = rest.slice(0, at);
249
+ rest = /\S/.test(rest[at - 1] ?? '') ? rest.slice(0, at).replace(/\S+$/, '') : rest.slice(0, at);
106
250
  }
107
251
  for (let before = ''; before !== rest;) {
108
252
  before = rest;
109
253
  rest = rest.replace(LEAD, '');
110
254
  }
111
- rest = rest.replace(/[\s,.:!?]+$/, '');
112
- if (NO_PARAPHRASE.test(rest))
255
+ // `order from tacos el grullense` names the store, never `from tacos el grullense`.
256
+ rest = rest.replace(/^(?:from|at)\s+/, '').replace(/[\s,]+(?:please|pls|thanks|thank you|thx|ty)$/, '').replace(/[\s,.:!?]+$/, '');
257
+ if (NO_PARAPHRASE.test(rest) || LEADING_PREPOSITION.test(rest))
113
258
  return null;
114
259
  const words = rest.split(' ').filter(Boolean);
115
- let short = words.slice(0, MAX_WORDS).join(' ');
260
+ // A long or messy subject is never cut to fit: the template goes out without one.
261
+ if (words.length > MAX_WORDS || words.some((word) => word.length > MAX_WORD_LENGTH))
262
+ return null;
263
+ let short = words.join(' ');
116
264
  while (TAIL.test(short))
117
265
  short = short.replace(TAIL, '');
118
266
  short = short.replace(/[\s,]+$/, '');
267
+ if (MESSY.test(short) || FIRST_PERSON.test(short))
268
+ return null;
269
+ // A house number and a direction, such as `1515 w pacific`, may be an address: the reply names nothing.
270
+ if (/^\d{3,6} [nsew]\.? /.test(short))
271
+ return null;
272
+ // The subject reads back only as a thing to buy or book: never one that opens with a verb or a
273
+ // pronoun, or that names the cart or the order.
274
+ if (NOT_A_THING.test(short) || /\b(?:cart|basket|order|orders|checkout|quantity|qty|items?)\b/.test(short))
275
+ return null;
276
+ // One word that reads as a step or a state, such as `connected` or `linked`, names nothing to buy.
277
+ if (!short.includes(' ') && /(?:ed|ing)$/.test(short))
278
+ return null;
119
279
  return /[a-z]{2}/.test(short) && isNewRequest(short) ? short : null;
120
280
  }
121
281
  /** The template at `index` for these words, with the paraphrase when one is safe. */
@@ -124,6 +284,17 @@ export function ackAt(index, text) {
124
284
  const what = paraphrase(text);
125
285
  return what ? withWhat.replace('{x}', what) : without;
126
286
  }
287
+ /** The reply at `index` for a message that is no search: it says Agentcard is working, and never that it is looking for anything. */
288
+ export function waitAt(index) {
289
+ return WAITS[((index % WAITS.length) + WAITS.length) % WAITS.length];
290
+ }
291
+ /**
292
+ * The reply a slow pick or confirm sends once Agentcard takes longer than the delay: fixed words that
293
+ * name nothing the shopper said and never say `looking for`, because the shopper already chose.
294
+ */
295
+ export function slowStepLine(step) {
296
+ return step === 'product' ? 'adding it now' : step === 'confirm' ? 'on it, one moment' : 'got it, one moment';
297
+ }
127
298
  /** A template other than `avoid`, picked at random. */
128
299
  export function nextTemplate(avoid, random = Math.random) {
129
300
  const pick = Math.floor(random() * (TEMPLATES.length - 1));
@@ -136,6 +307,42 @@ function templateOf(reply) {
136
307
  const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
137
308
  return TEMPLATES.findIndex(([withWhat, without]) => reply === without || new RegExp(`^${escape(withWhat).replace('\\{x\\}', '.+')}$`).test(reply));
138
309
  }
310
+ /** What a question asks the shopper for when its answer is their own details or a code. */
311
+ const DETAILS = new RegExp([
312
+ "\\byour (?:full |first |last |delivery |shipping |street |home |work |email |e-mail )?(?:address|name|phone(?: number)?|number|email|e-mail|zip(?: code)?|postal code|apartment|apt|unit|suite)\\b",
313
+ '\\b(?:delivery|shipping|street|home|drop-?off) address\\b',
314
+ "\\b(?:what|which) (?:is the |is your |'s the |'s your )?(?:address|name|zip|email)\\b",
315
+ '\\baddress (?:should|do|to|for)\\b',
316
+ '\\bname (?:for|on|under) the (?:order|table|booking|reservation|room|delivery)\\b',
317
+ '\\b(?:phone|mobile|cell|room|apartment|unit|suite|gate|flight) number\\b',
318
+ '\\b(?:email address|zip code|postal code)\\b',
319
+ '\\bwhere (?:should|do|can|would) (?:i|we)\\b.*\\b(?:send|deliver|ship|bring|drop)\\b',
320
+ '\\b(?:verification|confirmation|security|one-time|6-digit|six-digit) code\\b',
321
+ '\\b(?:enter|type|send me|share) the code\\b',
322
+ '\\bthe code (?:we|i|you|they)\\b',
323
+ ].join('|'));
324
+ /** True when a message asks the shopper for their own details or a code, in a question or a request such as `send me your address`. */
325
+ export function asksForDetails(message) {
326
+ const sentences = plain(String(message ?? '')).split(/(?<=[.!?\n])\s+/);
327
+ return sentences.some((sentence) => DETAILS.test(sentence) && (/\?\s*$/.test(sentence) || /^(?:please |pls )?(?:send|tell|share|give|reply|type|enter|text|drop|let me know|i need|i'll need|what|which|where)\b/.test(sentence)));
328
+ }
329
+ /** Words that open a new request rather than an answer, such as `find`, `can you get` or `what about`. */
330
+ const REQUEST_LEAD = /^(?:(?:ok|okay|so|actually|also|and|no|nah|wait|hmm|um|uh|instead|nevermind|never mind|nvm|hey|hi|hello|yo|just|now|then|pls|please)[\s,]+)*(?:find|search|look|looking|show|get me|get us|help me|help|i'?m looking|im looking|i'?m after|im after|can you|could you|would you|will you|can i|could i|i want|i need|i'?d like|id like|we want|we need|we'?d like|order|buy|book|reserve|grab|how about|what about|do you have|do they have|is there|are there|something|anything|another|different|also)\b/;
331
+ /** The most words a reply runs to when it only answers a question, as `im a 37` or `extra large please` does. */
332
+ const MAX_ANSWER_WORDS = 4;
333
+ /**
334
+ * True when the shopper's words answer the question the last message asked, such as `what size do you
335
+ * want?` or `which meat?`: the message asked a question, and the reply is short and opens no new request.
336
+ */
337
+ export function answersQuestion(text, asked) {
338
+ if (!/\?/.test(String(asked ?? '')))
339
+ return false;
340
+ // A decimal size, such as `9.5`, stays one word.
341
+ const words = plain(String(text ?? '')).replace(/[!?,;:]+|\.(?!\d)|(?<!\d)\./g, ' ').replace(/\b(?:please|pls|thanks|thank you|thx|ty)\b/g, ' ').replace(/\s+/g, ' ').trim();
342
+ if (!/[\p{L}\p{N}]/u.test(words) || words.split(' ').length > MAX_ANSWER_WORDS)
343
+ return false;
344
+ return !REQUEST_LEAD.test(words);
345
+ }
139
346
  /** The steps a shopper answers with their own details or a code, never with a new request. */
140
347
  const ANSWERED_STEPS = new Set(['shipping_details', 'guest_details', 'verification_code']);
141
348
  /**
@@ -156,7 +363,9 @@ const ANSWERED_STEPS = new Set(['shipping_details', 'guest_details', 'verificati
156
363
  export function instantAck(shopperText, options = {}) {
157
364
  if (typeof options?.nextAction === 'string' && ANSWERED_STEPS.has(options.nextAction))
158
365
  return null;
159
- if (!isNewRequest(shopperText))
366
+ if (asksForDetails(options?.asked) || answersQuestion(shopperText, options?.asked))
367
+ return null;
368
+ if (!isNewRequest(shopperText) || namesStore(shopperText, options?.stores))
160
369
  return null;
161
370
  return ackAt(nextTemplate(templateOf(options?.previous)), shopperText);
162
371
  }
@@ -9,7 +9,7 @@
9
9
  * Both hand the model the JSON text `buy.handle` returns, and a failed call
10
10
  * as the error's message with every link taken out.
11
11
  */
12
- import type { BuyTool } from './buy-tool.js';
12
+ import type { BuyTool, BuyToolTurn } from './buy-tool.js';
13
13
  /** @deprecated The buy tool is Anthropic's runnable tool already. Kept for one release. */
14
14
  export type AnthropicBuyTool = BuyTool;
15
15
  /** @deprecated Pass the buy tool itself: `toolRunner({ tools: [buy] })`. Returns the same object, for one release. */
@@ -34,5 +34,5 @@ export interface OpenRouterToolkit<T> {
34
34
  tool: (config: any) => T;
35
35
  z: ZodLike;
36
36
  }
37
- /** The buy tool for OpenRouter's `callModel({ tools: [toOpenRouterTool(buy, { tool, z })] })`. */
38
- export declare function toOpenRouterTool<T>(buy: BuyTool, toolkit: OpenRouterToolkit<T>): T;
37
+ /** The buy tool, or one turn of it from `buy.turn`, for OpenRouter's `callModel({ tools: [toOpenRouterTool(buy, { tool, z })] })`. */
38
+ export declare function toOpenRouterTool<T>(buy: BuyTool | BuyToolTurn, toolkit: OpenRouterToolkit<T>): T;
package/dist/adapters.js CHANGED
@@ -2,7 +2,7 @@
2
2
  export function toAnthropicTool(tool) {
3
3
  return tool;
4
4
  }
5
- /** The buy tool for OpenRouter's `callModel({ tools: [toOpenRouterTool(buy, { tool, z })] })`. */
5
+ /** The buy tool, or one turn of it from `buy.turn`, for OpenRouter's `callModel({ tools: [toOpenRouterTool(buy, { tool, z })] })`. */
6
6
  export function toOpenRouterTool(buy, toolkit) {
7
7
  const { tool, z } = toolkit ?? {};
8
8
  if (typeof tool !== 'function' || !z || typeof z.object !== 'function') {
@@ -17,6 +17,7 @@ export function toOpenRouterTool(buy, toolkit) {
17
17
  confirm: z.boolean().describe(properties.confirm.description),
18
18
  new_order: z.boolean().describe(properties.new_order.description),
19
19
  product_id: z.string().nullable().describe(properties.product_id.description),
20
+ store_id: z.string().nullable().describe(properties.store_id.description),
20
21
  }).strict(),
21
22
  // OpenRouter's SDK encodes what execute returns as JSON, so the view goes back as an object and reaches the model as the same text.
22
23
  execute: async (input) => JSON.parse(await buy.execute(input)),
@@ -21,10 +21,21 @@
21
21
  * still finishing gets one 12-second pause and one read of the conversation.
22
22
  * A turn that outlived Agentcard's budget gets a read every 5 seconds for up
23
23
  * to a minute, until the turn ends.
24
+ *
25
+ * `turn` binds the tool to one shopper message: the calls the model makes
26
+ * through it hand their links, answers and instant reply to that turn's own
27
+ * callbacks and lists, so a shopper who sends a second message before the
28
+ * first is answered never gets one message's cards beside the other's reply.
29
+ * Every turn of one tool still shares the shopper's conversation state, such
30
+ * as the store pages already sent. A turn with `onItem` hands the app its
31
+ * messages and links as one sequence, in the order they happened, and spaces
32
+ * the messages like someone texting.
24
33
  */
25
- import { AgentcardClient, type BuyCart, type BuyFlightItinerary, type BuyDeliveryAddress, type BuyNextAction, type BuyPick, type BuyProgress, type FetchLike } from './client.js';
34
+ import { AgentcardClient, type BuyCart, type BuyFlightItinerary, type BuyDeliveryAddress, type BuyNextAction, type BuyPick, type BuyProgress, type BuyStorePick, type BuyTurn, type FetchLike } from './client.js';
26
35
  import { type BuyToolDefinition, type BuyToolInput, type BuyToolInputSchema, type BuyToolStandardSchema, type OpenAIBuyTool } from './definition.js';
27
36
  import { type BuyToolModelView } from './model-view.js';
37
+ import { type MessagePace, type MessageStream } from './messages.js';
38
+ import type { ShopperMessage } from './shopper-message.js';
28
39
  /**
29
40
  * Where the purchase stands:
30
41
  * - `needs_input`: Agentcard asks the shopper something, or shows a cart.
@@ -41,10 +52,17 @@ export type BuyToolStatus = 'needs_input' | 'needs_card' | 'approval_sent' | 'pl
41
52
  * the page where they sign in to the store named in `store`, before Agentcard
42
53
  * can buy there. `product`: a product's page at its store, with its `title`,
43
54
  * `price`, `productId` and `imageUrl`. The page is `url`; `productId` holds no link.
55
+ * A flight option is a `product` link too, with a null `url`, since a flight has no page.
56
+ * `store`: a store's page at the merchant, for a store the shopper can pick, with
57
+ * its name in `title`, `storeId`, `etaMinutes` and `imageUrl`.
44
58
  */
45
59
  export interface BuyToolLink {
46
- kind: 'vault' | 'approval' | 'store_login' | 'product';
47
- url: string;
60
+ kind: 'vault' | 'approval' | 'store_login' | 'product' | 'store';
61
+ /**
62
+ * The page the link opens. Null only on a product link for a product with no page at a store, a
63
+ * flight option: show its card with no link to open, and let `productId` pick it.
64
+ */
65
+ url: string | null;
48
66
  /** When the link stops working, as an ISO 8601 timestamp, or null when Agentcard did not say. Always null on a product link. */
49
67
  expiresAt: string | null;
50
68
  /** On a store login link: the store's name, or null when Agentcard named none. */
@@ -59,8 +77,33 @@ export interface BuyToolLink {
59
77
  * can hand it back as the shopper's pick. Absent when Agentcard sent no such id, as an older API does.
60
78
  */
61
79
  productId?: string;
62
- /** On a product link: the product's picture, or null when Agentcard sent none. Show it beside the title. */
80
+ /**
81
+ * On a product link: the product's picture. On a store link: the picture the store's card shows at
82
+ * the merchant. Null when Agentcard sent none. Show it beside the title.
83
+ */
63
84
  imageUrl?: string | null;
85
+ /**
86
+ * On a store link: Agentcard's id for the store, with no link in it. It is the `store_id` the model
87
+ * reads for the same store, and the tool takes it as `store_id`, so a "Order here" button can hand it
88
+ * back as the shopper's pick.
89
+ */
90
+ storeId?: string;
91
+ /** On a store link: the store's delivery estimate in minutes, or null when the store showed none. */
92
+ etaMinutes?: number | null;
93
+ }
94
+ /** A store the answer's reply presents to choose from. */
95
+ export interface BuyToolStore {
96
+ /** Agentcard's id for the store, with no link in it: the model passes it as `store_id` when the shopper picks this store. */
97
+ id: string;
98
+ name: string;
99
+ /** The store's page at the merchant, or null when Agentcard sent none. */
100
+ url: string | null;
101
+ /** The delivery estimate in minutes, or null when the store showed none. */
102
+ etaMinutes: number | null;
103
+ /** The picture the store's card shows at the merchant, or null when Agentcard sent none. */
104
+ imageUrl: string | null;
105
+ /** True when the shopper holds this store's page: `sendLink` took it in this call or an earlier call of the same model turn. */
106
+ linkSent: boolean;
64
107
  }
65
108
  /** A product the last search found. */
66
109
  export interface BuyToolProduct {
@@ -121,6 +164,10 @@ export interface BuyToolResult {
121
164
  next_action: BuyNextAction | null;
122
165
  /** The product the model's `product_id` picked, by its catalog id, and whether the cart now holds it; null when the call picked none. */
123
166
  pick: BuyPick | null;
167
+ /** The stores the reply presents to choose from, at most three, in the order the reply names them; empty when it presents none. */
168
+ stores: BuyToolStore[];
169
+ /** The store the model's `store_id` picked, or the shopper's `the second one` did, and whether Agentcard selected it; null when none was picked. */
170
+ store_pick: BuyStorePick | null;
124
171
  /** Every cart hash in this answer. */
125
172
  hashes: string[];
126
173
  /** Agentcard's answer as it came: the purchase answer, or the body of the refusal. */
@@ -138,7 +185,9 @@ export interface BuyToolOptions {
138
185
  * holds the link before the model answers. A `sendLink` that throws on a
139
186
  * Vault or approval link makes the call throw too, and the model never
140
187
  * reads that a link went out. One that throws on a product link leaves
141
- * that product's `linkSent` false, and the call goes on.
188
+ * that product's `linkSent` false, and the call goes on. It hears the links of every call, whatever
189
+ * shopper message the call answers: a chat app where a shopper can send a second message before the
190
+ * first is answered passes `sendLink` to `buy.turn` instead, so each link reaches the turn it belongs to.
142
191
  */
143
192
  sendLink?: (link: BuyToolLink) => void | Promise<void>;
144
193
  /** Where the orders go, when you already hold the shopper's address. The model never sets it. */
@@ -152,12 +201,14 @@ export interface BuyToolOptions {
152
201
  /**
153
202
  * Sends the shopper an instant reply, such as `on it, looking for usb-c cables under $20`, while
154
203
  * Agentcard works on a new request. It goes out when Agentcard has not answered within
155
- * `ackDelayMs`, once for the calls the model makes in one turn, and never on a confirm, a pick or a
156
- * reply such as `yes`, `2` or a code. The call waits for it before it returns, so the shopper reads
157
- * it before any link or the model's answer. One that throws costs nothing but the instant reply.
204
+ * `ackDelayMs`, once for the calls the model makes in one turn. A slow pick gets `adding it now`
205
+ * for a product or `got it, one moment` for a store or a place in a list. A slow confirm gets
206
+ * `on it, one moment`. These replies name nothing the shopper said. A code gets no reply.
207
+ * The call waits for it before it returns, so the shopper reads it before any link or the model's
208
+ * answer. One that throws costs nothing but the instant reply.
158
209
  */
159
210
  onWorking?: (message: string) => void | Promise<void>;
160
- /** `false` sends no instant reply. A function writes it from the shopper's words instead, and an empty answer sends none. */
211
+ /** `false` sends no instant reply. A function writes it from the shopper's words instead, and an empty answer sends none. A turn whose message `instantAck` sent nothing for gets the built-in reply that names nothing. */
161
212
  ack?: false | ((ask: string) => string | null | undefined);
162
213
  /** How long Agentcard has to answer before the instant reply goes out. Default 1500. */
163
214
  ackDelayMs?: number;
@@ -165,7 +216,8 @@ export interface BuyToolOptions {
165
216
  * Called with the whole answer once per call, after its links went out and before the model reads
166
217
  * it, so your app can show the carts, the status, `next_action` or the order id without wrapping the
167
218
  * tool. A refusal the tool turns into an answer, such as a Vault link for a shopper with no card,
168
- * counts; a call that throws calls nothing. One that throws costs nothing but the call to it.
219
+ * counts; a call that throws calls nothing. One that throws costs nothing but the call to it. Like
220
+ * `sendLink`, it hears every call; pass it to `buy.turn` to hear only the answers of one turn.
169
221
  */
170
222
  onResult?: (result: BuyToolResult) => void | Promise<void>;
171
223
  /**
@@ -232,12 +284,137 @@ export interface BuyTool {
232
284
  * The instant reply for a shopper's message, such as `on it, looking for usb-c cables under $20`, or
233
285
  * null for a message that starts no search: `yes`, `1`, `ok`, a code, `hi`, or the shopper's own
234
286
  * details, such as a name, a phone number or an address, and any message that answers a request for
235
- * shipping or guest details or a code. Call it the moment the message arrives, before your model runs,
236
- * and send the text it returns. The tool then sends no reply of its own through `onWorking` for that
237
- * turn, and the model reads `shopper_already_told_looking`, so the shopper never hears it twice.
287
+ * shipping or guest details or a code, and a pick of a product or a store the shopper saw, such as
288
+ * `Order from Tacos El Grullense` as a store card sends it. Call it the moment the
289
+ * message arrives, before your model runs, and send the text it returns. When it returns a reply, the
290
+ * tool sends none of its own through `onWorking` for that turn, and the model reads
291
+ * `shopper_already_told_looking`, so the shopper never hears it twice. When it returns null for a
292
+ * message that picks nothing, such as `yes`, a slow call still sends `onWorking` a reply that names nothing.
238
293
  */
239
294
  instantAck(shopperText: string): string | null;
295
+ /**
296
+ * Opens the turn of one shopper message and returns the tool for that turn: pass it to your model in
297
+ * place of the tool, as `tools: [turn]`. Each call the model makes through it hands its links to this
298
+ * turn's `sendLink` and keeps them in `turn.links`, hands its answer to this turn's `onResult` and
299
+ * keeps it in `turn.results`, and sends this turn's instant reply to its `onWorking`. A shopper who
300
+ * sends a second message while the first still runs gets a second turn, and no link, cart or reply
301
+ * of one reaches the other. Call it the moment the message arrives, before your model runs: with an
302
+ * `onWorking`, the instant reply `instantAck` would return goes out at once. An option left out falls
303
+ * back to the one the tool was built with.
304
+ */
305
+ turn(shopperText: string, options?: BuyToolTurnOptions): BuyToolTurn;
306
+ }
307
+ /**
308
+ * One thing a turn hands the shopper: a message, such as the instant reply or a paragraph your model
309
+ * wrote, or a link, such as a product card. `buy.turn`'s `onItem` hears them in the order they happened.
310
+ */
311
+ export type BuyToolTurnItem = {
312
+ kind: 'text';
313
+ text: string;
314
+ } | {
315
+ kind: 'link';
316
+ link: BuyToolLink;
317
+ };
318
+ /** The callbacks of one turn. Each one left out falls back to the option of the same name the tool was built with, except with `onItem`. */
319
+ export interface BuyToolTurnOptions {
320
+ /**
321
+ * Hears everything the turn hands the shopper, one item at a time and in the order it happened: the
322
+ * instant reply and each message your model writes through `turn.messageStream()` or `turn.say()` as
323
+ * `text` items, and each link as a `link` item. Show each item below the one before it, and the chat
324
+ * reads in the order it happened: a search's cards, then the model's words about them. Each item
325
+ * waits for the one before it to settle. With `onItem`, the turn hands no link to a `sendLink` and
326
+ * no instant reply to an `onWorking`, the tool's included: pass `onItem`, or those, to the turn.
327
+ */
328
+ onItem?: (item: BuyToolTurnItem) => void | Promise<void>;
329
+ /**
330
+ * With `onItem`: spaces the turn's messages like someone texting. A message waits until
331
+ * `typingDelay` of it has passed since the item before it went out, so the shopper reads one message
332
+ * at a time. Time the model spent writing counts toward the pause. Links never wait. Default `true`;
333
+ * `false` hands each message over the moment it is ready, and a function returns the pause in
334
+ * milliseconds for a message.
335
+ */
336
+ pace?: MessagePace;
337
+ /** With `onItem`: hears `true` when the pause before a message starts and `false` the moment that message goes out, for a typing indicator. */
338
+ onTyping?: (typing: boolean) => void;
339
+ /**
340
+ * Delivers a link of this turn to the shopper, as the tool's `sendLink` does. Each call waits for it.
341
+ * Without one, `turn.links` still takes each link, and the model reads that it went out, so return
342
+ * `turn.links` beside the turn's reply.
343
+ */
344
+ sendLink?: (link: BuyToolLink) => void | Promise<void>;
345
+ /** Sends this turn's instant reply: at once when `turn` opens on a new request, or once a slow call passes `ackDelayMs`. */
346
+ onWorking?: (message: string) => void | Promise<void>;
347
+ /** Called with the whole answer of each call this turn makes, after its links went out and before the model reads it. */
348
+ onResult?: (result: BuyToolResult) => void | Promise<void>;
349
+ /** Called with each stage Agentcard reaches on a call this turn makes. */
350
+ onProgress?: (progress: BuyProgress) => void;
351
+ }
352
+ /**
353
+ * The buy tool for one shopper message, from `buy.turn`. It is the same tool, with the same name,
354
+ * schema and methods, so a framework takes it as it takes the tool, and the calls made through it
355
+ * belong to this turn alone. `ack`, `links` and `results` are read-only, and JSON leaves them out, so a
356
+ * tool runner that sends the tool to its API sends the same fields as before.
357
+ */
358
+ export interface BuyToolTurn extends Omit<BuyTool, 'instantAck' | 'turn'> {
359
+ /** The instant reply this turn sent to `onWorking` when it opened, or null when it sent none. */
360
+ readonly ack: string | null;
361
+ /**
362
+ * The messages Agentcard posted into this shopper's chat that the model has not read yet, oldest
363
+ * first, such as `card_added` once they saved a card: each one `agentcard.listen`'s or
364
+ * `agentcardWebhook`'s `onMessage` returned from, on the client this tool was built with. Once
365
+ * `messages` ran, the ones it handed the model.
366
+ */
367
+ readonly agentcardMessages: ShopperMessage[];
368
+ /**
369
+ * What to send your model for this turn: `history`, then what Agentcard told the shopper that the
370
+ * history does not hold yet, as the assistant's words with every link taken out, then the shopper's
371
+ * message as a user message. Pass it as the model's `messages` and save what the model's run ends
372
+ * with as the next history, so the model reads that the shopper saved a card, approved or got an
373
+ * order before it answers them. A message the history already holds goes in no second time, and one
374
+ * a failed turn never saved goes in again. The Anthropic, OpenAI and Vercel AI SDK message lists all
375
+ * take it. Agentcard's words join the history's last assistant message when it is plain text, and
376
+ * come as an assistant message of their own otherwise. `history` is never changed.
377
+ */
378
+ messages<M extends object = BuyToolTurnMessage>(history?: readonly M[] | null): Array<M | BuyToolTurnMessage>;
379
+ /**
380
+ * Every link this turn's calls handed over, in order: the Vault, approval and store login links,
381
+ * each product's store page and each store card's page. A link `sendLink` threw on is left out. Each read returns a new array.
382
+ */
383
+ readonly links: BuyToolLink[];
384
+ /** The whole answer of each call this turn made, in the order the calls returned. Each read returns a new array. */
385
+ readonly results: BuyToolResult[];
386
+ /**
387
+ * Every message and link the turn handed over, in the order they went out: the instant reply, each
388
+ * link, and each message written through `messageStream()` or `say()`. An item its send threw on is
389
+ * left out. Return it as the turn's answer, and an app renders it in order. Each read returns a new array.
390
+ */
391
+ readonly items: BuyToolTurnItem[];
392
+ /**
393
+ * A message stream whose messages become this turn's `text` items, after the items before them, as
394
+ * `messageStream` cuts them: push your model's text as it streams, then `end` it. Start one for each
395
+ * model message. With `onItem`, the turn spaces them by its `pace`.
396
+ */
397
+ messageStream(): MessageStream;
398
+ /** Cuts a whole text into messages, as `toMessages` does, and hands each one over as a `text` item. Resolves once every one went out. */
399
+ say(text: string): Promise<void>;
240
400
  }
401
+ /** A message in the one shape the Anthropic, OpenAI and Vercel AI SDK message lists all take. */
402
+ export type BuyToolTurnMessage = {
403
+ role: 'assistant';
404
+ content: string;
405
+ } | {
406
+ role: 'user';
407
+ content: string;
408
+ };
409
+ /**
410
+ * The model's messages for a turn: the history, what Agentcard told the shopper, then the shopper's
411
+ * words. Agentcard's words join a last assistant message of plain text, a string or text parts only,
412
+ * so no provider reads two assistant messages in a row. After anything else, such as a tool result
413
+ * or an OpenAI Responses item, they come as an assistant message of their own.
414
+ */
415
+ export declare function turnMessages<M extends object>(history: readonly M[] | null | undefined, told: readonly string[], shopperText: string): Array<M | BuyToolTurnMessage>;
416
+ /** Shared with the merchant monitor so link eligibility uses the tool's product mapping. */
417
+ export declare function productsOf(turn: BuyTurn): BuyToolProduct[];
241
418
  /**
242
419
  * A buy tool for one shopper. With a company client, name the shopper with
243
420
  * `externalUserId`; with a shopper's own `accessToken`, the token names them.