@agent-cards/commerce-sdk 0.13.0 → 0.14.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/client.js CHANGED
@@ -10,6 +10,8 @@
10
10
  */
11
11
  import { isRecord } from './verify.js';
12
12
  import { listenToAgentcard } from './listen.js';
13
+ /** The handlers that ask Agentcard to stream a turn. */
14
+ const STREAM_HANDLERS = ['onProgress', 'onStatus', 'onFound', 'onCatalog', 'onSegment', 'onCart', 'onDelta'];
13
15
  const PROGRESS_STAGES = new Set(['searching', 'building_cart', 'checking_out', 'working']);
14
16
  export const DEFAULT_BASE_URL = 'https://api.agentcard.sh';
15
17
  /** The wait for one purchase turn: past Agentcard's own 110-second budget, so a slow turn answers `turn_timeout` rather than aborting here. */
@@ -24,6 +26,37 @@ const DELIVERED_PER_SHOPPER = 20;
24
26
  const DELIVERED_SHOPPERS = 10_000;
25
27
  /** The event ids the client remembers taking in, so a message delivered twice reaches the model once. */
26
28
  const DELIVERED_IDS_KEPT = 10_000;
29
+ /** `@opentelemetry/api`, loaded once, only when the app installed it: the package depends on nothing. */
30
+ const OTEL_API = '@opentelemetry/api';
31
+ let otelLoaded = null;
32
+ function loadOtel() {
33
+ otelLoaded ??= import(/* webpackIgnore: true */ /* @vite-ignore */ OTEL_API).then((m) => (m.trace && m.propagation && m.context ? m : m.default?.trace ? m.default : null), () => null);
34
+ return otelLoaded;
35
+ }
36
+ /** The `traceparent` and `tracestate` headers for one call: the active span's, else the app's own, else none. */
37
+ function traceHeaders(otel, own) {
38
+ const carrier = {};
39
+ if (otel) {
40
+ try {
41
+ otel.propagation.inject(otel.context.active(), carrier);
42
+ }
43
+ catch { /* A broken propagator never stops a purchase. */ }
44
+ if (str(carrier.traceparent))
45
+ return pickTrace(carrier);
46
+ }
47
+ let given;
48
+ try {
49
+ given = own?.();
50
+ }
51
+ catch { /* An app's own trace lookup that throws sends no trace. */ }
52
+ const ctx = typeof given === 'string' ? { traceparent: given } : given;
53
+ // Only a well-formed W3C traceparent goes out: anything else would break the header or the backend's parse.
54
+ return isRecord(ctx) && TRACEPARENT.test(String(ctx.traceparent)) ? pickTrace(ctx) : {};
55
+ }
56
+ const TRACEPARENT = /^[\da-f]{2}-[\da-f]{32}-[\da-f]{16}-[\da-f]{2}$/;
57
+ // A tracestate is printable ASCII of at most 512 characters (W3C); any other value is left off, so it can never stop a purchase.
58
+ const TRACESTATE = /^[\x20-\x7e]{1,512}$/;
59
+ const pickTrace = (c) => typeof c.tracestate === 'string' && TRACESTATE.test(c.tracestate) ? { traceparent: c.traceparent, tracestate: c.tracestate } : { traceparent: c.traceparent };
27
60
  const NEXT_ACTIONS = new Set(['shipping_details', 'guest_details', 'store_login', 'add_card', 'confirm', 'verification_code', 'approve']);
