@agent-cards/commerce-sdk 0.13.0 → 0.15.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/README.md CHANGED
@@ -236,6 +236,28 @@ history = written !== null ? [...runner.params.messages, { role: 'assistant', co
236
236
 
237
237
  Any other answer, such as an approval sent, an order placed or a refusal, leaves `turn.replyAsWritten` null, and your model words it. So does a line the tool writes itself for your model, such as the note that the cart changed or a `buy.nextStep` turn's ask for the shopper's yes: only a reply Agentcard sent goes out as written. `replyAsWritten(result)` applies the same rule to one answer, for a loop that reads `turn.results` itself. Leave the option off to have your model write every answer.
238
238
 
239
+ ### Send a clear shopping message straight to Agentcard
240
+
241
+ Your model's first call on most messages only decides to call the buy tool. Pass `direct: true` to `buyTool`, and each turn reads its message by rule, with no model, and sets `turn.direct` when the message goes straight to Agentcard: a request to buy or book something (`find me usb-c cables under $20`, `aa batteries on amazon`), a pick of what Agentcard just showed (a card's tap, `the first one`, `1 and 3`), or the answer to the question Agentcard just asked (the address, a code, `medium`, or a plain yes to the total it asked the shopper to confirm). Send the call yourself, and with `replyAsWritten` the turn makes no model call at all:
242
+
243
+ ```ts
244
+ const turn = buy.turn(message, { onMessage: (text) => sendText(shopper.id, text) });
245
+ let messages = turn.messages(history);
246
+ if (turn.direct) {
247
+ const id = `toolu_direct_${crypto.randomUUID().replace(/-/g, '')}`;
248
+ const content = await turn.run(turn.direct.input);
249
+ messages = [...messages, { role: 'assistant', content: [{ type: 'tool_use', id, name: turn.name, input: turn.direct.input }] }, { role: 'user', content: [{ type: 'tool_result', tool_use_id: id, content }] }];
250
+ if (turn.replyAsWritten !== null) {
251
+ await turn.say(turn.replyAsWritten);
252
+ history = [...messages, { role: 'assistant', content: turn.replyAsWritten }];
253
+ return;
254
+ }
255
+ }
256
+ // Otherwise run your model on `messages` as before: it words the answer to the call already made, or decides the call itself.
257
+ ```
258
+
259
+ Everything else leaves `turn.direct` null for your model: a question, small talk, a change of mind such as `no` or `wait`, a command to the cart, and anything about the account or an order. A pick or an answer goes straight only when the shopper's last turn ended on Agentcard's answer, so a reply to something your model said reaches your model. `directCall(text, context)` applies the same rule to one message, for an app that keeps its own state. The option is off by default, because it moves the decision of when to call Agentcard from your model into the tool.
260
+
239
261
  ### Hand the app the turn in order
240
262
 
241
263
  A turn reaches the shopper as messages and links that interleave: the instant reply, the cards a search found, then your model's words about them. Pass `onItem` to `buy.turn`, and the turn hands your app all of them as one sequence, one item at a time, in the order they happened. Show each item below the one before it:
@@ -254,7 +276,7 @@ for await (const stream of runner) {
254
276
  }
255
277
  ```
256
278
 
257
- - A `text` item is the instant reply, or a message your model wrote through `turn.messageStream()` or `turn.say(text)`. A `link` item is each link the tool hands over, with the fields `sendLink` would get.
279
+ - A `text` item is the instant reply, or a message your model wrote through `turn.messageStream()` or `turn.say(text)`. A `link` item is each link the tool hands over, with the fields `sendLink` would get. A message of Agentcard's that `onText` takes holds its place in `turn.items` as a `text` item too, and reaches `onText` alone.
258
280
  - Each message goes out the moment it is ready. Pass `pace: true` to space them like someone texting: each one then waits until `typingDelay` of it has passed since the item before it went out, 300 ms plus 12 ms a character, at most 1.5 seconds. Time your model spent writing counts toward the pause, so a slow model gets no added wait, and links never wait. `onTyping` hears `true` when a pause starts and `false` when its message goes out. A function returns your own pause in milliseconds.
259
281
  - With `onItem`, the turn sends no link to a `sendLink` and no instant reply to an `onWorking`, so a tool built with those for other turns sends nothing twice. Passing `onItem` beside `sendLink` or `onWorking` to the same turn throws.
260
282
  - `turn.items` holds every item that went out, in order, so an app that answers the whole turn at once returns it as is. A turn without `onItem` keeps `turn.items` as well, unpaced.
@@ -418,18 +440,36 @@ Without a buy tool, `instantReaction(message, { nextAction, asked, stores })` an
418
440
 
419
441
  ## Show what Agentcard is doing
420
442
 
421
- Pass `onProgress`, and the tool asks Agentcard to stream each call. Each stage the call reaches goes to `onProgress` as it happens, `searching`, `building_cart`, `checking_out` or `working`, for a status line such as "searching amazon":
443
+ The tool streams every call, so each part of an answer reaches your app the moment Agentcard produces it: the stage the call reached, the products a search found, each message Agentcard writes and the cart it builds. Pass a callback for each part you show:
422
444
 
423
445
  ```ts
424
446
  const buy = buyTool({
425
447
  agentcard,
426
448
  externalUserId: shopper.id,
427
449
  sendLink: (link) => textShopper(shopper.id, link.url),
428
- onProgress: ({ stage }) => showStatus(shopper.id, stage),
450
+ onStatus: ({ text }) => showStatus(shopper.id, text),
451
+ onFound: ({ shown, found, more }) => showCount(shopper.id, `${shown} of ${found} shown`, more),
452
+ onProducts: (products, { merchantName }) => showProducts(shopper.id, merchantName, products),
453
+ onText: (text) => textShopper(shopper.id, text),
454
+ onCart: (cart, { carts }) => showCart(shopper.id, cart, carts),
429
455
  });
430
456
  ```
431
457
 
432
- The product cards still go out with the answer, so each card is a product the reply names. `earlyCards: true` sends the search's first three cards the moment the search answers instead, a few seconds sooner, and the answer then sends only the cards it adds. The reply often recommends other products than the search's first three, so with `earlyCards` a shopper can hold cards the reply never names. A stream that breaks after the call started reads the conversation for what the turn left, as a turn past Agentcard's budget does. Pass `stream: false` to wait for each whole answer.
458
+ - `onStatus` hears each step the moment Agentcard starts it, as a line to show in place of loading dots: `searching Target and Walmart`, `adding it to your cart`, `working through the store's checkout`. `step` names the step, such as `searching` or `adding_to_cart`, for an app that writes its own words. A call that only writes its reply sends none, so keep your dots until the first line comes.
459
+ - `onFound` hears how many products the searches found, after the cards it counts: `shown` cards of `found` products, such as "8 of 23 shown", each time the count grows. `more` holds the products past the cards as product links, in the order they were found, for a "show more" that runs no new search. The model never reads them.
460
+ - `onProgress` still hears the stage, `searching`, `building_cart`, `checking_out` or `working`.
461
+ - `onAvailability` hears whether the places a booking's cards show have room at the time the shopper asked for. A place's card can go out before Agentcard's live search knows: its `availability` reads `checking`, so show it without its choose button. The update names each card by its `productId` with `available` (its open times in `slots`, empty when none were named), `unavailable` (full then) or `unknown` (the search could not say). Change the card in place, and offer it as a choice only when it reads `available`. A call that ends with a card still `checking` sends `unknown` for it. Every other card, a retail product's included, carries no `availability`.
462
+ - `onProducts` hears the products a search found the moment it answers, before Agentcard writes its reply, with the conversation, when the search ran (`asOf`) and the store's name. With `sendLink`, each product's card goes out then too, a few seconds before the answer, and the answer sends only the cards it adds. Pass `earlyCards: false` to send only the cards the answer names, with the answer: the reply often names only some of a search's products.
463
+ - `onText` hears each message Agentcard writes, links included, the moment it is written, and after the cards of the search it is about.
464
+ - `onCart` hears the cart a confirm would place, or null, and every cart on the table, each time a call builds or changes one, before the message that announces it.
465
+
466
+ `buy.turn` takes the same callbacks, and each one left out falls back to the tool's. A turn's `onText` hears each message in its place in the turn, after the instant reply and the cards, paced by `pace`, and `turn.items` lists it; `onItem` does not hear it, so an app that passes both hears each message once.
467
+
468
+ When your app shows Agentcard's messages through `onText`, the model reads `shopper_already_read_reply: true` in that call's answer and adds nothing to them, as it reads `shopper_already_told_looking` after an instant reply: end the turn without a second model call, as with `replyAsWritten`, or let the model add one short line for what the reply left out. `turn.replyAsWritten` stays null for that answer, so the reply never reaches the shopper twice, and a step link the turn holds for your model's next message goes out right after Agentcard's ask instead.
469
+
470
+ A stream that breaks after the call started reads the conversation for what the turn left, as a turn past Agentcard's budget does. Pass `stream: false` to wait for each whole answer, with its cards sent beside it.
471
+
472
+ Code that calls the client itself streams with `agentcard.buy(body, handlers)`: `onCatalog`, `onSegment`, `onCart` and `onDelta` hear their events in the order Agentcard sent them, each once the one before it settled, and the answer waits for the last of them. `onDelta` asks Agentcard for the pieces of each message as it writes them, a draft that each `onSegment` message replaces.
433
473
 
434
474
  ## Send each paragraph as its own message
435
475
 
@@ -492,6 +532,7 @@ The model reads one object per call:
492
532
  "approval_expires_at": null,
493
533
  "shopper_need_not_reply": false,
494
534
  "shopper_already_told_looking": false,
535
+ "shopper_already_read_reply": false,
495
536
  "link_sent": false,
496
537
  "product_links_sent": 0,
497
538
  "pick": null,
package/dist/ack.d.ts CHANGED
@@ -14,6 +14,28 @@
14
14
  * instant reply at all, because it starts no search and Agentcard answers it fast. So does a pick of a
15
15
  * store the shopper saw, such as `order here`, or `Order from Tacos El Grullense` as a store card sends it.
16
16
  */
17
+ /** Words a reply opens with before it says what it means, such as `ok` in `ok lets checkout` or `yes,` in `yes, sounds good`. */
18
+ export declare const FILLER: RegExp;
19
+ /** A bare acknowledgement, such as `its ok`, `thats fine`, `sounds good` or `works for me`, after the filler comes off. */
20
+ export declare const ACKNOWLEDGED: RegExp;
21
+ /** A step toward paying for the cart already on the table, such as `lets checkout`, `place the order` or `pay`, after the filler comes off. */
22
+ export declare const CHECKOUT: RegExp;
23
+ /**
24
+ * A question about the conversation, an order or how things work, such as `how did it know to msg that
25
+ * number?`, `is it booked?` or `where is my order`: the model answers it straight away, with no search.
26
+ */
27
+ export declare const ABOUT: RegExp;
28
+ /** A command to the cart or the order, such as `lets clear the cart`, `remove the taco` or `change the quantity to 2`. */
29
+ export declare const CART_COMMAND: RegExp;
30
+ /** Small talk that asks for nothing to be bought, after `please` and `thanks` come off. */
31
+ export declare const CHITCHAT: RegExp;
32
+ /**
33
+ * A question about what the agent can buy or where it shops, such as `what stores can you buy from`,
34
+ * `where can you shop` or `which stores do you support`: the model answers it in a line or two, with no
35
+ * search, so no instant reply says it is looking. A question that names a product, such as `which stores
36
+ * can you buy aa batteries from`, is a search and keeps its reply.
37
+ */
38
+ export declare const CAPABILITY: RegExp;
17
39
  /**
18
40
  * True when the message names one of these stores and nothing else to buy: the store's whole name, or
19
41
  * two words of it in a row, such as `el grullense` for `Tacos El Grullense`, alone or after a frame such
@@ -23,6 +45,8 @@
23
45
  export declare function namesStore(text: string, storeNames: readonly string[] | null | undefined): boolean;
24
46
  /** True when the shopper's words pick a product they already saw, such as `the first one`, `2` or a card's title with its price. */
25
47
  export declare function isPickText(text: string): boolean;
48
+ /** The text as one plain line: lowercase, no quotes, and no dash between words. */
49
+ export declare function plain(text: string): string;
26
50
  /**
27
51
  * True when the shopper's words start a request, and false for an answer to a question, a pick, a
28
52
  * code, small talk, or a message that carries the shopper's own details, such as a phone number or a
package/dist/ack.js CHANGED
@@ -31,11 +31,11 @@ const TEMPLATES = [
31
31
  */
32
32
  const WAITS = ['one moment', 'just a sec', 'working on it', 'on it, one sec', 'hang tight', 'one sec, working on it'];
33
33
  /** Words a reply opens with before it says what it means, such as `ok` in `ok lets checkout` or `yes,` in `yes, sounds good`. */
34
- const FILLER = /^(?:(?:ok|okay|k|kk|yes|yeah|yea|yep|yup|ya|sure|alright|all right|cool|great|perfect|nice|so|then|and|um|uh|oh|well|hmm|now)[\s,]+)+/;
34
+ export const FILLER = /^(?:(?:ok|okay|k|kk|yes|yeah|yea|yep|yup|ya|sure|alright|all right|cool|great|perfect|nice|so|then|and|um|uh|oh|well|hmm|now)[\s,]+)+/;
35
35
  /** A bare acknowledgement, such as `its ok`, `thats fine`, `sounds good` or `works for me`, after the filler comes off. */
36
- const ACKNOWLEDGED = /^(?:ok|okay|k|kk|fine|alright|all right|cool|great|perfect|good|nice|sure|yes|yeah|yep|yup|right|correct|exactly|agreed|works|(?:that|this|it) works|(?:it|that|this)(?:'?s| is) (?:ok|okay|fine|good|great|perfect|cool|right|correct|alright|all right|all good|it)|its (?:ok|okay|fine|good|great|perfect|cool|right|correct|alright|all good)|thats (?:ok|okay|fine|good|great|perfect|cool|right|correct|alright|it|all)|sounds (?:good|great|fine|perfect|right|ok|okay)|looks (?:good|great|fine|perfect|right|ok|okay)|all good|all set|no worries|no problem|np|got it|gotcha|understood|makes sense|(?:works|fine|ok|okay|good) (?:for|by|with) me|i'?m good|im good|we'?re good)(?: (?:then|now|with me|for me|to me))?$/;
36
+ export const ACKNOWLEDGED = /^(?:ok|okay|k|kk|fine|alright|all right|cool|great|perfect|good|nice|sure|yes|yeah|yep|yup|right|correct|exactly|agreed|works|(?:that|this|it) works|(?:it|that|this)(?:'?s| is) (?:ok|okay|fine|good|great|perfect|cool|right|correct|alright|all right|all good|it)|its (?:ok|okay|fine|good|great|perfect|cool|right|correct|alright|all good)|thats (?:ok|okay|fine|good|great|perfect|cool|right|correct|alright|it|all)|sounds (?:good|great|fine|perfect|right|ok|okay)|looks (?:good|great|fine|perfect|right|ok|okay)|all good|all set|no worries|no problem|np|got it|gotcha|understood|makes sense|(?:works|fine|ok|okay|good) (?:for|by|with) me|i'?m good|im good|we'?re good)(?: (?:then|now|with me|for me|to me))?$/;
37
37
  /** A step toward paying for the cart already on the table, such as `lets checkout`, `place the order` or `pay`, after the filler comes off. */
38
- const CHECKOUT = /^(?:(?:let'?s|lets|i want to|we want to|i'?d like to|id like to|i'?m ready to|im ready to|ready to|can you|could you|can we|go ahead and|just|please)\s+)*(?:check ?out|pay|pay now|pay for (?:it|this|that|them|the order|my order|everything)|place (?:the |my |this |that |our )?order|place it|confirm(?: (?:the|my) (?:order|purchase|cart)| it)?|complete (?:the |my )?(?:order|purchase|checkout)|finish (?:the |my )?(?:order|purchase|checkout)|proceed(?: to (?:checkout|payment|pay))?|go ahead|go for it|buy (?:it|them|this|that|these|those)(?: now)?|order it|book it|submit (?:the |my )?order|send (?:the |my )?order|do it|go|ready|i'?m ready|im ready)(?: now| then| already)?$/;
38
+ export const CHECKOUT = /^(?:(?:let'?s|lets|i want to|we want to|i'?d like to|id like to|i'?m ready to|im ready to|ready to|can you|could you|can we|go ahead and|just|please)\s+)*(?:check ?out|pay|pay now|pay for (?:it|this|that|them|the order|my order|everything)|place (?:the |my |this |that |our )?order|place it|confirm(?: (?:the|my) (?:order|purchase|cart)| it)?|complete (?:the |my )?(?:order|purchase|checkout)|finish (?:the |my )?(?:order|purchase|checkout)|proceed(?: to (?:checkout|payment|pay))?|go ahead|go for it|buy (?:it|them|this|that|these|those)(?: now)?|order it|book it|submit (?:the |my )?order|send (?:the |my )?order|do it|go|ready|i'?m ready|im ready)(?: now| then| already)?$/;
39
39
  /** Whole replies that answer a question the shopper was asked, after `please` and `thanks` come off. */
40
40
  const ANSWERS = new RegExp('^(?:' +
41
41
  [
@@ -53,9 +53,9 @@ const STATUS = /^(?:(?:ok|okay|yes|yep|yeah|alright|cool|great|so|done|there)[\s
53
53
  * A question about the conversation, an order or how things work, such as `how did it know to msg that
54
54
  * number?`, `is it booked?` or `where is my order`: the model answers it straight away, with no search.
55
55
  */
56
- const ABOUT = /^(?:(?:ok|okay|so|wait|hmm|um|uh|and|but)[\s,]+)*(?:how (?:did|does|do|come|is|was|were|are)|why (?:did|does|do|is|was|are|were|would|didn'?t|isn'?t|won'?t)|what(?:'s| is| was) (?:my|the status|happening|going on|that|this)|what happened|where(?:'s| is| are) (?:my|the order|it)|(?:is|was|has|did|will) (?:it|my|this|that|the order|the booking|the reservation)|did (?:it|that|you) (?:go|work|book|place|send|get|cancel|charge|order|change|add|remove)|will you (?:let|tell|text|notify|email|update|remind|ping|keep (?:me|us) (?:updated|posted|informed|in the loop))|when (?:will|does|do|is|did) (?:it|my|the order|they)|how long (?:will|does|until|till))\b/;
56
+ export const ABOUT = /^(?:(?:ok|okay|so|wait|hmm|um|uh|and|but)[\s,]+)*(?:how (?:did|does|do|come|is|was|were|are)|why (?:did|does|do|is|was|are|were|would|didn'?t|isn'?t|won'?t)|what(?:'s| is| was) (?:my|the status|happening|going on|that|this)|what happened|where(?:'s| is| are) (?:my|the order|it)|(?:is|was|has|did|will) (?:it|my|this|that|the order|the booking|the reservation)|did (?:it|that|you) (?:go|work|book|place|send|get|cancel|charge|order|change|add|remove)|will you (?:let|tell|text|notify|email|update|remind|ping|keep (?:me|us) (?:updated|posted|informed|in the loop))|when (?:will|does|do|is|did) (?:it|my|the order|they)|how long (?:will|does|until|till))\b/;
57
57
  /** A command to the cart or the order, such as `lets clear the cart`, `remove the taco` or `change the quantity to 2`. */
58
- const CART_COMMAND = new RegExp('^(?:(?:ok|okay|so|actually|wait|and|um|uh)[\\s,]+)*(?:let\'?s |lets |can you |could you |pls |i want to |i\'?d like to |id like to |i need to |go ahead and |just )*(?:' +
58
+ export const CART_COMMAND = new RegExp('^(?:(?:ok|okay|so|actually|wait|and|um|uh)[\\s,]+)*(?:let\'?s |lets |can you |could you |pls |i want to |i\'?d like to |id like to |i need to |go ahead and |just )*(?:' +
59
59
  [
60
60
  // Verbs that never open a search: `remove fries`, `cancel order`.
61
61
  'remove|delete|cancel|undo|start over|take off|get rid of',
@@ -66,14 +66,14 @@ const CART_COMMAND = new RegExp('^(?:(?:ok|okay|so|actually|wait|and|um|uh)[\\s,
66
66
  ].join('|') +
67
67
  ')\\b');
68
68
  /** Small talk that asks for nothing to be bought, after `please` and `thanks` come off. */
69
- const CHITCHAT = /^(?:(?:hi|hey|hello|yo|hiya|howdy|sup)(?: there| again)?|good (?:morning|afternoon|evening|night)|g(?:oo)?d ?night|how are (?:you|u)(?: doing)?(?: today)?|how'?s it going|what'?s up|whats up|wassup|who are (?:you|u)|what (?:can|do) (?:you|u) do|are (?:you|u) (?:a )?(?:bot|real|human|there)|you there|test(?:ing)?|bye|see (?:you|ya)|later|love (?:it|you|u)|awesome|amazing|wow|omg|ha(?:ha)+|lmao|:\)|ok cool|cool thanks|so much|very much|a lot)$/;
69
+ export const CHITCHAT = /^(?:(?:hi|hey|hello|yo|hiya|howdy|sup)(?: there| again)?|good (?:morning|afternoon|evening|night)|g(?:oo)?d ?night|how are (?:you|u)(?: doing)?(?: today)?|how'?s it going|what'?s up|whats up|wassup|who are (?:you|u)|what (?:can|do) (?:you|u) do|are (?:you|u) (?:a )?(?:bot|real|human|there)|you there|test(?:ing)?|bye|see (?:you|ya)|later|love (?:it|you|u)|awesome|amazing|wow|omg|ha(?:ha)+|lmao|:\)|ok cool|cool thanks|so much|very much|a lot)$/;
70
70
  /**
71
71
  * A question about what the agent can buy or where it shops, such as `what stores can you buy from`,
72
72
  * `where can you shop` or `which stores do you support`: the model answers it in a line or two, with no
73
73
  * search, so no instant reply says it is looking. A question that names a product, such as `which stores
74
74
  * can you buy aa batteries from`, is a search and keeps its reply.
75
75
  */
76
- const CAPABILITY = /^(?:(?:ok|okay|so|and|but|hey|hi|um|uh|also|wait)[\s,]+)*(?:(?:what|which|where)(?: (?:kinds?|types?|sorts?) of)?(?: (?:other|else|all|the))?(?: (?:stores?|shops?|places?|sites?|websites?|merchants?|retailers?|restaurants?|brands?|products?|items?|things?|stuff))?(?: else)? (?:can|do|could|will|are) (?:you|u)(?: (?:able to|even|actually|also))? (?:buy|shop|order|get|purchase|do|use|support|work with|deliver|cover|access|sell|carry|offer|stock)(?: (?:from|at|on|in|with|for me|for us|to))?(?: for me)?|(?:what|which) (?:stores?|shops?|merchants?|retailers?|sites?) (?:are )?(?:supported|available|connected|on here)|(?:what|which) (?:stores?|shops?|merchants?|retailers?) do (?:you|u) (?:have|know|use|support|work with)|what (?:else )?(?:do|can) (?:you|u) (?:offer|sell|carry|get (?:me|us)|buy (?:me|us)|order (?:me|us)|find (?:me|us)))(?: (?:right now|here|today))?$/;
76
+ export const CAPABILITY = /^(?:(?:ok|okay|so|and|but|hey|hi|um|uh|also|wait)[\s,]+)*(?:(?:what|which|where)(?: (?:kinds?|types?|sorts?) of)?(?: (?:other|else|all|the))?(?: (?:stores?|shops?|places?|sites?|websites?|merchants?|retailers?|restaurants?|brands?|products?|items?|things?|stuff))?(?: else)? (?:can|do|could|will|are) (?:you|u)(?: (?:able to|even|actually|also))? (?:buy|shop|order|get|purchase|do|use|support|work with|deliver|cover|access|sell|carry|offer|stock)(?: (?:from|at|on|in|with|for me|for us|to))?(?: for me)?|(?:what|which) (?:stores?|shops?|merchants?|retailers?|sites?) (?:are )?(?:supported|available|connected|on here)|(?:what|which) (?:stores?|shops?|merchants?|retailers?) do (?:you|u) (?:have|know|use|support|work with)|what (?:else )?(?:do|can) (?:you|u) (?:offer|sell|carry|get (?:me|us)|buy (?:me|us)|order (?:me|us)|find (?:me|us)))(?: (?:right now|here|today))?$/;
77
77
  /** Unit words a number names a product by, such as `10000 mah`, so it is never read as a zip code. */
78
78
  const UNIT = '(?:packs?|count|ct|pcs?|pieces?|oz|ml|l|g|kg|gb|tb|mb|mah|w|watts?|v|volts?|lumens?|lm|inch(?:es)?|in|ft|feet|mm|cm|lbs?|dollars?|bucks|rpm|dpi|btu|cal|calories|thread)';
79
79
  /**
@@ -226,7 +226,7 @@ const LEADING_PREPOSITION = /^(?:from|at|in|on|to|for|of|with|by|near|into|onto|
226
226
  const MAX_WORDS = 8;
227
227
  const MAX_WORD_LENGTH = 24;
228
228
  /** The text as one plain line: lowercase, no quotes, and no dash between words. */
229
- function plain(text) {
229
+ export function plain(text) {
230
230
  return text.toLowerCase().replace(/[‘’]/g, "'").replace(/["“”`]/g, '').replace(/\s+[-–—]+\s+|[–—]/g, ' ').replace(/\s+/g, ' ').trim();
231
231
  }
232
232
  /**
@@ -31,8 +31,9 @@
31
31
  * messages and links as one sequence, in the order they happened, and spaces
32
32
  * the messages like someone texting.
33
33
  */
34
- import { AgentcardClient, type BuyCart, type BuyCartBuilding, type BuyFlightItinerary, type BuyDeliveryAddress, type BuyNextAction, type BuyPick, type BuyProgress, type BuyStorePick, type BuyTurn, type FetchLike } from './client.js';
34
+ import { AgentcardClient, type BuyCart, type BuyCartBuilding, type BuyFlightItinerary, type BuyDeliveryAddress, type BuyNextAction, type BuyPick, type BuyProgress, type BuyTurnStatus, type BuyAvailability, type BuyStorePick, type BuyTurn, type FetchLike } from './client.js';
35
35
  import { type BuyToolDefinition, type BuyToolInput, type BuyToolInputSchema, type BuyToolStandardSchema, type OpenAIBuyTool } from './definition.js';
36
+ import { type DirectCall } from './direct.js';
36
37
  import { type BuyToolModelView } from './model-view.js';
37
38
  import { type MessagePace, type MessageStream } from './messages.js';
38
39
  import { type ShopperMessage } from './shopper-message.js';
@@ -111,6 +112,12 @@ export interface BuyToolLink {
111
112
  storeId?: string;
112
113
  /** On a store link: the store's delivery estimate in minutes, or null when the store showed none. */
113
114
  etaMinutes?: number | null;
115
+ /**
116
+ * On a product link for a place to book: whether it has room at the time the shopper asked for. Offer the card as a
117
+ * choice only when it reads `available`; `checking` changes in place once `onAvailability` hears the live answer.
118
+ * Absent on every other card, such as a retail product's.
119
+ */
120
+ availability?: BuyAvailability;
114
121
  }
115
122
  /** A store the answer's reply presents to choose from. */
116
123
  export interface BuyToolStore {
@@ -157,6 +164,8 @@ export interface BuyToolProduct {
157
164
  * conversation.
158
165
  */
159
166
  linkSent: boolean;
167
+ /** For a place to book: whether it has room at the asked time. Absent on a product with no such question. */
168
+ availability?: BuyAvailability;
160
169
  }
161
170
  /** One answer of the buy tool, whole, for your code. Hand it to `forModel` before a model reads it. */
162
171
  export interface BuyToolResult {
@@ -188,6 +197,11 @@ export interface BuyToolResult {
188
197
  * the request is being worked on. Absent or false when none went out.
189
198
  */
190
199
  shopper_already_told_looking?: boolean;
200
+ /**
201
+ * True when every message of Agentcard's answer reached the shopper through `onText` as it was written, so the shopper
202
+ * already read `reply`. Absent or false when any message did not go out that way, and when the tool wrote the reply.
203
+ */
204
+ shopper_already_read_reply?: boolean;
191
205
  conversation_id: string | null;
192
206
  order_id: string | null;
193
207
  decline_code: string | null;
@@ -218,6 +232,30 @@ export interface BuyToolResult {
218
232
  * - `none`: nothing.
219
233
  */
220
234
  export type BuyAckMode = 'reaction' | 'text' | 'none';
235
+ /**
236
+ * How many products a turn's searches found, sent each time the count grows, after the cards it counts: `shown` cards of
237
+ * `found` products, "8 of 23 shown". `more` holds the products found past the cards, in the order they were found, as
238
+ * product links the app reveals on "show more" with no new search; the model never reads them. Find mode sends a count
239
+ * with no `more`.
240
+ */
241
+ /**
242
+ * What Agentcard's live search said of places whose cards went out as `checking`, each matched to its card by the
243
+ * card's `productId`. A call that ends without saying sends `unknown` for each card it left `checking`, so no card
244
+ * waits for an answer that is not coming.
245
+ */
246
+ export interface BuyToolAvailability {
247
+ conversationId: string;
248
+ items: Array<{
249
+ productId: string;
250
+ availability: BuyAvailability;
251
+ }>;
252
+ }
253
+ export interface BuyToolFound {
254
+ conversationId: string;
255
+ shown: number;
256
+ found: number;
257
+ more: BuyToolLink[];
258
+ }
221
259
  export interface BuyToolOptions {
222
260
  /** A client built with your company's credentials. Build it once and share it across every shopper's tool. */
223
261
  agentcard?: AgentcardClient;
@@ -288,17 +326,64 @@ export interface BuyToolOptions {
288
326
  */
289
327
  onProgress?: (progress: BuyProgress) => void;
290
328
  /**
291
- * `false` waits for each whole answer. With `onProgress`, or with `earlyCards` and `sendLink`, the
292
- * tool asks Agentcard to stream the call, and each stage goes to `onProgress` as it happens.
329
+ * Called the moment Agentcard starts a step of a call, with a short line to show in place of loading dots, such as
330
+ * `searching Target and Walmart` or `adding it to your cart`. No status comes for a call that only writes its reply, so
331
+ * keep the dots until the first one. One that throws costs nothing but the call to it.
332
+ */
333
+ onStatus?: (status: BuyTurnStatus) => void;
334
+ /**
335
+ * Hears how many products a call's searches found, each time the count grows (`BuyToolFound`), right after the cards it
336
+ * counts went out through `sendLink`. It needs `earlyCards`, the default: with the cards held for the answer, no count
337
+ * comes. One that throws costs nothing but the call to it.
338
+ */
339
+ onFound?: (found: BuyToolFound) => void | Promise<void>;
340
+ /**
341
+ * Hears whether the places a call's cards show have room (`BuyToolAvailability`), right after the cards it changes went
342
+ * out through `sendLink`: change each card in place, by its `productId`. It needs `earlyCards`, the default. One that
343
+ * throws costs nothing but the call to it.
344
+ */
345
+ onAvailability?: (update: BuyToolAvailability) => void | Promise<void>;
346
+ /**
347
+ * Hears the products a call's search found, the moment the search answers and before Agentcard writes its reply,
348
+ * with the conversation, when the search ran (`asOf`, as the answer's `catalog.as_of` names it) and the store's name.
349
+ * Show them as the answer's cards. Like `sendLink`, it hears every call; pass it to `buy.turn` to hear only the calls
350
+ * of one turn. One that throws costs nothing but the call to it.
351
+ */
352
+ onProducts?: (products: BuyToolProduct[], search: {
353
+ conversationId: string;
354
+ asOf: string | null;
355
+ merchantName: string;
356
+ }) => void | Promise<void>;
357
+ /**
358
+ * Hears each message Agentcard writes for the shopper, the moment it is written, links included, as `result.reply`
359
+ * and `result.messages` hold them. A message that lands before the instant reply was due quiets it, and one after it
360
+ * follows it. When every message of an answer went out through it, the model reads `shopper_already_read_reply` and
361
+ * adds nothing to them. One that throws leaves that flag false, so the model words the answer.
362
+ */
363
+ onText?: (text: string, info: {
364
+ conversationId: string | null;
365
+ }) => void | Promise<void>;
366
+ /**
367
+ * Hears the carts each time a call builds or changes one, before the message that announces it: the cart a confirm
368
+ * would place, or null, and every cart on the table. One that throws costs nothing but the call to it.
369
+ */
370
+ onCart?: (cart: BuyCart | null, info: {
371
+ carts: BuyCart[];
372
+ conversationId: string;
373
+ }) => void | Promise<void>;
374
+ /**
375
+ * `false` waits for each whole answer, and a search's cards go out with the answer that names them. Left out, the
376
+ * tool streams each call: the stages reach `onProgress`, the products `onProducts` and their cards `sendLink` the
377
+ * moment the search answers, each message `onText`, and the carts `onCart`, as Agentcard produces them. A call with
378
+ * none of these to hear it asks for the whole answer.
293
379
  */
294
380
  stream?: false;
295
381
  /**
296
- * `true` sends the store pages of a search's first three products to `sendLink` the moment the search
297
- * answers, before Agentcard writes its reply, and the answer then sends only the pages it adds. Off by
298
- * default: the reply often recommends other products than the search's first three, so a shopper
299
- * would hold cards the reply never names. Measured on five live searches on 2026-10-20, the early
300
- * pages arrived 2.2 to 3.3 seconds before the answer, and in four of them the reply named a product
301
- * outside the first three.
382
+ * Default `true`: the store page of every product a search found, at most eight, goes to `sendLink` the moment the
383
+ * search answers, before Agentcard writes its reply, and the answer then sends only the pages it adds. Measured on
384
+ * five live searches on 2026-10-20, the early pages arrived 2.2 to 3.3 seconds before the answer. `false` waits for
385
+ * the answer and sends only the cards it names: the reply often names only some of the search's products, so early
386
+ * cards can include ones the reply never names.
302
387
  */
303
388
  earlyCards?: boolean;
304
389
  /**
@@ -308,6 +393,14 @@ export interface BuyToolOptions {
308
393
  * writes every answer.
309
394
  */
310
395
  replyAsWritten?: boolean;
396
+ /**
397
+ * `true` reads each shopper message `buy.turn` opens by rule, with no model, and sets `turn.direct` to the call
398
+ * it makes straight to Agentcard when the message is a request to buy, a pick of what Agentcard just showed, or
399
+ * the answer to the question Agentcard just asked (`directCall`). Send it with `turn.run(turn.direct.input)` and
400
+ * skip your model's first call; every other message, small talk and questions among them, leaves `turn.direct`
401
+ * null for your model to decide. Off by default, so your model decides every call.
402
+ */
403
+ direct?: boolean;
311
404
  }
312
405
  /**
313
406
  * The buy tool, in the shape every agent framework takes, because their field
@@ -427,7 +520,7 @@ export interface BuyToolTurnOptions {
427
520
  */
428
521
  onItem?: (item: BuyToolTurnItem) => void | Promise<void>;
429
522
  /**
430
- * With `onItem` or `onMessage`: how the turn's messages are spaced. Default `false`: each message goes
523
+ * With `onItem`, `onMessage` or `onText`: how the turn's messages are spaced. Default `false`: each message goes
431
524
  * out the moment it is ready. `true` spaces them like someone texting: a message waits until
432
525
  * `typingDelay` of it has passed since the item before it went out, and time the model spent writing
433
526
  * counts toward the pause. A function returns the pause in milliseconds for a message. Links never wait.
@@ -438,7 +531,7 @@ export interface BuyToolTurnOptions {
438
531
  * null, so your model rewords every answer. Default: the tool's `replyAsWritten`.
439
532
  */
440
533
  replyAsWritten?: boolean;
441
- /** With `onItem` or `onMessage`: hears `true` when the pause before a message starts and `false` the moment that message goes out, for a typing indicator. */
534
+ /** With `onItem`, `onMessage` or `onText`: hears `true` when the pause before a message starts and `false` the moment that message goes out, for a typing indicator. */
442
535
  onTyping?: (typing: boolean) => void;
443
536
  /**
444
537
  * Without `onItem`: hears each message your model writes through `turn.messageStream()` or
@@ -461,6 +554,22 @@ export interface BuyToolTurnOptions {
461
554
  onResult?: (result: BuyToolResult) => void | Promise<void>;
462
555
  /** Called with each stage Agentcard reaches on a call this turn makes. */
463
556
  onProgress?: (progress: BuyProgress) => void;
557
+ /** Called with each step Agentcard starts on a call this turn makes, as the tool's `onStatus` is. */
558
+ onStatus?: BuyToolOptions['onStatus'];
559
+ /** Hears how many products this turn's calls found, as the tool's `onFound` does. */
560
+ onFound?: BuyToolOptions['onFound'];
561
+ /** Hears whether the places this turn's cards show have room, as the tool's `onAvailability` does. */
562
+ onAvailability?: BuyToolOptions['onAvailability'];
563
+ /** Hears the products each search of this turn's calls found, the moment it answers, as the tool's `onProducts` does. */
564
+ onProducts?: BuyToolOptions['onProducts'];
565
+ /**
566
+ * Hears each message Agentcard writes for this turn's calls, as the tool's `onText` does, in its place in the turn:
567
+ * after the instant reply and every card handed over before it, paced by `pace`. `turn.items` lists each one as a
568
+ * `text` item, and `onItem` does not hear it.
569
+ */
570
+ onText?: BuyToolOptions['onText'];
571
+ /** Hears the carts each time a call of this turn builds or changes one, as the tool's `onCart` does. */
572
+ onCart?: BuyToolOptions['onCart'];
464
573
  }
465
574
  /**
466
575
  * The buy tool for one shopper message, from `buy.turn`. It is the same tool, with the same name,
@@ -515,10 +624,18 @@ export interface BuyToolTurn extends Omit<BuyTool, 'instantAck' | 'instantReacti
515
624
  * itself for your model, such as the note that the cart changed or a `nextStep` turn's ask for the yes.
516
625
  */
517
626
  readonly replyAsWritten: string | null;
627
+ /**
628
+ * With `direct` on: the call this turn's message makes straight to Agentcard, with no model deciding it, and why,
629
+ * or null when your model decides. It is set when the turn opens, from what Agentcard last asked the shopper.
630
+ * Send it with `run(direct.input)`, which returns what your model would read, then show `replyAsWritten` when it
631
+ * is set; when it is null, hand your model the call and its result, and its words answer the shopper.
632
+ */
633
+ readonly direct: DirectCall | null;
518
634
  /**
519
635
  * Every message and link the turn handed over, in the order they went out: the instant reply, each
520
- * link, and each message written through `messageStream()` or `say()`. An item its send threw on is
521
- * left out. Return it as the turn's answer, and an app renders it in order. Each read returns a new array.
636
+ * link, each message of Agentcard's that `onText` took, and each message written through `messageStream()`
637
+ * or `say()`. An item its send threw on is left out. Return it as the turn's answer, and an app renders it
638
+ * in order. Each read returns a new array.
522
639
  */
523
640
  readonly items: BuyToolTurnItem[];
524
641
  /**