@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/README.md CHANGED
@@ -234,7 +234,13 @@ if (written !== null) await turn.say(written);
234
234
  history = written !== null ? [...runner.params.messages, { role: 'assistant', content: written }] : runner.params.messages;
235
235
  ```
236
236
 
237
- Any other answer, such as an approval sent, an order placed or a refusal, leaves `turn.replyAsWritten` null, and your model words it. So does a line the tool writes itself for your model, such as the note that the cart changed or a `buy.nextStep` turn's ask for the shopper's yes: only a reply Agentcard sent goes out as written. `replyAsWritten(result)` applies the same rule to one answer, for a loop that reads `turn.results` itself. Leave the option off to have your model write every answer.
237
+ An answer whose next step is a link the turn handed over gets the same short line every time, whoever wrote the reply, so sending the card link, the sign-in link or the approval link takes no model either: `add your card to place the order`, `sign in to DoorDash to place your order`, or `approve the payment to place your order (up to $17.89)`, with the most the shopper can be charged in the parentheses. Show it with `turn.say()` and the link follows it. When the shopper's own message just put the cart together, a line about the cart comes first, as its own message (`1 Chicken Salad from Souvla, $17.43`); after a yes or a saved card, the ask is the whole message. `stepAsWritten(result)` returns that line for one answer, or null when the answer is not a link step, its link did not go out, a card or approval step has no cart to place, or the answer also showed products.
238
+
239
+ Any other answer, such as an order placed or a refusal, leaves `turn.replyAsWritten` null, and your model words it. So does a line the tool writes itself for your model, such as the note that the cart changed or a `buy.nextStep` turn's ask for the shopper's yes: only a reply Agentcard sent goes out as written. `replyAsWritten(result)` applies the same rule to one answer, for a loop that reads `turn.results` itself. Leave the option off to have your model write every answer.
240
+
241
+ Every list in the reply comes as bullet lines that start with `• `, the choices Agentcard writes one per line with no marker included, such as the breads after `which bread do you want?`. A numbered list of the answer's products or stores keeps its numbers, since the shopper may answer `number 3`. Each message `onText` hears comes the same way. Pass `textingStyle: true`, to `buyTool` or `buy.turn`, to get the reply and those messages in a chat's texting style as well: lowercase at the start of each sentence and for "I", no markdown and no spaced dashes, with the bullets kept. `textingStyle(text)` and `asBullets(text)` do the same to any text.
242
+
243
+ An answer that asks the shopper to choose, such as a sandwich's bread, a shirt's size or a store's own size question, carries `result.choice`: each group with every value, its option id and its extra price in `price_delta_cents`. The reply shown as written lists each value on its own `• ` line under the question, with its extra price, and so does one more `onText` message when the streamed reply named the values inside a sentence. `withChoice(text, result.choice)` lists them under any text of your own.
238
244
 
239
245
  ### Send a clear shopping message straight to Agentcard
240
246
 
@@ -256,6 +262,8 @@ if (turn.direct) {
256
262
  // Otherwise run your model on `messages` as before: it words the answer to the call already made, or decides the call itself.
257
263
  ```
258
264
 
265
+ A turn `buy.nextStep` opens carries `turn.direct` too, with the reason `next_step`: its call reads where the purchase stands whatever the model would write, so send it the same way. With `replyAsWritten`, a step whose answer is a link, such as the approval link once the shopper saved their card, then reaches the shopper with no model call at all.
266
+
259
267
  Everything else leaves `turn.direct` null for your model: a question, small talk, a change of mind such as `no` or `wait`, a command to the cart, and anything about the account or an order. A pick or an answer goes straight only when the shopper's last turn ended on Agentcard's answer, so a reply to something your model said reaches your model. `directCall(text, context)` applies the same rule to one message, for an app that keeps its own state. The option is off by default, because it moves the decision of when to call Agentcard from your model into the tool.
260
268
 
261
269
  ### Hand the app the turn in order
