@agent-cards/commerce-sdk 0.15.0 → 0.16.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/buy-tool.js CHANGED
@@ -33,12 +33,13 @@
33
33
  */
34
34
  import { AgentcardApiError, AgentcardClient, cartBuildingOf, } from './client.js';
35
35
  import { buyToolDefinition, buyToolStandardSchema, openAIBuyTool, parseBuyToolInput, } from './definition.js';
36
- import { directCall } from './direct.js';
37
- import { forModel, forModelError, pickId, productPrice, productCardKey, replyAsWritten, shownProducts, shownStores, stripLinks } from './model-view.js';
36
+ import { directCall, isProductAsk } from './direct.js';
37
+ import { forModel, forModelError, pickId, productPrice, productCardKey, shownProducts, shownStores, stripLinks } from './model-view.js';
38
+ import { choiceLines, leadsIntoList, messageForShopper, messagesListChoice, replyAsWritten, textingStyle } from './texting.js';
38
39
  import { messageStream, paceOf, sleep, toMessages, typingHook } from './messages.js';
39
40
  import { isStale } from './shopper-message.js';
40
41
  import { actionLinkId, isActionLink } from './action-links.js';
41
- import { ACK_REACTION, ackAt, answersQuestion, instantAck as instantAckOf, isNewRequest, isPickText, keepsYes, namesStore, nextTemplate, slowStepLine, waitAt } from './ack.js';
42
+ import { ACK_REACTION, ackAt, answersCheckout, answersQuestion, holdsBack, instantAck as instantAckOf, isNewRequest, isPickText, keepsYes, namesStore, nextTemplate, slowStepLine, waitAt } from './ack.js';
42
43
  /** The action a product of `merchant` takes (BuyToolProductAction). */
43
44
  export function productAction(merchant) {
44
45
  if (merchant === 'reservations')
@@ -145,6 +146,70 @@ const NOTHING_TO_CONFIRM = 'There is no cart to confirm yet. Ask the shopper wha
145
146
  const ADDRESS_CHANGED = 'The delivery address changed since this cart was built. Ask for the cart again, and Agentcard prices it for the new address.';
146
147
  const STORE_PICK_GONE = "The store the shopper picked is no longer among the stores Agentcard found, so no store was chosen. Call this tool again with the shopper's words and store_id null, and Agentcard searches again.";
147
148
  const PICK_GONE = "The product the shopper picked is no longer among the products Agentcard found, so nothing was added. Call this tool again with the shopper's words and product_id null, and Agentcard searches again.";
149
+ /** One cart line as the order holds it: the product, its variant, its choices and its quantity. Prices are left out. */
150
+ const lineKey = (line) => JSON.stringify([line.product_id ?? String(line.name ?? '').trim().toLowerCase(), line.variant ?? null, [...(line.options ?? [])].sort(), line.qty]);
151
+ /**
152
+ * How two matched lines' prices stand: false when a price they both name moved, true when one they both name agrees, and
153
+ * null when they name none in common, so the lines say nothing about what they cost.
154
+ */
155
+ function linePriceStands(a, b) {
156
+ const named = (x, y) => typeof x === 'number' && typeof y === 'number' ? x === y : null;
157
+ const read = [named(a.priceCents, b.priceCents), named(a.lineTotalCents, b.lineTotalCents)];
158
+ if (read.includes(false))
159
+ return false;
160
+ return read.includes(true) ? true : null;
161
+ }
162
+ /**
163
+ * True when two carts are the same order: the same store, the same items in the same quantities with the same choices,
164
+ * and the same price for those items. Only the total may differ, as when the store prices its fees, tax and delivery in
165
+ * once the shopper's account holds the cart (Mendocino Farms on DoorDash, 2026-10-27: about $15.90, then $17.89).
166
+ *
167
+ * What the items cost has to be shown to be the same: every matched line's prices agree, or both carts name a subtotal
168
+ * and it is equal. A line whose price moved, two lines whose prices swapped under one subtotal, and a cart nothing
169
+ * prices are each a different order, so the shopper is asked again (review of #11087).
170
+ */
171
+ function sameOrder(a, b) {
172
+ if (a.merchant !== b.merchant || (a.store?.id ?? null) !== (b.store?.id ?? null))
173
+ return false;
174
+ if ((a.currency ?? null) !== (b.currency ?? null))
175
+ return false;
176
+ const subtotals = typeof a.subtotalCents === 'number' && typeof b.subtotalCents === 'number' ? a.subtotalCents === b.subtotalCents : null;
177
+ if (subtotals === false)
178
+ return false;
179
+ const lines = (cart) => [...(cart.items ?? [])].sort((p, q) => (lineKey(p) < lineKey(q) ? -1 : lineKey(p) > lineKey(q) ? 1 : 0));
180
+ const [x, y] = [lines(a), lines(b)];
181
+ if (x.length === 0 || x.length !== y.length || !x.every((line, i) => lineKey(line) === lineKey(y[i])))
182
+ return false;
183
+ const prices = x.map((line, i) => linePriceStands(line, y[i]));
184
+ if (prices.includes(false))
185
+ return false;
186
+ return subtotals === true || prices.every((stands) => stands === true);
187
+ }
188
+ /** True when `now` holds an item `before` did not: the call put something new in the cart. */
189
+ function addsItem(now, before) {
190
+ if (!now || (now.items ?? []).length === 0)
191
+ return false;
192
+ const had = new Set((before && before.merchant === now.merchant ? before.items ?? [] : []).map((line) => lineKey({ ...line, qty: 0 })));
193
+ return (now.items ?? []).some((line) => !had.has(lineKey({ ...line, qty: 0 })));
194
+ }
195
+ /**
196
+ * The yes left standing when a confirm stopped for a step outside the chat, or null. The cart the confirm stopped on must
197
+ * be the same order as the one the yes covers; the total the shopper said yes to stays the one they saw.
198
+ */
199
+ function yesAfter(result, yes) {
200
+ const stopped = result.cart ?? (result.carts.length === 1 ? result.carts[0] : null);
201
+ if ('carried' in yes) {
202
+ if (stopped && !sameOrder(stopped, yes.carried.cart))
203
+ return null;
204
+ return { ...yes.carried, cart: stopped ?? yes.carried.cart, conversationId: result.conversation_id ?? yes.carried.conversationId };
205
+ }
206
+ const seen = yes.shown ?? stopped;
207
+ if (!seen || (yes.shown && stopped && !sameOrder(yes.shown, stopped)))
208
+ return null;
209
+ return { conversationId: result.conversation_id, cart: stopped ?? seen, totalCents: seen.totalCents };
210
+ }
211
+ /** The words of the call a `nextStep` turn's `direct` makes. They never reach Agentcard: the turn's calls send no ask. */
212
+ const NEXT_STEP_ASK = 'take the next step';
148
213
  /** What a call of a `nextStep` turn answers with when the shopper has not said yes to the cart on the table. */
149
214
  const ASK_FOR_YES = "The cart is ready, and the shopper has not said yes to it in the chat, so nothing was confirmed. Say the cart's total in one short line and ask for their yes.";
150
215
  const STEP_AFTER_DECLINE = 'The shopper declined the approval, so nothing was charged and no order was placed. The cart is still here. Say so in one short line and offer the way forward: their yes sends a new approval link, or they can change the order.';
@@ -264,6 +329,7 @@ function answerOf(turn) {
264
329
  store_pick: turn.store_pick,
265
330
  hashes: hashesOf(turn.cart, turn.carts),
266
331
  cart_building: turn.cart_building,
332
+ choice: turn.choice ?? null,
267
333
  envelope: turn.raw,
268
334
  };
269
335
  }
