@agent-cards/commerce-sdk 0.18.4 → 0.18.6

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.d.ts CHANGED
@@ -1,5 +1,16 @@
1
+ /**
2
+ * The client the buy tool calls Agentcard with. A company builds one from its
3
+ * client credentials, and every shopper's tool shares the one access token it
4
+ * exchanges them for; or a tool runs on one shopper's own access token. The
5
+ * global fetch is the only transport, so the package keeps no dependencies.
6
+ *
7
+ * The answer fields and the error codes follow the buy module of
8
+ * @agent-cards/sdk (packages/checkout/src/buy.ts: turnOf, buyError), and the
9
+ * token cache follows its client (packages/checkout/src/client.ts accessToken).
10
+ */
11
+ import { type BuyPlan } from './plan.js';
1
12
  import { type AgentcardListener, type ListenOptions } from './listen.js';
2
- import type { ShopperMessage } from './shopper-message.js';
13
+ import { type ShopperMessage } from './shopper-message.js';
3
14
  /** The fetch the client calls: Node 18 and later have one built in. */
4
15
  export type FetchLike = (url: string, init: {
5
16
  method: string;
@@ -144,6 +155,13 @@ export interface BuyStreamHandlers {
144
155
  onDelta?: (update: BuyDeltaUpdate) => void | Promise<void>;
145
156
  /** Whether the places the turn's cards show have room, once Agentcard's live search says, after the cards it changes. */
146
157
  onAvailability?: (update: BuyAvailabilityUpdate) => void | Promise<void>;
158
+ /**
159
+ * The reply is written, after its messages, and the turn stays open only for a slower source's cards, such as a second
160
+ * hotel supplier's, and the one message that says what they added. Comes once, or not at all when nothing is still out.
161
+ */
162
+ onReplyWritten?: (update: {
163
+ conversationId: string | null;
164
+ }) => void | Promise<void>;
147
165
  }
148
166
  export declare const DEFAULT_BASE_URL = "https://api.agentcard.sh";
149
167
  /** The wait for one purchase turn: past Agentcard's own 110-second budget, so a slow turn answers `turn_timeout` rather than aborting here. */
@@ -228,6 +246,22 @@ export interface BuyRequest {
228
246
  resume?: true;
229
247
  /** The conversation a confirm of a cart hash belongs to, in place of `resume`. */
230
248
  conversation_id?: string;
249
+ /**
250
+ * Your own id for the chat or thread the shopper wrote in, such as a group chat's id (up to 200 characters). The shopper
251
+ * keeps one open purchase per thread: `resume` continues the newest one in this thread only, and a conversation_id from
252
+ * another thread is refused (409 `thread_mismatch`). Leave it out for an app with one chat per shopper.
253
+ */
254
+ thread_id?: string;
255
+ /**
256
+ * How long, in milliseconds, the reply waits for store searches before Agentcard writes it. Left out, Agentcard keeps
257
+ * its own wait. `buyTool({ storeWaitMs })` sends it on every request.
258
+ */
259
+ store_wait_ms?: number;
260
+ /**
261
+ * What happens to a store that answers after the reply: `stream` sends its cards after the reply in the same streamed
262
+ * turn, before the answer; `hold` keeps them out of the turn. Left out, Agentcard does as it does today.
263
+ */
264
+ late_results?: 'stream' | 'hold';
231
265
  /** Drop the cart on the table and start a new order. */
232
266
  new_order?: true;
233
267
  /**
@@ -529,6 +563,8 @@ export interface BuyTurn {
529
563
  cart_building: BuyCartBuilding | null;
530
564
  /** The choice the reply asks the shopper to make, each group with every value; null when it asks none, and on an older API. */
531
565
  choice: BuyChoice | null;
566
+ /** The checkout plan this answer's yes started or moved, with each item's state; null when none, and on an older API. */
567
+ plan: BuyPlan | null;
532
568
  /** The answer as Agentcard sent it. */
533
569
  raw: Record<string, unknown>;
534
570
  }
@@ -568,8 +604,22 @@ export interface BuyConversation {
568
604
  last_checkout: BuyLastCheckout | null;
569
605
  orders: BuyOrder[];
570
606
  carts: BuyCart[];
607
+ /**
608
+ * The reply Agentcard wrote since the shopper's last message, once no turn runs: the answer a call that ended with
609
+ * `turn_timeout` never got. Null while a turn runs, when no reply was written since, and on an older API.
610
+ */
611
+ latest_reply: BuyLatestReply | null;
571
612
  raw: Record<string, unknown>;
572
613
  }
614
+ /** A reply a conversation read hands back: its text, its messages, and the products it showed. */
615
+ export interface BuyLatestReply {
616
+ message_id: string;
617
+ text: string;
618
+ messages: string[];
619
+ /** The products the reply showed, when the call that asked ended before it was written; null otherwise. */
620
+ catalog: BuyCatalog | null;
621
+ created_at: string | null;
622
+ }
573
623
  /** A Vault link: the page where a shopper connects and adds a card. */
574
624
  export interface VaultLink {
575
625
  id: string;
@@ -615,6 +665,10 @@ export declare class AgentcardClient {
615
665
  private learnedSandbox;
616
666
  /** Each shopper's messages that no history the model ran with holds yet, under your own id for them. */
617
667
  private readonly inbox;
668
+ /** The chats (`threadId`, '' for a tool built without one) that need a kept message about the shopper alone no more: their history holds it, or their agent answered it (`forgetMessages`, `answered`). */
669
+ private readonly savedInChats;
670
+ /** The same marks by event id, for a copy of the message delivered later, such as a retried webhook's: `delivered` starts that copy with them. */
671
+ private readonly savedInChatsById;
618
672
  private readonly deliveredIds;
619
673
  private readonly trace;
620
674
  constructor(options: AgentcardClientOptions);
@@ -659,22 +713,41 @@ export declare class AgentcardClient {
659
713
  delivered(externalUserId: string, message: ShopperMessage): void;
660
714
  /**
661
715
  * Notes that your agent answers `message` with a turn of its own (`buy.nextStep`) instead of posting
662
- * its `text`, so the shopper never reads that text: `delivered` skips it from now on, and no later
663
- * `buy.turn` hands it to the model as words the shopper read. `buy.nextStep` calls it.
716
+ * its `text`, so the shopper never reads that text in that chat (`threadId`, or the tool built
717
+ * without one). A message about one purchase belongs to one chat, so it leaves the client:
718
+ * `delivered` skips it from now on, and no later `buy.turn` hands it to the model as words the
719
+ * shopper read. A message about the shopper alone, such as a saved card, is answered for that chat
720
+ * only: the shopper's other chats still keep it. `buy.nextStep` calls it.
664
721
  */
665
- answered(externalUserId: string, message: ShopperMessage): void;
722
+ answered(externalUserId: string, message: ShopperMessage, threadId?: string): void;
666
723
  /**
667
724
  * Takes back `answered` for a message your agent's own turn could not answer (`BuyToolTurn.release` calls it): once you
668
- * post its text after all, `delivered` keeps it, and a later `buy.turn` hands it to the model as words the shopper read.
725
+ * post its text after all, `delivered` keeps it, and a later `buy.turn` in that chat hands it to the model as words the
726
+ * shopper read.
727
+ */
728
+ unanswered(externalUserId: string, message: ShopperMessage, threadId?: string): void;
729
+ /**
730
+ * The messages kept for this shopper that this chat reads, oldest first: each one about a purchase
731
+ * opened in this chat or about the shopper alone, that reached their chat and that no history
732
+ * `turn.messages` read in this chat has held yet. The chat is `threadId`, or the tool built without
733
+ * one, which reads the purchases opened under no thread. `buy.turn` reads them when it opens.
734
+ */
735
+ keptMessages(externalUserId: string, threadId?: string): ShopperMessage[];
736
+ /**
737
+ * Forgets these messages for this shopper in this chat (`threadId`, or the tool built without one): `turn.messages`
738
+ * calls it once the history it reads holds them. A message about one purchase leaves the client, since one chat reads
739
+ * it. A message about the shopper alone, such as a saved card, stays kept for the shopper's other chats until their
740
+ * own histories hold it.
669
741
  */
670
- unanswered(externalUserId: string, message: ShopperMessage): void;
742
+ forgetMessages(externalUserId: string, messages: readonly ShopperMessage[], threadId?: string): void;
671
743
  /**
672
- * The messages kept for this shopper, oldest first: each one that reached their chat and that no
673
- * history `turn.messages` read has held yet. `buy.turn` reads them when it opens.
744
+ * Marks a message about the shopper alone as one this chat needs no more, its history holding it or its agent having
745
+ * answered it, or takes the mark back. The kept copies with the same event id carry the mark too, and so does a copy
746
+ * delivered later (`delivered`), while the client still remembers the id.
674
747
  */
675
- keptMessages(externalUserId: string): ShopperMessage[];
676
- /** Forgets these messages for this shopper: `turn.messages` calls it once the history it reads holds them. */
677
- forgetMessages(externalUserId: string, messages: readonly ShopperMessage[]): void;
748
+ private markChat;
749
+ /** Removes these messages from the shopper's kept messages, for every chat. */
750
+ private dropKept;
678
751
  /** A company names the shopper exactly once; a shopper's own token already names them. */
679
752
  private checkShopper;
680
753
  private call;
package/dist/client.js CHANGED
@@ -8,10 +8,12 @@
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';
11
12
  import { isRecord } from './verify.js';
12
13
  import { listenToAgentcard } from './listen.js';
14
+ import { messageInChat } from './shopper-message.js';
13
15
  /** The handlers that ask Agentcard to stream a turn. */
14
- const STREAM_HANDLERS = ['onProgress', 'onStatus', 'onFound', 'onCatalog', 'onSegment', 'onAck', 'onCart', 'onDelta', 'onAvailability'];
16
+ const STREAM_HANDLERS = ['onProgress', 'onStatus', 'onFound', 'onCatalog', 'onSegment', 'onAck', 'onCart', 'onDelta', 'onAvailability', 'onReplyWritten'];
15
17
  const PROGRESS_STAGES = new Set(['searching', 'building_cart', 'checking_out', 'working']);
16
18
  export const DEFAULT_BASE_URL = 'https://api.agentcard.sh';
17
19
  /** The wait for one purchase turn: past Agentcard's own 110-second budget, so a slow turn answers `turn_timeout` rather than aborting here. */
@@ -301,6 +303,7 @@ export function turnOf(body) {
301
303
  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,
302
304
  cart_building: cartBuildingOf(body.cart_building),
303
305
  choice: choiceOf(body.choice),
306
+ plan: planOf(body.plan),
304
307
  raw: body,
305
308
  };
306
309
  }
@@ -357,9 +360,23 @@ function conversationOf(body) {
357
360
  placed_at: str(o.placed_at),
358
361
  })) : [],
359
362
  carts: cartsOf(body.carts),
363
+ latest_reply: latestReplyOf(body.latest_reply),
360
364
  raw: body,
361
365
  };
362
366
  }