@@ -348,17 +356,22 @@ async function runModel(shopperId: string, turn: BuyToolTurn) {
348
356
 
349
357
  | The shopper finished | The next message |
350
358
  | --- | --- |
351
- | Saving a card, after their yes stopped for it | The approval link: `i sent you a link to approve the payment for your chicken salad. it works for about 15 minutes, and the most you'll be charged is $19.02.` |
352
- | Saving a card, with no yes yet | The total and the ask for the yes: `your Souvla chicken salad comes to $17.43 with fees. should i go ahead and place the order?` |
359
+ | Saving a card, after their yes stopped for it | The approval link: `approve the payment to place your order (up to $19.02)` |
360
+ | Saving a card, after picking the product and answering only the checkout's questions | The approval link: `approve the payment to place your order (up to $221.00)` |
361
+ | Saving a card, with no yes and no pick standing | The total and the ask for the yes: `your Souvla chicken salad comes to $17.43 with fees. should i go ahead and place the order?` |
353
362
  | Signing in to the store, with a card still to add | The add-card link: `next, add a card with the link i sent. it works for about 24 hours.` |
354
363
  | Signing in to the store, with a card on file | The total and the ask for the yes, or the approval link after a yes that stopped for the sign-in |
364
+ | Signing in or saving a card, after a yes, when the store priced its fees and tax in since | The new total, then the next link: `it's $17.89 with fees and tax now. approve the payment to place your order (up to $17.89)` |
355
365
  | Declining the approval | `you declined the payment, so you haven't been charged. your chicken salad is still in the cart, so just say yes if you want a new approval link, or tell me what to change` |
356
366
 
367
+ With `direct` and `replyAsWritten` on the tool, a step needs no model when its answer is a link: send `step.run(step.direct.input)` first, and when `step.replyAsWritten` is set, post it, with `step.links`, in place of the model's answer. When it is null, as for the ask for a yes, run your model as above.
368
+
357
369
  The turn moves the purchase only up to the shopper's own decision, and the tool holds it there whatever the model calls:
358
370
 
359
371
  - No call of the turn sends an ask to Agentcard, which could read the model's words as the shopper's go-ahead. Each call reads where the purchase stands and answers with the next step: the card link the sign-in held back, or the cart and the ask for the shopper's yes.
360
- - A confirm goes in only for the yes the shopper already gave in the chat, such as "lets place it", when that confirm stopped for the card or the sign-in they just finished. It goes in once, for that cart's hash, so Agentcard refuses it if the cart changed. A shopper message after the yes that is not a plain go-ahead, such as "wait, make it two", takes it back. Any other confirm the model sends is refused by the tool, and nothing reaches Agentcard.
361
- - The approval in the Vault is still the shopper's: the confirm sends the approval link, and the order goes in only once they approve it.
372
+ - A confirm goes in only for the yes the shopper already gave in the chat, such as "lets place it", when that confirm stopped for the card or the sign-in they just finished, and the shopper is never asked for that yes again. It goes on past each step, the sign-in and then the card, as long as the cart holds the same items, in the same quantities and at the same prices, at the same store: a total that moved only because the store priced its fees, tax or delivery in keeps the yes, and the next ask says the new total (`yes_carried` on the answer). Each confirm names the cart's hash as it stands, so Agentcard refuses it if the cart changed. A cart whose items, quantities or store changed, a shopper message after the yes that is not a plain go-ahead, such as "wait, make it two", and a declined approval each take the yes back, and the shopper is asked again. Any other confirm the model sends is refused by the tool, and nothing reaches Agentcard.
373
+ - A shopper who picked what is in the cart, by tapping a product's card or asking to buy it, and since answered only the checkout's questions (an address, a phone number, a name, an email, a yes to an address read back to them) or said a plain go-ahead, gave the same go-ahead: saving the card or signing in to the store confirms that cart by its hash as it stands, with no ask for a yes (`yes_carried.picked`). A cart whose items, quantities, item prices or store changed since the pick, any other message, such as "actually wait" or a question, and a declined approval take it back.
374
+ - The approval in the Vault is still the shopper's: the confirm sends the approval link, which names the most they can be charged, and the order goes in only once they approve it.
362
375
  - A shopper who was not connected yet when they asked gets their own first ask run, in their words, once their card is saved.
363
376
 
364
377
  `buy.nextStep` returns null when there is no step to take: a message whose `advances` is false, such as `order_placed`, whose `text` is already the whole message; a message for another shopper, or one whose link closed; a message it already took a turn for; and one that arrives while a call for the shopper's own message is still running, since that call's answer tells the model where the purchase stands. Post `message.text` then. `NEXT_STEP_KINDS` lists the kinds that advance: `card_added`, `store_connected` and `approval_declined`. Once `buy.nextStep` opens a turn, the client never hands the message's text to a later turn as words the shopper read, since they never read it. When your agent cannot finish the turn, call `step.release()` before you post `message.text`: the text then reaches later turns once `agentcard.delivered` records it, and a redelivery of the event opens a turn again.
@@ -673,6 +686,40 @@ The handler answers when `onMessage` is done: `200` when it returns, `500` when
673
686
 
674
687
  To read an event without the handler, `readShopperMessage(event)` returns the same message, or null for an event that carries none.
675
688
 
689
+ ## Show an order tracker
690
+
691
+ A message about an order also carries `order`, the order as a tracker draws it, so your chat can show one card that moves through the steps instead of a new line for each. `order_placed`, `order_failed`, `order_confirmed` and `order_updated` carry it, and every other message carries `null`.
692
+
693
+ ```ts
694
+ import { arrivalText, updateOrder, type ShopperOrder } from '@agent-cards/commerce-sdk/browser';
695
+ import type { ShopperMessage } from '@agent-cards/commerce-sdk';
696
+
697
+ const trackers = new Map<string, ShopperOrder>();
698
+
699
+ function onOrderMessage(message: ShopperMessage) {
700
+ const order = message.order;
701
+ if (!order) return;
702
+ const key = order.orderId ?? message.eventId;
703
+ const shown = updateOrder(trackers.get(key), order);
704
+ trackers.set(key, shown);
705
+ drawTracker(key, shown, arrivalText(shown, { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone }));
706
+ }
707
+ ```
708
+
709
+ `drawTracker` stands for your own component. Every message about one order carries the same `orderId`. An `order_failed` for a purchase that placed no order has no `orderId`, so it gets a tracker of its own under its `eventId`, and two such failures never replace each other. `updateOrder` keeps the furthest step, since Agentcard delivers each message at least once and in no set order, and fills what the newer message lacks from the earlier one, such as the total `order_placed` named.
710
+
711
+ | Field | What it holds |
712
+ |---|---|
713
+ | `step`, `label` | Where the order is: `placed` (Placed), `confirmed` (Accepted), `preparing` (Preparing), `on_the_way` (On the way), `delivered` (Delivered), or `canceled` and `failed`, which end it. |
714
+ | `steps` | The five steps of `ORDER_STEPS`, each with its `label` and `done`, `current` or `upcoming`, for the progress bar. |
715
+ | `merchantName`, `storeName` | The service, such as `DoorDash`, and the place the order comes from, such as `Mendocino Farms`, when Agentcard names it. |
716
+ | `title`, `detail` | The store's own line for the step and the sentence under it, such as `Preparing your order` and `Mendocino Farms is preparing your order.` |
717
+ | `arrival`, `deliveryWindow` | The store's arrival estimate as ISO 8601 times, and a retailer's quoted window in its own words. `arrivalText` writes either as one line, such as `Arriving 12:04 – 12:22 PM`. |
718
+ | `totalCents`, `currency` | The total, when the message names one. |
719
+ | `orderId`, `retailerOrderId`, `deliveredAt`, `reason`, `updatedAt` | The order's ids, when it was delivered, why it was canceled or failed, and when it reached this step. |
720
+
721
+ A value Agentcard did not send is `null`. Agentcard's order messages carry no items, delivery address, store logo or tracking link, so a tracker shows those only from what your own app kept.
722
+
676
723
  ## Mount the purchase message handler
677
724
 
678
725
  `purchaseAgentWebhook` is the handler from earlier versions. It hands you `purchase.message` events only, and leaves deduplication to you. New code takes `agentcardWebhook`.
package/dist/ack.d.ts CHANGED
@@ -1,19 +1,3 @@
1
- /**
2
- * The instant reply a shopper reads while Agentcard works on their request, such as `on it, looking
3
- * for usb-c cables under $20`. A search can take Agentcard most of a minute, and a shopper who hears
4
- * nothing in that time asks again. The words come from fixed templates, rotated so one shopper never
5
- * reads the same one twice in a row, with a short paraphrase of the request when one can be read
6
- * safely from it. No model writes them, so they cost nothing and arrive at once.
7
- *
8
- * The paraphrase never carries a link, an email address, a phone number, a street address or a long
9
- * number: the text stops before the first one, and a request that starts with one gets the template
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.
16
- */
17
1
  /** Words a reply opens with before it says what it means, such as `ok` in `ok lets checkout` or `yes,` in `yes, sounds good`. */
18
2
  export declare const FILLER: RegExp;
19
3
  /** A bare acknowledgement, such as `its ok`, `thats fine`, `sounds good` or `works for me`, after the filler comes off. */
@@ -142,5 +126,31 @@ export declare function instantReaction(shopperText: string, options?: AckForOpt
142
126
  * ```
143
127
  */
144
128
  export declare function keepsYes(shopperText: string): boolean;
129
+ /**
130
+ * True when the shopper's words hold the order back or change it, such as `cancel`, `no wait` or `actually make it two`.
131
+ * A go-ahead they gave before such a message covers nothing past it, whatever question the words also answer.
132
+ *
133
+ * ```ts
134
+ * holdsBack('cancel'); // true
135
+ * holdsBack('uk 8'); // false
136
+ * ```
137
+ */
138
+ export declare function holdsBack(shopperText: string): boolean;
139
+ /**
140
+ * True when the shopper's words answer a question the checkout asked, and nothing more: their address, phone number,
141
+ * name or email, or a yes to a question such as an address read back to them (`is it 1801 Wedemeyer St? say yes`). False
142
+ * for a message that holds back or changes the order (`no wait`, `actually make it two`), a stall (`let me think`), a
143
+ * question, a new request, and an answer that asks for something more beside it (`6282084916 and do you have a case`).
144
+ *
145
+ * ```ts
146
+ * answersCheckout('1801 wedemeyer', { asked: "what's the shipping address?" }); // true
147
+ * answersCheckout('ye', { asked: 'is it 1801 Wedemeyer St, San Francisco? say yes' }); // true
148
+ * answersCheckout('actually wait', { asked: "what's the best number?" }); // false
149
+ * ```
150
+ */
151
+ export declare function answersCheckout(shopperText: string, options?: {
152
+ nextAction?: string | null;
153
+ asked?: string | null;
154
+ }): boolean;
145
155
  /** The earlier name of `instantAck`, kept for apps that call it. */
146
156
  export declare function ackFor(text: string, options?: AckForOptions): string | null;
package/dist/ack.js CHANGED
@@ -14,6 +14,7 @@
14
14
  * instant reply at all, because it starts no search and Agentcard answers it fast. So does a pick of a
15
15
  * store the shopper saw, such as `order here`, or `Order from Tacos El Grullense` as a store card sends it.
16
16
  */
17
+ import { SENTENCE_END } from './sentences.js';
17
18
  /** Each template twice: with the paraphrase in place of `{x}`, and without one. */
18
19
  const TEMPLATES = [
19
20
  ['on it, looking for {x}', 'on it, looking for that now'],
@@ -248,7 +249,8 @@ export function isNewRequest(text) {
248
249
  }
249
250
  /** A short paraphrase of what the shopper asks for, safe to read back to them, or null when none is. */
250
251
  export function paraphrase(text) {
251
- let rest = plain(String(text ?? '')).split(/[.!?;\n](?:\s|$)/)[0] ?? '';
252
+ // Split before plain() lowercases, so an initial such as `J.` in `J. Crew` still reads as one.
253
+ let rest = plain((String(text ?? '').split(/;(?:\s|$)/)[0] ?? '').split(SENTENCE_END)[0] ?? '');
252
254
  for (const pattern of PRIVATE) {
253
255
  const at = rest.search(pattern);
254
256
  // A cut inside a word takes the rest of that word too, so no word is left half read.
@@ -330,7 +332,7 @@ const DETAILS = new RegExp([
330
332
  ].join('|'));
331
333
  /** 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`. */
332
334
  export function asksForDetails(message) {
333
- const sentences = plain(String(message ?? '')).split(/(?<=[.!?\n])\s+/);
335
+ const sentences = String(message ?? '').split(SENTENCE_END).map(plain);
334
336
  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)));
