@agent-cards/commerce-sdk 0.10.0 → 0.13.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.
@@ -0,0 +1,106 @@
1
+ /**
2
+ * `@agent-cards/commerce-sdk/browser`: the part of the package that runs in the shopper's browser.
3
+ * It imports no Node module, so any bundler takes it.
4
+ *
5
+ * `actionWindows` opens each step the shopper takes outside the chat (adding a card, signing in to a
6
+ * store, approving a payment) in a pop-up window over the chat, and closes it the moment the shopper
7
+ * message that ends the step arrives. On a phone, where a browser opens the pop-up as a tab, it
8
+ * closes that tab the same way.
9
+ */
10
+ import { type ActionLinkKind, type ActionLinkMessage, type ActionLinkOutcome } from './action-links.js';
11
+ export { ACTION_LINK_ENDINGS, ACTION_LINK_KINDS, actionLinkId, actionLinkOutcome, isActionLink, type ActionLinkKind, type ActionLinkMessage, type ActionLinkOutcome, } from './action-links.js';
12
+ /** The part of a window `window.open` returns that `actionWindows` uses. */
13
+ export interface PopupLike {
14
+ readonly closed: boolean;
15
+ close(): void;
16
+ focus(): void;
17
+ }
18
+ /** The part of the page's `window` that `actionWindows` uses. */
19
+ export interface WindowLike {
20
+ open(url: string, target: string, features: string): PopupLike | null;
21
+ readonly screenX?: number;
22
+ readonly screenY?: number;
23
+ readonly outerWidth?: number;
24
+ readonly outerHeight?: number;
25
+ readonly screen?: {
26
+ readonly availWidth?: number;
27
+ readonly availHeight?: number;
28
+ };
29
+ setInterval(handler: () => void, ms: number): unknown;
30
+ clearInterval(id: unknown): void;
31
+ }
32
+ /** A step's link, as the buy tool hands it over (`BuyToolLink`) or as an app passes it on: its kind and its page. */
33
+ export interface ActionLinkLike {
34
+ kind: string;
35
+ url?: string | null;
36
+ /** The store a store login or an approval is for, as the buy tool names it, so a message about another store leaves it open. */
37
+ store?: string | null;
38
+ /** The link's `actionLinkId`, when the buy tool put one on it. Read from the link when absent. */
39
+ id?: string;
40
+ }
41
+ /** A step open in a pop-up. */
42
+ export interface ActionWindow {
43
+ /** The link's `actionLinkId`. */
44
+ readonly id: string;
45
+ readonly kind: ActionLinkKind;
46
+ readonly url: string;
47
+ /** The store the step is for, when the link named one. */
48
+ readonly store: string | null;
49
+ /** False once the window closed, by the shopper or by `actionWindows`. */
50
+ readonly open: boolean;
51
+ }
52
+ export interface ActionWindowsOptions {
53
+ /** The page's window. Defaults to `globalThis.window`; pass one to test with a fake. */
54
+ window?: WindowLike;
55
+ /** The pop-up's size in CSS pixels, kept inside the screen. Defaults to 480 by 760. */
56
+ width?: number;
57
+ height?: number;
58
+ /** How often to check whether the shopper closed a pop-up, in milliseconds. Defaults to 500. */
59
+ pollMs?: number;
60
+ /**
61
+ * Hears a step that ended while its pop-up was open, after `settle` closed the pop-up. Show the step
62
+ * as done here.
63
+ */
64
+ onEnded?: (step: ActionWindow, outcome: ActionLinkOutcome) => void;
65
+ /**
66
+ * Hears a pop-up the shopper closed before the step ended. The step is still open: keep offering
67
+ * its link, so the shopper can open it again.
68
+ */
69
+ onClosed?: (step: ActionWindow) => void;
70
+ }
71
+ export interface ActionWindows {
72
+ /**
73
+ * Opens the step's page in a pop-up centered over the chat, or brings its pop-up to the front when
74
+ * one is open already. Returns null when the link is not a step with a page, or when the browser
75
+ * blocked the pop-up: then let the link open the way it would have, as a new tab. Call it from the
76
+ * tap's click handler, so the browser counts the tap as the reason for the pop-up:
77
+ *
78
+ * ```js
79
+ * a.addEventListener('click', (event) => { if (windows.open(link)) event.preventDefault(); });
80
+ * ```
81
+ */
82
+ open(link: ActionLinkLike): ActionWindow | null;
83
+ /**
84
+ * Hands over a shopper message (`ShopperMessage`, or any object with its `kind` and `facts`, whose
85
+ * `merchantName` keeps one store's message from closing another store's step).
86
+ * Each open pop-up whose step the message ends closes, and `onEnded` hears it. Returns the steps it
87
+ * ended, with how each ended; an empty list when the message ends none.
88
+ */
89
+ settle(message: ActionLinkMessage): {
90
+ step: ActionWindow;
91
+ outcome: ActionLinkOutcome;
92
+ }[];
93
+ /** The open pop-up for the link, or null. */
94
+ get(link: ActionLinkLike): ActionWindow | null;
95
+ /**
96
+ * Stops watching every pop-up, and leaves each one as it is: a chat that reloads or remounts never
97
+ * shuts the page a shopper is typing a card into. Call it when the chat goes away.
98
+ */
99
+ dispose(): void;
100
+ }
101
+ /**
102
+ * Opens each step outside the chat in a pop-up and closes it when the step ends. One per chat page.
103
+ * The pop-up keeps its opener, deliberately: a window opened with `noopener` hands back no handle,
104
+ * so the chat could neither close it nor tell when the shopper closed it.
105
+ */
106
+ export declare function actionWindows(options?: ActionWindowsOptions): ActionWindows;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * `@agent-cards/commerce-sdk/browser`: the part of the package that runs in the shopper's browser.
3
+ * It imports no Node module, so any bundler takes it.
4
+ *
5
+ * `actionWindows` opens each step the shopper takes outside the chat (adding a card, signing in to a
6
+ * store, approving a payment) in a pop-up window over the chat, and closes it the moment the shopper
7
+ * message that ends the step arrives. On a phone, where a browser opens the pop-up as a tab, it
8
+ * closes that tab the same way.
9
+ */
10
+ import { actionLinkId, actionLinkOutcome, isActionLink } from './action-links.js';
11
+ export { ACTION_LINK_ENDINGS, ACTION_LINK_KINDS, actionLinkId, actionLinkOutcome, isActionLink, } from './action-links.js';
12
+ const DEFAULT_WIDTH = 480;
13
+ const DEFAULT_HEIGHT = 760;
14
+ const DEFAULT_POLL_MS = 500;
15
+ /** `width=…,height=…,left=…,top=…` for a pop-up centered over the opener, kept on its screen. */
16
+ function featuresFor(win, width, height) {
17
+ const availWidth = win.screen?.availWidth ?? width;
18
+ const availHeight = win.screen?.availHeight ?? height;
19
+ const w = Math.max(320, Math.min(width, availWidth));
20
+ const h = Math.max(400, Math.min(height, availHeight));
21
+ const left = Math.max(0, Math.round((win.screenX ?? 0) + ((win.outerWidth ?? w) - w) / 2));
22
+ const top = Math.max(0, Math.round((win.screenY ?? 0) + ((win.outerHeight ?? h) - h) / 2));
23
+ return `popup=yes,width=${w},height=${h},left=${left},top=${top}`;
24
+ }
25
+ const view = (t) => t.step;
26
+ /**
27
+ * Opens each step outside the chat in a pop-up and closes it when the step ends. One per chat page.
28
+ * The pop-up keeps its opener, deliberately: a window opened with `noopener` hands back no handle,
29
+ * so the chat could neither close it nor tell when the shopper closed it.
30
+ */
31
+ export function actionWindows(options = {}) {
32
+ const open = new Map();
33
+ let timer = null;
34
+ const win = () => options.window ?? (globalThis.window ?? null);
35
+ const stopIfIdle = () => {
36
+ if (open.size === 0 && timer !== null) {
37
+ win()?.clearInterval(timer);
38
+ timer = null;
39
+ }
40
+ };
41
+ const poll = () => {
42
+ for (const [id, tracked] of open) {
43
+ if (!tracked.popup.closed)
44
+ continue;
45
+ open.delete(id);
46
+ tracked.step.open = false;
47
+ options.onClosed?.(view(tracked));
48
+ }
49
+ stopIfIdle();
50
+ };
51
+ const idOf = (link) => link.id ?? actionLinkId(link);
52
+ return {
53
+ open(link) {
54
+ if (!isActionLink(link))
55
+ return null;
56
+ const w = win();
57
+ if (!w)
58
+ return null;
59
+ const id = idOf(link);
60
+ const known = open.get(id);
61
+ if (known && !known.popup.closed) {
62
+ known.popup.focus();
63
+ return view(known);
64
+ }
65
+ let popup = null;
66
+ try {
67
+ // A window named for the step: tapping the link again reaches the same window, never a second one.
68
+ popup = w.open(link.url, `agentcard_${id}`, featuresFor(w, options.width ?? DEFAULT_WIDTH, options.height ?? DEFAULT_HEIGHT));
69
+ }
70
+ catch {
71
+ popup = null;
72
+ }
73
+ if (!popup)
74
+ return null;
75
+ const tracked = { step: { id, kind: link.kind, url: link.url, store: link.store ?? null, open: true }, popup };
76
+ open.set(id, tracked);
77
+ try {
78
+ popup.focus();
79
+ }
80
+ catch {
81
+ /* a browser that refuses focus still shows the window */
82
+ }
83
+ timer ??= w.setInterval(poll, options.pollMs ?? DEFAULT_POLL_MS);
84
+ return view(tracked);
85
+ },
86
+ settle(message) {
87
+ const ended = [];
88
+ for (const [id, tracked] of open) {
89
+ const outcome = actionLinkOutcome(tracked.step, message);
90
+ if (!outcome)
91
+ continue;
92
+ open.delete(id);
93
+ tracked.step.open = false;
94
+ try {
95
+ tracked.popup.close();
96
+ }
97
+ catch {
98
+ /* already gone */
99
+ }
100
+ ended.push({ step: view(tracked), outcome });
101
+ }
102
+ stopIfIdle();
103
+ for (const { step, outcome } of ended)
104
+ options.onEnded?.(step, outcome);
105
+ return ended;
106
+ },
107
+ get(link) {
108
+ if (!isActionLink(link))
109
+ return null;
110
+ const tracked = open.get(idOf(link));
111
+ return tracked && !tracked.popup.closed ? view(tracked) : null;
112
+ },
113
+ dispose() {
114
+ open.clear();
115
+ stopIfIdle();
116
+ },
117
+ };
118
+ }
@@ -31,11 +31,11 @@
31
31
  * messages and links as one sequence, in the order they happened, and spaces
