@agent-cards/commerce-sdk 0.18.9 → 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.82";
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.82";
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.",
@@ -34,6 +34,7 @@
34
34
  import type { BuyCardSize } from './card-size.js';
35
35
  import type { BuyPlan } from './plan.js';
36
36
  import type { BuyMerchantStatusUpdate, BuySearchStatus } from './search-status.js';
37
+ import type { BuyFreshness, BuyRecheck, BuyStoreCheckUpdate } from './client.js';
37
38
  import type { BuyFulfilledBy } from './fulfilled-by.js';
38
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';
39
40
  import { type BuyToolDefinition, type BuyToolInput, type BuyToolInputSchema, type BuyToolStandardSchema, type OpenAIBuyTool } from './definition.js';
@@ -137,6 +138,10 @@ export interface BuyToolLink {
137
138
  * Absent on every other card, such as a retail product's.
138
139
  */
139
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;
140
145
  }
141
146
  /** A store the answer's reply presents to choose from. */
142
147
  export interface BuyToolStore {
@@ -187,6 +192,10 @@ export interface BuyToolProduct {
187
192
  linkSent: boolean;
188
193
  /** For a place to book: whether it has room at the asked time. Absent on a product with no such question. */
189
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;
190
199
  /** Who sells or books it, where that is not the store it names (`via StubHub`); absent when Agentcard named none. */
191
200
  fulfilledBy?: BuyFulfilledBy;
192
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. */
@@ -412,6 +421,18 @@ export interface BuyToolOptions extends BuyStoreWait {
412
421
  * search starts, then what it found. One that throws costs nothing but the call to it.
413
422
  */
414
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>;
415
436
  /**
416
437
  * Hears the products a call's search found, the moment the search answers and before Agentcard writes its reply,
417
438
  * with the conversation, when the search ran (`asOf`, as the answer's `catalog.as_of` names it) and the store's name.
@@ -641,6 +662,12 @@ export interface BuyToolTurnOptions extends BuyStoreWait {
641
662
  onAvailability?: BuyToolOptions['onAvailability'];
642
663
  /** Hears each store this turn's searches ask, as the tool's `onMerchantStatus` does. */
643
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'];
644
671
  /** Hears the products each search of this turn's calls found, the moment it answers, as the tool's `onProducts` does. */
645
672
  onProducts?: BuyToolOptions['onProducts'];
646
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 } : {}), ...(product.shopperSize ? { shopperSize: product.shopperSize } : {}) };
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,6 +282,8 @@ 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 } : {}),
282
288
  ...(item.shopper_size ? { shopperSize: item.shopper_size } : {}),
283
289
  }));
