@agent-cards/commerce-sdk 0.2.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/buy-tool.js CHANGED
@@ -24,8 +24,9 @@
24
24
  */
25
25
  import { AgentcardApiError, AgentcardClient, } from './client.js';
26
26
  import { buyToolDefinition, buyToolStandardSchema, openAIBuyTool, parseBuyToolInput, } from './definition.js';
27
- import { forModel, forModelError, money, shownProducts } from './model-view.js';
27
+ import { forModel, forModelError, money, pickId, shownProducts } from './model-view.js';
28
28
  import { toMessages } from './messages.js';
29
+ import { ackAt, instantAck as instantAckOf, isNewRequest, nextTemplate } from './ack.js';
29
30
  /** The pause before the one read that follows a placement Agentcard is still finishing. */
30
31
  const PLACEMENT_WAIT_MS = 12_000;
31
32
  /** After a turn that outlived Agentcard's budget: one read every 5 seconds, for at most a minute. */
@@ -41,6 +42,12 @@ const RESUME_SKEW_MS = 2 * 60_000;
41
42
  const STOPPED_ORDER = new Set(['failed', 'cancelled']);
42
43
  /** The refusals a confirm gets once the order took the cart, or while the placement still runs. */
43
44
  const AFTER_PLACEMENT = new Set(['nothing_to_confirm', 'conversation_closed', 'turn_in_progress']);
45
+ /** How long Agentcard has to answer before the instant reply goes out. */
46
+ const ACK_DELAY_MS = 1_500;
47
+ /** A call that starts this soon after the last one ended belongs to the same model turn, which gets one instant reply. */
48
+ const SAME_TURN_MS = 15_000;
49
+ /** How long after `instantAck` the model's first call still belongs to the turn it acknowledged. */
50
+ const APP_TURN_MS = 120_000;
44
51
  const STILL_WORKING = "Agentcard is still working on the shopper's last request. Call this tool again in a moment.";
45
52
  const CARD_NEEDED = 'The shopper adds a card from a Vault link before Agentcard can buy for them.';
46
53
  const STILL_PLACING = 'The shopper approved, and the order is still going through.';
@@ -52,20 +59,37 @@ const ANSWER_LATE = "Agentcard's answer took too long to come back. Call this to
52
59
  const CART_CHANGED = 'The cart changed since the shopper saw it. Show the shopper the cart as it stands now.';
53
60
  const NOTHING_TO_CONFIRM = 'There is no cart to confirm yet. Ask the shopper what they want.';
54
61
  const ADDRESS_CHANGED = 'The delivery address changed since this cart was built. Ask for the cart again, and Agentcard prices it for the new address.';
62
+ const PICK_GONE = "The product the shopper picked is no longer among the products Agentcard found, so nothing was added. Call this tool again with the shopper's words and product_id null, and Agentcard searches again.";
63
+ /**
64
+ * The pick as a later read of the conversation shows it: a pick the early answer found missing is in
65
+ * the cart once a line carries its catalog id, as its product id or, for a retail product whose id is
66
+ * its store page, as its page. A pick already in the cart, or none, stays as it was.
67
+ */
68
+ function pickAfter(pick, carts) {
69
+ if (!pick || pick.in_cart)
70
+ return pick;
71
+ const held = carts.some((c) => c.items.some((line) => line.product_id === pick.product_id || line.url === pick.product_id));
72
+ return held ? { ...pick, in_cart: true } : pick;
73
+ }
55
74
  function hashesOf(cart, carts) {
56
75
  return [...new Set([cart?.hash, ...carts.map((c) => c.hash)].filter((h) => typeof h === 'string' && h !== ''))];
57
76
  }