367
+ /** The reply a conversation read hands back, or null when it names no text. */
368
+ function latestReplyOf(value) {
369
+ if (!isRecord(value) || !str(value.text)?.trim())
370
+ return null;
371
+ const messages = Array.isArray(value.messages) ? value.messages.filter((m) => typeof m === 'string' && m.trim() !== '') : [];
372
+ return {
373
+ message_id: str(value.message_id) ?? '',
374
+ text: value.text,
375
+ messages: messages.length > 0 ? messages : [value.text],
376
+ catalog: catalogOf(value.catalog),
377
+ created_at: str(value.created_at),
378
+ };
379
+ }
363
380
  /** An error code as the API spells one: lower-case words joined by underscores. */
364
381
  const CODE = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)+$/;
365
382
  /**
@@ -463,7 +480,7 @@ async function eachEvent(res, onEvent) {
463
480
  }
464
481
  /**
465
482
  * A streamed purchase turn, read to its `envelope`: the same answer a JSON turn returns, with its
466
- * status in `http_status`. Each `progress`, `status`, `delta`, `catalog`, `find`, `segment`, `cart` and `venue_availability` goes to its handler
483
+ * status in `http_status`. Each `progress`, `status`, `delta`, `catalog`, `find`, `segment`, `reply_written`, `cart` and `venue_availability` goes to its handler
467
484
  * on the way, and any other event, such as a `ping` or a `tool`, is read past. A stream that breaks or
468
485
  * ends before its envelope, once it named the conversation, reads as a turn past its budget
469
486
  * (`turn_timeout`), so the tool reads the conversation for what the turn left instead of losing it.
@@ -542,6 +559,9 @@ async function readStreamedTurn(res, stream, timeoutMs) {
542
559
  else if (event === 'segment' && typeof data.text === 'string' && data.text.trim() !== '' && stream.onSegment) {
543
560
  deliver(() => stream.onSegment({ conversationId: named, text: data.text }));
544
561
  }
562
+ else if (event === 'reply_written' && stream.onReplyWritten) {
563
+ deliver(() => stream.onReplyWritten({ conversationId: named }));
564
+ }
545
565
  else if (event === 'ack' && typeof data.text === 'string' && data.text.trim() !== '' && stream.onAck) {
546
566
  deliver(() => stream.onAck({ conversationId: named, text: data.text }));
547
567
  }
@@ -586,6 +606,10 @@ export class AgentcardClient {
586
606
  learnedSandbox = null;
587
607
  /** Each shopper's messages that no history the model ran with holds yet, under your own id for them. */