@@ -771,7 +777,7 @@ export function buyTool(options) {
771
777
  if (o.stream !== undefined && o.stream !== false)
772
778
  throw new TypeError('Pass stream as false to wait for each whole answer, or leave it out.');
773
779
  const onProgress = o.onProgress;
774
- for (const key of ['onStatus', 'onFound', 'onAvailability', 'onMerchantStatus']) {
780
+ for (const key of ['onStatus', 'onFound', 'onAvailability', 'onMerchantStatus', 'onStoreCheck', 'onRecheck', 'onFreshness']) {
775
781
  if (o[key] !== undefined && typeof o[key] !== 'function')
776
782
  throw new TypeError(`Pass ${key} as a function, or leave it out.`);
777
783
  }
@@ -853,6 +859,9 @@ export function buyTool(options) {
853
859
  onFound: o.onFound,
854
860
  onAvailability: o.onAvailability,
855
861
  onMerchantStatus: o.onMerchantStatus,
862
+ onStoreCheck: o.onStoreCheck,
863
+ onRecheck: o.onRecheck,
864
+ onFreshness: o.onFreshness,
856
865
  onProducts: o.onProducts,
857
866
  onText: o.onText,
858
867
  texting: textingByDefault,
@@ -1921,9 +1930,16 @@ export function buyTool(options) {
1921
1930
  },
1922
1931
  }
1923
1932
  : {}),
1924
- ...(early || t.onProducts
1933
+ ...(early || t.onProducts || t.onStoreCheck
1925
1934
  ? {
1926
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
+ }
1927
1943
  try {
1928
1944
  await t.onProducts?.(productsOfCatalog(update.catalog), { conversationId: update.conversationId, asOf: update.asOf, merchantName: update.catalog.merchant_name });
1929
1945
  }
@@ -1934,8 +1950,9 @@ export function buyTool(options) {
1934
1950
  const keepMore = () => { if (t.onFound && early)
1935
1951
  moreFound = { conversationId: update.conversationId, links: productsOfCatalog({ ...update.catalog, items: update.more }).map(productLink) }; };
1936
1952
  if (!early) {
1937
- // Without early cards nothing goes out here, but a store that answered after the reply still joins the result.
1938
- 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)
1939
1956
  lateCards.push(...productsOfCatalog(update.catalog));
1940
1957
  return;
1941
1958
  }
@@ -1978,6 +1995,50 @@ export function buyTool(options) {
1978
1995
  },
1979
1996
  }
1980
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
+ : {}),
1981
2042
  ...(t.onText || writtenText
1982
2043
  ? {
1983
2044
  onSegment: async ({ text, conversationId }) => {
@@ -2332,7 +2393,7 @@ export function buyTool(options) {
2332
2393
  const own = options ?? {};
2333
2394
  if (typeof own !== 'object')
2334
2395
  throw new TypeError('Pass the turn options as an object, or leave them out.');
2335
- 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']) {
2336
2397
  if (own[key] !== undefined && typeof own[key] !== 'function')
2337
2398
  throw new TypeError(`Pass ${key} to the turn as a function, or leave it out.`);
2338
2399
  }
@@ -2417,6 +2478,9 @@ export function buyTool(options) {
2417
2478
  onFound: own.onFound ?? o.onFound,
2418
2479
  onAvailability: own.onAvailability ?? o.onAvailability,
2419
2480
  onMerchantStatus: own.onMerchantStatus ?? o.onMerchantStatus,
2481
+ onStoreCheck: own.onStoreCheck ?? o.onStoreCheck,
2482
+ onRecheck: own.onRecheck ?? o.onRecheck,
2483
+ onFreshness: own.onFreshness ?? o.onFreshness,
2420
2484
  onProducts: own.onProducts ?? o.onProducts,
2421
2485
  // Agentcard's message takes its place in the turn's sequence, after the items before it, and goes to onText alone, so
2422
2486
  // an app that passes onItem too hears it once.
package/dist/client.d.ts CHANGED
@@ -8,6 +8,7 @@
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';
@@ -94,6 +95,26 @@ export interface BuyAvailabilityUpdate {
94
95
  availability: BuyAvailability;
95
96
  }>;
96
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
+ }
97
118
  /** How many products a running turn found so far, and how many of them it shows: "8 of 23 shown". */
98
119
  export interface BuyFoundUpdate {
99
120
  conversationId: string;
@@ -175,6 +196,52 @@ export interface BuyStreamHandlers {
175
196
  * follow one store, keep the updates `isMerchantStatusAt(update, 'walmart.com')` accepts. The answer's `search` holds every store.
176
197
  */
177
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[];
178
245
  }
179
246
  export declare const DEFAULT_BASE_URL = "https://api.agentcard.sh";
180
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. */
@@ -465,6 +532,14 @@ export interface BuyCatalogItem {
465
532
  url: string | null;
466
533
  /** Whether a place has room at the asked time (BuyAvailability). Absent on a product with no such question. */
467
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;
468
543
  /** Who sells or books the product, where that is not the store the card names (`booked with OpenTable`); absent when Agentcard named none. */
469
544
  fulfilled_by?: BuyFulfilledBy;
470
545
  /** The size the shopper named, as the store's read of this product found it (BuyCardSize); absent when none was asked or read. */
@@ -679,6 +754,8 @@ export interface BuyConversation {
679
754
  last_checkout: BuyLastCheckout | null;
680
755
  orders: BuyOrder[];
681
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;
682
759
  /**
683
760
  * The reply Agentcard wrote since the shopper's last message, once no turn runs: the answer a call that ended with
684
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,6 +8,7 @@
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';
@@ -15,6 +16,7 @@ import { failedPartyOf, fulfilledByField } from './fulfilled-by.js';
15
16
  import { cardSizeField } from './card-size.js';
16
17
  import { listenToAgentcard } from './listen.js';
17
18
  import { messageInChat } from './shopper-message.js';
19
+ const STORE_CHECK_STATES = new Set(['checking', 'confirmed', 'sold_out', 'unconfirmed']);
18
20
  /**
19
21
  * The words this SDK shows for a step whatever Agentcard's own line says, so every app reads the same line. `searching_wider`
20
22
  * comes when a search across every store has shown no product for a while and keeps running; its products still come as cards.
@@ -23,7 +25,7 @@ export const STATUS_WORDING = Object.freeze({
23
25
  searching_wider: 'Checking more stores for this…',
24
26
  });
25
27
  /** The handlers that ask Agentcard to stream a turn. */
26
- 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'];
27
29
  const PROGRESS_STAGES = new Set(['searching', 'building_cart', 'checking_out', 'working']);
28
30
  export const DEFAULT_BASE_URL = 'https://api.agentcard.sh';
29
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. */
@@ -108,6 +110,45 @@ export class AgentcardApiError extends Error {
108
110
  }
109
111
  const str = (v) => (typeof v === 'string' && v !== '' ? v : null);
110
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
+ }
111
152
  const bool = (v) => (typeof v === 'boolean' ? v : null);
112
153
  /**
113
154
  * A product page's address: an absolute http or https URL, serialized the way
@@ -276,6 +317,8 @@ function catalogItemsOf(v) {
276
317
  store: storeOf(i.store),
277
318
  url: pageUrl(i.url),
278
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),
279
322
  ...fulfilledByField(i),
280
323
  ...cardSizeField(i),
281
324
  };
@@ -416,6 +459,7 @@ function conversationOf(body) {
416
459
  })) : [],
417
460
  carts: cartsOf(body.carts),
418
461
  latest_reply: latestReplyOf(body.latest_reply),
462
+ trip: tripOf(body.trip),
419
463
  raw: body,
420
464
  };
421
465
  }
@@ -535,7 +579,7 @@ async function eachEvent(res, onEvent) {
535
579
  }
536
580
  /**
537
581
  * A streamed purchase turn, read to its `envelope`: the same answer a JSON turn returns, with its
538
- * 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
539
583
  * on the way, and any other event, such as a `ping` or a `tool`, is read past. A stream that breaks or
540
584
  * ends before its envelope, once it named the conversation, reads as a turn past its budget
541
585
  * (`turn_timeout`), so the tool reads the conversation for what the turn left instead of losing it.
@@ -620,6 +664,14 @@ async function readStreamedTurn(res, stream, timeoutMs) {
620
664
  else if (event === 'ack' && typeof data.text === 'string' && data.text.trim() !== '' && stream.onAck) {
621
665
  deliver(() => stream.onAck({ conversationId: named, text: data.text }));
622
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
+ }
623
675
  else if (event === 'venue_availability' && stream.onAvailability && str(data.conversation_id) && Array.isArray(data.items)) {
624
676
  const items = data.items.filter(isRecord).flatMap((i) => {
625
677
  const id = str(i.id);
@@ -629,6 +681,18 @@ async function readStreamedTurn(res, stream, timeoutMs) {
629
681
  if (items.length)
630
682
  deliver(() => stream.onAvailability({ conversationId: data.conversation_id, items }));
631
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
+ }
632
696
  else if (event === 'merchant' && stream.onMerchantStatus) {
633
697
  const update = merchantStatusOf(data);
634
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,7 +4,7 @@ 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 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';
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';
@@ -21,5 +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';
25
26
  export { cardSizeOf, type BuyCardSize } from './card-size.js';
package/dist/index.js CHANGED
@@ -20,5 +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';
24
25
  export { cardSizeOf } from './card-size.js';
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.9",
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",