58
77
  function productsOf(turn) {
59
- return (turn.catalog?.items ?? [])
78
+ return turn.catalog ? productsOfCatalog(turn.catalog) : [];
79
+ }
80
+ /** The products of a search, as the tool reads them. */
81
+ function productsOfCatalog(catalog) {
82
+ return (catalog.items ?? [])
60
83
  .filter((item) => item.name)
61
84
  .map((item) => ({
62
85
  id: item.id,
86
+ ref: item.ref,
63
87
  name: item.name,
64
88
  priceCents: item.priceCents,
65
89
  currency: item.currency,
66
90
  imageUrl: item.image_url,
67
91
  ...(item.flight ? { flight: item.flight } : {}),
68
- store: item.store?.name ?? item.store?.id ?? turn.catalog?.merchant_name ?? null,
92
+ store: item.store?.name ?? item.store?.id ?? catalog.merchant_name ?? null,
69
93
  url: item.url ?? null,
70
94
  linkSent: false,
71
95
  }));
@@ -94,6 +118,7 @@ function answerOf(turn) {
94
118
  decline_code: turn.decline_code,
95
119
  error_code: turn.error_code,
96
120
  next_action: turn.next_action,
121
+ pick: turn.pick,
97
122
  hashes: hashesOf(turn.cart, turn.carts),
98
123
  envelope: turn.raw,
99
124
  };
@@ -191,6 +216,7 @@ function resultOfRefusal(error) {
191
216
  decline_code: null,
192
217
  error_code: error.code,
193
218
  next_action: null,
219
+ pick: null,
194
220
  hashes: hashesOf(null, carts),
195
221
  envelope: error.body,
196
222
  });
@@ -202,6 +228,8 @@ function resultOfRefusal(error) {
202
228
  return answer(NOTHING_TO_CONFIRM, []);
203
229
  if (error.code === 'turn_in_progress')
204
230
  return answer(STILL_WORKING, []);
231
+ if (error.code === 'pick_not_found')
232
+ return answer(PICK_GONE, []);
205
233
  return null;
206
234
  }
207
235
  /** A shopper Agentcard has not connected to this company yet: the Vault link where they connect and add a card. */
@@ -222,6 +250,7 @@ function needsCard(link, body) {
222
250
  decline_code: null,
223
251
  error_code: 'user_not_connected',
224
252
  next_action: 'add_card',
253
+ pick: null,
225
254
  hashes: [],
226
255
  envelope: body,
227
256
  };
@@ -295,10 +324,103 @@ export function buyTool(options) {
295
324
  throw new TypeError('Pass sendLink as a function that delivers a link to the shopper.');
296
325
  }
297
326
  const sendLink = o.sendLink;
327
+ if (o.onProgress !== undefined && typeof o.onProgress !== 'function')
328
+ throw new TypeError('Pass onProgress as a function that takes each stage of a call.');
329
+ if (o.stream !== undefined && o.stream !== false)
330
+ throw new TypeError('Pass stream as false to wait for each whole answer, or leave it out.');
331
+ const onProgress = o.onProgress;
332
+ if (o.earlyCards !== undefined && typeof o.earlyCards !== 'boolean')
333
+ throw new TypeError('Pass earlyCards as true or false.');
334
+ const earlyCards = o.earlyCards === true && !!o.sendLink;
335
+ const streams = o.stream !== false && (earlyCards || !!onProgress);
336
+ if (o.onResult !== undefined && typeof o.onResult !== 'function') {
337
+ throw new TypeError('Pass onResult as a function that takes each answer.');
338
+ }
339
+ const onResult = o.onResult;
298
340
  const wait = o.wait ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
299
341
  if (typeof wait !== 'function')
300
342
  throw new TypeError('Pass wait as a function of milliseconds that resolves after the pause.');
301
343
  const who = client.company ? { externalUserId } : {};
