@agent-cards/commerce-sdk 0.11.0 → 0.14.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,12 @@
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 BuyTurnStatus, 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
+ import { type DirectCall } from './direct.js';
36
37
  import { type BuyToolModelView } from './model-view.js';
37
38
  import { type MessagePace, type MessageStream } from './messages.js';
38
- import type { ShopperMessage } from './shopper-message.js';
39
+ import { type ShopperMessage } from './shopper-message.js';
39
40
  /**
40
41
  * Where the purchase stands:
41
42
  * - `needs_input`: Agentcard asks the shopper something, or shows a cart.
@@ -51,13 +52,21 @@ export type BuyToolStatus = 'needs_input' | 'needs_card' | 'approval_sent' | 'pl
51
52
  * card. `approval`: the page where they approve this purchase. `store_login`:
52
53
  * the page where they sign in to the store named in `store`, before Agentcard
53
54
  * 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.
55
+ * `price`, `productId`, `imageUrl` and `images`. The page is `url`; `productId` holds no link.
55
56
  * A flight option is a `product` link too, with a null `url`, since a flight has no page.
56
57
  * `store`: a store's page at the merchant, for a store the shopper can pick, with
57
58
  * its name in `title`, `storeId`, `etaMinutes` and `imageUrl`.
58
59
  */
