@agent-cards/commerce-sdk 0.18.5 → 0.18.6
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 +34 -3
- package/dist/action-links.js +1 -0
- package/dist/app-instructions.d.ts +17 -0
- package/dist/app-instructions.generated.d.ts +9 -0
- package/dist/app-instructions.generated.js +10 -0
- package/dist/app-instructions.js +22 -0
- package/dist/buy-tool.d.ts +14 -2
- package/dist/buy-tool.js +76 -43
- package/dist/client.d.ts +37 -0
- package/dist/client.js +16 -0
- package/dist/definition.js +2 -1
- package/dist/direct.d.ts +8 -0
- package/dist/direct.js +13 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -1
- package/dist/message.d.ts +2 -1
- package/dist/model-view.d.ts +29 -4
- package/dist/model-view.js +93 -13
- package/dist/plan.d.ts +41 -0
- package/dist/plan.js +34 -0
- package/dist/store-wait.d.ts +27 -0
- package/dist/store-wait.js +41 -0
- package/dist/trip-ask.d.ts +1 -1
- package/dist/trip-ask.js +4 -4
- package/examples/anthropic.mjs +3 -2
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ Build one client with your company's credentials and one buy tool per shopper, t
|
|
|
51
51
|
|
|
52
52
|
```ts
|
|
53
53
|
import Anthropic from '@anthropic-ai/sdk';
|
|
54
|
-
import { AgentcardClient, buyTool } from '@agent-cards/commerce-sdk';
|
|
54
|
+
import { AgentcardClient, appInstructions, buyTool } from '@agent-cards/commerce-sdk';
|
|
55
55
|
|
|
56
56
|
const agentcard = new AgentcardClient({
|
|
57
57
|
clientId: process.env.AGENTCARD_CLIENT_ID ?? '',
|
|
@@ -67,6 +67,7 @@ const buy = buyTool({
|
|
|
67
67
|
const answer = await new Anthropic().beta.messages.toolRunner({
|
|
68
68
|
model: 'claude-opus-5-5',
|
|
69
69
|
max_tokens: 16000,
|
|
70
|
+
system: appInstructions({ markdown: true }),
|
|
70
71
|
tools: [buy],
|
|
71
72
|
messages: [{ role: 'user', content: 'two packs of AA batteries from Amazon' }],
|
|
72
73
|
});
|
|
@@ -78,6 +79,16 @@ Build the client once and keep it. Every shopper's tool shares the client's acce
|
|
|
78
79
|
|
|
79
80
|
The model sends `ask`, `confirm`, `new_order`, `product_id` and `store_id`, and nothing else. A product link the shopper sends, such as a `buy.trychannel3.com` link, goes in `ask` exactly as sent, and Agentcard buys that exact product from it. 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.
|
|
80
81
|
|
|
82
|
+
## Give your model Agentcard's instructions
|
|
83
|
+
|
|
84
|
+
`appInstructions()` returns the system prompt Agentcard recommends for your model: what Agentcard can do for the shopper, in the words Agentcard's own buy agent uses, how to hand a request to the buy tool, and how to write for your app. Put it first in your system prompt, and add only what is about your app, such as its name:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
const system = [appInstructions({ markdown: true, textingStyle: true }), 'You are the shopping chat in Acme.'].join('\n\n');
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Pass `markdown: true` when your app shows the model's words as Markdown, so a shopper who asks for a table gets one; leave it out and the model writes plain text, with lists as `• ` lines. Pass `textingStyle: true` when your app is a chat that writes in lowercase, as you pass it to `buyTool`. The text ships inside the package and changes with each release; `APP_INSTRUCTIONS_VERSION` names the version you run.
|
|
91
|
+
|
|
81
92
|
## Use the tool in any framework
|
|
82
93
|
|
|
83
94
|
Pass `buy` as it is. Each framework reads its own fields from the same object, so you pick no adapter:
|
|
@@ -152,7 +163,7 @@ Each step ends with a shopper message: `actionLinkOutcome(link, message)` says h
|
|
|
152
163
|
| --- | --- | --- | --- | --- |
|
|
153
164
|
| `vault` | `card_added` | | | `card_link_expired` |
|
|
154
165
|
| `store_login` | `store_connected` | | `store_connect_failed` | `store_connect_expired` |
|
|
155
|
-
| `approval` | `order_placed`, `order_not_placed_yet`, `order_unconfirmed`, `order_confirmed`, `order_failed`, `payment_declined` | `approval_declined` | | `approval_expired` |
|
|
166
|
+
| `approval` | `order_placing`, `order_placed`, `order_not_placed_yet`, `order_unconfirmed`, `order_confirmed`, `order_failed`, `payment_declined` | `approval_declined` | | `approval_expired` |
|
|
156
167
|
|
|
157
168
|
A reminder, such as `card_link_unfinished` or `approval_expiring`, leaves the step open. An approval is `completed` once the shopper approved, even when the order then failed or the payment was refused: the approval page has nothing more to do, and the message tells the shopper what happened. A message whose facts name another link of the same kind (`facts.approvalUrl`, `facts.cardLinkUrl`) leaves this one open, and so does one about another store: a store login link names its store in `store`, and so does an approval link when the answer names the cart's store, so DoorDash's `store_connected` (`facts.merchantName: 'DoorDash'`) never ends a Locale sign-in the shopper has open beside it.
|
|
158
169
|
|
|
@@ -338,6 +349,24 @@ const buy = buyTool({ agentcard, externalUserId: shopper.id, threadId: chat.id }
|
|
|
338
349
|
|
|
339
350
|
A message about one chat's purchase carries that chat as `threadId`, so post it into that chat. One about the shopper alone, such as a saved card, carries `threadId: null`. A chat's tool takes a next step only for its own chat's messages and the shopper's own, and a tool built without `threadId` only for purchases opened under no thread and the shopper's own.
|
|
340
351
|
|
|
352
|
+
## Choose how long a reply waits for stores
|
|
353
|
+
|
|
354
|
+
A search that asks several stores writes its reply once the stores answered or a wait ran out. Choose that wait with `storeWaitMs`, and choose with `lateResults` what happens to a store that answers after the reply:
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
// A streaming chat: cards show as each store answers, the reply waits up to 25 s (99.7% of stores' first products), and a later store's cards stream below it.
|
|
358
|
+
const buy = buyTool({ agentcard, externalUserId: shopper.id, storeWaitMs: 25000, lateResults: 'stream' });
|
|
359
|
+
// A texting app: the reply waits up to 20 s (96% of stores' first products) and is the turn's last word.
|
|
360
|
+
const sms = buyTool({ agentcard, externalUserId: shopper.id, storeWaitMs: 20000, lateResults: 'hold' });
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Agentcard ends every store search at 30 s, so a `storeWaitMs` above `30000` waits no longer than `30000` does.
|
|
364
|
+
|
|
365
|
+
- `lateResults: 'stream'` sends a late store's cards after the reply, in the same turn, the way a slower hotel supplier's cards come: to `sendLink`, `onProducts` and `onItem`, with the one message that says what they added. The turn's `onResult` lists them in `result.products` after the answer's own.
|
|
366
|
+
- `lateResults: 'hold'` keeps a late store's cards out of the turn, so a texting app never sends a card after the reply.
|
|
367
|
+
- `buy.turn(message, { storeWaitMs, lateResults })` sets them for one turn, and each one left out keeps the tool's. An app that calls `client.buy` itself sends `store_wait_ms` and `late_results`.
|
|
368
|
+
- Left out, the tool sends neither, and Agentcard keeps its own wait. A wait that is not a whole number of milliseconds, 0 or more, throws a TypeError before any call.
|
|
369
|
+
|
|
341
370
|
## Tell the model what Agentcard told the shopper
|
|
342
371
|
|
|
343
372
|
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.
|
|
@@ -527,7 +556,7 @@ const buy = buyTool({
|
|
|
527
556
|
|
|
528
557
|
When your app shows Agentcard's messages through `onText`, the model reads `shopper_already_read_reply: true` in that call's answer and adds nothing to them, as it reads `shopper_already_told_looking` after an instant reply: end the turn without a second model call, as with `replyAsWritten`, or let the model add one short line for what the reply left out. `turn.replyAsWritten` stays null for that answer, so the reply never reaches the shopper twice, and a step link the turn holds for your model's next message goes out right after Agentcard's ask instead.
|
|
529
558
|
|
|
530
|
-
A turn built with `replyAsWritten` and `earlyCards` that hears messages through `onMessage` or `onItem` and passes no `onText` gets the same once a reply is written while a slower source's cards are still coming, such as a second hotel supplier's: Agentcard's messages go out through that callback the moment Agentcard says the reply is written, the slower cards and their one message follow in the same call, and the answer reads `shopper_already_read_reply: true`
|
|
559
|
+
A turn built with `replyAsWritten` and `earlyCards` that hears messages through `onMessage` or `onItem` and passes no `onText` gets the same once a reply is written while a slower source's cards are still coming, such as a second hotel supplier's: Agentcard's messages go out through that callback the moment Agentcard says the reply is written, the slower cards and their one message follow in the same call, and the answer reads `shopper_already_read_reply: true`. `turn.replyAsWritten` then holds the messages the shopper read, and `turn.say` of it sends nothing, so an app that shows the reply as written and skips its model shows no second reply.
|
|
531
560
|
|
|
532
561
|
A stream that breaks after the call started reads the conversation for what the turn left, as a turn past Agentcard's budget does. Pass `stream: false` to wait for each whole answer, with its cards sent beside it.
|
|
533
562
|
|
|
@@ -719,6 +748,7 @@ Subscribe your endpoint to each event below by name. A catch-all `*` alone does
|
|
|
719
748
|
| `purchase.message` | `order_failed` | The order failed after the shopper approved. | Agentcard |
|
|
720
749
|
| `purchase.message` | `order_not_placed_yet` | The shopper approved, and the order is not placed yet. | Agentcard |
|
|
721
750
|
| `purchase.message` | `order_unconfirmed` | The store's reply to the order was lost, so the order may exist, and Agentcard is confirming it. | Agentcard |
|
|
751
|
+
| `purchase.message` | `order_placing` | The shopper approved, and Agentcard is placing the order now. One of the order kinds above follows. | Agentcard |
|
|
722
752
|
| `purchase.message` | `approval_expiring` | The approval link expires soon. `text` carries the link. | Agentcard |
|
|
723
753
|
| `purchase.message` | `approval_expired` | The approval expired before the shopper answered, and no charge went through. | Agentcard |
|
|
724
754
|
| `purchase.message` | `approval_declined` | The shopper declined the approval, and no charge went through. | Agentcard |
|
|
@@ -848,6 +878,7 @@ The facts are `merchantName`, `totalCents`, `currency`, `cardBrand`, `cardLast4`
|
|
|
848
878
|
| `card_link_expired` | The card link expired before the shopper added a card. |
|
|
849
879
|
| `cart_idle` | A cart sat, and nobody sent it for approval. |
|
|
850
880
|
| `order_unconfirmed` | The store's reply to the order was lost, and Agentcard is confirming it. |
|
|
881
|
+
| `order_placing` | The shopper approved, and Agentcard is placing the order now. The order's own message follows. |
|
|
851
882
|
| `card_added` | The shopper saved a card from the card link your app sent. |
|
|
852
883
|
| `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. |
|
|
853
884
|
| `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. |
|
package/dist/action-links.js
CHANGED
|
@@ -14,6 +14,7 @@ export const ACTION_LINK_ENDINGS = {
|
|
|
14
14
|
vault: { card_added: 'completed', card_link_expired: 'expired' },
|
|
15
15
|
store_login: { store_connected: 'completed', store_connect_failed: 'failed', store_connect_expired: 'expired' },
|
|
16
16
|
approval: {
|
|
17
|
+
order_placing: 'completed',
|
|
17
18
|
order_placed: 'completed',
|
|
18
19
|
order_not_placed_yet: 'completed',
|
|
19
20
|
order_unconfirmed: 'completed',
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** The version of the text `appInstructions` returns. It changes whenever the text does. */
|
|
2
|
+
export declare const APP_INSTRUCTIONS_VERSION: string;
|
|
3
|
+
export interface AppInstructionsOptions {
|
|
4
|
+
/** `true` when your app shows the model's words as Markdown: tables, lists and bold. Default `false`, plain text. */
|
|
5
|
+
markdown?: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* `true` when your app is a chat that writes like a text message: all lowercase except names. Pass the same value
|
|
8
|
+
* you pass `buyTool` as `textingStyle`. Default `false`.
|
|
9
|
+
*/
|
|
10
|
+
textingStyle?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The lines to put in your model's system prompt, before your own. Add only what is about your app, such as its name
|
|
14
|
+
* or what a shopper sees on its screen; what Agentcard can do and how to use the buy tool come from here, so they
|
|
15
|
+
* change with each SDK release.
|
|
16
|
+
*/
|
|
17
|
+
export declare function appInstructions(options?: AppInstructionsOptions): string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** The version of the voice package the text was copied from. */
|
|
2
|
+
export declare const VOICE_VERSION = "0.2.79";
|
|
3
|
+
export declare const APP_TEXT: {
|
|
4
|
+
readonly core: "You help a shopper buy what they ask for, with the `buy` tool from Agentcard.\n\nWhen the shopper asks what you can do, answer from these lines without calling the tool:\n- You buy from Amazon, Walmart, Target, Best Buy and thousands more stores. Say \"and thousands more stores\" every time you name the stores you shop, in a reply about one kind of thing too, such as \"travel gear from Amazon, Target and thousands more stores\". Never let a few names read as the whole list, never say \"most online stores\", \"plenty more\" or \"many more\", and never give a number of stores.\n- You plan a whole trip: the flights, a hotel and a rental car together. When they ask what you can do for travel, lead with the whole trip, then name the parts they can book on their own.\n- You order food delivery and book restaurant tables.\n\nHow you use the buy tool:\n- Call it for anything the shopper wants to find, buy, book or order, a trip with no dates or airport yet included: the tool asks for what it still needs.\n- Never say whether you can buy at a store the shopper names, either way. Only the tool knows, so say you will look and call it.\n- Never write a link. Each link reaches the shopper on its own, so when `link_sent` is true, tell them a link is waiting for them.\n- Tell the shopper no reply is needed only when `shopper_need_not_reply` is true.\n- Ask for one thing per message, the step `next_action` names, and leave every later step for a later message.\n- Never tell the shopper you can't show Markdown, a table or other formatting, and never say you keep to plain text.";
|
|
5
|
+
readonly markdown: "The app shows your words as Markdown. When the shopper asks for a table, a list or bold, write it in Markdown. Use a list or a table where it reads better than a sentence, and keep each message short all the same.";
|
|
6
|
+
readonly plainText: "The app shows your words as plain text, so write no Markdown: asterisks, underscores, pound signs, backticks and table pipes show up as written. Write a list as lines that start with \"• \", and a table the shopper asks for as one line per row.";
|
|
7
|
+
readonly texting: "Write like a text message: all lowercase except names, short and plain, with no dashes.";
|
|
8
|
+
readonly standard: "Write short, plain sentences, the way a person writes in a chat.";
|
|
9
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// GENERATED by scripts/app-instructions.mjs from apps/agent-voice/kb/app.md. Do not edit: edit kb/app.md.
|
|
2
|
+
/** The version of the voice package the text was copied from. */
|
|
3
|
+
export const VOICE_VERSION = "0.2.79";
|
|
4
|
+
export const APP_TEXT = {
|
|
5
|
+
"core": "You help a shopper buy what they ask for, with the `buy` tool from Agentcard.\n\nWhen the shopper asks what you can do, answer from these lines without calling the tool:\n- You buy from Amazon, Walmart, Target, Best Buy and thousands more stores. Say \"and thousands more stores\" every time you name the stores you shop, in a reply about one kind of thing too, such as \"travel gear from Amazon, Target and thousands more stores\". Never let a few names read as the whole list, never say \"most online stores\", \"plenty more\" or \"many more\", and never give a number of stores.\n- You plan a whole trip: the flights, a hotel and a rental car together. When they ask what you can do for travel, lead with the whole trip, then name the parts they can book on their own.\n- You order food delivery and book restaurant tables.\n\nHow you use the buy tool:\n- Call it for anything the shopper wants to find, buy, book or order, a trip with no dates or airport yet included: the tool asks for what it still needs.\n- Never say whether you can buy at a store the shopper names, either way. Only the tool knows, so say you will look and call it.\n- Never write a link. Each link reaches the shopper on its own, so when `link_sent` is true, tell them a link is waiting for them.\n- Tell the shopper no reply is needed only when `shopper_need_not_reply` is true.\n- Ask for one thing per message, the step `next_action` names, and leave every later step for a later message.\n- Never tell the shopper you can't show Markdown, a table or other formatting, and never say you keep to plain text.",
|
|
6
|
+
"markdown": "The app shows your words as Markdown. When the shopper asks for a table, a list or bold, write it in Markdown. Use a list or a table where it reads better than a sentence, and keep each message short all the same.",
|
|
7
|
+
"plainText": "The app shows your words as plain text, so write no Markdown: asterisks, underscores, pound signs, backticks and table pipes show up as written. Write a list as lines that start with \"• \", and a table the shopper asks for as one line per row.",
|
|
8
|
+
"texting": "Write like a text message: all lowercase except names, short and plain, with no dashes.",
|
|
9
|
+
"standard": "Write short, plain sentences, the way a person writes in a chat."
|
|
10
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The system prompt Agentcard recommends for your own model, the one that holds the buy tool and talks to the
|
|
3
|
+
* shopper. The buy tool's description says how to call it; these lines say who the model is for the shopper: what
|
|
4
|
+
* Agentcard can do, in the same words Agentcard's buy agent uses, how the model hands a request to the tool, and
|
|
5
|
+
* how its words look in your app. The text is copied from the voice package (apps/agent-voice/kb/app.md) when the
|
|
6
|
+
* SDK is built, so it ships inside the package and costs no request.
|
|
7
|
+
*/
|
|
8
|
+
import { APP_TEXT, VOICE_VERSION } from './app-instructions.generated.js';
|
|
9
|
+
/** The version of the text `appInstructions` returns. It changes whenever the text does. */
|
|
10
|
+
export const APP_INSTRUCTIONS_VERSION = VOICE_VERSION;
|
|
11
|
+
/**
|
|
12
|
+
* The lines to put in your model's system prompt, before your own. Add only what is about your app, such as its name
|
|
13
|
+
* or what a shopper sees on its screen; what Agentcard can do and how to use the buy tool come from here, so they
|
|
14
|
+
* change with each SDK release.
|
|
15
|
+
*/
|
|
16
|
+
export function appInstructions(options = {}) {
|
|
17
|
+
return [
|
|
18
|
+
APP_TEXT.core,
|
|
19
|
+
options.markdown ? APP_TEXT.markdown : APP_TEXT.plainText,
|
|
20
|
+
options.textingStyle ? APP_TEXT.texting : APP_TEXT.standard,
|
|
21
|
+
].join('\n\n');
|
|
22
|
+
}
|
package/dist/buy-tool.d.ts
CHANGED
|
@@ -31,12 +31,14 @@
|
|
|
31
31
|
* messages and links as one sequence, in the order they happened, and spaces
|
|
32
32
|
* the messages like someone texting.
|
|
33
33
|
*/
|
|
34
|
+
import type { BuyPlan } from './plan.js';
|
|
34
35
|
import { AgentcardClient, type BuyCart, type BuyCartBuilding, type BuyChoice, type BuyFlightItinerary, type BuyDeliveryAddress, type BuyNextAction, type BuyPick, type BuyProgress, type BuyTurnStatus, type BuyAvailability, type BuyStorePick, type BuyTurn, type FetchLike } from './client.js';
|
|
35
36
|
import { type BuyToolDefinition, type BuyToolInput, type BuyToolInputSchema, type BuyToolStandardSchema, type OpenAIBuyTool } from './definition.js';
|
|
36
37
|
import { type DirectCall } from './direct.js';
|
|
37
38
|
import { type BuyToolModelView } from './model-view.js';
|
|
38
39
|
import { type MessagePace, type MessageStream } from './messages.js';
|
|
39
40
|
import { type ShopperMessage } from './shopper-message.js';
|
|
41
|
+
import { type BuyStoreWait } from './store-wait.js';
|
|
40
42
|
/**
|
|
41
43
|
* Where the purchase stands:
|
|
42
44
|
* - `needs_input`: Agentcard asks the shopper something, or shows a cart.
|
|
@@ -230,6 +232,8 @@ export interface BuyToolResult {
|
|
|
230
232
|
* message from Agentcard, and only the shopper's yes to that total confirms it.
|
|
231
233
|
*/
|
|
232
234
|
cart_building?: BuyCartBuilding | null;
|
|
235
|
+
/** The checkout plan the answer started or moved, such as a trip's flight, hotel and car with each one's state; null when none. */
|
|
236
|
+
plan?: BuyPlan | null;
|
|
233
237
|
/**
|
|
234
238
|
* True when this call sent Agentcard a confirm: the shopper's yes, or the yes the tool carried past a step the shopper
|
|
235
239
|
* finished outside the chat. Absent when the call sent the shopper's words or read the conversation.
|
|
@@ -289,7 +293,7 @@ export interface BuyToolFound {
|
|
|
289
293
|
found: number;
|
|
290
294
|
more: BuyToolLink[];
|
|
291
295
|
}
|
|
292
|
-
export interface BuyToolOptions {
|
|
296
|
+
export interface BuyToolOptions extends BuyStoreWait {
|
|
293
297
|
/** A client built with your company's credentials. Build it once and share it across every shopper's tool. */
|
|
294
298
|
agentcard?: AgentcardClient;
|
|
295
299
|
/** Or one shopper's own Agentcard access token, for a tool that buys only for them. */
|
|
@@ -557,7 +561,7 @@ export type BuyToolTurnItem = {
|
|
|
557
561
|
link: BuyToolLink;
|
|
558
562
|
};
|
|
559
563
|
/** The callbacks of one turn. Each one left out falls back to the option of the same name the tool was built with, except with `onItem`. */
|
|
560
|
-
export interface BuyToolTurnOptions {
|
|
564
|
+
export interface BuyToolTurnOptions extends BuyStoreWait {
|
|
561
565
|
/**
|
|
562
566
|
* Hears everything the turn hands the shopper, one item at a time and in the order it happened: the
|
|
563
567
|
* instant reply and each message your model writes through `turn.messageStream()` or `turn.say()` as
|
|
@@ -622,6 +626,14 @@ export interface BuyToolTurnOptions {
|
|
|
622
626
|
onText?: BuyToolOptions['onText'];
|
|
623
627
|
/** Hears the carts each time a call of this turn builds or changes one, as the tool's `onCart` does. */
|
|
624
628
|
onCart?: BuyToolOptions['onCart'];
|
|
629
|
+
/**
|
|
630
|
+
* The card the shopper tapped to open this turn, by the `productId` its link carried, whatever answer showed it. With `direct`
|
|
631
|
+
* on, the turn's call goes straight to Agentcard as a pick of exactly that card (`turn.direct`, reason `pick`), and Agentcard
|
|
632
|
+
* carts it, so a tap on a card two answers back carts that card. Send the tap's words as the turn's message too.
|
|
633
|
+
*/
|
|
634
|
+
tapped?: {
|
|
635
|
+
productId: string;
|
|
636
|
+
};
|
|
625
637
|
}
|
|
626
638
|
/**
|
|
627
639
|
* The buy tool for one shopper message, from `buy.turn`. It is the same tool, with the same name,
|
package/dist/buy-tool.js
CHANGED
|
@@ -1,36 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The buy tool: one call that buys for one shopper, in one object that every
|
|
3
|
-
* agent framework takes. `answer` sends the model's input to Agentcard and
|
|
4
|
-
* returns the whole answer for your code, with the status, Agentcard's
|
|
5
|
-
* reply, the cart and its hash, the products and the links. `forModel` turns
|
|
6
|
-
* that answer into what the model may read, with every link taken out, and
|
|
7
|
-
* `run`, `execute` and `handle` return that view as JSON text. A link
|
|
8
|
-
* reaches the shopper through `sendLink`, and each call waits for it before
|
|
9
|
-
* it returns: the Vault and approval links, then the store page of each
|
|
10
|
-
* product the model reads.
|
|
11
|
-
*
|
|
12
|
-
* Agentcard keeps the conversation: every call continues the shopper's open
|
|
13
|
-
* purchase (`resume: true`), and a confirm places the cart Agentcard last
|
|
14
|
-
* showed (`confirm: true`). The tool keeps one thing between calls: the store
|
|
15
|
-
* pages it sent last. Agentcard repeats the last search in each answer for a
|
|
16
|
-
* while, so a list it shows again in the same conversation does not reach
|
|
17
|
-
* the shopper twice. A tool built for each message buys the same way, and
|
|
18
|
-
* sends such a list again.
|
|
19
|
-
*
|
|
20
|
-
* Two answers make the tool wait before it returns. A placement Agentcard is
|
|
21
|
-
* still finishing gets one 12-second pause and one read of the conversation.
|
|
22
|
-
* A turn that outlived Agentcard's budget gets a read every 5 seconds for up
|
|
23
|
-
* to a minute, until the turn ends.
|
|
24
|
-
*
|
|
25
|
-
* `turn` binds the tool to one shopper message: the calls the model makes
|
|
26
|
-
* through it hand their links, answers and instant reply to that turn's own
|
|
27
|
-
* callbacks and lists, so a shopper who sends a second message before the
|
|
28
|
-
* first is answered never gets one message's cards beside the other's reply.
|
|
29
|
-
* Every turn of one tool still shares the shopper's conversation state, such
|
|
30
|
-
* as the store pages already sent. A turn with `onItem` hands the app its
|
|
31
|
-
* messages and links as one sequence, in the order they happened, and spaces
|
|
32
|
-
* the messages like someone texting.
|
|
33
|
-
*/
|
|
34
1
|
import { AgentcardApiError, AgentcardClient, cartBuildingOf, } from './client.js';
|
|
35
2
|
import { buyToolDefinition, buyToolStandardSchema, openAIBuyTool, parseBuyToolInput, } from './definition.js';
|
|
36
3
|
import { directCall, isProductAsk } from './direct.js';
|
|
@@ -39,6 +6,7 @@ import { choiceLines, leadsIntoList, messageForShopper, messagesListChoice, repl
|
|
|
39
6
|
import { joinLeadIns, leadInJoiner, messageStream, paceOf, paragraphsOf, sleep, toMessages, typingHook } from './messages.js';
|
|
40
7
|
import { isStale, messageInChat } from './shopper-message.js';
|
|
41
8
|
import { actionLinkId, isActionLink } from './action-links.js';
|
|
9
|
+
import { checkStoreWait, storeWaitFields, withLateCards } from './store-wait.js';
|
|
42
10
|
import { ACK_REACTION, ackAt, answersCheckout, answersQuestion, asksForSimilar, holdsBack, instantAck as instantAckOf, isNewRequest, isPickText, keepsYes, namesStore, nextTemplate, readsAsAckLine, slowStepLine } from './ack.js';
|
|
43
11
|
/** The action a product of `merchant` takes (BuyToolProductAction). */
|
|
44
12
|
export function productAction(merchant) {
|
|
@@ -151,6 +119,8 @@ const CART_CHANGED = 'The cart changed since the shopper saw it. Show the shoppe
|
|
|
151
119
|
const NOTHING_TO_CONFIRM = 'There is no cart to confirm yet. Ask the shopper what they want.';
|
|
152
120
|
const ADDRESS_CHANGED = 'The delivery address changed since this cart was built. Ask for the cart again, and Agentcard prices it for the new address.';
|
|
153
121
|
const STORE_PICK_GONE = "The store the shopper picked is no longer among the stores Agentcard found, so no store was chosen. Call this tool again with the shopper's words and store_id null, and Agentcard searches again.";
|
|
122
|
+
/** The most product cards a tool keeps from earlier answers for a tap on an older card. */
|
|
123
|
+
const SHOWN_BEFORE_MAX = 200;
|
|
154
124
|
const PICK_GONE = "The product the shopper picked is no longer among the products Agentcard found, so nothing was added. Call this tool again with the shopper's words and product_id null, and Agentcard searches again.";
|
|
155
125
|
/** One cart line as the order holds it: the product, its variant, its choices and its quantity. Prices are left out. */
|
|
156
126
|
const lineKey = (line) => JSON.stringify([line.product_id ?? String(line.name ?? '').trim().toLowerCase(), line.variant ?? null, [...(line.options ?? [])].sort(), line.qty]);
|
|
@@ -352,6 +322,7 @@ function answerOf(turn) {
|
|
|
352
322
|
hashes: hashesOf(turn.cart, turn.carts),
|
|
353
323
|
cart_building: turn.cart_building,
|
|
354
324
|
choice: turn.choice ?? null,
|
|
325
|
+
plan: turn.plan ?? null,
|
|
355
326
|
envelope: turn.raw,
|
|
356
327
|
};
|
|
357
328
|
}
|
|
@@ -419,6 +390,13 @@ const CARD_ASK = /\b(?:add|save|link|enter|attach)\s+(?:a|your|the)\s+(?:(?:paym
|
|
|
419
390
|
* which `askingForNamedStep` replaces when Agentcard names a step that comes first. Every message after it waits behind
|
|
420
391
|
* it, so the shopper reads them in order; a message before it goes out at once.
|
|
421
392
|
*/
|
|
393
|
+
/**
|
|
394
|
+
* Agentcard's messages of an answer, as written, that the shopper read in full through the early reply (onWrittenText)
|
|
395
|
+
* before the call ended, by the answer they belong to. `turn.replyAsWritten` hands them back for the turn's last answer and
|
|
396
|
+
* `turn.say` of them sends nothing, so the app shows no second reply and its model is not asked to word one
|
|
397
|
+
* (chat.opencommerce.sh, 2026-10-29: after "i found 16 more hotels" the model wrote a second recommendation of the same hotels).
|
|
398
|
+
*/
|
|
399
|
+
const readAsWritten = new WeakMap();
|
|
422
400
|
function waitsForAnswer(message) {
|
|
423
401
|
return CARD_ASK.test(message) || new RegExp(URL_IN_TEXT.source, 'i').test(message);
|
|
424
402
|
}
|
|
@@ -771,6 +749,7 @@ export function buyTool(options) {
|
|
|
771
749
|
throw new TypeError('Pass threadId as your own id for the chat or thread, 1 to 200 characters, or leave it out.');
|
|
772
750
|
}
|
|
773
751
|
const threadId = o.threadId?.trim();
|
|
752
|
+
checkStoreWait(o);
|
|
774
753
|
/** Whether a message is this tool's to read (`messageInChat`): its chat's purchase, or news about the shopper alone. */
|
|
775
754
|
const inThisThread = (message) => messageInChat(message, threadId);
|
|
776
755
|
if (o.sendLink !== undefined && typeof o.sendLink !== 'function') {
|
|
@@ -844,6 +823,9 @@ export function buyTool(options) {
|
|
|
844
823
|
const { onWorking, onReaction } = ackRoutes(o.onWorking, o.onReaction);
|
|
845
824
|
/** Calls running now, when the last one ended, and the template the instant reply used last. */
|
|
846
825
|
let running = 0;
|
|
826
|
+
// When the newest call of this tool started: a late reply on the conversation answers the newest shopper message, so a call
|
|
827
|
+
// that a newer one followed no longer owns it.
|
|
828
|
+
let newestCallAt = 0;
|
|
847
829
|
let lastEndedAt = Number.NEGATIVE_INFINITY;
|
|
848
830
|
let lastTemplate = -1;
|
|
849
831
|
/** Counts the model's turns: the calls of one turn share its number. */
|
|
@@ -942,6 +924,9 @@ export function buyTool(options) {
|
|
|
942
924
|
let lastAnswerProducts = [];
|
|
943
925
|
/** Every product card of the last answer's call the shopper can tap by name, each once (the answer's and the streamed). */
|
|
944
926
|
let lastTappable = [];
|
|
927
|
+
/** Every product card this conversation's answers showed, newest first, each once: a tap on an older card reads these. */
|
|
928
|
+
let shownBefore = [];
|
|
929
|
+
let shownIn = null;
|
|
945
930
|
let lastAnswerStores = [];
|
|
946
931
|
let previousTurn = null;
|
|
947
932
|
/** The names of the stores the last answer that listed stores showed: a message that names one, as a tap on its card does, picks it. */
|
|
@@ -1278,7 +1263,8 @@ export function buyTool(options) {
|
|
|
1278
1263
|
* picks one first. A placement another attempt is still finishing keeps the
|
|
1279
1264
|
* reading going and ends as `placing`, never as the cart. A minute without
|
|
1280
1265
|
* any of them returns Agentcard's own answer, or what the last read showed
|
|
1281
|
-
* while the turn still ran.
|
|
1266
|
+
* while the turn still ran. A turn that ended with a reply of its own
|
|
1267
|
+
* (`latest_reply`) answers with that reply and the products it showed.
|
|
1282
1268
|
*/
|
|
1283
1269
|
const awaitCart = async (base, conversationId, since) => {
|
|
1284
1270
|
const shown = new Set(base.hashes);
|
|
@@ -1313,13 +1299,18 @@ export function buyTool(options) {
|
|
|
1313
1299
|
// conversation and it shows one cart. With carts at two stores `confirm: true` still names the
|
|
1314
1300
|
// cart an earlier answer showed, so none is offered: the shopper's pick goes out as an ask.
|
|
1315
1301
|
const one = conversation.carts.length === 1 ? newest : null;
|
|
1302
|
+
// The reply the turn wrote after its early answer is what the shopper waited for: it goes out in place of the
|
|
1303
|
+
// tool's own line, with the products it showed (conv_054988b998634b16ab4a91d9, 2026-10-30: a stop message
|
|
1304
|
+
// written while the shopper read "one moment" was never shown, and the model was told to call again).
|
|
1305
|
+
const late = conversation.turn_in_progress || newestCallAt > since ? null : conversation.latest_reply;
|
|
1316
1306
|
const answer = {
|
|
1317
1307
|
...base,
|
|
1318
|
-
reply: one ? CART_READY : newest ? CHOOSE_CART : ANSWER_LATE,
|
|
1308
|
+
reply: late ? late.text : one ? CART_READY : newest ? CHOOSE_CART : ANSWER_LATE,
|
|
1309
|
+
...(late?.catalog ? { products: productsOfCatalog(late.catalog) } : {}),
|
|
1319
1310
|
cart: one,
|
|
1320
1311
|
carts: conversation.carts,
|
|
1321
1312
|
hashes: conversation.carts.map((c) => c.hash),
|
|
1322
|
-
error_code: newest ? null : base.error_code,
|
|
1313
|
+
error_code: newest || late ? null : base.error_code,
|
|
1323
1314
|
// A read replaced the answer the step was named for, and names no step of its own: the reply asks.
|
|
1324
1315
|
next_action: null,
|
|
1325
1316
|
// The early answer judged the pick before the turn finished adding it.
|
|
@@ -1818,6 +1809,9 @@ export function buyTool(options) {
|
|
|
1818
1809
|
let cardsOut = false;
|
|
1819
1810
|
/** The product cards this call's searches sent out as they answered, which the shopper can tap whatever the answer lists. */
|
|
1820
1811
|
const streamedCards = [];
|
|
1812
|
+
/** Whether Agentcard said the reply is written (`reply_written`), and the cards that went out after it: a store that answered late. */
|
|
1813
|
+
let replyWritten = false;
|
|
1814
|
+
const lateCards = [];
|
|
1821
1815
|
let heldCount = null;
|
|
1822
1816
|
/** The card each catalog id of this call went out as (its `productId`), and the cards still `checking`, by conversation. */
|
|
1823
1817
|
const cardOfId = new Map();
|
|
@@ -1934,13 +1928,19 @@ export function buyTool(options) {
|
|
|
1934
1928
|
// The products past the cards are read after the cards went out, so the first card never waits for them.
|
|
1935
1929
|
const keepMore = () => { if (t.onFound && early)
|
|
1936
1930
|
moreFound = { conversationId: update.conversationId, links: productsOfCatalog({ ...update.catalog, items: update.more }).map(productLink) }; };
|
|
1937
|
-
if (!early)
|
|
1931
|
+
if (!early) {
|
|
1932
|
+
// Without early cards nothing goes out here, but a store that answered after the reply still joins the result.
|
|
1933
|
+
if (replyWritten)
|
|
1934
|
+
lateCards.push(...productsOfCatalog(update.catalog));
|
|
1938
1935
|
return;
|
|
1936
|
+
}
|
|
1939
1937
|
// Products arrived before the instant reply was due: they are the reply.
|
|
1940
1938
|
ack?.cancel();
|
|
1941
1939
|
await t.ackOut;
|
|
1942
1940
|
// The turn's own search presents its products, as a reply that lists them does.
|
|
1943
|
-
await deliverPagesInTurn(earlyPages(update.conversationId, update.asOf, withAnswers(productsOfCatalog(update.catalog))), { turn: callTurn, presented: true, owner: t }, send);
|
|
1941
|
+
const out = await deliverPagesInTurn(earlyPages(update.conversationId, update.asOf, withAnswers(productsOfCatalog(update.catalog))), { turn: callTurn, presented: true, owner: t }, send);
|
|
1942
|
+
if (replyWritten)
|
|
1943
|
+
lateCards.push(...out.products);
|
|
1944
1944
|
// A place shown `checking`, on a card or behind "show more", waits for the live search's word on it.
|
|
1945
1945
|
noteCards([...productsOfCatalog(update.catalog), ...productsOfCatalog({ ...update.catalog, items: update.more })], update.conversationId);
|
|
1946
1946
|
streamedCards.push(...productsOfCatalog(update.catalog));
|
|
@@ -1972,10 +1972,11 @@ export function buyTool(options) {
|
|
|
1972
1972
|
},
|
|
1973
1973
|
}
|
|
1974
1974
|
: {}),
|
|
1975
|
-
...(
|
|
1975
|
+
...(early || t.onProducts
|
|
1976
1976
|
? {
|
|
1977
1977
|
onReplyWritten: async () => {
|
|
1978
|
-
|
|
1978
|
+
replyWritten = true;
|
|
1979
|
+
if (speak || !writtenText)
|
|
1979
1980
|
return;
|
|
1980
1981
|
speak = writtenText;
|
|
1981
1982
|
for (const message of unsaid.splice(0))
|
|
@@ -2021,10 +2022,15 @@ export function buyTool(options) {
|
|
|
2021
2022
|
// Every request names the tool's thread, so Agentcard continues this chat's purchase and no other.
|
|
2022
2023
|
if (threadId)
|
|
2023
2024
|
sent = { ...sent, thread_id: threadId };
|
|
2025
|
+
// Every request names how long the reply waits for stores, and what happens to a store that answers after it.
|
|
2026
|
+
sent = { ...sent, ...storeWaitFields(t.storeWait ?? {}, o) };
|
|
2024
2027
|
if (plan && 'local' in plan) {
|
|
2025
2028
|
// Nothing went to Agentcard: the answer is where the purchase stands.
|
|
2026
2029
|
}
|
|
2027
2030
|
else {
|
|
2031
|
+
// This call now owns the conversation's newest reply: an earlier call still waiting for its late reply gives it up.
|
|
2032
|
+
// A call that threw on its input, or answered from where the purchase stands, sent nothing and takes none.
|
|
2033
|
+
newestCallAt = Math.max(newestCallAt, startedAt);
|
|
2028
2034
|
try {
|
|
2029
2035
|
turn = await client.buy(sent, handlers);
|
|
2030
2036
|
}
|
|
@@ -2119,8 +2125,14 @@ export function buyTool(options) {
|
|
|
2119
2125
|
}
|
|
2120
2126
|
}
|
|
2121
2127
|
const read = choiceShown && !!turn && (settled.reply === turn.reply ? readEvery(told, turn.messages) : replaced && readEvery(told, paragraphsOf(settled.reply)));
|
|
2128
|
+
// The shopper read the whole answer before the call ended, the late source's message included: the turn's reply is what
|
|
2129
|
+
// they read, and nothing is worded again (chat.opencommerce.sh, 2026-10-29: after "i found 16 more hotels" the model
|
|
2130
|
+
// wrote a second recommendation of the same hotels).
|
|
2122
2131
|
const answer = withMessages(settled, turn);
|
|
2123
|
-
|
|
2132
|
+
// A late reply read from the conversation presents the products of its own catalog, which the early answer never held.
|
|
2133
|
+
const late = settled.envelope?.latest_reply;
|
|
2134
|
+
const presented = turn?.catalog?.presented === true || (late?.text === settled.reply && late?.catalog?.presented === true);
|
|
2135
|
+
const delivered = await deliver(answered.size > 0 ? { ...answer, products: withAnswers(answer.products) } : answer, { turn: callTurn, presented, owner: t }, send);
|
|
2124
2136
|
// A count that came before any card (find mode puts its cards in the answer) goes out once the answer's cards did.
|
|
2125
2137
|
if (heldCount && (delivered.products?.length ?? 0) > 0) {
|
|
2126
2138
|
const held = heldCount;
|
|
@@ -2145,11 +2157,15 @@ export function buyTool(options) {
|
|
|
2145
2157
|
: null;
|
|
2146
2158
|
const result = {
|
|
2147
2159
|
...delivered,
|
|
2160
|
+
// The cards of a store that answered after the reply join the answer's, when the answer does not list them already.
|
|
2161
|
+
...(lateCards.length > 0 ? { products: withLateCards(delivered.products, lateCards) } : {}),
|
|
2148
2162
|
...(t.ackDelivered ? { shopper_already_told_looking: true } : {}),
|
|
2149
2163
|
...(read ? { shopper_already_read_reply: true } : {}),
|
|
2150
2164
|
...(confirmSent ? { confirm_sent: true } : {}),
|
|
2151
2165
|
...(yesCarried ? { yes_carried: yesCarried } : {}),
|
|
2152
2166
|
};
|
|
2167
|
+
if (read && speak && speak === writtenText)
|
|
2168
|
+
readAsWritten.set(result, told.join('\n\n'));
|
|
2153
2169
|
lastNextAction = result.next_action;
|
|
2154
2170
|
lastReply = result.reply;
|
|
2155
2171
|
// A refusal reports no carts, so only a turn's answer says how many wait, and whether a checkout just ended with none.
|
|
@@ -2199,6 +2215,13 @@ export function buyTool(options) {
|
|
|
2199
2215
|
}
|
|
2200
2216
|
lastAnswerProducts = shown.map((product) => ({ productId: pickId(product), name: product.name }));
|
|
2201
2217
|
lastTappable = tappable.map((product) => ({ productId: pickId(product), name: product.name }));
|
|
2218
|
+
// A new conversation starts the cards shown before afresh; each card keeps its newest showing, up to SHOWN_BEFORE_MAX.
|
|
2219
|
+
if (result.conversation_id && result.conversation_id !== shownIn)
|
|
2220
|
+
shownBefore = [];
|
|
2221
|
+
if (result.conversation_id)
|
|
2222
|
+
shownIn = result.conversation_id;
|
|
2223
|
+
const fresh = new Set(lastTappable.map((product) => product.productId ?? `name:${product.name}`));
|
|
2224
|
+
shownBefore = [...lastTappable, ...shownBefore.filter((product) => !fresh.has(product.productId ?? `name:${product.name}`))].slice(0, SHOWN_BEFORE_MAX);
|
|
2202
2225
|
lastAnswerStores = storesShown.map((store) => ({ storeId: store.id, name: store.name }));
|
|
2203
2226
|
t.results?.push(result);
|
|
2204
2227
|
if (t.onResult) {
|
|
@@ -2310,6 +2333,7 @@ export function buyTool(options) {
|
|
|
2310
2333
|
if (own.textingStyle !== undefined && typeof own.textingStyle !== 'boolean')
|
|
2311
2334
|
throw new TypeError('Pass textingStyle to the turn as true or false, or leave it out.');
|
|
2312
2335
|
const texting = own.textingStyle ?? textingByDefault;
|
|
2336
|
+
checkStoreWait(own, ' to the turn');
|
|
2313
2337
|
const hearText = own.onText ?? o.onText;
|
|
2314
2338
|
// Messages are spaced only for an app that hears each one as it goes out.
|
|
2315
2339
|
const pauseOf = paceOf(own.pace);
|
|
@@ -2386,6 +2410,7 @@ export function buyTool(options) {
|
|
|
2386
2410
|
? (text) => feed.push({ kind: 'text', text }, () => (onItem ? onItem({ kind: 'text', text }) : onMessage?.(text)))
|
|
2387
2411
|
: undefined,
|
|
2388
2412
|
texting,
|
|
2413
|
+
storeWait: { storeWaitMs: own.storeWaitMs, lateResults: own.lateResults },
|
|
2389
2414
|
onCart: own.onCart ?? o.onCart,
|
|
2390
2415
|
tell: onItem || baseOnWorking ? (text) => feed.push({ kind: 'text', text }, () => (onItem ? onItem({ kind: 'text', text }) : baseOnWorking?.(text))) : undefined,
|
|
2391
2416
|
links: [],
|
|
@@ -2478,13 +2503,17 @@ export function buyTool(options) {
|
|
|
2478
2503
|
const results = state.results;
|
|
2479
2504
|
// A pick or an answer goes straight only after a turn that ended on Agentcard's answer, and never while a call still runs.
|
|
2480
2505
|
const answered = previousTurn !== null && (previousTurn.results?.length ?? 0) > 0;
|
|
2506
|
+
// A tap names its card by the id its link carried, whatever answer showed it: the pick goes straight with that id.
|
|
2507
|
+
const tappedId = typeof options?.tapped?.productId === 'string' && options.tapped.productId.trim() ? options.tapped.productId.trim() : null;
|
|
2481
2508
|
// A `nextStep` turn's call is the same whatever the model writes (`stepPlan` reads where the purchase stands), so it goes
|
|
2482
2509
|
// straight too: with `replyAsWritten`, a step whose answer is a link, such as the approval after a saved card, needs no model.
|
|
2483
2510
|
const direct = !directOn || running > 0
|
|
2484
2511
|
? null
|
|
2485
2512
|
: event
|
|
2486
2513
|
? { input: { ask: NEXT_STEP_ASK, confirm: false, new_order: false, product_id: null, store_id: null }, reason: 'next_step' }
|
|
2487
|
-
:
|
|
2514
|
+
: tappedId && String(shopperText ?? '').trim()
|
|
2515
|
+
? { input: { ask: String(shopperText).trim(), confirm: false, new_order: false, product_id: tappedId, store_id: null }, reason: 'pick' }
|
|
2516
|
+
: directCall(String(shopperText ?? ''), { answered, nextAction: lastNextAction, asked: lastReply, products: lastAnswerProducts, tappable: lastTappable, shownBefore, stores: lastAnswerStores });
|
|
2488
2517
|
// Agentcard writes the line for a search the turn sends straight. A turn the app's model runs gets the model's own line,
|
|
2489
2518
|
// or the wait line below.
|
|
2490
2519
|
state.agentcardAcks = (ask) => direct !== null && direct.reason === 'product_ask' && ask.trim() === direct.input.ask.trim();
|
|
@@ -2595,6 +2624,10 @@ export function buyTool(options) {
|
|
|
2595
2624
|
/** Says a whole text as the turn's messages, after every item before it (`say()`). */
|
|
2596
2625
|
const sayWhole = (whole) => {
|
|
2597
2626
|
writesThroughTurn = true;
|
|
2627
|
+
// The reply the shopper already read as Agentcard wrote it (`readAsWritten`), of the turn's last answer: saying it again sends nothing.
|
|
2628
|
+
const read = results.length > 0 ? readAsWritten.get(results[results.length - 1]) : undefined;
|
|
2629
|
+
if (read && whole === read)
|
|
2630
|
+
return releaseForReply();
|
|
2598
2631
|
firstWords(whole);
|
|
2599
2632
|
if (state.modelTurn && /\S/.test(String(whole ?? '')))
|
|
2600
2633
|
state.modelTurn.spoke = true;
|
|
@@ -2678,7 +2711,7 @@ export function buyTool(options) {
|
|
|
2678
2711
|
results: { get: () => [...results], enumerable: false },
|
|
2679
2712
|
direct: { value: direct, enumerable: false },
|
|
2680
2713
|
stopped: { get: () => state.stopped === true, enumerable: false },
|
|
2681
|
-
replyAsWritten: { get: () => (written && results.length > 0 && !state.stopped ? replyAsWritten(results[results.length - 1], { textingStyle: texting }) : null), enumerable: false },
|
|
2714
|
+
replyAsWritten: { get: () => (written && results.length > 0 && !state.stopped ? readAsWritten.get(results[results.length - 1]) ?? replyAsWritten(results[results.length - 1], { textingStyle: texting }) : null), enumerable: false },
|
|
2682
2715
|
items: { get: () => feed.items.map((item) => (item.kind === 'link' ? { kind: 'link', link: { ...item.link } } : { ...item })), enumerable: false },
|
|
2683
2716
|
// Each message joins the feed the moment it is cut, so a link the tool hands over during a pause never overtakes text already written.
|
|
2684
2717
|
// A step link held for this message goes out right after its last paragraph, once the stream ends.
|
package/dist/client.d.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The client the buy tool calls Agentcard with. A company builds one from its
|
|
3
|
+
* client credentials, and every shopper's tool shares the one access token it
|
|
4
|
+
* exchanges them for; or a tool runs on one shopper's own access token. The
|
|
5
|
+
* global fetch is the only transport, so the package keeps no dependencies.
|
|
6
|
+
*
|
|
7
|
+
* The answer fields and the error codes follow the buy module of
|
|
8
|
+
* @agent-cards/sdk (packages/checkout/src/buy.ts: turnOf, buyError), and the
|
|
9
|
+
* token cache follows its client (packages/checkout/src/client.ts accessToken).
|
|
10
|
+
*/
|
|
11
|
+
import { type BuyPlan } from './plan.js';
|
|
1
12
|
import { type AgentcardListener, type ListenOptions } from './listen.js';
|
|
2
13
|
import { type ShopperMessage } from './shopper-message.js';
|
|
3
14
|
/** The fetch the client calls: Node 18 and later have one built in. */
|
|
@@ -241,6 +252,16 @@ export interface BuyRequest {
|
|
|
241
252
|
* another thread is refused (409 `thread_mismatch`). Leave it out for an app with one chat per shopper.
|
|
242
253
|
*/
|
|
243
254
|
thread_id?: string;
|
|
255
|
+
/**
|
|
256
|
+
* How long, in milliseconds, the reply waits for store searches before Agentcard writes it. Left out, Agentcard keeps
|
|
257
|
+
* its own wait. `buyTool({ storeWaitMs })` sends it on every request.
|
|
258
|
+
*/
|
|
259
|
+
store_wait_ms?: number;
|
|
260
|
+
/**
|
|
261
|
+
* What happens to a store that answers after the reply: `stream` sends its cards after the reply in the same streamed
|
|
262
|
+
* turn, before the answer; `hold` keeps them out of the turn. Left out, Agentcard does as it does today.
|
|
263
|
+
*/
|
|
264
|
+
late_results?: 'stream' | 'hold';
|
|
244
265
|
/** Drop the cart on the table and start a new order. */
|
|
245
266
|
new_order?: true;
|
|
246
267
|
/**
|
|
@@ -542,6 +563,8 @@ export interface BuyTurn {
|
|
|
542
563
|
cart_building: BuyCartBuilding | null;
|
|
543
564
|
/** The choice the reply asks the shopper to make, each group with every value; null when it asks none, and on an older API. */
|
|
544
565
|
choice: BuyChoice | null;
|
|
566
|
+
/** The checkout plan this answer's yes started or moved, with each item's state; null when none, and on an older API. */
|
|
567
|
+
plan: BuyPlan | null;
|
|
545
568
|
/** The answer as Agentcard sent it. */
|
|
546
569
|
raw: Record<string, unknown>;
|
|
547
570
|
}
|
|
@@ -581,8 +604,22 @@ export interface BuyConversation {
|
|
|
581
604
|
last_checkout: BuyLastCheckout | null;
|
|
582
605
|
orders: BuyOrder[];
|
|
583
606
|
carts: BuyCart[];
|
|
607
|
+
/**
|
|
608
|
+
* The reply Agentcard wrote since the shopper's last message, once no turn runs: the answer a call that ended with
|
|
609
|
+
* `turn_timeout` never got. Null while a turn runs, when no reply was written since, and on an older API.
|
|
610
|
+
*/
|
|
611
|
+
latest_reply: BuyLatestReply | null;
|
|
584
612
|
raw: Record<string, unknown>;
|
|
585
613
|
}
|
|
614
|
+
/** A reply a conversation read hands back: its text, its messages, and the products it showed. */
|
|
615
|
+
export interface BuyLatestReply {
|
|
616
|
+
message_id: string;
|
|
617
|
+
text: string;
|
|
618
|
+
messages: string[];
|
|
619
|
+
/** The products the reply showed, when the call that asked ended before it was written; null otherwise. */
|
|
620
|
+
catalog: BuyCatalog | null;
|
|
621
|
+
created_at: string | null;
|
|
622
|
+
}
|
|
586
623
|
/** A Vault link: the page where a shopper connects and adds a card. */
|
|
587
624
|
export interface VaultLink {
|
|
588
625
|
id: string;
|
package/dist/client.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* @agent-cards/sdk (packages/checkout/src/buy.ts: turnOf, buyError), and the
|
|
9
9
|
* token cache follows its client (packages/checkout/src/client.ts accessToken).
|
|
10
10
|
*/
|
|
11
|
+
import { planOf } from './plan.js';
|
|
11
12
|
import { isRecord } from './verify.js';
|
|
12
13
|
import { listenToAgentcard } from './listen.js';
|
|
13
14
|
import { messageInChat } from './shopper-message.js';
|
|
@@ -302,6 +303,7 @@ export function turnOf(body) {
|
|
|
302
303
|
store_pick: isRecord(body.store_pick) && str(body.store_pick.store_id) ? { store_id: body.store_pick.store_id, name: str(body.store_pick.name), selected: body.store_pick.selected === true } : null,
|
|
303
304
|
cart_building: cartBuildingOf(body.cart_building),
|
|
304
305
|
choice: choiceOf(body.choice),
|
|
306
|
+
plan: planOf(body.plan),
|
|
305
307
|
raw: body,
|
|
306
308
|
};
|
|
307
309
|
}
|
|
@@ -358,9 +360,23 @@ function conversationOf(body) {
|
|
|
358
360
|
placed_at: str(o.placed_at),
|
|
359
361
|
})) : [],
|
|
360
362
|
carts: cartsOf(body.carts),
|
|
363
|
+
latest_reply: latestReplyOf(body.latest_reply),
|
|
361
364
|
raw: body,
|
|
362
365
|
};
|
|
363
366
|
}
|
|
367
|
+
/** The reply a conversation read hands back, or null when it names no text. */
|
|
368
|
+
function latestReplyOf(value) {
|
|
369
|
+
if (!isRecord(value) || !str(value.text)?.trim())
|
|
370
|
+
return null;
|
|
371
|
+
const messages = Array.isArray(value.messages) ? value.messages.filter((m) => typeof m === 'string' && m.trim() !== '') : [];
|
|
372
|
+
return {
|
|
373
|
+
message_id: str(value.message_id) ?? '',
|
|
374
|
+
text: value.text,
|
|
375
|
+
messages: messages.length > 0 ? messages : [value.text],
|
|
376
|
+
catalog: catalogOf(value.catalog),
|
|
377
|
+
created_at: str(value.created_at),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
364
380
|
/** An error code as the API spells one: lower-case words joined by underscores. */
|
|
365
381
|
const CODE = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)+$/;
|
|
366
382
|
/**
|