344
+ if (o.onWorking !== undefined && typeof o.onWorking !== 'function')
345
+ throw new TypeError('Pass onWorking as a function that sends the shopper a message.');
346
+ if (o.ack !== undefined && o.ack !== false && typeof o.ack !== 'function')
347
+ throw new TypeError('Pass ack as false, or as a function that writes the instant reply from the ask.');
348
+ const ackDelayMs = o.ackDelayMs ?? ACK_DELAY_MS;
349
+ if (typeof ackDelayMs !== 'number' || !Number.isFinite(ackDelayMs) || ackDelayMs < 0)
350
+ throw new TypeError('Pass ackDelayMs as a number of milliseconds, 0 or more.');
351
+ const onWorking = o.ack === false ? undefined : o.onWorking;
352
+ const writeAck = typeof o.ack === 'function' ? o.ack : null;
353
+ /** Calls running now, when the last one ended, whether this turn's instant reply is taken, and the template it used last. */
354
+ let running = 0;
355
+ let lastEndedAt = Number.NEGATIVE_INFINITY;
356
+ let turnAcked = false;
357
+ let lastTemplate = -1;
358
+ /** Counts the model's turns: the calls of one turn share its number. */
359
+ let modelTurn = 0;
360
+ /** This turn's instant reply while it is being sent: every call of the turn waits for it before it hands over a link. */
361
+ let ackOut = null;
362
+ /** True once `onWorking` delivered this turn's instant reply, so the model is told not to acknowledge again. */
363
+ let ackDelivered = false;
364
+ /**
365
+ * The app's instant reply for the shopper's newest message, waiting for the model's first call on that
366
+ * message, and the step the last answer asked the shopper for. A call still running for an earlier
367
+ * message keeps its own turn's state.
368
+ */
369
+ let appAck = null;
370
+ let lastNextAction = null;
371
+ /** The app's own instant reply: the turn its message opens sends no `onWorking` reply. */
372
+ const instantAck = (shopperText) => {
373
+ const text = String(shopperText ?? '');
374
+ const said = instantAckOf(text, { nextAction: lastNextAction }) !== null;
375
+ // The same rotation onWorking uses, so the next reply comes from another template.
376
+ if (said)
377
+ lastTemplate = nextTemplate(lastTemplate);
378
+ appAck = { at: Date.now(), said };
379
+ return said ? ackAt(lastTemplate, text) : null;
380
+ };
381
+ /**
382
+ * Holds the instant reply for a new request until `ackDelayMs` passes. `cancel` stops one that has
383
+ * not gone out, and frees the turn's reply for a later call.
384
+ */
385
+ const holdAck = (ask) => {
386
+ if (!onWorking || turnAcked || !isNewRequest(ask))
387
+ return null;
388
+ turnAcked = true;
389
+ let held = true;
390
+ const timer = setTimeout(() => {
391
+ held = false;
392
+ let text;
393
+ if (writeAck) {
394
+ try {
395
+ text = writeAck(ask);
396
+ }
397
+ catch {
398
+ text = null;
399
+ }
400
+ }
401
+ else {
402
+ lastTemplate = nextTemplate(lastTemplate);
403
+ text = ackAt(lastTemplate, ask);
404
+ }
405
+ if (typeof text !== 'string' || text.trim() === '') {
406
+ turnAcked = false;
407
+ return;
408
+ }
409
+ const message = text.trim();
410
+ ackOut = Promise.resolve()
411
+ .then(() => onWorking(message))
412
+ .then(() => { ackDelivered = true; }, () => { });
413
+ }, ackDelayMs);
414
+ return {
415
+ cancel: () => {
416
+ if (!held)
417
+ return;
418
+ held = false;
419
+ clearTimeout(timer);
420
+ turnAcked = false;
421
+ },
422
+ };
423
+ };
302
424
  /** One read of the conversation, or null when it fails: a read only ever improves an answer the tool already has. */
