@agent-cards/commerce-sdk 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/basket.d.ts +14 -3
- package/dist/basket.js +3 -1
- package/dist/buy-tool.d.ts +9 -0
- package/dist/buy-tool.js +83 -6
- package/dist/client.d.ts +9 -0
- package/dist/client.js +6 -0
- package/dist/direct.d.ts +3 -1
- package/dist/direct.js +28 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/message.d.ts +19 -0
- package/dist/message.js +6 -0
- package/dist/model-view.d.ts +11 -2
- package/dist/model-view.js +13 -4
- package/package.json +1 -1
package/dist/basket.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A basket on a purchase answer: the carts at several sellers that one approval pays, each seller charging the card
|
|
3
|
-
* separately for
|
|
3
|
+
* separately for exactly its line, or up to its hold for a store that charges its final amount (`held`). `approval_url` is the one link the shopper approves on while the basket waits for
|
|
4
4
|
* that approval or pays under it. Null on an answer with no basket, and on an older API.
|
|
5
5
|
*
|
|
6
6
|
* A basket whose stores reach their payment step after this answer has no link yet: the link then arrives as a
|
|
@@ -17,15 +17,26 @@ export interface BuyBasketLine {
|
|
|
17
17
|
/** The seller as the shopper knows it: the store or the airline. */
|
|
18
18
|
seller: string;
|
|
19
19
|
status: BuyBasketLineStatus;
|
|
20
|
+
/** The seller's cart total, the amount to show for this line. */
|
|
20
21
|
total_cents: number;
|
|
21
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* The most this seller may charge. It equals total_cents for a seller that quotes its final price: that seller is paid
|
|
24
|
+
* exactly the amount the shopper approved. A store that charges its final amount when the order is placed holds the
|
|
25
|
+
* card above its total, and cap_cents is that hold.
|
|
26
|
+
*/
|
|
22
27
|
cap_cents: number;
|
|
28
|
+
/**
|
|
29
|
+
* True when cap_cents is above total_cents: show the total and the hold beside it, as in "Walmart $100.00, card held up
|
|
30
|
+
* to $125.00, you pay only the final amount".
|
|
31
|
+
*/
|
|
32
|
+
held: boolean;
|
|
23
33
|
}
|
|
24
34
|
export interface BuyBasket {
|
|
25
35
|
id: string;
|
|
26
36
|
status: BuyBasketStatus;
|
|
37
|
+
/** The sum of the sellers' cart totals, the amount the shopper approves. */
|
|
27
38
|
total_cents: number;
|
|
28
|
-
/** The most every seller together may charge: total_cents, the
|
|
39
|
+
/** The most every seller together may charge: total_cents, plus the hold above each held line's total. */
|
|
29
40
|
cap_cents: number;
|
|
30
41
|
currency: string;
|
|
31
42
|
approval_url: string | null;
|
package/dist/basket.js
CHANGED
|
@@ -15,7 +15,9 @@ export function basketOf(v) {
|
|
|
15
15
|
for (const l of v.lines) {
|
|
16
16
|
if (!isRecord(l) || typeof l.seller !== 'string' || typeof l.status !== 'string' || !LINE_STATUSES.has(l.status) || !whole(l.total_cents) || !whole(l.cap_cents))
|
|
17
17
|
return null;
|
|
18
|
-
|
|
18
|
+
if (l.cap_cents < l.total_cents)
|
|
19
|
+
return null;
|
|
20
|
+
lines.push({ seller: l.seller, status: l.status, total_cents: l.total_cents, cap_cents: l.cap_cents, held: l.cap_cents > l.total_cents });
|
|
19
21
|
}
|
|
20
22
|
return { id: v.id, status: v.status, total_cents: v.total_cents, cap_cents: v.cap_cents, currency: v.currency,
|
|
21
23
|
approval_url: typeof v.approval_url === 'string' ? v.approval_url : null, expires_at: v.expires_at, lines };
|
package/dist/buy-tool.d.ts
CHANGED
|
@@ -604,6 +604,15 @@ export interface BuyTool {
|
|
|
604
604
|
* call's answer tells the model where the purchase stands. Post `message.text` then, as before.
|
|
605
605
|
*/
|
|
606
606
|
nextStep(message: ShopperMessage, options?: BuyToolTurnOptions): BuyToolTurn | null;
|
|
607
|
+
/**
|
|
608
|
+
* The carts this tool last showed, with the cart an `order_placed` message is about marked placed: its `totalStatus`
|
|
609
|
+
* reads `placed` and its total is the total the order placed at. A store that sets its final amount later names only an
|
|
610
|
+
* estimate, and the cart keeps `totalIsEstimate` true at that estimate. The tool's `onCart` hears the carts before the
|
|
611
|
+
* promise settles, so await it in your webhook's `onMessage`. Pass every message `onMessage` got. Answers null for any
|
|
612
|
+
* other message, a message about another conversation or shopper, or one whose cart this tool cannot tell: the message
|
|
613
|
+
* names a store none of the carts is at, and more than one cart is on the table.
|
|
614
|
+
*/
|
|
615
|
+
placedCarts(message: ShopperMessage): Promise<BuyCart[] | null>;
|
|
607
616
|
}
|
|
608
617
|
/**
|
|
609
618
|
* One thing a turn hands the shopper: a message, such as the instant reply or a paragraph your model
|
package/dist/buy-tool.js
CHANGED
|
@@ -769,7 +769,7 @@ function endTurn(state) {
|
|
|
769
769
|
* The hash alone is not enough, since a store pricing a cart again changes `pricing` and its total under one hash.
|
|
770
770
|
*/
|
|
771
771
|
function cartsKey(carts) {
|
|
772
|
-
return JSON.stringify(carts.map((c) => [c.hash, c.totalCents, c.pricing === true, c.totalIsEstimate, c.items.map((i) => [i.line_id, i.qty, i.lineTotalCents])]));
|
|
772
|
+
return JSON.stringify(carts.map((c) => [c.hash, c.totalCents, c.pricing === true, c.totalIsEstimate, c.totalStatus, c.items.map((i) => [i.line_id, i.qty, i.lineTotalCents])]));
|
|
773
773
|
}
|
|
774
774
|
export function buyTool(options) {
|
|
775
775
|
const o = options ?? {};
|
|
@@ -934,6 +934,24 @@ export function buyTool(options) {
|
|
|
934
934
|
let callsStarted = 0;
|
|
935
935
|
let cartsFromCall = 0;
|
|
936
936
|
let lastCarts = [];
|
|
937
|
+
/**
|
|
938
|
+
* The carts an order placed marked (`placedCarts`), by hash, with their conversation: an answer in that conversation that
|
|
939
|
+
* still lists one, such as a call that began before the order placed, shows it placed. The first answer that no longer
|
|
940
|
+
* lists it forgets it, so the same items carted again later are a new purchase.
|
|
941
|
+
*/
|
|
942
|
+
const placedByHash = new Map();
|
|
943
|
+
const placedIn = (hash, conversationId) => {
|
|
944
|
+
const kept = placedByHash.get(hash);
|
|
945
|
+
return kept && (!kept.conversationId || !conversationId || kept.conversationId === conversationId) ? kept.cart : undefined;
|
|
946
|
+
};
|
|
947
|
+
const withPlaced = (carts, conversationId) => carts.map((c) => placedIn(c.hash, conversationId) ?? c);
|
|
948
|
+
/** The conversation the carts that left the table were in. */
|
|
949
|
+
let leftCartsIn = null;
|
|
950
|
+
/** The newest call whose answer set the carts the tool keeps. */
|
|
951
|
+
let cartsKeptFromCall = 0;
|
|
952
|
+
const cartKey = (c) => `${c.merchant}|${c.store?.id ?? ''}`;
|
|
953
|
+
/** The carts an answer took off the table, newest first, at most five: an order placed before its message arrived leaves its cart here. */
|
|
954
|
+
let leftCarts = [];
|
|
937
955
|
/** The cart the last answer showed: the cart a confirm sent now confirms. */
|
|
938
956
|
let lastShownCart = null;
|
|
939
957
|
/**
|
|
@@ -2144,10 +2162,11 @@ export function buyTool(options) {
|
|
|
2144
2162
|
: {}),
|
|
2145
2163
|
...(t.onCart
|
|
2146
2164
|
? {
|
|
2147
|
-
onCart: async ({ cart, carts, conversationId }) => {
|
|
2165
|
+
onCart: async ({ cart: sent, carts: sentCarts, conversationId }) => {
|
|
2148
2166
|
// A newer call already handed on its carts: this older stream's are stale, however late they come.
|
|
2149
2167
|
if (callNo < cartsFromCall)
|
|
2150
2168
|
return;
|
|
2169
|
+
const [cart, carts] = [sent ? placedIn(sent.hash, conversationId) ?? sent : sent, withPlaced(sentCarts, conversationId)];
|
|
2151
2170
|
t.cartsHeard = cartsKey(carts);
|
|
2152
2171
|
cartsFromCall = callNo;
|
|
2153
2172
|
try {
|
|
@@ -2329,7 +2348,26 @@ export function buyTool(options) {
|
|
|
2329
2348
|
// A refusal reports no carts, so only a turn's answer says how many wait, and whether a checkout just ended with none.
|
|
2330
2349
|
if (turn) {
|
|
2331
2350
|
lastCartCount = result.carts.length;
|
|
2332
|
-
|
|
2351
|
+
// A cart an order placed reads placed in every answer that still lists it.
|
|
2352
|
+
const answeredIn = result.conversation_id ?? lastConversationId;
|
|
2353
|
+
result.carts = withPlaced(result.carts, answeredIn);
|
|
2354
|
+
if (result.cart)
|
|
2355
|
+
result.cart = placedIn(result.cart.hash, answeredIn) ?? result.cart;
|
|
2356
|
+
// Only the newest call's answer moves what the tool keeps: an older call's answer is no word on the carts as they stand.
|
|
2357
|
+
if (callNo >= cartsKeptFromCall) {
|
|
2358
|
+
cartsKeptFromCall = callNo;
|
|
2359
|
+
for (const hash of [...placedByHash.keys()])
|
|
2360
|
+
if (!result.error_code && !result.carts.some((c) => c.hash === hash))
|
|
2361
|
+
placedByHash.delete(hash);
|
|
2362
|
+
// The carts that left the table, newest first, one per store. A cart the answer still holds at the same store, edited
|
|
2363
|
+
// or priced again, is the same purchase in a new version, so its older version never counts as one that left.
|
|
2364
|
+
const left = lastCarts.filter((c) => !result.carts.some((n) => n.hash === c.hash || cartKey(n) === cartKey(c)));
|
|
2365
|
+
// A new conversation is a new purchase: the carts that left the one before it are no longer its carts.
|
|
2366
|
+
const sameConversation = !answeredIn || !leftCartsIn || answeredIn === leftCartsIn;
|
|
2367
|
+
leftCarts = [...(sameConversation ? left : []), ...(sameConversation ? leftCarts : []).filter((c) => !left.some((n) => cartKey(n) === cartKey(c)))].slice(0, 5);
|
|
2368
|
+
leftCartsIn = answeredIn ?? leftCartsIn;
|
|
2369
|
+
lastCarts = result.carts;
|
|
2370
|
+
}
|
|
2333
2371
|
// The answer's carts are the conversation's as they stand, an empty list included: onCart hears them when the stream
|
|
2334
2372
|
// sent none or older ones, so an app never keeps showing a cart Agentcard no longer holds (a stream sends a cart only
|
|
2335
2373
|
// when a tool changed it, and a call with stream: false sends none).
|
|
@@ -2391,8 +2429,8 @@ export function buyTool(options) {
|
|
|
2391
2429
|
held.add(id);
|
|
2392
2430
|
tappable.push(product);
|
|
2393
2431
|
}
|
|
2394
|
-
lastAnswerProducts = shown.map((product) => ({ productId: pickId(product), name: product.name }));
|
|
2395
|
-
lastTappable = tappable.map((product) => ({ productId: pickId(product), name: product.name }));
|
|
2432
|
+
lastAnswerProducts = shown.map((product) => ({ productId: pickId(product), name: product.name, store: product.store }));
|
|
2433
|
+
lastTappable = tappable.map((product) => ({ productId: pickId(product), name: product.name, store: product.store }));
|
|
2396
2434
|
// A new conversation starts the cards shown before afresh; each card keeps its newest showing, up to SHOWN_BEFORE_MAX.
|
|
2397
2435
|
if (result.conversation_id && result.conversation_id !== shownIn)
|
|
2398
2436
|
shownBefore = [];
|
|
@@ -2489,6 +2527,45 @@ export function buyTool(options) {
|
|
|
2489
2527
|
client.answered(externalUserId, message, threadId);
|
|
2490
2528
|
return openTurn(nextStepPrompt(message), options, message);
|
|
2491
2529
|
};
|
|
2530
|
+
/** The carts with the one an order_placed message is about marked placed at its total (`BuyTool.placedCarts`). */
|
|
2531
|
+
const placedCarts = async (message) => {
|
|
2532
|
+
if (!message || typeof message !== 'object' || message.kind !== 'order_placed')
|
|
2533
|
+
return null;
|
|
2534
|
+
if (client.company && message.externalUserId !== externalUserId)
|
|
2535
|
+
return null;
|
|
2536
|
+
if (message.conversationId && lastConversationId && message.conversationId !== lastConversationId)
|
|
2537
|
+
return null;
|
|
2538
|
+
const store = message.facts?.merchantName?.trim().toLowerCase();
|
|
2539
|
+
const named = (pool) => (store ? pool.filter((c) => [c.merchant_name, c.store?.name, c.store?.id, c.merchant].some((n) => n?.trim().toLowerCase() === store)) : []);
|
|
2540
|
+
const [open, left] = [named(lastCarts), named(leftCarts)];
|
|
2541
|
+
// The one cart the message names, on the table or one an answer took off it once the order placed, else the only cart.
|
|
2542
|
+
// A name that fits more than one cart, an open one and one that left included, tells none apart: the order may be either.
|
|
2543
|
+
const cart = open.length + left.length > 1 ? null
|
|
2544
|
+
: open[0] ?? left[0] ?? (lastCarts.length === 1 ? lastCarts[0] : lastCarts.length === 0 && leftCarts.length === 1 ? leftCarts[0] : null);
|
|
2545
|
+
if (!cart)
|
|
2546
|
+
return null;
|
|
2547
|
+
const [total, estimate] = [message.facts?.totalCents, message.facts?.estimatedTotalCents];
|
|
2548
|
+
const final = typeof total === 'number' && total >= 0;
|
|
2549
|
+
const placed = {
|
|
2550
|
+
...cart,
|
|
2551
|
+
totalStatus: 'placed',
|
|
2552
|
+
...(final ? { totalCents: total, totalIsEstimate: false } : typeof estimate === 'number' && estimate >= 0 ? { totalCents: estimate, totalIsEstimate: true } : {}),
|
|
2553
|
+
};
|
|
2554
|
+
// A cart an answer took off the table is shown again, placed, beside the carts still open.
|
|
2555
|
+
const carts = lastCarts.includes(cart) ? lastCarts.map((c) => (c === cart ? placed : c)) : [...lastCarts, placed];
|
|
2556
|
+
leftCarts = leftCarts.filter((c) => c !== cart);
|
|
2557
|
+
lastCarts = carts;
|
|
2558
|
+
placedByHash.set(cart.hash, { cart: placed, conversationId: lastConversationId });
|
|
2559
|
+
if (o.onCart && lastConversationId) {
|
|
2560
|
+
try {
|
|
2561
|
+
await o.onCart(placed, { carts, conversationId: lastConversationId });
|
|
2562
|
+
}
|
|
2563
|
+
catch {
|
|
2564
|
+
// The app's display failing never fails the message.
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
return carts;
|
|
2568
|
+
};
|
|
2492
2569
|
const openTurn = (shopperText, options, event) => {
|
|
2493
2570
|
const own = options ?? {};
|
|
2494
2571
|
if (typeof own !== 'object')
|
|
@@ -2919,5 +2996,5 @@ export function buyTool(options) {
|
|
|
2919
2996
|
say: { value: sayWhole, enumerable: false },
|
|
2920
2997
|
});
|
|
2921
2998
|
};
|
|
2922
|
-
return toolOf(answer, { instantAck, instantReaction, turn, nextStep }, {});
|
|
2999
|
+
return toolOf(answer, { instantAck, instantReaction, turn, nextStep, placedCarts }, {});
|
|
2923
3000
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -472,6 +472,8 @@ export interface BuyCartHold {
|
|
|
472
472
|
amountCents: number;
|
|
473
473
|
reason: 'flight' | 'hotel' | 'checkout_estimate';
|
|
474
474
|
}
|
|
475
|
+
/** Where a cart's total stands: an estimate, the store's confirmed total, or the total of the order placed. */
|
|
476
|
+
export type BuyCartTotalStatus = 'estimate' | 'confirmed' | 'placed';
|
|
475
477
|
export interface BuyCart {
|
|
476
478
|
merchant: string;
|
|
477
479
|
merchant_name: string;
|
|
@@ -494,6 +496,13 @@ export interface BuyCart {
|
|
|
494
496
|
* known fees only: show it as an estimate. DoorDash prices a cart once the shopper's own account holds it.
|
|
495
497
|
*/
|
|
496
498
|
totalIsEstimate: boolean;
|
|
499
|
+
/**
|
|
500
|
+
* Where `totalCents` stands: `estimate` before the store priced the cart, `confirmed` once the store priced it, such
|
|
501
|
+
* as when the approval asks for the store's total, and `placed` once the order is placed (`buy.placedCarts`). A placed
|
|
502
|
+
* order whose store has not set its final amount yet keeps `totalIsEstimate` true. An older API sends none, and the
|
|
503
|
+
* cart reads `estimate` when `totalIsEstimate` is true, else `confirmed`.
|
|
504
|
+
*/
|
|
505
|
+
totalStatus: BuyCartTotalStatus;
|
|
497
506
|
/** The ISO 4217 code the store prices the cart in, or null when Agentcard sent none. */
|
|
498
507
|
currency: string | null;
|
|
499
508
|
approvedCeilingCents: number | null;
|
package/dist/client.js
CHANGED
|
@@ -232,6 +232,11 @@ function feesOf(v) {
|
|
|
232
232
|
&& amount !== null && Number.isSafeInteger(amount) && amount >= 0 ? 'known' : 'unknown';
|
|
233
233
|
return { serviceFeesCents: status === 'known' ? amount : null, serviceFeesStatus: status };
|
|
234
234
|
}
|
|
235
|
+
function totalStatusOf(v) {
|
|
236
|
+
if (v.totalStatus === 'estimate' || v.totalStatus === 'confirmed' || v.totalStatus === 'placed')
|
|
237
|
+
return v.totalStatus;
|
|
238
|
+
return v.totalIsEstimate === true ? 'estimate' : 'confirmed';
|
|
239
|
+
}
|
|
235
240
|
function cartOf(v) {
|
|
236
241
|
if (!isRecord(v) || !str(v.hash) || !str(v.merchant))
|
|
237
242
|
return null;
|
|
@@ -265,6 +270,7 @@ function cartOf(v) {
|
|
|
265
270
|
tipCents: num(v.tipCents) ?? 0,
|
|
266
271
|
totalCents: num(v.totalCents) ?? 0,
|
|
267
272
|
totalIsEstimate: v.totalIsEstimate === true,
|
|
273
|
+
totalStatus: totalStatusOf(v),
|
|
268
274
|
currency: str(v.currency),
|
|
269
275
|
approvedCeilingCents: num(v.approvedCeilingCents),
|
|
270
276
|
hold: holdOf(v.hold),
|
package/dist/direct.d.ts
CHANGED
|
@@ -21,10 +21,11 @@ export interface DirectContext {
|
|
|
21
21
|
nextAction?: BuyNextAction | null;
|
|
22
22
|
/** Agentcard's last reply, the question a short answer answers. */
|
|
23
23
|
asked?: string | null;
|
|
24
|
-
/** The products Agentcard's last answer showed, in order, with the `product_id` a pick sends. A place (`the second one`) reads these. */
|
|
24
|
+
/** The products Agentcard's last answer showed, in order, with the `product_id` a pick sends and the store that sells each. A place (`the second one`) reads these. */
|
|
25
25
|
products?: ReadonlyArray<{
|
|
26
26
|
productId: string | null;
|
|
27
27
|
name: string;
|
|
28
|
+
store?: string | null;
|
|
28
29
|
}>;
|
|
29
30
|
/**
|
|
30
31
|
* Every product card the shopper can tap by its name, each once: the answer's and the ones its searches sent as they answered,
|
|
@@ -35,6 +36,7 @@ export interface DirectContext {
|
|
|
35
36
|
tappable?: ReadonlyArray<{
|
|
36
37
|
productId: string | null;
|
|
37
38
|
name: string;
|
|
39
|
+
store?: string | null;
|
|
38
40
|
}>;
|
|
39
41
|
/**
|
|
40
42
|
* Every product card an earlier answer of this conversation showed, each once, newest first. A card's tap whose name no card
|
package/dist/direct.js
CHANGED
|
@@ -164,6 +164,33 @@ function tappedName(raw) {
|
|
|
164
164
|
const name = TAP.exec(raw.trim())?.[1]?.trim();
|
|
165
165
|
return name ? name : null;
|
|
166
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* The words a pick by one word of a product's name says around that word, as `nameWords` spells them: `lets do the X ones`,
|
|
169
|
+
* `I'll take the X`, `go with X`, `X please`, `X, 2 of them`, `X is fine`. A yes, a no or any other word is not among them.
|
|
170
|
+
*/
|
|
171
|
+
const PICK_FRAME = new Set('the a an one ones that this lets let s do go with i ll take get want give me is fine good works uh uhh um umm please pls plz of them those it x pack packs'.split(' '));
|
|
172
|
+
/** The stores a message names by brand, `home depot` and `barnes & noble` whole, as the words `nameWords` spells them. */
|
|
173
|
+
const STORE_NAMED = new RegExp(`(?<![\\p{L}\\p{N}])(?:${STORES})(?![\\p{L}\\p{N}])`, 'gu');
|
|
174
|
+
function storesNamed(raw) {
|
|
175
|
+
return [...String(raw ?? '').toLowerCase().matchAll(STORE_NAMED)].map((match) => nameWords(match[0]));
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* True when a message picks this product by a word only its name holds, such as its brand. Every other word of the message is the
|
|
179
|
+
* product's own or a pick frame's (`PICK_FRAME`), and the word is no part of a store's name the message holds (`home depot`), or of
|
|
180
|
+
* the product's own store, unless the product's name opens with that store's name, as Amazon Basics does. On 2026-11-05
|
|
181
|
+
* (conv_64712285dd09f5bbab6fc737) the agent showed eight books and offered to look at eBay, the shopper answered "yes ebay
|
|
182
|
+
* specifically", and "ebay", held by "The Expert Guide To Cashing In On eBay" alone, went as a tap on that card.
|
|
183
|
+
*/
|
|
184
|
+
function marksProduct(raw, said, product, cards) {
|
|
185
|
+
const own = nameWords(product.name).split(' ');
|
|
186
|
+
const title = ` ${own.join(' ')} `;
|
|
187
|
+
// Each store the message names, and the product's own store, unless the product's name opens with it (a store's own brand).
|
|
188
|
+
const stores = [...storesNamed(raw), nameWords(product.store ?? '')].filter((store) => store !== '' && !title.startsWith(` ${store} `));
|
|
189
|
+
const isStore = (word) => stores.some((store) => store.split(' ').includes(word));
|
|
190
|
+
const words = said.split(' ');
|
|
191
|
+
const marks = words.filter((word) => word.length >= 4 && !/^\d/.test(word) && own.includes(word) && !isStore(word) && cards.filter((card) => nameWords(card.name).split(' ').includes(word)).length === 1);
|
|
192
|
+
return marks.length > 0 && words.every((word) => own.includes(word) || PICK_FRAME.has(word) || /^\d+$/.test(word));
|
|
193
|
+
}
|
|
167
194
|
/** The pick a message makes of what Agentcard just showed, null when it picks nothing, or false when it counts past the list. */
|
|
168
195
|
function pickOf(raw, bare, context) {
|
|
169
196
|
const products = context.products ?? [];
|
|
@@ -213,9 +240,7 @@ function pickOf(raw, bare, context) {
|
|
|
213
240
|
if (named.length > 1)
|
|
214
241
|
return call({});
|
|
215
242
|
// A word only one product's name holds, such as its brand: `lets do the coppertop ones`, `duracell 24 count is fine`.
|
|
216
|
-
const
|
|
217
|
-
const owners = (word) => byName.filter((product) => nameWords(product.name).split(' ').includes(word));
|
|
218
|
-
const marked = byName.filter((product) => nameWords(product.name).split(' ').some((word) => word.length >= 4 && !/^\d/.test(word) && words.has(word) && owners(word).length === 1));
|
|
243
|
+
const marked = byName.filter((product) => marksProduct(raw, said, product, byName));
|
|
219
244
|
if (marked.length === 1 && said.split(' ').length <= 10 && !QUESTION_START.test(bare) && !DOUBT.test(said))
|
|
220
245
|
return call({ product_id: marked[0].productId });
|
|
221
246
|
const places = placesOf(bare, products.length);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
export { verifyAgentcardWebhook, AgentcardSignatureError, type AgentcardSignatureReason, type AgentcardEvent, type VerifyOptions, } from './verify.js';
|
|
2
2
|
export { shopperLocationOf, type ShopperLocation, type ShopperShipTo, type BuyShopperLocation } from './shopper-location.js';
|
|
3
|
-
export { readPurchaseMessage, type PurchaseMessage, type PurchaseMessageKind, type PurchaseFacts, } from './message.js';
|
|
3
|
+
export { readPurchaseMessage, type PurchaseMessage, type PurchaseMessageKind, type PurchaseFacts, type CardRefusal, } from './message.js';
|
|
4
4
|
export { readShopperMessage, isStale, SHOPPER_EVENT_TYPES, NEXT_STEP_KINDS, type NextStepKind, type ShopperMessage, type ShopperMessageKind, type ShopperFacts, type ShopperEventType, type OrderStatus, } from './shopper-message.js';
|
|
5
5
|
export { ORDER_STEPS, updateOrder, arrivalText, type ShopperOrder, type OrderStep, type OrderPathStep, type OrderStepState, type ArrivalTextOptions, } from './order.js';
|
|
6
6
|
export { agentcardWebhook, purchaseAgentWebhook, type AgentcardWebhookOptions, type AgentcardWebhookHandler, type PurchaseAgentWebhookOptions, type WebhookFetchRequest, type WebhookRequest, type WebhookResponse, } from './handler.js';
|
|
7
7
|
export { searchAt, isMerchantStatusAt, merchantStatusLine, searchStatusLines, type BuySearchStatus, type BuyMerchantStatus, type BuyMerchantSearchStatus, type BuyMerchantStatusUpdate, } from './search-status.js';
|
|
8
8
|
export { groupsOf, type BuyGroup, type BuyGroupKind, type BuyGroupStatus, type BuyGroupWithProducts, type BuyGroupsSource, type BuyGroupsUpdate, } from './search-groups.js';
|
|
9
|
-
export { AgentcardClient, AgentcardApiError, BUY_TURN_TIMEOUT_MS, DEFAULT_BASE_URL, type AgentcardClientOptions, type AgentcardTraceOptions, type OtelApi, type TraceContext, type FetchLike, type FetchLikeResponse, type BuyProgress, type BuyProgressStage, type BuyCatalogUpdate, type BuyAvailability, type BuyAvailabilityUpdate, type BuyStoreCheckUpdate, type BuyFoundUpdate, type BuyTurnStatus, STATUS_WORDING, type BuySegmentUpdate, type BuyRecheck, type BuyFreshness, type BuyFreshnessState, type BuyCartUpdate, type BuyDeltaUpdate, type BuyStreamHandlers, type Shopper, type BuyLastAnswer, type BuyRequest, type BuyDeliveryAddress, type BuyTurn, type BuyStatus, type BuyCart, type BuyCartBuilding, type BuyCartLine, type BuyCartFeeLine, type BuyCartDiscount, type BuyCartHold, type BuyStore, type BuyCatalog, type BuyCatalogItem, type BuyFlightItinerary, type BuyFlightParty, type BuyPaymentSource, type BuyConversation, type BuyLatestReply, type BuyLastCheckout, type BuyOrder, type VaultLink, type BuyNextAction, type BuyPick, type BuyStoreChoice, type BuyStorePick, type BuyStores, type BuyChoice, type BuyCodeRequest, type BuyChoiceGroup, type BuyChoiceDelivery, type BuyChoiceOption, } from './client.js';
|
|
9
|
+
export { AgentcardClient, AgentcardApiError, BUY_TURN_TIMEOUT_MS, DEFAULT_BASE_URL, type AgentcardClientOptions, type AgentcardTraceOptions, type OtelApi, type TraceContext, type FetchLike, type FetchLikeResponse, type BuyProgress, type BuyProgressStage, type BuyCatalogUpdate, type BuyAvailability, type BuyAvailabilityUpdate, type BuyStoreCheckUpdate, type BuyFoundUpdate, type BuyTurnStatus, STATUS_WORDING, type BuySegmentUpdate, type BuyRecheck, type BuyFreshness, type BuyFreshnessState, type BuyCartUpdate, type BuyDeltaUpdate, type BuyStreamHandlers, type Shopper, type BuyLastAnswer, type BuyRequest, type BuyDeliveryAddress, type BuyTurn, type BuyStatus, type BuyCart, type BuyCartBuilding, type BuyCartLine, type BuyCartFeeLine, type BuyCartDiscount, type BuyCartHold, type BuyCartTotalStatus, type BuyStore, type BuyCatalog, type BuyCatalogItem, type BuyFlightItinerary, type BuyFlightParty, type BuyPaymentSource, type BuyConversation, type BuyLatestReply, type BuyLastCheckout, type BuyOrder, type VaultLink, type BuyNextAction, type BuyPick, type BuyStoreChoice, type BuyStorePick, type BuyStores, type BuyChoice, type BuyCodeRequest, type BuyChoiceGroup, type BuyChoiceDelivery, type BuyChoiceOption, } from './client.js';
|
|
10
10
|
export { BUY_TOOL_NAME, BUY_TOOL_DESCRIPTION, buyToolDefinition, parseBuyToolInput, type BuyToolDefinition, type BuyToolInput, type BuyToolInputSchema, type BuyToolInputCheck, type BuyToolStandardSchema, type OpenAIBuyTool, } from './definition.js';
|
|
11
11
|
export { toMessages, messageStream, typingDelay, type MessagePace, type MessageStream, type MessageStreamOptions, } from './messages.js';
|
|
12
12
|
export { buyTool, nextStepPrompt, type BuyTool, type BuyToolOptions, type BuyToolResult, type BuyToolStatus, type BuyToolLink, type BuyToolFound, type BuyToolAvailability, type BuyToolProduct, type BuyToolProductAction, productAction, type BuyToolStore, type BuyToolTurn, type BuyToolTurnItem, type BuyToolTurnOptions, type BuyToolTurnMessage, type BuyAckMode, } from './buy-tool.js';
|
|
13
13
|
export { instantAck, instantReaction, ackFor, keepsYes, ACK_REACTION, type AckForOptions } from './ack.js';
|
|
14
14
|
export { directCall, isProductAsk, type DirectCall, type DirectContext, type DirectReason } from './direct.js';
|
|
15
|
-
export { forModel, grandTotals, forModelError, stepAsWritten, withoutCardList, stripLinks, flightPartyWords, type BuyToolModelView, type BuyToolModelCart, type BuyToolGrandTotal, type BuyToolModelProduct, type BuyToolModelStore, type ShownCard, } from './model-view.js';
|
|
15
|
+
export { forModel, cartsForModel, grandTotals, forModelError, stepAsWritten, withoutCardList, stripLinks, flightPartyWords, type BuyToolModelView, type BuyToolModelCart, type BuyToolGrandTotal, type BuyToolModelProduct, type BuyToolModelStore, type ShownCard, } from './model-view.js';
|
|
16
16
|
export { asBullets, choiceLines, replyAsWritten, textingStyle, withChoice, type ReplyAsWrittenOptions } from './texting.js';
|
|
17
17
|
export { appInstructions, APP_INSTRUCTIONS_VERSION, type AppInstructionsOptions } from './app-instructions.js';
|
|
18
18
|
export { toAnthropicTool, toOpenRouterTool, type AnthropicBuyTool, type OpenRouterToolkit, type ZodLike, } from './adapters.js';
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ export { toMessages, messageStream, typingDelay, } from './messages.js';
|
|
|
12
12
|
export { buyTool, nextStepPrompt, productAction, } from './buy-tool.js';
|
|
13
13
|
export { instantAck, instantReaction, ackFor, keepsYes, ACK_REACTION } from './ack.js';
|
|
14
14
|
export { directCall, isProductAsk } from './direct.js';
|
|
15
|
-
export { forModel, grandTotals, forModelError, stepAsWritten, withoutCardList, stripLinks, flightPartyWords, } from './model-view.js';
|
|
15
|
+
export { forModel, cartsForModel, grandTotals, forModelError, stepAsWritten, withoutCardList, stripLinks, flightPartyWords, } from './model-view.js';
|
|
16
16
|
export { asBullets, choiceLines, replyAsWritten, textingStyle, withChoice } from './texting.js';
|
|
17
17
|
export { appInstructions, APP_INSTRUCTIONS_VERSION } from './app-instructions.js';
|
|
18
18
|
export { toAnthropicTool, toOpenRouterTool, } from './adapters.js';
|
package/dist/message.d.ts
CHANGED
|
@@ -59,6 +59,25 @@ export interface PurchaseFacts {
|
|
|
59
59
|
validUntil?: string;
|
|
60
60
|
/** 1 for a cart's first nudge, 2 for its second. */
|
|
61
61
|
nudgeNumber?: number;
|
|
62
|
+
/**
|
|
63
|
+
* On an `order_failed` whose card the store's payment processor refused after the approval: why it refused the card,
|
|
64
|
+
* and whether any money moved, so your app can show the reason without reading `reason`'s words.
|
|
65
|
+
*/
|
|
66
|
+
cardRefusal?: CardRefusal;
|
|
67
|
+
/** The one step that moves the purchase on: `add_card`, with the add-a-card link in `cardLinkUrl`. */
|
|
68
|
+
nextAction?: 'add_card' | (string & {});
|
|
69
|
+
/**
|
|
70
|
+
* When `cardLinkUrl` stops working, as an ISO 8601 timestamp, on a message that stays true after it does (`order_failed`):
|
|
71
|
+
* past it, ask Agentcard for a new link instead of sending this one.
|
|
72
|
+
*/
|
|
73
|
+
cardLinkExpiresAt?: string;
|
|
74
|
+
}
|
|
75
|
+
/** Why a store's payment processor refused the shopper's card, and whether any money moved. */
|
|
76
|
+
export interface CardRefusal {
|
|
77
|
+
/** `incorrect_number`, `insufficient_funds`, `expired_card`, `incorrect_cvc`, or `card_declined` for any other refusal. */
|
|
78
|
+
reason: 'incorrect_number' | 'insufficient_funds' | 'expired_card' | 'incorrect_cvc' | 'card_declined' | (string & {});
|
|
79
|
+
/** `none`: nothing was charged. `unknown`: Agentcard has no confirmation yet either way. */
|
|
80
|
+
charged: 'none' | 'unknown' | (string & {});
|
|
62
81
|
}
|
|
63
82
|
/** A `purchase.message` event, read. */
|
|
64
83
|
export interface PurchaseMessage {
|
package/dist/message.js
CHANGED
|
@@ -18,6 +18,8 @@ const STRING_FACTS = [
|
|
|
18
18
|
['card_link_url', 'cardLinkUrl'],
|
|
19
19
|
['retry_until', 'retryUntil'],
|
|
20
20
|
['valid_until', 'validUntil'],
|
|
21
|
+
['next_action', 'nextAction'],
|
|
22
|
+
['card_link_expires_at', 'cardLinkExpiresAt'],
|
|
21
23
|
];
|
|
22
24
|
const NUMBER_FACTS = [
|
|
23
25
|
['total_cents', 'totalCents'],
|
|
@@ -39,6 +41,10 @@ function readFacts(raw) {
|
|
|
39
41
|
if (typeof value === 'number' && Number.isFinite(value))
|
|
40
42
|
facts[name] = value;
|
|
41
43
|
}
|
|
44
|
+
const refusal = raw.card_refusal;
|
|
45
|
+
if (isRecord(refusal) && typeof refusal.reason === 'string' && refusal.reason !== '') {
|
|
46
|
+
facts.cardRefusal = { reason: refusal.reason, charged: typeof refusal.charged === 'string' && refusal.charged !== '' ? refusal.charged : 'unknown' };
|
|
47
|
+
}
|
|
42
48
|
return facts;
|
|
43
49
|
}
|
|
44
50
|
function requiredString(data, field) {
|
package/dist/model-view.d.ts
CHANGED
|
@@ -51,6 +51,11 @@ export interface BuyToolModelCart {
|
|
|
51
51
|
total: string;
|
|
52
52
|
/** True when the store has not priced its fees and tax yet: `total` is an estimate, never the final amount. */
|
|
53
53
|
total_is_estimate: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Where `total` stands: `estimate` before the store priced the cart, `confirmed` once the store priced it, and `placed`
|
|
56
|
+
* once the order is placed. `total` is the final amount unless `total_is_estimate` is true.
|
|
57
|
+
*/
|
|
58
|
+
total_status: 'estimate' | 'confirmed' | 'placed';
|
|
54
59
|
/** What an estimated `total` leaves out, in words the model can repeat; null when the store priced the cart. */
|
|
55
60
|
total_leaves_out: string | null;
|
|
56
61
|
/** The most the shopper's card can be held for this cart: above `total` at a store that prices its final at checkout, `total` itself for a flight, which holds exactly that; else null. */
|
|
@@ -194,9 +199,11 @@ export declare function stripLinks(text: string): string;
|
|
|
194
199
|
* one. A line that pointed at a link ends as a sentence when the answer sent the shopper what it pointed at, a product or a
|
|
195
200
|
* store card or a step's link, and goes when it sent none, so the reply never promises what the shopper does not get. 2026-10-27, chat.opencommerce.sh:
|
|
196
201
|
* "here's the stool on Wayfair:" reached the shopper with nothing after it. The page of a card the answer sent, `pages`,
|
|
197
|
-
* stays as written.
|
|
202
|
+
* stays as written. A line that points at a step link the answer hands the app, `stepLinks`, keeps its words as a sentence
|
|
203
|
+
* whether or not the tool sent it: the app sends that link itself. 2026-11-05, a company's app with no sendLink: Marshalls'
|
|
204
|
+
* one-line sign-in ask went whole, and the app read an empty reply beside the sign-in link.
|
|
198
205
|
*/
|
|
199
|
-
export declare function replyWithoutLinks(reply: string, sent: boolean, pages?: readonly (string | null | undefined)[]): string;
|
|
206
|
+
export declare function replyWithoutLinks(reply: string, sent: boolean, pages?: readonly (string | null | undefined)[], stepLinks?: readonly string[]): string;
|
|
200
207
|
/**
|
|
201
208
|
* An amount in its currency's minor unit, as the model reads it: the currency's own minor units place the
|
|
202
209
|
* decimal point, so 1331 is $13.31 in USD, CA$13.31 in CAD and ¥1,331 in JPY. An amount whose currency
|
|
@@ -264,6 +271,8 @@ export declare function expiresIn(iso: string | null | undefined, now?: number):
|
|
|
264
271
|
* whether the shopper has each link. A status this version does not know
|
|
265
272
|
* reads as `needs_input`.
|
|
266
273
|
*/
|
|
274
|
+
/** The carts and their grand totals as the model reads them, for carts a turn's answer did not bring, such as `buy.placedCarts`'. */
|
|
275
|
+
export declare function cartsForModel(carts: readonly BuyCart[]): Pick<BuyToolModelView, 'carts' | 'grand_totals'>;
|
|
267
276
|
export declare function forModel(result: BuyToolResult): BuyToolModelView;
|
|
268
277
|
/**
|
|
269
278
|
* The fixed short line for an answer whose next step is a link the shopper already holds: `add your card to place the
|
package/dist/model-view.js
CHANGED
|
@@ -4,6 +4,8 @@ import { GONE, LINK_WORD_LINKS, MARKDOWN_IMAGE, holdStorePages, namesOnlyALink,
|
|
|
4
4
|
import { withoutEmptyMarks } from './list-marks.js';
|
|
5
5
|
/** The steps the shopper takes on a page a link opens: the message that asks for one is one short line. */
|
|
6
6
|
const STEP_ACTIONS = new Set(['store_login', 'add_card', 'approve']);
|
|
7
|
+
/** The links an answer hands over for a step the shopper takes: the add-card, approval and store sign-in links. */
|
|
8
|
+
const STEP_LINKS = new Set(['vault', 'approval', 'store_login']);
|
|
7
9
|
const MODEL_STATUSES = new Set(['needs_input', 'needs_card', 'approval_sent', 'placing', 'order_placed', 'declined']);
|
|
8
10
|
/** At most twenty products reach the model. Agentcard's /buy answer lists up to eight, so this cap holds only for an older answer. */
|
|
9
11
|
const MAX_PRODUCTS = 20;
|
|
@@ -81,13 +83,15 @@ const POINTS_AT_LINK = /^([^\n]*?\S)[ \t]*:[ \t]*(?:\n[ \t]*)?<?(?:https?:\/\/|(
|
|
|
81
83
|
* one. A line that pointed at a link ends as a sentence when the answer sent the shopper what it pointed at, a product or a
|
|
82
84
|
* store card or a step's link, and goes when it sent none, so the reply never promises what the shopper does not get. 2026-10-27, chat.opencommerce.sh:
|
|
83
85
|
* "here's the stool on Wayfair:" reached the shopper with nothing after it. The page of a card the answer sent, `pages`,
|
|
84
|
-
* stays as written.
|
|
86
|
+
* stays as written. A line that points at a step link the answer hands the app, `stepLinks`, keeps its words as a sentence
|
|
87
|
+
* whether or not the tool sent it: the app sends that link itself. 2026-11-05, a company's app with no sendLink: Marshalls'
|
|
88
|
+
* one-line sign-in ask went whole, and the app read an empty reply beside the sign-in link.
|
|
85
89
|
*/
|
|
86
|
-
export function replyWithoutLinks(reply, sent, pages = []) {
|
|
90
|
+
export function replyWithoutLinks(reply, sent, pages = [], stepLinks = []) {
|
|
87
91
|
// A card's own store page stays where the reply wrote it (store-pages.ts), and a line whose words only named a link goes with it.
|
|
88
92
|
// A bold label keeps its colon outside the bold (`**Photo:** <address>` reads `**Photo**: <address>`), so it reads as a label.
|
|
89
93
|
const { text, restore } = holdStorePages(String(reply ?? '').replace(/(\*\*|__)([^*_\n]+?):\1/g, '$1$2$1:'), pages);
|
|
90
|
-
return restore(stripLinks(withoutGoneLines(text.replace(POINTS_AT_LINK, (
|
|
94
|
+
return restore(stripLinks(withoutGoneLines(text.replace(POINTS_AT_LINK, (line, lead) => ((sent || stepLinks.some((url) => line.includes(url))) && !namesOnlyALink(lead) ? `${lead}.` : GONE)))));
|
|
91
95
|
}
|
|
92
96
|
/** A currency's minor units as Intl knows them: 2 for USD and CAD, 0 for JPY. */
|
|
93
97
|
function minorUnits(format) {
|
|
@@ -226,6 +230,7 @@ function cartView(cart, building) {
|
|
|
226
230
|
tip: cart.tipCents > 0 ? money(cart.tipCents, cart.currency) : null,
|
|
227
231
|
total: money(cart.totalCents, cart.currency) ?? money(0, cart.currency),
|
|
228
232
|
total_is_estimate: cart.totalIsEstimate === true,
|
|
233
|
+
total_status: cart.totalStatus,
|
|
229
234
|
// 2026-10-21, swarm-bigbox-a10: an estimate read "about $18.41 total with tax and shipping" to the shopper. The words
|
|
230
235
|
// ride beside the flag, so the model never has to guess which side of the total tax and shipping sit on.
|
|
231
236
|
total_leaves_out: cart.totalIsEstimate === true ? leftOut(cart.merchantFeeLines ?? []) : null,
|
|
@@ -299,6 +304,10 @@ function linkExpiry(result) {
|
|
|
299
304
|
* whether the shopper has each link. A status this version does not know
|
|
300
305
|
* reads as `needs_input`.
|
|
301
306
|
*/
|
|
307
|
+
/** The carts and their grand totals as the model reads them, for carts a turn's answer did not bring, such as `buy.placedCarts`'. */
|
|
308
|
+
export function cartsForModel(carts) {
|
|
309
|
+
return { carts: carts.map((cart) => cartView(cart)), grand_totals: grandTotals(carts) };
|
|
310
|
+
}
|
|
302
311
|
export function forModel(result) {
|
|
303
312
|
const status = typeof result.status === 'string' && MODEL_STATUSES.has(result.status) ? result.status : 'needs_input';
|
|
304
313
|
const products = shownProducts(result.products);
|
|
@@ -313,7 +322,7 @@ export function forModel(result) {
|
|
|
313
322
|
question: result.choice.question === null ? null : stripLinks(result.choice.question),
|
|
314
323
|
groups: result.choice.groups.map((group) => ({ name: stripLinks(group.name), required: group.required, options: group.options.map((option) => `${stripLinks(option.label)}${option.price_delta_cents ? ` (+$${(option.price_delta_cents / 100).toFixed(2)})` : ''}`) })),
|
|
315
324
|
} } : {}),
|
|
316
|
-
reply: replyWithoutLinks(result.reply, products.some(held) || stores.some(heldStore) || result.link_sent === true, [...products.filter(held).map((product) => product.url), ...stores.filter(heldStore).map((store) => store.url)]),
|
|
325
|
+
reply: replyWithoutLinks(result.reply, products.some(held) || stores.some(heldStore) || result.link_sent === true, [...products.filter(held).map((product) => product.url), ...stores.filter(heldStore).map((store) => store.url)], (result.links ?? []).flatMap((link) => (STEP_LINKS.has(link.kind) && typeof link.url === 'string' && link.url !== '' ? [link.url] : []))),
|
|
317
326
|
cart: result.cart ? cartView(result.cart, result.cart_building?.store) : null,
|
|
318
327
|
carts: (result.carts ?? []).map((cart) => cartView(cart, result.cart_building?.store)),
|
|
319
328
|
products: products.map((product) => ({ product_id: pickId(product), name: stripLinks(product.name), store: stripLinks(product.store ?? ''), price: productPrice(product), link_sent: held(product) })),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-cards/commerce-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Give your agent one tool that buys for your shoppers at real stores, and post each purchase update Agentcard sends into the shopper's chat, over a connection your server opens or a webhook endpoint.",
|
|
5
5
|
"homepage": "https://opencommerce.sh",
|
|
6
6
|
"type": "module",
|