59
60
  export interface BuyToolLink {
60
61
  kind: 'vault' | 'approval' | 'store_login' | 'product' | 'store';
62
+ /**
63
+ * On a `vault`, `approval` or `store_login` link, a step the shopper takes outside the chat: a stable
64
+ * id for the link (`actionLinkId`), the same each time the same link is handed over, with no part of
65
+ * the URL in it. `actionLinkOutcome(link, message)` says whether a shopper message ended the step,
66
+ * and `actionWindows` from `@agent-cards/commerce-sdk/browser` opens it in a pop-up that closes then.
67
+ * Absent on product and store links.
68
+ */
69
+ id?: string;
61
70
  /**
62
71
  * The page the link opens. Null only on a product link for a product with no page at a store, a
63
72
  * flight option: show its card with no link to open, and let `productId` pick it.
@@ -65,11 +74,19 @@ export interface BuyToolLink {
65
74
  url: string | null;
66
75
  /** When the link stops working, as an ISO 8601 timestamp, or null when Agentcard did not say. Always null on a product link. */
67
76
  expiresAt: string | null;
68
- /** On a store login link: the store's name, or null when Agentcard named none. */
77
+ /**
78
+ * On a store login link: the store's name, or null when Agentcard named none. On an approval link:
79
+ * the store whose cart it approves, when the answer names the cart.
80
+ */
69
81
  store?: string | null;
70
82
  /** On a product link: the product's name. */
71
83
  title?: string;
72
- /** On a product link: the price as the shopper reads it, such as `$12.99`, or null when Agentcard named none. */
84
+ /**
85
+ * On a product link: what taking the product does, so a button can say it. `reserve` is a table the
86
+ * shopper reserves, `book` a flight, room, class or ticket they book, and `buy` anything they buy.
87
+ */
88
+ action?: BuyToolProductAction;
89
+ /** 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
90
  price?: string | null;
74
91
  /**
75
92
  * On a product link: Agentcard's id for the product, with no link in it. It is the `product_id` the
@@ -82,6 +99,11 @@ export interface BuyToolLink {
82
99
  * the merchant. Null when Agentcard sent none. Show it beside the title.
83
100
  */
84
101
  imageUrl?: string | null;
102
+ /**
103
+ * On a product link: every photo of the product, `imageUrl` first, at most eight; one entry for a product
104
+ * with one photo and none for a product with none. Show the others as thumbnails that swap the main picture.
105
+ */
106
+ images?: string[];
85
107
  /**
86
108
  * On a store link: Agentcard's id for the store, with no link in it. It is the `store_id` the model
87
109
  * reads for the same store, and the tool takes it as `store_id`, so a "Order here" button can hand it
@@ -106,17 +128,27 @@ export interface BuyToolStore {
106
128
  linkSent: boolean;
107
129
  }
108
130
  /** A product the last search found. */
131
+ /** What taking a product does: `reserve` a table, `book` a flight, room, class or ticket, `buy` anything else. */
132
+ export type BuyToolProductAction = 'buy' | 'reserve' | 'book';
133
+ /** The action a product of `merchant` takes (BuyToolProductAction). */
134
+ export declare function productAction(merchant: string | null | undefined): BuyToolProductAction;
109
135
  export interface BuyToolProduct {
110
136
  flight?: BuyFlightItinerary;
137
+ /** What taking it does: `reserve` a table, `book` a flight, room, class or ticket, `buy` anything else. */
138
+ action: BuyToolProductAction;
111
139
  id: string;
112
140
  /** 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
141
  ref: string | null;
114
142
  name: string;
115
143
  /** The price in the minor unit of `currency`. */
116
144
  priceCents: number | null;
145
+ /** True when `priceCents` is the lowest price of a product priced by a required choice, which reads `from $10.69`. */
146
+ priceFrom?: true;
117
147
  /** The ISO 4217 code the store prices the product in, or null when Agentcard sent none. */
118
148
  currency: string | null;
119
149
  imageUrl: string | null;
150
+ /** Every photo of the product, `imageUrl` first, at most eight; empty when it has none. */
151
+ images: string[];
120
152
  store: string | null;
121
153
  /** The product's page at its store, or null when Agentcard sent none. */
122
154
  url: string | null;
@@ -157,6 +189,11 @@ export interface BuyToolResult {
157
189
  * the request is being worked on. Absent or false when none went out.
158
190
  */
159
191
  shopper_already_told_looking?: boolean;
192
+ /**
193
+ * True when every message of Agentcard's answer reached the shopper through `onText` as it was written, so the shopper
194
+ * already read `reply`. Absent or false when any message did not go out that way, and when the tool wrote the reply.
195
+ */
196
+ shopper_already_read_reply?: boolean;
160
197
  conversation_id: string | null;
161
198
  order_id: string | null;
162
199
  decline_code: string | null;
@@ -165,12 +202,17 @@ export interface BuyToolResult {
165
202
  next_action: BuyNextAction | null;
166
203
  /** 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. */
167
204
  pick: BuyPick | null;
168
- /** The stores the reply presents to choose from, at most three, in the order the reply names them; empty when it presents none. */
205
+ /** The stores the reply presents to choose from, at most eight, in the order the reply names them; empty when it presents none. */
169
206
  stores: BuyToolStore[];
170
207
  /** 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. */
171
208
  store_pick: BuyStorePick | null;
172
209
  /** Every cart hash in this answer. */
173
210
  hashes: string[];
211
+ /**
212
+ * The cart the store's checkout is still building after this answer, or null: its cart and total reach the shopper as a
213
+ * message from Agentcard, and only the shopper's yes to that total confirms it.
214
+ */
215
+ cart_building?: BuyCartBuilding | null;
174
216
  /** Agentcard's answer as it came: the purchase answer, or the body of the refusal. */
175
217
  envelope: Record<string, unknown>;
176
218
  }
@@ -182,6 +224,18 @@ export interface BuyToolResult {
182
224
  * - `none`: nothing.
183
225
  */
184
226
  export type BuyAckMode = 'reaction' | 'text' | 'none';
227
+ /**
228
+ * How many products a turn's searches found, sent each time the count grows, after the cards it counts: `shown` cards of
229
+ * `found` products, "8 of 23 shown". `more` holds the products found past the cards, in the order they were found, as
230
+ * product links the app reveals on "show more" with no new search; the model never reads them. Find mode sends a count
231
+ * with no `more`.
232
+ */
233
+ export interface BuyToolFound {
234
+ conversationId: string;
235
+ shown: number;
236
+ found: number;
237
+ more: BuyToolLink[];
238
+ }
185
239
  export interface BuyToolOptions {
186
240
  /** A client built with your company's credentials. Build it once and share it across every shopper's tool. */
187
241
  agentcard?: AgentcardClient;
@@ -252,19 +306,75 @@ export interface BuyToolOptions {
252
306
  */
253
307
  onProgress?: (progress: BuyProgress) => void;
254
308
  /**
255
- * `false` waits for each whole answer. With `onProgress`, or with `earlyCards` and `sendLink`, the
256
- * tool asks Agentcard to stream the call, and each stage goes to `onProgress` as it happens.
309
+ * Called the moment Agentcard starts a step of a call, with a short line to show in place of loading dots, such as
310
+ * `searching Target and Walmart` or `adding it to your cart`. No status comes for a call that only writes its reply, so
311
+ * keep the dots until the first one. One that throws costs nothing but the call to it.
312
+ */
313
+ onStatus?: (status: BuyTurnStatus) => void;
314
+ /**
315
+ * Hears how many products a call's searches found, each time the count grows (`BuyToolFound`), right after the cards it
316
+ * counts went out through `sendLink`. It needs `earlyCards`, the default: with the cards held for the answer, no count
317
+ * comes. One that throws costs nothing but the call to it.
318
+ */
319
+ onFound?: (found: BuyToolFound) => void | Promise<void>;
320
+ /**
321
+ * Hears the products a call's search found, the moment the search answers and before Agentcard writes its reply,
322
+ * with the conversation, when the search ran (`asOf`, as the answer's `catalog.as_of` names it) and the store's name.
323
+ * Show them as the answer's cards. Like `sendLink`, it hears every call; pass it to `buy.turn` to hear only the calls
324
+ * of one turn. One that throws costs nothing but the call to it.
325
+ */
326
+ onProducts?: (products: BuyToolProduct[], search: {
327
+ conversationId: string;
328
+ asOf: string | null;
329
+ merchantName: string;
330
+ }) => void | Promise<void>;
331
+ /**
332
+ * Hears each message Agentcard writes for the shopper, the moment it is written, links included, as `result.reply`
333
+ * and `result.messages` hold them. A message that lands before the instant reply was due quiets it, and one after it
334
+ * follows it. When every message of an answer went out through it, the model reads `shopper_already_read_reply` and
335
+ * adds nothing to them. One that throws leaves that flag false, so the model words the answer.
336
+ */
337
+ onText?: (text: string, info: {
338
+ conversationId: string | null;
339
+ }) => void | Promise<void>;
340
+ /**
341
+ * Hears the carts each time a call builds or changes one, before the message that announces it: the cart a confirm
342
+ * would place, or null, and every cart on the table. One that throws costs nothing but the call to it.
343
+ */
344
+ onCart?: (cart: BuyCart | null, info: {
345
+ carts: BuyCart[];
346
+ conversationId: string;
347
+ }) => void | Promise<void>;
348
+ /**
349
+ * `false` waits for each whole answer, and a search's cards go out with the answer that names them. Left out, the
350
+ * tool streams each call: the stages reach `onProgress`, the products `onProducts` and their cards `sendLink` the
351
+ * moment the search answers, each message `onText`, and the carts `onCart`, as Agentcard produces them. A call with
352
+ * none of these to hear it asks for the whole answer.
257
353
  */
258
354
  stream?: false;
259
355
  /**
260
- * `true` sends the store pages of a search's first three products to `sendLink` the moment the search
261
- * answers, before Agentcard writes its reply, and the answer then sends only the pages it adds. Off by
262
- * default: the reply often recommends other products than the search's first three, so a shopper
263
- * would hold cards the reply never names. Measured on five live searches on 2026-10-20, the early
264
- * pages arrived 2.2 to 3.3 seconds before the answer, and in four of them the reply named a product
265
- * outside the first three.
356
+ * Default `true`: the store page of every product a search found, at most eight, goes to `sendLink` the moment the
357
+ * search answers, before Agentcard writes its reply, and the answer then sends only the pages it adds. Measured on
358
+ * five live searches on 2026-10-20, the early pages arrived 2.2 to 3.3 seconds before the answer. `false` waits for
359
+ * the answer and sends only the cards it names: the reply often names only some of the search's products, so early
360
+ * cards can include ones the reply never names.
266
361
  */
267
362
  earlyCards?: boolean;
363
+ /**
364
+ * `true` sets `turn.replyAsWritten` on every turn this tool opens, unless the turn says otherwise:
365
+ * Agentcard's reply, when an answer only asks the shopper something (`replyAsWritten`). Show it as
366
+ * the turn's answer and skip the model call that would reword it. Off by default, so your model
367
+ * writes every answer.
368
+ */
369
+ replyAsWritten?: boolean;
370
+ /**
371
+ * `true` reads each shopper message `buy.turn` opens by rule, with no model, and sets `turn.direct` to the call
372
+ * it makes straight to Agentcard when the message is a request to buy, a pick of what Agentcard just showed, or
373
+ * the answer to the question Agentcard just asked (`directCall`). Send it with `turn.run(turn.direct.input)` and
374
+ * skip your model's first call; every other message, small talk and questions among them, leaves `turn.direct`
375
+ * null for your model to decide. Off by default, so your model decides every call.
376
+ */
377
+ direct?: boolean;
268
378
  }
269
379
  /**
270
380
  * The buy tool, in the shape every agent framework takes, because their field
@@ -338,6 +448,26 @@ export interface BuyTool {
338
448
  * built with.
339
449
  */
340
450
  turn(shopperText: string, options?: BuyToolTurnOptions): BuyToolTurn;
451
+ /**
452
+ * Opens the turn your agent takes on its own once the shopper finished a step outside the chat, such
453
+ * as saving a card or signing in to the store: a message whose `advances` is true. Pass it to your
454
+ * model as you pass `buy.turn`, with `turn.messages(history)` as the messages, and the model's answer
455
+ * is the shopper's next message, the step after the one they took, so they never have to say "ok"
456
+ * or "done" first. Post that answer in place of `message.text`, which the shopper then never reads.
457
+ *
458
+ * The turn moves the purchase only up to the shopper's own decision. Its calls never reach Agentcard
459
+ * with an ask, so nothing in it can place an order, confirm a cart or charge a card for the shopper.
460
+ * Each call reads where the purchase stands and answers with the next step: the card link, or the
461
+ * cart and the ask for the shopper's yes. The one exception is the yes the shopper already gave in the
462
+ * chat for this exact cart, when that confirm stopped for the step they just finished: it goes in for
463
+ * that cart's hash alone, and Agentcard refuses it if the cart changed.
464
+ *
465
+ * Returns null when there is no next step to take: a message whose `advances` is false, a message
466
+ * for another shopper or one whose link closed, a message this tool already took a turn for, and a
467
+ * message that arrives while a call for the shopper's own message is still running, since that
468
+ * call's answer tells the model where the purchase stands. Post `message.text` then, as before.
469
+ */
470
+ nextStep(message: ShopperMessage, options?: BuyToolTurnOptions): BuyToolTurn | null;
341
471
  }
342
472
  /**
343
473
  * One thing a turn hands the shopper: a message, such as the instant reply or a paragraph your model
@@ -356,21 +486,34 @@ export interface BuyToolTurnOptions {
356
486
  * Hears everything the turn hands the shopper, one item at a time and in the order it happened: the
357
487
  * instant reply and each message your model writes through `turn.messageStream()` or `turn.say()` as
358
488
  * `text` items, and each link as a `link` item. Show each item below the one before it, and the chat
359
- * reads in the order it happened: a search's cards, then the model's words about them. Each item
489
+ * reads in the order it happened: a search's cards, then the model's words about them. A step link
490
+ * (`vault`, `approval`, `store_login`) is the one exception: it waits for the message that asks for
491
+ * it, and goes out right after that message, so the shopper reads the ask, then taps. Each item
360
492
  * waits for the one before it to settle. With `onItem`, the turn hands no link to a `sendLink` and
361
493
  * no instant reply to an `onWorking`, the tool's included: pass `onItem`, or those, to the turn.
362
494
  */
363
495
  onItem?: (item: BuyToolTurnItem) => void | Promise<void>;
364
496
  /**
365
- * With `onItem`: spaces the turn's messages like someone texting. A message waits until
366
- * `typingDelay` of it has passed since the item before it went out, so the shopper reads one message
367
- * at a time. Time the model spent writing counts toward the pause. Links never wait. Default `true`;
368
- * `false` hands each message over the moment it is ready, and a function returns the pause in
369
- * milliseconds for a message.
497
+ * With `onItem`, `onMessage` or `onText`: how the turn's messages are spaced. Default `false`: each message goes
498
+ * out the moment it is ready. `true` spaces them like someone texting: a message waits until
499
+ * `typingDelay` of it has passed since the item before it went out, and time the model spent writing
500
+ * counts toward the pause. A function returns the pause in milliseconds for a message. Links never wait.
370
501
  */
371
502
  pace?: MessagePace;
372
- /** With `onItem`: hears `true` when the pause before a message starts and `false` the moment that message goes out, for a typing indicator. */
503
+ /**
504
+ * `true` sets `turn.replyAsWritten` when an answer only asks the shopper something; `false` leaves it
505
+ * null, so your model rewords every answer. Default: the tool's `replyAsWritten`.
506
+ */
507
+ replyAsWritten?: boolean;
508
+ /** With `onItem`, `onMessage` or `onText`: hears `true` when the pause before a message starts and `false` the moment that message goes out, for a typing indicator. */
373
509
  onTyping?: (typing: boolean) => void;
510
+ /**
511
+ * Without `onItem`: hears each message your model writes through `turn.messageStream()` or
512
+ * `turn.say()`, paced by `pace`, in the same order as the links `sendLink` hears. Write the model's
513
+ * text through the turn, and a step link (`vault`, `approval`, `store_login`) reaches `sendLink`
514
+ * after the message that asks for it, never before.
515
+ */
516
+ onMessage?: (message: string) => void | Promise<void>;
374
517
  /**
375
518
  * Delivers a link of this turn to the shopper, as the tool's `sendLink` does. Each call waits for it.
376
519
  * Without one, `turn.links` still takes each link, and the model reads that it went out, so return
@@ -385,6 +528,20 @@ export interface BuyToolTurnOptions {
385
528
  onResult?: (result: BuyToolResult) => void | Promise<void>;
386
529
  /** Called with each stage Agentcard reaches on a call this turn makes. */
387
530
  onProgress?: (progress: BuyProgress) => void;
531
+ /** Called with each step Agentcard starts on a call this turn makes, as the tool's `onStatus` is. */
532
+ onStatus?: BuyToolOptions['onStatus'];
533
+ /** Hears how many products this turn's calls found, as the tool's `onFound` does. */
534
+ onFound?: BuyToolOptions['onFound'];
535
+ /** Hears the products each search of this turn's calls found, the moment it answers, as the tool's `onProducts` does. */
536
+ onProducts?: BuyToolOptions['onProducts'];
537
+ /**
538
+ * Hears each message Agentcard writes for this turn's calls, as the tool's `onText` does, in its place in the turn:
539
+ * after the instant reply and every card handed over before it, paced by `pace`. `turn.items` lists each one as a
540
+ * `text` item, and `onItem` does not hear it.
541
+ */
542
+ onText?: BuyToolOptions['onText'];
543
+ /** Hears the carts each time a call of this turn builds or changes one, as the tool's `onCart` does. */
544
+ onCart?: BuyToolOptions['onCart'];
388
545
  }
389
546
  /**
390
547
  * The buy tool for one shopper message, from `buy.turn`. It is the same tool, with the same name,
@@ -392,7 +549,15 @@ export interface BuyToolTurnOptions {
392
549
  * belong to this turn alone. `ack`, `links` and `results` are read-only, and JSON leaves them out, so a
393
550
  * tool runner that sends the tool to its API sends the same fields as before.
394
551
  */
395
- export interface BuyToolTurn extends Omit<BuyTool, 'instantAck' | 'instantReaction' | 'turn'> {
552
+ export interface BuyToolTurn extends Omit<BuyTool, 'instantAck' | 'instantReaction' | 'turn' | 'nextStep'> {
553
+ /** For a turn `buy.nextStep` opened, the message whose step the shopper finished; null for a turn of a shopper message. */
554
+ readonly event: ShopperMessage | null;
555
+ /**
556
+ * For a turn `buy.nextStep` opened that your agent could not finish: call it before you post `message.text` instead.
557
+ * The message's text then reaches later turns as words the shopper read (`agentcard.delivered`), and a later
558
+ * `buy.nextStep` for the same event opens a turn again, as on a webhook's redelivery. Does nothing on another turn.
559
+ */
560
+ release(): void;
396
561
  /** The instant reply this turn sent to `onWorking` when it opened, or null when it sent none, as with a reaction. */
397
562
  readonly ack: string | null;
398
563
  /** 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. */
@@ -422,19 +587,40 @@ export interface BuyToolTurn extends Omit<BuyTool, 'instantAck' | 'instantReacti
422
587
  readonly links: BuyToolLink[];
423
588
  /** The whole answer of each call this turn made, in the order the calls returned. Each read returns a new array. */
424
589
  readonly results: BuyToolResult[];
590
+ /**
591
+ * With `replyAsWritten` on: Agentcard's reply when the turn's last answer only asks the shopper
592
+ * something, such as a product list, a size or a cart's total and the ask for their yes, with every
593
+ * link taken out. Read it after the tool runs. When it is set, show it with `say()` and end the turn
594
+ * without calling your model again: the reply already says what the model would. Null when the option
595
+ * is off, before any call, after an answer your model has to word, and after a line the tool wrote
596
+ * itself for your model, such as the note that the cart changed or a `nextStep` turn's ask for the yes.
597
+ */
598
+ readonly replyAsWritten: string | null;
599
+ /**
600
+ * With `direct` on: the call this turn's message makes straight to Agentcard, with no model deciding it, and why,
601
+ * or null when your model decides. It is set when the turn opens, from what Agentcard last asked the shopper.
602
+ * Send it with `run(direct.input)`, which returns what your model would read, then show `replyAsWritten` when it
603
+ * is set; when it is null, hand your model the call and its result, and its words answer the shopper.
604
+ */
605
+ readonly direct: DirectCall | null;
425
606
  /**
426
607
  * Every message and link the turn handed over, in the order they went out: the instant reply, each
427
- * link, and each message written through `messageStream()` or `say()`. An item its send threw on is
428
- * left out. Return it as the turn's answer, and an app renders it in order. Each read returns a new array.
608
+ * link, each message of Agentcard's that `onText` took, and each message written through `messageStream()`
609
+ * or `say()`. An item its send threw on is left out. Return it as the turn's answer, and an app renders it
610
+ * in order. Each read returns a new array.
429
611
  */
430
612
  readonly items: BuyToolTurnItem[];
431
613
  /**
432
614
  * A message stream whose messages become this turn's `text` items, after the items before them, as
433
615
  * `messageStream` cuts them: push your model's text as it streams, then `end` it. Start one for each
434
- * model message. With `onItem`, the turn spaces them by its `pace`.
616
+ * model message. With `onItem`, the turn spaces them by its `pace`. `end` resolves once the messages and the step links
617
+ * held for them went out, and rejects with the first that failed to send.
435
618
  */
436
619
  messageStream(): MessageStream;
437
- /** Cuts a whole text into messages, as `toMessages` does, and hands each one over as a `text` item. Resolves once every one went out. */
620
+ /**
621
+ * Cuts a whole text into messages, as `toMessages` does, and hands each one over as a `text` item. Resolves once every
622
+ * one, and every step link held for it, went out, and rejects with the first that failed to send.
623
+ */
438
624
  say(text: string): Promise<void>;
439
625
  }
440
626
  /** A message in the one shape the Anthropic, OpenAI and Vercel AI SDK message lists all take. */
@@ -452,6 +638,12 @@ export type BuyToolTurnMessage = {
452
638
  * or an OpenAI Responses item, they come as an assistant message of their own.
453
639
  */
454
640
  export declare function turnMessages<M extends object>(history: readonly M[] | null | undefined, told: readonly string[], shopperText: string): Array<M | BuyToolTurnMessage>;
641
+ /**
642
+ * The words a `nextStep` turn hands the model as the user's message: the event, marked as no one's
643
+ * typing, and the instruction to take the next step now in one short message, without the filler of
644
+ * a step announced and a question about what comes next.
645
+ */
646
+ export declare function nextStepPrompt(message: ShopperMessage): string;
455
647
  /** Shared with the merchant monitor so link eligibility uses the tool's product mapping. */
456
648
  export declare function productsOf(turn: BuyTurn): BuyToolProduct[];
457
649
  /**