@@ -689,6 +755,9 @@ export function buyTool(options) {
689
755
  if (o.replyAsWritten !== undefined && typeof o.replyAsWritten !== 'boolean')
690
756
  throw new TypeError('Pass replyAsWritten as true or false.');
691
757
  const writtenByDefault = o.replyAsWritten === true;
758
+ if (o.textingStyle !== undefined && typeof o.textingStyle !== 'boolean')
759
+ throw new TypeError('Pass textingStyle as true or false.');
760
+ const textingByDefault = o.textingStyle === true;
692
761
  if (o.direct !== undefined && typeof o.direct !== 'boolean')
693
762
  throw new TypeError('Pass direct as true or false.');
694
763
  const directOn = o.direct === true;
@@ -749,6 +818,7 @@ export function buyTool(options) {
749
818
  onAvailability: o.onAvailability,
750
819
  onProducts: o.onProducts,
751
820
  onText: o.onText,
821
+ texting: textingByDefault,
752
822
  onCart: o.onCart,
753
823
  links: null,
754
824
  results: null,
@@ -781,14 +851,25 @@ export function buyTool(options) {
781
851
  /** The conversation and the carts of the last answer, for a `nextStep` call that cannot read the conversation. */
782
852
  let lastConversationId = null;
783
853
  let lastCarts = [];
784
- /** The hash of the cart the last answer showed: the cart a confirm sent now confirms. */
785
- let lastShownHash = null;
854
+ /** The cart the last answer showed: the cart a confirm sent now confirms. */
855
+ let lastShownCart = null;
786
856
  /**
787
- * The yes the shopper gave in the chat to one cart, by its hash, when the confirm it sent stopped for a
788
- * step outside the chat: a card to add or a store to sign in to. A `nextStep` turn confirms that cart
789
- * once the step is done. Any shopper message that is not a plain go-ahead, `ok` or `done` takes it back.
857
+ * The yes the shopper gave in the chat to one cart, when the confirm it sent stopped for a step outside the chat: a card
858
+ * to add or a store to sign in to. A `nextStep` turn confirms the cart once the step is done, as long as it holds the
859
+ * same items at the same store (`sameOrder`): a total that moved because the store priced its fees and tax in keeps the
860
+ * yes, and the turn says the new total. Any shopper message that is not a plain go-ahead, `ok` or `done` takes it back,
861
+ * and so does a declined approval.
790
862
  */
791
863
  let carriedYes = null;
864
+ /**
865
+ * The go-ahead of the shopper's pick, when they picked what is in the cart and answered only the checkout's questions
866
+ * since (address, phone, name, email, a yes to an address read back). A `nextStep` turn after a saved card or a store
867
+ * sign-in confirms that cart as it stands, when it is the same order (`sameOrder`), whatever the Vault does: the approval
868
+ * link is the shopper's approval. Any other shopper message takes it back, and so do a cart that changed and a declined approval.
869
+ */
870
+ let pickedOrder = null;
871
+ /** How many shopper messages took a pick's go-ahead back: a call that started before one leaves no pick behind. */
872
+ let pickWithdrawals = 0;
792
873
  /** The add-card link an answer carried while it asked for the store sign-in first: the card is the step after it. */
793
874
  let cardAfterSignIn = null;
794
875
  /** The shopper's words that never ran because they were not connected yet: they run once their card is saved. */
@@ -834,7 +915,17 @@ export function buyTool(options) {
834
915
  * way a shopper message reaches the tool runs it: `turn`, `instantAck` and `instantReaction` (review of #9220).
835
916
  */
836
917
  const withdrawUnlessGoAhead = (shopperText) => {
837
- if (keepsYes(String(shopperText ?? '')))
918
+ const text = String(shopperText ?? '');
919
+ const goAhead = keepsYes(text);
920
+ // A pick's go-ahead also stands through the answers the checkout asks for, and through the answer that lets the store
921
+ // cart the pick, such as a size, while no cart holds it yet.
922
+ // Counted with no pick standing too, so a pick still running when the shopper says "no wait" leaves none behind.
923
+ // A short `cancel`, `wait` or `no` answers the question's shape, so it never counts as the answer the pick waits on.
924
+ if (!goAhead && !answersCheckout(text, { nextAction: lastNextAction, asked: lastReply }) && !(pickedOrder?.cart === null && answersQuestion(text, lastReply) && !holdsBack(text))) {
925
+ pickedOrder = null;
926
+ pickWithdrawals++;
927
+ }
928
+ if (goAhead)
838
929
  return;
839
930
  carriedYes = null;
840
931
  unconnectedAsk = null;
@@ -1322,7 +1413,7 @@ export function buyTool(options) {
1322
1413
  return;
1323
1414
  const outside = result.status === 'needs_card' || result.next_action === 'add_card' || result.next_action === 'store_login';
1324
1415
  if (call.confirm)
1325
- carriedYes = outside && call.yesHash ? { hash: call.yesHash } : null;
1416
+ carriedYes = outside ? yesAfter(result, call.yes) : null;
1326
1417
  if (result.next_action === 'store_login' && turn?.add_card_url)
1327
1418
  cardAfterSignIn = { url: turn.add_card_url, expiresAt: turn.add_card_expires_at };
1328
1419
  else if (result.next_action !== 'store_login')
@@ -1330,6 +1421,45 @@ export function buyTool(options) {
1330
1421
  if (!call.confirm && call.ask !== '')
1331
1422
  unconnectedAsk = result.error_code === 'user_not_connected' ? call.ask : null;
1332
1423
  };
1424
+ /**
1425
+ * What a shopper's own call leaves of a pick's go-ahead (`pickedOrder`). A pick, a tap on a product's card or a request
1426
+ * to buy it, that leaves one cart and no list of products to choose from starts one, with that cart; one whose store still
1427
+ * asks a question before it carts the pick starts one with no cart yet. A later answer keeps it while the cart stays the
1428
+ * same order, and notes the cart as the chat shows it now; an answer whose cart changed, another purchase, more than one
1429
+ * cart, or a confirm of the shopper's own ends it. `heard` is false for a call whose shopper message no turn read
1430
+ * (`withdrawUnlessGoAhead`): its words must then answer the checkout too.
1431
+ */
1432
+ const notePick = (result, call) => {
1433
+ if (call.pickWithdrawalsAtStart !== pickWithdrawals)
1434
+ return;
1435
+ const now = result.cart ?? (result.carts.length === 1 ? result.carts[0] : null);
1436
+ if (call.picking) {
1437
+ // A search that listed products beside a cart already there picked nothing in it; one that carted what it names did.
1438
+ const listed = shownProducts(result.products).length > 0;
1439
+ pickedOrder = result.carts.length > 1
1440
+ ? null
1441
+ : now && (!listed || call.added)
1442
+ ? { conversationId: result.conversation_id, cart: now }
1443
+ : !now && !listed && result.error_code === null
1444
+ ? { conversationId: result.conversation_id, cart: null }
1445
+ : null;
1446
+ return;
1447
+ }
1448
+ const picked = pickedOrder;
1449
+ if (!picked)
1450
+ return;
1451
+ if (call.confirm || result.carts.length > 1 || (picked.conversationId && result.conversation_id && result.conversation_id !== picked.conversationId)) {
1452
+ pickedOrder = null;
1453
+ return;
1454
+ }
1455
+ if (!call.heard && !keepsYes(call.ask) && !answersCheckout(call.ask, { nextAction: call.asked.nextAction, asked: call.asked.reply })) {
1456
+ pickedOrder = null;
1457
+ return;
1458
+ }
1459
+ if (!now)
1460
+ return;
1461
+ pickedOrder = picked.cart === null || sameOrder(now, picked.cart) ? { conversationId: picked.conversationId ?? result.conversation_id, cart: now } : null;
1462
+ };
1333
1463
  /** An answer the tool writes itself, from where the purchase stands, with nothing sent to Agentcard. */
1334
1464
  const stepResult = (fields) => ({
1335
1465
  messages: [],
@@ -1371,6 +1501,7 @@ export function buyTool(options) {
1371
1501
  const standing = { cart: one ?? carts.at(-1) ?? null, carts, hashes: carts.map((c) => c.hash), conversation_id: conversationId };
1372
1502
  if (event.kind === 'approval_declined') {
1373
1503
  carriedYes = null;
1504
+ pickedOrder = null;
1374
1505
  return { local: stepResult({ ...standing, status: 'needs_input', reply: carts.length > 0 ? STEP_AFTER_DECLINE : NO_CART_WAITING }) };
1375
1506
  }
1376
1507
  if (event.kind === 'card_added' && unconnectedAsk) {
@@ -1384,9 +1515,28 @@ export function buyTool(options) {
1384
1515
  cardAfterSignIn = null;
1385
1516
  return { local: stepResult({ ...standing, status: 'needs_card', reply: CARD_AFTER_SIGN_IN, links: [{ kind: 'vault', url: link.url, expiresAt: link.expiresAt }], next_action: 'add_card' }) };
1386
1517
  }
1387
- if (carriedYes && one && conversationId && one.hash === carriedYes.hash) {
1518
+ // The shopper's yes goes on past the step they just finished: the same items at the same store, in the same purchase,
1519
+ // confirmed by the cart's hash as it stands now. A total that moved because the store priced its fees and tax in keeps
1520
+ // the yes, and the answer says the new total beside the next ask; the approval link is still the shopper's consent to
1521
+ // the payment (Karen, 2026-10-27).
1522
+ const yes = carriedYes;
1523
+ const inPurchase = (at) => at === null || at === conversationId;
1524
+ if (yes && one && conversationId && inPurchase(yes.conversationId) && sameOrder(one, yes.cart)) {
1388
1525
  carriedYes = null;
1389
- return { body: { confirm: one.hash, conversation_id: conversationId, payment_source: 'vault', ...who } };
1526
+ return { body: { confirm: one.hash, conversation_id: conversationId, payment_source: 'vault', ...who }, yes: { ...yes, cart: one, conversationId } };
1527
+ }
1528
+ // The shopper picked what is in the cart and finished the step outside the chat: finishing it is their go-ahead for the
1529
+ // same order, confirmed by the cart's hash as it stands now, and the approval link stays their consent to the payment
1530
+ // (Karen, 2026-10-27: "we are again asking for confirmation after user adds a card. we should assume they want to move
1531
+ // forward"). That holds whatever the shopper's Vault does: the approval link is the approval (Karen, 2026-10-27: "That
1532
+ // is the goal of the approval link. You shouldn't be asking again. Take the approval from the approval link as the
1533
+ // approval.").
1534
+ const picked = pickedOrder;
1535
+ if (picked?.cart && one && conversationId && inPurchase(picked.conversationId) && (event.kind === 'card_added' || event.kind === 'store_connected')) {
1536
+ pickedOrder = null;
1537
+ if (sameOrder(one, picked.cart)) {
1538
+ return { body: { confirm: one.hash, conversation_id: conversationId, payment_source: 'vault', ...who }, yes: { conversationId, cart: one, totalCents: picked.cart.totalCents, picked: true } };
1539
+ }
1390
1540
  }
1391
1541
  // A checkout the shopper already moved past their yes (an approval link waiting, or the order going through) is reported as
1392
1542
  // it stands: the cart stays while it waits, and asking for another yes would contradict the link they have (review of #9220).
@@ -1443,13 +1593,22 @@ export function buyTool(options) {
1443
1593
  ...(client.company ? { external_user_id: externalUserId } : {}),
1444
1594
  };
1445
1595
  // The cart a confirm sent now confirms, for the yes it carries past a step outside the chat.
1446
- const yesHash = confirm ? lastShownHash : null;
1596
+ const yesShown = confirm ? lastShownCart : null;
1597
+ // A pick of what the shopper saw, by a product's id or its name, or a request to buy something: when its answer carts
1598
+ // it, the shopper's go-ahead for that cart (`notePick`). Read before the answer replaces what the shopper saw.
1599
+ const picksShown = !confirm && (!!pick?.product_id || pickOf(ask) !== null || isProductAsk(ask));
1600
+ const cartBefore = lastShownCart;
1601
+ const pickWithdrawalsAtStart = pickWithdrawals;
1602
+ const askedBefore = { nextAction: lastNextAction, reply: lastReply };
1447
1603
  // The first call after `instantAck` opens the turn of the message it answered, whose reply is the
1448
1604
  // app's. Any other call that starts while another runs, or right after one ended, is the model
1449
1605
  // working through the same turn. A call made through `buy.turn` belongs to that turn alone.
1606
+ // Whether a turn read the shopper's message (`withdrawUnlessGoAhead`): `buy.turn`, or the app's instant reply.
1607
+ let heard = !!explicit;
1450
1608
  if (!explicit) {
1451
1609
  const opened = appAck && Date.now() - appAck.at < APP_TURN_MS ? appAck : null;
1452
1610
  appAck = null;
1611
+ heard = !!opened;
1453
1612
  if (opened) {
1454
1613
  // Only a reply the app sent quiets onWorking. A message such as `yes` that starts a slow search
1455
1614
  // still gets a reply that names nothing, and a slow pick gets one that says it is on it.
@@ -1490,6 +1649,14 @@ export function buyTool(options) {
1490
1649
  * and every message after it, so the shopper reads them in the order Agentcard wrote them.
1491
1650
  */
1492
1651
  const waiting = [];
1652
+ /** True when the last message the shopper read ended in a colon or a question mark, so choices that open the next one are its list. */
1653
+ let leadIn = false;
1654
+ /** Shows one of Agentcard's messages through onText, its lists as bullets (messageForShopper); `told` keeps it as Agentcard wrote it. */
1655
+ const showText = async (text, conversationId) => {
1656
+ await t.onText(messageForShopper(text, leadIn, t.texting), { conversationId });
1657
+ leadIn = leadsIntoList(text);
1658
+ told.push(text);
1659
+ };
1493
1660
  /** The products past the cards of the catalog this call heard last, for the count that follows it. */
1494
1661
  let moreFound = null;
1495
1662
  /** Whether this call's cards went out, and a count that came before them. */
@@ -1631,8 +1798,7 @@ export function buyTool(options) {
1631
1798
  return;
1632
1799
  }
1633
1800
  try {
1634
- await t.onText(text, { conversationId });
1635
- told.push(text);
1801
+ await showText(text, conversationId);
1636
1802
  }
1637
1803
  catch {
1638
1804
  // A message the app failed to show was never read, so the model words the answer.
@@ -1695,8 +1861,7 @@ export function buyTool(options) {
1695
1861
  continue;
1696
1862
  }
1697
1863
  try {
1698
- await t.onText(message.text, { conversationId: message.conversationId });
1699
- told.push(message.text);
1864
+ await showText(message.text, message.conversationId);
1700
1865
  }
1701
1866
  catch {
1702
1867
  // A message the app failed to show was never read, so the model words the answer.
@@ -1705,8 +1870,7 @@ export function buyTool(options) {
1705
1870
  const stepAsk = turn?.next_action ? CHAT_STEP_ASKS[turn.next_action] : undefined;
1706
1871
  if (replaced && stepAsk && settled.reply.endsWith(stepAsk)) {
1707
1872
  try {
1708
- await t.onText(stepAsk, { conversationId: settled.conversation_id });
1709
- told.push(stepAsk);
1873
+ await showText(stepAsk, settled.conversation_id);
1710
1874
  }
1711
1875
  catch {
1712
1876
  // Not shown, so the model asks for the step.
@@ -1715,7 +1879,20 @@ export function buyTool(options) {
1715
1879
  }
1716
1880
  // Every message of Agentcard's own answer reached the shopper through onText, so the model adds nothing to it. An answer
1717
1881
  // whose card ask gave way to the named step was read the same way once every message of it, that ask last, went out.
1718
- const read = !!turn && (settled.reply === turn.reply ? readEvery(told, turn.messages) : replaced && readEvery(told, toMessages(settled.reply)));
1882
+ // A reply the shopper read that asks them to choose, with the values named inside a sentence or not at all, is followed
1883
+ // by one more message that lists every value on its own line (choiceLines), so no choice is read buried in prose.
1884
+ let choiceShown = true;
1885
+ if (t.onText && turn?.choice && told.length > 0 && !messagesListChoice(told, turn.choice)) {
1886
+ const lines = choiceLines(turn.choice);
1887
+ try {
1888
+ await t.onText(t.texting ? textingStyle(lines) : lines, { conversationId: turn.conversation_id || null });
1889
+ }
1890
+ catch {
1891
+ // Not shown, so the shopper has not read the whole answer: the model words it, with the choice it reads.
1892
+ choiceShown = false;
1893
+ }
1894
+ }
1895
+ const read = choiceShown && !!turn && (settled.reply === turn.reply ? readEvery(told, turn.messages) : replaced && readEvery(told, toMessages(settled.reply)));
1719
1896
  const answer = withMessages(settled, turn);
1720
1897
  const delivered = await deliver(answered.size > 0 ? { ...answer, products: withAnswers(answer.products) } : answer, { turn: callTurn, presented: turn?.catalog?.presented === true, owner: t }, send);
1721
1898
  // A count that came before any card (find mode puts its cards in the answer) goes out once the answer's cards did.
@@ -1733,10 +1910,19 @@ export function buyTool(options) {
1733
1910
  // The call waits for it, and one that fails to send fails the call, as a step link the turn does not hold does.
1734
1911
  if (read)
1735
1912
  await t.releaseNow?.();
1913
+ // A confirm the turn sent on the shopper's earlier yes names the total they said yes to beside the cart's total now. A
1914
+ // confirm Agentcard refused, or one whose answer holds another order or asks for a yes, carried nothing: the model asks.
1915
+ const carried = plan && 'body' in plan && plan.yes && turn && delivered.next_action !== 'confirm' ? plan.yes : null;
1916
+ const nowCart = !carried ? null : !delivered.cart ? carried.cart : sameOrder(delivered.cart, carried.cart) ? delivered.cart : null;
1917
+ const yesCarried = carried && nowCart
1918
+ ? { said_yes_to_cents: carried.totalCents, total_cents: nowCart.totalCents, currency: nowCart.currency ?? null, ...(carried.picked ? { picked: true } : {}) }
1919
+ : null;
1736
1920
  const result = {
1737
1921
  ...delivered,
1738
1922
  ...(t.ackDelivered ? { shopper_already_told_looking: true } : {}),
1739
1923
  ...(read ? { shopper_already_read_reply: true } : {}),
1924
+ ...(confirmSent ? { confirm_sent: true } : {}),
1925
+ ...(yesCarried ? { yes_carried: yesCarried } : {}),
1740
1926
  };
1741
1927
  lastNextAction = result.next_action;
1742
1928
  lastReply = result.reply;
@@ -1749,12 +1935,19 @@ export function buyTool(options) {
1749
1935
  if (result.conversation_id)
1750
1936
  lastConversationId = result.conversation_id;
1751
1937
  if (result.cart?.hash)
1752
- lastShownHash = result.cart.hash;
1938
+ lastShownCart = result.cart;
1753
1939
  // A shopper's own call, or the shopper's yes or words a turn taken on its own sent, leaves what the next step needs.
1754
1940
  if (!t.event)
1755
- noteShopperStep(result, turn, { confirm: !!confirm, yesHash, ask, withdrawalsAtStart });
1941
+ noteShopperStep(result, turn, { confirm: !!confirm, yes: { shown: yesShown }, ask, withdrawalsAtStart });
1756
1942
  else if (plan && 'body' in plan)
1757
- noteShopperStep(result, turn, { confirm: confirmSent, yesHash: typeof sent.confirm === 'string' ? sent.confirm : null, ask: sent.ask ?? '', withdrawalsAtStart });
1943
+ noteShopperStep(result, turn, { confirm: confirmSent && !!plan.yes, yes: plan.yes ? { carried: plan.yes } : { shown: null }, ask: sent.ask ?? '', withdrawalsAtStart });
1944
+ if (!t.event && turn) {
1945
+ // Words that name something to buy and put a new item in the cart picked it too, as `the chicken pesto caprese from
1946
+ // mendocino farms` carts the dish it names.
1947
+ const added = result.pick?.in_cart === true || addsItem(result.cart, cartBefore);
1948
+ const picking = !confirm && (picksShown || result.pick?.in_cart === true || (isNewRequest(ask) && added));
1949
+ notePick(result, { picking, added, confirm: !!confirm, ask, heard, asked: askedBefore, pickWithdrawalsAtStart });
1950
+ }
1758
1951
  // An answer that lists products replaces the names, even when every one is too short to keep.
1759
1952
  const shown = shownProducts(result.products);
1760
1953
  if (shown.length > 0)
@@ -1867,6 +2060,9 @@ export function buyTool(options) {
1867
2060
  if (own.replyAsWritten !== undefined && typeof own.replyAsWritten !== 'boolean')
1868
2061
  throw new TypeError('Pass replyAsWritten to the turn as true or false, or leave it out.');
1869
2062
  const written = own.replyAsWritten ?? writtenByDefault;
2063
+ if (own.textingStyle !== undefined && typeof own.textingStyle !== 'boolean')
2064
+ throw new TypeError('Pass textingStyle to the turn as true or false, or leave it out.');
2065
+ const texting = own.textingStyle ?? textingByDefault;
1870
2066
  const hearText = own.onText ?? o.onText;
1871
2067
  // Messages are spaced only for an app that hears each one as it goes out.
1872
2068
  const pauseOf = paceOf(own.pace);
@@ -1936,6 +2132,7 @@ export function buyTool(options) {
1936
2132
  // Agentcard's message takes its place in the turn's sequence, after the items before it, and goes to onText alone, so
1937
2133
  // an app that passes onItem too hears it once.
1938
2134
  onText: hearText ? (text, info) => feed.push({ kind: 'text', text }, () => hearText(text, info)) : undefined,
2135
+ texting,
1939
2136
  onCart: own.onCart ?? o.onCart,
1940
2137
  tell: onItem || baseOnWorking ? (text) => feed.push({ kind: 'text', text }, () => (onItem ? onItem({ kind: 'text', text }) : baseOnWorking?.(text))) : undefined,
1941
2138
  links: [],
@@ -2004,9 +2201,13 @@ export function buyTool(options) {
2004
2201
  const results = state.results;
2005
2202
  // A pick or an answer goes straight only after a turn that ended on Agentcard's answer, and never while a call still runs.
2006
2203
  const answered = previousTurn !== null && (previousTurn.results?.length ?? 0) > 0;
2007
- const direct = directOn && !event && running === 0
2008
- ? directCall(String(shopperText ?? ''), { answered, nextAction: lastNextAction, asked: lastReply, products: lastAnswerProducts, stores: lastAnswerStores })
2009
- : null;
2204
+ // A `nextStep` turn's call is the same whatever the model writes (`stepPlan` reads where the purchase stands), so it goes
2205
+ // straight too: with `replyAsWritten`, a step whose answer is a link, such as the approval after a saved card, needs no model.
2206
+ const direct = !directOn || running > 0
2207
+ ? null
2208
+ : event
2209
+ ? { input: { ask: NEXT_STEP_ASK, confirm: false, new_order: false, product_id: null, store_id: null }, reason: 'next_step' }
2210
+ : directCall(String(shopperText ?? ''), { answered, nextAction: lastNextAction, asked: lastReply, products: lastAnswerProducts, stores: lastAnswerStores });
2010
2211
  if (!event)
2011
2212
  previousTurn = state;
2012
2213
  // What Agentcard told the shopper that no history the model ran with holds yet.
@@ -2067,7 +2268,7 @@ export function buyTool(options) {
2067
2268
  links: { get: () => [...links], enumerable: false },
2068
2269
  results: { get: () => [...results], enumerable: false },
2069
2270
  direct: { value: direct, enumerable: false },
2070
- replyAsWritten: { get: () => (written && results.length > 0 ? replyAsWritten(results[results.length - 1]) : null), enumerable: false },
2271
+ replyAsWritten: { get: () => (written && results.length > 0 ? replyAsWritten(results[results.length - 1], { textingStyle: texting }) : null), enumerable: false },
2071
2272
  items: { get: () => feed.items.map((item) => (item.kind === 'link' ? { kind: 'link', link: { ...item.link } } : { ...item })), enumerable: false },
2072
2273
  // Each message joins the feed the moment it is cut, so a link the tool hands over during a pause never overtakes text already written.
2073
2274
  // A step link held for this message goes out right after its last paragraph, once the stream ends.
package/dist/client.d.ts CHANGED
@@ -373,6 +373,32 @@ export interface BuyStorePick {
373
373
  name: string | null;
374
374
  selected: boolean;
375
375
  }
376
+ /** One value the shopper picks from. `id` is the option id the cart takes, null when the shopper's words answer it. */
377
+ export interface BuyChoiceOption {
378
+ id: string | null;
379
+ label: string;
380
+ /** What choosing it adds to the price, in cents: 0 when free, null when the store does not say. */
381
+ price_delta_cents: number | null;
382
+ }
383
+ /** One group of values, such as a sandwich's bread or a shirt's size. `min` and `max` count the values picked; `max` null for no limit. */
384
+ export interface BuyChoiceGroup {
385
+ name: string;
386
+ required: boolean;
387
+ min: number;
388
+ max: number | null;
389
+ options: BuyChoiceOption[];
390
+ }
391
+ /**
392
+ * The choice an answer's reply asks the shopper to make, as data: the question, the item it is about, where the values come
393
+ * from (`menu_item`: a restaurant item's options, `store_checkout`: a store's own size or colour question, `catalog_variant`:
394
+ * a product's sizes or colours in stock), and every group with its values.
395
+ */
396
+ export interface BuyChoice {
397
+ question: string | null;
398
+ item: string | null;
399
+ source: string;
400
+ groups: BuyChoiceGroup[];
401
+ }
376
402
  export interface BuyPaymentSource {
377
403
  source: string;
378
404
  brand: string | null;
@@ -435,6 +461,8 @@ export interface BuyTurn {
435
461
  store_pick: BuyStorePick | null;
436
462
  /** The cart the store's checkout is still building after this answer; null when none is, and on an older API. */
437
463
  cart_building: BuyCartBuilding | null;
464
+ /** The choice the reply asks the shopper to make, each group with every value; null when it asks none, and on an older API. */
465
+ choice: BuyChoice | null;
438
466
  /** The answer as Agentcard sent it. */
439
467
  raw: Record<string, unknown>;
440
468
  }
@@ -506,6 +534,8 @@ export declare class AgentcardApiError extends Error {
506
534
  }
507
535
  /** A turn as Agentcard answers it. */
508
536
  export declare function turnOf(body: Record<string, unknown>): BuyTurn;
537
+ /** An answer's `choice`, its groups with at least one value each, or null when it carries none. */
538
+ export declare function choiceOf(value: unknown): BuyChoice | null;
509
539
  /** A cart being built, as an answer, a refusal or the conversation read names it. */
510
540
  export declare function cartBuildingOf(value: unknown): BuyCartBuilding | null;
511
541
  /** The client every shopper's buy tool shares. Build one per set of credentials and keep it. */
package/dist/client.js CHANGED
@@ -299,9 +299,20 @@ export function turnOf(body) {
299
299
  stores: storesOf(body.stores),
300
300
  store_pick: isRecord(body.store_pick) && str(body.store_pick.store_id) ? { store_id: body.store_pick.store_id, name: str(body.store_pick.name), selected: body.store_pick.selected === true } : null,
301
301
  cart_building: cartBuildingOf(body.cart_building),
302
+ choice: choiceOf(body.choice),
302
303
  raw: body,
303
304
  };
304
305
  }
306
+ /** An answer's `choice`, its groups with at least one value each, or null when it carries none. */
307
+ export function choiceOf(value) {
308
+ if (!isRecord(value) || !Array.isArray(value.groups))
309
+ return null;
310
+ const groups = value.groups.filter(isRecord).flatMap((g) => {
311
+ const options = (Array.isArray(g.options) ? g.options : []).filter(isRecord).flatMap((o) => str(o.label) ? [{ id: str(o.id), label: o.label.trim(), price_delta_cents: num(o.price_delta_cents) }] : []);
312
+ return options.length ? [{ name: str(g.name) ?? '', required: g.required === true, min: num(g.min) ?? 0, max: num(g.max), options }] : [];
313
+ });
314
+ return groups.length ? { question: str(value.question), item: str(value.item), source: str(value.source) ?? '', groups } : null;
315
+ }
305
316
  /** A cart being built, as an answer, a refusal or the conversation read names it. */
306
317
  export function cartBuildingOf(value) {
307
318
  return isRecord(value) && str(value.store) ? { store: value.store, started_at: str(value.started_at) ?? '' } : null;
@@ -42,6 +42,7 @@ export const BUY_TOOL_DESCRIPTION = [
42
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
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
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 `yes_carried` is set, the shopper already said yes to this cart, or picked it (`yes_carried.picked`), and Agentcard went ahead on it once they finished the step outside the chat, so never ask for their yes again: ask only for the step `next_action` names. When `yes_carried.total_changed` is true, that one short line opens with the new total, the one price it names beside `max_charge`, such as "it\'s $17.89 with fees and tax now. approve the payment to place your order (up to $19.02)".',
45
46
  '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
47
  '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".',
47
48
  '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.',
@@ -52,6 +53,7 @@ export const BUY_TOOL_DESCRIPTION = [
52
53
  "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
54
  '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
55
  '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".',
56
+ 'When you list options or choices for the shopper, such as sizes, breads, times, or stores no card shows, write one per line, each starting with "• ", in the same paragraph as the line that asks which one, so the question and its choices reach the shopper as one message. When the result has `choice`, list every value it holds that way, with its extra price, such as "• Gluten-free bread (+$2.46)", even when `reply` names them inside a sentence. The products and stores the cards show are still never listed again.',
55
57
  '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
58
  '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
59
  '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.',
package/dist/direct.d.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import type { BuyNextAction } from './client.js';
2
2
  import type { BuyToolInput } from './definition.js';
3
- /** Why a message goes straight to Agentcard. */
4
- export type DirectReason = 'product_ask' | 'pick' | 'answer' | 'confirm';
3
+ /**
4
+ * Why a message goes straight to Agentcard. `next_step` is a turn `buy.nextStep` opened: its call reads where the
5
+ * purchase stands and goes on from there whatever the model would write, so it never needs the model to decide it.
6
+ */
7
+ export type DirectReason = 'product_ask' | 'pick' | 'answer' | 'confirm' | 'next_step';
5
8
  /** The buy call for a message that goes straight to Agentcard, and why it does. */
6
9
  export interface DirectCall {
7
10
  input: BuyToolInput;
package/dist/index.d.ts CHANGED
@@ -1,14 +1,16 @@
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
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
+ export { ORDER_STEPS, updateOrder, arrivalText, type ShopperOrder, type OrderStep, type OrderPathStep, type OrderStepState, type ArrivalTextOptions, } from './order.js';
4
5
  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 AgentcardTraceOptions, type OtelApi, type TraceContext, type FetchLike, type FetchLikeResponse, type BuyProgress, type BuyProgressStage, type BuyCatalogUpdate, type BuyAvailability, type BuyAvailabilityUpdate, type BuyFoundUpdate, type BuyTurnStatus, type BuySegmentUpdate, type BuyCartUpdate, type BuyDeltaUpdate, type BuyStreamHandlers, type Shopper, type 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
+ export { AgentcardClient, AgentcardApiError, BUY_TURN_TIMEOUT_MS, DEFAULT_BASE_URL, type AgentcardClientOptions, type AgentcardTraceOptions, type OtelApi, type TraceContext, type FetchLike, type FetchLikeResponse, type BuyProgress, type BuyProgressStage, type BuyCatalogUpdate, type BuyAvailability, type BuyAvailabilityUpdate, type BuyFoundUpdate, type BuyTurnStatus, type BuySegmentUpdate, type BuyCartUpdate, type BuyDeltaUpdate, type BuyStreamHandlers, type Shopper, type 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, type BuyChoice, type BuyChoiceGroup, type BuyChoiceOption, } from './client.js';
6
7
  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
8
  export { toMessages, messageStream, typingDelay, type MessagePace, type MessageStream, type MessageStreamOptions, } from './messages.js';
8
9
  export { buyTool, nextStepPrompt, type BuyTool, type BuyToolOptions, type BuyToolResult, type BuyToolStatus, type BuyToolLink, type BuyToolFound, type BuyToolAvailability, type BuyToolProduct, type BuyToolProductAction, productAction, type BuyToolStore, type BuyToolTurn, type BuyToolTurnItem, type BuyToolTurnOptions, type BuyToolTurnMessage, type BuyAckMode, } from './buy-tool.js';
9
10
  export { instantAck, instantReaction, ackFor, keepsYes, ACK_REACTION, type AckForOptions } from './ack.js';
10
11
  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';
12
+ export { forModel, forModelError, stepAsWritten, withoutCardList, stripLinks, type BuyToolModelView, type BuyToolModelCart, type BuyToolModelProduct, type BuyToolModelStore, type ShownCard, } from './model-view.js';
13
+ export { asBullets, choiceLines, replyAsWritten, textingStyle, withChoice, type ReplyAsWrittenOptions } from './texting.js';
12
14
  export { toAnthropicTool, toOpenRouterTool, type AnthropicBuyTool, type OpenRouterToolkit, type ZodLike, } from './adapters.js';
13
15
  export { listenToAgentcard, type AgentcardListener, type ListenOptions, type ListenCursorStore, type StreamFetch, } from './listen.js';
14
16
  export { ACTION_LINK_ENDINGS, ACTION_LINK_KINDS, actionLinkId, actionLinkOutcome, isActionLink, type ActionLinkKind, type ActionLinkMessage, type ActionLinkOutcome, } from './action-links.js';
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { verifyAgentcardWebhook, AgentcardSignatureError, } from './verify.js';
2
2
  export { readPurchaseMessage, } from './message.js';
3
3
  export { readShopperMessage, isStale, SHOPPER_EVENT_TYPES, NEXT_STEP_KINDS, } from './shopper-message.js';
4
+ export { ORDER_STEPS, updateOrder, arrivalText, } from './order.js';
4
5
  export { agentcardWebhook, purchaseAgentWebhook, } from './handler.js';
5
6
  export { AgentcardClient, AgentcardApiError, BUY_TURN_TIMEOUT_MS, DEFAULT_BASE_URL, } from './client.js';
6
7
  export { BUY_TOOL_NAME, BUY_TOOL_DESCRIPTION, buyToolDefinition, parseBuyToolInput, } from './definition.js';
@@ -8,7 +9,8 @@ export { toMessages, messageStream, typingDelay, } from './messages.js';
8
9
  export { buyTool, nextStepPrompt, productAction, } from './buy-tool.js';
9
10
  export { instantAck, instantReaction, ackFor, keepsYes, ACK_REACTION } from './ack.js';
10
11
  export { directCall, isProductAsk } from './direct.js';
11
- export { forModel, forModelError, replyAsWritten, withoutCardList, stripLinks, } from './model-view.js';
12
+ export { forModel, forModelError, stepAsWritten, withoutCardList, stripLinks, } from './model-view.js';
13
+ export { asBullets, choiceLines, replyAsWritten, textingStyle, withChoice } from './texting.js';
12
14
  export { toAnthropicTool, toOpenRouterTool, } from './adapters.js';
13
15
  export { listenToAgentcard, } from './listen.js';
14
16
  export { ACTION_LINK_ENDINGS, ACTION_LINK_KINDS, actionLinkId, actionLinkOutcome, isActionLink, } from './action-links.js';
package/dist/messages.js CHANGED
@@ -6,14 +6,14 @@
6
6
  */
7
7
  /** A line holding nothing but spaces or tabs. */
8
8
  const BLANK = /^[ \t]*$/;
9
- /** A list item: `-`, `*` or `+`, or a number followed by `.` or `)`, then a space or the line's end. */
10
- const LIST_ITEM = /^ {0,3}(?:[-*+]|\d{1,9}[.)])(?:[ \t]|$)/;
9
+ /** A list item: `-`, `*`, `+` or the bullet `•`, or a number followed by `.` or `)`, then a space or the line's end. */
10
+ const LIST_ITEM = /^ {0,3}(?:[-*+•]|\d{1,9}[.)])(?:[ \t]|$)/;
11
11
  /** A list item whose marker a space already follows, the only kind a line still being written is sure to stay. */
12
- const LIST_ITEM_SPACED = /^ {0,3}(?:[-*+]|\d{1,9}[.)])[ \t]/;
12
+ const LIST_ITEM_SPACED = /^ {0,3}(?:[-*+•]|\d{1,9}[.)])[ \t]/;
13
13
  /** A line indented under a list item: two spaces or a tab, then text. */
14
14
  const INDENTED = /^(?: {2,}|\t)\S/;
15
15
  /** The start of a line that can still grow into a list item or an indented line once more text arrives. */
16
- const MAYBE_ITEM = /^ {0,3}(?:[-*+]|\d{1,9}[.)]?)?$|^[ \t]*$/;
16
+ const MAYBE_ITEM = /^ {0,3}(?:[-*+•]|\d{1,9}[.)]?)?$|^[ \t]*$/;
17
17
  /** A code fence: three or more backticks or tildes, indented by at most three spaces. */
18
18
  const FENCE = /^ {0,3}(`{3,}|~{3,})/;
19
19
  /** How the line after a blank line inside a list reads: it continues the list, it ends it, or it is too short to tell. */