@agent-cards/commerce-sdk 0.19.1 → 0.20.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.
@@ -604,6 +604,15 @@ export interface BuyTool {
604
604
  * call's answer tells the model where the purchase stands. Post `message.text` then, as before.
605
605
  */
606
606
  nextStep(message: ShopperMessage, options?: BuyToolTurnOptions): BuyToolTurn | null;
607
+ /**
608
+ * The carts this tool last showed, with the cart an `order_placed` message is about marked placed: its `totalStatus`
609
+ * reads `placed` and its total is the total the order placed at. A store that sets its final amount later names only an
610
+ * estimate, and the cart keeps `totalIsEstimate` true at that estimate. The tool's `onCart` hears the carts before the
611
+ * promise settles, so await it in your webhook's `onMessage`. Pass every message `onMessage` got. Answers null for any
612
+ * other message, a message about another conversation or shopper, or one whose cart this tool cannot tell: the message
613
+ * names a store none of the carts is at, and more than one cart is on the table.
614
+ */
615
+ placedCarts(message: ShopperMessage): Promise<BuyCart[] | null>;
607
616
  }
608
617
  /**
609
618
  * One thing a turn hands the shopper: a message, such as the instant reply or a paragraph your model
package/dist/buy-tool.js CHANGED
@@ -769,7 +769,7 @@ function endTurn(state) {
769
769
  * The hash alone is not enough, since a store pricing a cart again changes `pricing` and its total under one hash.
770
770
  */
771
771
  function cartsKey(carts) {
772
- return JSON.stringify(carts.map((c) => [c.hash, c.totalCents, c.pricing === true, c.totalIsEstimate, c.items.map((i) => [i.line_id, i.qty, i.lineTotalCents])]));
772
+ return JSON.stringify(carts.map((c) => [c.hash, c.totalCents, c.pricing === true, c.totalIsEstimate, c.totalStatus, c.items.map((i) => [i.line_id, i.qty, i.lineTotalCents])]));
773
773
  }