32
32
  * the messages like someone texting.
33
33
  */
34
- import { AgentcardClient, type BuyCart, type BuyFlightItinerary, type BuyDeliveryAddress, type BuyNextAction, type BuyPick, type BuyProgress, type BuyStorePick, type BuyTurn, type FetchLike } from './client.js';
34
+ import { AgentcardClient, type BuyCart, type BuyCartBuilding, type BuyFlightItinerary, type BuyDeliveryAddress, type BuyNextAction, type BuyPick, type BuyProgress, type BuyStorePick, type BuyTurn, type FetchLike } from './client.js';
35
35
  import { type BuyToolDefinition, type BuyToolInput, type BuyToolInputSchema, type BuyToolStandardSchema, type OpenAIBuyTool } from './definition.js';
36
36
  import { type BuyToolModelView } from './model-view.js';
37
37
  import { type MessagePace, type MessageStream } from './messages.js';
38
- import type { ShopperMessage } from './shopper-message.js';
38
+ import { type ShopperMessage } from './shopper-message.js';
39
39
  /**
40
40
  * Where the purchase stands:
41
41
  * - `needs_input`: Agentcard asks the shopper something, or shows a cart.
@@ -51,13 +51,21 @@ export type BuyToolStatus = 'needs_input' | 'needs_card' | 'approval_sent' | 'pl
51
51
  * card. `approval`: the page where they approve this purchase. `store_login`:
52
52
  * the page where they sign in to the store named in `store`, before Agentcard
53
53
  * can buy there. `product`: a product's page at its store, with its `title`,
54
- * `price`, `productId` and `imageUrl`. The page is `url`; `productId` holds no link.
54
+ * `price`, `productId`, `imageUrl` and `images`. The page is `url`; `productId` holds no link.
55
55
  * A flight option is a `product` link too, with a null `url`, since a flight has no page.
56
56
  * `store`: a store's page at the merchant, for a store the shopper can pick, with
57
57
  * its name in `title`, `storeId`, `etaMinutes` and `imageUrl`.
58
58
  */
