@agent-cards/commerce-sdk 0.18.8 → 0.18.10

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.
@@ -1,5 +1,5 @@
1
1
  /** The version of the voice package the text was copied from. */
2
- export declare const VOICE_VERSION = "0.2.81";
2
+ export declare const VOICE_VERSION = "0.2.83";
3
3
  export declare const APP_TEXT: {
4
4
  readonly core: "You help a shopper buy what they ask for, with the `buy` tool from Agentcard.\n\nWhen the shopper asks what you can do, answer from these lines without calling the tool:\n- You buy from Amazon, Walmart, Target, Best Buy and thousands more stores. Say \"and thousands more stores\" every time you name the stores you shop, in a reply about one kind of thing too, such as \"travel gear from Amazon, Target and thousands more stores\". Never let a few names read as the whole list, never say \"most online stores\", \"plenty more\" or \"many more\", and never give a number of stores.\n- You plan a whole trip: the flights, a hotel and a rental car together. When they ask what you can do for travel, lead with the whole trip, then name the parts they can book on their own.\n- You order food delivery and book restaurant tables.\n\nHow you use the buy tool:\n- Call it for anything the shopper wants to find, buy, book or order, a trip with no dates or airport yet included: the tool asks for what it still needs.\n- Never say whether you can buy at a store the shopper names, either way. Only the tool knows, so say you will look and call it.\n- Never write a link. Each link reaches the shopper on its own, so when `link_sent` is true, tell them a link is waiting for them.\n- Tell the shopper no reply is needed only when `shopper_need_not_reply` is true.\n- Ask for one thing per message, the step `next_action` names, and leave every later step for a later message.\n- Never tell the shopper you can't show Markdown, a table or other formatting, and never say you keep to plain text.";
5
5
  readonly markdown: "The app shows your words as Markdown. When the shopper asks for a table, a list or bold, write it in Markdown. Use a list or a table where it reads better than a sentence, and keep each message short all the same.";
@@ -1,6 +1,6 @@
1
1
  // GENERATED by scripts/app-instructions.mjs from apps/agent-voice/kb/app.md. Do not edit: edit kb/app.md.
2
2
  /** The version of the voice package the text was copied from. */
3
- export const VOICE_VERSION = "0.2.81";
3
+ export const VOICE_VERSION = "0.2.83";
4
4
  export const APP_TEXT = {
5
5
  "core": "You help a shopper buy what they ask for, with the `buy` tool from Agentcard.\n\nWhen the shopper asks what you can do, answer from these lines without calling the tool:\n- You buy from Amazon, Walmart, Target, Best Buy and thousands more stores. Say \"and thousands more stores\" every time you name the stores you shop, in a reply about one kind of thing too, such as \"travel gear from Amazon, Target and thousands more stores\". Never let a few names read as the whole list, never say \"most online stores\", \"plenty more\" or \"many more\", and never give a number of stores.\n- You plan a whole trip: the flights, a hotel and a rental car together. When they ask what you can do for travel, lead with the whole trip, then name the parts they can book on their own.\n- You order food delivery and book restaurant tables.\n\nHow you use the buy tool:\n- Call it for anything the shopper wants to find, buy, book or order, a trip with no dates or airport yet included: the tool asks for what it still needs.\n- Never say whether you can buy at a store the shopper names, either way. Only the tool knows, so say you will look and call it.\n- Never write a link. Each link reaches the shopper on its own, so when `link_sent` is true, tell them a link is waiting for them.\n- Tell the shopper no reply is needed only when `shopper_need_not_reply` is true.\n- Ask for one thing per message, the step `next_action` names, and leave every later step for a later message.\n- Never tell the shopper you can't show Markdown, a table or other formatting, and never say you keep to plain text.",
6
6
  "markdown": "The app shows your words as Markdown. When the shopper asks for a table, a list or bold, write it in Markdown. Use a list or a table where it reads better than a sentence, and keep each message short all the same.",
@@ -31,8 +31,10 @@
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 type { BuyCardSize } from './card-size.js';
34
35
  import type { BuyPlan } from './plan.js';
35
36
  import type { BuyMerchantStatusUpdate, BuySearchStatus } from './search-status.js';
37
+ import type { BuyFreshness, BuyRecheck, BuyStoreCheckUpdate } from './client.js';
36
38
  import type { BuyFulfilledBy } from './fulfilled-by.js';
37
39
  import { AgentcardClient, type BuyCart, type BuyCartBuilding, type BuyChoice, type BuyCodeRequest, type BuyFlightItinerary, type BuyDeliveryAddress, type BuyNextAction, type BuyPick, type BuyProgress, type BuyTurnStatus, type BuyAvailability, type BuyStorePick, type BuyTurn, type FetchLike } from './client.js';
38
40
  import { type BuyToolDefinition, type BuyToolInput, type BuyToolInputSchema, type BuyToolStandardSchema, type OpenAIBuyTool } from './definition.js';
@@ -97,6 +99,8 @@ export interface BuyToolLink {
97
99
  * `{ name: 'StubHub', kind: 'marketplace', line: 'via StubHub' }`. Show `line` quietly under the price. Absent when none.
98
100
  */
99
101
  fulfilledBy?: BuyFulfilledBy;
102
+ /** On a product link: the shopper's size as the store's read found it. Show `line` under the price when it is not null. */
103
+ shopperSize?: BuyCardSize;
100
104
  /**
101
105
  * On a product link: what taking the product does, so a button can say it. `reserve` is a table the
102
106
  * shopper reserves, `book` a flight, room, class or ticket they book, and `buy` anything they buy.
@@ -134,6 +138,10 @@ export interface BuyToolLink {
134
138
  * Absent on every other card, such as a retail product's.
135
139
  */
136
140
  availability?: BuyAvailability;
141
+ /** On a product link: whether the product ships to the shopper's country, as on `BuyToolProduct.shipsToYou`. */
142
+ shipsToYou?: 'yes' | 'no';
143
+ /** On a product link: the note for a product that ranks down for where it ships, as on `BuyToolProduct.shipsNote`. */
144
+ shipsNote?: string;
137
145
  }
138
146
  /** A store the answer's reply presents to choose from. */
139
147
  export interface BuyToolStore {
@@ -184,8 +192,14 @@ export interface BuyToolProduct {
184
192
  linkSent: boolean;
185
193
  /** For a place to book: whether it has room at the asked time. Absent on a product with no such question. */
186
194
  availability?: BuyAvailability;
195
+ /** Whether the product ships to the shopper's country (`BuyCatalogItem.ships_to_you`): `yes` or `no`, and absent when Agentcard cannot say or an older API sends none. */
196
+ shipsToYou?: 'yes' | 'no';
197
+ /** A short note for a product that ranks down for where it ships ("ships from the UK", "price in £"). Absent when there is none. */
198
+ shipsNote?: string;
187
199
  /** Who sells or books it, where that is not the store it names (`via StubHub`); absent when Agentcard named none. */
188
200
  fulfilledBy?: BuyFulfilledBy;
201
+ /** The shopper's size as the store's read found it (`men's 10.5 in stock`); absent when none was asked or read. */
202
+ shopperSize?: BuyCardSize;
189
203
  }
190
204
  /** One answer of the buy tool, whole, for your code. Hand it to `forModel` before a model reads it. */
191
205
  export interface BuyToolResult {
@@ -407,6 +421,18 @@ export interface BuyToolOptions extends BuyStoreWait {
407
421
  * search starts, then what it found. One that throws costs nothing but the call to it.
408
422
  */
409
423
  onMerchantStatus?: (update: BuyMerchantStatusUpdate) => void | Promise<void>;
424
+ /**
425
+ * Hears each store behind a card while Agentcard checks its price and stock, then the store's answer
426
+ * (`BuyStoreCheckUpdate`): `cardId` is the card's `productId`. One that throws costs nothing but the call to it.
427
+ */
428
+ onStoreCheck?: (update: BuyStoreCheckUpdate) => void | Promise<void>;
429
+ /**
430
+ * Hears Agentcard read the shopper's quotes again (`BuyRecheck`): at the start, the line to show and each item checking;
431
+ * at the end, the answer and each item's result. One that throws costs nothing but the call to it.
432
+ */
433
+ onRecheck?: (update: BuyRecheck) => void | Promise<void>;
434
+ /** Hears each quote's answer as its read lands (`BuyFreshness`). One that throws costs nothing but the call to it. */
435
+ onFreshness?: (update: BuyFreshness) => void | Promise<void>;
410
436
  /**
411
437
  * Hears the products a call's search found, the moment the search answers and before Agentcard writes its reply,
412
438
  * with the conversation, when the search ran (`asOf`, as the answer's `catalog.as_of` names it) and the store's name.
@@ -636,6 +662,12 @@ export interface BuyToolTurnOptions extends BuyStoreWait {
636
662
  onAvailability?: BuyToolOptions['onAvailability'];
637
663
  /** Hears each store this turn's searches ask, as the tool's `onMerchantStatus` does. */
638
664
  onMerchantStatus?: BuyToolOptions['onMerchantStatus'];
665
+ /** Hears each store behind this turn's cards as it is checked, as the tool's `onStoreCheck` does. */
666
+ onStoreCheck?: BuyToolOptions['onStoreCheck'];
667
+ /** Hears this turn's re-check of the shopper's quotes, as the tool's `onRecheck` does. */
668
+ onRecheck?: BuyToolOptions['onRecheck'];
669
+ /** Hears each quote's answer in this turn's re-check, as the tool's `onFreshness` does. */
670
+ onFreshness?: BuyToolOptions['onFreshness'];
639
671
  /** Hears the products each search of this turn's calls found, the moment it answers, as the tool's `onProducts` does. */
640
672
  onProducts?: BuyToolOptions['onProducts'];
641
673
  /**
package/dist/buy-tool.js CHANGED
@@ -223,12 +223,16 @@ export function nextStepPrompt(message) {
223
223
  /**
224
224
  * The pick as a later read of the conversation shows it: a pick the early answer found missing is in
225
225
  * the cart once a line carries its catalog id, as its product id or, for a retail product whose id is
226
- * its store page, as its page. A pick already in the cart, or none, stays as it was.
226
+ * its store page, as its page, or names its card by `ref`. A catalog card carted at a store page has the
227
+ * page as its line's id: on 2026-11-01 (conv_611973099c62bc0cdb2c041b) a pan set carted at kohls.com read
228
+ * as missing, so the model told the shopper the card it had just added was gone. A pick already in the
229
+ * cart, or none, stays as it was.
227
230
  */
228
231
  function pickAfter(pick, carts) {
229
232
  if (!pick || pick.in_cart)
230
233
  return pick;
231
- const held = carts.some((c) => c.items.some((line) => line.product_id === pick.product_id || line.url === pick.product_id));
234
+ const names = (ref) => !!ref && (ref === pick.product_id || ref === pick.ref);
235
+ const held = carts.some((c) => c.items.some((line) => line.product_id === pick.product_id || line.url === pick.product_id || names(line.ref)));
232
236
  return held ? { ...pick, in_cart: true } : pick;
233
237
  }
234
238
  function hashesOf(cart, carts) {
@@ -242,7 +246,7 @@ export function productsOf(turn) {
242
246
  /** A product's card as `sendLink` hears it: the id the model reads and a pick takes, never the store page, which is `url`. A flight option has no page at a store: its card carries no url, and its productId picks it. */
243
247
  function productLink(product) {
244
248
  const productId = pickId(product);
245
- return { kind: 'product', url: product.url || null, title: product.name, price: productPrice(product), ...(productId ? { productId } : {}), action: product.action ?? 'buy', imageUrl: product.imageUrl, images: product.images ?? (product.imageUrl ? [product.imageUrl] : []), store: product.store ?? null, storeDomain: product.storeDomain ?? null, expiresAt: null, ...(product.availability ? { availability: product.availability } : {}), ...(product.fulfilledBy ? { fulfilledBy: product.fulfilledBy } : {}) };
249
+ return { kind: 'product', url: product.url || null, title: product.name, price: productPrice(product), ...(productId ? { productId } : {}), action: product.action ?? 'buy', imageUrl: product.imageUrl, images: product.images ?? (product.imageUrl ? [product.imageUrl] : []), store: product.store ?? null, storeDomain: product.storeDomain ?? null, expiresAt: null, ...(product.availability ? { availability: product.availability } : {}), ...(product.fulfilledBy ? { fulfilledBy: product.fulfilledBy } : {}), ...(product.shipsToYou ? { shipsToYou: product.shipsToYou } : {}), ...(product.shipsNote ? { shipsNote: product.shipsNote } : {}), ...(product.shopperSize ? { shopperSize: product.shopperSize } : {}) };
246
250
  }
247
251
  /** A host name: dot-separated labels of letters, digits and hyphens, ending in a letters-only top-level label. */
248
252
  const STORE_HOST = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
@@ -278,7 +282,10 @@ function productsOfCatalog(catalog) {
278
282
  action: productAction(catalog.merchant),
279
283
  linkSent: false,
280
284
  ...(item.availability ? { availability: item.availability } : {}),
285
+ ...(item.ships_to_you === 'yes' || item.ships_to_you === 'no' ? { shipsToYou: item.ships_to_you } : {}),
286
+ ...(item.ships_note ? { shipsNote: item.ships_note } : {}),
281
287
  ...(item.fulfilled_by ? { fulfilledBy: item.fulfilled_by } : {}),
288
+ ...(item.shopper_size ? { shopperSize: item.shopper_size } : {}),
282
289
  }));
283
290
  }
284
291
  /** The stores an answer presents, as the tool reads them. */
@@ -770,7 +777,7 @@ export function buyTool(options) {
770
777
  if (o.stream !== undefined && o.stream !== false)
771
778
  throw new TypeError('Pass stream as false to wait for each whole answer, or leave it out.');
772
779
  const onProgress = o.onProgress;
773
- for (const key of ['onStatus', 'onFound', 'onAvailability', 'onMerchantStatus']) {
780
+ for (const key of ['onStatus', 'onFound', 'onAvailability', 'onMerchantStatus', 'onStoreCheck', 'onRecheck', 'onFreshness']) {
774
781
  if (o[key] !== undefined && typeof o[key] !== 'function')
775
782
  throw new TypeError(`Pass ${key} as a function, or leave it out.`);
776
783
  }
@@ -852,6 +859,9 @@ export function buyTool(options) {
852
859
  onFound: o.onFound,
853
860
  onAvailability: o.onAvailability,
854
861
  onMerchantStatus: o.onMerchantStatus,
862
+ onStoreCheck: o.onStoreCheck,
863
+ onRecheck: o.onRecheck,
864
+ onFreshness: o.onFreshness,
855
865
  onProducts: o.onProducts,
856
866
  onText: o.onText,
857
867
  texting: textingByDefault,
@@ -1920,9 +1930,16 @@ export function buyTool(options) {
1920
1930
  },
1921
1931
  }
1922
1932
  : {}),
1923
- ...(early || t.onProducts
1933
+ ...(early || t.onProducts || t.onStoreCheck
1924
1934
  ? {
1925
1935
  onCatalog: async (update) => {
1936
+ // Each card's productId is noted as its catalog arrives, whether or not cards go out early, so a store
1937
+ // check that follows names the card the app drew.
1938
+ for (const product of [...productsOfCatalog(update.catalog), ...productsOfCatalog({ ...update.catalog, items: update.more })]) {
1939
+ const productId = pickId(product);
1940
+ if (productId)
1941
+ cardOfId.set(product.id, productId);
1942
+ }
1926
1943
  try {
1927
1944
  await t.onProducts?.(productsOfCatalog(update.catalog), { conversationId: update.conversationId, asOf: update.asOf, merchantName: update.catalog.merchant_name });
1928
1945
  }
@@ -1933,8 +1950,9 @@ export function buyTool(options) {
1933
1950
  const keepMore = () => { if (t.onFound && early)
1934
1951
  moreFound = { conversationId: update.conversationId, links: productsOfCatalog({ ...update.catalog, items: update.more }).map(productLink) }; };
1935
1952
  if (!early) {
1936
- // Without early cards nothing goes out here, but a store that answered after the reply still joins the result.
1937
- if (replyWritten)
1953
+ // Without early cards nothing goes out here, but a store that answered after the reply still joins the
1954
+ // result for an app that draws products; one that asked only for store checks reads the answer as before.
1955
+ if (replyWritten && t.onProducts)
1938
1956
  lateCards.push(...productsOfCatalog(update.catalog));
1939
1957
  return;
1940
1958
  }
@@ -1977,6 +1995,50 @@ export function buyTool(options) {
1977
1995
  },
1978
1996
  }
1979
1997
  : {}),
1998
+ ...(t.onStoreCheck
1999
+ ? {
2000
+ onStoreCheck: async (update) => {
2001
+ try {
2002
+ await t.onStoreCheck({ ...update, cardId: cardOfId.get(update.cardId) ?? update.cardId });
2003
+ }
2004
+ catch {
2005
+ // The app's display failing never fails the turn.
2006
+ }
2007
+ },
2008
+ }
2009
+ : {}),
2010
+ ...(t.onRecheck
2011
+ ? {
2012
+ onRecheck: async (update) => {
2013
+ try {
2014
+ await t.onRecheck(update);
2015
+ }
2016
+ catch {
2017
+ // The app's display failing never fails the turn, and the wait line stays, so the shopper still hears one.
2018
+ return;
2019
+ }
2020
+ // The re-check's line, once the app showed it, is the turn's acknowledgement: no wait line follows it,
2021
+ // however long the reads run.
2022
+ if (update.phase === 'started' && update.text) {
2023
+ ack?.cancel();
2024
+ t.dropWaitLine?.();
2025
+ t.acked = true;
2026
+ }
2027
+ },
2028
+ }
2029
+ : {}),
2030
+ ...(t.onFreshness
2031
+ ? {
2032
+ onFreshness: async (update) => {
2033
+ try {
2034
+ await t.onFreshness(update);
2035
+ }
2036
+ catch {
2037
+ // The app's display failing never fails the turn.
2038
+ }
2039
+ },
2040
+ }
2041
+ : {}),
1980
2042
  ...(t.onText || writtenText
1981
2043
  ? {
1982
2044
  onSegment: async ({ text, conversationId }) => {
@@ -2331,7 +2393,7 @@ export function buyTool(options) {
2331
2393
  const own = options ?? {};
2332
2394
  if (typeof own !== 'object')
2333
2395
  throw new TypeError('Pass the turn options as an object, or leave them out.');
2334
- for (const key of ['sendLink', 'onWorking', 'onReaction', 'onResult', 'onProgress', 'onStatus', 'onFound', 'onAvailability', 'onMerchantStatus', 'onItem', 'onMessage', 'onProducts', 'onText', 'onCart']) {
2396
+ for (const key of ['sendLink', 'onWorking', 'onReaction', 'onResult', 'onProgress', 'onStatus', 'onFound', 'onAvailability', 'onMerchantStatus', 'onStoreCheck', 'onRecheck', 'onFreshness', 'onItem', 'onMessage', 'onProducts', 'onText', 'onCart']) {
2335
2397
  if (own[key] !== undefined && typeof own[key] !== 'function')
2336
2398
  throw new TypeError(`Pass ${key} to the turn as a function, or leave it out.`);
2337
2399
  }
@@ -2416,6 +2478,9 @@ export function buyTool(options) {
2416
2478
  onFound: own.onFound ?? o.onFound,
2417
2479
  onAvailability: own.onAvailability ?? o.onAvailability,
2418
2480
  onMerchantStatus: own.onMerchantStatus ?? o.onMerchantStatus,
2481
+ onStoreCheck: own.onStoreCheck ?? o.onStoreCheck,
2482
+ onRecheck: own.onRecheck ?? o.onRecheck,
2483
+ onFreshness: own.onFreshness ?? o.onFreshness,
2419
2484
  onProducts: own.onProducts ?? o.onProducts,
2420
2485
  // Agentcard's message takes its place in the turn's sequence, after the items before it, and goes to onText alone, so
2421
2486
  // an app that passes onItem too hears it once.
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The shopper's size on a product card, as Agentcard sends it on a shoe or a garment it read at the store for the size the
3
+ * shopper named: the size asked for, the store's own label for it, and whether the store has it in stock. `available` is
4
+ * null when no read answered, and the card then says nothing of the size: an app never shows a size as in stock that
5
+ * Agentcard did not read. `line` is the words to show under the price, or null when there is nothing to say.
6
+ */
7
+ export interface BuyCardSize {
8
+ /** The size the shopper asked for, as they read it: `men's 10.5`, `EU 44`, `size M`. */
9
+ requested: string;
10
+ /** The store's own label for that size (`10.5`, `M 10 / W 11.5`), or null when the store does not sell it. */
11
+ label: string | null;
12
+ /** True when the store has it in stock, false when it does not sell it or sold out of it, null when unread. */
13
+ available: boolean | null;
14
+ /** `men's 10.5 in stock` or `No men's 10.5 here`; null when `available` is null. */
15
+ line: string | null;
16
+ }
17
+ /** `shopper_size` as Agentcard sent it, or null when it sent none or a shape this version cannot read. */
18
+ export declare function cardSizeOf(v: unknown): BuyCardSize | null;
19
+ /** `{ shopper_size }` when Agentcard sent one, else nothing: a card with no size carries no field. */
20
+ export declare function cardSizeField(v: Record<string, unknown>): {
21
+ shopper_size?: BuyCardSize;
22
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The shopper's size on a product card, as Agentcard sends it on a shoe or a garment it read at the store for the size the
3
+ * shopper named: the size asked for, the store's own label for it, and whether the store has it in stock. `available` is
4
+ * null when no read answered, and the card then says nothing of the size: an app never shows a size as in stock that
5
+ * Agentcard did not read. `line` is the words to show under the price, or null when there is nothing to say.
6
+ */
7
+ /** `shopper_size` as Agentcard sent it, or null when it sent none or a shape this version cannot read. */
8
+ export function cardSizeOf(v) {
9
+ if (!v || typeof v !== 'object')
10
+ return null;
11
+ const r = v;
12
+ if (typeof r.requested !== 'string' || !r.requested.trim())
13
+ return null;
14
+ const available = r.available === true ? true : r.available === false ? false : null;
15
+ const label = typeof r.label === 'string' && r.label.trim() ? r.label : null;
16
+ const line = available === true ? `${r.requested} in stock` : available === false ? `No ${r.requested} here` : null;
17
+ return { requested: r.requested, label, available, line };
18
+ }
19
+ /** `{ shopper_size }` when Agentcard sent one, else nothing: a card with no size carries no field. */
20
+ export function cardSizeField(v) {
21
+ const size = cardSizeOf(v.shopper_size);
22
+ return size ? { shopper_size: size } : {};
23
+ }
package/dist/client.d.ts CHANGED
@@ -8,9 +8,11 @@
8
8
  * @agent-cards/sdk (packages/checkout/src/buy.ts: turnOf, buyError), and the
9
9
  * token cache follows its client (packages/checkout/src/client.ts accessToken).
10
10
  */
11
+ import { type BuyTrip } from './trip.js';
11
12
  import { type BuyPlan } from './plan.js';
12
13
  import { type BuyMerchantStatusUpdate, type BuySearchStatus } from './search-status.js';
13
14
  import { type BuyFulfilledBy } from './fulfilled-by.js';
15
+ import { type BuyCardSize } from './card-size.js';
14
16
  import { type AgentcardListener, type ListenOptions } from './listen.js';
15
17
  import { type ShopperMessage } from './shopper-message.js';
16
18
  /** The fetch the client calls: Node 18 and later have one built in. */
@@ -93,6 +95,26 @@ export interface BuyAvailabilityUpdate {
93
95
  availability: BuyAvailability;
94
96
  }>;
95
97
  }
98
+ /**
99
+ * One store behind one card of a running turn, as Agentcard asks the store itself for its price and stock: `checking` until
100
+ * the store answers, then `confirmed` (with the store's own price in `priceCents` when it gave one), `sold_out` (the store
101
+ * leaves the card) or `unconfirmed` (the store gave no stock answer; the card keeps the catalog's word for it). Show
102
+ * "checking <storeName>…" on the card until its store answers.
103
+ *
104
+ * Agentcard checks at most eight stores a turn, in the order the cards show, and each store once, under the first card that
105
+ * lists it: a later card that lists the same store, and a store past the eighth, hear no update and keep the catalog's word.
106
+ */
107
+ export interface BuyStoreCheckUpdate {
108
+ conversationId: string;
109
+ /** The card's catalog `id`. */
110
+ cardId: string;
111
+ /** The store's website, such as `walmart.com`. */
112
+ store: string;
113
+ /** The store as a shopper reads it, such as `Walmart`. */
114
+ storeName: string;
115
+ state: 'checking' | 'confirmed' | 'sold_out' | 'unconfirmed';
116
+ priceCents: number | null;
117
+ }
96
118
  /** How many products a running turn found so far, and how many of them it shows: "8 of 23 shown". */
97
119
  export interface BuyFoundUpdate {
98
120
  conversationId: string;
@@ -174,6 +196,52 @@ export interface BuyStreamHandlers {
174
196
  * follow one store, keep the updates `isMerchantStatusAt(update, 'walmart.com')` accepts. The answer's `search` holds every store.
175
197
  */
176
198
  onMerchantStatus?: (update: BuyMerchantStatusUpdate) => void | Promise<void>;
199
+ /**
200
+ * Agentcard reads the shopper's quotes again: on their return after a gap, or before a yes to a price older than its
201
+ * window. Comes with `phase: 'started'`, the line to show at once and every item `checking`, then `phase: 'finished'` with
202
+ * the answer to show and every item's result. A changed price is a question: the turn's reply asks it and nothing is booked.
203
+ */
204
+ onRecheck?: (update: BuyRecheck) => void | Promise<void>;
205
+ /** One item's answer as its read lands, so its status moves from checking to confirmed, changed or gone. */
206
+ onFreshness?: (update: BuyFreshness) => void | Promise<void>;
207
+ /** A store behind a card, while Agentcard checks its price and stock and once it answers: up to eight stores a turn, each under its first card. */
208
+ onStoreCheck?: (update: BuyStoreCheckUpdate) => void | Promise<void>;
209
+ }
210
+ /** Whether a quote read again still stands: `same` price, a `changed` one, or `gone` from the seller. */
211
+ export type BuyFreshnessState = 'same' | 'changed' | 'gone';
212
+ /** One quote after it was read again. Amounts are in the currency's smallest unit. */
213
+ export interface BuyFreshness {
214
+ conversationId: string | null;
215
+ ref: string;
216
+ kind: 'flight' | 'event_ticket' | 'hotel' | 'rental_car' | 'restaurant_table' | 'store_cart';
217
+ name: string;
218
+ /** Who sells it, where the name does not say: a store's name. Null for a trip's item. */
219
+ seller: string | null;
220
+ freshness: BuyFreshnessState;
221
+ /** The quiet status to show beside the item: `confirmed`, `changed` or `gone`. */
222
+ status: 'confirmed' | 'changed' | 'gone';
223
+ /** The status in words: `Confirmed`, `Now $239.00`, `Sold out`. */
224
+ text: string;
225
+ previousCents: number | null;
226
+ amountCents: number | null;
227
+ currency: string | null;
228
+ /** When the read saw this price, ISO 8601. */
229
+ checkedAt: string;
230
+ }
231
+ /** A re-check of the shopper's quotes, as it starts and as it ends. */
232
+ export interface BuyRecheck {
233
+ conversationId: string | null;
234
+ phase: 'started' | 'finished';
235
+ /** `return`: the shopper came back after a gap. `commit`: a yes to prices older than their window. */
236
+ reason: 'return' | 'commit';
237
+ /** The line to show: what is being read again when it starts, the answer when it ends. Null when no read answered. */
238
+ text: string | null;
239
+ /** At the start, each item with `status: 'checking'`; at the end, each item that answered. */
240
+ items: Array<Pick<BuyFreshness, 'ref' | 'kind' | 'name' | 'seller' | 'text'> & Omit<Partial<BuyFreshness>, 'status'> & {
241
+ status: 'checking' | BuyFreshness['status'];
242
+ }>;
243
+ /** Items whose read did not answer: they keep their old price, and the checkout's own amount check still holds. */
244
+ unread: string[];
177
245
  }
178
246
  export declare const DEFAULT_BASE_URL = "https://api.agentcard.sh";
179
247
  /** The wait for one purchase turn: past Agentcard's own 110-second budget, so a slow turn answers `turn_timeout` rather than aborting here. */
@@ -429,6 +497,18 @@ export interface BuyFlightItinerary {
429
497
  arrivalAt: string | null;
430
498
  cabinClasses: Array<string | null>;
431
499
  }>;
500
+ /** Who the price covers, when it covers more than one passenger: `priceCents` is then the whole party's. Absent for one passenger and from an older API. */
501
+ party?: BuyFlightParty;
502
+ }
503
+ /**
504
+ * A flight price for a party. `totalCents` is the price for every passenger together, the same as the card's `priceCents`.
505
+ * `perPassengerCents` is each passenger's share, adults first, when Agentcard knows it; absent when the fares differ by age.
506
+ */
507
+ export interface BuyFlightParty {
508
+ adults: number;
509
+ children: number;
510
+ totalCents: number;
511
+ perPassengerCents?: number[];
432
512
  }
433
513
  export interface BuyCatalogItem {
434
514
  flight?: BuyFlightItinerary;
@@ -452,8 +532,18 @@ export interface BuyCatalogItem {
452
532
  url: string | null;
453
533
  /** Whether a place has room at the asked time (BuyAvailability). Absent on a product with no such question. */
454
534
  availability?: BuyAvailability;
535
+ /**
536
+ * Whether the product ships to the shopper's country: `yes` or `no` when the store lists the countries it ships to and
537
+ * Agentcard knows where the shopper is, `unknown` otherwise and from an older API. Agentcard never leaves a product out for
538
+ * it, and ranks a `no` behind the products that match the ask as well; an app may filter on it.
539
+ */
540
+ ships_to_you: 'yes' | 'no' | 'unknown';
541
+ /** A short note for a product that ranks down for where it ships ("ships from the UK", "price in £"), or null. */
542
+ ships_note: string | null;
455
543
  /** Who sells or books the product, where that is not the store the card names (`booked with OpenTable`); absent when Agentcard named none. */
456
544
  fulfilled_by?: BuyFulfilledBy;
545
+ /** The size the shopper named, as the store's read of this product found it (BuyCardSize); absent when none was asked or read. */
546
+ shopper_size?: BuyCardSize;
457
547
  }
458
548
  /**
459
549
  * The products the last search found. `presented` is true when this answer's reply presents them: the
@@ -664,6 +754,8 @@ export interface BuyConversation {
664
754
  last_checkout: BuyLastCheckout | null;
665
755
  orders: BuyOrder[];
666
756
  carts: BuyCart[];
757
+ /** A trip being planned: each background search's status and results, flights first; null for any other conversation (trip.ts). */
758
+ trip: BuyTrip | null;
667
759
  /**
668
760
  * The reply Agentcard wrote since the shopper's last message, once no turn runs: the answer a call that ended with
669
761
  * `turn_timeout` never got. Null while a turn runs, when no reply was written since, and on an older API.
package/dist/client.js CHANGED
@@ -8,12 +8,15 @@
8
8
  * @agent-cards/sdk (packages/checkout/src/buy.ts: turnOf, buyError), and the
9
9
  * token cache follows its client (packages/checkout/src/client.ts accessToken).
10
10
  */
11
+ import { tripOf } from './trip.js';
11
12
  import { planOf } from './plan.js';
12
13
  import { merchantStatusOf, searchOf } from './search-status.js';
13
14
  import { isRecord } from './verify.js';
14
15
  import { failedPartyOf, fulfilledByField } from './fulfilled-by.js';
16
+ import { cardSizeField } from './card-size.js';
15
17
  import { listenToAgentcard } from './listen.js';
16
18
  import { messageInChat } from './shopper-message.js';
19
+ const STORE_CHECK_STATES = new Set(['checking', 'confirmed', 'sold_out', 'unconfirmed']);
17
20
  /**
18
21
  * The words this SDK shows for a step whatever Agentcard's own line says, so every app reads the same line. `searching_wider`
19
22
  * comes when a search across every store has shown no product for a while and keeps running; its products still come as cards.
@@ -22,7 +25,7 @@ export const STATUS_WORDING = Object.freeze({
22
25
  searching_wider: 'Checking more stores for this…',
23
26
  });
24
27
  /** The handlers that ask Agentcard to stream a turn. */
25
- const STREAM_HANDLERS = ['onProgress', 'onStatus', 'onFound', 'onCatalog', 'onSegment', 'onAck', 'onCart', 'onDelta', 'onAvailability', 'onReplyWritten', 'onMerchantStatus'];
28
+ const STREAM_HANDLERS = ['onProgress', 'onStatus', 'onFound', 'onCatalog', 'onSegment', 'onAck', 'onCart', 'onDelta', 'onAvailability', 'onReplyWritten', 'onMerchantStatus', 'onRecheck', 'onFreshness', 'onStoreCheck'];
26
29
  const PROGRESS_STAGES = new Set(['searching', 'building_cart', 'checking_out', 'working']);
27
30
  export const DEFAULT_BASE_URL = 'https://api.agentcard.sh';
28
31
  /** The wait for one purchase turn: past Agentcard's own 110-second budget, so a slow turn answers `turn_timeout` rather than aborting here. */
@@ -107,6 +110,45 @@ export class AgentcardApiError extends Error {
107
110
  }
108
111
  const str = (v) => (typeof v === 'string' && v !== '' ? v : null);
109
112
  const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
113
+ const QUOTE_KINDS = new Set(['flight', 'event_ticket', 'hotel', 'rental_car', 'restaurant_table', 'store_cart']);
114
+ const FRESHNESS = new Set(['same', 'changed', 'gone']);
115
+ const FRESH_STATUS = { same: 'confirmed', changed: 'changed', gone: 'gone' };
116
+ /** One item of a `freshness` event or a finished `recheck`, or null when it is not one. */
117
+ function freshnessOf(data, conversationId) {
118
+ const ref = str(data.ref);
119
+ const kind = str(data.kind);
120
+ const name = str(data.name);
121
+ const freshness = str(data.freshness);
122
+ if (!ref || !kind || !QUOTE_KINDS.has(kind) || !name || !freshness || !FRESHNESS.has(freshness))
123
+ return null;
124
+ return {
125
+ conversationId, ref, kind: kind, name, seller: str(data.seller),
126
+ freshness: freshness, status: FRESH_STATUS[freshness], text: str(data.text) ?? '',
127
+ previousCents: num(data.previous_cents), amountCents: num(data.amount_cents), currency: str(data.currency), checkedAt: str(data.checked_at) ?? '',
128
+ };
129
+ }
130
+ /** A `recheck` event as the app reads it. */
131
+ function recheckOf(data, conversationId) {
132
+ const items = (Array.isArray(data.items) ? data.items : []).flatMap((raw) => {
133
+ if (!raw || typeof raw !== 'object')
134
+ return [];
135
+ const i = raw;
136
+ const done = freshnessOf(i, conversationId);
137
+ if (done)
138
+ return [done];
139
+ const ref = str(i.ref);
140
+ const kind = str(i.kind);
141
+ const name = str(i.name);
142
+ // Only a started re-check's item is still checking: a finished one this version cannot read is left out.
143
+ if (data.phase !== 'started' || !ref || !kind || !QUOTE_KINDS.has(kind) || !name)
144
+ return [];
145
+ return [{ ref, kind: kind, name, seller: str(i.seller), text: str(i.text) ?? '', status: 'checking' }];
146
+ });
147
+ return {
148
+ conversationId, phase: data.phase, reason: data.reason === 'commit' ? 'commit' : 'return',
149
+ text: str(data.text), items, unread: Array.isArray(data.unread) ? data.unread.filter((u) => typeof u === 'string') : [],
150
+ };
151
+ }
110
152
  const bool = (v) => (typeof v === 'boolean' ? v : null);
111
153
  /**
112
154
  * A product page's address: an absolute http or https URL, serialized the way
@@ -240,8 +282,18 @@ function flightOf(value) {
240
282
  departureAt: str(leg.departureAt), arrivalAt: str(leg.arrivalAt),
241
283
  cabinClasses: Array.isArray(leg.cabinClasses) ? leg.cabinClasses.map(str) : [] };
242
284
  }),
285
+ ...partyOf(value.party),
243
286
  };
244
287
  }
288
+ function partyOf(value) {
289
+ if (!isRecord(value))
290
+ return {};
291
+ const adults = num(value.adults), children = num(value.children), totalCents = num(value.totalCents);
292
+ if (adults === null || children === null || totalCents === null || adults + children < 2)
293
+ return {};
294
+ const each = Array.isArray(value.perPassengerCents) ? value.perPassengerCents.map(num) : null;
295
+ return { party: { adults, children, totalCents, ...(each && each.length === adults + children && each.every((c) => c !== null) ? { perPassengerCents: each } : {}) } };
296
+ }
245
297
  function catalogOf(v) {
246
298
  if (!isRecord(v))
247
299
  return null;
@@ -265,7 +317,10 @@ function catalogItemsOf(v) {
265
317
  store: storeOf(i.store),
266
318
  url: pageUrl(i.url),
267
319
  ...availabilityField(i),
320
+ ships_to_you: i.ships_to_you === 'yes' || i.ships_to_you === 'no' ? i.ships_to_you : 'unknown',
321
+ ships_note: str(i.ships_note),
268
322
  ...fulfilledByField(i),
323
+ ...cardSizeField(i),
269
324
  };
270
325
  }) : [];
271
326
  }
@@ -404,6 +459,7 @@ function conversationOf(body) {
404
459
  })) : [],
405
460
  carts: cartsOf(body.carts),
406
461
  latest_reply: latestReplyOf(body.latest_reply),
462
+ trip: tripOf(body.trip),
407
463
  raw: body,
408
464
  };
409
465
  }
@@ -523,7 +579,7 @@ async function eachEvent(res, onEvent) {
523
579
  }
524
580
  /**
525
581
  * A streamed purchase turn, read to its `envelope`: the same answer a JSON turn returns, with its
526
- * status in `http_status`. Each `progress`, `status`, `delta`, `catalog`, `find`, `segment`, `reply_written`, `cart`, `venue_availability` and `merchant` goes to its handler
582
+ * status in `http_status`. Each `progress`, `status`, `delta`, `catalog`, `find`, `segment`, `reply_written`, `cart`, `venue_availability`, `merchant` and `store_check` goes to its handler
527
583
  * on the way, and any other event, such as a `ping` or a `tool`, is read past. A stream that breaks or
528
584
  * ends before its envelope, once it named the conversation, reads as a turn past its budget
529
585
  * (`turn_timeout`), so the tool reads the conversation for what the turn left instead of losing it.
@@ -608,6 +664,14 @@ async function readStreamedTurn(res, stream, timeoutMs) {
608
664
  else if (event === 'ack' && typeof data.text === 'string' && data.text.trim() !== '' && stream.onAck) {
609
665
  deliver(() => stream.onAck({ conversationId: named, text: data.text }));
610
666
  }
667
+ else if (event === 'recheck' && stream.onRecheck && (data.phase === 'started' || data.phase === 'finished')) {
668
+ const recheck = recheckOf(data, named);
669
+ deliver(() => stream.onRecheck(recheck));
670
+ }
671
+ else if (event === 'freshness' && stream.onFreshness && freshnessOf(data, named)) {
672
+ const one = freshnessOf(data, named);
673
+ deliver(() => stream.onFreshness(one));
674
+ }
611
675
  else if (event === 'venue_availability' && stream.onAvailability && str(data.conversation_id) && Array.isArray(data.items)) {
612
676
  const items = data.items.filter(isRecord).flatMap((i) => {
613
677
  const id = str(i.id);
@@ -617,6 +681,18 @@ async function readStreamedTurn(res, stream, timeoutMs) {
617
681
  if (items.length)
618
682
  deliver(() => stream.onAvailability({ conversationId: data.conversation_id, items }));
619
683
  }
684
+ else if (event === 'store_check' && stream.onStoreCheck && str(data.conversation_id) && isRecord(data.item)) {
685
+ const i = data.item;
686
+ const cardId = str(i.id);
687
+ const store = str(i.store);
688
+ if (cardId && store && typeof i.state === 'string' && STORE_CHECK_STATES.has(i.state)) {
689
+ const update = {
690
+ conversationId: data.conversation_id, cardId, store, storeName: str(i.store_name) ?? store,
691
+ state: i.state, priceCents: typeof i.price_cents === 'number' ? i.price_cents : null,
692
+ };
693
+ deliver(() => stream.onStoreCheck(update));
694
+ }
695
+ }
620
696
  else if (event === 'merchant' && stream.onMerchantStatus) {
621
697
  const update = merchantStatusOf(data);
622
698
  if (update)
@@ -45,7 +45,7 @@ export const BUY_TOOL_RULES = [
45
45
  '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?".',
46
46
  '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.',
47
47
  '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)".',
48
- '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.',
48
+ 'When `next_action` is `confirm`, ask in one short message: the items, each at its price before fees as the cart lists it, saying so when that is not the price its card showed, the total said as "with fees", and to say yes to place it, such as "1 carolina black socks pack from walmart, $9.34. $9.57 with fees. say yes to place it". Never give the total as an item\'s price. Leave out the fees one by one, the hold, and the address unless the shopper just changed it.',
49
49
  '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".',
50
50
  '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.',
51
51
  'Never tell the shopper to remove, delete or replace a card or to change a setting, even when `reply` says to.',
@@ -54,7 +54,7 @@ export const BUY_TOOL_RULES = [
54
54
  'Write every sentence in your own style, casing included, even where `reply` is written another way.',
55
55
  "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, Best Buy and thousands more stores, so name those four and say \"and thousands more stores\" every time you name the stores you shop, in a reply about one kind of thing too, such as \"travel gear from Amazon, Target and thousands more stores\": never let the four read as the whole list, never say \"plenty more\", \"many more\" or \"most online stores\", and never give a number of stores. Name no other store unless a result showed it.",
56
56
  "Never tell the shopper you can't render or show Markdown, a table or other formatting, and never say you keep to plain text: how your words look is your app's to decide.",
57
- '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.',
57
+ '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, $11.88, about $12.17 with fees before delivery and tax" and then "next, sign in to doordash with the link so i can place it": each item at its price before fees as the cart lists it, saying so when that is not the price its card showed, and one total said as "with fees", never the total as an item\'s price, 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.',
58
58
  '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".',
59
59
  '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.',
60
60
  '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.',
package/dist/events.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * name. The stream (`agentcard.listen`) and a webhook endpoint carry the same types, with the same
4
4
  * envelope and the same ids. A test holds this list to the backend's catalog.
5
5
  */
6
- export declare const AGENTCARD_EVENT_TYPES: readonly ['card.created', 'card.updated', 'card.closed', 'cardholder.created', 'cardholder.updated', 'transaction.authorized', 'transaction.declined', 'transaction.cleared', 'transaction.voided', 'balance.low', 'cardholder_onboarding_session.completed', 'platform_connect_session.completed', 'merchant.connected', 'order.placed', 'order.confirmed', 'order.failed', 'order.updated', 'purchase.message', 'connection.created', 'connected_card.updated', 'vault.card_stored', 'vault.session_linked', 'vault.session_card_picked', 'vault.payment_permission.updated', 'action.confirmed', 'action.done', 'checkout_authorization.approved', 'checkout_authorization.submitted', 'checkout_authorization.declined', 'checkout_authorization.cancelled', 'checkout_authorization.expired', 'checkout_authorization.duplicate_charge', 'checkout_authorization.refused', 'checkout_authorization.watched', 'checkout_authorization.amount_mismatch', 'checkout_authorization.settled', 'checkout_authorization.settlement_unconfirmed', 'checkout_authorization.outcome_reported', 'card_flow.started', 'card_flow.failed', 'transfer.approved', 'transfer.initiated', 'transfer.completed', 'transfer.failed', 'transfer.released', 'wallet.funded', 'user_wallet.funding_detected', 'user_wallet.funded', 'wallet.balance.low', 'recovery.requested', 'recovery.completed', 'recovery.rejected', 'wallet.withdrawal.initiated', 'wallet.withdrawal.completed', 'wallet.withdrawal.failed', 'identity.verification.updated', 'approval.requested', 'reward.earned', 'reward.reversed', 'wallet_link.opened'];
6
+ export declare const AGENTCARD_EVENT_TYPES: readonly ['card.created', 'card.updated', 'card.closed', 'cardholder.created', 'cardholder.updated', 'transaction.authorized', 'transaction.declined', 'transaction.cleared', 'transaction.voided', 'balance.low', 'cardholder_onboarding_session.completed', 'platform_connect_session.completed', 'merchant.connected', 'order.placed', 'order.confirmed', 'order.failed', 'order.updated', 'purchase.message', 'connection.created', 'connected_card.updated', 'vault.card_stored', 'vault.session_linked', 'vault.session_card_picked', 'vault.payment_permission.updated', 'action.confirmed', 'action.done', 'checkout_authorization.approved', 'checkout_authorization.submitted', 'checkout_authorization.declined', 'checkout_authorization.cancelled', 'checkout_authorization.expired', 'checkout_authorization.duplicate_charge', 'checkout_authorization.refused', 'checkout_authorization.watched', 'checkout_authorization.amount_mismatch', 'checkout_authorization.settled', 'checkout_authorization.settlement_unconfirmed', 'checkout_authorization.outcome_reported', 'card_flow.started', 'card_flow.failed', 'transfer.approved', 'transfer.initiated', 'transfer.completed', 'transfer.failed', 'transfer.released', 'wallet.funded', 'user_wallet.funding_detected', 'user_wallet.funded', 'wallet.balance.low', 'recovery.requested', 'recovery.completed', 'recovery.rejected', 'wallet.withdrawal.initiated', 'wallet.withdrawal.completed', 'wallet.withdrawal.failed', 'identity.verification.updated', 'approval.requested', 'reward.earned', 'reward.reversed', 'wallet_link.opened', 'plan.updated', 'plan.item.updated'];
7
7
  /** One event type Agentcard sends. A type Agentcard adds later still reads as its own name. */
8
8
  export type AgentcardEventType = (typeof AGENTCARD_EVENT_TYPES)[number];
9
9
  /** Whether a filter entry names something Agentcard sends: an event type, a family such as `order.*`, or `*`. */
package/dist/events.js CHANGED
@@ -64,6 +64,8 @@ export const AGENTCARD_EVENT_TYPES = [
64
64
  'reward.earned',
65
65
  'reward.reversed',
66
66
  'wallet_link.opened',
67
+ 'plan.updated',
68
+ 'plan.item.updated',
67
69
  ];
68
70
  const KNOWN = new Set(AGENTCARD_EVENT_TYPES);
69
71
  const FAMILIES = new Set(AGENTCARD_EVENT_TYPES.map((t) => `${t.split('.')[0]}.*`));
package/dist/index.d.ts CHANGED
@@ -4,13 +4,13 @@ export { readShopperMessage, isStale, SHOPPER_EVENT_TYPES, NEXT_STEP_KINDS, type
4
4
  export { ORDER_STEPS, updateOrder, arrivalText, type ShopperOrder, type OrderStep, type OrderPathStep, type OrderStepState, type ArrivalTextOptions, } from './order.js';
5
5
  export { agentcardWebhook, purchaseAgentWebhook, type AgentcardWebhookOptions, type AgentcardWebhookHandler, type PurchaseAgentWebhookOptions, type WebhookFetchRequest, type WebhookRequest, type WebhookResponse, } from './handler.js';
6
6
  export { searchAt, isMerchantStatusAt, merchantStatusLine, searchStatusLines, type BuySearchStatus, type BuyMerchantStatus, type BuyMerchantSearchStatus, type BuyMerchantStatusUpdate, } from './search-status.js';
7
- 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, STATUS_WORDING, type BuySegmentUpdate, 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 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';
7
+ 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';
8
8
  export { BUY_TOOL_NAME, BUY_TOOL_DESCRIPTION, buyToolDefinition, parseBuyToolInput, type BuyToolDefinition, type BuyToolInput, type BuyToolInputSchema, type BuyToolInputCheck, type BuyToolStandardSchema, type OpenAIBuyTool, } from './definition.js';
9
9
  export { toMessages, messageStream, typingDelay, type MessagePace, type MessageStream, type MessageStreamOptions, } from './messages.js';
10
10
  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';
11
11
  export { instantAck, instantReaction, ackFor, keepsYes, ACK_REACTION, type AckForOptions } from './ack.js';
12
12
  export { directCall, isProductAsk, type DirectCall, type DirectContext, type DirectReason } from './direct.js';
13
- export { forModel, grandTotals, forModelError, stepAsWritten, withoutCardList, stripLinks, type BuyToolModelView, type BuyToolModelCart, type BuyToolGrandTotal, type BuyToolModelProduct, type BuyToolModelStore, type ShownCard, } from './model-view.js';
13
+ export { forModel, grandTotals, forModelError, stepAsWritten, withoutCardList, stripLinks, flightPartyWords, type BuyToolModelView, type BuyToolModelCart, type BuyToolGrandTotal, type BuyToolModelProduct, type BuyToolModelStore, type ShownCard, } from './model-view.js';
14
14
  export { asBullets, choiceLines, replyAsWritten, textingStyle, withChoice, type ReplyAsWrittenOptions } from './texting.js';
15
15
  export { appInstructions, APP_INSTRUCTIONS_VERSION, type AppInstructionsOptions } from './app-instructions.js';
16
16
  export { toAnthropicTool, toOpenRouterTool, type AnthropicBuyTool, type OpenRouterToolkit, type ZodLike, } from './adapters.js';
@@ -21,4 +21,6 @@ export { turnStream, readTurn, TURN_RECORD_TTL_SECONDS, type TurnStream, type Tu
21
21
  export { TURN_RECOVERY_ON, isTurnId, type TurnEvent, type TurnRecord, type TurnRecordStore } from './turn-record.js';
22
22
  export type { BuyLateResults, BuyStoreWait } from './store-wait.js';
23
23
  export { planOf, type BuyPlan, type BuyPlanItem, type BuyPlanItemState, type BuyPlanStatus } from './plan.js';
24
+ export { tripOf, type BuyTrip, type TripLane, type TripLaneName, type TripLaneResult, type TripLaneStatus } from './trip.js';
24
25
  export { fulfilledByLine, fulfilledByOf, type BuyFulfilledBy, type BuyFulfilledByKind } from './fulfilled-by.js';
26
+ export { cardSizeOf, type BuyCardSize } from './card-size.js';
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ export { toMessages, messageStream, typingDelay, } from './messages.js';
10
10
  export { buyTool, nextStepPrompt, productAction, } from './buy-tool.js';
11
11
  export { instantAck, instantReaction, ackFor, keepsYes, ACK_REACTION } from './ack.js';
12
12
  export { directCall, isProductAsk } from './direct.js';
13
- export { forModel, grandTotals, forModelError, stepAsWritten, withoutCardList, stripLinks, } from './model-view.js';
13
+ export { forModel, grandTotals, forModelError, stepAsWritten, withoutCardList, stripLinks, flightPartyWords, } from './model-view.js';
14
14
  export { asBullets, choiceLines, replyAsWritten, textingStyle, withChoice } from './texting.js';
15
15
  export { appInstructions, APP_INSTRUCTIONS_VERSION } from './app-instructions.js';
16
16
  export { toAnthropicTool, toOpenRouterTool, } from './adapters.js';
@@ -20,4 +20,6 @@ export { AGENTCARD_EVENT_TYPES, } from './events.js';
20
20
  export { turnStream, readTurn, TURN_RECORD_TTL_SECONDS } from './turn-stream.js';
21
21
  export { TURN_RECOVERY_ON, isTurnId } from './turn-record.js';
22
22
  export { planOf } from './plan.js';
23
+ export { tripOf } from './trip.js';
23
24
  export { fulfilledByLine, fulfilledByOf } from './fulfilled-by.js';
25
+ export { cardSizeOf } from './card-size.js';
@@ -7,7 +7,7 @@
7
7
  * `product_links_sent` for the products' store pages, and `link_sent` on each
8
8
  * store and `store_links_sent` for the pages of the stores to pick from.
9
9
  */
10
- import type { BuyCart, BuyNextAction } from './client.js';
10
+ import type { BuyCart, BuyFlightParty, BuyNextAction } from './client.js';
11
11
  import type { BuyToolProduct, BuyToolResult, BuyToolStatus, BuyToolStore } from './buy-tool.js';
12
12
  export interface BuyToolModelCart {
13
13
  store: string;
@@ -208,7 +208,15 @@ export declare function productPrice(product: {
208
208
  priceCents: number | null;
209
209
  currency: string | null;
210
210
  priceFrom?: true;
211
+ flight?: {
212
+ party?: BuyFlightParty;
213
+ };
211
214
  }): string | null;
215
+ /**
216
+ * Who a party's flight price covers, as every app shows it after the price: `for 2`, or `for 2 adults and 1 child` when
217
+ * children fly. `$1,610 for 2` is the price of both seats together.
218
+ */
219
+ export declare function flightPartyWords(party: BuyFlightParty): string;
212
220
  export declare function money(minor: number | null | undefined, currency: string | null | undefined): string | null;
213
221
  /**
214
222
  * The products the model reads: the first twenty that keep a name and a store
@@ -110,8 +110,19 @@ function currencyFormat(currency) {
110
110
  */
111
111
  export function productPrice(product) {
112
112
  const price = money(product.priceCents, product.currency);
113
+ if (price !== null && product.flight?.party)
114
+ return `${price} ${flightPartyWords(product.flight.party)}`;
113
115
  return price !== null && product.priceFrom ? `from ${price}` : price;
114
116
  }
117
+ /**
118
+ * Who a party's flight price covers, as every app shows it after the price: `for 2`, or `for 2 adults and 1 child` when
119
+ * children fly. `$1,610 for 2` is the price of both seats together.
120
+ */
121
+ export function flightPartyWords(party) {
122
+ if (party.children === 0)
123
+ return `for ${party.adults}`;
124
+ return `for ${party.adults} adult${party.adults === 1 ? '' : 's'} and ${party.children} ${party.children === 1 ? 'child' : 'children'}`;
125
+ }
115
126
  export function money(minor, currency) {
116
127
  if (typeof minor !== 'number' || !Number.isFinite(minor))
117
128
  return null;
package/dist/trip.d.ts ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * A trip Agentcard is planning, as a conversation read carries it (`BuyConversation.trip`): the place, the dates and the
3
+ * party, and each search Agentcard runs for it in the background, flights first. An app shows each lane as its results
4
+ * arrive and shows `checking` beside a lane until `done`; it never presents a lane's results as the full list before then.
5
+ * `fulfilled_by` on a result names who the shopper books with ("Korean Air", "Marriott"), when Agentcard knows it.
6
+ */
7
+ export type TripLaneName = 'flights' | 'hotels' | 'car' | 'activities' | 'payment';
8
+ export type TripLaneStatus = 'waiting' | 'queued' | 'searching' | 'confirmed' | 'no_results' | 'failed' | 'timed_out' | 'cancelled';
9
+ export interface TripLaneResult {
10
+ product_id: string;
11
+ title: string;
12
+ price_cents: number | null;
13
+ currency: string | null;
14
+ fulfilled_by: {
15
+ name: string;
16
+ kind: string;
17
+ } | null;
18
+ }
19
+ export interface TripLane {
20
+ lane: TripLaneName;
21
+ status: TripLaneStatus;
22
+ /** False while the lane waits, queues or searches. */
23
+ done: boolean;
24
+ /** The quiet line to show beside the lane until it is done ("still checking hotels…"); null once it is. */
25
+ checking: string | null;
26
+ /** Why the lane waits or how it ended, in a word ("needs_origin"), or null. */
27
+ why: string | null;
28
+ results: number;
29
+ /** The lane's results, ordered by what the shopper said matters, at most 8. */
30
+ items: TripLaneResult[];
31
+ /** The lane's last answer to a stated preference: re-ranked, searched again narrowly, or both, and why. */
32
+ decision: {
33
+ action: 'rerank' | 'narrow' | 'both';
34
+ why: string[];
35
+ } | null;
36
+ /** Hotels: false when the picked flight lands on another day than these hotels were searched for. */
37
+ fits_flight?: boolean | null;
38
+ /** The card check: true when the shopper's card pays without stopping at checkout. */
39
+ ready?: boolean;
40
+ }
41
+ export interface BuyTrip {
42
+ place: string | null;
43
+ check_in: string | null;
44
+ check_out: string | null;
45
+ travelers: number | null;
46
+ origin: string | null;
47
+ lanes: TripLane[];
48
+ }
49
+ /** The trip a conversation read carries, or null when it plans none or the API is older. */
50
+ export declare function tripOf(value: unknown): BuyTrip | null;
package/dist/trip.js ADDED
@@ -0,0 +1,31 @@
1
+ const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
2
+ const str = (v) => (typeof v === 'string' && v !== '' ? v : null);
3
+ const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
4
+ const LANES = ['flights', 'hotels', 'car', 'activities', 'payment'];
5
+ const STATUSES = ['waiting', 'queued', 'searching', 'confirmed', 'no_results', 'failed', 'timed_out', 'cancelled'];
6
+ const ACTIONS = ['rerank', 'narrow', 'both'];
7
+ const oneOf = (allowed, v) => (typeof v === 'string' && allowed.includes(v) ? v : null);
8
+ /** The trip a conversation read carries, or null when it plans none or the API is older. */
9
+ export function tripOf(value) {
10
+ if (!isRecord(value) || !Array.isArray(value.lanes))
11
+ return null;
12
+ return {
13
+ place: str(value.place), check_in: str(value.check_in), check_out: str(value.check_out), travelers: num(value.travelers), origin: str(value.origin),
14
+ // A lane this SDK does not know (a newer API's) is left out, and a status it does not know reads as still searching.
15
+ lanes: value.lanes.filter(isRecord).filter((l) => oneOf(LANES, l.lane) !== null).map((l) => ({
16
+ lane: oneOf(LANES, l.lane),
17
+ status: oneOf(STATUSES, l.status) ?? 'searching',
18
+ done: l.done === true,
19
+ checking: l.done === true ? null : str(l.checking),
20
+ why: str(l.why),
21
+ results: num(l.results) ?? 0,
22
+ items: Array.isArray(l.items) ? l.items.filter(isRecord).map((i) => ({
23
+ product_id: str(i.product_id) ?? '', title: str(i.title) ?? '', price_cents: num(i.price_cents), currency: str(i.currency),
24
+ fulfilled_by: isRecord(i.fulfilled_by) && str(i.fulfilled_by.name) ? { name: str(i.fulfilled_by.name), kind: str(i.fulfilled_by.kind) ?? 'seller' } : null,
25
+ })) : [],
26
+ decision: isRecord(l.decision) && oneOf(ACTIONS, l.decision.action) ? { action: oneOf(ACTIONS, l.decision.action), why: Array.isArray(l.decision.why) ? l.decision.why.filter((w) => typeof w === 'string') : [] } : null,
27
+ ...('fits_flight' in l ? { fits_flight: typeof l.fits_flight === 'boolean' ? l.fits_flight : null } : {}),
28
+ ...('ready' in l ? { ready: l.ready === true } : {}),
29
+ })),
30
+ };
31
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-cards/commerce-sdk",
3
- "version": "0.18.8",
3
+ "version": "0.18.10",
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",