@agent-cards/commerce-sdk 0.18.5 → 0.18.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js CHANGED
@@ -8,11 +8,14 @@
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 { planOf } from './plan.js';
12
+ import { merchantStatusOf, searchOf } from './search-status.js';
11
13
  import { isRecord } from './verify.js';
14
+ import { failedPartyOf, fulfilledByField } from './fulfilled-by.js';
12
15
  import { listenToAgentcard } from './listen.js';
13
16
  import { messageInChat } from './shopper-message.js';
14
17
  /** The handlers that ask Agentcard to stream a turn. */
15
- const STREAM_HANDLERS = ['onProgress', 'onStatus', 'onFound', 'onCatalog', 'onSegment', 'onAck', 'onCart', 'onDelta', 'onAvailability', 'onReplyWritten'];
18
+ const STREAM_HANDLERS = ['onProgress', 'onStatus', 'onFound', 'onCatalog', 'onSegment', 'onAck', 'onCart', 'onDelta', 'onAvailability', 'onReplyWritten', 'onMerchantStatus'];
16
19
  const PROGRESS_STAGES = new Set(['searching', 'building_cart', 'checking_out', 'working']);
17
20
  export const DEFAULT_BASE_URL = 'https://api.agentcard.sh';
18
21
  /** The wait for one purchase turn: past Agentcard's own 110-second budget, so a slow turn answers `turn_timeout` rather than aborting here. */
@@ -58,6 +61,15 @@ const TRACEPARENT = /^[\da-f]{2}-[\da-f]{32}-[\da-f]{16}-[\da-f]{2}$/;
58
61
  // A tracestate is printable ASCII of at most 512 characters (W3C); any other value is left off, so it can never stop a purchase.
59
62
  const TRACESTATE = /^[\x20-\x7e]{1,512}$/;
60
63
  const pickTrace = (c) => typeof c.tracestate === 'string' && TRACESTATE.test(c.tracestate) ? { traceparent: c.traceparent, tracestate: c.tracestate } : { traceparent: c.traceparent };
64
+ /** An answer's code request, from its `browser_checkout` when that checkout waits on a wallet's code; null otherwise. */
65
+ export function codeRequestOf(value) {
66
+ if (!isRecord(value) || value.status !== 'needs_code' || !str(value.store) || !isRecord(value.code))
67
+ return null;
68
+ const { wallet, channel, destination } = value.code;
69
+ if ((wallet !== 'link' && wallet !== 'shop_pay') || (channel !== 'text' && channel !== 'email'))
70
+ return null;
71
+ return { store: value.store, wallet, channel, destination: str(destination) };
72
+ }
61
73
  const NEXT_ACTIONS = new Set(['shipping_details', 'guest_details', 'store_login', 'add_card', 'confirm', 'verification_code', 'approve']);