28
61
  /**
29
62
  * A refusal, or a failure to reach Agentcard. `code` is the API's own
@@ -111,6 +144,13 @@ function feeLinesOf(v) {
111
144
  const lines = v.filter(isRecord).flatMap((f) => (str(f.label) && num(f.amountCents) !== null ? [{ label: f.label, amountCents: f.amountCents }] : []));
112
145
  return lines.length ? lines : null;
113
146
  }
147
+ function feesOf(v) {
148
+ const amount = num(v.serviceFeesCents);
149
+ const status = v.merchantFeesPending === true || v.serviceFeesStatus === 'pending' ? 'pending'
150
+ : (v.serviceFeesStatus === undefined || v.serviceFeesStatus === 'known')
151
+ && amount !== null && Number.isSafeInteger(amount) && amount >= 0 ? 'known' : 'unknown';
152
+ return { serviceFeesCents: status === 'known' ? amount : null, serviceFeesStatus: status };
153
+ }
114
154
  function cartOf(v) {
115
155
  if (!isRecord(v) || !str(v.hash) || !str(v.merchant))
116
156
  return null;
@@ -135,7 +175,7 @@ function cartOf(v) {
135
175
  };
136
176
  }) : [],
137
177
  subtotalCents: num(v.subtotalCents),
138
- serviceFeesCents: num(v.serviceFeesCents) ?? 0,
178
+ ...feesOf(v),
139
179
  merchantFeeLines: feeLinesOf(v.merchantFeeLines),
140
180
  tipCents: num(v.tipCents) ?? 0,
141
181
  totalCents: num(v.totalCents) ?? 0,
@@ -164,7 +204,12 @@ function flightOf(value) {
164
204
  function catalogOf(v) {
165
205
  if (!isRecord(v))
166
206
  return null;
167
- const items = Array.isArray(v.items) ? v.items.filter(isRecord).map((i) => {
207
+ const items = catalogItemsOf(v.items);
208
+ return { merchant: str(v.merchant) ?? '', merchant_name: str(v.merchant_name) ?? str(v.merchant) ?? '', items, presented: v.presented === true };
209
+ }
210
+ /** A catalog block's products, as `items` or `more` lists them. */
211
+ function catalogItemsOf(v) {
212
+ return Array.isArray(v) ? v.filter(isRecord).map((i) => {
168
213
  const flight = flightOf(i.flight);
169
214
  return {
170
215
  id: str(i.id) ?? '',
@@ -180,7 +225,10 @@ function catalogOf(v) {
180
225
  url: pageUrl(i.url),
181
226
  };
182
227
  }) : [];
183
- return { merchant: str(v.merchant) ?? '', merchant_name: str(v.merchant_name) ?? str(v.merchant) ?? '', items, presented: v.presented === true };
228
+ }
229
+ /** A count Agentcard sent: a whole number, never negative. Null otherwise. */
230
+ function countOf(v) {
231
+ return typeof v === 'number' && Number.isInteger(v) && v >= 0 ? v : null;
184
232
  }
185
233
  /** The stores an answer presents: each one with an id and a name, and no block when none has both. */
186
234
  function storesOf(v) {
@@ -376,14 +424,21 @@ async function eachEvent(res, onEvent) {
376
424
  }
377
425
  /**
378
426
  * A streamed purchase turn, read to its `envelope`: the same answer a JSON turn returns, with its
379
- * status in `http_status`. Each `progress` and `catalog` goes to its handler on the way. A stream that
380
- * breaks or ends before its envelope, once it named the conversation, reads as a turn past its budget
427
+ * status in `http_status`. Each `progress`, `status`, `delta`, `catalog`, `find`, `segment` and `cart` goes to its handler
428
+ * on the way, and any other event, such as a `ping` or a `tool`, is read past. A stream that breaks or
429
+ * ends before its envelope, once it named the conversation, reads as a turn past its budget
381
430
  * (`turn_timeout`), so the tool reads the conversation for what the turn left instead of losing it.
382
431
  */
383
432
  async function readStreamedTurn(res, stream, timeoutMs) {
384
433
  let envelope = null;
385
434
  let conversationId = null;
386
- const delivering = [];
435
+ // The catalog, segment, cart and delta handlers run one after another, in the order their events arrived, so an app
436
+ // never shows a message above the cards it names, nor a piece of the next message before the message ahead of it. The
437
+ // stream is read on meanwhile, and the answer waits for the last one. A handler that throws costs only itself.
438
+ let delivering = Promise.resolve();
439
+ const deliver = (hand) => {
440
+ delivering = delivering.then(() => hand()).catch(() => { });
441
+ };
387
442
  let failure = null;
388
443
  try {
389
444
  await eachEvent(res, (event, text) => {
@@ -391,6 +446,8 @@ async function readStreamedTurn(res, stream, timeoutMs) {
391
446
  if (!data)
392
447
  return;
393
448
  conversationId = str(data.conversation_id) ?? conversationId;
449
+ // The conversation this event belongs to: its own, or the one an earlier event of the same turn named.
450
+ const named = conversationId;
394
451
  if (event === 'envelope') {
395
452
  envelope = data;
396
453
  return true;
@@ -403,19 +460,59 @@ async function readStreamedTurn(res, stream, timeoutMs) {
403
460
  // The app's display failing never fails the turn.
404
461
  }
405
462
  }
406
- else if (event === 'catalog' && stream.onCatalog && str(data.conversation_id)) {
463
+ else if (event === 'status' && typeof data.text === 'string' && data.text.trim() !== '' && typeof data.step === 'string' && stream.onStatus) {
464
+ try {
465
+ stream.onStatus({ conversationId: named, step: data.step, text: data.text });
466
+ }
467
+ catch {
468
+ // The app's display failing never fails the turn.
469
+ }
470
+ }
471
+ else if (event === 'delta' && typeof data.text === 'string' && data.text !== '' && stream.onDelta) {
472
+ deliver(() => stream.onDelta({ conversationId: named, text: data.text }));
473
+ }
474
+ else if (event === 'catalog' && (stream.onCatalog || stream.onFound) && str(data.conversation_id)) {
407
475
  const catalog = catalogOf(data.catalog);
408
476
  if (!catalog || catalog.items.length === 0)
409
477
  return;
410
- const asOf = isRecord(data.catalog) ? str(data.catalog.as_of) : null;
411
- delivering.push(Promise.resolve().then(() => stream.onCatalog({ conversationId: data.conversation_id, catalog, asOf })).catch(() => { }));
478
+ const block = data.catalog;
479
+ const asOf = str(block.as_of);
480
+ const shown = countOf(block.shown);
481
+ const found = countOf(block.found);
482
+ const update = { conversationId: data.conversation_id, catalog, asOf, shown, found, more: catalogItemsOf(block.more) };
483
+ // The count follows the cards it counts, so an app never shows "8 of 23" above no cards, and it settles before the
484
+ // next event, so a later count never lands before an earlier one. A card display that throws never costs the count.
485
+ deliver(async () => {
486
+ try {
487
+ await stream.onCatalog?.(update);
488
+ }
489
+ catch {
490
+ // The app's display failing never fails the turn.
491
+ }
492
+ if (shown !== null && found !== null && found >= shown)
493
+ await stream.onFound?.({ conversationId: update.conversationId, shown, found });
494
+ });
495
+ }
496
+ else if (event === 'find' && stream.onFound && str(data.conversation_id)) {
497
+ // Find mode's count from the product index: the cards it shows of the products it found.
498
+ const shown = countOf(data.shown);
499
+ const found = countOf(data.found);
500
+ if (shown !== null && found !== null && found >= shown)
501
+ deliver(() => stream.onFound({ conversationId: data.conversation_id, shown, found }));
502
+ }
503
+ else if (event === 'segment' && typeof data.text === 'string' && data.text.trim() !== '' && stream.onSegment) {
504
+ deliver(() => stream.onSegment({ conversationId: named, text: data.text }));
505
+ }
506
+ else if (event === 'cart' && stream.onCart && str(data.conversation_id)) {
507
+ const update = { conversationId: data.conversation_id, cart: cartOf(data.cart), carts: cartsOf(data.carts) };
508
+ deliver(() => stream.onCart(update));
412
509
  }
413
510
  });
414
511
  }
415
512
  catch (error) {
416
513
  failure = error;
417
514
  }
418
- await Promise.all(delivering);
515
+ await delivering;
419
516
  const answered = envelope;
420
517
  if (answered) {
421
518
  const { http_status: httpStatus, ...body } = answered;
@@ -439,6 +536,7 @@ export class AgentcardClient {
439
536
  /** Each shopper's messages that no history the model ran with holds yet, under your own id for them. */
440
537
  inbox = new Map();
441
538
  deliveredIds = new Set();
539
+ trace;
442
540
  constructor(options) {
443
541
  const o = (options ?? {});
444
542
  const hasCredentials = o.clientId !== undefined || o.clientSecret !== undefined;
@@ -457,6 +555,7 @@ export class AgentcardClient {
457
555
  this.baseUrl = (str(o.baseUrl) ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
458
556
  this.credentials = hasCredentials ? { clientId: o.clientId, clientSecret: o.clientSecret } : null;
459
557
  this.shopperToken = hasCredentials ? null : o.accessToken;
558
+ this.trace = isRecord(o.trace) ? o.trace : {};
460
559
  }
461
560
  /** True when the client buys for many shoppers with a company's credentials; false under one shopper's own token. */
462
561
  get company() {
@@ -491,10 +590,39 @@ export class AgentcardClient {
491
590
  if (!isRecord(body))
492
591
  throw new TypeError('Pass the purchase request as an object, such as { ask: "two AA battery packs" }.');
493
592
  this.checkShopper({ externalUserId: body.external_user_id, userId: body.user_id });
494
- const streamed = !!stream && (typeof stream.onProgress === 'function' || typeof stream.onCatalog === 'function');
495
- // A streamed turn keeps the soft budget a JSON turn has, so it answers within BUY_TURN_TIMEOUT_MS too.
496
- const path = streamed ? '/buy?stream=1&soft_timeout=1' : '/buy';
497
- return turnOf(await this.call('POST', path, body, BUY_TURN_TIMEOUT_MS, streamed ? stream : undefined));
593
+ const streamed = !!stream && STREAM_HANDLERS.some((name) => typeof stream[name] === 'function');
594
+ // A streamed turn keeps the soft budget a JSON turn has, so it answers within BUY_TURN_TIMEOUT_MS too. The pieces of
595
+ // each message go out only to a caller that asks for them.
596
+ const path = !streamed ? '/buy' : typeof stream.onDelta === 'function' ? '/buy?stream=deltas&soft_timeout=1' : '/buy?stream=1&soft_timeout=1';
597
+ const otel = this.trace.otel === false ? null : this.trace.otel ?? await loadOtel();
598
+ const run = async () => {
599
+ const headers = traceHeaders(otel, this.trace.traceContext);
600
+ return turnOf(await this.call('POST', path, body, BUY_TURN_TIMEOUT_MS, streamed ? stream : undefined, headers));
601
+ };
602
+ if (!otel)
603
+ return run();
604
+ // One `sdk.buy` span per call, a client span (kind 2) the backend's `buy.request` hangs under. It names the
605
+ // conversation and how the call ended, never what the shopper said.
606
+ const attributes = { 'agentcard.streamed': streamed };
607
+ const conversationId = str(body.conversation_id);
608
+ if (conversationId)
609
+ attributes['agentcard.conversation_id'] = conversationId;
610
+ return otel.trace.getTracer('commerce-sdk').startActiveSpan('sdk.buy', { kind: 2, attributes }, async (span) => {
611
+ try {
612
+ const turn = await run();
613
+ if (turn.conversation_id)
614
+ span.setAttribute('agentcard.conversation_id', turn.conversation_id);
615
+ span.setAttribute('agentcard.buy_status', turn.status);
616
+ return turn;
617
+ }
618
+ catch (error) {
619
+ span.setStatus({ code: 2, message: error instanceof AgentcardApiError ? error.code : 'error' });
620
+ throw error;
621
+ }
622
+ finally {
623
+ span.end();
624
+ }
625
+ });
498
626
  }
499
627
  /** One read of a conversation: whether a turn is running, the last checkout, the orders it placed, the carts on the table. */
500
628
  async conversation(conversationId, who = {}) {
@@ -618,10 +746,10 @@ export class AgentcardClient {
618
746
  throw new TypeError('Name the shopper once: externalUserId, your own id for them, or userId, the Agentcard user id.');
619
747
  }
620
748
  }
621
- async call(method, path, body, timeoutMs, stream) {
749
+ async call(method, path, body, timeoutMs, stream, extra = {}) {
622
750
  let token = await this.bearer();
623
751
  for (let attempt = 0;; attempt++) {
624
- const answer = await this.send(method, path, token, body, timeoutMs, stream);
752
+ const answer = await this.send(method, path, token, body, timeoutMs, stream, extra);
625
753
  if (answer.status === 401 && attempt === 0 && this.credentials) {
626
754
  token = await this.renewAfter(token);
627
755
  continue;
@@ -639,12 +767,13 @@ export class AgentcardClient {
639
767
  throw errorOf(answer.status, answer.body);
640
768
  }
641
769
  }
642
- async send(method, path, token, body, timeoutMs, stream) {
770
+ async send(method, path, token, body, timeoutMs, stream, extra = {}) {
643
771
  let res;
644
772
  try {
645
773
  res = await this.fetchFn(`${this.baseUrl}${path}`, {
646
774
  method,
647
775
  headers: {
776
+ ...extra,
648
777
  authorization: `Bearer ${token}`,
649
778
  accept: stream ? 'text/event-stream, application/json' : 'application/json',
650
779
  ...(body !== undefined ? { 'content-type': 'application/json' } : {}),
@@ -13,6 +13,7 @@ export const BUY_TOOL_DESCRIPTION = [
13
13
  'When the shopper sends a link to a product, such as a buy.trychannel3.com link, put the whole link in `ask` exactly as they sent it, query string included, with their words beside it: Agentcard buys that exact product from the link, so never describe the link in words instead.',
14
14
  'Tell the shopper what `reply` says, in your own voice, except the products and stores it lists that reach the shopper as cards.',
15
15
  "When `shopper_already_told_looking` is true, the shopper already knows you are working on their request, from a reply or a 👀 on their message, so never acknowledge it again: open with the answer or the question.",
16
+ 'When `shopper_already_read_reply` is true, the shopper already read `reply` as Agentcard wrote it, so never say it again in any words: answer with only what `reply` leaves out, in one short line, or with nothing.',
16
17
  'Show the shopper the cart before you confirm it.',
17
18
  'When `cart_building` is set, the store is still building that cart: tell the shopper in one short line that its total is on its way, never ask them to confirm it, and confirm it only after they say yes to the total in the message Agentcard sends next.',
18
19
  'Ask the shopper for exactly one thing per message: the step `next_action` names, even when `reply` lists more. Never mention a later step, and never ask for the next one until the shopper has done this one.',
@@ -0,0 +1,50 @@
1
+ import type { BuyNextAction } from './client.js';
2
+ import type { BuyToolInput } from './definition.js';
3
+ /** Why a message goes straight to Agentcard. */
4
+ export type DirectReason = 'product_ask' | 'pick' | 'answer' | 'confirm';
5
+ /** The buy call for a message that goes straight to Agentcard, and why it does. */
6
+ export interface DirectCall {
7
+ input: BuyToolInput;
8
+ reason: DirectReason;
9
+ }
10
+ /** What Agentcard last told the shopper, which decides whether a pick or an answer goes straight. */
11
+ export interface DirectContext {
12
+ /**
13
+ * True when the shopper's last turn ended on an answer from Agentcard: its last call returned, and
14
+ * no turn since ran without one. False or left out, only a request to buy goes straight.
15
+ */
16
+ answered?: boolean;
17
+ /** The step Agentcard's last answer asked for, its `next_action`. */
18
+ nextAction?: BuyNextAction | null;
19
+ /** Agentcard's last reply, the question a short answer answers. */
20
+ asked?: string | null;
21
+ /** The products Agentcard's last answer showed, in order, with the `product_id` a pick sends. */
22
+ products?: ReadonlyArray<{
23
+ productId: string | null;
24
+ name: string;
25
+ }>;
26
+ /** The stores Agentcard's last answer showed, with the `store_id` a pick sends. */
27
+ stores?: ReadonlyArray<{
28
+ storeId: string;
29
+ name: string;
30
+ }>;
31
+ }
32
+ /**
33
+ * True when the message asks to buy or book something, read with no context: a shopping verb with what
34
+ * to get, a product link, a product at a named store, or a table. A question, small talk, a pick or an
35
+ * answer, a command to the cart, the shopper's own details and anything about the account read false.
36
+ */
37
+ export declare function isProductAsk(text: string): boolean;
38
+ /**
39
+ * The call a shopper's message makes straight to Agentcard, with no model deciding it, or null when
40
+ * your model decides: see the top of this file for what goes straight. `ask` carries the shopper's
41
+ * words as they sent them, a confirm goes in only for a plain yes to the total Agentcard asked them to
42
+ * confirm, and a pick names its `product_id` or `store_id` only when the message names one exactly.
43
+ *
44
+ * ```ts
45
+ * directCall('find me usb-c cables under $20'); // { input: { ask: 'find me usb-c cables under $20', confirm: false, ... }, reason: 'product_ask' }
46
+ * directCall('what time is it'); // null
47
+ * directCall('yes', { answered: true, nextAction: 'confirm' }); // { input: { ask: 'yes', confirm: true, ... }, reason: 'confirm' }
48
+ * ```
49
+ */
50
+ export declare function directCall(shopperText: string, context?: DirectContext): DirectCall | null;
package/dist/direct.js ADDED
@@ -0,0 +1,237 @@
1
+ /**
2
+ * The buy call a shopper's message makes with no model deciding it: `directCall` reads the message, and
3
+ * what Agentcard last asked the shopper, and returns the call your app sends straight to Agentcard, or
4
+ * null when your model decides. Only three kinds of message go straight:
5
+ *
6
+ * - a request to buy or book something: a shopping verb with what to get (`find me usb-c cables under
7
+ * $20`, `i need a gift for my dad`, `add a toothbrush from walmart to my cart`), a product link, a
8
+ * product at a named store (`aa batteries on amazon`), or a table (`a table for 2 tonight at 7`);
9
+ * - a pick of what Agentcard just showed, by a card's tap (`I'll take the KODAK AA (24 Pack) ($13.49)`,
10
+ * `Order from Shake Shack`), its place (`the first one`, `#2`, `1 and 3`) or a reference (`that one`);
11
+ * - the answer to the question Agentcard just asked: the address, name or code it asked for, a short
12
+ * answer such as `medium` or `size 9`, or a plain yes to the total it asked the shopper to confirm.
13
+ *
14
+ * Everything else goes to the model: a question, small talk, a change of mind (`no`, `wait`, `cancel`),
15
+ * a command to the cart, and anything about the account, an order or how the chat works. A pick or an
16
+ * answer goes straight only while the shopper's last turn ended on Agentcard's answer, so a reply to
17
+ * something your model said reaches your model.
18
+ *
19
+ * The rule is tuned for precision over recall: a message that reaches Agentcard by mistake gets a
20
+ * purchase reply to small talk, while one the model reads by mistake costs only the model's call. On
21
+ * the 188 labelled messages in `test/fixtures/direct-messages.json`, it sends none it should not.
22
+ */
23
+ import { ABOUT, ACKNOWLEDGED, CAPABILITY, CART_COMMAND, CHECKOUT, CHITCHAT, FILLER, asksForDetails, isNewRequest, isPickText, namesStore, plain } from './ack.js';
24
+ /** The longest message that goes straight: a long one is read by the model. */
25
+ const MAX_CHARS = 500;
26
+ /** The steps a shopper answers with their own details or a code. */
27
+ const DETAIL_STEPS = new Set(['shipping_details', 'guest_details', 'verification_code']);
28
+ /** The words of a message: plain, with punctuation, `please` and `thanks` taken out. */
29
+ function wordsOf(text) {
30
+ return plain(text).replace(/[^\p{L}\p{N}\s$'&+./#-]+/gu, ' ').replace(/[!?.,;:]+(?=\s|$)/g, ' ').replace(/[!?,;:]+/g, ' ').replace(/\b(?:please|pls|plz|thanks|thank you|thx|ty)\b/g, ' ').replace(/\s+/g, ' ').trim();
31
+ }
32
+ /** Words a message opens with before it says anything: a greeting or filler. */
33
+ const OPENING = /^(?:(?:hey+|hi+|hello+|yo|hiya|howdy|good (?:morning|afternoon|evening)|ok|okay|so|um|uh|also|and|alright|cool|great)(?: there)?\s+)+/;
34
+ /** A change of mind, a refusal or a stall: never an answer to send on. */
35
+ const TAKEBACK = /^(?:i'?ll think|let me think|thinking|i'?m thinking|let me check|i'?ll check|i'?ll let you know|no|nope|nah|not (?:yet|now|really|sure)|cancel|stop|wait|hold on|hang on|never ?mind|nvm|forget (?:it|that|about it)|start over|nothing|none|neither|later|maybe|idk|i don'?t know|dunno|not sure|hmm+|um+|uh+|lol|lmao|haha+|ok|okay|k|kk|thanks?|ty|cool|nice|great|wow|oh|ah)\b/;
36
+ /** Words that say the message is about something other than buying, wherever they sit. */
37
+ const NOT_SHOPPING = new RegExp([
38
+ 'joke', 'weather', 'forecast', 'news', 'poem', 'riddle', 'lyrics', 'translate', 'homework', 'essay', 'coding', 'python', 'javascript',
39
+ 'bug', 'error', 'crash(?:ed|es)?', 'broken', 'not working', "doesn'?t work", 'refund', 'return policy', 'password', 'log ?in', 'sign ?in',
40
+ 'account', 'api', 'support', 'customer service', 'agent ?card', 'what time', 'time is it', "today'?s date", 'human', 'real person',
41
+ 'cancel', 'track(?:ing)?', "where'?s my", 'status', 'receipt', 'invoice', 'unsubscribe', 'privacy', 'kyc', 'identity', 'verify',
42
+ 'promo', 'coupon', 'discount code', 'charged', 'charge me', 'balance', 'wallet', 'approve', 'approval', 'link',
43
+ '(?:my|a|the|new|your|payment|credit|debit|virtual|bank|visa|mastercard) card', 'card (?:number|details|on file)',
44
+ ]
45
+ .map((word) => `\\b${word}\\b`)
46
+ .join('|'));
47
+ /** A message that opens as a question: the model reads it, unless it asks Agentcard to get something (`can you find me ...`). */
48
+ const QUESTION_START = /^(?:what|whats|what'?s|where|wheres|where'?s|when|why|how|who|whose|which|is|are|was|were|do|does|did|should|shall|will|would|have|has|tell me|explain|any idea|wanna|you)\b/;
49
+ /** A shopping verb and what follows it, after any opening such as `can you`. */
50
+ const SHOP_FRAME = new RegExp('^(?:(?:can|could|would|will) (?:you|u) (?:please )?|please |i\'?d like (?:you )?to |help me )?' +
51
+ '(?:find(?: me| us)?|search(?: for)?|look(?:ing)? for|look up|show me|shop for|buy(?: me)?|order(?: me)?|purchase|get me|get us|grab(?: me)?|pick up|book(?: me)?|reserve|add|' +
52
+ "i want|i need|we need|we want|i'?d like|id like|i would like|i'?m looking for|im looking for|we'?re looking for|i'?m after|im after|" +
53
+ 'need|want|can i get|could i get|can i have|could i have|i\'?ll have|send me|get)\\s+(.+)$');
54
+ /** What a shopping verb takes that names nothing to buy: help, a person, a pronoun, the cart, a question. */
55
+ const NOT_AN_ITEM = /^(?:(?:stuff|things|anything|items|products|everything)(?: (?:on|from|at|in|here|there|online)\b.*)?$|help|some help|assistance|someone|somebody|a human|a person|an agent|to (?!(?:buy|order|get|find|book|reserve|purchase|shop|grab|try)\b)\w+|info|information|advice|more info|you|u|me|it|this|that|these|those|them|nothing|out|lost|more|again|a (?:moment|minute|sec|second|break|nap|hug|vacation|friend)|how|what|where|why|when|who|(?:the |my |our )?(?:cart|basket|order|orders|total|purchases?|history|address)|my)\b/;
56
+ /** A verb that needs an object word after it before it names a thing: `add` alone, or `get` with one word, says too little. */
57
+ const MIN_ITEM_CHARS = 3;
58
+ /** Stores a shopper names by brand, so `aa batteries amazon` reads as a product at a store. */
59
+ const STORES = "amazon|walmart|target|best ?buy|costco|home depot|lowe'?s|ebay|etsy|doordash|door dash|uber ?eats|instacart|cvs|walgreens|nike|adidas|sephora|ulta|macy'?s|nordstrom|ikea|wayfair|chewy|whole foods|trader joe'?s|safeway|kroger|old navy|uniqlo|zara|h&m|rei|staples|office depot|barnes (?:&|and) noble|petco|petsmart|aldi|publix|sam'?s club|shein|temu|newegg|lululemon|patagonia|allbirds|glossier|shake shack|chipotle|starbucks|mcdonald'?s|taco bell|five guys|souvla|opentable|resy|1-800-flowers|zappos|kohl'?s|ace hardware|hollister|abercrombie|gap|apple store|dick'?s sporting goods";
60
+ /** A store's own address, such as `livlyclothing.com`. */
61
+ const DOMAIN = '[a-z0-9][a-z0-9-]*\\.(?:com|co|net|org|shop|store|us|uk|ca|io|co\\.uk)';
62
+ /** A thing at a named store: `aa batteries from amazon`, `bows at livlyclothing.com`, `aa batteries amazon`, `tacos near me`. */
63
+ const AT_STORE = new RegExp(`^(?:a |an |some |\\d+ )?[\\p{L}\\p{N}$'&+./-]+(?: [\\p{L}\\p{N}$'&+./-]+){0,9} (?:(?:(?:from|at|on|off) (?:the )?(?:${STORES}|${DOMAIN}))(?: [\\p{L}\\p{N}$'&+./-]+){0,6}|(?:${STORES})|near me)$`, 'u');
64
+ /** Words a thing never opens with: a pronoun or a verb of the chat. */
65
+ const NOT_A_THING = /^(?:i|i'?m|im|i'?ll|ill|i'?ve|ive|we|we'?re|you|u|your|it|its|it'?s|this|that|these|those|they|he|she|my|our|let|lets|let'?s|thank|thanks|just|still|so|not|no|yes|cool|nice|great|love|like|hate|wish|hope|think|guess|really|very|too)\b/;
66
+ /** A table, or a reservation, for a number of people. */
67
+ const TABLE = /^(?:(?:a |an |book (?:a |me a |us a )?)?(?:\w+ )?)?(?:table|reservation|res|booking) for (?:\d{1,2}|one|two|three|four|five|six|seven|eight|ten|twelve)\b/;
68
+ /** A count or size a product is named by: `81 ct`, `30 rolls`, `160ct`, `6 pack`, `12 oz`. */
69
+ const COUNTED = /\b\d+(?:\.\d+)? ?(?:ct|count|pk|pack|packs|rolls?|pcs|pieces|oz|fl oz|ml|lb|lbs|kg|g|gb|tb|mah|inch|in|ft|qt|quart|gallon|gal|capsules|tablets|caplets|gels|bars|cans|bottles)\b/;
70
+ /** Words that tell of something that already happened, not something to get: `returned 3 bottles`, `the 12 pack never came`. */
71
+ const HAPPENED = /\b(?:returned|bought|ordered|got|received|have|had|has|was|were|is|are|did|never|came|arrived|missing|broke|broken|delivered|shipped|yesterday|last)\b/;
72
+ /** A purchase that already happened, which the shopper tells of rather than asks for: `returned my nike shoes yesterday`. */
73
+ const PAST = /\b(?:returned|bought|ordered|received|arrived|came|never|yesterday|last (?:week|time|month|night)|already|refund(?:ed)?|expired)\b/;
74
+ /** A correction of what came before, such as `actually i need AAA not AA`: the model reads it with what it corrects. */
75
+ const CORRECTION = /^(?:actually|instead|no|nah|nope|wait|oh wait|hmm|oops|sorry|i meant|meant)\b|\bnot\b|\binstead\b/;
76
+ /** Words that point at something only the shopper knows, such as `the usual` or `my home address`, or at a step to repeat. */
77
+ const REFERS = /\b(?:whatever|anything|up to you|you pick|you choose|your call|surprise me|dealer'?s choice|usual|same as|as before|like before|like last time|my (?:home|work|office|regular|normal)\b|code|again)\b/;
78
+ /** A complaint or a joke at a price or a store, which asks for nothing: `another $9.99 delivery fee on doordash`, `duracell is a ripoff`. */
79
+ const COMPLAINT = /\b(?:fees?|ripoff|rip off|scam|overpriced|ugh|smh|annoying|terrible|worst|sucks|hate|love that|tbh|lol|lmao|wtf|seriously|bruh)\b/;
80
+ const LINK = /\bhttps?:\/\/\S+|\bwww\.\S+/i;
81
+ /**
82
+ * True when the message asks to buy or book something, read with no context: a shopping verb with what
83
+ * to get, a product link, a product at a named store, or a table. A question, small talk, a pick or an
84
+ * answer, a command to the cart, the shopper's own details and anything about the account read false.
85
+ */
86
+ export function isProductAsk(text) {
87
+ const raw = String(text ?? '');
88
+ if (raw.length > MAX_CHARS)
89
+ return false;
90
+ const words = wordsOf(raw);
91
+ if (!/[\p{L}]/u.test(words))
92
+ return false;
93
+ const bare = words.replace(OPENING, '');
94
+ if (LINK.test(raw))
95
+ return !QUESTION_START.test(bare) && !NOT_SHOPPING.test(words.replace(/\S*https?:\/\/\S+|\S*www\.\S+/g, ''));
96
+ if (!isNewRequest(raw) || CAPABILITY.test(words) || NOT_SHOPPING.test(words) || PAST.test(words) || CORRECTION.test(words) || REFERS.test(words) || COMPLAINT.test(words))
97
+ return false;
98
+ // A question goes to the model, unless it asks Agentcard to get something: `can you find me usb-c cables?`.
99
+ const framed = SHOP_FRAME.exec(bare);
100
+ if (/\?/.test(raw) && !(framed && /^(?:can|could|would|will) (?:you|u)\b/.test(bare)))
101
+ return false;
102
+ if (QUESTION_START.test(bare))
103
+ return false;
104
+ if (framed) {
105
+ const item = framed[1].trim();
106
+ return item.length >= MIN_ITEM_CHARS && !NOT_AN_ITEM.test(item);
107
+ }
108
+ if (TABLE.test(bare))
109
+ return true;
110
+ // A product named with its count or size, such as `tide pods 81 ct` or `kirkland toilet paper 30 rolls`, with no verb.
111
+ if (COUNTED.test(bare) && !NOT_A_THING.test(bare) && !HAPPENED.test(bare) && bare.split(' ').length <= 10)
112
+ return true;
113
+ return AT_STORE.test(bare) && !NOT_A_THING.test(bare);
114
+ }
115
+ /** A plain yes: to a total, the go-ahead that confirms it. */
116
+ const YES = /^(?:y|ya|yes+|yeah|yea|yep|yup|sure|confirm(?:ed)?|sounds good|looks good|perfect|yes (?:please|place it|go ahead|do it|confirm|that'?s right|correct))$/;
117
+ function isYes(words) {
118
+ if (YES.test(words) || CHECKOUT.test(words))
119
+ return true;
120
+ // A yes, then the go-ahead in other words: `yep go ahead`, `ok do it`, `yes, place the order`, `sure, sounds good`.
121
+ const rest = words.replace(FILLER, '');
122
+ return rest !== words && (rest === '' || YES.test(rest) || CHECKOUT.test(rest));
123
+ }
124
+ /** Words that open a new request or a question rather than a short answer. */
125
+ const REQUEST_START = /^(?:find|search|look|show|get|order|buy|book|can|could|would|will|what|which|how|is|are|do|does|did|i want|i need|please)\b/;
126
+ /** Words that open an objection to giving details: the model answers it. */
127
+ const OBJECTION = /\b(?:expired|wrong|invalid|another|new code|resend|lol|don'?t|do not|won'?t|didn'?t|did not|haven'?t|not|never|no|rather not|why|what|where|how|privacy|safe|secure|instead|different|change)\b/;
128
+ /** Words that weigh a product rather than pick it: `duracell is too expensive`, `energizer instead of these`. */
129
+ const DOUBT = /\b(?:far|close|closest|open|hours|distance|minutes|rating|reviews|not|no|too|expensive|cheaper|pricey|better|worse|than|instead|vs|versus|or|compare|difference|why|isn'?t|aren'?t|don'?t|doesn'?t|hate|meh)\b/;
130
+ const ORDINALS = { first: 1, second: 2, third: 3, fourth: 4, fifth: 5, sixth: 6, seventh: 7, eighth: 8 };
131
+ /** The places in a list a message names, such as `1 and 3` or `the second one`, in order: empty when it names none. */
132
+ function placesOf(bare, count) {
133
+ const said = bare.replace(/^(?:i'?ll take|i'?ll do|i'?ll go with|go with|let'?s do|lets do|i want|give me|get me|add|the|both|option|options|number|numbers|item|#)\s*/g, ' ');
134
+ const tokens = said.match(/#?\d{1,2}(?:st|nd|rd|th)?|first|second|third|fourth|fifth|sixth|seventh|eighth|last/g) ?? [];
135
+ // The places stand alone, joined by `and` or a comma, with only `the`, `one`, `ones`, `option` or `#` around them.
136
+ const rest = said.replace(/#?\d{1,2}(?:st|nd|rd|th)?|first|second|third|fourth|fifth|sixth|seventh|eighth|last|\band\b|&|\+|,|\bthe\b|\bones?\b|\boptions?\b|\bnumbers?\b|\bitems?\b|#|\bplease\b/g, ' ').trim();
137
+ if (tokens.length === 0 || rest !== '')
138
+ return null;
139
+ const places = tokens.map((token) => (token === 'last' ? count : ORDINALS[token] ?? Number(token.replace(/\D/g, ''))));
140
+ return places.every((place) => place >= 1 && place <= count) ? places : null;
141
+ }
142
+ /** Text as lowercase words with single spaces, as the tool compares a product's name with a tap. */
143
+ function nameWords(text) {
144
+ return String(text ?? '').toLowerCase().replace(/[^\p{L}\p{N}]+/gu, ' ').trim();
145
+ }
146
+ /** The pick a message makes of what Agentcard just showed, null when it picks nothing, or false when it counts past the list. */
147
+ function pickOf(raw, bare, context) {
148
+ const products = context.products ?? [];
149
+ const stores = context.stores ?? [];
150
+ const call = (fields) => ({ input: { ask: raw, confirm: false, new_order: false, product_id: null, store_id: null, ...fields }, reason: 'pick' });
151
+ if (stores.length > 0) {
152
+ const said = ` ${nameWords(raw)} `;
153
+ // A store's card names it whole; a store named with the dishes to order from it (`five guys, bacon cheeseburger`) picks it too.
154
+ const named = stores.filter((store) => namesStore(raw, [store.name]) || (nameWords(store.name).length >= 5 && said.includes(` ${nameWords(store.name)} `)));
155
+ if (named.length === 1 && !QUESTION_START.test(bare) && !DOUBT.test(said))
156
+ return call({ store_id: named[0].storeId });
157
+ if (named.length > 0 && (QUESTION_START.test(bare) || DOUBT.test(said)))
158
+ return false;
159
+ if (named.length > 1)
160
+ return call({});
161
+ }
162
+ if (products.length > 0) {
163
+ // A card's tap names the product whole: `I'll take the KODAK AA Alkaline Batteries (24 Pack) ($13.49)`.
164
+ const said = nameWords(raw);
165
+ const named = products.filter((product) => nameWords(product.name).length >= 12 && said.includes(nameWords(product.name)));
166
+ if (named.length === 1)
167
+ return call({ product_id: named[0].productId });
168
+ if (named.length > 1)
169
+ return call({});
170
+ // A word only one product's name holds, such as its brand: `lets do the coppertop ones`, `duracell 24 count is fine`.
171
+ const words = new Set(said.split(' '));
172
+ const owners = (word) => products.filter((product) => nameWords(product.name).split(' ').includes(word));
173
+ const marked = products.filter((product) => nameWords(product.name).split(' ').some((word) => word.length >= 4 && !/^\d/.test(word) && words.has(word) && owners(word).length === 1));
174
+ if (marked.length === 1 && said.split(' ').length <= 10 && !QUESTION_START.test(bare) && !DOUBT.test(said))
175
+ return call({ product_id: marked[0].productId });
176
+ const places = placesOf(bare, products.length);
177
+ if (places)
178
+ return call(places.length === 1 ? { product_id: products[places[0] - 1].productId } : {});
179
+ // A place past the end of the list picks nothing the shopper saw.
180
+ if (placesOf(bare, Number.MAX_SAFE_INTEGER))
181
+ return false;
182
+ if (/^(?:both|all|all of them|both of them|all three|all 3)$/.test(bare))
183
+ return call({});
184
+ }
185
+ if ((products.length > 0 || stores.length > 0) && isPickText(raw))
186
+ return call({});
187
+ return null;
188
+ }
189
+ /**
190
+ * The call a shopper's message makes straight to Agentcard, with no model deciding it, or null when
191
+ * your model decides: see the top of this file for what goes straight. `ask` carries the shopper's
192
+ * words as they sent them, a confirm goes in only for a plain yes to the total Agentcard asked them to
193
+ * confirm, and a pick names its `product_id` or `store_id` only when the message names one exactly.
194
+ *
195
+ * ```ts
196
+ * directCall('find me usb-c cables under $20'); // { input: { ask: 'find me usb-c cables under $20', confirm: false, ... }, reason: 'product_ask' }
197
+ * directCall('what time is it'); // null
198
+ * directCall('yes', { answered: true, nextAction: 'confirm' }); // { input: { ask: 'yes', confirm: true, ... }, reason: 'confirm' }
199
+ * ```
200
+ */
201
+ export function directCall(shopperText, context = {}) {
202
+ const raw = String(shopperText ?? '').trim();
203
+ if (raw === '' || raw.length > MAX_CHARS)
204
+ return null;
205
+ const ctx = context ?? {};
206
+ const words = wordsOf(raw);
207
+ if (!/[\p{L}\p{N}]/u.test(words))
208
+ return null;
209
+ const bare = words.replace(OPENING, '') || words;
210
+ const plainCall = (reason, confirm = false) => ({ input: { ask: raw, confirm, new_order: false, product_id: null, store_id: null }, reason });
211
+ if (ctx.answered === true && !/\?/.test(raw)) {
212
+ const told = CHITCHAT.test(words) || ABOUT.test(words) || CART_COMMAND.test(words) || CAPABILITY.test(words);
213
+ if (ctx.nextAction === 'confirm' && isYes(words))
214
+ return plainCall('confirm', true);
215
+ // A question, a story of what happened, an objection or a change of mind is the model's to answer, whatever came before.
216
+ const aside = QUESTION_START.test(bare) || REFERS.test(words) || COMPLAINT.test(words) || PAST.test(words) || NOT_SHOPPING.test(words) || OBJECTION.test(words) || CORRECTION.test(words);
217
+ if (!told && !aside && !TAKEBACK.test(words)) {
218
+ const pick = pickOf(raw, bare, ctx);
219
+ if (pick === false)
220
+ return null;
221
+ if (pick)
222
+ return pick;
223
+ // The address, name or code Agentcard asked for: what the shopper sends is the answer, unless it objects.
224
+ const details = (typeof ctx.nextAction === 'string' && DETAIL_STEPS.has(ctx.nextAction)) || asksForDetails(ctx.asked);
225
+ if (details && !OBJECTION.test(words) && !REQUEST_START.test(bare) && !QUESTION_START.test(bare) && !PAST.test(words) && !/\b(?:same as|on file|you have|the usual|ur|your)\b/.test(words) && !/^(?:resend|send|again|still)\b/.test(bare) && !NOT_SHOPPING.test(words) && words.split(' ').length <= 40)
226
+ return plainCall('answer');
227
+ // A short answer to Agentcard's question, such as `medium` after `what size?`, or a yes to `want me to check amazon?`.
228
+ if (/\?/.test(String(ctx.asked ?? '')) && ctx.nextAction !== 'confirm' && !NOT_SHOPPING.test(words)) {
229
+ if (isYes(words))
230
+ return plainCall('answer');
231
+ if (!ACKNOWLEDGED.test(words) && words.split(' ').length <= 4 && !REQUEST_START.test(bare))
232
+ return plainCall('answer');
233
+ }
234
+ }
235
+ }
236
+ return isProductAsk(raw) ? plainCall('product_ask') : null;
237
+ }
package/dist/events.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * name. The stream (`agentcard.listen`) and a webhook endpoint carry the same types, with the same
4
4
  * envelope and the same ids. A test holds this list to the backend's catalog.
5
5
  */
6
- export declare const AGENTCARD_EVENT_TYPES: readonly ["card.created", "card.updated", "card.closed", "cardholder.created", "cardholder.updated", "transaction.authorized", "transaction.declined", "transaction.cleared", "transaction.voided", "balance.low", "cardholder_onboarding_session.completed", "platform_connect_session.completed", "merchant.connected", "order.placed", "order.confirmed", "order.failed", "order.updated", "purchase.message", "connection.created", "connected_card.updated", "vault.card_stored", "vault.session_linked", "vault.session_card_picked", "vault.payment_permission.updated", "action.confirmed", "action.done", "checkout_authorization.approved", "checkout_authorization.submitted", "checkout_authorization.declined", "checkout_authorization.cancelled", "checkout_authorization.expired", "checkout_authorization.duplicate_charge", "checkout_authorization.refused", "checkout_authorization.watched", "checkout_authorization.amount_mismatch", "checkout_authorization.settled", "checkout_authorization.settlement_unconfirmed", "checkout_authorization.outcome_reported", "card_flow.started", "card_flow.failed", "transfer.approved", "transfer.initiated", "transfer.completed", "transfer.failed", "transfer.released", "wallet.funded", "user_wallet.funding_detected", "user_wallet.funded", "wallet.balance.low", "recovery.requested", "recovery.completed", "recovery.rejected", "wallet.withdrawal.initiated", "wallet.withdrawal.completed", "wallet.withdrawal.failed", "identity.verification.updated", "approval.requested", "reward.earned", "reward.reversed", "wallet_link.opened"];
6
+ export declare const AGENTCARD_EVENT_TYPES: readonly ['card.created', 'card.updated', 'card.closed', 'cardholder.created', 'cardholder.updated', 'transaction.authorized', 'transaction.declined', 'transaction.cleared', 'transaction.voided', 'balance.low', 'cardholder_onboarding_session.completed', 'platform_connect_session.completed', 'merchant.connected', 'order.placed', 'order.confirmed', 'order.failed', 'order.updated', 'purchase.message', 'connection.created', 'connected_card.updated', 'vault.card_stored', 'vault.session_linked', 'vault.session_card_picked', 'vault.payment_permission.updated', 'action.confirmed', 'action.done', 'checkout_authorization.approved', 'checkout_authorization.submitted', 'checkout_authorization.declined', 'checkout_authorization.cancelled', 'checkout_authorization.expired', 'checkout_authorization.duplicate_charge', 'checkout_authorization.refused', 'checkout_authorization.watched', 'checkout_authorization.amount_mismatch', 'checkout_authorization.settled', 'checkout_authorization.settlement_unconfirmed', 'checkout_authorization.outcome_reported', 'card_flow.started', 'card_flow.failed', 'transfer.approved', 'transfer.initiated', 'transfer.completed', 'transfer.failed', 'transfer.released', 'wallet.funded', 'user_wallet.funding_detected', 'user_wallet.funded', 'wallet.balance.low', 'recovery.requested', 'recovery.completed', 'recovery.rejected', 'wallet.withdrawal.initiated', 'wallet.withdrawal.completed', 'wallet.withdrawal.failed', 'identity.verification.updated', 'approval.requested', 'reward.earned', 'reward.reversed', 'wallet_link.opened'];
7
7
  /** One event type Agentcard sends. A type Agentcard adds later still reads as its own name. */
8
8
  export type AgentcardEventType = (typeof AGENTCARD_EVENT_TYPES)[number];
9
9
  /** Whether a filter entry names something Agentcard sends: an event type, a family such as `order.*`, or `*`. */
package/dist/index.d.ts CHANGED
@@ -2,12 +2,13 @@ export { verifyAgentcardWebhook, AgentcardSignatureError, type AgentcardSignatur
2
2
  export { readPurchaseMessage, type PurchaseMessage, type PurchaseMessageKind, type PurchaseFacts, } from './message.js';
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 { agentcardWebhook, purchaseAgentWebhook, type AgentcardWebhookOptions, type AgentcardWebhookHandler, type PurchaseAgentWebhookOptions, type WebhookFetchRequest, type WebhookRequest, type WebhookResponse, } from './handler.js';
5
- export { AgentcardClient, AgentcardApiError, BUY_TURN_TIMEOUT_MS, DEFAULT_BASE_URL, type AgentcardClientOptions, type FetchLike, type FetchLikeResponse, type BuyProgress, type BuyProgressStage, type BuyCatalogUpdate, type BuyStreamHandlers, type Shopper, 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, } from './client.js';
5
+ 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 BuyFoundUpdate, type BuyTurnStatus, type BuySegmentUpdate, type BuyCartUpdate, type BuyDeltaUpdate, type BuyStreamHandlers, type Shopper, 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, } from './client.js';
6
6
  export { BUY_TOOL_NAME, BUY_TOOL_DESCRIPTION, buyToolDefinition, parseBuyToolInput, type BuyToolDefinition, type BuyToolInput, type BuyToolInputSchema, type BuyToolInputCheck, type BuyToolStandardSchema, type OpenAIBuyTool, } from './definition.js';
7
7
  export { toMessages, messageStream, typingDelay, type MessagePace, type MessageStream, type MessageStreamOptions, } from './messages.js';
8
- export { buyTool, nextStepPrompt, type BuyTool, type BuyToolOptions, type BuyToolResult, type BuyToolStatus, type BuyToolLink, type BuyToolProduct, type BuyToolProductAction, productAction, type BuyToolStore, type BuyToolTurn, type BuyToolTurnItem, type BuyToolTurnOptions, type BuyToolTurnMessage, type BuyAckMode, } from './buy-tool.js';
8
+ export { buyTool, nextStepPrompt, type BuyTool, type BuyToolOptions, type BuyToolResult, type BuyToolStatus, type BuyToolLink, type BuyToolFound, type BuyToolProduct, type BuyToolProductAction, productAction, type BuyToolStore, type BuyToolTurn, type BuyToolTurnItem, type BuyToolTurnOptions, type BuyToolTurnMessage, type BuyAckMode, } from './buy-tool.js';
9
9
  export { instantAck, instantReaction, ackFor, keepsYes, ACK_REACTION, type AckForOptions } from './ack.js';
10
- export { forModel, forModelError, replyAsWritten, stripLinks, type BuyToolModelView, type BuyToolModelCart, type BuyToolModelProduct, type BuyToolModelStore, } from './model-view.js';
10
+ export { directCall, isProductAsk, type DirectCall, type DirectContext, type DirectReason } from './direct.js';
11
+ export { forModel, forModelError, replyAsWritten, withoutCardList, stripLinks, type BuyToolModelView, type BuyToolModelCart, type BuyToolModelProduct, type BuyToolModelStore, } from './model-view.js';
11
12
  export { toAnthropicTool, toOpenRouterTool, type AnthropicBuyTool, type OpenRouterToolkit, type ZodLike, } from './adapters.js';
12
13
  export { listenToAgentcard, type AgentcardListener, type ListenOptions, type ListenCursorStore, type StreamFetch, } from './listen.js';
13
14
  export { ACTION_LINK_ENDINGS, ACTION_LINK_KINDS, actionLinkId, actionLinkOutcome, isActionLink, type ActionLinkKind, type ActionLinkMessage, type ActionLinkOutcome, } from './action-links.js';