59
59
  export interface BuyToolLink {
60
60
  kind: 'vault' | 'approval' | 'store_login' | 'product' | 'store';
61
+ /**
62
+ * On a `vault`, `approval` or `store_login` link, a step the shopper takes outside the chat: a stable
63
+ * id for the link (`actionLinkId`), the same each time the same link is handed over, with no part of
64
+ * the URL in it. `actionLinkOutcome(link, message)` says whether a shopper message ended the step,
65
+ * and `actionWindows` from `@agent-cards/commerce-sdk/browser` opens it in a pop-up that closes then.
66
+ * Absent on product and store links.
67
+ */
68
+ id?: string;
61
69
  /**
62
70
  * The page the link opens. Null only on a product link for a product with no page at a store, a
63
71
  * flight option: show its card with no link to open, and let `productId` pick it.
@@ -65,11 +73,19 @@ export interface BuyToolLink {
65
73
  url: string | null;
66
74
  /** When the link stops working, as an ISO 8601 timestamp, or null when Agentcard did not say. Always null on a product link. */
67
75
  expiresAt: string | null;
68
- /** On a store login link: the store's name, or null when Agentcard named none. */
76
+ /**
77
+ * On a store login link: the store's name, or null when Agentcard named none. On an approval link:
78
+ * the store whose cart it approves, when the answer names the cart.
79
+ */
69
80
  store?: string | null;
70
81
  /** On a product link: the product's name. */
71
82
  title?: string;
72
- /** On a product link: the price as the shopper reads it, such as `$12.99`, or null when Agentcard named none. */
83
+ /**
84
+ * On a product link: what taking the product does, so a button can say it. `reserve` is a table the
85
+ * shopper reserves, `book` a flight, room, class or ticket they book, and `buy` anything they buy.
86
+ */
87
+ action?: BuyToolProductAction;
88
+ /** On a product link: the price as the shopper reads it, such as `$12.99`, or `from $10.69` for a product priced by a size or another required choice, or null when Agentcard named none. */
73
89
  price?: string | null;
74
90
  /**
75
91
  * On a product link: Agentcard's id for the product, with no link in it. It is the `product_id` the
@@ -82,6 +98,11 @@ export interface BuyToolLink {
82
98
  * the merchant. Null when Agentcard sent none. Show it beside the title.
83
99
  */
84
100
  imageUrl?: string | null;
101
+ /**
102
+ * On a product link: every photo of the product, `imageUrl` first, at most eight; one entry for a product
103
+ * with one photo and none for a product with none. Show the others as thumbnails that swap the main picture.
104
+ */
105
+ images?: string[];
85
106
  /**
86
107
  * On a store link: Agentcard's id for the store, with no link in it. It is the `store_id` the model
87
108
  * reads for the same store, and the tool takes it as `store_id`, so a "Order here" button can hand it
@@ -106,17 +127,27 @@ export interface BuyToolStore {
106
127
  linkSent: boolean;
107
128
  }
108
129
  /** A product the last search found. */
130
+ /** What taking a product does: `reserve` a table, `book` a flight, room, class or ticket, `buy` anything else. */
131
+ export type BuyToolProductAction = 'buy' | 'reserve' | 'book';
132
+ /** The action a product of `merchant` takes (BuyToolProductAction). */
133
+ export declare function productAction(merchant: string | null | undefined): BuyToolProductAction;
109
134
  export interface BuyToolProduct {
110
135
  flight?: BuyFlightItinerary;
136
+ /** What taking it does: `reserve` a table, `book` a flight, room, class or ticket, `buy` anything else. */
137
+ action: BuyToolProductAction;
111
138
  id: string;
112
139
  /** 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. */
113
140
  ref: string | null;
114
141
  name: string;
115
142
  /** The price in the minor unit of `currency`. */
116
143
  priceCents: number | null;
144
+ /** True when `priceCents` is the lowest price of a product priced by a required choice, which reads `from $10.69`. */
145
+ priceFrom?: true;
117
146
  /** The ISO 4217 code the store prices the product in, or null when Agentcard sent none. */
118
147
  currency: string | null;
119
148
  imageUrl: string | null;
149
+ /** Every photo of the product, `imageUrl` first, at most eight; empty when it has none. */
150
+ images: string[];
120
151
  store: string | null;
121
152
  /** The product's page at its store, or null when Agentcard sent none. */
122
153
  url: string | null;
@@ -152,8 +183,9 @@ export interface BuyToolResult {
152
183
  /** True when Agentcard tells the shopper how the order went, so the shopper need not reply. */
153
184
  shopper_need_not_reply: boolean;
154
185
  /**
155
- * True when `onWorking` delivered this turn's instant reply, such as `got it, finding a taco`, so the
156
- * shopper already heard that the request is being worked on. Absent or false when none went out.
186
+ * True when this turn's acknowledgement reached the shopper: the 👀 `onReaction` put on their message,
187
+ * or the instant reply `onWorking` sent, such as `got it, finding a taco`. The shopper already knows
188
+ * the request is being worked on. Absent or false when none went out.
157
189
  */
158
190
  shopper_already_told_looking?: boolean;
159
191
  conversation_id: string | null;
@@ -164,15 +196,28 @@ export interface BuyToolResult {
164
196
  next_action: BuyNextAction | null;
165
197
  /** 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. */
166
198
  pick: BuyPick | null;
167
- /** The stores the reply presents to choose from, at most three, in the order the reply names them; empty when it presents none. */
199
+ /** The stores the reply presents to choose from, at most eight, in the order the reply names them; empty when it presents none. */
168
200
  stores: BuyToolStore[];
169
201
  /** The store the model's `store_id` picked, or the shopper's `the second one` did, and whether Agentcard selected it; null when none was picked. */
170
202
  store_pick: BuyStorePick | null;
171
203
  /** Every cart hash in this answer. */
172
204
  hashes: string[];
205
+ /**
206
+ * The cart the store's checkout is still building after this answer, or null: its cart and total reach the shopper as a
207
+ * message from Agentcard, and only the shopper's yes to that total confirms it.
208
+ */
209
+ cart_building?: BuyCartBuilding | null;
173
210
  /** Agentcard's answer as it came: the purchase answer, or the body of the refusal. */
174
211
  envelope: Record<string, unknown>;
175
212
  }
213
+ /**
214
+ * How the tool acknowledges a shopper's message while Agentcard works on it:
215
+ * - `reaction`, the default: a 👀 on the shopper's own message, through `onReaction`. No text goes out.
216
+ * - `text`: an instant reply, such as `on it, looking for usb-c cables under $20`, through `onWorking`,
217
+ * for SMS, or any chat that shows no reactions.
218
+ * - `none`: nothing.
219
+ */
220
+ export type BuyAckMode = 'reaction' | 'text' | 'none';
176
221
  export interface BuyToolOptions {
177
222
  /** A client built with your company's credentials. Build it once and share it across every shopper's tool. */
178
223
  agentcard?: AgentcardClient;
@@ -198,9 +243,19 @@ export interface BuyToolOptions {
198
243
  fetch?: FetchLike;
199
244
  /** The pause between two reads of the conversation. Default: a timer. */
200
245
  wait?: (ms: number) => Promise<void>;
246
+ /**
247
+ * Reacts to the shopper's message with `emoji`, always `👀`, to say the tool is on it: show it on the
248
+ * shopper's own bubble, as a tapback. It fires once per message: the moment `buy.turn` opens on a
249
+ * search, or, for a message that got none, such as `yes`, once a call passes `ackDelayMs`. Never on a
250
+ * pick, an address, an answer to a question or a step toward checkout when the turn opens. With it,
251
+ * the tool sends no text acknowledgement at all, unless `ack` is `'text'`. One that throws costs
252
+ * nothing but the reaction.
253
+ */
254
+ onReaction?: (emoji: string) => void | Promise<void>;
201
255
  /**
202
256
  * Sends the shopper an instant reply, such as `on it, looking for usb-c cables under $20`, while
203
- * Agentcard works on a new request. It goes out when Agentcard has not answered within
257
+ * Agentcard works on a new request, when the tool acknowledges in text: with `ack: 'text'`, or with
258
+ * no `onReaction`. It goes out when Agentcard has not answered within
204
259
  * `ackDelayMs`, once for the calls the model makes in one turn. A slow pick gets `adding it now`
205
260
  * for a product or `got it, one moment` for a store or a place in a list. A slow confirm gets
206
261
  * `on it, one moment`. These replies name nothing the shopper said. A code gets no reply.
@@ -208,9 +263,16 @@ export interface BuyToolOptions {
208
263
  * answer. One that throws costs nothing but the instant reply.
209
264
  */
210
265
  onWorking?: (message: string) => void | Promise<void>;
211
- /** `false` sends no instant reply. A function writes it from the shopper's words instead, and an empty answer sends none. A turn whose message `instantAck` sent nothing for gets the built-in reply that names nothing. */
212
- ack?: false | ((ask: string) => string | null | undefined);
213
- /** How long Agentcard has to answer before the instant reply goes out. Default 1500. */
266
+ /**
267
+ * How the tool acknowledges a message (`BuyAckMode`). Left out, it reacts through `onReaction` when
268
+ * the tool or the turn has one, and otherwise sends text through `onWorking`, as 0.9 did. `'text'`
269
+ * sends text even with an `onReaction`, and `'none'`, or `false`, sends nothing. A function is
270
+ * `'text'` with your own words: it writes the instant reply from the shopper's words, and an empty
271
+ * answer sends none. A turn whose message `instantAck` sent nothing for gets the built-in reply that
272
+ * names nothing.
273
+ */
274
+ ack?: BuyAckMode | false | ((ask: string) => string | null | undefined);
275
+ /** How long Agentcard has to answer before the instant reply or the reaction goes out. Default 1500. */
214
276
  ackDelayMs?: number;
215
277
  /**
216
278
  * Called with the whole answer once per call, after its links went out and before the model reads
@@ -239,6 +301,13 @@ export interface BuyToolOptions {
239
301
  * outside the first three.
240
302
  */
241
303
  earlyCards?: boolean;
304
+ /**
305
+ * `true` sets `turn.replyAsWritten` on every turn this tool opens, unless the turn says otherwise:
306
+ * Agentcard's reply, when an answer only asks the shopper something (`replyAsWritten`). Show it as
307
+ * the turn's answer and skip the model call that would reword it. Off by default, so your model
308
+ * writes every answer.
309
+ */
310
+ replyAsWritten?: boolean;
242
311
  }
243
312
  /**
244
313
  * The buy tool, in the shape every agent framework takes, because their field
@@ -292,6 +361,14 @@ export interface BuyTool {
292
361
  * message that picks nothing, such as `yes`, a slow call still sends `onWorking` a reply that names nothing.
293
362
  */
294
363
  instantAck(shopperText: string): string | null;
364
+ /**
365
+ * The reaction for a shopper's message, `👀`, or null for one that starts no search, read as
366
+ * `instantAck` reads it. Call it the moment the message arrives, before your model runs, and put the
367
+ * emoji it returns on the shopper's message. When it returns one, the tool sends no reaction or text of
368
+ * its own for that turn, and the model reads `shopper_already_told_looking`. With `buy.turn` and
369
+ * `onReaction`, the turn does this for you.
370
+ */
371
+ instantReaction(shopperText: string): string | null;
295
372
  /**
296
373
  * Opens the turn of one shopper message and returns the tool for that turn: pass it to your model in
297
374
  * place of the tool, as `tools: [turn]`. Each call the model makes through it hands its links to this
@@ -299,10 +376,31 @@ export interface BuyTool {
299
376
  * keeps it in `turn.results`, and sends this turn's instant reply to its `onWorking`. A shopper who
300
377
  * sends a second message while the first still runs gets a second turn, and no link, cart or reply
301
378
  * of one reaches the other. Call it the moment the message arrives, before your model runs: with an
302
- * `onWorking`, the instant reply `instantAck` would return goes out at once. An option left out falls
303
- * back to the one the tool was built with.
379
+ * `onReaction`, a search gets its 👀 at once, and with only an `onWorking`, the instant reply
380
+ * `instantAck` would return goes out at once. An option left out falls back to the one the tool was
381
+ * built with.
304
382
  */
305
383
  turn(shopperText: string, options?: BuyToolTurnOptions): BuyToolTurn;
384
+ /**
385
+ * Opens the turn your agent takes on its own once the shopper finished a step outside the chat, such
386
+ * as saving a card or signing in to the store: a message whose `advances` is true. Pass it to your
387
+ * model as you pass `buy.turn`, with `turn.messages(history)` as the messages, and the model's answer
388
+ * is the shopper's next message, the step after the one they took, so they never have to say "ok"
389
+ * or "done" first. Post that answer in place of `message.text`, which the shopper then never reads.
390
+ *
391
+ * The turn moves the purchase only up to the shopper's own decision. Its calls never reach Agentcard
392
+ * with an ask, so nothing in it can place an order, confirm a cart or charge a card for the shopper.
393
+ * Each call reads where the purchase stands and answers with the next step: the card link, or the
394
+ * cart and the ask for the shopper's yes. The one exception is the yes the shopper already gave in the
395
+ * chat for this exact cart, when that confirm stopped for the step they just finished: it goes in for
396
+ * that cart's hash alone, and Agentcard refuses it if the cart changed.
397
+ *
398
+ * Returns null when there is no next step to take: a message whose `advances` is false, a message
399
+ * for another shopper or one whose link closed, a message this tool already took a turn for, and a
400
+ * message that arrives while a call for the shopper's own message is still running, since that
401
+ * call's answer tells the model where the purchase stands. Post `message.text` then, as before.
402
+ */
403
+ nextStep(message: ShopperMessage, options?: BuyToolTurnOptions): BuyToolTurn | null;
306
404
  }
307
405
  /**
308
406
  * One thing a turn hands the shopper: a message, such as the instant reply or a paragraph your model
@@ -321,28 +419,43 @@ export interface BuyToolTurnOptions {
321
419
  * Hears everything the turn hands the shopper, one item at a time and in the order it happened: the
322
420
  * instant reply and each message your model writes through `turn.messageStream()` or `turn.say()` as
323
421
  * `text` items, and each link as a `link` item. Show each item below the one before it, and the chat
324
- * reads in the order it happened: a search's cards, then the model's words about them. Each item
422
+ * reads in the order it happened: a search's cards, then the model's words about them. A step link
423
+ * (`vault`, `approval`, `store_login`) is the one exception: it waits for the message that asks for
424
+ * it, and goes out right after that message, so the shopper reads the ask, then taps. Each item
325
425
  * waits for the one before it to settle. With `onItem`, the turn hands no link to a `sendLink` and
326
426
  * no instant reply to an `onWorking`, the tool's included: pass `onItem`, or those, to the turn.
327
427
  */
328
428
  onItem?: (item: BuyToolTurnItem) => void | Promise<void>;
329
429
  /**
330
- * With `onItem`: spaces the turn's messages like someone texting. A message waits until
331
- * `typingDelay` of it has passed since the item before it went out, so the shopper reads one message
332
- * at a time. Time the model spent writing counts toward the pause. Links never wait. Default `true`;
333
- * `false` hands each message over the moment it is ready, and a function returns the pause in
334
- * milliseconds for a message.
430
+ * With `onItem` or `onMessage`: how the turn's messages are spaced. Default `false`: each message goes
431
+ * out the moment it is ready. `true` spaces them like someone texting: a message waits until
432
+ * `typingDelay` of it has passed since the item before it went out, and time the model spent writing
433
+ * counts toward the pause. A function returns the pause in milliseconds for a message. Links never wait.
335
434
  */
336
435
  pace?: MessagePace;
337
- /** With `onItem`: hears `true` when the pause before a message starts and `false` the moment that message goes out, for a typing indicator. */
436
+ /**
437
+ * `true` sets `turn.replyAsWritten` when an answer only asks the shopper something; `false` leaves it
438
+ * null, so your model rewords every answer. Default: the tool's `replyAsWritten`.
439
+ */
440
+ replyAsWritten?: boolean;
441
+ /** With `onItem` or `onMessage`: hears `true` when the pause before a message starts and `false` the moment that message goes out, for a typing indicator. */
338
442
  onTyping?: (typing: boolean) => void;
443
+ /**
444
+ * Without `onItem`: hears each message your model writes through `turn.messageStream()` or
445
+ * `turn.say()`, paced by `pace`, in the same order as the links `sendLink` hears. Write the model's
446
+ * text through the turn, and a step link (`vault`, `approval`, `store_login`) reaches `sendLink`
447
+ * after the message that asks for it, never before.
448
+ */
449
+ onMessage?: (message: string) => void | Promise<void>;
339
450
  /**
340
451
  * Delivers a link of this turn to the shopper, as the tool's `sendLink` does. Each call waits for it.
341
452
  * Without one, `turn.links` still takes each link, and the model reads that it went out, so return
342
453
  * `turn.links` beside the turn's reply.
343
454
  */
344
455
  sendLink?: (link: BuyToolLink) => void | Promise<void>;
345
- /** Sends this turn's instant reply: at once when `turn` opens on a new request, or once a slow call passes `ackDelayMs`. */
456
+ /** Reacts 👀 to this turn's message: at once when `turn` opens on a new request, or once a slow call passes `ackDelayMs`. */
457
+ onReaction?: (emoji: string) => void | Promise<void>;
458
+ /** Sends this turn's instant reply in text, when the tool acknowledges in text: at once when `turn` opens on a new request, or once a slow call passes `ackDelayMs`. */
346
459
  onWorking?: (message: string) => void | Promise<void>;
347
460
  /** Called with the whole answer of each call this turn makes, after its links went out and before the model reads it. */
348
461
  onResult?: (result: BuyToolResult) => void | Promise<void>;
@@ -355,9 +468,19 @@ export interface BuyToolTurnOptions {
355
468
  * belong to this turn alone. `ack`, `links` and `results` are read-only, and JSON leaves them out, so a
356
469
  * tool runner that sends the tool to its API sends the same fields as before.
357
470
  */
358
- export interface BuyToolTurn extends Omit<BuyTool, 'instantAck' | 'turn'> {
359
- /** The instant reply this turn sent to `onWorking` when it opened, or null when it sent none. */
471
+ export interface BuyToolTurn extends Omit<BuyTool, 'instantAck' | 'instantReaction' | 'turn' | 'nextStep'> {
472
+ /** For a turn `buy.nextStep` opened, the message whose step the shopper finished; null for a turn of a shopper message. */
473
+ readonly event: ShopperMessage | null;
474
+ /**
475
+ * For a turn `buy.nextStep` opened that your agent could not finish: call it before you post `message.text` instead.
476
+ * The message's text then reaches later turns as words the shopper read (`agentcard.delivered`), and a later
477
+ * `buy.nextStep` for the same event opens a turn again, as on a webhook's redelivery. Does nothing on another turn.
478
+ */
479
+ release(): void;
480
+ /** The instant reply this turn sent to `onWorking` when it opened, or null when it sent none, as with a reaction. */
360
481
  readonly ack: string | null;
482
+ /** The reaction this turn put on the shopper's message, `👀`, when it opened or once a call ran slow, or null while it put none or when `onReaction` threw. */
483
+ readonly reaction: string | null;
361
484
  /**
362
485
  * The messages Agentcard posted into this shopper's chat that the model has not read yet, oldest
363
486
  * first, such as `card_added` once they saved a card: each one `agentcard.listen`'s or
@@ -383,6 +506,15 @@ export interface BuyToolTurn extends Omit<BuyTool, 'instantAck' | 'turn'> {
383
506
  readonly links: BuyToolLink[];
384
507
  /** The whole answer of each call this turn made, in the order the calls returned. Each read returns a new array. */
385
508
  readonly results: BuyToolResult[];
509
+ /**
510
+ * With `replyAsWritten` on: Agentcard's reply when the turn's last answer only asks the shopper
511
+ * something, such as a product list, a size or a cart's total and the ask for their yes, with every
512
+ * link taken out. Read it after the tool runs. When it is set, show it with `say()` and end the turn
513
+ * without calling your model again: the reply already says what the model would. Null when the option
514
+ * is off, before any call, after an answer your model has to word, and after a line the tool wrote
515
+ * itself for your model, such as the note that the cart changed or a `nextStep` turn's ask for the yes.
516
+ */
517
+ readonly replyAsWritten: string | null;
386
518
  /**
387
519
  * Every message and link the turn handed over, in the order they went out: the instant reply, each
388
520
  * link, and each message written through `messageStream()` or `say()`. An item its send threw on is
@@ -392,10 +524,14 @@ export interface BuyToolTurn extends Omit<BuyTool, 'instantAck' | 'turn'> {
392
524
  /**
393
525
  * A message stream whose messages become this turn's `text` items, after the items before them, as
394
526
  * `messageStream` cuts them: push your model's text as it streams, then `end` it. Start one for each
395
- * model message. With `onItem`, the turn spaces them by its `pace`.
527
+ * model message. With `onItem`, the turn spaces them by its `pace`. `end` resolves once the messages and the step links
528
+ * held for them went out, and rejects with the first that failed to send.
396
529
  */
397
530
  messageStream(): MessageStream;
398
- /** Cuts a whole text into messages, as `toMessages` does, and hands each one over as a `text` item. Resolves once every one went out. */
531
+ /**
532
+ * Cuts a whole text into messages, as `toMessages` does, and hands each one over as a `text` item. Resolves once every
533
+ * one, and every step link held for it, went out, and rejects with the first that failed to send.
534
+ */
399
535
  say(text: string): Promise<void>;
400
536
  }
401
537
  /** A message in the one shape the Anthropic, OpenAI and Vercel AI SDK message lists all take. */
@@ -413,6 +549,12 @@ export type BuyToolTurnMessage = {
413
549
  * or an OpenAI Responses item, they come as an assistant message of their own.
414
550
  */
415
551
  export declare function turnMessages<M extends object>(history: readonly M[] | null | undefined, told: readonly string[], shopperText: string): Array<M | BuyToolTurnMessage>;
552
+ /**
553
+ * The words a `nextStep` turn hands the model as the user's message: the event, marked as no one's
554
+ * typing, and the instruction to take the next step now in one short message, without the filler of
555
+ * a step announced and a question about what comes next.
556
+ */
557
+ export declare function nextStepPrompt(message: ShopperMessage): string;
416
558
  /** Shared with the merchant monitor so link eligibility uses the tool's product mapping. */
417
559
  export declare function productsOf(turn: BuyTurn): BuyToolProduct[];
418
560
  /**