@agent-cards/commerce-sdk 0.10.0 → 0.11.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 +46 -11
- package/dist/ack.d.ts +17 -0
- package/dist/ack.js +19 -0
- package/dist/buy-tool.d.ts +50 -11
- package/dist/buy-tool.js +67 -12
- package/dist/definition.js +7 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/message.d.ts +7 -2
- package/dist/model-view.d.ts +6 -0
- package/dist/model-view.js +11 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -150,7 +150,7 @@ A shopper can send a second message before the first is answered. The `sendLink`
|
|
|
150
150
|
const buy = buyTool({ agentcard, externalUserId: shopper.id }); // one per shopper, kept
|
|
151
151
|
|
|
152
152
|
async function onShopperMessage(message: string) {
|
|
153
|
-
const turn = buy.turn(message, {
|
|
153
|
+
const turn = buy.turn(message, { onReaction: (emoji) => reactToShopperMessage(shopper.id, emoji) });
|
|
154
154
|
const runner = new Anthropic().beta.messages.toolRunner({
|
|
155
155
|
model: 'claude-opus-5-5',
|
|
156
156
|
max_tokens: 16000,
|
|
@@ -167,11 +167,11 @@ async function onShopperMessage(message: string) {
|
|
|
167
167
|
|
|
168
168
|
- `turn.links` holds each link the turn's calls handed over, in order: the Vault, approval and store login links, each product's store page, and each store card's page. The model reads that each one went out, so send them with the reply. Pass `sendLink` to `buy.turn` as well to send each one the moment the tool hands it over, such as down a server-sent event stream. A link your `sendLink` throws on stays out of `turn.links`.
|
|
169
169
|
- `turn.results` holds the whole answer of each call, and an `onResult` passed to `buy.turn` hears each one as it arrives.
|
|
170
|
-
- With `
|
|
170
|
+
- With `onReaction`, `buy.turn` puts a 👀 on a message that starts a search at once, before your model runs, and `turn.reaction` holds it. With only `onWorking`, it sends the text reply `buy.instantAck` would return, or the one your `ack` function writes, and `turn.ack` holds that text. You never call `instantReaction` or `instantAck` yourself. A slow call of the turn acknowledges through the same callback.
|
|
171
171
|
- An `onProgress` passed to `buy.turn` hears only that turn's stages.
|
|
172
172
|
- `turn.agentcardMessages` holds each message `agentcard.listen` or `agentcardWebhook` posted into this shopper's chat that the model has not read yet, and `turn.messages(history)` hands them to the model.
|
|
173
173
|
|
|
174
|
-
A callback you leave out of `buy.turn` falls back to the one the tool was built with, so a tool whose `sendLink` texts the shopper keeps texting, and `turn.links` lists the same links. The turn is the tool, with the same name, schema and methods, so pass it to every framework the way you pass `buy`: `tools: [turn]`, `tools: { buy: turn }`, `tools: [turn.openai]` with `turn.handle`, or `toOpenRouterTool(turn, { tool, z })`. `turn.ack`, `turn.links` and `turn.results` stay out of the tool's JSON, so a tool runner sends the API the same fields. Build the tool once per shopper and open a turn from it for each message: the turns share the shopper's conversation, so a card a shopper already holds from an earlier turn is not sent again.
|
|
174
|
+
A callback you leave out of `buy.turn` falls back to the one the tool was built with, so a tool whose `sendLink` texts the shopper keeps texting, and `turn.links` lists the same links. The turn is the tool, with the same name, schema and methods, so pass it to every framework the way you pass `buy`: `tools: [turn]`, `tools: { buy: turn }`, `tools: [turn.openai]` with `turn.handle`, or `toOpenRouterTool(turn, { tool, z })`. `turn.ack`, `turn.reaction`, `turn.links` and `turn.results` stay out of the tool's JSON, so a tool runner sends the API the same fields. Build the tool once per shopper and open a turn from it for each message: the turns share the shopper's conversation, so a card a shopper already holds from an earlier turn is not sent again.
|
|
175
175
|
|
|
176
176
|
A call made on `buy` itself still works as before: it belongs to the turn the tool reads from its timing, and its links go to the tool's `sendLink`.
|
|
177
177
|
|
|
@@ -245,35 +245,62 @@ The tool sends each store page once for the calls the model makes in one turn. A
|
|
|
245
245
|
|
|
246
246
|
## Tell the shopper you are on it
|
|
247
247
|
|
|
248
|
-
A first search can take Agentcard most of a minute. Pass `
|
|
248
|
+
A first search can take Agentcard most of a minute. Pass `onReaction`, and the shopper sees a 👀 on their own message while Agentcard looks, the way a friend reacts to a text before answering it. Show it as a tapback on the shopper's bubble:
|
|
249
249
|
|
|
250
250
|
```ts
|
|
251
251
|
const buy = buyTool({
|
|
252
252
|
agentcard,
|
|
253
253
|
externalUserId: shopper.id,
|
|
254
254
|
sendLink: (link) => textShopper(shopper.id, link.url),
|
|
255
|
+
onReaction: (emoji) => reactToShopperMessage(shopper.id, emoji), // emoji is always '👀'
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
A reaction is the default acknowledgement. It names nothing the shopper said, so it never reads a request back wrong, and the chat holds only real answers. Each message gets at most one 👀:
|
|
260
|
+
|
|
261
|
+
- A message that starts a search gets it the moment `buy.turn` opens, before your model runs.
|
|
262
|
+
- A pick of a product or a store the shopper saw, an address or other details, an answer to a question such as `yes`, `2` or a code, a step toward checkout such as `ok lets checkout`, and an acknowledgement such as `its ok` get none when the turn opens, because Agentcard answers them fast.
|
|
263
|
+
- A call that Agentcard has not answered within 1.5 seconds puts the 👀 on a message that has none yet: a slow `yes`, a pick, or a confirm that is still placing the order. It sends no text.
|
|
264
|
+
- A turn that runs long, even past a minute, still gets no text line. A search often takes 10 to 40 seconds, so a line at any wait short enough to help would land on most searches and bring back the chatter the reaction replaces. To show a long wait, pass `onProgress` and show its stage, such as `checking_out`, as a status line or a typing indicator.
|
|
265
|
+
|
|
266
|
+
With `onItem` on `buy.turn`, the 👀 still goes to `onReaction` and never joins the turn's items, because it sits on the shopper's bubble rather than in the agent's sequence. Without an `onReaction`, the text instant reply is the turn's first `text` item, as before.
|
|
267
|
+
|
|
268
|
+
When the 👀 went out, the model reads `shopper_already_told_looking: true` in that turn's answers, and the tool's description tells it not to acknowledge the request again, so its reply opens with the answer or the question rather than `on it`. It reads false on a turn that reacted with nothing, and when `onReaction` threw. An `onReaction` that throws costs only the reaction.
|
|
269
|
+
|
|
270
|
+
### Acknowledge in text, or not at all
|
|
271
|
+
|
|
272
|
+
For SMS, or any chat that shows no reactions, acknowledge in text. Pass `onWorking` and no `onReaction`, or set `ack: 'text'`, and the shopper reads an instant reply instead, such as `on it, looking for usb-c cables under $20` or `give me a sec, checking for a table for 2 in north beach`:
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
const buy = buyTool({
|
|
276
|
+
agentcard,
|
|
277
|
+
externalUserId: shopper.id,
|
|
278
|
+
ack: 'text',
|
|
255
279
|
onWorking: (text) => textShopper(shopper.id, text),
|
|
256
280
|
});
|
|
257
281
|
```
|
|
258
282
|
|
|
259
|
-
|
|
283
|
+
`ack` takes `'reaction'`, `'text'` or `'none'`. Left out, the tool reacts when the tool or the turn has an `onReaction`, and sends text through `onWorking` otherwise, so an app built on 0.9 with only `onWorking` keeps its text replies. With both callbacks, the shopper gets the 👀 and never the text too. `'none'`, or `false`, sends nothing.
|
|
260
284
|
|
|
261
|
-
|
|
285
|
+
In text mode, the tool sends the reply when Agentcard has not answered a new request within 1.5 seconds, so a quick answer arrives alone. It sends one for the calls the model makes in one turn, where a call that starts within 15 seconds of the last one ending counts as the same turn. A slow confirm or pick never hears `looking for`: a pick of a product the shopper saw hears `adding it now`, a pick of a store or a place in a list, such as `the first one`, hears `got it, one moment`, and a confirm hears `on it, one moment`. None of them names anything the shopper said, and a code hears nothing. The words rotate through six templates, and one tool never sends the same template twice in a row. A short paraphrase of the request goes in when the tool can read one: it stops before any link, email address, phone number, street address or long number, and a request that starts with one gets a reply that names nothing, such as `looking for that now`. The call waits for `onWorking`, so the shopper reads the reply before any link and before the model answers. An `onWorking` that throws costs only the instant reply.
|
|
262
286
|
|
|
263
|
-
Pass `ack:
|
|
287
|
+
Pass `ack: (ask) => text` to write your own text from the shopper's words, where an empty answer sends none, and `ackDelayMs` to change the 1.5 seconds for either mode.
|
|
264
288
|
|
|
265
|
-
The model takes several seconds to call the tool after the shopper's message arrives.
|
|
289
|
+
The model takes several seconds to call the tool after the shopper's message arrives. Without `buy.turn`, react sooner by calling `buy.instantReaction(message)` the moment the message arrives, before your model runs, and put the emoji it returns on the message. In text mode, call `buy.instantAck(message)` and send the text it returns:
|
|
266
290
|
|
|
267
291
|
```ts
|
|
292
|
+
const reaction = buy.instantReaction(message); // '👀' or null
|
|
293
|
+
if (reaction) await reactToShopperMessage(shopper.id, reaction);
|
|
294
|
+
// In text mode instead:
|
|
268
295
|
const ack = buy.instantAck(message);
|
|
269
296
|
if (ack) await textShopper(shopper.id, ack);
|
|
270
297
|
```
|
|
271
298
|
|
|
272
|
-
With `buy.turn`, pass `onWorking` to the turn instead: it
|
|
299
|
+
With `buy.turn`, pass `onReaction` or `onWorking` to the turn instead: it acknowledges the moment the turn opens, and calls `instantReaction` or `instantAck` for you. When either returns a 👀 or a reply, the tool sends none of its own for that turn.
|
|
273
300
|
|
|
274
|
-
`instantAck`
|
|
301
|
+
`instantReaction` and `instantAck` return null for a message that starts no search: a confirmation or a short answer such as `yes`, `1` or `ok`, a step toward paying for the cart such as `ok lets checkout`, `place the order` or `pay`, an acknowledgement such as `its ok`, `thats fine` or `sounds good`, a pick of a product the shopper already saw, such as `the first one`, `#2`, `I want the blue one` or a card's title with its price, a pick of a store, such as `order here` or a store card's `Order from Tacos El Grullense`, a code, small talk such as `hi` or `how are you`, a message that carries the shopper's own details, such as a name with a phone number, an email address, a street address, a bare house number and street such as `1801 wedemeyer`, or a zip code, any message that answers the last answer's request for an address, shipping or guest details or a code, and a short reply right after any other question the last answer asked, such as `im a 37` after `what size do you want?`, or `carne asada` after `which meat?`. A reply that opens a new request, such as `find me black running shoes`, or that runs past four words is still a search. The reply never reads those details back, never repeats the shopper's own first-person words, and names the request only when it reads as a short, whole phrase; otherwise it says `on it, give me a moment` or another reply that names nothing. The tool also reads a message that quotes the name of a product its last answer showed, or names one of the stores its last answer listed and nothing else to buy, as a pick of it. A request for a store the shopper did not see, such as `order from chipotle`, reads back as `looking for chipotle`, never `looking for from chipotle`. When it returns a reply, the tool sends none of its own through `onWorking` for that turn, and the model reads `shopper_already_told_looking: true`, so the shopper hears one acknowledgement. When it returns null for a message that picks nothing, such as `yes`, a call that runs past `ackDelayMs` still acknowledges: with a 👀 in reaction mode, and in text mode with an `onWorking` reply that names nothing and never says it is looking for anything, such as `working on it` or `one moment`, and when it returns null for a pick, such a call sends `adding it now` or `got it, one moment`.
|
|
275
302
|
|
|
276
|
-
Without a buy tool, `instantAck(message, { previous, nextAction, asked, stores })` from the package root
|
|
303
|
+
Without a buy tool, `instantReaction(message, { nextAction, asked, stores })` and `instantAck(message, { previous, nextAction, asked, stores })` from the package root decide the same way, and `ACK_REACTION` is the `👀` `instantReaction` returns: `previous` is the reply you sent last, so the next one comes from another template, `nextAction` is the last answer's `next_action`, `asked` is the last message the shopper read, so an answer to its question for an address, a name or a code, or a short answer to any other question it asked, gets none, and `stores` holds the names of the stores the last answer listed, so a message that picks one gets none. Pass `ack: 'none'` to the tool when you acknowledge yourself. `ackFor` is the earlier name of the same function.
|
|
277
304
|
|
|
278
305
|
## Show what Agentcard is doing
|
|
279
306
|
|
|
@@ -470,12 +497,15 @@ Subscribe your endpoint to each event below by name. A catch-all `*` alone does
|
|
|
470
497
|
| Event | `kind` | When it arrives | Who wrote `text` |
|
|
471
498
|
|---|---|---|---|
|
|
472
499
|
| `purchase.message` | `card_added` | The shopper saved a card from the card link your app sent, or picked one they keep. | Agentcard |
|
|
500
|
+
| `purchase.message` | `store_connected` | The shopper signed in to their own account at the store from the sign-in link your app sent. | Agentcard |
|
|
473
501
|
| `purchase.message` | `order_placed` | The shopper approved, and Agentcard placed the order. | Agentcard |
|
|
474
502
|
| `purchase.message` | `order_failed` | The order failed after the shopper approved. | Agentcard |
|
|
475
503
|
| `purchase.message` | `order_not_placed_yet` | The shopper approved, and the order is not placed yet. | Agentcard |
|
|
476
504
|
| `purchase.message` | `order_unconfirmed` | The store's reply to the order was lost, so the order may exist, and Agentcard is confirming it. | Agentcard |
|
|
477
505
|
| `purchase.message` | `approval_expiring` | The approval link expires soon. `text` carries the link. | Agentcard |
|
|
478
506
|
| `purchase.message` | `approval_expired` | The approval expired before the shopper answered, and no charge went through. | Agentcard |
|
|
507
|
+
| `purchase.message` | `approval_declined` | The shopper declined the approval, and no charge went through. | Agentcard |
|
|
508
|
+
| `purchase.message` | `payment_declined` | The shopper approved, and the payment was refused before the order went in, such as a declined card. `facts.reason` says why. No charge went through. | Agentcard |
|
|
479
509
|
| `purchase.message` | `card_link_unfinished` | The shopper has not added a card from their card link. `text` carries the link again. | Agentcard |
|
|
480
510
|
| `purchase.message` | `card_link_expired` | The card link expired before the shopper added a card. | Agentcard |
|
|
481
511
|
| `purchase.message` | `cart_idle` | A cart sat, and nobody sent it for approval. | Agentcard |
|
|
@@ -561,11 +591,16 @@ The facts are `merchantName`, `totalCents`, `currency`, `cardBrand`, `cardLast4`
|
|
|
561
591
|
| `order_not_placed_yet` | The shopper approved, and the order is not placed yet. |
|
|
562
592
|
| `approval_expiring` | The approval link expires soon. `text` carries the link. |
|
|
563
593
|
| `approval_expired` | The approval expired before the shopper answered, and no charge went through. |
|
|
594
|
+
| `approval_declined` | The shopper declined the approval, and no charge went through. |
|
|
595
|
+
| `payment_declined` | The shopper approved, and the payment was refused before the order went in. `facts.reason` says why. |
|
|
564
596
|
| `card_link_unfinished` | The shopper has not added a card from their card link. `text` carries the link again. |
|
|
565
597
|
| `card_link_expired` | The card link expired before the shopper added a card. |
|
|
566
598
|
| `cart_idle` | A cart sat, and nobody sent it for approval. |
|
|
567
599
|
| `order_unconfirmed` | The store's reply to the order was lost, and Agentcard is confirming it. |
|
|
568
600
|
| `card_added` | The shopper saved a card from the card link your app sent. |
|
|
601
|
+
| `store_connected` | The shopper signed in to their own account at the store from the sign-in link your app sent, such as DoorDash, so the order can go through on it. |
|
|
602
|
+
| `store_connect_failed` | The shopper's sign-in to the store did not go through, and the link no longer works. `text` offers a new one. |
|
|
603
|
+
| `store_connect_expired` | The sign-in link expired before the shopper finished signing in. `text` offers a new one. |
|
|
569
604
|
|
|
570
605
|
Agentcard can add a kind later. A kind your code does not know still carries `text`, ready to send as is.
|
|
571
606
|
|
package/dist/ack.d.ts
CHANGED
|
@@ -89,5 +89,22 @@ export declare function answersQuestion(text: string, asked: string | null | und
|
|
|
89
89
|
* ```
|
|
90
90
|
*/
|
|
91
91
|
export declare function instantAck(shopperText: string, options?: AckForOptions): string | null;
|
|
92
|
+
/**
|
|
93
|
+
* The reaction the tool puts on a shopper's message to say it is on it: 👀, as a chat app shows a
|
|
94
|
+
* tapback on the shopper's own bubble. It names nothing the shopper said, so it can never read a
|
|
95
|
+
* request back wrong.
|
|
96
|
+
*/
|
|
97
|
+
export declare const ACK_REACTION = "\uD83D\uDC40";
|
|
98
|
+
/**
|
|
99
|
+
* The reaction for a shopper's message, `👀`, or null for a message that starts no search. It reads the
|
|
100
|
+
* message exactly as `instantAck` does, so a pick, an address, an answer to a question, a step toward
|
|
101
|
+
* checkout or an acknowledgement such as `its ok` gets none.
|
|
102
|
+
*
|
|
103
|
+
* ```ts
|
|
104
|
+
* instantReaction('can you find me usb-c cables under $20'); // '👀'
|
|
105
|
+
* instantReaction('yes'); // null
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
export declare function instantReaction(shopperText: string, options?: AckForOptions): string | null;
|
|
92
109
|
/** The earlier name of `instantAck`, kept for apps that call it. */
|
|
93
110
|
export declare function ackFor(text: string, options?: AckForOptions): string | null;
|
package/dist/ack.js
CHANGED
|
@@ -369,6 +369,25 @@ export function instantAck(shopperText, options = {}) {
|
|
|
369
369
|
return null;
|
|
370
370
|
return ackAt(nextTemplate(templateOf(options?.previous)), shopperText);
|
|
371
371
|
}
|
|
372
|
+
/**
|
|
373
|
+
* The reaction the tool puts on a shopper's message to say it is on it: 👀, as a chat app shows a
|
|
374
|
+
* tapback on the shopper's own bubble. It names nothing the shopper said, so it can never read a
|
|
375
|
+
* request back wrong.
|
|
376
|
+
*/
|
|
377
|
+
export const ACK_REACTION = '👀';
|
|
378
|
+
/**
|
|
379
|
+
* The reaction for a shopper's message, `👀`, or null for a message that starts no search. It reads the
|
|
380
|
+
* message exactly as `instantAck` does, so a pick, an address, an answer to a question, a step toward
|
|
381
|
+
* checkout or an acknowledgement such as `its ok` gets none.
|
|
382
|
+
*
|
|
383
|
+
* ```ts
|
|
384
|
+
* instantReaction('can you find me usb-c cables under $20'); // '👀'
|
|
385
|
+
* instantReaction('yes'); // null
|
|
386
|
+
* ```
|
|
387
|
+
*/
|
|
388
|
+
export function instantReaction(shopperText, options = {}) {
|
|
389
|
+
return instantAck(shopperText, options) === null ? null : ACK_REACTION;
|
|
390
|
+
}
|
|
372
391
|
/** The earlier name of `instantAck`, kept for apps that call it. */
|
|
373
392
|
export function ackFor(text, options = {}) {
|
|
374
393
|
return instantAck(text, options);
|
package/dist/buy-tool.d.ts
CHANGED
|
@@ -152,8 +152,9 @@ export interface BuyToolResult {
|
|
|
152
152
|
/** True when Agentcard tells the shopper how the order went, so the shopper need not reply. */
|
|
153
153
|
shopper_need_not_reply: boolean;
|
|
154
154
|
/**
|
|
155
|
-
* True when
|
|
156
|
-
*
|
|
155
|
+
* True when this turn's acknowledgement reached the shopper: the 👀 `onReaction` put on their message,
|
|
156
|
+
* or the instant reply `onWorking` sent, such as `got it, finding a taco`. The shopper already knows
|
|
157
|
+
* the request is being worked on. Absent or false when none went out.
|
|
157
158
|
*/
|
|
158
159
|
shopper_already_told_looking?: boolean;
|
|
159
160
|
conversation_id: string | null;
|
|
@@ -173,6 +174,14 @@ export interface BuyToolResult {
|
|
|
173
174
|
/** Agentcard's answer as it came: the purchase answer, or the body of the refusal. */
|
|
174
175
|
envelope: Record<string, unknown>;
|
|
175
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* How the tool acknowledges a shopper's message while Agentcard works on it:
|
|
179
|
+
* - `reaction`, the default: a 👀 on the shopper's own message, through `onReaction`. No text goes out.
|
|
180
|
+
* - `text`: an instant reply, such as `on it, looking for usb-c cables under $20`, through `onWorking`,
|
|
181
|
+
* for SMS, or any chat that shows no reactions.
|
|
182
|
+
* - `none`: nothing.
|
|
183
|
+
*/
|
|
184
|
+
export type BuyAckMode = 'reaction' | 'text' | 'none';
|
|
176
185
|
export interface BuyToolOptions {
|
|
177
186
|
/** A client built with your company's credentials. Build it once and share it across every shopper's tool. */
|
|
178
187
|
agentcard?: AgentcardClient;
|
|
@@ -198,9 +207,19 @@ export interface BuyToolOptions {
|
|
|
198
207
|
fetch?: FetchLike;
|
|
199
208
|
/** The pause between two reads of the conversation. Default: a timer. */
|
|
200
209
|
wait?: (ms: number) => Promise<void>;
|
|
210
|
+
/**
|
|
211
|
+
* Reacts to the shopper's message with `emoji`, always `👀`, to say the tool is on it: show it on the
|
|
212
|
+
* shopper's own bubble, as a tapback. It fires once per message: the moment `buy.turn` opens on a
|
|
213
|
+
* search, or, for a message that got none, such as `yes`, once a call passes `ackDelayMs`. Never on a
|
|
214
|
+
* pick, an address, an answer to a question or a step toward checkout when the turn opens. With it,
|
|
215
|
+
* the tool sends no text acknowledgement at all, unless `ack` is `'text'`. One that throws costs
|
|
216
|
+
* nothing but the reaction.
|
|
217
|
+
*/
|
|
218
|
+
onReaction?: (emoji: string) => void | Promise<void>;
|
|
201
219
|
/**
|
|
202
220
|
* Sends the shopper an instant reply, such as `on it, looking for usb-c cables under $20`, while
|
|
203
|
-
* Agentcard works on a new request
|
|
221
|
+
* Agentcard works on a new request, when the tool acknowledges in text: with `ack: 'text'`, or with
|
|
222
|
+
* no `onReaction`. It goes out when Agentcard has not answered within
|
|
204
223
|
* `ackDelayMs`, once for the calls the model makes in one turn. A slow pick gets `adding it now`
|
|
205
224
|
* for a product or `got it, one moment` for a store or a place in a list. A slow confirm gets
|
|
206
225
|
* `on it, one moment`. These replies name nothing the shopper said. A code gets no reply.
|
|
@@ -208,9 +227,16 @@ export interface BuyToolOptions {
|
|
|
208
227
|
* answer. One that throws costs nothing but the instant reply.
|
|
209
228
|
*/
|
|
210
229
|
onWorking?: (message: string) => void | Promise<void>;
|
|
211
|
-
/**
|
|
212
|
-
|
|
213
|
-
|
|
230
|
+
/**
|
|
231
|
+
* How the tool acknowledges a message (`BuyAckMode`). Left out, it reacts through `onReaction` when
|
|
232
|
+
* the tool or the turn has one, and otherwise sends text through `onWorking`, as 0.9 did. `'text'`
|
|
233
|
+
* sends text even with an `onReaction`, and `'none'`, or `false`, sends nothing. A function is
|
|
234
|
+
* `'text'` with your own words: it writes the instant reply from the shopper's words, and an empty
|
|
235
|
+
* answer sends none. A turn whose message `instantAck` sent nothing for gets the built-in reply that
|
|
236
|
+
* names nothing.
|
|
237
|
+
*/
|
|
238
|
+
ack?: BuyAckMode | false | ((ask: string) => string | null | undefined);
|
|
239
|
+
/** How long Agentcard has to answer before the instant reply or the reaction goes out. Default 1500. */
|
|
214
240
|
ackDelayMs?: number;
|
|
215
241
|
/**
|
|
216
242
|
* Called with the whole answer once per call, after its links went out and before the model reads
|
|
@@ -292,6 +318,14 @@ export interface BuyTool {
|
|
|
292
318
|
* message that picks nothing, such as `yes`, a slow call still sends `onWorking` a reply that names nothing.
|
|
293
319
|
*/
|
|
294
320
|
instantAck(shopperText: string): string | null;
|
|
321
|
+
/**
|
|
322
|
+
* The reaction for a shopper's message, `👀`, or null for one that starts no search, read as
|
|
323
|
+
* `instantAck` reads it. Call it the moment the message arrives, before your model runs, and put the
|
|
324
|
+
* emoji it returns on the shopper's message. When it returns one, the tool sends no reaction or text of
|
|
325
|
+
* its own for that turn, and the model reads `shopper_already_told_looking`. With `buy.turn` and
|
|
326
|
+
* `onReaction`, the turn does this for you.
|
|
327
|
+
*/
|
|
328
|
+
instantReaction(shopperText: string): string | null;
|
|
295
329
|
/**
|
|
296
330
|
* Opens the turn of one shopper message and returns the tool for that turn: pass it to your model in
|
|
297
331
|
* place of the tool, as `tools: [turn]`. Each call the model makes through it hands its links to this
|
|
@@ -299,8 +333,9 @@ export interface BuyTool {
|
|
|
299
333
|
* keeps it in `turn.results`, and sends this turn's instant reply to its `onWorking`. A shopper who
|
|
300
334
|
* sends a second message while the first still runs gets a second turn, and no link, cart or reply
|
|
301
335
|
* of one reaches the other. Call it the moment the message arrives, before your model runs: with an
|
|
302
|
-
* `
|
|
303
|
-
* back to the one the tool was
|
|
336
|
+
* `onReaction`, a search gets its 👀 at once, and with only an `onWorking`, the instant reply
|
|
337
|
+
* `instantAck` would return goes out at once. An option left out falls back to the one the tool was
|
|
338
|
+
* built with.
|
|
304
339
|
*/
|
|
305
340
|
turn(shopperText: string, options?: BuyToolTurnOptions): BuyToolTurn;
|
|
306
341
|
}
|
|
@@ -342,7 +377,9 @@ export interface BuyToolTurnOptions {
|
|
|
342
377
|
* `turn.links` beside the turn's reply.
|
|
343
378
|
*/
|
|
344
379
|
sendLink?: (link: BuyToolLink) => void | Promise<void>;
|
|
345
|
-
/**
|
|
380
|
+
/** Reacts 👀 to this turn's message: at once when `turn` opens on a new request, or once a slow call passes `ackDelayMs`. */
|
|
381
|
+
onReaction?: (emoji: string) => void | Promise<void>;
|
|
382
|
+
/** Sends this turn's instant reply in text, when the tool acknowledges in text: at once when `turn` opens on a new request, or once a slow call passes `ackDelayMs`. */
|
|
346
383
|
onWorking?: (message: string) => void | Promise<void>;
|
|
347
384
|
/** Called with the whole answer of each call this turn makes, after its links went out and before the model reads it. */
|
|
348
385
|
onResult?: (result: BuyToolResult) => void | Promise<void>;
|
|
@@ -355,9 +392,11 @@ export interface BuyToolTurnOptions {
|
|
|
355
392
|
* belong to this turn alone. `ack`, `links` and `results` are read-only, and JSON leaves them out, so a
|
|
356
393
|
* tool runner that sends the tool to its API sends the same fields as before.
|
|
357
394
|
*/
|
|
358
|
-
export interface BuyToolTurn extends Omit<BuyTool, 'instantAck' | 'turn'> {
|
|
359
|
-
/** The instant reply this turn sent to `onWorking` when it opened, or null when it sent none. */
|
|
395
|
+
export interface BuyToolTurn extends Omit<BuyTool, 'instantAck' | 'instantReaction' | 'turn'> {
|
|
396
|
+
/** The instant reply this turn sent to `onWorking` when it opened, or null when it sent none, as with a reaction. */
|
|
360
397
|
readonly ack: string | null;
|
|
398
|
+
/** The reaction this turn put on the shopper's message, `👀`, when it opened or once a call ran slow, or null while it put none or when `onReaction` threw. */
|
|
399
|
+
readonly reaction: string | null;
|
|
361
400
|
/**
|
|
362
401
|
* The messages Agentcard posted into this shopper's chat that the model has not read yet, oldest
|
|
363
402
|
* first, such as `card_added` once they saved a card: each one `agentcard.listen`'s or
|
package/dist/buy-tool.js
CHANGED
|
@@ -35,7 +35,7 @@ import { AgentcardApiError, AgentcardClient, } from './client.js';
|
|
|
35
35
|
import { buyToolDefinition, buyToolStandardSchema, openAIBuyTool, parseBuyToolInput, } from './definition.js';
|
|
36
36
|
import { forModel, forModelError, money, pickId, productCardKey, shownProducts, shownStores, stripLinks } from './model-view.js';
|
|
37
37
|
import { messageStream, paceOf, sleep, toMessages, typingHook } from './messages.js';
|
|
38
|
-
import { ackAt, answersQuestion, instantAck as instantAckOf, isNewRequest, isPickText, namesStore, nextTemplate, slowStepLine, waitAt } from './ack.js';
|
|
38
|
+
import { ACK_REACTION, ackAt, answersQuestion, instantAck as instantAckOf, isNewRequest, isPickText, namesStore, nextTemplate, slowStepLine, waitAt } from './ack.js';
|
|
39
39
|
const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
40
40
|
/**
|
|
41
41
|
* The model's messages for a turn: the history, what Agentcard told the shopper, then the shopper's
|
|
@@ -535,13 +535,31 @@ export function buyTool(options) {
|
|
|
535
535
|
const who = client.company ? { externalUserId } : {};
|
|
536
536
|
if (o.onWorking !== undefined && typeof o.onWorking !== 'function')
|
|
537
537
|
throw new TypeError('Pass onWorking as a function that sends the shopper a message.');
|
|
538
|
-
if (o.
|
|
539
|
-
throw new TypeError('Pass
|
|
538
|
+
if (o.onReaction !== undefined && typeof o.onReaction !== 'function')
|
|
539
|
+
throw new TypeError('Pass onReaction as a function that puts the emoji on the shopper\'s message.');
|
|
540
|
+
if (o.ack !== undefined && o.ack !== false && o.ack !== 'reaction' && o.ack !== 'text' && o.ack !== 'none' && typeof o.ack !== 'function') {
|
|
541
|
+
throw new TypeError("Pass ack as 'reaction', 'text' or 'none', as false, or as a function that writes the instant reply from the ask.");
|
|
542
|
+
}
|
|
540
543
|
const ackDelayMs = o.ackDelayMs ?? ACK_DELAY_MS;
|
|
541
544
|
if (typeof ackDelayMs !== 'number' || !Number.isFinite(ackDelayMs) || ackDelayMs < 0)
|
|
542
545
|
throw new TypeError('Pass ackDelayMs as a number of milliseconds, 0 or more.');
|
|
543
|
-
const onWorking = o.ack === false ? undefined : o.onWorking;
|
|
544
546
|
const writeAck = typeof o.ack === 'function' ? o.ack : null;
|
|
547
|
+
/** The mode `ack` names, or null when it names none and the callbacks a turn holds decide. */
|
|
548
|
+
const ackMode = o.ack === undefined ? null : o.ack === false || o.ack === 'none' ? 'none' : o.ack === 'reaction' ? 'reaction' : 'text';
|
|
549
|
+
/**
|
|
550
|
+
* Where a turn acknowledges, from the callbacks it holds: a reaction when it can react and `ack` asks
|
|
551
|
+
* for no text, else text. At most one of the two comes back, so a shopper never gets a 👀 and a line.
|
|
552
|
+
*/
|
|
553
|
+
const ackRoutes = (working, reacting) => {
|
|
554
|
+
if (ackMode === 'none')
|
|
555
|
+
return { onWorking: undefined, onReaction: undefined };
|
|
556
|
+
if (ackMode === 'text')
|
|
557
|
+
return { onWorking: working, onReaction: undefined };
|
|
558
|
+
if (ackMode === 'reaction' || reacting)
|
|
559
|
+
return { onWorking: undefined, onReaction: reacting };
|
|
560
|
+
return { onWorking: working, onReaction: undefined };
|
|
561
|
+
};
|
|
562
|
+
const { onWorking, onReaction } = ackRoutes(o.onWorking, o.onReaction);
|
|
545
563
|
/** Calls running now, when the last one ended, and the template the instant reply used last. */
|
|
546
564
|
let running = 0;
|
|
547
565
|
let lastEndedAt = Number.NEGATIVE_INFINITY;
|
|
@@ -556,8 +574,10 @@ export function buyTool(options) {
|
|
|
556
574
|
picked: null,
|
|
557
575
|
ackOut: null,
|
|
558
576
|
ackDelivered: false,
|
|
577
|
+
reaction: null,
|
|
559
578
|
sendLink,
|
|
560
579
|
onWorking,
|
|
580
|
+
onReaction,
|
|
561
581
|
onResult,
|
|
562
582
|
onProgress,
|
|
563
583
|
links: null,
|
|
@@ -609,25 +629,53 @@ export function buyTool(options) {
|
|
|
609
629
|
appAck = { at: Date.now(), said: text !== null, picked };
|
|
610
630
|
return text;
|
|
611
631
|
};
|
|
632
|
+
/** The app's own reaction: the turn its message opens sends no reaction or reply of its own. */
|
|
633
|
+
const instantReaction = (shopperText) => (instantAck(shopperText) === null ? null : ACK_REACTION);
|
|
634
|
+
/**
|
|
635
|
+
* Puts the 👀 on the turn's message. Every call of the turn waits for it before it hands over a link,
|
|
636
|
+
* and one that throws costs only the reaction: `turn.reaction` goes back to null, and the model reads
|
|
637
|
+
* that the shopper was not told.
|
|
638
|
+
*/
|
|
639
|
+
const react = (t, reacting) => {
|
|
640
|
+
t.acked = true;
|
|
641
|
+
t.reaction = ACK_REACTION;
|
|
642
|
+
let sent;
|
|
643
|
+
try {
|
|
644
|
+
sent = reacting(ACK_REACTION);
|
|
645
|
+
}
|
|
646
|
+
catch (error) {
|
|
647
|
+
sent = Promise.reject(error);
|
|
648
|
+
}
|
|
649
|
+
t.ackOut = Promise.resolve(sent).then(() => { t.ackDelivered = true; }, () => { t.reaction = null; });
|
|
650
|
+
};
|
|
612
651
|
/**
|
|
613
652
|
* Holds the instant reply for a new request until `ackDelayMs` passes. `cancel` stops one that has
|
|
614
653
|
* not gone out, and frees the turn's reply for a later call.
|
|
615
654
|
*/
|
|
616
655
|
const holdAck = (ask, t, step = null) => {
|
|
617
656
|
const onWorking = t.onWorking;
|
|
618
|
-
|
|
657
|
+
const reacting = t.onReaction;
|
|
658
|
+
if ((!onWorking && !reacting) || t.acked)
|
|
619
659
|
return null;
|
|
620
660
|
// A confirm, or a pick of what the shopper saw, gets fixed words that never say `looking for`.
|
|
621
661
|
const slow = step ?? t.picked ?? (!t.neutral && (namesShown(ask) || isPickText(ask)) ? 'pick' : null);
|
|
622
662
|
// A short answer to the question the last reply asked, such as `medium` after `what size?`, is no
|
|
623
663
|
// search: like `yes`, a slow call still tells the shopper, in words that look for nothing.
|
|
624
664
|
const neutral = t.neutral || answersQuestion(ask, lastReply);
|
|
625
|
-
|
|
665
|
+
// A slow call of any message may get the 👀, which reads nothing back; only a text reply needs one of these.
|
|
666
|
+
if (!reacting && !slow && !neutral && !isNewRequest(ask))
|
|
626
667
|
return null;
|
|
627
668
|
t.acked = true;
|
|
628
669
|
let held = true;
|
|
629
670
|
const timer = setTimeout(() => {
|
|
630
671
|
held = false;
|
|
672
|
+
// With reactions, a slow call puts the message's one 👀 on it, and never adds a line of text.
|
|
673
|
+
if (reacting) {
|
|
674
|
+
react(t, reacting);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
if (!onWorking)
|
|
678
|
+
return;
|
|
631
679
|
let text;
|
|
632
680
|
if (slow) {
|
|
633
681
|
text = slowStepLine(slow);
|
|
@@ -1149,7 +1197,7 @@ export function buyTool(options) {
|
|
|
1149
1197
|
const own = options ?? {};
|
|
1150
1198
|
if (typeof own !== 'object')
|
|
1151
1199
|
throw new TypeError('Pass the turn options as an object, or leave them out.');
|
|
1152
|
-
for (const key of ['sendLink', 'onWorking', 'onResult', 'onProgress', 'onItem']) {
|
|
1200
|
+
for (const key of ['sendLink', 'onWorking', 'onReaction', 'onResult', 'onProgress', 'onItem']) {
|
|
1153
1201
|
if (own[key] !== undefined && typeof own[key] !== 'function')
|
|
1154
1202
|
throw new TypeError(`Pass ${key} to the turn as a function, or leave it out.`);
|
|
1155
1203
|
}
|
|
@@ -1168,7 +1216,8 @@ export function buyTool(options) {
|
|
|
1168
1216
|
const state = {
|
|
1169
1217
|
...toolTurn(++modelTurn),
|
|
1170
1218
|
sendLink: (link) => feed.push({ kind: 'link', link }, () => (onItem ? onItem({ kind: 'link', link: { ...link } }) : baseSendLink?.(link))),
|
|
1171
|
-
|
|
1219
|
+
// A reaction goes to onReaction even with onItem: it sits on the shopper's bubble, not in the turn's sequence.
|
|
1220
|
+
...ackRoutes(acks ? (text) => feed.push({ kind: 'text', text }, () => (onItem ? onItem({ kind: 'text', text }) : baseOnWorking?.(text))) : undefined, own.onReaction ?? o.onReaction),
|
|
1172
1221
|
onResult: own.onResult ?? onResult,
|
|
1173
1222
|
onProgress: own.onProgress ?? onProgress,
|
|
1174
1223
|
links: [],
|
|
@@ -1178,11 +1227,12 @@ export function buyTool(options) {
|
|
|
1178
1227
|
const text = (message, skip) => feed.push({ kind: 'text', text: message }, () => onItem?.({ kind: 'text', text: message }), skip);
|
|
1179
1228
|
let ack = null;
|
|
1180
1229
|
const working = state.onWorking;
|
|
1181
|
-
|
|
1230
|
+
const reacting = state.onReaction;
|
|
1231
|
+
if (working || reacting) {
|
|
1182
1232
|
const message = String(shopperText ?? '');
|
|
1183
1233
|
const read = ackOfMessage(message);
|
|
1184
1234
|
let text = read.text;
|
|
1185
|
-
if (text !== null && writeAck) {
|
|
1235
|
+
if (text !== null && writeAck && working) {
|
|
1186
1236
|
try {
|
|
1187
1237
|
text = writeAck(message) ?? null;
|
|
1188
1238
|
}
|
|
@@ -1199,7 +1249,11 @@ export function buyTool(options) {
|
|
|
1199
1249
|
// A message that starts no search, such as `yes`: a slow call still tells the shopper, in words that name nothing.
|
|
1200
1250
|
state.neutral = true;
|
|
1201
1251
|
}
|
|
1202
|
-
else if (
|
|
1252
|
+
else if (reacting) {
|
|
1253
|
+
// A search gets its 👀 the moment the turn opens, and no text: the reaction reads nothing back.
|
|
1254
|
+
react(state, reacting);
|
|
1255
|
+
}
|
|
1256
|
+
else if (text !== null && working) {
|
|
1203
1257
|
ack = text;
|
|
1204
1258
|
state.acked = true;
|
|
1205
1259
|
let sent;
|
|
@@ -1258,6 +1312,7 @@ export function buyTool(options) {
|
|
|
1258
1312
|
agentcardMessages: { get: () => [...(handed ?? kept)], enumerable: false },
|
|
1259
1313
|
messages: { value: messagesFor, enumerable: false },
|
|
1260
1314
|
ack: { value: ack, enumerable: false },
|
|
1315
|
+
reaction: { get: () => state.reaction, enumerable: false },
|
|
1261
1316
|
links: { get: () => [...links], enumerable: false },
|
|
1262
1317
|
results: { get: () => [...results], enumerable: false },
|
|
1263
1318
|
items: { get: () => feed.items.map((item) => (item.kind === 'link' ? { kind: 'link', link: { ...item.link } } : { ...item })), enumerable: false },
|
|
@@ -1274,5 +1329,5 @@ export function buyTool(options) {
|
|
|
1274
1329
|
},
|
|
1275
1330
|
});
|
|
1276
1331
|
};
|
|
1277
|
-
return toolOf(answer, { instantAck, turn }, {});
|
|
1332
|
+
return toolOf(answer, { instantAck, instantReaction, turn }, {});
|
|
1278
1333
|
}
|
package/dist/definition.js
CHANGED
|
@@ -11,7 +11,7 @@ export const BUY_TOOL_DESCRIPTION = [
|
|
|
11
11
|
'Buy what this shopper asks for at real stores.',
|
|
12
12
|
"Put the shopper's own words in `ask`.",
|
|
13
13
|
'Tell the shopper what `reply` says, in your own voice, except the products and stores it lists that reach the shopper as cards.',
|
|
14
|
-
"When `shopper_already_told_looking` is true, the shopper
|
|
14
|
+
"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.",
|
|
15
15
|
'Show the shopper the cart before you confirm it.',
|
|
16
16
|
'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.',
|
|
17
17
|
'When `next_action` is null and `reply` is about something other than a cart, ask for no checkout step: a cart from an earlier request gets one short line at most, after the answer to what the shopper just asked, and never a question about confirming or placing it.',
|
|
@@ -32,6 +32,12 @@ export const BUY_TOOL_DESCRIPTION = [
|
|
|
32
32
|
'Store links reach the shopper on their own, as cards that show each store\'s name and delivery time. When `store_links_sent` is above zero, never list those stores again: ask which one in one short line.',
|
|
33
33
|
'When `store_links_sent` is 0 and you offer the `stores`, name each one by its `name`, never only "their links are there".',
|
|
34
34
|
'When `pick.in_cart` is false, the product the shopper picked is not in the cart: tell the shopper what `reply` says, and never ask them to confirm the cart.',
|
|
35
|
+
"Talk to the shopper only about their order, in their terms: what is in the cart, what was found and what they can do next. Leave out how it was done, even when `reply` says it: never mention this tool, `reply`, a field, a system, \"my end\" or what you can or can't see, the searches run, the stores checked or not, a timeout, account setup, or a link that did not go out. When parts of the result disagree, say what you are sure of and ask, never which part said what.",
|
|
36
|
+
'Say as fact only what the result shows: never promise what a store will do later, such as confirm a size when the order goes through.',
|
|
37
|
+
'Never tell the shopper to remove, delete or replace a card or to change a setting, even when `reply` says to.',
|
|
38
|
+
'Name each link by what it is for, from `next_action`: the link to add a card, to sign in to the store, or to approve the payment.',
|
|
39
|
+
'Within a step, ask for one detail per message, such as the delivery address before the name for the label.',
|
|
40
|
+
'Write every sentence in your own style, casing included, even where `reply` is written another way.',
|
|
35
41
|
].join(' ');
|
|
36
42
|
/** A fresh copy of the tool's definition, so a caller that edits one never changes another. */
|
|
37
43
|
export function buyToolDefinition() {
|
package/dist/index.d.ts
CHANGED
|
@@ -5,8 +5,8 @@ export { agentcardWebhook, purchaseAgentWebhook, type AgentcardWebhookOptions, t
|
|
|
5
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 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, type BuyTool, type BuyToolOptions, type BuyToolResult, type BuyToolStatus, type BuyToolLink, type BuyToolProduct, type BuyToolStore, type BuyToolTurn, type BuyToolTurnItem, type BuyToolTurnOptions, type BuyToolTurnMessage, } from './buy-tool.js';
|
|
9
|
-
export { instantAck, ackFor, type AckForOptions } from './ack.js';
|
|
8
|
+
export { buyTool, type BuyTool, type BuyToolOptions, type BuyToolResult, type BuyToolStatus, type BuyToolLink, type BuyToolProduct, type BuyToolStore, type BuyToolTurn, type BuyToolTurnItem, type BuyToolTurnOptions, type BuyToolTurnMessage, type BuyAckMode, } from './buy-tool.js';
|
|
9
|
+
export { instantAck, instantReaction, ackFor, ACK_REACTION, type AckForOptions } from './ack.js';
|
|
10
10
|
export { forModel, forModelError, stripLinks, type BuyToolModelView, type BuyToolModelCart, type BuyToolModelProduct, type BuyToolModelStore, } from './model-view.js';
|
|
11
11
|
export { toAnthropicTool, toOpenRouterTool, type AnthropicBuyTool, type OpenRouterToolkit, type ZodLike, } from './adapters.js';
|
|
12
12
|
export { listenToAgentcard, type AgentcardListener, type ListenOptions, type ListenCursorStore, type StreamFetch, } from './listen.js';
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ export { AgentcardClient, AgentcardApiError, BUY_TURN_TIMEOUT_MS, DEFAULT_BASE_U
|
|
|
6
6
|
export { BUY_TOOL_NAME, BUY_TOOL_DESCRIPTION, buyToolDefinition, parseBuyToolInput, } from './definition.js';
|
|
7
7
|
export { toMessages, messageStream, typingDelay, } from './messages.js';
|
|
8
8
|
export { buyTool, } from './buy-tool.js';
|
|
9
|
-
export { instantAck, ackFor } from './ack.js';
|
|
9
|
+
export { instantAck, instantReaction, ackFor, ACK_REACTION } from './ack.js';
|
|
10
10
|
export { forModel, forModelError, stripLinks, } from './model-view.js';
|
|
11
11
|
export { toAnthropicTool, toOpenRouterTool, } from './adapters.js';
|
|
12
12
|
export { listenToAgentcard, } from './listen.js';
|
package/dist/message.d.ts
CHANGED
|
@@ -12,12 +12,17 @@ import { type AgentcardEvent } from './verify.js';
|
|
|
12
12
|
* - `order_unconfirmed`: the store's reply to the order was lost, so the order may exist; Agentcard is confirming it.
|
|
13
13
|
* - `approval_expiring`: the approval link expires soon; `text` carries the link.
|
|
14
14
|
* - `approval_expired`: the approval expired before the shopper answered, and nothing was charged.
|
|
15
|
+
* - `approval_declined`: the shopper declined the approval, and nothing was charged.
|
|
16
|
+
* - `payment_declined`: the shopper approved, and the payment was refused before the order went in; `facts.reason` says why. Nothing was charged.
|
|
15
17
|
* - `card_link_unfinished`: the shopper has not added a card from their card link; `text` carries the link again.
|
|
16
18
|
* - `card_link_expired`: the card link expired before the shopper added a card.
|
|
17
19
|
* - `cart_idle`: a cart sat without being sent for approval; `facts.nudgeNumber` counts the nudges.
|
|
18
20
|
* - `card_added`: the shopper saved a card from the card link your app sent, and can check out with it.
|
|
21
|
+
* - `store_connected`: the shopper signed in to their own account at the store from the sign-in link your app sent, so the order can go through on it.
|
|
22
|
+
* - `store_connect_failed`: the shopper's sign-in to the store did not go through, and the link no longer works.
|
|
23
|
+
* - `store_connect_expired`: the sign-in link expired before the shopper finished signing in.
|
|
19
24
|
*/
|
|
20
|
-
export type PurchaseMessageKind = 'order_placed' | 'order_failed' | 'order_not_placed_yet' | 'order_unconfirmed' | 'approval_expiring' | 'approval_expired' | 'card_link_unfinished' | 'card_link_expired' | 'cart_idle' | 'card_added';
|
|
25
|
+
export type PurchaseMessageKind = 'order_placed' | 'order_failed' | 'order_not_placed_yet' | 'order_unconfirmed' | 'approval_expiring' | 'approval_expired' | 'approval_declined' | 'payment_declined' | 'card_link_unfinished' | 'card_link_expired' | 'cart_idle' | 'card_added' | 'store_connected' | 'store_connect_failed' | 'store_connect_expired';
|
|
21
26
|
/** The message's values one by one, for a company that writes its own copy. A fact that does not apply is absent. */
|
|
22
27
|
export interface PurchaseFacts {
|
|
23
28
|
merchantName?: string;
|
|
@@ -28,7 +33,7 @@ export interface PurchaseFacts {
|
|
|
28
33
|
cardBrand?: string;
|
|
29
34
|
cardLast4?: string;
|
|
30
35
|
orderId?: string;
|
|
31
|
-
/** Why an order failed or was not placed, in words. */
|
|
36
|
+
/** Why an order failed or was not placed, or a payment was declined, in words. */
|
|
32
37
|
reason?: string;
|
|
33
38
|
approvalUrl?: string;
|
|
34
39
|
/** When the approval link stops working, as an ISO 8601 timestamp. */
|
package/dist/model-view.d.ts
CHANGED
|
@@ -66,6 +66,12 @@ export interface BuyToolModelView {
|
|
|
66
66
|
shopper_already_told_looking: boolean;
|
|
67
67
|
/** True when the shopper already has the link this answer needed. */
|
|
68
68
|
link_sent: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Which links went out with this answer: `store_login` is the store's own login page (the shopper
|
|
71
|
+
* logs in to their account there), `vault` is the page where they add a card, `approval` the page
|
|
72
|
+
* where they approve the payment. Name the link by what it is: a store login is never a card link.
|
|
73
|
+
*/
|
|
74
|
+
links_sent: Array<'vault' | 'approval' | 'store_login'>;
|
|
69
75
|
/** How many of the products above the shopper holds as cards, each with its name, price and picture; 0 when none went out. */
|
|
70
76
|
product_links_sent: number;
|
|
71
77
|
/** The product this call's `product_id` picked, and whether the cart now holds it; null when the call picked none. */
|
package/dist/model-view.js
CHANGED
|
@@ -4,8 +4,14 @@ const MAX_PRODUCTS = 20;
|
|
|
4
4
|
const MARKDOWN_LINK = /\[([^\]]*)\]\([^)]*\)/g;
|
|
5
5
|
const SCHEME_URL = /\b[a-z][a-z0-9+.-]*:\/\/[^\s<>"'`)\]]*/gi;
|
|
6
6
|
const WWW_URL = /\bwww\.[^\s<>"'`)\]]*/gi;
|
|
7
|
-
/**
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* A host followed by a path, a query or a fragment: `vault.agentcard.sh/a/1`, `vault.agentcard.sh?token=abc`. A `?` that
|
|
9
|
+
* only punctuation follows before a space, a closing bracket or quote, or the end starts no query: in "at glossier.com?
|
|
10
|
+
* Say yes" it is the question's mark, and the host reads as its site's name (BARE_HOST). A `?` that more of an address
|
|
11
|
+
* follows starts one, punctuation or not (`glossier.com?.ref=x`), and a `#` always starts a fragment
|
|
12
|
+
* (`glossier.com#:~:text=Balm`).
|
|
13
|
+
*/
|
|
14
|
+
const HOST_PATH = /\b(?:[a-z0-9-]+\.)+[a-z]{2,}(?:[/#]|\?(?![.,;:!?]*(?:[\s<>"'`)\]]|$)))[^\s<>"'`)\]]*/gi;
|
|
9
15
|
/**
|
|
10
16
|
* A bare host on a common top-level domain, such as `agentcard.sh` or `amazon.com`, which a chat app
|
|
11
17
|
* turns into a link. The list keeps words with a dot, such as `Node.js`, and an email address's host.
|
|
@@ -163,6 +169,9 @@ export function forModel(result) {
|
|
|
163
169
|
shopper_need_not_reply: result.shopper_need_not_reply === true,
|
|
164
170
|
shopper_already_told_looking: result.shopper_already_told_looking === true,
|
|
165
171
|
link_sent: result.link_sent === true,
|
|
172
|
+
links_sent: result.link_sent === true
|
|
173
|
+
? [...new Set((result.links ?? []).map((link) => link.kind).filter((kind) => kind === 'vault' || kind === 'approval' || kind === 'store_login'))]
|
|
174
|
+
: [],
|
|
166
175
|
// A card per product, so two dishes on a restaurant's one page are two cards.
|
|
167
176
|
product_links_sent: new Set(products.filter(held).map(productCardKey)).size,
|
|
168
177
|
// The id the model sent, never the catalog id, which is a store page for a retail product.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-cards/commerce-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Give your agent one tool that buys for your shoppers at real stores, and post each purchase update Agentcard sends into the shopper's chat, over a connection your server opens or a webhook endpoint.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|