774
774
  export function buyTool(options) {
775
775
  const o = options ?? {};
@@ -934,6 +934,24 @@ export function buyTool(options) {
934
934
  let callsStarted = 0;
935
935
  let cartsFromCall = 0;
936
936
  let lastCarts = [];
937
+ /**
938
+ * The carts an order placed marked (`placedCarts`), by hash, with their conversation: an answer in that conversation that
939
+ * still lists one, such as a call that began before the order placed, shows it placed. The first answer that no longer
940
+ * lists it forgets it, so the same items carted again later are a new purchase.
941
+ */
942
+ const placedByHash = new Map();
943
+ const placedIn = (hash, conversationId) => {
944
+ const kept = placedByHash.get(hash);
945
+ return kept && (!kept.conversationId || !conversationId || kept.conversationId === conversationId) ? kept.cart : undefined;
946
+ };
947
+ const withPlaced = (carts, conversationId) => carts.map((c) => placedIn(c.hash, conversationId) ?? c);
948
+ /** The conversation the carts that left the table were in. */
949
+ let leftCartsIn = null;
950
+ /** The newest call whose answer set the carts the tool keeps. */
951
+ let cartsKeptFromCall = 0;
952
+ const cartKey = (c) => `${c.merchant}|${c.store?.id ?? ''}`;
953
+ /** The carts an answer took off the table, newest first, at most five: an order placed before its message arrived leaves its cart here. */
954
+ let leftCarts = [];
937
955
  /** The cart the last answer showed: the cart a confirm sent now confirms. */
938
956
  let lastShownCart = null;
939
957
  /**
@@ -2144,10 +2162,11 @@ export function buyTool(options) {
2144
2162
  : {}),
2145
2163
  ...(t.onCart
2146
2164
  ? {
2147
- onCart: async ({ cart, carts, conversationId }) => {
2165
+ onCart: async ({ cart: sent, carts: sentCarts, conversationId }) => {
2148
2166
  // A newer call already handed on its carts: this older stream's are stale, however late they come.
2149
2167
  if (callNo < cartsFromCall)
2150
2168
  return;
2169
+ const [cart, carts] = [sent ? placedIn(sent.hash, conversationId) ?? sent : sent, withPlaced(sentCarts, conversationId)];
2151
2170
  t.cartsHeard = cartsKey(carts);
2152
2171
  cartsFromCall = callNo;
2153
2172
  try {
@@ -2329,7 +2348,26 @@ export function buyTool(options) {
2329
2348
  // A refusal reports no carts, so only a turn's answer says how many wait, and whether a checkout just ended with none.
2330
2349
  if (turn) {
2331
2350
  lastCartCount = result.carts.length;
2332
- lastCarts = result.carts;
2351
+ // A cart an order placed reads placed in every answer that still lists it.
2352
+ const answeredIn = result.conversation_id ?? lastConversationId;
2353
+ result.carts = withPlaced(result.carts, answeredIn);
2354
+ if (result.cart)
2355
+ result.cart = placedIn(result.cart.hash, answeredIn) ?? result.cart;
2356
+ // Only the newest call's answer moves what the tool keeps: an older call's answer is no word on the carts as they stand.
2357
+ if (callNo >= cartsKeptFromCall) {
2358
+ cartsKeptFromCall = callNo;
2359
+ for (const hash of [...placedByHash.keys()])
2360
+ if (!result.error_code && !result.carts.some((c) => c.hash === hash))
2361
+ placedByHash.delete(hash);
2362
+ // The carts that left the table, newest first, one per store. A cart the answer still holds at the same store, edited
2363
+ // or priced again, is the same purchase in a new version, so its older version never counts as one that left.
2364
+ const left = lastCarts.filter((c) => !result.carts.some((n) => n.hash === c.hash || cartKey(n) === cartKey(c)));
2365
+ // A new conversation is a new purchase: the carts that left the one before it are no longer its carts.
2366
+ const sameConversation = !answeredIn || !leftCartsIn || answeredIn === leftCartsIn;
2367
+ leftCarts = [...(sameConversation ? left : []), ...(sameConversation ? leftCarts : []).filter((c) => !left.some((n) => cartKey(n) === cartKey(c)))].slice(0, 5);
2368
+ leftCartsIn = answeredIn ?? leftCartsIn;
2369
+ lastCarts = result.carts;
2370
+ }
2333
2371
  // The answer's carts are the conversation's as they stand, an empty list included: onCart hears them when the stream
2334
2372
  // sent none or older ones, so an app never keeps showing a cart Agentcard no longer holds (a stream sends a cart only
2335
2373
  // when a tool changed it, and a call with stream: false sends none).
@@ -2391,8 +2429,8 @@ export function buyTool(options) {
2391
2429
  held.add(id);
2392
2430
  tappable.push(product);
2393
2431
  }
2394
- lastAnswerProducts = shown.map((product) => ({ productId: pickId(product), name: product.name }));
2395
- lastTappable = tappable.map((product) => ({ productId: pickId(product), name: product.name }));
2432
+ lastAnswerProducts = shown.map((product) => ({ productId: pickId(product), name: product.name, store: product.store }));
2433
+ lastTappable = tappable.map((product) => ({ productId: pickId(product), name: product.name, store: product.store }));
2396
2434
  // A new conversation starts the cards shown before afresh; each card keeps its newest showing, up to SHOWN_BEFORE_MAX.
2397
2435
  if (result.conversation_id && result.conversation_id !== shownIn)
2398
2436
  shownBefore = [];
@@ -2489,6 +2527,45 @@ export function buyTool(options) {
2489
2527
  client.answered(externalUserId, message, threadId);
2490
2528
  return openTurn(nextStepPrompt(message), options, message);
2491
2529
  };
2530
+ /** The carts with the one an order_placed message is about marked placed at its total (`BuyTool.placedCarts`). */
2531
+ const placedCarts = async (message) => {
2532
+ if (!message || typeof message !== 'object' || message.kind !== 'order_placed')
2533
+ return null;
2534
+ if (client.company && message.externalUserId !== externalUserId)
2535
+ return null;
2536
+ if (message.conversationId && lastConversationId && message.conversationId !== lastConversationId)
2537
+ return null;
2538
+ const store = message.facts?.merchantName?.trim().toLowerCase();
2539
+ const named = (pool) => (store ? pool.filter((c) => [c.merchant_name, c.store?.name, c.store?.id, c.merchant].some((n) => n?.trim().toLowerCase() === store)) : []);
2540
+ const [open, left] = [named(lastCarts), named(leftCarts)];
2541
+ // The one cart the message names, on the table or one an answer took off it once the order placed, else the only cart.
2542
+ // A name that fits more than one cart, an open one and one that left included, tells none apart: the order may be either.
2543
+ const cart = open.length + left.length > 1 ? null
2544
+ : open[0] ?? left[0] ?? (lastCarts.length === 1 ? lastCarts[0] : lastCarts.length === 0 && leftCarts.length === 1 ? leftCarts[0] : null);
2545
+ if (!cart)
2546
+ return null;
2547
+ const [total, estimate] = [message.facts?.totalCents, message.facts?.estimatedTotalCents];
2548
+ const final = typeof total === 'number' && total >= 0;
2549
+ const placed = {
2550
+ ...cart,
2551
+ totalStatus: 'placed',
2552
+ ...(final ? { totalCents: total, totalIsEstimate: false } : typeof estimate === 'number' && estimate >= 0 ? { totalCents: estimate, totalIsEstimate: true } : {}),
2553
+ };
2554
+ // A cart an answer took off the table is shown again, placed, beside the carts still open.
2555
+ const carts = lastCarts.includes(cart) ? lastCarts.map((c) => (c === cart ? placed : c)) : [...lastCarts, placed];
2556
+ leftCarts = leftCarts.filter((c) => c !== cart);
2557
+ lastCarts = carts;
2558
+ placedByHash.set(cart.hash, { cart: placed, conversationId: lastConversationId });
2559
+ if (o.onCart && lastConversationId) {
2560
+ try {
2561
+ await o.onCart(placed, { carts, conversationId: lastConversationId });
2562
+ }
2563
+ catch {
2564
+ // The app's display failing never fails the message.
2565
+ }
2566
+ }
2567
+ return carts;
2568
+ };
2492
2569
  const openTurn = (shopperText, options, event) => {
2493
2570
  const own = options ?? {};
2494
2571
  if (typeof own !== 'object')
@@ -2919,5 +2996,5 @@ export function buyTool(options) {
2919
2996
  say: { value: sayWhole, enumerable: false },
2920
2997
  });
2921
2998
  };
2922
- return toolOf(answer, { instantAck, instantReaction, turn, nextStep }, {});
2999
+ return toolOf(answer, { instantAck, instantReaction, turn, nextStep, placedCarts }, {});
2923
3000
  }