62
74
  /**
63
75
  * A refusal, or a failure to reach Agentcard. `code` is the API's own
@@ -145,6 +157,23 @@ function feeLinesOf(v) {
145
157
  const lines = v.filter(isRecord).flatMap((f) => (str(f.label) && num(f.amountCents) !== null ? [{ label: f.label, amountCents: f.amountCents }] : []));
146
158
  return lines.length ? lines : null;
147
159
  }
160
+ const HOLD_REASONS = new Set(['flight', 'hotel', 'checkout_estimate']);
161
+ function holdOf(v) {
162
+ if (!isRecord(v) || typeof v.reason !== 'string' || !HOLD_REASONS.has(v.reason))
163
+ return null;
164
+ const amount = num(v.amountCents);
165
+ return amount !== null && amount > 0 ? { amountCents: amount, reason: v.reason } : null;
166
+ }
167
+ function discountsOf(v) {
168
+ if (!Array.isArray(v))
169
+ return [];
170
+ return v.filter(isRecord).flatMap((d) => {
171
+ const amount = num(d.amountCents);
172
+ if (!str(d.label) || amount === null || !(amount < 0))
173
+ return [];
174
+ return [d.kind === 'coupon' ? { label: d.label, amountCents: amount, kind: 'coupon', code: str(d.code) } : { label: d.label, amountCents: amount, kind: 'discount', code: null }];
175
+ });
176
+ }
148
177
  function feesOf(v) {
149
178
  const amount = num(v.serviceFeesCents);
150
179
  const status = v.merchantFeesPending === true || v.serviceFeesStatus === 'pending' ? 'pending'
@@ -173,16 +202,20 @@ function cartOf(v) {
173
202
  product_id: str(i.product_id),
174
203
  ref: str(i.ref),
175
204
  url: pageUrl(i.url),
205
+ ...fulfilledByField(i),
176
206
  };
177
207
  }) : [],
178
208
  subtotalCents: num(v.subtotalCents),
179
209
  ...feesOf(v),
180
210
  merchantFeeLines: feeLinesOf(v.merchantFeeLines),
211
+ discounts: discountsOf(v.discounts),
212
+ pricing: v.pricing === true,
181
213
  tipCents: num(v.tipCents) ?? 0,
182
214
  totalCents: num(v.totalCents) ?? 0,
183
215
  totalIsEstimate: v.totalIsEstimate === true,
184
216
  currency: str(v.currency),
185
217
  approvedCeilingCents: num(v.approvedCeilingCents),
218
+ hold: holdOf(v.hold),
186
219
  hash: v.hash,
187
220
  };
188
221
  }
@@ -225,6 +258,7 @@ function catalogItemsOf(v) {
225
258
  store: storeOf(i.store),
226
259
  url: pageUrl(i.url),
227
260
  ...availabilityField(i),
261
+ ...fulfilledByField(i),
228
262
  };
229
263
  }) : [];
230
264
  }
@@ -285,6 +319,7 @@ export function turnOf(body) {
285
319
  approval_url: str(body.approval_url),
286
320
  charge_status: str(body.charge_status),
287
321
  error_code: str(body.error_code),
322
+ fulfilled_by: failedPartyOf(body.placements),
288
323
  authorization_id: str(body.authorization_id),
289
324
  merchant_total_cents: num(body.merchant_total_cents),
290
325
  merchant_currency: str(body.merchant_currency),
@@ -297,11 +332,14 @@ export function turnOf(body) {
297
332
  store_login_expires_at: str(body.store_login_expires_at),
298
333
  store_login_merchant_name: str(body.store_login_merchant_name),
299
334
  next_action: typeof body.next_action === 'string' && NEXT_ACTIONS.has(body.next_action) ? body.next_action : null,
335
+ code_request: codeRequestOf(body.browser_checkout),
300
336
  pick: isRecord(body.pick) && str(body.pick.product_id) ? { product_id: body.pick.product_id, ref: str(body.pick.ref), in_cart: body.pick.in_cart === true } : null,
301
337
  stores: storesOf(body.stores),
302
338
  store_pick: isRecord(body.store_pick) && str(body.store_pick.store_id) ? { store_id: body.store_pick.store_id, name: str(body.store_pick.name), selected: body.store_pick.selected === true } : null,
303
339
  cart_building: cartBuildingOf(body.cart_building),
304
340
  choice: choiceOf(body.choice),
341
+ plan: planOf(body.plan),
342
+ search: searchOf(body.search),
305
343
  raw: body,
306
344
  };
307
345
  }
@@ -358,9 +396,23 @@ function conversationOf(body) {
358
396
  placed_at: str(o.placed_at),
359
397
  })) : [],
360
398
  carts: cartsOf(body.carts),
399
+ latest_reply: latestReplyOf(body.latest_reply),
361
400
  raw: body,
362
401
  };
363
402
  }
403
+ /** The reply a conversation read hands back, or null when it names no text. */
404
+ function latestReplyOf(value) {
405
+ if (!isRecord(value) || !str(value.text)?.trim())
406
+ return null;
407
+ const messages = Array.isArray(value.messages) ? value.messages.filter((m) => typeof m === 'string' && m.trim() !== '') : [];
408
+ return {
409
+ message_id: str(value.message_id) ?? '',
410
+ text: value.text,
411
+ messages: messages.length > 0 ? messages : [value.text],
412
+ catalog: catalogOf(value.catalog),
413
+ created_at: str(value.created_at),
414
+ };
415
+ }
364
416
  /** An error code as the API spells one: lower-case words joined by underscores. */
365
417
  const CODE = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)+$/;