588
608
  inbox = new Map();
609
+ /** The chats (`threadId`, '' for a tool built without one) that need a kept message about the shopper alone no more: their history holds it, or their agent answered it (`forgetMessages`, `answered`). */
610
+ savedInChats = new WeakMap();
611
+ /** The same marks by event id, for a copy of the message delivered later, such as a retried webhook's: `delivered` starts that copy with them. */
612
+ savedInChatsById = new Map();
589
613
  deliveredIds = new Set();
590
614
  trace;
591
615
  constructor(options) {
@@ -730,6 +754,10 @@ export class AgentcardClient {
730
754
  this.deliveredIds.delete(this.deliveredIds.values().next().value);
731
755
  }
732
756
  const kept = this.inbox.get(externalUserId) ?? [];
757
+ // A copy delivered again, such as a retried webhook's, keeps the chats that already answered it.
758
+ const marks = str(message.eventId) ? this.savedInChatsById.get(message.eventId) : undefined;
759
+ if (marks)
760
+ this.savedInChats.set(message, new Set(marks));
733
761
  // The shopper written to last moves to the end, so the one dropped first is the one written to longest ago.
734
762
  this.inbox.delete(externalUserId);
735
763
  this.inbox.set(externalUserId, [...kept, message].slice(-DELIVERED_PER_SHOPPER));
@@ -738,14 +766,21 @@ export class AgentcardClient {
738
766
  }
739
767
  /**
740
768
  * Notes that your agent answers `message` with a turn of its own (`buy.nextStep`) instead of posting
741
- * its `text`, so the shopper never reads that text: `delivered` skips it from now on, and no later
742
- * `buy.turn` hands it to the model as words the shopper read. `buy.nextStep` calls it.
769
+ * its `text`, so the shopper never reads that text in that chat (`threadId`, or the tool built
770
+ * without one). A message about one purchase belongs to one chat, so it leaves the client:
771
+ * `delivered` skips it from now on, and no later `buy.turn` hands it to the model as words the
772
+ * shopper read. A message about the shopper alone, such as a saved card, is answered for that chat
773
+ * only: the shopper's other chats still keep it. `buy.nextStep` calls it.
743
774
  */
744
- answered(externalUserId, message) {
775
+ answered(externalUserId, message, threadId) {
745
776
  if (!str(externalUserId))
746
777
  throw new TypeError('Pass externalUserId: your own id for the shopper the message is for.');
747
778
  if (!isRecord(message))
748
779
  throw new TypeError('Pass the message onMessage got.');
780
+ if (!message.threadId && !message.conversationId) {
781
+ this.markChat(externalUserId, message, threadId ?? '', true);
782
+ return;
783
+ }
749
784
  if (str(message.eventId)) {
750
785
  this.deliveredIds.add(message.eventId);
751
786
  if (this.deliveredIds.size > DELIVERED_IDS_KEPT)
@@ -753,31 +788,82 @@ export class AgentcardClient {
753
788
  }
754
789
  const kept = this.inbox.get(externalUserId);
755
790
  if (kept)
756
- this.forgetMessages(externalUserId, kept.filter((m) => m === message || (!!message.eventId && m.eventId === message.eventId)));
791
+ this.dropKept(externalUserId, kept.filter((m) => m === message || (!!message.eventId && m.eventId === message.eventId)));
757
792
  }
758
793
  /**
759
794
  * Takes back `answered` for a message your agent's own turn could not answer (`BuyToolTurn.release` calls it): once you
760
- * post its text after all, `delivered` keeps it, and a later `buy.turn` hands it to the model as words the shopper read.
795
+ * post its text after all, `delivered` keeps it, and a later `buy.turn` in that chat hands it to the model as words the
796
+ * shopper read.
761
797
  */
762
- unanswered(externalUserId, message) {
798
+ unanswered(externalUserId, message, threadId) {
763
799
  if (!str(externalUserId))
764
800
  throw new TypeError('Pass externalUserId: your own id for the shopper the message is for.');
765
801
  if (!isRecord(message))
766
802
  throw new TypeError('Pass the message onMessage got.');
803
+ if (!message.threadId && !message.conversationId) {
804
+ this.markChat(externalUserId, message, threadId ?? '', false);
805
+ return;
806
+ }
767
807
  if (str(message.eventId))
768
808
  this.deliveredIds.delete(message.eventId);
769
809
  }
770
810
  /**
771
- * The messages kept for this shopper, oldest first: each one that reached their chat and that no
772
- * history `turn.messages` read has held yet. `buy.turn` reads them when it opens.
811
+ * The messages kept for this shopper that this chat reads, oldest first: each one about a purchase
812
+ * opened in this chat or about the shopper alone, that reached their chat and that no history
813
+ * `turn.messages` read in this chat has held yet. The chat is `threadId`, or the tool built without
814
+ * one, which reads the purchases opened under no thread. `buy.turn` reads them when it opens.
773
815
  */
774
- keptMessages(externalUserId) {
775
- return [...(this.inbox.get(externalUserId) ?? [])];
816
+ keptMessages(externalUserId, threadId) {
817
+ const chat = threadId ?? '';
818
+ return (this.inbox.get(externalUserId) ?? []).filter((m) => messageInChat(m, threadId) && !this.savedInChats.get(m)?.has(chat));
776
819
  }
777
- /** Forgets these messages for this shopper: `turn.messages` calls it once the history it reads holds them. */
778
- forgetMessages(externalUserId, messages) {
820
+ /**
821
+ * Forgets these messages for this shopper in this chat (`threadId`, or the tool built without one): `turn.messages`
822
+ * calls it once the history it reads holds them. A message about one purchase leaves the client, since one chat reads
823
+ * it. A message about the shopper alone, such as a saved card, stays kept for the shopper's other chats until their
824
+ * own histories hold it.
825
+ */
826
+ forgetMessages(externalUserId, messages, threadId) {
827
+ const chat = threadId ?? '';
828
+ this.dropKept(externalUserId, messages.filter((m) => {
829
+ if (m.threadId || m.conversationId)
830
+ return true;
831
+ this.markChat(externalUserId, m, chat, true);
832
+ return false;
833
+ }));
834
+ }
835
+ /**
836
+ * Marks a message about the shopper alone as one this chat needs no more, its history holding it or its agent having
837
+ * answered it, or takes the mark back. The kept copies with the same event id carry the mark too, and so does a copy
838
+ * delivered later (`delivered`), while the client still remembers the id.
839
+ */
840
+ markChat(externalUserId, message, chat, done) {
841
+ const id = str(message.eventId);
842
+ const mark = (chats) => {
843
+ if (done)
844
+ chats.add(chat);
845
+ else
846
+ chats.delete(chat);
847
+ return chats;
848
+ };
849
+ const same = (m) => m === message || (!!id && m.eventId === id);
850
+ for (const m of new Set([message, ...(this.inbox.get(externalUserId) ?? []).filter(same)])) {
851
+ this.savedInChats.set(m, mark(this.savedInChats.get(m) ?? new Set()));
852
+ }
853
+ if (!id)
854
+ return;
855
+ // The id marked last moves to the end, so the one dropped first is the one marked longest ago. A kept copy keeps its
856
+ // own marks after its id is dropped.
857
+ const chats = mark(this.savedInChatsById.get(id) ?? new Set());
858
+ this.savedInChatsById.delete(id);
859
+ this.savedInChatsById.set(id, chats);
860
+ if (this.savedInChatsById.size > DELIVERED_IDS_KEPT)
861
+ this.savedInChatsById.delete(this.savedInChatsById.keys().next().value);
862
+ }
863
+ /** Removes these messages from the shopper's kept messages, for every chat. */
864
+ dropKept(externalUserId, messages) {
779
865
  const kept = this.inbox.get(externalUserId);
780
- if (!kept)
866
+ if (!kept || messages.length === 0)
781
867
  return;
782
868
  const left = kept.filter((m) => !messages.includes(m));
783
869
  if (left.length > 0)
@@ -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);
package/dist/index.d.ts CHANGED
@@ -3,17 +3,20 @@ 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 { 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 BuyLatestReply, 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';
7
7
  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
8
  export { toMessages, messageStream, typingDelay, type MessagePace, type MessageStream, type MessageStreamOptions, } from './messages.js';
9
9
  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
10
  export { instantAck, instantReaction, ackFor, keepsYes, ACK_REACTION, type AckForOptions } from './ack.js';
11
11
  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';
12
+ export { forModel, grandTotals, forModelError, stepAsWritten, withoutCardList, stripLinks, type BuyToolModelView, type BuyToolModelCart, type BuyToolGrandTotal, type BuyToolModelProduct, type BuyToolModelStore, type ShownCard, } from './model-view.js';
13
13
  export { asBullets, choiceLines, replyAsWritten, textingStyle, withChoice, type ReplyAsWrittenOptions } from './texting.js';
14
+ export { appInstructions, APP_INSTRUCTIONS_VERSION, type AppInstructionsOptions } from './app-instructions.js';
14
15
  export { toAnthropicTool, toOpenRouterTool, type AnthropicBuyTool, type OpenRouterToolkit, type ZodLike, } from './adapters.js';
15
16
  export { listenToAgentcard, type AgentcardListener, type ListenOptions, type ListenCursorStore, type StreamFetch, } from './listen.js';
16
17
  export { ACTION_LINK_ENDINGS, ACTION_LINK_KINDS, actionLinkId, actionLinkOutcome, isActionLink, type ActionLinkKind, type ActionLinkMessage, type ActionLinkOutcome, } from './action-links.js';
17
18
  export { AGENTCARD_EVENT_TYPES, type AgentcardEventType, } from './events.js';
18
19
  export { turnStream, readTurn, TURN_RECORD_TTL_SECONDS, type TurnStream, type TurnStreamOptions } from './turn-stream.js';
19
20
  export { TURN_RECOVERY_ON, isTurnId, type TurnEvent, type TurnRecord, type TurnRecordStore } from './turn-record.js';
21
+ export type { BuyLateResults, BuyStoreWait } from './store-wait.js';
22
+ export { planOf, type BuyPlan, type BuyPlanItem, type BuyPlanItemState, type BuyPlanStatus } from './plan.js';
package/dist/index.js CHANGED
@@ -9,11 +9,13 @@ export { toMessages, messageStream, typingDelay, } from './messages.js';
9
9
  export { buyTool, nextStepPrompt, productAction, } from './buy-tool.js';
10
10
  export { instantAck, instantReaction, ackFor, keepsYes, ACK_REACTION } from './ack.js';
11
11
  export { directCall, isProductAsk } from './direct.js';
12
- export { forModel, forModelError, stepAsWritten, withoutCardList, stripLinks, } from './model-view.js';
12
+ export { forModel, grandTotals, forModelError, stepAsWritten, withoutCardList, stripLinks, } from './model-view.js';
13
13
  export { asBullets, choiceLines, replyAsWritten, textingStyle, withChoice } from './texting.js';
14
+ export { appInstructions, APP_INSTRUCTIONS_VERSION } from './app-instructions.js';
14
15
  export { toAnthropicTool, toOpenRouterTool, } from './adapters.js';
15
16
  export { listenToAgentcard, } from './listen.js';
16
17
  export { ACTION_LINK_ENDINGS, ACTION_LINK_KINDS, actionLinkId, actionLinkOutcome, isActionLink, } from './action-links.js';
17
18
  export { AGENTCARD_EVENT_TYPES, } from './events.js';
18
19
  export { turnStream, readTurn, TURN_RECORD_TTL_SECONDS } from './turn-stream.js';
19
20
  export { TURN_RECOVERY_ON, isTurnId } from './turn-record.js';
21
+ export { planOf } from './plan.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,7 +7,7 @@
7
7
  * `product_links_sent` for the products' store pages, and `link_sent` on each
8
8
  * store and `store_links_sent` for the pages of the stores to pick from.
9
9
  */
10
- import type { 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;
@@ -60,6 +60,22 @@ export interface BuyToolModelStore {
60
60
  /** True when the shopper holds this store's page, which `sendLink` delivered beside the reply. */
61
61
  link_sent: boolean;
62
62
  }
63
+ /**
64
+ * What every store cart on the table adds up to, in one currency: the stores' totals summed in minor units, each store's
65
+ * tip, fees and tax counted once as its `total` counts them. `is_estimate` is true when any store's total is an estimate,
66
+ * and `leaves_out` names what the estimated totals leave out together; null when every store priced its cart.
67
+ */
68
+ export interface BuyToolGrandTotal {
69
+ amount: string;
70
+ /** The sum in the currency's minor unit, such as cents. */
71
+ amount_cents: number;
72
+ /** The ISO 4217 code, upper case, or null when Agentcard named none for these carts. */
73
+ currency: string | null;
74
+ is_estimate: boolean;
75
+ leaves_out: string | null;
76
+ /** How many store carts this total adds up. */
77
+ stores: number;
78
+ }
63
79
  /** The buy tool's answer as the model reads it. */
64
80
  export interface BuyToolModelView {
65
81
  status: BuyToolStatus;
@@ -78,6 +94,8 @@ export interface BuyToolModelView {
78
94
  cart: BuyToolModelCart | null;
79
95
  /** Every cart on the table, the newest last, when the shopper is buying from more than one store. */
80
96
  carts: BuyToolModelCart[];
97
+ /** 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. */
98
+ grand_totals: BuyToolGrandTotal[];
81
99
  products: BuyToolModelProduct[];
82
100
  approval_expires_at: string | null;
83
101
  /**
@@ -197,6 +215,11 @@ export declare function cardKey(product: Pick<BuyToolProduct, 'id' | 'ref' | 'ur
197
215
  * and a place's card is where the shopper reads whether it has room.
198
216
  */
199
217
  export declare function productCardKey(product: Pick<BuyToolProduct, 'id' | 'ref' | 'url' | 'flight' | 'availability'>): string | null;
218
+ /**
219
+ * The carts' grand totals, one per currency, summed from each cart's `totalCents`, which already holds its tip, fees and
220
+ * tax: a display string is never parsed. Carts in two currencies are never added together.
221
+ */
222
+ export declare function grandTotals(carts: readonly BuyCart[]): BuyToolGrandTotal[];
200
223
  /** How long until `iso`, from `now`, as the shopper reads it: `about 24 hours`, `about 15 minutes`, `no longer`, or null. */
201
224
  export declare function expiresIn(iso: string | null | undefined, now?: number): string | null;
202
225
  /**
@@ -243,9 +266,11 @@ export interface ShownCard {
243
266
  * 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
267
  * are ...`, `there's also ...`) and names a card, and, past the one or two products a reply recommends in words, each
245
268
  * 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
269
+ * and so does a list line written without a bullet, whole, up to the question on it. The lines set in under a list
270
+ * line that goes, its details such as ` - ships from London`, go with it, and once the list went, a sentence that only
271
+ * points back at it, `both are priced in foreign currency`, goes when the cards show another number than it counts.
272
+ * What is left is the reply around the list, such as `found these at amazon.` and `which one do you want?`. A reply
273
+ * 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
274
  * message (toMessages), so the shopper never reads `two options came in under $20:` on its own.
250
275
  *
251
276
  * Each card is its name, or its name and its price as the model reads it (`{ name, price: '$22.89' }`): with the price,