303
425
  const read = async (conversationId) => {
304
426
  try {
@@ -456,6 +578,8 @@ export function buyTool(options) {
456
578
  error_code: newest ? null : base.error_code,
457
579
  // A read replaced the answer the step was named for, and names no step of its own: the reply asks.
458
580
  next_action: null,
581
+ // The early answer judged the pick before the turn finished adding it.
582
+ pick: pickAfter(base.pick, conversation.carts),
459
583
  envelope: conversation.raw,
460
584
  };
461
585
  if (!conversation.turn_in_progress)
@@ -512,15 +636,21 @@ export function buyTool(options) {
512
636
  // A running turn that placed nothing may be a search rather than a placement, so the shopper hears it is still working.
513
637
  return settled.status === 'placing' && !placing ? refused : settled;
514
638
  };
515
- /** The store pages this tool sent last, with their conversation. */
639
+ /** The store pages this tool sent last, with their conversation, the model turn that sent them, and the search they list. */
516
640
  let lastPages = null;
517
641
  /**
518
642
  * The store page of each product the model reads, sent once per page. A
519
643
  * page the last batch in this conversation already sent stays with the
520
- * shopper and goes out no second time. A page whose `sendLink` throws
644
+ * shopper and goes out no second time, unless a later model turn's reply
645
+ * presents the products again (the catalog's `presented`): a repeated ask
646
+ * answered with the same products gets their pages beside it, so every
647
+ * product the reply names has a card. Calls of one model turn still send a
648
+ * page once. An answer that lists fewer of the same search's products, such
649
+ * as a pick's, keeps the pages that search already sent, so a later answer
650
+ * that lists them again sends none twice. A page whose `sendLink` throws
521
651
  * stays unsent, and the call goes on, so the next answer tries it again.
522
652
  */
523
- const deliverPages = async (result) => {
653
+ const deliverPages = async (result, call) => {
524
654
  if (!sendLink)
525
655
  return result;
526
656
  const pages = new Map();
@@ -531,12 +661,21 @@ export function buyTool(options) {
531
661
  // An answer with no store page leaves the last batch as it was.
532
662
  if (pages.size === 0)
533
663
  return result;
534
- const before = lastPages?.conversationId === result.conversation_id ? lastPages.urls : new Set();
535
- const held = new Set();
664
+ const kept = lastPages?.conversationId === result.conversation_id && (!call.presented || lastPages.turn === call.turn);
665
+ const before = kept && lastPages ? lastPages.urls : new Set();
666
+ // Agentcard names the search an answer lists by when it ran. An answer that lists fewer of the same
667
+ // search's products, such as a pick's, keeps every page that search already sent, so a later answer
668
+ // that lists them again sends none twice.
669
+ const catalog = result.envelope?.catalog;
670
+ const search = typeof catalog?.as_of === 'string' ? catalog.as_of : null;
671
+ const sameSearch = kept && search !== null && lastPages?.search === search;
672
+ const held = new Set(sameSearch ? before : []);
536
673
  for (const [url, product] of pages) {
537
674
  if (!before.has(url)) {
538
675
  try {
539
- await sendLink({ kind: 'product', url, title: product.name, price: money(product.priceCents, product.currency), productId: product.id, imageUrl: product.imageUrl, expiresAt: null });
676
+ // The id the model reads and a pick takes, never the store page: the page is `url`.
677
+ const productId = pickId(product);
678
+ await sendLink({ kind: 'product', url, title: product.name, price: money(product.priceCents, product.currency), ...(productId ? { productId } : {}), imageUrl: product.imageUrl, expiresAt: null });
540
679
  }
541
680
  catch {
542
681
  continue;
@@ -544,25 +683,27 @@ export function buyTool(options) {
544
683
  }
545
684
  held.add(url);
546
685
  }
547
- lastPages = { conversationId: result.conversation_id, urls: held };
686
+ lastPages = { conversationId: result.conversation_id, turn: call.turn, search, urls: held };
548
687
  return { ...result, products: result.products.map((product) => (product.url && held.has(product.url) ? { ...product, linkSent: true } : product)) };
549
688
  };
689
+ /** The products a streamed search found, as an answer the page delivery reads: its conversation, its search and its products. */
690
+ const earlyPages = (conversationId, asOf, products) => ({ conversation_id: conversationId, envelope: { catalog: { as_of: asOf } }, products });
550
691
  /** One batch of pages at a time: a call that overlaps another reads the pages that one sent. */
551
692
  let pagesInTurn = Promise.resolve();
552
- const deliverPagesInTurn = (result) => {
553
- const batch = pagesInTurn.then(() => deliverPages(result));
693
+ const deliverPagesInTurn = (result, call) => {
694
+ const batch = pagesInTurn.then(() => deliverPages(result, call));
554
695
  pagesInTurn = batch.catch(() => { });
555
696
  return batch;
556
697
  };
557
698
  /** The Vault or approval link first, because the purchase waits on it, then the store pages. */
558
- const deliver = async (result) => {
699
+ const deliver = async (result, call) => {
559
700
  let delivered = result;
560
701
  if (result.links.length > 0 && sendLink) {
561
702
  for (const link of result.links)
562
703
  await sendLink({ ...link });
563
704
  delivered = { ...result, link_sent: true };
564
705
  }
565
- return deliverPagesInTurn(delivered);
706
+ return deliverPagesInTurn(delivered, call);
566
707
  };
567
708
  const answer = async (raw) => {
568
709
  const startedAt = Date.now();
@@ -572,22 +713,87 @@ export function buyTool(options) {
572
713
  const ask = input.ask.trim();
573
714
  if (!confirm && ask === '')
574
715
  throw new TypeError("Put the shopper's words in ask.");
716
+ // A pick chooses from the products this conversation showed: never on a confirm, and never on a new order, which shows none yet.
717
+ const pick = !confirm && !input.new_order && input.product_id ? { product_id: input.product_id } : null;
575
718
  const body = {
576
719
  resume: true,
577
720
  ...(confirm ? { confirm: true, payment_source: 'vault' } : { ask }),
578
721
  ...(input.new_order ? { new_order: true } : {}),
722
+ ...(pick ? { pick } : {}),
579
723
  ...(o.deliveryAddress ? { delivery_address: o.deliveryAddress } : {}),
580
724
  ...(client.company ? { external_user_id: externalUserId } : {}),
581
725
  };
582
- let turn = null;
583
- let refusal;
726
+ // The first call after `instantAck` opens the turn of the message it answered, whose reply is the
727
+ // app's. Any other call that starts while another runs, or right after one ended, is the model
728
+ // working through the same turn.
729
+ const opened = appAck && Date.now() - appAck.at < APP_TURN_MS ? appAck : null;
730
+ appAck = null;
731
+ if (opened) {
732
+ turnAcked = true;
733
+ ackOut = null;
734
+ ackDelivered = opened.said;
735
+ modelTurn += 1;
736
+ }
737
+ else if (running === 0 && Date.now() - lastEndedAt >= SAME_TURN_MS) {
738
+ turnAcked = false;
739
+ ackOut = null;
740
+ ackDelivered = false;
741
+ modelTurn += 1;
742
+ }
743
+ const callTurn = modelTurn;
744
+ running += 1;
745
+ // A pick adds a product the shopper already saw, so no "looking for" reply goes out, as for "the first one".
746
+ const ack = confirm || pick ? null : holdAck(ask);
584
747
  try {
585
- turn = await client.buy(body);
748
+ let turn = null;
749
+ let refusal;
750
+ // Streamed: the stages go to onProgress, and a search's store pages go out the moment it answers.
751
+ const handlers = streams
752
+ ? {
753
+ ...(onProgress ? { onProgress: (progress) => { try {
754
+ onProgress(progress);
755
+ }
756
+ catch { /* the app's display failing never fails the call */ } } } : {}),
757
+ ...(earlyCards && sendLink && !confirm
758
+ ? {
759
+ onCatalog: async (update) => {
760
+ // Products arrived before the instant reply was due: they are the reply.
761
+ ack?.cancel();
762
+ await ackOut;
763
+ // The turn's own search presents its products, as a reply that lists them does.
764
+ await deliverPagesInTurn(earlyPages(update.conversationId, update.asOf, productsOfCatalog(update.catalog)), { turn: callTurn, presented: true });
765
+ },
766
+ }
767
+ : {}),
768
+ }
769
+ : undefined;
770
+ try {
771
+ turn = await client.buy(body, handlers);
772
+ }
773
+ catch (error) {
774
+ refusal = error;
775
+ }
776
+ ack?.cancel();
777
+ const settled = turn ? await settleTurn(turn, startedAt) : await settleRefusal(refusal, confirm, startedAt);
778
+ await ackOut;
779
+ const delivered = await deliver(withMessages(settled, turn), { turn: callTurn, presented: turn?.catalog?.presented === true });
780
+ const result = ackDelivered ? { ...delivered, shopper_already_told_looking: true } : delivered;
781
+ lastNextAction = result.next_action;
782
+ if (onResult) {
783
+ try {
784
+ await onResult(result);
785
+ }
786
+ catch {
787
+ // The app's display failing never fails the purchase call.
788
+ }
789
+ }
790
+ return result;
586
791
  }
587
- catch (error) {
588
- refusal = error;
792
+ finally {
793
+ ack?.cancel();
794
+ running -= 1;
795
+ lastEndedAt = Date.now();
589
796
  }
590
- return deliver(withMessages(turn ? await settleTurn(turn, startedAt) : await settleRefusal(refusal, confirm, startedAt), turn));
591
797
  };
592
798
  /** Checks the model's input, with every link taken out of a refusal. */
593
799
  const parse = (input) => {
@@ -608,7 +814,7 @@ export function buyTool(options) {
608
814
  }
609
815
  };
610
816
  const { name, description, input_schema } = buyToolDefinition();
611
- const tool = { name, description, input_schema, strict: true, parse, run: handle, execute: handle, handle, answer, forModel };
817
+ const tool = { name, description, input_schema, strict: true, parse, run: handle, execute: handle, handle, answer, forModel, instantAck };
612
818
  // JSON skips these three, so a runner that sends the tool to its API as is sends Anthropic's fields and nothing else.
613
819
  const unsent = (value) => ({ value, enumerable: false, writable: true, configurable: true });
614
820
  return Object.defineProperties(tool, {
package/dist/client.d.ts CHANGED
@@ -1,14 +1,55 @@
1
+ import { type AgentcardListener, type ListenOptions } from './listen.js';
1
2
  /** The fetch the client calls: Node 18 and later have one built in. */
2
3
  export type FetchLike = (url: string, init: {
3
4
  method: string;
4
5
  headers: Record<string, string>;
5
6
  body?: string;
6
7
  signal?: AbortSignal;
7
- }) => Promise<{
8
+ }) => Promise<FetchLikeResponse>;
9
+ /**
10
+ * The answer a fetch returns, as far as the client reads it. `headers` and `body` are optional: a
11
+ * fetch without them is read whole with `text()`, and a streamed purchase answer still arrives, at its end.
12
+ */
13
+ export interface FetchLikeResponse {
8
14
  status: number;
9
15
  ok: boolean;
10
16
  text(): Promise<string>;
11
- }>;
17
+ headers?: {
18
+ get(name: string): string | null;
19
+ } | null;
20
+ body?: {
21
+ getReader(): {
22
+ read(): Promise<{
23
+ done: boolean;
24
+ value?: Uint8Array;
25
+ }>;
26
+ cancel?(): Promise<void>;
27
+ };
28
+ } | null;
29
+ }
30
+ /**
31
+ * What the purchase turn is doing while it runs: `searching` the stores, `building_cart`, `checking_out`,
32
+ * or `working` on anything else. Agentcard names only the stage, never what the shopper said.
33
+ */
34
+ export type BuyProgressStage = 'searching' | 'building_cart' | 'checking_out' | 'working';
35
+ /** One step of a running purchase turn. */
36
+ export interface BuyProgress {
37
+ stage: BuyProgressStage;
38
+ conversationId: string | null;
39
+ }
40
+ /** The products a running turn's search found, sent the moment the search answers and before the reply is written. */
41
+ export interface BuyCatalogUpdate {
42
+ conversationId: string;
43
+ catalog: BuyCatalog;
44
+ /** When the search ran, as the answer's `catalog.as_of` names it: the same search in the answer has the same value. */
45
+ asOf: string | null;
46
+ }
47
+ /** Called while a purchase turn runs. Passing either one asks Agentcard to stream the turn. */
48
+ export interface BuyStreamHandlers {
49
+ onProgress?: (progress: BuyProgress) => void;
50
+ /** The client waits for what this returns before it hands back the turn's answer. */
51
+ onCatalog?: (update: BuyCatalogUpdate) => void | Promise<void>;
52
+ }
12
53
  export declare const DEFAULT_BASE_URL = "https://api.agentcard.sh";
13
54
  /** The wait for one purchase turn: past Agentcard's own 110-second budget, so a slow turn answers `turn_timeout` rather than aborting here. */
14
55
  export declare const BUY_TURN_TIMEOUT_MS = 130000;
@@ -51,6 +92,13 @@ export interface BuyRequest {
51
92
  resume?: true;
52
93
  /** Drop the cart on the table and start a new order. */
53
94
  new_order?: true;
95
+ /**
96
+ * The product the shopper picked from the last search's `catalog.items`, by its `id` or its `ref`:
97
+ * Agentcard adds exactly that item, with no new search. Goes with an `ask`, never with `confirm` or `new_order`.
98
+ */
99
+ pick?: {
100
+ product_id: string;
101
+ };
54
102
  payment_source?: 'vault';
55
103
  delivery_address?: BuyDeliveryAddress;
56
104
  external_user_id?: string;
@@ -101,6 +149,8 @@ export interface BuyFlightItinerary {
101
149
  export interface BuyCatalogItem {
102
150
  flight?: BuyFlightItinerary;
103
151
  id: string;
152
+ /** The item's id with no link in it, which `pick.product_id` takes as it takes `id`; null from an older API. */
153
+ ref: string | null;
104
154
  name: string | null;
105
155
  priceCents: number | null;
106
156
  /** The ISO 4217 code the store prices the product in, or null when Agentcard sent none. */
@@ -110,11 +160,22 @@ export interface BuyCatalogItem {
110
160
  /** The product's page at its store, as an http or https address, or null when Agentcard sent none. */
111
161
  url: string | null;
112
162
  }
113
- /** The products the last search found. */
163
+ /**
164
+ * The products the last search found. `presented` is true when this answer's reply presents them: the
165
+ * turn searched, or answered a repeated ask from the results it already held. False when they only
166
+ * stand from an earlier answer, and on an older API.
167
+ */
114
168
  export interface BuyCatalog {
115
169
  merchant: string;
116
170
  merchant_name: string;
117
171
  items: BuyCatalogItem[];
172
+ presented: boolean;
173
+ }
174
+ /** The product a `pick` named, by its catalog id and ref, and whether the cart now holds it. */
175
+ export interface BuyPick {
176
+ product_id: string;
177
+ ref: string | null;
178
+ in_cart: boolean;
118
179
  }
119
180
  export interface BuyPaymentSource {
120
181
  source: string;
@@ -160,6 +221,8 @@ export interface BuyTurn {
160
221
  store_login_expires_at: string | null;
161
222
  /** The store that page signs in to. */
162
223
  store_login_merchant_name: string | null;
224
+ /** The product this turn's `pick` named, and whether the cart holds it; null for a turn without one. */
225
+ pick: BuyPick | null;
163
226
  /** The answer as Agentcard sent it. */
164
227
  raw: Record<string, unknown>;
165
228
  }
@@ -211,7 +274,7 @@ export interface VaultLink {
211
274
  /**
212
275
  * A refusal, or a failure to reach Agentcard. `code` is the API's own
213
276
  * (`user_not_connected`, `external_user_id_ambiguous`, `turn_in_progress`,
214
- * `user_id_mismatch`), one read from the shape of the answer
277
+ * `user_id_mismatch`, `pick_not_found`), one read from the shape of the answer
215
278
  * (`cart_changed`, `nothing_to_confirm`, `conversation_closed`,
216
279
  * `conversation_not_found`), or the client's (`unreachable`, `timeout`,
217
280
  * `oauth_failed`, `unauthorized`, `http_<status>`). `status` is 0 when
@@ -243,15 +306,18 @@ export declare class AgentcardClient {
243
306
  get company(): boolean;
244
307
  /** True for a sandbox client, as the token exchange said; null until the first exchange, and under a shopper's token. */
245
308
  get sandbox(): boolean | null;
246
- /** The access token the client sends: the shopper's own, or the company's, exchanged once and renewed a minute before it expires. */
247
- bearer(): Promise<string>;
309
+ /**
310
+ * The access token the client sends: the shopper's own, or the company's, exchanged once and
311
+ * renewed a minute before it expires. Pass a company token Agentcard refused to get a new one.
312
+ */
313
+ bearer(refused?: string): Promise<string>;
248
314
  /**
249
315
  * One purchase turn, sent once with a 130-second wait. A turn is a model run
250
316
  * against live stores, so a lost answer is never sent again: read the
251
317
  * conversation instead. Only a 401 is sent again, once, with a fresh token,
252
318
  * because Agentcard refused it before anything ran.
253
319
  */
254
- buy(body: BuyRequest): Promise<BuyTurn>;
320
+ buy(body: BuyRequest, stream?: BuyStreamHandlers): Promise<BuyTurn>;
255
321
  /** One read of a conversation: whether a turn is running, the last checkout, the orders it placed, the carts on the table. */
256
322
  conversation(conversationId: string, who?: Shopper): Promise<BuyConversation>;
257
323
  /**
@@ -262,6 +328,11 @@ export declare class AgentcardClient {
262
328
  mintVaultLink(input: {
263
329
  externalUserId: string;
264
330
  }): Promise<VaultLink>;
331
+ /**
332
+ * Hear every event Agentcard sends your company over one connection your server opens, with no
333
+ * webhook endpoint: `listenToAgentcard(this, opts)`. Takes your company credentials.
334
+ */
335
+ listen(opts: ListenOptions): AgentcardListener;
265
336
  /** A company names the shopper exactly once; a shopper's own token already names them. */
266
337
  private checkShopper;
267
338
  private call;