package/dist/client.d.ts CHANGED
@@ -472,6 +472,8 @@ export interface BuyCartHold {
472
472
  amountCents: number;
473
473
  reason: 'flight' | 'hotel' | 'checkout_estimate';
474
474
  }
475
+ /** Where a cart's total stands: an estimate, the store's confirmed total, or the total of the order placed. */
476
+ export type BuyCartTotalStatus = 'estimate' | 'confirmed' | 'placed';
475
477
  export interface BuyCart {
476
478
  merchant: string;
477
479
  merchant_name: string;
@@ -494,6 +496,13 @@ export interface BuyCart {
494
496
  * known fees only: show it as an estimate. DoorDash prices a cart once the shopper's own account holds it.
495
497
  */
496
498
  totalIsEstimate: boolean;
499
+ /**
500
+ * Where `totalCents` stands: `estimate` before the store priced the cart, `confirmed` once the store priced it, such
501
+ * as when the approval asks for the store's total, and `placed` once the order is placed (`buy.placedCarts`). A placed
502
+ * order whose store has not set its final amount yet keeps `totalIsEstimate` true. An older API sends none, and the
503
+ * cart reads `estimate` when `totalIsEstimate` is true, else `confirmed`.
504
+ */
505
+ totalStatus: BuyCartTotalStatus;
497
506
  /** The ISO 4217 code the store prices the cart in, or null when Agentcard sent none. */
498
507
  currency: string | null;
499
508
  approvedCeilingCents: number | null;
package/dist/client.js CHANGED
@@ -232,6 +232,11 @@ function feesOf(v) {
232
232
  && amount !== null && Number.isSafeInteger(amount) && amount >= 0 ? 'known' : 'unknown';
233
233
  return { serviceFeesCents: status === 'known' ? amount : null, serviceFeesStatus: status };
234
234
  }
235
+ function totalStatusOf(v) {
236
+ if (v.totalStatus === 'estimate' || v.totalStatus === 'confirmed' || v.totalStatus === 'placed')
237
+ return v.totalStatus;
238
+ return v.totalIsEstimate === true ? 'estimate' : 'confirmed';
239
+ }
235
240
  function cartOf(v) {
236
241
  if (!isRecord(v) || !str(v.hash) || !str(v.merchant))
237
242
  return null;
@@ -265,6 +270,7 @@ function cartOf(v) {
265
270
  tipCents: num(v.tipCents) ?? 0,
266
271
  totalCents: num(v.totalCents) ?? 0,
267
272
  totalIsEstimate: v.totalIsEstimate === true,
273
+ totalStatus: totalStatusOf(v),
268
274
  currency: str(v.currency),
269
275
  approvedCeilingCents: num(v.approvedCeilingCents),
270
276
  hold: holdOf(v.hold),
package/dist/direct.d.ts CHANGED
@@ -21,10 +21,11 @@ export interface DirectContext {
21
21
  nextAction?: BuyNextAction | null;
22
22
  /** Agentcard's last reply, the question a short answer answers. */
23
23
  asked?: string | null;
24
- /** The products Agentcard's last answer showed, in order, with the `product_id` a pick sends. A place (`the second one`) reads these. */
24
+ /** The products Agentcard's last answer showed, in order, with the `product_id` a pick sends and the store that sells each. A place (`the second one`) reads these. */
25
25
  products?: ReadonlyArray<{
26
26
  productId: string | null;
27
27
  name: string;
28
+ store?: string | null;
28
29
  }>;
29
30
  /**
30
31
  * Every product card the shopper can tap by its name, each once: the answer's and the ones its searches sent as they answered,
@@ -35,6 +36,7 @@ export interface DirectContext {
35
36
  tappable?: ReadonlyArray<{
36
37
  productId: string | null;
37
38
  name: string;
39
+ store?: string | null;
38
40
  }>;
39
41
  /**
40
42
  * Every product card an earlier answer of this conversation showed, each once, newest first. A card's tap whose name no card
package/dist/direct.js CHANGED
@@ -164,6 +164,33 @@ function tappedName(raw) {
164
164
  const name = TAP.exec(raw.trim())?.[1]?.trim();
165
165
  return name ? name : null;
166
166
  }
167
+ /**
168
+ * The words a pick by one word of a product's name says around that word, as `nameWords` spells them: `lets do the X ones`,
169
+ * `I'll take the X`, `go with X`, `X please`, `X, 2 of them`, `X is fine`. A yes, a no or any other word is not among them.
170
+ */
171
+ const PICK_FRAME = new Set('the a an one ones that this lets let s do go with i ll take get want give me is fine good works uh uhh um umm please pls plz of them those it x pack packs'.split(' '));
172
+ /** The stores a message names by brand, `home depot` and `barnes & noble` whole, as the words `nameWords` spells them. */
173
+ const STORE_NAMED = new RegExp(`(?<![\\p{L}\\p{N}])(?:${STORES})(?![\\p{L}\\p{N}])`, 'gu');
174
+ function storesNamed(raw) {
175
+ return [...String(raw ?? '').toLowerCase().matchAll(STORE_NAMED)].map((match) => nameWords(match[0]));
176
+ }
177
+ /**
178
+ * True when a message picks this product by a word only its name holds, such as its brand. Every other word of the message is the
179
+ * product's own or a pick frame's (`PICK_FRAME`), and the word is no part of a store's name the message holds (`home depot`), or of
180
+ * the product's own store, unless the product's name opens with that store's name, as Amazon Basics does. On 2026-11-05
181
+ * (conv_64712285dd09f5bbab6fc737) the agent showed eight books and offered to look at eBay, the shopper answered "yes ebay
182
+ * specifically", and "ebay", held by "The Expert Guide To Cashing In On eBay" alone, went as a tap on that card.
183
+ */
184
+ function marksProduct(raw, said, product, cards) {
185
+ const own = nameWords(product.name).split(' ');
186
+ const title = ` ${own.join(' ')} `;
187
+ // Each store the message names, and the product's own store, unless the product's name opens with it (a store's own brand).
188
+ const stores = [...storesNamed(raw), nameWords(product.store ?? '')].filter((store) => store !== '' && !title.startsWith(` ${store} `));
189
+ const isStore = (word) => stores.some((store) => store.split(' ').includes(word));
190
+ const words = said.split(' ');
191
+ const marks = words.filter((word) => word.length >= 4 && !/^\d/.test(word) && own.includes(word) && !isStore(word) && cards.filter((card) => nameWords(card.name).split(' ').includes(word)).length === 1);
192
+ return marks.length > 0 && words.every((word) => own.includes(word) || PICK_FRAME.has(word) || /^\d+$/.test(word));
193
+ }
167
194
  /** The pick a message makes of what Agentcard just showed, null when it picks nothing, or false when it counts past the list. */
168
195
  function pickOf(raw, bare, context) {
169
196
  const products = context.products ?? [];
@@ -213,9 +240,7 @@ function pickOf(raw, bare, context) {
213
240
  if (named.length > 1)
214
241
  return call({});
215
242
  // A word only one product's name holds, such as its brand: `lets do the coppertop ones`, `duracell 24 count is fine`.
216
- const words = new Set(said.split(' '));
217
- const owners = (word) => byName.filter((product) => nameWords(product.name).split(' ').includes(word));
218
- const marked = byName.filter((product) => nameWords(product.name).split(' ').some((word) => word.length >= 4 && !/^\d/.test(word) && words.has(word) && owners(word).length === 1));
243
+ const marked = byName.filter((product) => marksProduct(raw, said, product, byName));
219
244
  if (marked.length === 1 && said.split(' ').length <= 10 && !QUESTION_START.test(bare) && !DOUBT.test(said))
220
245
  return call({ product_id: marked[0].productId });
221
246
  const places = placesOf(bare, products.length);
package/dist/index.d.ts CHANGED
@@ -6,13 +6,13 @@ export { ORDER_STEPS, updateOrder, arrivalText, type ShopperOrder, type OrderSte
6
6
  export { agentcardWebhook, purchaseAgentWebhook, type AgentcardWebhookOptions, type AgentcardWebhookHandler, type PurchaseAgentWebhookOptions, type WebhookFetchRequest, type WebhookRequest, type WebhookResponse, } from './handler.js';
7
7
  export { searchAt, isMerchantStatusAt, merchantStatusLine, searchStatusLines, type BuySearchStatus, type BuyMerchantStatus, type BuyMerchantSearchStatus, type BuyMerchantStatusUpdate, } from './search-status.js';
8
8
  export { groupsOf, type BuyGroup, type BuyGroupKind, type BuyGroupStatus, type BuyGroupWithProducts, type BuyGroupsSource, type BuyGroupsUpdate, } from './search-groups.js';
9
- 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 BuyStoreCheckUpdate, type BuyFoundUpdate, type BuyTurnStatus, STATUS_WORDING, type BuySegmentUpdate, type BuyRecheck, type BuyFreshness, type BuyFreshnessState, type BuyCartUpdate, type BuyDeltaUpdate, type BuyStreamHandlers, type Shopper, type BuyLastAnswer, type BuyRequest, type BuyDeliveryAddress, type BuyTurn, type BuyStatus, type BuyCart, type BuyCartBuilding, type BuyCartLine, type BuyCartFeeLine, type BuyCartDiscount, type BuyCartHold, type BuyStore, type BuyCatalog, type BuyCatalogItem, type BuyFlightItinerary, type BuyFlightParty, type BuyPaymentSource, type BuyConversation, type BuyLatestReply, type BuyLastCheckout, type BuyOrder, type VaultLink, type BuyNextAction, type BuyPick, type BuyStoreChoice, type BuyStorePick, type BuyStores, type BuyChoice, type BuyCodeRequest, type BuyChoiceGroup, type BuyChoiceDelivery, type BuyChoiceOption, } from './client.js';
9
+ 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 BuyStoreCheckUpdate, type BuyFoundUpdate, type BuyTurnStatus, STATUS_WORDING, type BuySegmentUpdate, type BuyRecheck, type BuyFreshness, type BuyFreshnessState, type BuyCartUpdate, type BuyDeltaUpdate, type BuyStreamHandlers, type Shopper, type BuyLastAnswer, type BuyRequest, type BuyDeliveryAddress, type BuyTurn, type BuyStatus, type BuyCart, type BuyCartBuilding, type BuyCartLine, type BuyCartFeeLine, type BuyCartDiscount, type BuyCartHold, type BuyCartTotalStatus, type BuyStore, type BuyCatalog, type BuyCatalogItem, type BuyFlightItinerary, type BuyFlightParty, type BuyPaymentSource, type BuyConversation, type BuyLatestReply, type BuyLastCheckout, type BuyOrder, type VaultLink, type BuyNextAction, type BuyPick, type BuyStoreChoice, type BuyStorePick, type BuyStores, type BuyChoice, type BuyCodeRequest, type BuyChoiceGroup, type BuyChoiceDelivery, type BuyChoiceOption, } from './client.js';
10
10
  export { BUY_TOOL_NAME, BUY_TOOL_DESCRIPTION, buyToolDefinition, parseBuyToolInput, type BuyToolDefinition, type BuyToolInput, type BuyToolInputSchema, type BuyToolInputCheck, type BuyToolStandardSchema, type OpenAIBuyTool, } from './definition.js';
11
11
  export { toMessages, messageStream, typingDelay, type MessagePace, type MessageStream, type MessageStreamOptions, } from './messages.js';
12
12
  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';
13
13
  export { instantAck, instantReaction, ackFor, keepsYes, ACK_REACTION, type AckForOptions } from './ack.js';
14
14
  export { directCall, isProductAsk, type DirectCall, type DirectContext, type DirectReason } from './direct.js';
15
- export { forModel, grandTotals, forModelError, stepAsWritten, withoutCardList, stripLinks, flightPartyWords, type BuyToolModelView, type BuyToolModelCart, type BuyToolGrandTotal, type BuyToolModelProduct, type BuyToolModelStore, type ShownCard, } from './model-view.js';
15
+ export { forModel, cartsForModel, grandTotals, forModelError, stepAsWritten, withoutCardList, stripLinks, flightPartyWords, type BuyToolModelView, type BuyToolModelCart, type BuyToolGrandTotal, type BuyToolModelProduct, type BuyToolModelStore, type ShownCard, } from './model-view.js';
16
16
  export { asBullets, choiceLines, replyAsWritten, textingStyle, withChoice, type ReplyAsWrittenOptions } from './texting.js';
17
17
  export { appInstructions, APP_INSTRUCTIONS_VERSION, type AppInstructionsOptions } from './app-instructions.js';
18
18
  export { toAnthropicTool, toOpenRouterTool, type AnthropicBuyTool, type OpenRouterToolkit, type ZodLike, } from './adapters.js';
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ export { toMessages, messageStream, typingDelay, } from './messages.js';
12
12
  export { buyTool, nextStepPrompt, productAction, } from './buy-tool.js';
13
13
  export { instantAck, instantReaction, ackFor, keepsYes, ACK_REACTION } from './ack.js';
14
14
  export { directCall, isProductAsk } from './direct.js';
15
- export { forModel, grandTotals, forModelError, stepAsWritten, withoutCardList, stripLinks, flightPartyWords, } from './model-view.js';
15
+ export { forModel, cartsForModel, grandTotals, forModelError, stepAsWritten, withoutCardList, stripLinks, flightPartyWords, } from './model-view.js';
16
16
  export { asBullets, choiceLines, replyAsWritten, textingStyle, withChoice } from './texting.js';
17
17
  export { appInstructions, APP_INSTRUCTIONS_VERSION } from './app-instructions.js';
18
18
  export { toAnthropicTool, toOpenRouterTool, } from './adapters.js';
@@ -51,6 +51,11 @@ export interface BuyToolModelCart {
51
51
  total: string;
52
52
  /** True when the store has not priced its fees and tax yet: `total` is an estimate, never the final amount. */
53
53
  total_is_estimate: boolean;
54
+ /**
55
+ * Where `total` stands: `estimate` before the store priced the cart, `confirmed` once the store priced it, and `placed`
56
+ * once the order is placed. `total` is the final amount unless `total_is_estimate` is true.
57
+ */
58
+ total_status: 'estimate' | 'confirmed' | 'placed';
54
59
  /** What an estimated `total` leaves out, in words the model can repeat; null when the store priced the cart. */
55
60
  total_leaves_out: string | null;
56
61
  /** The most the shopper's card can be held for this cart: above `total` at a store that prices its final at checkout, `total` itself for a flight, which holds exactly that; else null. */
@@ -266,6 +271,8 @@ export declare function expiresIn(iso: string | null | undefined, now?: number):
266
271
  * whether the shopper has each link. A status this version does not know
267
272
  * reads as `needs_input`.
268
273
  */
274
+ /** The carts and their grand totals as the model reads them, for carts a turn's answer did not bring, such as `buy.placedCarts`'. */
275
+ export declare function cartsForModel(carts: readonly BuyCart[]): Pick<BuyToolModelView, 'carts' | 'grand_totals'>;
269
276
  export declare function forModel(result: BuyToolResult): BuyToolModelView;
270
277
  /**
271
278
  * The fixed short line for an answer whose next step is a link the shopper already holds: `add your card to place the
@@ -230,6 +230,7 @@ function cartView(cart, building) {
230
230
  tip: cart.tipCents > 0 ? money(cart.tipCents, cart.currency) : null,
231
231
  total: money(cart.totalCents, cart.currency) ?? money(0, cart.currency),
232
232
  total_is_estimate: cart.totalIsEstimate === true,
233
+ total_status: cart.totalStatus,
233
234
  // 2026-10-21, swarm-bigbox-a10: an estimate read "about $18.41 total with tax and shipping" to the shopper. The words
234
235
  // ride beside the flag, so the model never has to guess which side of the total tax and shipping sit on.
235
236
  total_leaves_out: cart.totalIsEstimate === true ? leftOut(cart.merchantFeeLines ?? []) : null,
@@ -303,6 +304,10 @@ function linkExpiry(result) {
303
304
  * whether the shopper has each link. A status this version does not know
304
305
  * reads as `needs_input`.
305
306
  */
307
+ /** The carts and their grand totals as the model reads them, for carts a turn's answer did not bring, such as `buy.placedCarts`'. */
308
+ export function cartsForModel(carts) {
309
+ return { carts: carts.map((cart) => cartView(cart)), grand_totals: grandTotals(carts) };
310
+ }
306
311
  export function forModel(result) {
307
312
  const status = typeof result.status === 'string' && MODEL_STATUSES.has(result.status) ? result.status : 'needs_input';
308
313
  const products = shownProducts(result.products);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-cards/commerce-sdk",
3
- "version": "0.19.1",
3
+ "version": "0.20.0",
4
4
  "description": "Give your agent one tool that buys for your shoppers at real stores, and post each purchase update Agentcard sends into the shopper's chat, over a connection your server opens or a webhook endpoint.",
5
5
  "homepage": "https://opencommerce.sh",
6
6
  "type": "module",