335
337
  }
336
338
  /** Words that open a new request rather than an answer, such as `find`, `can you get` or `what about`. */
@@ -416,6 +418,66 @@ export function keepsYes(shopperText) {
416
418
  // A report after filler ("well signed in", "k connected") is read on its bare words too (review of #9220).
417
419
  return ACKNOWLEDGED.test(bare) || CHECKOUT.test(bare) || STATUS.test(words) || STATUS.test(bare) || /^(?:done|added(?: it)?|i added(?: it| the card| a card)?)$/.test(bare);
418
420
  }
421
+ /** Words that hold the order back or change it: a message with one of them never answers a checkout question. */
422
+ const HOLDS_BACK = /\b(?:no|nope|nah|wait|hold on|hang on|cancel|stop|actually|never ?mind|nvm|instead|change|switch|swap|remove|different|another|rather|not|don'?t|dont|later|hmm+|um+|uh+|let me|lemme|i'll|i will|think|thinking|check|maybe|unsure|idk|dunno)\b/;
423
+ /**
424
+ * True when the shopper's words hold the order back or change it, such as `cancel`, `no wait` or `actually make it two`.
425
+ * A go-ahead they gave before such a message covers nothing past it, whatever question the words also answer.
426
+ *
427
+ * ```ts
428
+ * holdsBack('cancel'); // true
429
+ * holdsBack('uk 8'); // false
430
+ * ```
431
+ */
432
+ export function holdsBack(shopperText) {
433
+ return HOLDS_BACK.test(plain(String(shopperText ?? '')).replace(/[!?.,;:]+/g, ' ').replace(/\s+/g, ' ').trim());
434
+ }
435
+ /** A yes to a question the shopper was asked, such as an address read back to them: `ye`, `yes`, `correct`, `that's it`. */
436
+ const YES_TO_QUESTION = /^(?:y|ye|ya|yes+|yeah|yea|yep|yup|sure|correct|right|exactly|perfect|confirmed?|(?:that'?s|thats|it'?s|its) (?:it|right|correct|the one))$/;
437
+ /** The most words a name runs to when it only answers the question that asked for it. */
438
+ const MAX_NAME_WORDS = 4;
439
+ /**
440
+ * Words that ask for something more, wherever they sit: `6282084916 and do you have a case for it` gives the number and
441
+ * asks for another product, so it answers more than the checkout's question.
442
+ */
443
+ const ASKS_MORE = /\b(?:do you|did you|can you|could you|would you|will you|can i|could i|is there|are there|also|plus|what|how|why|which|find|search|show me|get me|add|buy|any other|more)\b/;
444
+ /**
445
+ * An email address alone, so a word the shopper punctuated it against is still read: `karen@example.com,cancel`
446
+ * leaves `cancel` behind, where a match on every non-space character took the cancellation with the address.
447
+ */
448
+ const EMAIL_ADDRESS = /[\p{L}\p{N}._%+'-]+@[\p{L}\p{N}-]+(?:\.[\p{L}\p{N}-]+)*\.[\p{L}]{2,}/gu;
449
+ /** Words a name never holds, which a stall or a remark does: `let me think`, `that is fine`. */
450
+ const NOT_A_NAME = /\b(?:i|me|my|you|your|it|its|is|are|was|be|the|a|an|to|of|for|and|or|but|so|this|that|know|sure|ok|okay|yes|yeah|good|fine|great|cool|thanks)\b/;
451
+ /**
452
+ * True when the shopper's words answer a question the checkout asked, and nothing more: their address, phone number,
453
+ * name or email, or a yes to a question such as an address read back to them (`is it 1801 Wedemeyer St? say yes`). False
454
+ * for a message that holds back or changes the order (`no wait`, `actually make it two`), a stall (`let me think`), a
455
+ * question, a new request, and an answer that asks for something more beside it (`6282084916 and do you have a case`).
456
+ *
457
+ * ```ts
458
+ * answersCheckout('1801 wedemeyer', { asked: "what's the shipping address?" }); // true
459
+ * answersCheckout('ye', { asked: 'is it 1801 Wedemeyer St, San Francisco? say yes' }); // true
460
+ * answersCheckout('actually wait', { asked: "what's the best number?" }); // false
461
+ * ```
462
+ */
463
+ export function answersCheckout(shopperText, options = {}) {
464
+ const line = plain(String(shopperText ?? ''));
465
+ const words = line.replace(/[!?.,;:]+/g, ' ').replace(/\b(?:please|pls|thanks|thank you|thx|ty)\b/g, ' ').replace(/\s+/g, ' ').trim();
466
+ // The whole message answers the question: a hesitation (`let me think`) or a request beside the answer keeps nothing.
467
+ // An email address is read for neither, so `karen+buy@example.com` stays an answer while the words around it are checked.
468
+ const around = line.replace(EMAIL_ADDRESS, ' ').replace(/[!?.,;:]+/g, ' ').replace(/\s+/g, ' ').trim();
469
+ if (!/[\p{L}\p{N}]/u.test(words) || /\?/.test(line) || HOLDS_BACK.test(around) || ASKS_MORE.test(around))
470
+ return false;
471
+ // The shopper's own details: a phone number, an email, a street address, a unit, or a place alone such as `1801 wedemeyer`.
472
+ if (hasPersonalNumber(line) || PERSONAL.some((pattern) => pattern.test(line)) || PLACE_ONLY.some((pattern) => pattern.test(words)))
473
+ return true;
474
+ const asked = String(options?.asked ?? '');
475
+ if (/\?/.test(asked) && YES_TO_QUESTION.test(words.replace(FILLER, '') || words))
476
+ return true;
477
+ // A name the checkout asked for: a few words of letters, none of them a word a name never holds.
478
+ const detailsAsked = (typeof options?.nextAction === 'string' && ANSWERED_STEPS.has(options.nextAction)) || asksForDetails(asked);
479
+ return detailsAsked && /^[\p{L}'.-]+(?: [\p{L}'.-]+){0,3}$/u.test(words) && words.split(' ').length <= MAX_NAME_WORDS && !NOT_A_NAME.test(words) && !REQUEST_LEAD.test(words) && !CART_COMMAND.test(words) && !ABOUT.test(words);
480
+ }
419
481
  /** The earlier name of `instantAck`, kept for apps that call it. */