366
418
  /**
@@ -464,7 +516,7 @@ async function eachEvent(res, onEvent) {
464
516
  }
465
517
  /**
466
518
  * A streamed purchase turn, read to its `envelope`: the same answer a JSON turn returns, with its
467
- * status in `http_status`. Each `progress`, `status`, `delta`, `catalog`, `find`, `segment`, `reply_written`, `cart` and `venue_availability` goes to its handler
519
+ * status in `http_status`. Each `progress`, `status`, `delta`, `catalog`, `find`, `segment`, `reply_written`, `cart`, `venue_availability` and `merchant` goes to its handler
468
520
  * on the way, and any other event, such as a `ping` or a `tool`, is read past. A stream that breaks or
469
521
  * ends before its envelope, once it named the conversation, reads as a turn past its budget
470
522
  * (`turn_timeout`), so the tool reads the conversation for what the turn left instead of losing it.
@@ -558,6 +610,11 @@ async function readStreamedTurn(res, stream, timeoutMs) {
558
610
  if (items.length)
559
611
  deliver(() => stream.onAvailability({ conversationId: data.conversation_id, items }));
560
612
  }
613
+ else if (event === 'merchant' && stream.onMerchantStatus) {
614
+ const update = merchantStatusOf(data);
615
+ if (update)
616
+ deliver(() => stream.onMerchantStatus(update));
617
+ }
561
618
  else if (event === 'cart' && stream.onCart && str(data.conversation_id)) {
562
619
  const update = { conversationId: data.conversation_id, cart: cartOf(data.cart), carts: cartsOf(data.carts) };
563
620
  deliver(() => stream.onCart(update));
@@ -52,7 +52,8 @@ export const BUY_TOOL_RULES = [
52
52
  'Name each link by what it is for, from `next_action`: the link to add a card, to sign in to the store, or to approve the payment.',
53
53
  'Within a step, ask for one detail per message, such as the delivery address before the name for the label.',
54
54
  'Write every sentence in your own style, casing included, even where `reply` is written another way.',
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 and Best Buy, so name those, and no other store unless a result showed it.",
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
+ "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.",
56
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
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".',
58
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.',
package/dist/direct.d.ts CHANGED
@@ -36,6 +36,14 @@ export interface DirectContext {
36
36
  productId: string | null;
37
37
  name: string;
38
38
  }>;
39
+ /**
40
+ * Every product card an earlier answer of this conversation showed, each once, newest first. A card's tap whose name no card
41
+ * of `tappable` holds is read against these, so a tap on a card two answers back still sends its `product_id`.
42
+ */
43
+ shownBefore?: ReadonlyArray<{
44
+ productId: string | null;
45
+ name: string;
46
+ }>;
39
47
  /** The stores Agentcard's last answer showed, with the `store_id` a pick sends. */
