@agent-cards/commerce-sdk 0.8.0 → 0.10.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 +117 -8
- package/dist/ack.d.ts +44 -2
- package/dist/ack.js +219 -10
- package/dist/adapters.d.ts +3 -3
- package/dist/adapters.js +2 -1
- package/dist/buy-tool.d.ts +190 -13
- package/dist/buy-tool.js +544 -91
- package/dist/client.d.ts +71 -3
- package/dist/client.js +79 -1
- package/dist/definition.d.ts +14 -6
- package/dist/definition.js +31 -12
- package/dist/handler.d.ts +6 -0
- package/dist/handler.js +4 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/listen.d.ts +2 -0
- package/dist/listen.js +4 -1
- package/dist/messages.d.ts +33 -2
- package/dist/messages.js +82 -5
- package/dist/model-view.d.ts +48 -3
- package/dist/model-view.js +40 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,6 +39,8 @@ agentcard.listen({
|
|
|
39
39
|
});
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
Once `onMessage` returns, the client keeps the message for that shopper's next `buy.turn`, and `turn.messages(history)` hands it to your model before the shopper's words, so the model knows the shopper saved a card or got an order. "Tell the model what Agentcard told the shopper" shows it.
|
|
43
|
+
|
|
42
44
|
`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
45
|
|
|
44
46
|
`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.
|
|
@@ -74,7 +76,7 @@ const answer = await new Anthropic().beta.messages.toolRunner({
|
|
|
74
76
|
|
|
75
77
|
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.
|
|
76
78
|
|
|
77
|
-
The model sends `ask`, `confirm`, `new_order` and `
|
|
79
|
+
The model sends `ask`, `confirm`, `new_order`, `product_id` and `store_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.
|
|
78
80
|
|
|
79
81
|
## Use the tool in any framework
|
|
80
82
|
|
|
@@ -134,18 +136,113 @@ console.log(await result.getText());
|
|
|
134
136
|
|
|
135
137
|
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.
|
|
136
138
|
|
|
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.
|
|
139
|
+
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. Each product gets its own link, so the three dishes a restaurant's reply offers are three cards, though they share the store's one page. A flight has no page at a store, so each flight a search finds is a `product` link whose `url` is null, with the airline, the route and its times in `title`: show it as a card with no link to open, and hand its `productId` back as the shopper's pick. The model reads `product_links_sent` and never a URL. The tool's description tells it that the shopper already sees those products as cards, so its reply never lists them again by name or price: it writes one short line, such as which one they want, and 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
140
|
|
|
139
141
|
`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.
|
|
140
142
|
|
|
141
143
|
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.
|
|
142
144
|
|
|
145
|
+
## Answer each shopper message in its own turn
|
|
146
|
+
|
|
147
|
+
A shopper can send a second message before the first is answered. The `sendLink`, `onWorking` and `onResult` you pass to `buyTool` hear every call, whichever message the call answers, so a callback that hands each link to "the message running now" gives the first message's cards to the second. Open a turn for each message instead, and pass the turn to your model in place of the tool:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
const buy = buyTool({ agentcard, externalUserId: shopper.id }); // one per shopper, kept
|
|
151
|
+
|
|
152
|
+
async function onShopperMessage(message: string) {
|
|
153
|
+
const turn = buy.turn(message, { onWorking: (text) => textShopper(shopper.id, text) });
|
|
154
|
+
const runner = new Anthropic().beta.messages.toolRunner({
|
|
155
|
+
model: 'claude-opus-5-5',
|
|
156
|
+
max_tokens: 16000,
|
|
157
|
+
tools: [turn],
|
|
158
|
+
messages: turn.messages(history),
|
|
159
|
+
});
|
|
160
|
+
const answer = await runner;
|
|
161
|
+
history = runner.params.messages;
|
|
162
|
+
return { answer, links: turn.links, carts: turn.results.at(-1)?.carts ?? [] };
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`history` stands for the shopper's earlier messages. `turn.messages(history)` is the history, then what Agentcard told the shopper that the history doesn't hold yet, then `message`. Every call the model makes through `turn` belongs to that message alone:
|
|
167
|
+
|
|
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
|
+
- `turn.results` holds the whole answer of each call, and an `onResult` passed to `buy.turn` hears each one as it arrives.
|
|
170
|
+
- With `onWorking`, `buy.turn` sends the instant reply at once, before your model runs: the one `buy.instantAck` would return, or the one your `ack` function writes. You never call `instantAck` yourself. A slow call of the turn sends its reply to that `onWorking` too, and `turn.ack` holds the text the turn sent when it opened.
|
|
171
|
+
- An `onProgress` passed to `buy.turn` hears only that turn's stages.
|
|
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
|
+
|
|
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.
|
|
175
|
+
|
|
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
|
+
|
|
178
|
+
### Hand the app the turn in order
|
|
179
|
+
|
|
180
|
+
A turn reaches the shopper as messages and links that interleave: the instant reply, the cards a search found, then your model's words about them. Pass `onItem` to `buy.turn`, and the turn hands your app all of them as one sequence, one item at a time, in the order they happened. Show each item below the one before it:
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
const turn = buy.turn(message, {
|
|
184
|
+
onItem: (item) => (item.kind === 'text' ? sendText(shopper.id, item.text) : sendCard(shopper.id, item.link)),
|
|
185
|
+
onTyping: (typing) => showTyping(shopper.id, typing),
|
|
186
|
+
});
|
|
187
|
+
const runner = new Anthropic().beta.messages.toolRunner({ model: 'claude-opus-5-5', max_tokens: 16000, tools: [turn], messages: turn.messages(history), stream: true });
|
|
188
|
+
for await (const stream of runner) {
|
|
189
|
+
const paragraphs = turn.messageStream();
|
|
190
|
+
stream.on('text', (delta) => paragraphs.push(delta));
|
|
191
|
+
await stream.done();
|
|
192
|
+
await paragraphs.end();
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
- A `text` item is the instant reply, or a message your model wrote through `turn.messageStream()` or `turn.say(text)`. A `link` item is each link the tool hands over, with the fields `sendLink` would get.
|
|
197
|
+
- Messages arrive one at a time, like someone texting. Each one waits until `typingDelay` of it has passed since the item before it went out: 300 ms, plus 12 ms a character, at most 1.5 seconds. Time your model spent writing counts toward the pause, so a slow model gets no added wait, and links never wait. `onTyping` hears `true` when a pause starts and `false` when its message goes out. Pass `pace: false` to send each message the moment it is ready, or a function that returns the pause in milliseconds.
|
|
198
|
+
- With `onItem`, the turn sends no link to a `sendLink` and no instant reply to an `onWorking`, so a tool built with those for other turns sends nothing twice. Passing `onItem` beside `sendLink` or `onWorking` to the same turn throws.
|
|
199
|
+
- `turn.items` holds every item that went out, in order, so an app that answers the whole turn at once returns it as is. A turn without `onItem` keeps `turn.items` as well, unpaced.
|
|
200
|
+
|
|
201
|
+
## Tell the model what Agentcard told the shopper
|
|
202
|
+
|
|
203
|
+
Agentcard posts messages into the shopper's chat between turns, through `listen` or the webhook: `Your Visa ending 3132 is saved, so you're ready to check out.`, an order placed, an approval about to expire. The shopper reads them, so your model has to know them too. A model that never saw `card_added` answers "ok lets checkout" with "did you save your card?" and never calls the tool.
|
|
204
|
+
|
|
205
|
+
Build the listener and every buy tool on the same `AgentcardClient`. Then pass `turn.messages(history)` as the model's messages, and save what the run ends with as the next history:
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
const agentcard = new AgentcardClient({ clientId, clientSecret });
|
|
209
|
+
agentcard.listen({ onMessage: (shopperId, message) => postToChat(shopperId, message.text) });
|
|
210
|
+
|
|
211
|
+
const turn = buyTool({ agentcard, externalUserId: shopperId }).turn(message);
|
|
212
|
+
const runner = anthropic.beta.messages.toolRunner({ model: 'claude-opus-5-5', max_tokens: 16000, tools: [turn], messages: turn.messages(history) });
|
|
213
|
+
await runner;
|
|
214
|
+
history = runner.params.messages;
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
How it works:
|
|
218
|
+
|
|
219
|
+
- Once your `onMessage` returns, the client keeps the message for that shopper. A message whose `onMessage` threw every time never reached the chat, so the client does not keep it.
|
|
220
|
+
- `buy.turn` reads the messages kept for its shopper when it opens. `turn.agentcardMessages` holds them, oldest first.
|
|
221
|
+
- `turn.messages(history)` returns a new array: `history`, then Agentcard's `text` as the assistant's words, then the shopper's message as `{ role: 'user', content: message }`. The chat showed Agentcard's message in your agent's voice, so the model reads it as its own. Every link is taken out of the model's copy, as `forModel` takes them out of an answer, and the message `onMessage` got keeps them.
|
|
222
|
+
- A message stays kept until a history you pass to `turn.messages` holds it, and then the client forgets it. So when a turn fails and you keep the history you had, the next turn hands the message to the model again. Once you saved a history that holds it, no turn hands it over a second time.
|
|
223
|
+
- When the last message in `history` is the assistant's plain text, Agentcard's words join that message, so no provider reads two assistant messages in a row. That covers a string, or `text` parts as the Anthropic API, the Vercel AI SDK and OpenAI's chat completions write them. After anything else, such as a tool result, a tool call or an OpenAI Responses item, they come as an assistant message of their own. `history` itself is never changed.
|
|
224
|
+
- The Anthropic SDK's `messages`, the Vercel AI SDK's `messages`, OpenAI's chat completions `messages` and the Responses API's `input` all take what it returns.
|
|
225
|
+
|
|
226
|
+
With a webhook, pass the client to the handler: `agentcardWebhook({ secret, agentcard, onMessage })`. For a message you deliver some other way, call `agentcard.delivered(shopperId, message)` once it reaches the chat. The client keeps each message once per `eventId`, so a message both the listener and the webhook carry reaches the model once.
|
|
227
|
+
|
|
228
|
+
The messages live in the client's memory, at most 20 per shopper. So the listener, or the webhook, and the turn have to run in one process on one client. When your listener runs in one process and your model in another, such as a serverless chat route, keep each message where your chat history lives as `onMessage` delivers it, as an assistant message, and the model reads it from the history.
|
|
229
|
+
|
|
143
230
|
## Add the exact product the shopper picks
|
|
144
231
|
|
|
145
232
|
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
233
|
|
|
147
234
|
`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
235
|
|
|
236
|
+
## Show the stores the shopper picks from
|
|
237
|
+
|
|
238
|
+
A delivery order often starts with a choice of store: the shopper asks for a taco, and Agentcard answers with the taquerias near them that deliver. Each store the reply names reaches `sendLink` as a `store` link, up to three per answer, in the order the reply names them. The link carries the store's page at the merchant in `url`, its name in `title`, Agentcard's id for it in `storeId`, the delivery estimate in minutes in `etaMinutes`, or null when the store showed none, and the picture the store's card shows at the merchant in `imageUrl`, or null when it shows none. Show it as a card with the picture and an "Order here" button. `result.stores` carries the same stores for your code, each with its `url` and `linkSent`.
|
|
239
|
+
|
|
240
|
+
The model reads each store in `stores` by its `store_id`, name and `eta_minutes`, never its page, and `store_links_sent` says how many store pages the shopper holds. When the shopper picks a store, by its name, its place in the list or a tap on its card, the model passes that `store_id`, and Agentcard selects exactly that store before its agent answers, so the products it finds next are from that store. A card's button can call `buy.answer` with the link's `storeId` as `store_id`. A shopper who answers only with a place in the list, such as `the second one` or `2`, picks the store at that place in the last answer's list, whether or not the model passed the id. A store pick starts no search, so it gets no instant reply: a card tap that sends `Order from Tacos El Grullense`, the store's name alone or with `please`, and `order here` or `this place` all read as a pick of a store the last answer listed. A dish beside the store's name, as in `order tacos from el farolito`, is a search.
|
|
241
|
+
|
|
242
|
+
`result.store_pick` says what happened: the store's id and name, and `selected`, whether the merchant took the selection; when it did not, Agentcard's agent selects the store itself. The model reads `store_pick` by the `store_id` it sent. A store Agentcard no longer holds, because the stores are more than 15 minutes old or a newer search replaced them, selects nothing, and the model reads that it should search again with the shopper's words.
|
|
243
|
+
|
|
244
|
+
The tool sends each store page once for the calls the model makes in one turn. A later turn whose reply presents the stores again gets their pages again, so every store the reply names arrives with its card. A store link your `sendLink` throws on counts as not sent. Agentcard lists stores only for an answer whose reply presents them as a choice: a turn that searched stores on its way to a menu, or one that shows a cart, lists none.
|
|
245
|
+
|
|
149
246
|
## Tell the shopper you are on it
|
|
150
247
|
|
|
151
248
|
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`:
|
|
@@ -159,7 +256,7 @@ const buy = buyTool({
|
|
|
159
256
|
});
|
|
160
257
|
```
|
|
161
258
|
|
|
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
|
|
259
|
+
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.
|
|
163
260
|
|
|
164
261
|
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
262
|
|
|
@@ -172,9 +269,11 @@ const ack = buy.instantAck(message);
|
|
|
172
269
|
if (ack) await textShopper(shopper.id, ack);
|
|
173
270
|
```
|
|
174
271
|
|
|
175
|
-
|
|
272
|
+
With `buy.turn`, pass `onWorking` to the turn instead: it sends the same reply the moment the turn opens, and calls `instantAck` for you.
|
|
176
273
|
|
|
177
|
-
|
|
274
|
+
`instantAck` returns 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 sends `onWorking` a 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
|
+
|
|
276
|
+
Without a buy tool, `instantAck(message, { previous, nextAction, asked, stores })` from the package root decides the same way: `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: false` to the tool when you send it yourself. `ackFor` is the earlier name of the same function.
|
|
178
277
|
|
|
179
278
|
## Show what Agentcard is doing
|
|
180
279
|
|
|
@@ -203,7 +302,7 @@ for (const text of toMessages(reply)) await textShopper(shopper.id, text);
|
|
|
203
302
|
|
|
204
303
|
`result.messages` from `buy.answer` holds Agentcard's own answer cut the same way, links included, so your code sends it without cutting it again.
|
|
205
304
|
|
|
206
|
-
For a reply your model streams, `messageStream` hands over each paragraph
|
|
305
|
+
For a reply your model streams, `messageStream` hands over each paragraph once the blank line that ends it arrives, however the stream cuts the text. Push each piece of text, then call `end` for the last paragraph:
|
|
207
306
|
|
|
208
307
|
```ts
|
|
209
308
|
import { messageStream } from '@agent-cards/commerce-sdk';
|
|
@@ -214,7 +313,9 @@ await stream.done();
|
|
|
214
313
|
await paragraphs.end();
|
|
215
314
|
```
|
|
216
315
|
|
|
217
|
-
`stream` stands for your model's streamed answer. When `onMessage` returns a promise, the stream waits for it before it sends the next paragraph, so the shopper reads them in order, and `end` resolves once every paragraph went out. After a send fails, no later paragraph goes out, and `end` rejects with that error. A paragraph inside a list waits for the first characters of the next line, which say whether the list goes on. Start a new `messageStream` for each model message, so one message never runs into the next.
|
|
316
|
+
`stream` stands for your model's streamed answer. `toMessages` and `messageStream` keep nothing between two texts, so a shopper's overlapping messages never mix in them: the paragraphs of one model message go to the `onMessage` of its own stream, and the links of that turn's tool calls are in its `turn.links`. When `onMessage` returns a promise, the stream waits for it before it sends the next paragraph, so the shopper reads them in order, and `end` resolves once every paragraph went out. After a send fails, no later paragraph goes out, and `end` rejects with that error. A paragraph inside a list waits for the first characters of the next line, which say whether the list goes on. Start a new `messageStream` for each model message, so one message never runs into the next.
|
|
317
|
+
|
|
318
|
+
A model often writes three paragraphs within 200 ms, and a shopper who gets them at once reads a wall, not a conversation. So `messageStream` spaces them like someone texting: the first goes out at once, and each one after it waits until `typingDelay` of it has passed since the one before, at most 1.5 seconds. Time the model spent writing counts toward the pause, so a slow model gets no added wait. Pass `onTyping` to show a typing indicator during each pause, `pace: false` to hand each paragraph over the moment it is cut, or `pace: (text) => ms` for your own pause. With `buy.turn`'s `onItem`, use `turn.messageStream()` instead, which spaces the model's messages after the cards and the instant reply too.
|
|
218
319
|
|
|
219
320
|
## Sign up a new shopper
|
|
220
321
|
|
|
@@ -251,7 +352,11 @@ The model reads one object per call:
|
|
|
251
352
|
"shopper_need_not_reply": false,
|
|
252
353
|
"shopper_already_told_looking": false,
|
|
253
354
|
"link_sent": false,
|
|
254
|
-
"product_links_sent": 0
|
|
355
|
+
"product_links_sent": 0,
|
|
356
|
+
"pick": null,
|
|
357
|
+
"stores": [],
|
|
358
|
+
"store_links_sent": 0,
|
|
359
|
+
"store_pick": null
|
|
255
360
|
}
|
|
256
361
|
```
|
|
257
362
|
|
|
@@ -295,6 +400,8 @@ const buy = buyTool({
|
|
|
295
400
|
});
|
|
296
401
|
```
|
|
297
402
|
|
|
403
|
+
This `onResult` hears every call the tool makes. To show each message's carts beside that message, pass `onResult` to `buy.turn`, or read `turn.results` once the turn ends.
|
|
404
|
+
|
|
298
405
|
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
406
|
|
|
300
407
|
## Buy with a shopper's own token
|
|
@@ -356,6 +463,8 @@ const webhook = agentcardWebhook({ secret: process.env.AGENTCARD_WEBHOOK_SECRET
|
|
|
356
463
|
export const POST = webhook.fetch;
|
|
357
464
|
```
|
|
358
465
|
|
|
466
|
+
Pass `agentcard`, the client your buy tools share, and the handler keeps each message it delivered for that shopper's next turn, as `listen` does: `agentcardWebhook({ secret, agentcard, onMessage })`. "Tell the model what Agentcard told the shopper" shows how the model reads it.
|
|
467
|
+
|
|
359
468
|
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
469
|
|
|
361
470
|
| Event | `kind` | When it arrives | Who wrote `text` |
|
package/dist/ack.d.ts
CHANGED
|
@@ -7,9 +7,22 @@
|
|
|
7
7
|
*
|
|
8
8
|
* The paraphrase never carries a link, an email address, a phone number, a street address or a long
|
|
9
9
|
* number: the text stops before the first one, and a request that starts with one gets the template
|
|
10
|
-
* without a paraphrase.
|
|
11
|
-
*
|
|
10
|
+
* without a paraphrase. It never reads back the shopper's own first-person words, such as `i'll take` or
|
|
11
|
+
* `my mom`, and it never cuts a phrase short: a subject that runs long or carries brackets gets the
|
|
12
|
+
* template without one. A reply to a question, such as `yes`, `2` or a code, and a pick of a product the
|
|
13
|
+
* shopper already saw, such as `the first one`, `I'll take the KODAK AA ($13.49)` or `#2`, gets no
|
|
14
|
+
* instant reply at all, because it starts no search and Agentcard answers it fast. So does a pick of a
|
|
15
|
+
* store the shopper saw, such as `order here`, or `Order from Tacos El Grullense` as a store card sends it.
|
|
12
16
|
*/
|
|
17
|
+
/**
|
|
18
|
+
* True when the message names one of these stores and nothing else to buy: the store's whole name, or
|
|
19
|
+
* two words of it in a row, such as `el grullense` for `Tacos El Grullense`, alone or after a frame such
|
|
20
|
+
* as `order from` or `let's go with`. One word of a name, such as `tacos`, and a dish beside the name,
|
|
21
|
+
* such as `order tacos from el farolito`, name no store.
|
|
22
|
+
*/
|
|
23
|
+
export declare function namesStore(text: string, storeNames: readonly string[] | null | undefined): boolean;
|
|
24
|
+
/** True when the shopper's words pick a product they already saw, such as `the first one`, `2` or a card's title with its price. */
|
|
25
|
+
export declare function isPickText(text: string): boolean;
|
|
13
26
|
/**
|
|
14
27
|
* True when the shopper's words start a request, and false for an answer to a question, a pick, a
|
|
15
28
|
* code, small talk, or a message that carries the shopper's own details, such as a phone number or a
|
|
@@ -20,6 +33,15 @@ export declare function isNewRequest(text: string): boolean;
|
|
|
20
33
|
export declare function paraphrase(text: string): string | null;
|
|
21
34
|
/** The template at `index` for these words, with the paraphrase when one is safe. */
|
|
22
35
|
export declare function ackAt(index: number, text: string): string;
|
|
36
|
+
/** The reply at `index` for a message that is no search: it says Agentcard is working, and never that it is looking for anything. */
|
|
37
|
+
export declare function waitAt(index: number): string;
|
|
38
|
+
/** What a slow call that is not a new request is doing: adding a product the shopper picked, choosing a store, or going through with a cart. */
|
|
39
|
+
export type SlowStep = 'product' | 'pick' | 'confirm';
|
|
40
|
+
/**
|
|
41
|
+
* The reply a slow pick or confirm sends once Agentcard takes longer than the delay: fixed words that
|
|
42
|
+
* name nothing the shopper said and never say `looking for`, because the shopper already chose.
|
|
43
|
+
*/
|
|
44
|
+
export declare function slowStepLine(step: SlowStep): string;
|
|
23
45
|
/** A template other than `avoid`, picked at random. */
|
|
24
46
|
export declare function nextTemplate(avoid: number, random?: () => number): number;
|
|
25
47
|
export interface AckForOptions {
|
|
@@ -30,7 +52,27 @@ export interface AckForOptions {
|
|
|
30
52
|
* shipping or guest details or a code, the next message answers it, so it gets no instant reply.
|
|
31
53
|
*/
|
|
32
54
|
nextAction?: string | null;
|
|
55
|
+
/**
|
|
56
|
+
* The last message the shopper read from you or from Agentcard. When it asks for an address, a name,
|
|
57
|
+
* a phone number, an email or a code, the next message answers it, so it gets no instant reply. When
|
|
58
|
+
* it asks any other question, such as `what size do you want?`, a short reply that asks for nothing
|
|
59
|
+
* new, such as `im a 37`, `medium`, `2`, `black` or `carne asada`, answers it and gets none either.
|
|
60
|
+
*/
|
|
61
|
+
asked?: string | null;
|
|
62
|
+
/**
|
|
63
|
+
* The names of the stores the last purchase answer showed the shopper, its `stores`. A message that
|
|
64
|
+
* names one of them and nothing else, such as `Order from Tacos El Grullense` as a store card sends
|
|
65
|
+
* it, picks that store, so it gets no instant reply.
|
|
66
|
+
*/
|
|
67
|
+
stores?: readonly string[] | null;
|
|
33
68
|
}
|
|
69
|
+
/** True when a message asks the shopper for their own details or a code, in a question or a request such as `send me your address`. */
|
|
70
|
+
export declare function asksForDetails(message: string | null | undefined): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* True when the shopper's words answer the question the last message asked, such as `what size do you
|
|
73
|
+
* want?` or `which meat?`: the message asked a question, and the reply is short and opens no new request.
|
|
74
|
+
*/
|
|
75
|
+
export declare function answersQuestion(text: string, asked: string | null | undefined): boolean;
|
|
34
76
|
/**
|
|
35
77
|
* The instant reply for a shopper's message, such as `on it, looking for usb-c cables under $20`, to
|
|
36
78
|
* send the moment the message arrives and before your model runs; or null for a message that starts no
|