@agent-cards/commerce-sdk 0.2.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -4
- package/dist/ack.d.ts +51 -0
- package/dist/ack.js +166 -0
- package/dist/adapters.d.ts +4 -1
- package/dist/adapters.js +1 -0
- package/dist/buy-tool.d.ts +63 -3
- package/dist/buy-tool.js +227 -21
- package/dist/client.d.ts +78 -7
- package/dist/client.js +147 -13
- package/dist/definition.d.ts +15 -4
- package/dist/definition.js +20 -7
- package/dist/events.d.ts +12 -0
- package/dist/events.js +79 -0
- package/dist/handler.d.ts +58 -5
- package/dist/handler.js +207 -46
- package/dist/index.d.ts +7 -3
- package/dist/index.js +6 -2
- package/dist/listen.d.ts +93 -0
- package/dist/listen.js +496 -0
- package/dist/message.d.ts +3 -1
- package/dist/model-view.d.ts +25 -5
- package/dist/model-view.js +48 -10
- package/dist/shopper-message.d.ts +86 -0
- package/dist/shopper-message.js +175 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @agent-cards/commerce-sdk
|
|
2
2
|
|
|
3
|
-
Give your agent one tool that buys for your shoppers at real stores, and tell each shopper what happened to their purchase in your agent's voice. The buy tool sends what the shopper says to Agentcard, and Agentcard finds the products, builds the cart and places the order once the shopper approves it. Agentcard then
|
|
3
|
+
Give your agent one tool that buys for your shoppers at real stores, and tell each shopper what happened to their purchase in your agent's voice. The buy tool sends what the shopper says to Agentcard, and Agentcard finds the products, builds the cart and places the order once the shopper approves it. Agentcard then tells your server about each moment a shopper should hear about, such as a card saved, an order going through or an approval link about to expire, and the package hands you a message ready to post into that shopper's chat. Your server opens the connection to Agentcard, so you host no webhook endpoint.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -10,6 +10,39 @@ npm install @agent-cards/commerce-sdk
|
|
|
10
10
|
|
|
11
11
|
The package runs on Node 18 or later and has no dependencies.
|
|
12
12
|
|
|
13
|
+
## Hear from Agentcard without webhooks
|
|
14
|
+
|
|
15
|
+
Call `listen` once when your server starts. Your server opens one connection to Agentcard with your company credentials, and Agentcard sends every event for your company down it, so you need no public URL, no webhook secret and no signature check:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { AgentcardClient } from '@agent-cards/commerce-sdk';
|
|
19
|
+
|
|
20
|
+
const agentcard = new AgentcardClient({ clientId: process.env.AGENTCARD_CLIENT_ID ?? '', clientSecret: process.env.AGENTCARD_CLIENT_SECRET ?? '' });
|
|
21
|
+
agentcard.listen({
|
|
22
|
+
onMessage: (shopperId, message) => postToChat(shopperId, message.text),
|
|
23
|
+
onEvent: (event) => console.log(event.type, event.data),
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`postToChat` stands for your own code. `onMessage` gets each message for one of your shoppers under your own id for them, the `externalUserId` you gave the buy tool, with the same ready-to-send `text` as `agentcardWebhook`: a card saved (`card_added`), an order placed, an approval about to expire, a delivery on its way. `onEvent` gets every other event Agentcard sends your company, such as `vault.card_stored`, `checkout_authorization.approved` or `transfer.completed`, as the same envelope a webhook endpoint would receive, with the same `id`. Without `onMessage`, `onEvent` gets every event. `AGENTCARD_EVENT_TYPES` lists every type, and `events: ['order.*', 'purchase.message']` keeps only the ones you name.
|
|
28
|
+
|
|
29
|
+
The listener keeps itself connected. It reconnects after a dropped connection, waiting longer after each failure up to 30 seconds, and reconnects when Agentcard sends no heartbeat for 45 seconds. Each reconnect resumes after the last event it handled, so nothing that happened while it was away is lost, and it skips an event it already handled. Agentcard keeps each event for the stream for 72 hours. Where the stream does not get through, the listener reads the same events every two seconds instead.
|
|
30
|
+
|
|
31
|
+
Events run one at a time, in the order Agentcard recorded them. The listener waits for your callback before it hands over the next event, and a callback that throws runs again, up to five times, before the listener passes the error to `onError` and moves on.
|
|
32
|
+
|
|
33
|
+
A new process starts from the moment it connects. To resume where the last process stopped, pass a `cursor` store. After a resume, Agentcard hands over the last ten seconds of events again, in case one of them arrived late. The listener saves the cursor with the id of each event it handled in that window, so a new process skips the ones it already handled:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
agentcard.listen({
|
|
37
|
+
onMessage: (shopperId, message) => postToChat(shopperId, message.text),
|
|
38
|
+
cursor: { load: () => redis.get('agentcard:cursor'), save: (cursor) => redis.set('agentcard:cursor', cursor) },
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`listen` returns `{ ready, close }`: `ready` resolves once the listener reaches Agentcard, and `close()` stops it after the callback that is running returns. Run one listener per company and mode. Two listeners each hear every event, so with two, skip a `message.eventId` you already stored. Sandbox credentials hear sandbox events, live credentials live ones, and a shopper message is always live.
|
|
43
|
+
|
|
44
|
+
`listen` needs a process that keeps running, such as a Node server, a worker or a container. On a host that ends each request after a few seconds, such as a serverless function, take the events at a webhook endpoint instead, as "Hear from Agentcard at a webhook endpoint" shows.
|
|
45
|
+
|
|
13
46
|
## Give your agent the buy tool
|
|
14
47
|
|
|
15
48
|
Build one client with your company's credentials and one buy tool per shopper, then pass the tool to your agent as it is:
|
|
@@ -41,7 +74,7 @@ const answer = await new Anthropic().beta.messages.toolRunner({
|
|
|
41
74
|
|
|
42
75
|
Build the client once and keep it. Every shopper's tool shares the client's access token, and the client renews the token a minute before it expires. Keep each shopper's tool as well. Agentcard keeps each purchase between calls, so a tool built for every message buys the same way, but it sends the same product links again each time Agentcard shows the same products.
|
|
43
76
|
|
|
44
|
-
The model sends `ask`, `confirm` and `
|
|
77
|
+
The model sends `ask`, `confirm`, `new_order` and `product_id`, and nothing else. Pass `deliveryAddress` when you already hold the shopper's address: the order goes there, and Agentcard never asks the shopper for it. The model can change neither the address nor the shopper.
|
|
45
78
|
|
|
46
79
|
## Use the tool in any framework
|
|
47
80
|
|
|
@@ -101,10 +134,63 @@ console.log(await result.getText());
|
|
|
101
134
|
|
|
102
135
|
A purchase needs a link at three moments: the shopper approves the purchase on an approval page, a new shopper adds a card on a Vault page, and a shopper signs in to a store's own account on its login page before Agentcard can buy there. Each link's `kind` says which: `approval`, `vault` or `store_login`, and a `store_login` link names the store in `store`. The tool hands each link to `sendLink` and waits for it, so the shopper holds the link before the model answers. The model reads `link_sent: true` and never the link itself, because a tool runner puts each answer straight into the model's context, and a model that holds a link writes it into the chat.
|
|
103
136
|
|
|
104
|
-
The tool also hands `sendLink` a `product` link for each product a search finds, up to three per answer. The link carries the product's page at its store in `url`, the product's name in `title`, its price in `price`, such as `$12.99`,
|
|
137
|
+
The tool also hands `sendLink` a `product` link for each product a search finds, up to three per answer. The link carries the product's page at its store in `url`, the product's name in `title`, its price in `price`, such as `$12.99`, Agentcard's id for it in `productId`, and its picture in `imageUrl`, or null when the store sent none. Every price the tool writes, in a link or in the model's view, uses the currency Agentcard names for the cart or the product, in that currency's own minor units: `1331` is `$13.31` in USD, `CA$13.31` in CAD and `¥1,331` in JPY. A price whose currency Agentcard does not name shows with no symbol. The link arrives beside the reply, and you decide how to show it: as a card, in a list, or as a text with the URL. The model reads `product_links_sent` and never a URL, so a shopper who asks for links hears that the links are there. When a later answer in the conversation only stands beside the same products, as a cart answer does, the tool sends none of their links again. When a later turn's reply presents them again, as the answer to a repeated ask does, the tool sends their links again, so every product the reply names arrives with its link. A product link your `sendLink` throws on counts as not sent, and the call goes on.
|
|
138
|
+
|
|
139
|
+
`productId` holds no link: it is the same `product_id` the model reads for that product, and the page is `url`. An answer that only adds a product the shopper picked lists that product alone, so it sends no card for a product nobody offered on that turn, and the pages the search already sent still count as sent on the answers after it.
|
|
105
140
|
|
|
106
141
|
Without `sendLink`, `result.links` still carries each Vault, approval and store login link for your code, each product in `result.products` carries its `url`, and the model still never reads one.
|
|
107
142
|
|
|
143
|
+
## Add the exact product the shopper picks
|
|
144
|
+
|
|
145
|
+
Each product in the model's view carries a `product_id`, Agentcard's id for it with no link in it. When the shopper picks one of those products, by its name, its place in the list or a tap on its card that sends its title, the model passes that `product_id`, and Agentcard adds exactly that product from the products it showed, with no new search. Your app sends no id of its own: a "Buy this" button that sends the shopper's words, such as `I'll take the KODAK AA Alkaline Batteries (24 Pack) ($13.49)`, is enough, because the model reads the `product_id` beside that name in its last result. Keep each tool result in the model's history, as a tool runner does, so the model still holds those ids on the shopper's next message. Your own code can pick too: a card's button can call `buy.answer` with the link's `productId` as `product_id`, and Agentcard adds that product.
|
|
146
|
+
|
|
147
|
+
`result.pick` says what happened: the product's id, and `in_cart`, whether the cart now holds it. The model reads the same `pick` in its view, by the `product_id` it sent. A pick the cart does not hold names no next step, so the model tells the shopper what Agentcard said rather than asking for a card or a yes for an older cart. A product Agentcard no longer holds, because the products are more than 15 minutes old or a newer search replaced them, adds nothing, and the model reads that it should search again with the shopper's words.
|
|
148
|
+
|
|
149
|
+
## Tell the shopper you are on it
|
|
150
|
+
|
|
151
|
+
A first search can take Agentcard most of a minute. Pass `onWorking`, and the shopper reads an instant reply while Agentcard looks, 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`:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const buy = buyTool({
|
|
155
|
+
agentcard,
|
|
156
|
+
externalUserId: shopper.id,
|
|
157
|
+
sendLink: (link) => textShopper(shopper.id, link.url),
|
|
158
|
+
onWorking: (text) => textShopper(shopper.id, text),
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
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, and none on a confirm, on a pick of a product the shopper saw, or on an answer such as `yes`, `2`, `the first one` or a code. 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.
|
|
163
|
+
|
|
164
|
+
When the instant reply 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 own reply opens with the answer or the question rather than a second `on it`. It reads false on a turn that sent none, and when `onWorking` threw.
|
|
165
|
+
|
|
166
|
+
Pass `ack: false` to send none, `ack: (ask) => text` to write your own from the shopper's words, where an empty answer sends none, and `ackDelayMs` to change the 1.5 seconds.
|
|
167
|
+
|
|
168
|
+
The model takes several seconds to call the tool after the shopper's message arrives. To answer sooner, call `buy.instantAck(message)` the moment the message arrives, before your model runs, and send the text it returns:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
const ack = buy.instantAck(message);
|
|
172
|
+
if (ack) await textShopper(shopper.id, ack);
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`instantAck` returns null for a message that starts no search: a confirmation or a short answer such as `yes`, `1` or `ok`, 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 or a zip code, and any message that answers the last answer's request for shipping or guest details or a code. The reply never reads those details back. Once you call it, the tool sends no reply of its own through `onWorking` for that turn, and the model reads `shopper_already_told_looking: true` when your reply went out, so the shopper hears one acknowledgement.
|
|
176
|
+
|
|
177
|
+
Without a buy tool, `instantAck(message, { previous, nextAction })` from the package root decides the same way: `previous` is the reply you sent last, so the next one comes from another template, and `nextAction` is the last answer's `next_action`. Pass `ack: false` to the tool when you send it yourself. `ackFor` is the earlier name of the same function.
|
|
178
|
+
|
|
179
|
+
## Show what Agentcard is doing
|
|
180
|
+
|
|
181
|
+
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":
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
const buy = buyTool({
|
|
185
|
+
agentcard,
|
|
186
|
+
externalUserId: shopper.id,
|
|
187
|
+
sendLink: (link) => textShopper(shopper.id, link.url),
|
|
188
|
+
onProgress: ({ stage }) => showStatus(shopper.id, stage),
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
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.
|
|
193
|
+
|
|
108
194
|
## Send each paragraph as its own message
|
|
109
195
|
|
|
110
196
|
A chat app reads best when each paragraph arrives as its own message. `toMessages` cuts text at its blank lines, trims each message and leaves out the empty ones. A list whose items blank lines separate stays one message, and so does a fenced code block:
|
|
@@ -163,6 +249,7 @@ The model reads one object per call:
|
|
|
163
249
|
"products": [],
|
|
164
250
|
"approval_expires_at": null,
|
|
165
251
|
"shopper_need_not_reply": false,
|
|
252
|
+
"shopper_already_told_looking": false,
|
|
166
253
|
"link_sent": false,
|
|
167
254
|
"product_links_sent": 0
|
|
168
255
|
}
|
|
@@ -170,6 +257,8 @@ The model reads one object per call:
|
|
|
170
257
|
|
|
171
258
|
`cart` is the newest cart, and `carts` lists every cart on the table, the newest last, so a shopper buying from more than one store hears each one.
|
|
172
259
|
|
|
260
|
+
Each product in `products` names its `store`, and a product Agentcard sends without a store is left out, so the model never offers a product without saying where it is sold. No link reaches the model: an address with a path or a scheme is taken out, and a bare one, such as `walmart.com` in `reply`, reads as the site's name, `Walmart`.
|
|
261
|
+
|
|
173
262
|
`status` says where the purchase stands:
|
|
174
263
|
|
|
175
264
|
| `status` | What happened |
|
|
@@ -194,6 +283,20 @@ const view = buy.forModel(result); // the tool result your model reads
|
|
|
194
283
|
|
|
195
284
|
`buy.run`, `buy.execute` and `buy.handle` each return the same text, `JSON.stringify(buy.forModel(result))`: Anthropic's tool runner calls `buy.run`, the Vercel AI SDK calls `buy.execute`, and your own loop calls `buy.handle`.
|
|
196
285
|
|
|
286
|
+
## Show the cart in your app
|
|
287
|
+
|
|
288
|
+
Pass `onResult` to read every answer while your framework keeps calling `buy.run`, so you never wrap the tool or parse the model's JSON. It gets the whole answer once per call, after its links went out and before the model reads it: the carts, `status`, `next_action` and `order_id`. This one keeps the cart above the chat input:
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
const buy = buyTool({
|
|
292
|
+
agentcard,
|
|
293
|
+
externalUserId: shopper.id,
|
|
294
|
+
onResult: (result) => showCartAboveInput(shopper.id, result.carts, result.next_action),
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
A refusal the tool turns into an answer, such as the Vault link for a shopper with no card, reaches `onResult` too. A call that throws calls nothing. An `onResult` that throws costs only that call to it, and the model still reads the answer. If you wrap `buy.answer` yourself, hand its errors to `forModelError` before a model reads them, and every link is taken out.
|
|
299
|
+
|
|
197
300
|
## Buy with a shopper's own token
|
|
198
301
|
|
|
199
302
|
A tool that buys for one signed-in shopper takes that shopper's own Agentcard access token instead of a client, and names nobody:
|
|
@@ -231,7 +334,55 @@ The tool sends each purchase request once, because a purchase turn runs against
|
|
|
231
334
|
|
|
232
335
|
`buy.answer` throws the `AgentcardApiError` itself. `buy.run`, `buy.execute` and `buy.handle` throw the same message with every link taken out, and keep the original error on `error.cause` for your logs.
|
|
233
336
|
|
|
234
|
-
##
|
|
337
|
+
## Hear from Agentcard at a webhook endpoint
|
|
338
|
+
|
|
339
|
+
A company that prefers webhooks, or runs on a host where no process keeps running, takes the same events at an endpoint of its own. Agentcard tells your webhook endpoint about each moment a shopper should hear about, even when the shopper is not chatting: a card saved, an approval about to expire, an order placed, a delivery on its way. `agentcardWebhook` checks each delivery's signature, skips an event it already handled, and calls `onMessage` with your own id for the shopper and a message ready to post into that shopper's chat:
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
import express from 'express';
|
|
343
|
+
import { agentcardWebhook } from '@agent-cards/commerce-sdk';
|
|
344
|
+
|
|
345
|
+
const app = express();
|
|
346
|
+
app.post('/agentcard/webhooks', express.raw({ type: 'application/json' }), agentcardWebhook({
|
|
347
|
+
secret: process.env.AGENTCARD_WEBHOOK_SECRET ?? '', // returned once, when you created the endpoint
|
|
348
|
+
onMessage: (shopperId, message) => postToChat(shopperId, message.text),
|
|
349
|
+
}));
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
`postToChat` stands for your own code. `shopperId` is the `externalUserId` you gave the buy tool, so the message lands in the chat the purchase started in. In a Next.js route handler, export the same handler's `fetch`:
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
const webhook = agentcardWebhook({ secret: process.env.AGENTCARD_WEBHOOK_SECRET ?? '', onMessage: (shopperId, message) => postToChat(shopperId, message.text) });
|
|
356
|
+
export const POST = webhook.fetch;
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Subscribe your endpoint to each event below by name. A catch-all `*` alone does not turn `purchase.message` on. `SHOPPER_EVENT_TYPES` holds the three names:
|
|
360
|
+
|
|
361
|
+
| Event | `kind` | When it arrives | Who wrote `text` |
|
|
362
|
+
|---|---|---|---|
|
|
363
|
+
| `purchase.message` | `card_added` | The shopper saved a card from the card link your app sent, or picked one they keep. | Agentcard |
|
|
364
|
+
| `purchase.message` | `order_placed` | The shopper approved, and Agentcard placed the order. | Agentcard |
|
|
365
|
+
| `purchase.message` | `order_failed` | The order failed after the shopper approved. | Agentcard |
|
|
366
|
+
| `purchase.message` | `order_not_placed_yet` | The shopper approved, and the order is not placed yet. | Agentcard |
|
|
367
|
+
| `purchase.message` | `order_unconfirmed` | The store's reply to the order was lost, so the order may exist, and Agentcard is confirming it. | Agentcard |
|
|
368
|
+
| `purchase.message` | `approval_expiring` | The approval link expires soon. `text` carries the link. | Agentcard |
|
|
369
|
+
| `purchase.message` | `approval_expired` | The approval expired before the shopper answered, and no charge went through. | Agentcard |
|
|
370
|
+
| `purchase.message` | `card_link_unfinished` | The shopper has not added a card from their card link. `text` carries the link again. | Agentcard |
|
|
371
|
+
| `purchase.message` | `card_link_expired` | The card link expired before the shopper added a card. | Agentcard |
|
|
372
|
+
| `purchase.message` | `cart_idle` | A cart sat, and nobody sent it for approval. | Agentcard |
|
|
373
|
+
| `order.confirmed` | `order_confirmed` | The retailer accepted the order on its side, with its own order number and delivery window. | The package |
|
|
374
|
+
| `order.updated` | `order_updated` | The order reached a new delivery stage, named in `facts.orderStatus`: `confirmed`, `courier_assigned`, `courier_at_store`, `ready_for_pickup`, `picked_up`, `arriving`, `delivered` or `canceled`. | The package |
|
|
375
|
+
|
|
376
|
+
Every message carries `text`, ready to send as is, `note` for your agent, `facts` with the values one by one, `externalUserId`, `userId`, `conversationId` when Agentcard names the buy conversation, and `eventId`. A kind Agentcard adds later still carries `text`.
|
|
377
|
+
|
|
378
|
+
The handler answers when `onMessage` is done: `200` when it returns, `500` when it throws, and Agentcard then delivers the event again, so a failed post is not lost. It remembers the last 10,000 events it finished, so a redelivery of one runs nothing, and a second delivery that lands while the first still runs waits for its answer. That memory belongs to one process: when more than one server takes deliveries, also skip a `message.eventId` you already stored. A message whose link or window closed before it arrived (`isStale`) reaches no callback. A message for a shopper you gave Agentcard no id for, and every other event, such as `vault.card_stored` or `order.placed`, goes to `onEvent`. Agentcard waits 10 seconds for the answer, so keep `onMessage` short: post the text, or queue the agent turn, then return.
|
|
379
|
+
|
|
380
|
+
To read an event without the handler, `readShopperMessage(event)` returns the same message, or null for an event that carries none.
|
|
381
|
+
|
|
382
|
+
## Mount the purchase message handler
|
|
383
|
+
|
|
384
|
+
`purchaseAgentWebhook` is the handler from earlier versions. It hands you `purchase.message` events only, and leaves deduplication to you. New code takes `agentcardWebhook`.
|
|
385
|
+
|
|
235
386
|
|
|
236
387
|
Subscribe your webhook endpoint to `purchase.message`, then mount the handler on its route with `express.raw`:
|
|
237
388
|
|
|
@@ -304,6 +455,8 @@ The facts are `merchantName`, `totalCents`, `currency`, `cardBrand`, `cardLast4`
|
|
|
304
455
|
| `card_link_unfinished` | The shopper has not added a card from their card link. `text` carries the link again. |
|
|
305
456
|
| `card_link_expired` | The card link expired before the shopper added a card. |
|
|
306
457
|
| `cart_idle` | A cart sat, and nobody sent it for approval. |
|
|
458
|
+
| `order_unconfirmed` | The store's reply to the order was lost, and Agentcard is confirming it. |
|
|
459
|
+
| `card_added` | The shopper saved a card from the card link your app sent. |
|
|
307
460
|
|
|
308
461
|
Agentcard can add a kind later. A kind your code does not know still carries `text`, ready to send as is.
|
|
309
462
|
|
package/dist/ack.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The instant reply a shopper reads while Agentcard works on their request, such as `on it, looking
|
|
3
|
+
* for usb-c cables under $20`. A search can take Agentcard most of a minute, and a shopper who hears
|
|
4
|
+
* nothing in that time asks again. The words come from fixed templates, rotated so one shopper never
|
|
5
|
+
* reads the same one twice in a row, with a short paraphrase of the request when one can be read
|
|
6
|
+
* safely from it. No model writes them, so they cost nothing and arrive at once.
|
|
7
|
+
*
|
|
8
|
+
* The paraphrase never carries a link, an email address, a phone number, a street address or a long
|
|
9
|
+
* number: the text stops before the first one, and a request that starts with one gets the template
|
|
10
|
+
* without a paraphrase. A reply to a question, such as `yes`, `2`, `the first one` or a code, gets no
|
|
11
|
+
* instant reply at all, because it starts no search.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* True when the shopper's words start a request, and false for an answer to a question, a pick, a
|
|
15
|
+
* code, small talk, or a message that carries the shopper's own details, such as a phone number or a
|
|
16
|
+
* street address.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isNewRequest(text: string): boolean;
|
|
19
|
+
/** A short paraphrase of what the shopper asks for, safe to read back to them, or null when none is. */
|
|
20
|
+
export declare function paraphrase(text: string): string | null;
|
|
21
|
+
/** The template at `index` for these words, with the paraphrase when one is safe. */
|
|
22
|
+
export declare function ackAt(index: number, text: string): string;
|
|
23
|
+
/** A template other than `avoid`, picked at random. */
|
|
24
|
+
export declare function nextTemplate(avoid: number, random?: () => number): number;
|
|
25
|
+
export interface AckForOptions {
|
|
26
|
+
/** The instant reply this shopper read last, so the next one comes from another template. */
|
|
27
|
+
previous?: string;
|
|
28
|
+
/**
|
|
29
|
+
* The step the last purchase answer asked the shopper for, its `next_action`. After a question for
|
|
30
|
+
* shipping or guest details or a code, the next message answers it, so it gets no instant reply.
|
|
31
|
+
*/
|
|
32
|
+
nextAction?: string | null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The instant reply for a shopper's message, such as `on it, looking for usb-c cables under $20`, to
|
|
36
|
+
* send the moment the message arrives and before your model runs; or null for a message that starts no
|
|
37
|
+
* search: a confirmation or a short answer such as `yes`, `1` or `ok`, a code, small talk such as `hi`,
|
|
38
|
+
* and the shopper's own details, such as a name, a phone number or an address. The words come from
|
|
39
|
+
* fixed lowercase templates, rotated, and never repeat the shopper's personal details.
|
|
40
|
+
*
|
|
41
|
+
* With the buy tool, call `buy.instantAck(text)` instead: the tool then sends no reply of its own
|
|
42
|
+
* through `onWorking` for that turn, and tells the model the shopper already heard one.
|
|
43
|
+
*
|
|
44
|
+
* ```ts
|
|
45
|
+
* instantAck('can you find me usb-c cables under $20'); // 'on it, looking for usb-c cables under $20', or another template
|
|
46
|
+
* instantAck('karen, 628 208 4916, 1801 wedemeyer st'); // null
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare function instantAck(shopperText: string, options?: AckForOptions): string | null;
|
|
50
|
+
/** The earlier name of `instantAck`, kept for apps that call it. */
|
|
51
|
+
export declare function ackFor(text: string, options?: AckForOptions): string | null;
|
package/dist/ack.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The instant reply a shopper reads while Agentcard works on their request, such as `on it, looking
|
|
3
|
+
* for usb-c cables under $20`. A search can take Agentcard most of a minute, and a shopper who hears
|
|
4
|
+
* nothing in that time asks again. The words come from fixed templates, rotated so one shopper never
|
|
5
|
+
* reads the same one twice in a row, with a short paraphrase of the request when one can be read
|
|
6
|
+
* safely from it. No model writes them, so they cost nothing and arrive at once.
|
|
7
|
+
*
|
|
8
|
+
* The paraphrase never carries a link, an email address, a phone number, a street address or a long
|
|
9
|
+
* number: the text stops before the first one, and a request that starts with one gets the template
|
|
10
|
+
* without a paraphrase. A reply to a question, such as `yes`, `2`, `the first one` or a code, gets no
|
|
11
|
+
* instant reply at all, because it starts no search.
|
|
12
|
+
*/
|
|
13
|
+
/** Each template twice: with the paraphrase in place of `{x}`, and without one. */
|
|
14
|
+
const TEMPLATES = [
|
|
15
|
+
['on it, looking for {x}', 'on it, looking for that now'],
|
|
16
|
+
['give me a sec, checking for {x}', 'give me a sec, checking'],
|
|
17
|
+
['looking for {x} now', 'looking for that now'],
|
|
18
|
+
['got it, finding {x}', 'got it, one moment while i look'],
|
|
19
|
+
['searching for {x}, one moment', 'checking on that for you now'],
|
|
20
|
+
['checking what i can find for {x}', 'on it, give me a moment'],
|
|
21
|
+
];
|
|
22
|
+
/** Whole replies that answer a question the shopper was asked, after `please` and `thanks` come off. */
|
|
23
|
+
const ANSWERS = new RegExp('^(?:' +
|
|
24
|
+
[
|
|
25
|
+
'hi', 'hey', 'hello', 'yo', 'sup', 'lol', 'haha', 'hmm', 'y', 'ya', 'yes', 'yeah', 'yea', 'yep', 'yup', 'sure', 'ok', 'okay', 'k', 'kk', 'no', 'nope', 'nah',
|
|
26
|
+
'confirm(?:ed)?', 'approve[ds]?(?: it)?', 'i approved(?: it)?', 'done', 'added(?: it)?', 'i added(?: it| the card| a card)?',
|
|
27
|
+
'go', 'go ahead', 'do it', '(?:buy|order|place|book) it', 'sounds good', 'perfect', 'great', 'cool', 'nice',
|
|
28
|
+
'cancel', 'stop', 'never ?mind', 'nvm', 'wait', 'hold on', 'same', 'either', 'any', 'whatever',
|
|
29
|
+
'(?:the )?(?:first|second|third|fourth|fifth|last|other|\\d{1,2}(?:st|nd|rd|th)?)(?: one)?', '(?:that|this) one',
|
|
30
|
+
'(?:option|number|no|#) ?\\d{1,2}',
|
|
31
|
+
].join('|') +
|
|
32
|
+
')$');
|
|
33
|
+
/** Small talk that asks for nothing to be bought, after `please` and `thanks` come off. */
|
|
34
|
+
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)$/;
|
|
35
|
+
/** Unit words a number names a product by, such as `10000 mah`, so it is never read as a zip code. */
|
|
36
|
+
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)';
|
|
37
|
+
/**
|
|
38
|
+
* Details a shopper sends about themselves rather than a request: an email address, a street address,
|
|
39
|
+
* an apartment number, or words that hand over a name, a number or a place. Phone numbers, zip codes
|
|
40
|
+
* and card numbers are read by `hasPersonalNumber`.
|
|
41
|
+
*/
|
|
42
|
+
const PERSONAL = [
|
|
43
|
+
/\S+@\S+\.\S+/,
|
|
44
|
+
// A house number, a direction, then a street name that is no unit (`16 gb flash drive` is a product), then the street's kind.
|
|
45
|
+
new RegExp(`(?:^|[\\s,])\\d{2,6}\\s+(?:[nsew]\\.?\\s+)?(?!${UNIT}\\b)[a-z][a-z'.-]+(?:\\s+[a-z][a-z'.-]+){0,2}\\s+(?:st|street|ave|avenue|rd|road|blvd|boulevard|dr|drive|ln|lane|way|ct|court|pl|place|ter|terrace|hwy|highway|pkwy|parkway)\\b`),
|
|
46
|
+
/\b(?:apt|apartment|unit|suite)\s*#?\s*\d/,
|
|
47
|
+
/\b(?:my name is|my name'?s|name is|i'?m called|my (?:address|number|phone|email|zip)|address is|phone is|email is|zip is|deliver(?:ed)? (?:it )?to|ship(?:ped)? (?:it )?to|send (?:it )?to|bring (?:it )?to|call me|text me)\b/,
|
|
48
|
+
new RegExp(`(?<![\\w/.$-])\\d{5}(?:-\\d{4})?(?![\\w.,]\\d)(?!\\w)(?!\\s*${UNIT}\\b)`),
|
|
49
|
+
];
|
|
50
|
+
/** True when the text holds a phone number (10 to 15 digits, with or without separators) or a run of 12 digits or more, such as a card number. */
|
|
51
|
+
function hasPersonalNumber(line) {
|
|
52
|
+
if (/\d{12,}/.test(line))
|
|
53
|
+
return true;
|
|
54
|
+
for (const run of line.match(/\+?\d[\d\s().-]{6,}\d/g) ?? []) {
|
|
55
|
+
const digits = run.replace(/\D/g, '').length;
|
|
56
|
+
if (digits >= 10 && digits <= 15)
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
/** A code or a number on its own, such as `111111` or `4 2 1 9`. */
|
|
62
|
+
const CODE = /^[a-z]{0,3}\d[\d ]*$/;
|
|
63
|
+
/** Words that start a request and say nothing about what the shopper wants, taken off the front. */
|
|
64
|
+
const LEAD = /^(?:hey|hi|hello|yo|ok|okay|so|also|and|um|uh|actually|just|please|pls|can you|could you|would you|will you|can i (?:get|have)|could i (?:get|have)|i want to|i want|i need to|i need|i would like|i'?d like|id like|i'?m looking for|im looking for|looking for|get|find|order|buy|book|grab|reserve|search for|look for|look up|show|me|us)\b[\s,]*/;
|
|
65
|
+
/** A request that reads as a question or a change to the purchase: the template goes out without a paraphrase. */
|
|
66
|
+
const NO_PARAPHRASE = /^(?:what|where|when|why|how|who|which|is|are|was|were|do|does|did|can|could|will|would|should|has|have|change|make|swap|switch|add|remove|drop|cancel|update|use|try|send|deliver|ship|pay|charge|check|tell|give|put|set|move|keep|skip|replace|return|refund|track|reorder|pick|choose|select|go|let|help|see|compare)\b/;
|
|
67
|
+
/** Where the text stops: before the first link, contact detail, address, long number or delivery clause. */
|
|
68
|
+
const PRIVATE = [
|
|
69
|
+
/https?:\/\/|www\./,
|
|
70
|
+
/\S+@\S+/,
|
|
71
|
+
/[\w-]+\.[a-z]{2,}\b/,
|
|
72
|
+
/\+?\d[\d\s().-]{6,}\d/,
|
|
73
|
+
/\d{5,}/,
|
|
74
|
+
/(?:^|\s)\d{3,}\s+(?!(?:packs?|count|ct|pcs?|pieces?|oz|ml|l|g|kg|gb|tb|mah|w|watts?|inch(?:es)?|in|ft|feet|mm|cm|lbs?|dollars?|bucks|thread)\b)[a-z]/,
|
|
75
|
+
/\b\d+\s+(?:[a-z]+\s+){1,2}(?:st|street|ave|avenue|rd|road|blvd|boulevard|dr|drive|ln|lane|way|ct|court|pl|place|ter|terrace|hwy|highway)\b/,
|
|
76
|
+
/\b(?:deliver(?:ed)? (?:it )?to|ship(?:ped)? (?:it )?to|send (?:it )?to|bring (?:it )?to|to my (?:place|house|home|apartment|apt|office|work)|my (?:address|number|phone|email|name|card)|address is|call me|text me|named|called|apt|apartment|unit|suite)\b/,
|
|
77
|
+
];
|
|
78
|
+
/** Words a cut paraphrase never ends on. */
|
|
79
|
+
const TAIL = /\s+(?:for|with|and|or|but|the|a|an|to|of|in|at|on|from|under|over|below|above|near|by|who|that|which|my|some|around|about)$/;
|
|
80
|
+
const MAX_WORDS = 8;
|
|
81
|
+
/** The text as one plain line: lowercase, no quotes, and no dash between words. */
|
|
82
|
+
function plain(text) {
|
|
83
|
+
return text.toLowerCase().replace(/[‘’]/g, "'").replace(/["“”`]/g, '').replace(/\s+[-–—]+\s+|[–—]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* True when the shopper's words start a request, and false for an answer to a question, a pick, a
|
|
87
|
+
* code, small talk, or a message that carries the shopper's own details, such as a phone number or a
|
|
88
|
+
* street address.
|
|
89
|
+
*/
|
|
90
|
+
export function isNewRequest(text) {
|
|
91
|
+
const line = plain(String(text ?? ''));
|
|
92
|
+
const words = line.replace(/[!?.,;:]+/g, ' ').replace(/\b(?:please|pls|thanks|thank you|thx|ty)\b/g, ' ').replace(/\s+/g, ' ').trim();
|
|
93
|
+
if (!/[\p{L}\p{N}]/u.test(words))
|
|
94
|
+
return false;
|
|
95
|
+
if (hasPersonalNumber(line) || PERSONAL.some((pattern) => pattern.test(line)))
|
|
96
|
+
return false;
|
|
97
|
+
return !ANSWERS.test(words) && !CODE.test(words) && !CHITCHAT.test(words);
|
|
98
|
+
}
|
|
99
|
+
/** A short paraphrase of what the shopper asks for, safe to read back to them, or null when none is. */
|
|
100
|
+
export function paraphrase(text) {
|
|
101
|
+
let rest = plain(String(text ?? '')).split(/[.!?;\n](?:\s|$)/)[0] ?? '';
|
|
102
|
+
for (const pattern of PRIVATE) {
|
|
103
|
+
const at = rest.search(pattern);
|
|
104
|
+
if (at >= 0)
|
|
105
|
+
rest = rest.slice(0, at);
|
|
106
|
+
}
|
|
107
|
+
for (let before = ''; before !== rest;) {
|
|
108
|
+
before = rest;
|
|
109
|
+
rest = rest.replace(LEAD, '');
|
|
110
|
+
}
|
|
111
|
+
rest = rest.replace(/[\s,.:!?]+$/, '');
|
|
112
|
+
if (NO_PARAPHRASE.test(rest))
|
|
113
|
+
return null;
|
|
114
|
+
const words = rest.split(' ').filter(Boolean);
|
|
115
|
+
let short = words.slice(0, MAX_WORDS).join(' ');
|
|
116
|
+
while (TAIL.test(short))
|
|
117
|
+
short = short.replace(TAIL, '');
|
|
118
|
+
short = short.replace(/[\s,]+$/, '');
|
|
119
|
+
return /[a-z]{2}/.test(short) && isNewRequest(short) ? short : null;
|
|
120
|
+
}
|
|
121
|
+
/** The template at `index` for these words, with the paraphrase when one is safe. */
|
|
122
|
+
export function ackAt(index, text) {
|
|
123
|
+
const [withWhat, without] = TEMPLATES[index % TEMPLATES.length];
|
|
124
|
+
const what = paraphrase(text);
|
|
125
|
+
return what ? withWhat.replace('{x}', what) : without;
|
|
126
|
+
}
|
|
127
|
+
/** A template other than `avoid`, picked at random. */
|
|
128
|
+
export function nextTemplate(avoid, random = Math.random) {
|
|
129
|
+
const pick = Math.floor(random() * (TEMPLATES.length - 1));
|
|
130
|
+
return avoid < 0 || pick < avoid ? pick : pick + 1;
|
|
131
|
+
}
|
|
132
|
+
/** The template an earlier instant reply came from, or -1. */
|
|
133
|
+
function templateOf(reply) {
|
|
134
|
+
if (!reply)
|
|
135
|
+
return -1;
|
|
136
|
+
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
137
|
+
return TEMPLATES.findIndex(([withWhat, without]) => reply === without || new RegExp(`^${escape(withWhat).replace('\\{x\\}', '.+')}$`).test(reply));
|
|
138
|
+
}
|
|
139
|
+
/** The steps a shopper answers with their own details or a code, never with a new request. */
|
|
140
|
+
const ANSWERED_STEPS = new Set(['shipping_details', 'guest_details', 'verification_code']);
|
|
141
|
+
/**
|
|
142
|
+
* The instant reply for a shopper's message, such as `on it, looking for usb-c cables under $20`, to
|
|
143
|
+
* send the moment the message arrives and before your model runs; or null for a message that starts no
|
|
144
|
+
* search: a confirmation or a short answer such as `yes`, `1` or `ok`, a code, small talk such as `hi`,
|
|
145
|
+
* and the shopper's own details, such as a name, a phone number or an address. The words come from
|
|
146
|
+
* fixed lowercase templates, rotated, and never repeat the shopper's personal details.
|
|
147
|
+
*
|
|
148
|
+
* With the buy tool, call `buy.instantAck(text)` instead: the tool then sends no reply of its own
|
|
149
|
+
* through `onWorking` for that turn, and tells the model the shopper already heard one.
|
|
150
|
+
*
|
|
151
|
+
* ```ts
|
|
152
|
+
* instantAck('can you find me usb-c cables under $20'); // 'on it, looking for usb-c cables under $20', or another template
|
|
153
|
+
* instantAck('karen, 628 208 4916, 1801 wedemeyer st'); // null
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
export function instantAck(shopperText, options = {}) {
|
|
157
|
+
if (typeof options?.nextAction === 'string' && ANSWERED_STEPS.has(options.nextAction))
|
|
158
|
+
return null;
|
|
159
|
+
if (!isNewRequest(shopperText))
|
|
160
|
+
return null;
|
|
161
|
+
return ackAt(nextTemplate(templateOf(options?.previous)), shopperText);
|
|
162
|
+
}
|
|
163
|
+
/** The earlier name of `instantAck`, kept for apps that call it. */
|
|
164
|
+
export function ackFor(text, options = {}) {
|
|
165
|
+
return instantAck(text, options);
|
|
166
|
+
}
|
package/dist/adapters.d.ts
CHANGED
|
@@ -14,13 +14,16 @@ import type { BuyTool } from './buy-tool.js';
|
|
|
14
14
|
export type AnthropicBuyTool = BuyTool;
|
|
15
15
|
/** @deprecated Pass the buy tool itself: `toolRunner({ tools: [buy] })`. Returns the same object, for one release. */
|
|
16
16
|
export declare function toAnthropicTool(tool: BuyTool): BuyTool;
|
|
17
|
-
/** The
|
|
17
|
+
/** The zod builders the OpenRouter adapter calls; `z` from zod 3 or 4 has them. */
|
|
18
18
|
export interface ZodLike {
|
|
19
19
|
object(shape: Record<string, unknown>): {
|
|
20
20
|
strict(): unknown;
|
|
21
21
|
};
|
|
22
22
|
string(): {
|
|
23
23
|
describe(description: string): unknown;
|
|
24
|
+
nullable(): {
|
|
25
|
+
describe(description: string): unknown;
|
|
26
|
+
};
|
|
24
27
|
};
|
|
25
28
|
boolean(): {
|
|
26
29
|
describe(description: string): unknown;
|
package/dist/adapters.js
CHANGED
|
@@ -16,6 +16,7 @@ export function toOpenRouterTool(buy, toolkit) {
|
|
|
16
16
|
ask: z.string().describe(properties.ask.description),
|
|
17
17
|
confirm: z.boolean().describe(properties.confirm.description),
|
|
18
18
|
new_order: z.boolean().describe(properties.new_order.description),
|
|
19
|
+
product_id: z.string().nullable().describe(properties.product_id.description),
|
|
19
20
|
}).strict(),
|
|
20
21
|
// OpenRouter's SDK encodes what execute returns as JSON, so the view goes back as an object and reaches the model as the same text.
|
|
21
22
|
execute: async (input) => JSON.parse(await buy.execute(input)),
|
package/dist/buy-tool.d.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* A turn that outlived Agentcard's budget gets a read every 5 seconds for up
|
|
23
23
|
* to a minute, until the turn ends.
|
|
24
24
|
*/
|
|
25
|
-
import { AgentcardClient, type BuyCart, type BuyFlightItinerary, type BuyDeliveryAddress, type BuyNextAction, type FetchLike } from './client.js';
|
|
25
|
+
import { AgentcardClient, type BuyCart, type BuyFlightItinerary, type BuyDeliveryAddress, type BuyNextAction, type BuyPick, type BuyProgress, type FetchLike } from './client.js';
|
|
26
26
|
import { type BuyToolDefinition, type BuyToolInput, type BuyToolInputSchema, type BuyToolStandardSchema, type OpenAIBuyTool } from './definition.js';
|
|
27
27
|
import { type BuyToolModelView } from './model-view.js';
|
|
28
28
|
/**
|
|
@@ -40,7 +40,7 @@ export type BuyToolStatus = 'needs_input' | 'needs_card' | 'approval_sent' | 'pl
|
|
|
40
40
|
* card. `approval`: the page where they approve this purchase. `store_login`:
|
|
41
41
|
* the page where they sign in to the store named in `store`, before Agentcard
|
|
42
42
|
* can buy there. `product`: a product's page at its store, with its `title`,
|
|
43
|
-
* `price`, `productId` and `imageUrl`.
|
|
43
|
+
* `price`, `productId` and `imageUrl`. The page is `url`; `productId` holds no link.
|
|
44
44
|
*/
|
|
45
45
|
export interface BuyToolLink {
|
|
46
46
|
kind: 'vault' | 'approval' | 'store_login' | 'product';
|
|
@@ -53,7 +53,11 @@ export interface BuyToolLink {
|
|
|
53
53
|
title?: string;
|
|
54
54
|
/** On a product link: the price as the shopper reads it, such as `$12.99`, or null when Agentcard named none. */
|
|
55
55
|
price?: string | null;
|
|
56
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* On a product link: Agentcard's id for the product, with no link in it. It is the `product_id` the
|
|
58
|
+
* model reads for the same product, and the tool takes it as `product_id`, so a "Buy this" button
|
|
59
|
+
* can hand it back as the shopper's pick. Absent when Agentcard sent no such id, as an older API does.
|
|
60
|
+
*/
|
|
57
61
|
productId?: string;
|
|
58
62
|
/** On a product link: the product's picture, or null when Agentcard sent none. Show it beside the title. */
|
|
59
63
|
imageUrl?: string | null;
|
|
@@ -62,6 +66,8 @@ export interface BuyToolLink {
|
|
|
62
66
|
export interface BuyToolProduct {
|
|
63
67
|
flight?: BuyFlightItinerary;
|
|
64
68
|
id: string;
|
|
69
|
+
/** Agentcard's link-free id for the product, which the model passes as `product_id` when the shopper picks it; null from an older API. */
|
|
70
|
+
ref: string | null;
|
|
65
71
|
name: string;
|
|
66
72
|
/** The price in the minor unit of `currency`. */
|
|
67
73
|
priceCents: number | null;
|
|
@@ -102,12 +108,19 @@ export interface BuyToolResult {
|
|
|
102
108
|
approval_expires_at: string | null;
|
|
103
109
|
/** True when Agentcard tells the shopper how the order went, so the shopper need not reply. */
|
|
104
110
|
shopper_need_not_reply: boolean;
|
|
111
|
+
/**
|
|
112
|
+
* True when `onWorking` delivered this turn's instant reply, such as `got it, finding a taco`, so the
|
|
113
|
+
* shopper already heard that the request is being worked on. Absent or false when none went out.
|
|
114
|
+
*/
|
|
115
|
+
shopper_already_told_looking?: boolean;
|
|
105
116
|
conversation_id: string | null;
|
|
106
117
|
order_id: string | null;
|
|
107
118
|
decline_code: string | null;
|
|
108
119
|
error_code: string | null;
|
|
109
120
|
/** The one step the shopper takes next, as Agentcard named it, or null. Only that step's link is in `links`. */
|
|
110
121
|
next_action: BuyNextAction | null;
|
|
122
|
+
/** The product the model's `product_id` picked, by its catalog id, and whether the cart now holds it; null when the call picked none. */
|
|
123
|
+
pick: BuyPick | null;
|
|
111
124
|
/** Every cart hash in this answer. */
|
|
112
125
|
hashes: string[];
|
|
113
126
|
/** Agentcard's answer as it came: the purchase answer, or the body of the refusal. */
|
|
@@ -136,6 +149,44 @@ export interface BuyToolOptions {
|
|
|
136
149
|
fetch?: FetchLike;
|
|
137
150
|
/** The pause between two reads of the conversation. Default: a timer. */
|
|
138
151
|
wait?: (ms: number) => Promise<void>;
|
|
152
|
+
/**
|
|
153
|
+
* Sends the shopper an instant reply, such as `on it, looking for usb-c cables under $20`, while
|
|
154
|
+
* Agentcard works on a new request. It goes out when Agentcard has not answered within
|
|
155
|
+
* `ackDelayMs`, once for the calls the model makes in one turn, and never on a confirm, a pick or a
|
|
156
|
+
* reply such as `yes`, `2` or a code. The call waits for it before it returns, so the shopper reads
|
|
157
|
+
* it before any link or the model's answer. One that throws costs nothing but the instant reply.
|
|
158
|
+
*/
|
|
159
|
+
onWorking?: (message: string) => void | Promise<void>;
|
|
160
|
+
/** `false` sends no instant reply. A function writes it from the shopper's words instead, and an empty answer sends none. */
|
|
161
|
+
ack?: false | ((ask: string) => string | null | undefined);
|
|
162
|
+
/** How long Agentcard has to answer before the instant reply goes out. Default 1500. */
|
|
163
|
+
ackDelayMs?: number;
|
|
164
|
+
/**
|
|
165
|
+
* Called with the whole answer once per call, after its links went out and before the model reads
|
|
166
|
+
* it, so your app can show the carts, the status, `next_action` or the order id without wrapping the
|
|
167
|
+
* tool. A refusal the tool turns into an answer, such as a Vault link for a shopper with no card,
|
|
168
|
+
* counts; a call that throws calls nothing. One that throws costs nothing but the call to it.
|
|
169
|
+
*/
|
|
170
|
+
onResult?: (result: BuyToolResult) => void | Promise<void>;
|
|
171
|
+
/**
|
|
172
|
+
* Called while Agentcard works on a call, with the stage it reached: `searching`, `building_cart`,
|
|
173
|
+
* `checking_out` or `working`. Show it as a status line. One that throws costs nothing but the call to it.
|
|
174
|
+
*/
|
|
175
|
+
onProgress?: (progress: BuyProgress) => void;
|
|
176
|
+
/**
|
|
177
|
+
* `false` waits for each whole answer. With `onProgress`, or with `earlyCards` and `sendLink`, the
|
|
178
|
+
* tool asks Agentcard to stream the call, and each stage goes to `onProgress` as it happens.
|
|
179
|
+
*/
|
|
180
|
+
stream?: false;
|
|
181
|
+
/**
|
|
182
|
+
* `true` sends the store pages of a search's first three products to `sendLink` the moment the search
|
|
183
|
+
* answers, before Agentcard writes its reply, and the answer then sends only the pages it adds. Off by
|
|
184
|
+
* default: the reply often recommends other products than the search's first three, so a shopper
|
|
185
|
+
* would hold cards the reply never names. Measured on five live searches on 2026-10-20, the early
|
|
186
|
+
* pages arrived 2.2 to 3.3 seconds before the answer, and in four of them the reply named a product
|
|
187
|
+
* outside the first three.
|
|
188
|
+
*/
|
|
189
|
+
earlyCards?: boolean;
|
|
139
190
|
}
|
|
140
191
|
/**
|
|
141
192
|
* The buy tool, in the shape every agent framework takes, because their field
|
|
@@ -177,6 +228,15 @@ export interface BuyTool {
|
|
|
177
228
|
answer(input: BuyToolInput): Promise<BuyToolResult>;
|
|
178
229
|
/** The answer as the model may read it, with every link taken out. */
|
|
179
230
|
forModel(result: BuyToolResult): BuyToolModelView;
|
|
231
|
+
/**
|
|
232
|
+
* The instant reply for a shopper's message, such as `on it, looking for usb-c cables under $20`, or
|
|
233
|
+
* null for a message that starts no search: `yes`, `1`, `ok`, a code, `hi`, or the shopper's own
|
|
234
|
+
* details, such as a name, a phone number or an address, and any message that answers a request for
|
|
235
|
+
* shipping or guest details or a code. Call it the moment the message arrives, before your model runs,
|
|
236
|
+
* and send the text it returns. The tool then sends no reply of its own through `onWorking` for that
|
|
237
|
+
* turn, and the model reads `shopper_already_told_looking`, so the shopper never hears it twice.
|
|
238
|
+
*/
|
|
239
|
+
instantAck(shopperText: string): string | null;
|
|
180
240
|
}
|
|
181
241
|
/**
|
|
182
242
|
* A buy tool for one shopper. With a company client, name the shopper with
|