40
48
  stores?: ReadonlyArray<{
41
49
  storeId: string;
package/dist/direct.js CHANGED
@@ -185,14 +185,25 @@ function pickOf(raw, bare, context) {
185
185
  if (named.length > 1)
186
186
  return call({});
187
187
  }
188
+ // A tap on a card an earlier answer showed, after an answer that showed no cards: it is read against every card shown before.
189
+ const tapNow = byName.length === 0 ? tappedName(raw) : null;
190
+ if (tapNow !== null) {
191
+ const before = (context.shownBefore ?? []).filter((product) => product.productId && [nameWords(tapNow), nameWords(`the ${tapNow}`)].includes(nameWords(product.name)));
192
+ if (before.length > 0)
193
+ return call(new Set(before.map((product) => product.productId)).size === 1 ? { product_id: before[0].productId } : {});
194
+ }
188
195
  if (byName.length > 0) {
189
196
  // A card's tap names the product whole, `I'll take the Burrito Bowl ($15.15)`: the product of exactly that name is the pick,
190
197
  // never the Burrito whose one word it shares, and a tap that names no product here (a card this list does not hold), or two
191
198
  // alike, sends none, so Agentcard reads the words against every card it showed.
192
199
  const tap = tappedName(raw);
193
200
  if (tap !== null) {
194
- const exact = byName.filter((product) => [nameWords(tap), nameWords(`the ${tap}`)].includes(nameWords(product.name)));
195
- return call(exact.length === 1 ? { product_id: exact[0].productId } : {});
201
+ const named = (cards) => cards.filter((product) => [nameWords(tap), nameWords(`the ${tap}`)].includes(nameWords(product.name)));
202
+ const exact = named(byName);
203
+ // A tap on a card an earlier answer showed names a card of no newer list: it is read against every card shown before.
204
+ const before = exact.length === 0 ? named(context.shownBefore ?? []).filter((product) => product.productId) : [];
205
+ const card = exact.length === 1 ? exact[0] : exact.length === 0 && new Set(before.map((product) => product.productId)).size === 1 ? before[0] : null;
206
+ return call(card ? { product_id: card.productId } : {});
196
207
  }
197
208
  // A product's whole name inside the message: `I want the KODAK AA Alkaline Batteries (24 Pack)`.
198
209
  const said = nameWords(raw);
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Who sells or books a product, where that is not the place its card names: a ticket "via StubHub", an event's tickets
3
+ * "sold by Gomry", a table "booked with OpenTable". Agentcard sends the party on a card, a cart line and a failed
4
+ * purchase as `fulfilled_by: { name, kind }`, and this file reads it and words the one quiet line an app shows under the
5
+ * price. A shopper who reads the party tells one seller's failure from another's, and from Agentcard's.
6
+ *
7
+ * Agentcard names only a party the shopper deals with, never the service behind it, and only where the store the card
8
+ * names does not sell it itself: a retail or Shopify card carries none.
9
+ */
10
+ /** How the party relates to the product: a ticket marketplace, the seller, a table's or class's booking provider, an airline or a hotel. */
11
+ export type BuyFulfilledByKind = 'marketplace' | 'seller' | 'booking_provider' | 'airline' | 'hotel';
12
+ /** The party a shopper deals with for one card, cart line or failed purchase. `line` is the words to show, such as `via StubHub`. */
13
+ export interface BuyFulfilledBy {
14
+ name: string;
15
+ kind: BuyFulfilledByKind;
16
+ line: string;
17
+ }
18
+ /** The quiet line for a party: `via StubHub`, `sold by Gomry`, `booked with OpenTable`, `flown by Delta`. */
19
+ export declare function fulfilledByLine(party: {
20
+ name: string;
21
+ kind: BuyFulfilledByKind;
22
+ }): string;
23
+ /** `fulfilled_by` as Agentcard sent it, or null when it sent none, or a kind this version cannot word. */
24
+ export declare function fulfilledByOf(v: unknown): BuyFulfilledBy | null;
25
+ /** `{ fulfilled_by }` when Agentcard sent a party, else nothing: a card or a line with no party carries no field. */
26
+ export declare function fulfilledByField(v: Record<string, unknown>): {
27
+ fulfilled_by?: BuyFulfilledBy;
28
+ };
29
+ /**
30
+ * The party of the first purchase an answer says failed (`placements[].fulfilled_by` beside an `error_code`), or null when
31
+ * that purchase names none: the turn's `error_code` is that first failure's, so a later failed purchase's party never
32
+ * stands beside it.
33
+ */
34
+ export declare function failedPartyOf(placements: unknown): BuyFulfilledBy | null;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Who sells or books a product, where that is not the place its card names: a ticket "via StubHub", an event's tickets
3
+ * "sold by Gomry", a table "booked with OpenTable". Agentcard sends the party on a card, a cart line and a failed
4
+ * purchase as `fulfilled_by: { name, kind }`, and this file reads it and words the one quiet line an app shows under the
5
+ * price. A shopper who reads the party tells one seller's failure from another's, and from Agentcard's.
6
+ *
7
+ * Agentcard names only a party the shopper deals with, never the service behind it, and only where the store the card
8
+ * names does not sell it itself: a retail or Shopify card carries none.
9
+ */
10
+ const LEAD = {
11
+ marketplace: 'via',
12
+ seller: 'sold by',
13
+ booking_provider: 'booked with',
14
+ airline: 'flown by',
15
+ hotel: 'booked with',
16
+ };
17
+ /** The quiet line for a party: `via StubHub`, `sold by Gomry`, `booked with OpenTable`, `flown by Delta`. */
18
+ export function fulfilledByLine(party) {
19
+ return `${LEAD[party.kind]} ${party.name}`;
20
+ }
21
+ /** `fulfilled_by` as Agentcard sent it, or null when it sent none, or a kind this version cannot word. */
22
+ export function fulfilledByOf(v) {
23
+ if (typeof v !== 'object' || v === null)
24
+ return null;
25
+ const { name, kind } = v;
26
+ if (typeof name !== 'string' || !name.trim() || typeof kind !== 'string' || !Object.hasOwn(LEAD, kind))
27
+ return null;
28
+ const party = { name: name.trim(), kind: kind };
29
+ return { ...party, line: fulfilledByLine(party) };
30
+ }
31
+ /** `{ fulfilled_by }` when Agentcard sent a party, else nothing: a card or a line with no party carries no field. */
32
+ export function fulfilledByField(v) {
33
+ const party = fulfilledByOf(v.fulfilled_by);
34
+ return party ? { fulfilled_by: party } : {};
35
+ }
36
+ /**
37
+ * The party of the first purchase an answer says failed (`placements[].fulfilled_by` beside an `error_code`), or null when
38
+ * that purchase names none: the turn's `error_code` is that first failure's, so a later failed purchase's party never
39
+ * stands beside it.
40
+ */
41
+ export function failedPartyOf(placements) {
42
+ if (!Array.isArray(placements))
43
+ return null;
44
+ for (const p of placements) {
45
+ if (typeof p !== 'object' || p === null)
46
+ continue;
47
+ const { error_code: code, fulfilled_by: party } = p;
48
+ if (typeof code === 'string' && code)
49
+ return fulfilledByOf(party);
50
+ }
51
+ return null;
52
+ }
package/dist/index.d.ts CHANGED
@@ -3,17 +3,22 @@ export { readPurchaseMessage, type PurchaseMessage, type PurchaseMessageKind, ty
3
3
  export { readShopperMessage, isStale, SHOPPER_EVENT_TYPES, NEXT_STEP_KINDS, type NextStepKind, type ShopperMessage, type ShopperMessageKind, type ShopperFacts, type ShopperEventType, type OrderStatus, } from './shopper-message.js';
4
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
- export { AgentcardClient, AgentcardApiError, BUY_TURN_TIMEOUT_MS, DEFAULT_BASE_URL, type AgentcardClientOptions, type AgentcardTraceOptions, type OtelApi, type TraceContext, type FetchLike, type FetchLikeResponse, type BuyProgress, type BuyProgressStage, type BuyCatalogUpdate, type BuyAvailability, type BuyAvailabilityUpdate, type BuyFoundUpdate, type BuyTurnStatus, type BuySegmentUpdate, type BuyCartUpdate, type BuyDeltaUpdate, type BuyStreamHandlers, type Shopper, type BuyLastAnswer, type BuyRequest, type BuyDeliveryAddress, type BuyTurn, type BuyStatus, type BuyCart, type BuyCartBuilding, type BuyCartLine, type BuyCartFeeLine, type BuyStore, type BuyCatalog, type BuyCatalogItem, type BuyFlightItinerary, type BuyPaymentSource, type BuyConversation, type BuyLastCheckout, type BuyOrder, type VaultLink, type BuyNextAction, type BuyPick, type BuyStoreChoice, type BuyStorePick, type BuyStores, type BuyChoice, type BuyChoiceGroup, type BuyChoiceDelivery, type BuyChoiceOption, } from './client.js';
6
+ export { searchAt, isMerchantStatusAt, 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, 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
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';
8
9
  export { toMessages, messageStream, typingDelay, type MessagePace, type MessageStream, type MessageStreamOptions, } from './messages.js';
9
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';
10
11
  export { instantAck, instantReaction, ackFor, keepsYes, ACK_REACTION, type AckForOptions } from './ack.js';
11
12
  export { directCall, isProductAsk, type DirectCall, type DirectContext, type DirectReason } from './direct.js';
12
- export { forModel, forModelError, stepAsWritten, withoutCardList, stripLinks, type BuyToolModelView, type BuyToolModelCart, type BuyToolModelProduct, type BuyToolModelStore, type ShownCard, } from './model-view.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
14
  export { asBullets, choiceLines, replyAsWritten, textingStyle, withChoice, type ReplyAsWrittenOptions } from './texting.js';
15
+ export { appInstructions, APP_INSTRUCTIONS_VERSION, type AppInstructionsOptions } from './app-instructions.js';
14
16
  export { toAnthropicTool, toOpenRouterTool, type AnthropicBuyTool, type OpenRouterToolkit, type ZodLike, } from './adapters.js';
15
17
  export { listenToAgentcard, type AgentcardListener, type ListenOptions, type ListenCursorStore, type StreamFetch, } from './listen.js';
16
18
  export { ACTION_LINK_ENDINGS, ACTION_LINK_KINDS, actionLinkId, actionLinkOutcome, isActionLink, type ActionLinkKind, type ActionLinkMessage, type ActionLinkOutcome, } from './action-links.js';
17
19
  export { AGENTCARD_EVENT_TYPES, type AgentcardEventType, } from './events.js';
18
20
  export { turnStream, readTurn, TURN_RECORD_TTL_SECONDS, type TurnStream, type TurnStreamOptions } from './turn-stream.js';
19
21
  export { TURN_RECOVERY_ON, isTurnId, type TurnEvent, type TurnRecord, type TurnRecordStore } from './turn-record.js';
22
+ export type { BuyLateResults, BuyStoreWait } from './store-wait.js';
23
+ export { planOf, type BuyPlan, type BuyPlanItem, type BuyPlanItemState, type BuyPlanStatus } from './plan.js';
24
+ export { fulfilledByLine, fulfilledByOf, type BuyFulfilledBy, type BuyFulfilledByKind } from './fulfilled-by.js';
package/dist/index.js CHANGED
@@ -3,17 +3,21 @@ export { readPurchaseMessage, } from './message.js';
3
3
  export { readShopperMessage, isStale, SHOPPER_EVENT_TYPES, NEXT_STEP_KINDS, } from './shopper-message.js';
4
4
  export { ORDER_STEPS, updateOrder, arrivalText, } from './order.js';
5
5
  export { agentcardWebhook, purchaseAgentWebhook, } from './handler.js';
6
+ export { searchAt, isMerchantStatusAt, } from './search-status.js';
6
7
  export { AgentcardClient, AgentcardApiError, BUY_TURN_TIMEOUT_MS, DEFAULT_BASE_URL, } from './client.js';
7
8
  export { BUY_TOOL_NAME, BUY_TOOL_DESCRIPTION, buyToolDefinition, parseBuyToolInput, } from './definition.js';
8
9
  export { toMessages, messageStream, typingDelay, } from './messages.js';
9
10
  export { buyTool, nextStepPrompt, productAction, } from './buy-tool.js';
10
11
  export { instantAck, instantReaction, ackFor, keepsYes, ACK_REACTION } from './ack.js';
11
12
  export { directCall, isProductAsk } from './direct.js';
12
- export { forModel, forModelError, stepAsWritten, withoutCardList, stripLinks, } from './model-view.js';
13
+ export { forModel, grandTotals, forModelError, stepAsWritten, withoutCardList, stripLinks, } from './model-view.js';
13
14
  export { asBullets, choiceLines, replyAsWritten, textingStyle, withChoice } from './texting.js';
15
+ export { appInstructions, APP_INSTRUCTIONS_VERSION } from './app-instructions.js';
14
16
  export { toAnthropicTool, toOpenRouterTool, } from './adapters.js';
15
17
  export { listenToAgentcard, } from './listen.js';
16
18
  export { ACTION_LINK_ENDINGS, ACTION_LINK_KINDS, actionLinkId, actionLinkOutcome, isActionLink, } from './action-links.js';
17
19
  export { AGENTCARD_EVENT_TYPES, } from './events.js';
18
20
  export { turnStream, readTurn, TURN_RECORD_TTL_SECONDS } from './turn-stream.js';
19
21
  export { TURN_RECOVERY_ON, isTurnId } from './turn-record.js';
22
+ export { planOf } from './plan.js';
23
+ export { fulfilledByLine, fulfilledByOf } from './fulfilled-by.js';
package/dist/message.d.ts CHANGED
@@ -10,6 +10,7 @@ import { type AgentcardEvent } from './verify.js';
10
10
  * - `order_failed`: the order failed after the approval, and nothing more happens for that approval.
11
11
  * - `order_not_placed_yet`: the shopper approved, and the order is not placed yet.
12
12
  * - `order_unconfirmed`: the store's reply to the order was lost, so the order may exist; Agentcard is confirming it.
13
+ * - `order_placing`: the shopper approved, and Agentcard is placing the order now; one of the order kinds above follows.
13
14
  * - `approval_expiring`: the approval link expires soon; `text` carries the link.
14
15
  * - `approval_expired`: the approval expired before the shopper answered, and nothing was charged.
15
16
  * - `approval_declined`: the shopper declined the approval, and nothing was charged.
@@ -25,7 +26,7 @@ import { type AgentcardEvent } from './verify.js';
25
26
  * - `cart_needs_input`: that checkout asks the shopper something first, such as a size, a delivery address, a sign-in to the store or an approval of the most the order can cost.
26
27
  * - `cart_failed`: that checkout built no cart, and nothing was ordered or charged.
27
28
  */
28
- export type PurchaseMessageKind = 'order_placed' | 'order_failed' | 'order_not_placed_yet' | 'order_unconfirmed' | 'approval_expiring' | 'approval_expired' | 'approval_declined' | 'payment_declined' | 'card_link_unfinished' | 'card_link_expired' | 'cart_idle' | 'card_added' | 'store_connected' | 'store_connect_failed' | 'store_connect_expired' | 'cart_ready' | 'cart_needs_input' | 'cart_failed';
29
+ export type PurchaseMessageKind = 'order_placed' | 'order_failed' | 'order_not_placed_yet' | 'order_unconfirmed' | 'order_placing' | 'approval_expiring' | 'approval_expired' | 'approval_declined' | 'payment_declined' | 'card_link_unfinished' | 'card_link_expired' | 'cart_idle' | 'card_added' | 'store_connected' | 'store_connect_failed' | 'store_connect_expired' | 'cart_ready' | 'cart_needs_input' | 'cart_failed';
29
30
  /** The message's values one by one, for a company that writes its own copy. A fact that does not apply is absent. */
30
31
  export interface PurchaseFacts {
31
32
  merchantName?: string;
@@ -7,14 +7,15 @@
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 { BuyNextAction } from './client.js';
10
+ import type { BuyCart, 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;
14
14
  /**
15
15
  * `price` is one unit's; `line_total` is what the line costs with its add-ons and quantity, and the
16
16
  * lines' totals add up before `fees`. `options` are the choices made on the line and `variant` the
17
- * variant it is. `product_id` is the id the line's product carries in `products`, or null.
17
+ * variant it is. `product_id` is the id the line's product carries in `products`, or null. `fulfilled_by` is who
18
+ * sells or books the line where that is not the store, such as `via StubHub`; absent when Agentcard named none.
18
19
  */
19
20
  items: Array<{
20
21
  product_id: string | null;
@@ -24,6 +25,7 @@ export interface BuyToolModelCart {
24
25
  line_total: string | null;
25
26
  options: string[];
26
27
  variant: string | null;
28
+ fulfilled_by?: string;
27
29
  }>;
28
30
  fees: string | null;
29
31
  fees_status?: 'known' | 'pending' | 'unknown';
@@ -32,6 +34,19 @@ export interface BuyToolModelCart {
32
34
  label: string;
33
35
  amount: string;
34
36
  }>;
37
+ /**
38
+ * The discounts and coupons inside `total`, each negative, which `fee_lines` also lists: show them as money taken off,
39
+ * never add them to `total` again. `code` names a coupon's code. Absent when the cart has none.
40
+ */
41
+ discounts?: Array<{
42
+ label: string;
43
+ amount: string;
44
+ amount_cents: number;
45
+ kind: 'discount' | 'coupon';
46
+ code: string | null;
47
+ }>;
48
+ /** True while the store's checkout prices this cart, several stores at once included, so `total` is not its price yet; absent once it is priced. */
49
+ pricing?: true;
35
50
  tip: string | null;
36
51
  total: string;
37
52
  /** True when the store has not priced its fees and tax yet: `total` is an estimate, never the final amount. */
@@ -40,6 +55,16 @@ export interface BuyToolModelCart {
40
55
  total_leaves_out: string | null;
41
56
  /** The most the shopper's card can be held for this cart: above `total` at a store that prices its final at checkout, `total` itself for a flight, which holds exactly that; else null. */
42
57
  max_charge: string | null;
58
+ /**
59
+ * Why the card is held for `max_charge`: `flight` or `hotel` for a booking's hold on the card, which the shopper sees
60
+ * on their statement, and `checkout_estimate` for the headroom over a store's total, released unused. Show a hold line
61
+ * for `flight` and `hotel` alone. Absent when nothing is held.
62
+ */
63
+ hold?: {
64
+ amount: string;
65
+ amount_cents: number;
66
+ reason: 'flight' | 'hotel' | 'checkout_estimate';
67
+ };
43
68
  }
44
69
  export interface BuyToolModelProduct {
45
70
  /** What the model passes as `product_id` when the shopper picks this product: Agentcard's ref, never a link. Null when there is none. */
@@ -60,6 +85,22 @@ export interface BuyToolModelStore {
60
85
  /** True when the shopper holds this store's page, which `sendLink` delivered beside the reply. */
61
86
  link_sent: boolean;
62
87
  }
88
+ /**
89
+ * What every store cart on the table adds up to, in one currency: the stores' totals summed in minor units, each store's
90
+ * tip, fees and tax counted once as its `total` counts them. `is_estimate` is true when any store's total is an estimate,
91
+ * and `leaves_out` names what the estimated totals leave out together; null when every store priced its cart.
92
+ */
93
+ export interface BuyToolGrandTotal {
94
+ amount: string;
95
+ /** The sum in the currency's minor unit, such as cents. */
96
+ amount_cents: number;
97
+ /** The ISO 4217 code, upper case, or null when Agentcard named none for these carts. */
98
+ currency: string | null;
99
+ is_estimate: boolean;
100
+ leaves_out: string | null;
101
+ /** How many store carts this total adds up. */
102
+ stores: number;
103
+ }
63
104
  /** The buy tool's answer as the model reads it. */
64
105
  export interface BuyToolModelView {
65
106
  status: BuyToolStatus;
@@ -78,6 +119,8 @@ export interface BuyToolModelView {
78
119
  cart: BuyToolModelCart | null;
79
120
  /** Every cart on the table, the newest last, when the shopper is buying from more than one store. */
80
121
  carts: BuyToolModelCart[];
122
+ /** What the carts add up to, one entry per currency, in the order the carts first name it; empty when no cart is on the table. */
123
+ grand_totals: BuyToolGrandTotal[];
81
124
  products: BuyToolModelProduct[];
82
125
  approval_expires_at: string | null;
83
126
  /**
@@ -197,6 +240,11 @@ export declare function cardKey(product: Pick<BuyToolProduct, 'id' | 'ref' | 'ur
197
240
  * and a place's card is where the shopper reads whether it has room.
198
241
  */
199
242
  export declare function productCardKey(product: Pick<BuyToolProduct, 'id' | 'ref' | 'url' | 'flight' | 'availability'>): string | null;
243
+ /**
244
+ * The carts' grand totals, one per currency, summed from each cart's `totalCents`, which already holds its tip, fees and
245
+ * tax: a display string is never parsed. Carts in two currencies are never added together.
246
+ */
247
+ export declare function grandTotals(carts: readonly BuyCart[]): BuyToolGrandTotal[];
200
248
  /** How long until `iso`, from `now`, as the shopper reads it: `about 24 hours`, `about 15 minutes`, `no longer`, or null. */
201
249
  export declare function expiresIn(iso: string | null | undefined, now?: number): string | null;
202
250
  /**
@@ -243,9 +291,11 @@ export interface ShownCard {
243
291
  * points at a number the list gave (`the dishwand in #8`), a sentence that goes on to more of the list (`other 6-packs
244
292
  * are ...`, `there's also ...`) and names a card, and, past the one or two products a reply recommends in words, each
245
293
  * further sentence that names another card. A list written on one line goes the same way, item by item (inlineList),
246
- * and so does a list line written without a bullet, whole, up to the question on it. What is left is the reply around
247
- * the list, such as `found these at amazon.` and `which one do you want?`. A reply whose cards went out names none of
248
- * them twice over. A list that keeps an item for a product no card shows keeps its lead-in, and the two read as one
294
+ * and so does a list line written without a bullet, whole, up to the question on it. The lines set in under a list
295
+ * line that goes, its details such as ` - ships from London`, go with it, and once the list went, a sentence that only
296
+ * points back at it, `both are priced in foreign currency`, goes when the cards show another number than it counts.
297
+ * What is left is the reply around the list, such as `found these at amazon.` and `which one do you want?`. A reply
298
+ * whose cards went out names none of them twice over. A list that keeps an item for a product no card shows keeps its lead-in, and the two read as one
249
299
  * message (toMessages), so the shopper never reads `two options came in under $20:` on its own.
250
300
  *
251
301
  * Each card is its name, or its name and its price as the model reads it (`{ name, price: '$22.89' }`): with the price,