420
482
  export function ackFor(text, options = {}) {
421
483
  return instantAck(text, options);
package/dist/browser.d.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { type ActionLinkKind, type ActionLinkMessage, type ActionLinkOutcome } from './action-links.js';
11
11
  export { ACTION_LINK_ENDINGS, ACTION_LINK_KINDS, actionLinkId, actionLinkOutcome, isActionLink, type ActionLinkKind, type ActionLinkMessage, type ActionLinkOutcome, } from './action-links.js';
12
+ export { ORDER_STEPS, updateOrder, arrivalText, type ShopperOrder, type OrderStep, type OrderPathStep, type OrderStepState, type ArrivalTextOptions, } from './order.js';
12
13
  /** The part of a window `window.open` returns that `actionWindows` uses. */
13
14
  export interface PopupLike {
14
15
  readonly closed: boolean;
package/dist/browser.js CHANGED
@@ -9,6 +9,8 @@
9
9
  */
10
10
  import { actionLinkId, actionLinkOutcome, isActionLink } from './action-links.js';
11
11
  export { ACTION_LINK_ENDINGS, ACTION_LINK_KINDS, actionLinkId, actionLinkOutcome, isActionLink, } from './action-links.js';
12
+ // An order tracker draws `message.order` in the browser: one per order, moved by `updateOrder`.
13
+ export { ORDER_STEPS, updateOrder, arrivalText, } from './order.js';
12
14
  const DEFAULT_WIDTH = 480;
13
15
  const DEFAULT_HEIGHT = 760;
14
16
  const DEFAULT_POLL_MS = 500;
@@ -31,7 +31,7 @@
31
31
  * messages and links as one sequence, in the order they happened, and spaces
32
32
  * the messages like someone texting.
33
33
  */
34
- import { AgentcardClient, type BuyCart, type BuyCartBuilding, type BuyFlightItinerary, type BuyDeliveryAddress, type BuyNextAction, type BuyPick, type BuyProgress, type BuyTurnStatus, type BuyAvailability, type BuyStorePick, type BuyTurn, type FetchLike } from './client.js';
34
+ import { AgentcardClient, type BuyCart, type BuyCartBuilding, type BuyChoice, type BuyFlightItinerary, type BuyDeliveryAddress, type BuyNextAction, type BuyPick, type BuyProgress, type BuyTurnStatus, type BuyAvailability, type BuyStorePick, type BuyTurn, type FetchLike } from './client.js';
35
35
  import { type BuyToolDefinition, type BuyToolInput, type BuyToolInputSchema, type BuyToolStandardSchema, type OpenAIBuyTool } from './definition.js';
36
36
  import { type DirectCall } from './direct.js';
37
37
  import { type BuyToolModelView } from './model-view.js';
@@ -221,6 +221,30 @@ export interface BuyToolResult {
221
221
  * message from Agentcard, and only the shopper's yes to that total confirms it.
222
222
  */
223
223
  cart_building?: BuyCartBuilding | null;
224
+ /**
225
+ * True when this call sent Agentcard a confirm: the shopper's yes, or the yes the tool carried past a step the shopper
226
+ * finished outside the chat. Absent when the call sent the shopper's words or read the conversation.
227
+ */
228
+ confirm_sent?: true;
229
+ /**
230
+ * Set when a `nextStep` turn went ahead on the yes the shopper already gave in the chat, once they finished the store
231
+ * sign-in or saved a card: the total they said yes to and the cart's total now, in the minor unit of `currency`. The two
232
+ * differ when the store priced its fees and tax in after the yes. The approval link, which names the most the shopper
233
+ * can be charged, is still their consent to the payment. `picked` is set when the go-ahead was the shopper's pick of
234
+ * what is in the cart rather than a yes: `said_yes_to_cents` is then the total the chat last showed them.
235
+ */
236
+ yes_carried?: {
237
+ said_yes_to_cents: number;
238
+ total_cents: number;
239
+ currency: string | null;
240
+ picked?: true;
241
+ };
242
+ /**
243
+ * The choice the reply asks the shopper to make, such as a sandwich's bread or a shirt's size: each group with every value,
244
+ * its option id and its extra price. The reply shown as written (`replyAsWritten`) and the messages `onText` hears list the
245
+ * values one per line under the question. Absent or null when the reply asks none.
246
+ */
247
+ choice?: BuyChoice | null;
224
248
  /** Agentcard's answer as it came: the purchase answer, or the body of the refusal. */
225
249
  envelope: Record<string, unknown>;
226
250
  }
@@ -388,11 +412,19 @@ export interface BuyToolOptions {
388
412
  earlyCards?: boolean;
389
413
  /**
390
414
  * `true` sets `turn.replyAsWritten` on every turn this tool opens, unless the turn says otherwise:
391
- * Agentcard's reply, when an answer only asks the shopper something (`replyAsWritten`). Show it as
415
+ * Agentcard's reply, when an answer only asks the shopper something (`replyAsWritten`), or the step's fixed
416
+ * short line, when it asks for a card, a store sign-in or the approval by a link it handed over. Show it as
392
417
  * the turn's answer and skip the model call that would reword it. Off by default, so your model
393
418
  * writes every answer.
394
419
  */
395
420
  replyAsWritten?: boolean;
421
+ /**
422
+ * `true` writes what the tool hands out as Agentcard wrote it, `turn.replyAsWritten` and each message `onText` hears,
423
+ * in a chat's texting style (`textingStyle`): lowercase at the start of each sentence and for "I", no markdown, no
424
+ * spaced dashes, and every list as `• ` bullet lines. Off by default: the words stay as Agentcard wrote them, with
425
+ * their lists as `• ` bullet lines all the same.
426
+ */
427
+ textingStyle?: boolean;
396
428
  /**
397
429
  * `true` reads each shopper message `buy.turn` opens by rule, with no model, and sets `turn.direct` to the call
398
430
  * it makes straight to Agentcard when the message is a request to buy, a pick of what Agentcard just showed, or
@@ -531,6 +563,8 @@ export interface BuyToolTurnOptions {
531
563
  * null, so your model rewords every answer. Default: the tool's `replyAsWritten`.
532
564
  */
533
565
  replyAsWritten?: boolean;
566
+ /** `true` writes `turn.replyAsWritten` and each message `onText` hears in the texting style (`textingStyle`). Default: the tool's `textingStyle`. */
567
+ textingStyle?: boolean;
534
568
  /** With `onItem`, `onMessage` or `onText`: hears `true` when the pause before a message starts and `false` the moment that message goes out, for a typing indicator. */
535
569
  onTyping?: (typing: boolean) => void;
536
570
  /**
@@ -618,16 +652,19 @@ export interface BuyToolTurn extends Omit<BuyTool, 'instantAck' | 'instantReacti
618
652
  /**
619
653
  * With `replyAsWritten` on: Agentcard's reply when the turn's last answer only asks the shopper
620
654
  * something, such as a product list, a size or a cart's total and the ask for their yes, with every
621
- * link taken out. Read it after the tool runs. When it is set, show it with `say()` and end the turn
622
- * without calling your model again: the reply already says what the model would. Null when the option
623
- * is off, before any call, after an answer your model has to word, and after a line the tool wrote
624
- * itself for your model, such as the note that the cart changed or a `nextStep` turn's ask for the yes.
655
+ * link taken out. When the answer's next step is a link the turn handed over (`add_card`, `approve` or
656
+ * `store_login`), it is that step's fixed short line instead (`stepAsWritten`), such as `approve the
657
+ * payment to place your order (up to $17.89)`, and the link follows it. Read it after the tool runs.
658
+ * When it is set, show it with `say()` and end the turn without calling your model again: the reply
659
+ * already says what the model would. Null when the option is off, before any call, after an answer your
660
+ * model has to word, and after a line the tool wrote itself for your model, such as the note that the
661
+ * cart changed or a `nextStep` turn's ask for the yes.
625
662
  */
626
663
  readonly replyAsWritten: string | null;
627
664
  /**
628
665
  * With `direct` on: the call this turn's message makes straight to Agentcard, with no model deciding it, and why,
629
- * or null when your model decides. It is set when the turn opens, from what Agentcard last asked the shopper.
630
- * Send it with `run(direct.input)`, which returns what your model would read, then show `replyAsWritten` when it
666
+ * or null when your model decides. It is set when the turn opens, from what Agentcard last asked the shopper, and on
667
+ * every turn `buy.nextStep` opens, with the reason `next_step`. Send it with `run(direct.input)`, which returns what your model would read, then show `replyAsWritten` when it
631
668
  * is set; when it is null, hand your model the call and its result, and its words answer the shopper.
632
669
  */
633
670
  readonly direct: DirectCall | null;