@agent-cards/commerce-sdk 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/buy-tool.d.ts +29 -1
- package/dist/buy-tool.js +76 -5
- package/dist/client.d.ts +28 -0
- package/dist/client.js +32 -2
- package/dist/index.d.ts +2 -2
- package/dist/model-view.d.ts +11 -3
- package/dist/model-view.js +23 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -458,6 +458,7 @@ const buy = buyTool({
|
|
|
458
458
|
- `onStatus` hears each step the moment Agentcard starts it, as a line to show in place of loading dots: `searching Target and Walmart`, `adding it to your cart`, `working through the store's checkout`. `step` names the step, such as `searching` or `adding_to_cart`, for an app that writes its own words. A call that only writes its reply sends none, so keep your dots until the first line comes.
|
|
459
459
|
- `onFound` hears how many products the searches found, after the cards it counts: `shown` cards of `found` products, such as "8 of 23 shown", each time the count grows. `more` holds the products past the cards as product links, in the order they were found, for a "show more" that runs no new search. The model never reads them.
|
|
460
460
|
- `onProgress` still hears the stage, `searching`, `building_cart`, `checking_out` or `working`.
|
|
461
|
+
- `onAvailability` hears whether the places a booking's cards show have room at the time the shopper asked for. A place's card can go out before Agentcard's live search knows: its `availability` reads `checking`, so show it without its choose button. The update names each card by its `productId` with `available` (its open times in `slots`, empty when none were named), `unavailable` (full then) or `unknown` (the search could not say). Change the card in place, and offer it as a choice only when it reads `available`. A call that ends with a card still `checking` sends `unknown` for it. Every other card, a retail product's included, carries no `availability`.
|
|
461
462
|
- `onProducts` hears the products a search found the moment it answers, before Agentcard writes its reply, with the conversation, when the search ran (`asOf`) and the store's name. With `sendLink`, each product's card goes out then too, a few seconds before the answer, and the answer sends only the cards it adds. Pass `earlyCards: false` to send only the cards the answer names, with the answer: the reply often names only some of a search's products.
|
|
462
463
|
- `onText` hears each message Agentcard writes, links included, the moment it is written, and after the cards of the search it is about.
|
|
463
464
|
- `onCart` hears the cart a confirm would place, or null, and every cart on the table, each time a call builds or changes one, before the message that announces it.
|
package/dist/buy-tool.d.ts
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
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 BuyCartBuilding, type BuyFlightItinerary, type BuyDeliveryAddress, type BuyNextAction, type BuyPick, type BuyProgress, type BuyTurnStatus, 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 BuyAvailability, 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 DirectCall } from './direct.js';
|
|
37
37
|
import { type BuyToolModelView } from './model-view.js';
|
|
@@ -112,6 +112,12 @@ export interface BuyToolLink {
|
|
|
112
112
|
storeId?: string;
|
|
113
113
|
/** On a store link: the store's delivery estimate in minutes, or null when the store showed none. */
|
|
114
114
|
etaMinutes?: number | null;
|
|
115
|
+
/**
|
|
116
|
+
* On a product link for a place to book: whether it has room at the time the shopper asked for. Offer the card as a
|
|
117
|
+
* choice only when it reads `available`; `checking` changes in place once `onAvailability` hears the live answer.
|
|
118
|
+
* Absent on every other card, such as a retail product's.
|
|
119
|
+
*/
|
|
120
|
+
availability?: BuyAvailability;
|
|
115
121
|
}
|
|
116
122
|
/** A store the answer's reply presents to choose from. */
|
|
117
123
|
export interface BuyToolStore {
|
|
@@ -158,6 +164,8 @@ export interface BuyToolProduct {
|
|
|
158
164
|
* conversation.
|
|
159
165
|
*/
|
|
160
166
|
linkSent: boolean;
|
|
167
|
+
/** For a place to book: whether it has room at the asked time. Absent on a product with no such question. */
|
|
168
|
+
availability?: BuyAvailability;
|
|
161
169
|
}
|
|
162
170
|
/** One answer of the buy tool, whole, for your code. Hand it to `forModel` before a model reads it. */
|
|
163
171
|
export interface BuyToolResult {
|
|
@@ -230,6 +238,18 @@ export type BuyAckMode = 'reaction' | 'text' | 'none';
|
|
|
230
238
|
* product links the app reveals on "show more" with no new search; the model never reads them. Find mode sends a count
|
|
231
239
|
* with no `more`.
|
|
232
240
|
*/
|
|
241
|
+
/**
|
|
242
|
+
* What Agentcard's live search said of places whose cards went out as `checking`, each matched to its card by the
|
|
243
|
+
* card's `productId`. A call that ends without saying sends `unknown` for each card it left `checking`, so no card
|
|
244
|
+
* waits for an answer that is not coming.
|
|
245
|
+
*/
|
|
246
|
+
export interface BuyToolAvailability {
|
|
247
|
+
conversationId: string;
|
|
248
|
+
items: Array<{
|
|
249
|
+
productId: string;
|
|
250
|
+
availability: BuyAvailability;
|
|
251
|
+
}>;
|
|
252
|
+
}
|
|
233
253
|
export interface BuyToolFound {
|
|
234
254
|
conversationId: string;
|
|
235
255
|
shown: number;
|
|
@@ -317,6 +337,12 @@ export interface BuyToolOptions {
|
|
|
317
337
|
* comes. One that throws costs nothing but the call to it.
|
|
318
338
|
*/
|
|
319
339
|
onFound?: (found: BuyToolFound) => void | Promise<void>;
|
|
340
|
+
/**
|
|
341
|
+
* Hears whether the places a call's cards show have room (`BuyToolAvailability`), right after the cards it changes went
|
|
342
|
+
* out through `sendLink`: change each card in place, by its `productId`. It needs `earlyCards`, the default. One that
|
|
343
|
+
* throws costs nothing but the call to it.
|
|
344
|
+
*/
|
|
345
|
+
onAvailability?: (update: BuyToolAvailability) => void | Promise<void>;
|
|
320
346
|
/**
|
|
321
347
|
* Hears the products a call's search found, the moment the search answers and before Agentcard writes its reply,
|
|
322
348
|
* with the conversation, when the search ran (`asOf`, as the answer's `catalog.as_of` names it) and the store's name.
|
|
@@ -532,6 +558,8 @@ export interface BuyToolTurnOptions {
|
|
|
532
558
|
onStatus?: BuyToolOptions['onStatus'];
|
|
533
559
|
/** Hears how many products this turn's calls found, as the tool's `onFound` does. */
|
|
534
560
|
onFound?: BuyToolOptions['onFound'];
|
|
561
|
+
/** Hears whether the places this turn's cards show have room, as the tool's `onAvailability` does. */
|
|
562
|
+
onAvailability?: BuyToolOptions['onAvailability'];
|
|
535
563
|
/** Hears the products each search of this turn's calls found, the moment it answers, as the tool's `onProducts` does. */
|
|
536
564
|
onProducts?: BuyToolOptions['onProducts'];
|
|
537
565
|
/**
|
package/dist/buy-tool.js
CHANGED
|
@@ -201,7 +201,7 @@ export function productsOf(turn) {
|
|
|
201
201
|
/** A product's card as `sendLink` hears it: the id the model reads and a pick takes, never the store page, which is `url`. A flight option has no page at a store: its card carries no url, and its productId picks it. */
|
|
202
202
|
function productLink(product) {
|
|
203
203
|
const productId = pickId(product);
|
|
204
|
-
return { kind: 'product', url: product.url || null, title: product.name, price: productPrice(product), ...(productId ? { productId } : {}), action: product.action ?? 'buy', imageUrl: product.imageUrl, images: product.images ?? (product.imageUrl ? [product.imageUrl] : []), expiresAt: null };
|
|
204
|
+
return { kind: 'product', url: product.url || null, title: product.name, price: productPrice(product), ...(productId ? { productId } : {}), action: product.action ?? 'buy', imageUrl: product.imageUrl, images: product.images ?? (product.imageUrl ? [product.imageUrl] : []), expiresAt: null, ...(product.availability ? { availability: product.availability } : {}) };
|
|
205
205
|
}
|
|
206
206
|
function productsOfCatalog(catalog) {
|
|
207
207
|
return (catalog.items ?? [])
|
|
@@ -220,6 +220,7 @@ function productsOfCatalog(catalog) {
|
|
|
220
220
|
url: item.url ?? null,
|
|
221
221
|
action: productAction(catalog.merchant),
|
|
222
222
|
linkSent: false,
|
|
223
|
+
...(item.availability ? { availability: item.availability } : {}),
|
|
223
224
|
}));
|
|
224
225
|
}
|
|
225
226
|
/** The stores an answer presents, as the tool reads them. */
|
|
@@ -673,7 +674,7 @@ export function buyTool(options) {
|
|
|
673
674
|
if (o.stream !== undefined && o.stream !== false)
|
|
674
675
|
throw new TypeError('Pass stream as false to wait for each whole answer, or leave it out.');
|
|
675
676
|
const onProgress = o.onProgress;
|
|
676
|
-
for (const key of ['onStatus', 'onFound']) {
|
|
677
|
+
for (const key of ['onStatus', 'onFound', 'onAvailability']) {
|
|
677
678
|
if (o[key] !== undefined && typeof o[key] !== 'function')
|
|
678
679
|
throw new TypeError(`Pass ${key} as a function, or leave it out.`);
|
|
679
680
|
}
|
|
@@ -745,6 +746,7 @@ export function buyTool(options) {
|
|
|
745
746
|
onProgress,
|
|
746
747
|
onStatus: o.onStatus,
|
|
747
748
|
onFound: o.onFound,
|
|
749
|
+
onAvailability: o.onAvailability,
|
|
748
750
|
onProducts: o.onProducts,
|
|
749
751
|
onText: o.onText,
|
|
750
752
|
onCart: o.onCart,
|
|
@@ -1476,6 +1478,8 @@ export function buyTool(options) {
|
|
|
1476
1478
|
// A confirm or a pick of what the shopper saw never hears "looking for": once the call is slow, it hears a
|
|
1477
1479
|
// fixed line that names nothing they said (slowStepLine), so a 20-second checkout or menu read is never silent.
|
|
1478
1480
|
const ack = t.event ? null : holdAck(ask, t, confirm ? 'confirm' : pick?.product_id ? 'product' : pick ? 'pick' : null);
|
|
1481
|
+
/** Says `unknown` of every card the call left `checking`; it runs once the answer's cards went out, or when the call fails. */
|
|
1482
|
+
let settleLeft = null;
|
|
1479
1483
|
try {
|
|
1480
1484
|
let turn = null;
|
|
1481
1485
|
let refusal;
|
|
@@ -1491,6 +1495,55 @@ export function buyTool(options) {
|
|
|
1491
1495
|
/** Whether this call's cards went out, and a count that came before them. */
|
|
1492
1496
|
let cardsOut = false;
|
|
1493
1497
|
let heldCount = null;
|
|
1498
|
+
/** The card each catalog id of this call went out as (its `productId`), and the cards still `checking`, by conversation. */
|
|
1499
|
+
const cardOfId = new Map();
|
|
1500
|
+
const checking = new Map();
|
|
1501
|
+
/**
|
|
1502
|
+
* What the live search answered for each card, so a later copy of the same place, such as a card whose first send
|
|
1503
|
+
* failed and goes out again with the answer, carries that answer and never goes back to `checking`.
|
|
1504
|
+
*/
|
|
1505
|
+
const answered = new Map();
|
|
1506
|
+
const withAnswers = (products) => answered.size === 0 ? products : products.map((product) => {
|
|
1507
|
+
const known = product.availability ? answered.get(pickId(product) ?? '') : undefined;
|
|
1508
|
+
return known ? { ...product, availability: known } : product;
|
|
1509
|
+
});
|
|
1510
|
+
/**
|
|
1511
|
+
* Notes the card each product goes out as, and each place still `checking`, whichever way the app got it: a card
|
|
1512
|
+
* through `sendLink` now or on a later try, or a product past the cards that `onFound` hands over for "show more".
|
|
1513
|
+
*/
|
|
1514
|
+
const noteCards = (products, conversationId) => {
|
|
1515
|
+
for (const product of products) {
|
|
1516
|
+
const productId = pickId(product);
|
|
1517
|
+
if (!productId)
|
|
1518
|
+
continue;
|
|
1519
|
+
cardOfId.set(product.id, productId);
|
|
1520
|
+
if (product.availability?.status === 'checking' && !answered.has(productId))
|
|
1521
|
+
checking.set(productId, conversationId);
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
1524
|
+
/** Hands the app what the live search said of the places, each by its card, and settles those cards. */
|
|
1525
|
+
settleLeft = async () => {
|
|
1526
|
+
if (!t.onAvailability || !early)
|
|
1527
|
+
return;
|
|
1528
|
+
const left = [...checking];
|
|
1529
|
+
for (const conversationId of new Set(left.map(([, c]) => c))) {
|
|
1530
|
+
await tellAvailability(conversationId, left.filter(([, c]) => c === conversationId).map(([productId]) => ({ productId, availability: { status: 'unknown' } })));
|
|
1531
|
+
}
|
|
1532
|
+
};
|
|
1533
|
+
const tellAvailability = async (conversationId, items) => {
|
|
1534
|
+
for (const item of items) {
|
|
1535
|
+
checking.delete(item.productId);
|
|
1536
|
+
answered.set(item.productId, item.availability);
|
|
1537
|
+
}
|
|
1538
|
+
if (!t.onAvailability || items.length === 0)
|
|
1539
|
+
return;
|
|
1540
|
+
try {
|
|
1541
|
+
await t.onAvailability({ conversationId, items });
|
|
1542
|
+
}
|
|
1543
|
+
catch {
|
|
1544
|
+
// The app's display failing never fails the call.
|
|
1545
|
+
}
|
|
1546
|
+
};
|
|
1494
1547
|
/** Hands the app one count with the products past the cards of the catalog it counts. */
|
|
1495
1548
|
const tellCount = async ({ conversationId, shown, found }) => {
|
|
1496
1549
|
const more = moreFound?.conversationId === conversationId ? moreFound.links : [];
|
|
@@ -1546,7 +1599,9 @@ export function buyTool(options) {
|
|
|
1546
1599
|
ack?.cancel();
|
|
1547
1600
|
await t.ackOut;
|
|
1548
1601
|
// The turn's own search presents its products, as a reply that lists them does.
|
|
1549
|
-
await deliverPagesInTurn(earlyPages(update.conversationId, update.asOf, productsOfCatalog(update.catalog)), { turn: callTurn, presented: true, owner: t }, send);
|
|
1602
|
+
await deliverPagesInTurn(earlyPages(update.conversationId, update.asOf, withAnswers(productsOfCatalog(update.catalog))), { turn: callTurn, presented: true, owner: t }, send);
|
|
1603
|
+
// A place shown `checking`, on a card or behind "show more", waits for the live search's word on it.
|
|
1604
|
+
noteCards([...productsOfCatalog(update.catalog), ...productsOfCatalog({ ...update.catalog, items: update.more })], update.conversationId);
|
|
1550
1605
|
keepMore();
|
|
1551
1606
|
cardsOut = true;
|
|
1552
1607
|
// A count held for these cards goes out now, unless the catalog carries its own, which follows it.
|
|
@@ -1557,6 +1612,13 @@ export function buyTool(options) {
|
|
|
1557
1612
|
},
|
|
1558
1613
|
}
|
|
1559
1614
|
: {}),
|
|
1615
|
+
...(t.onAvailability && early
|
|
1616
|
+
? {
|
|
1617
|
+
onAvailability: async ({ conversationId, items }) => {
|
|
1618
|
+
await tellAvailability(conversationId, items.map(({ id, availability }) => ({ productId: cardOfId.get(id) ?? id, availability })));
|
|
1619
|
+
},
|
|
1620
|
+
}
|
|
1621
|
+
: {}),
|
|
1560
1622
|
...(t.onText
|
|
1561
1623
|
? {
|
|
1562
1624
|
onSegment: async ({ text, conversationId }) => {
|
|
@@ -1654,13 +1716,19 @@ export function buyTool(options) {
|
|
|
1654
1716
|
// Every message of Agentcard's own answer reached the shopper through onText, so the model adds nothing to it. An answer
|
|
1655
1717
|
// whose card ask gave way to the named step was read the same way once every message of it, that ask last, went out.
|
|
1656
1718
|
const read = !!turn && (settled.reply === turn.reply ? readEvery(told, turn.messages) : replaced && readEvery(told, toMessages(settled.reply)));
|
|
1657
|
-
const
|
|
1719
|
+
const answer = withMessages(settled, turn);
|
|
1720
|
+
const delivered = await deliver(answered.size > 0 ? { ...answer, products: withAnswers(answer.products) } : answer, { turn: callTurn, presented: turn?.catalog?.presented === true, owner: t }, send);
|
|
1658
1721
|
// A count that came before any card (find mode puts its cards in the answer) goes out once the answer's cards did.
|
|
1659
1722
|
if (heldCount && (delivered.products?.length ?? 0) > 0) {
|
|
1660
1723
|
const held = heldCount;
|
|
1661
1724
|
heldCount = null;
|
|
1662
1725
|
await tellCount(held);
|
|
1663
1726
|
}
|
|
1727
|
+
// A call that ends with cards still `checking`, the answer's own cards included, says `unknown` of each, so none waits
|
|
1728
|
+
// for an answer that is not coming.
|
|
1729
|
+
if (delivered.conversation_id)
|
|
1730
|
+
noteCards(delivered.products ?? [], delivered.conversation_id);
|
|
1731
|
+
await settleLeft();
|
|
1664
1732
|
// The message that asks for a step link already went out, so the link follows it now instead of waiting for the model's.
|
|
1665
1733
|
// The call waits for it, and one that fails to send fails the call, as a step link the turn does not hold does.
|
|
1666
1734
|
if (read)
|
|
@@ -1709,6 +1777,8 @@ export function buyTool(options) {
|
|
|
1709
1777
|
return result;
|
|
1710
1778
|
}
|
|
1711
1779
|
finally {
|
|
1780
|
+
// A call that fails, Agentcard's error or a thrown handler, still settles the cards it left `checking`.
|
|
1781
|
+
await settleLeft?.().catch(() => { });
|
|
1712
1782
|
ack?.cancel();
|
|
1713
1783
|
running -= 1;
|
|
1714
1784
|
lastEndedAt = Date.now();
|
|
@@ -1782,7 +1852,7 @@ export function buyTool(options) {
|
|
|
1782
1852
|
const own = options ?? {};
|
|
1783
1853
|
if (typeof own !== 'object')
|
|
1784
1854
|
throw new TypeError('Pass the turn options as an object, or leave them out.');
|
|
1785
|
-
for (const key of ['sendLink', 'onWorking', 'onReaction', 'onResult', 'onProgress', 'onStatus', 'onFound', 'onItem', 'onMessage', 'onProducts', 'onText', 'onCart']) {
|
|
1855
|
+
for (const key of ['sendLink', 'onWorking', 'onReaction', 'onResult', 'onProgress', 'onStatus', 'onFound', 'onAvailability', 'onItem', 'onMessage', 'onProducts', 'onText', 'onCart']) {
|
|
1786
1856
|
if (own[key] !== undefined && typeof own[key] !== 'function')
|
|
1787
1857
|
throw new TypeError(`Pass ${key} to the turn as a function, or leave it out.`);
|
|
1788
1858
|
}
|
|
@@ -1861,6 +1931,7 @@ export function buyTool(options) {
|
|
|
1861
1931
|
onProgress: own.onProgress ?? onProgress,
|
|
1862
1932
|
onStatus: own.onStatus ?? o.onStatus,
|
|
1863
1933
|
onFound: own.onFound ?? o.onFound,
|
|
1934
|
+
onAvailability: own.onAvailability ?? o.onAvailability,
|
|
1864
1935
|
onProducts: own.onProducts ?? o.onProducts,
|
|
1865
1936
|
// Agentcard's message takes its place in the turn's sequence, after the items before it, and goes to onText alone, so
|
|
1866
1937
|
// an app that passes onItem too hears it once.
|
package/dist/client.d.ts
CHANGED
|
@@ -51,6 +51,30 @@ export interface BuyCatalogUpdate {
|
|
|
51
51
|
/** The products found past the cards, in the order they were found, to show on "show more" with no new search. */
|
|
52
52
|
more: BuyCatalog['items'];
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Whether a place a card shows has room at the time the shopper asked for: `checking` while Agentcard's live search runs,
|
|
56
|
+
* `available` once it found the place open, with the open times it named in `slots` (empty when it named none),
|
|
57
|
+
* `unavailable` when it found the place full then, and `unknown` when the search ended without saying either way. Only
|
|
58
|
+
* `available` is a place to offer as a choice. A product with no such question, such as a retail product, carries none.
|
|
59
|
+
*/
|
|
60
|
+
export type BuyAvailability = {
|
|
61
|
+
status: 'checking';
|
|
62
|
+
} | {
|
|
63
|
+
status: 'available';
|
|
64
|
+
slots: string[];
|
|
65
|
+
} | {
|
|
66
|
+
status: 'unavailable';
|
|
67
|
+
} | {
|
|
68
|
+
status: 'unknown';
|
|
69
|
+
};
|
|
70
|
+
/** What Agentcard's live search said of the places a running turn's cards show, matched to each card by its catalog `id`. */
|
|
71
|
+
export interface BuyAvailabilityUpdate {
|
|
72
|
+
conversationId: string;
|
|
73
|
+
items: Array<{
|
|
74
|
+
id: string;
|
|
75
|
+
availability: BuyAvailability;
|
|
76
|
+
}>;
|
|
77
|
+
}
|
|
54
78
|
/** How many products a running turn found so far, and how many of them it shows: "8 of 23 shown". */
|
|
55
79
|
export interface BuyFoundUpdate {
|
|
56
80
|
conversationId: string;
|
|
@@ -107,6 +131,8 @@ export interface BuyStreamHandlers {
|
|
|
107
131
|
onCart?: (update: BuyCartUpdate) => void | Promise<void>;
|
|
108
132
|
/** Each piece of a message as Agentcard writes it. Passing it asks Agentcard for the pieces (`?stream=deltas`). */
|
|
109
133
|
onDelta?: (update: BuyDeltaUpdate) => void | Promise<void>;
|
|
134
|
+
/** Whether the places the turn's cards show have room, once Agentcard's live search says, after the cards it changes. */
|
|
135
|
+
onAvailability?: (update: BuyAvailabilityUpdate) => void | Promise<void>;
|
|
110
136
|
}
|
|
111
137
|
export declare const DEFAULT_BASE_URL = "https://api.agentcard.sh";
|
|
112
138
|
/** The wait for one purchase turn: past Agentcard's own 110-second budget, so a slow turn answers `turn_timeout` rather than aborting here. */
|
|
@@ -301,6 +327,8 @@ export interface BuyCatalogItem {
|
|
|
301
327
|
store: BuyStore | null;
|
|
302
328
|
/** The product's page at its store, as an http or https address, or null when Agentcard sent none. */
|
|
303
329
|
url: string | null;
|
|
330
|
+
/** Whether a place has room at the asked time (BuyAvailability). Absent on a product with no such question. */
|
|
331
|
+
availability?: BuyAvailability;
|
|
304
332
|
}
|
|
305
333
|
/**
|
|
306
334
|
* The products the last search found. `presented` is true when this answer's reply presents them: the
|
package/dist/client.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { isRecord } from './verify.js';
|
|
12
12
|
import { listenToAgentcard } from './listen.js';
|
|
13
13
|
/** The handlers that ask Agentcard to stream a turn. */
|
|
14
|
-
const STREAM_HANDLERS = ['onProgress', 'onStatus', 'onFound', 'onCatalog', 'onSegment', 'onCart', 'onDelta'];
|
|
14
|
+
const STREAM_HANDLERS = ['onProgress', 'onStatus', 'onFound', 'onCatalog', 'onSegment', 'onCart', 'onDelta', 'onAvailability'];
|
|
15
15
|
const PROGRESS_STAGES = new Set(['searching', 'building_cart', 'checking_out', 'working']);
|
|
16
16
|
export const DEFAULT_BASE_URL = 'https://api.agentcard.sh';
|
|
17
17
|
/** The wait for one purchase turn: past Agentcard's own 110-second budget, so a slow turn answers `turn_timeout` rather than aborting here. */
|
|
@@ -223,9 +223,30 @@ function catalogItemsOf(v) {
|
|
|
223
223
|
...(flight ? { flight } : {}),
|
|
224
224
|
store: storeOf(i.store),
|
|
225
225
|
url: pageUrl(i.url),
|
|
226
|
+
...availabilityField(i),
|
|
226
227
|
};
|
|
227
228
|
}) : [];
|
|
228
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* A place's availability as Agentcard marks it: `checking`, `open` or `available` with the times in `slots`, `none` or
|
|
232
|
+
* `unavailable`, and `unknown`. A mark this version does not know reads as `unknown`, so no place is offered on a guess.
|
|
233
|
+
*/
|
|
234
|
+
function availabilityOf(v) {
|
|
235
|
+
const mark = v.availability;
|
|
236
|
+
if (mark === 'checking')
|
|
237
|
+
return { status: 'checking' };
|
|
238
|
+
if (mark === 'open' || mark === 'available') {
|
|
239
|
+
const slots = Array.isArray(v.slots) ? v.slots.filter((s) => typeof s === 'string' && s.trim() !== '') : [];
|
|
240
|
+
return { status: 'available', slots };
|
|
241
|
+
}
|
|
242
|
+
if (mark === 'none' || mark === 'unavailable')
|
|
243
|
+
return { status: 'unavailable' };
|
|
244
|
+
return typeof mark === 'string' ? { status: 'unknown' } : null;
|
|
245
|
+
}
|
|
246
|
+
function availabilityField(v) {
|
|
247
|
+
const availability = availabilityOf(v);
|
|
248
|
+
return availability ? { availability } : {};
|
|
249
|
+
}
|
|
229
250
|
/** A count Agentcard sent: a whole number, never negative. Null otherwise. */
|
|
230
251
|
function countOf(v) {
|
|
231
252
|
return typeof v === 'number' && Number.isInteger(v) && v >= 0 ? v : null;
|
|
@@ -424,7 +445,7 @@ async function eachEvent(res, onEvent) {
|
|
|
424
445
|
}
|
|
425
446
|
/**
|
|
426
447
|
* A streamed purchase turn, read to its `envelope`: the same answer a JSON turn returns, with its
|
|
427
|
-
* status in `http_status`. Each `progress`, `status`, `delta`, `catalog`, `find`, `segment` and `
|
|
448
|
+
* status in `http_status`. Each `progress`, `status`, `delta`, `catalog`, `find`, `segment`, `cart` and `venue_availability` goes to its handler
|
|
428
449
|
* on the way, and any other event, such as a `ping` or a `tool`, is read past. A stream that breaks or
|
|
429
450
|
* ends before its envelope, once it named the conversation, reads as a turn past its budget
|
|
430
451
|
* (`turn_timeout`), so the tool reads the conversation for what the turn left instead of losing it.
|
|
@@ -503,6 +524,15 @@ async function readStreamedTurn(res, stream, timeoutMs) {
|
|
|
503
524
|
else if (event === 'segment' && typeof data.text === 'string' && data.text.trim() !== '' && stream.onSegment) {
|
|
504
525
|
deliver(() => stream.onSegment({ conversationId: named, text: data.text }));
|
|
505
526
|
}
|
|
527
|
+
else if (event === 'venue_availability' && stream.onAvailability && str(data.conversation_id) && Array.isArray(data.items)) {
|
|
528
|
+
const items = data.items.filter(isRecord).flatMap((i) => {
|
|
529
|
+
const id = str(i.id);
|
|
530
|
+
const availability = availabilityOf(i);
|
|
531
|
+
return id && availability ? [{ id, availability }] : [];
|
|
532
|
+
});
|
|
533
|
+
if (items.length)
|
|
534
|
+
deliver(() => stream.onAvailability({ conversationId: data.conversation_id, items }));
|
|
535
|
+
}
|
|
506
536
|
else if (event === 'cart' && stream.onCart && str(data.conversation_id)) {
|
|
507
537
|
const update = { conversationId: data.conversation_id, cart: cartOf(data.cart), carts: cartsOf(data.carts) };
|
|
508
538
|
deliver(() => stream.onCart(update));
|
package/dist/index.d.ts
CHANGED
|
@@ -2,10 +2,10 @@ export { verifyAgentcardWebhook, AgentcardSignatureError, type AgentcardSignatur
|
|
|
2
2
|
export { readPurchaseMessage, type PurchaseMessage, type PurchaseMessageKind, type PurchaseFacts, } from './message.js';
|
|
3
3
|
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';
|
|
4
4
|
export { agentcardWebhook, purchaseAgentWebhook, type AgentcardWebhookOptions, type AgentcardWebhookHandler, type PurchaseAgentWebhookOptions, type WebhookFetchRequest, type WebhookRequest, type WebhookResponse, } from './handler.js';
|
|
5
|
-
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 BuyFoundUpdate, type BuyTurnStatus, type BuySegmentUpdate, type BuyCartUpdate, type BuyDeltaUpdate, type BuyStreamHandlers, type Shopper, type BuyRequest, type BuyDeliveryAddress, type BuyTurn, type BuyStatus, type BuyCart, type BuyCartBuilding, type BuyCartLine, type BuyCartFeeLine, type BuyStore, type BuyCatalog, type BuyCatalogItem, type BuyFlightItinerary, type BuyPaymentSource, type BuyConversation, type BuyLastCheckout, type BuyOrder, type VaultLink, type BuyNextAction, type BuyPick, type BuyStoreChoice, type BuyStorePick, type BuyStores, } from './client.js';
|
|
5
|
+
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 BuyFoundUpdate, type BuyTurnStatus, type BuySegmentUpdate, type BuyCartUpdate, type BuyDeltaUpdate, type BuyStreamHandlers, type Shopper, type BuyRequest, type BuyDeliveryAddress, type BuyTurn, type BuyStatus, type BuyCart, type BuyCartBuilding, type BuyCartLine, type BuyCartFeeLine, type BuyStore, type BuyCatalog, type BuyCatalogItem, type BuyFlightItinerary, type BuyPaymentSource, type BuyConversation, type BuyLastCheckout, type BuyOrder, type VaultLink, type BuyNextAction, type BuyPick, type BuyStoreChoice, type BuyStorePick, type BuyStores, } from './client.js';
|
|
6
6
|
export { BUY_TOOL_NAME, BUY_TOOL_DESCRIPTION, buyToolDefinition, parseBuyToolInput, type BuyToolDefinition, type BuyToolInput, type BuyToolInputSchema, type BuyToolInputCheck, type BuyToolStandardSchema, type OpenAIBuyTool, } from './definition.js';
|
|
7
7
|
export { toMessages, messageStream, typingDelay, type MessagePace, type MessageStream, type MessageStreamOptions, } from './messages.js';
|
|
8
|
-
export { buyTool, nextStepPrompt, type BuyTool, type BuyToolOptions, type BuyToolResult, type BuyToolStatus, type BuyToolLink, type BuyToolFound, type BuyToolProduct, type BuyToolProductAction, productAction, type BuyToolStore, type BuyToolTurn, type BuyToolTurnItem, type BuyToolTurnOptions, type BuyToolTurnMessage, type BuyAckMode, } from './buy-tool.js';
|
|
8
|
+
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';
|
|
9
9
|
export { instantAck, instantReaction, ackFor, keepsYes, ACK_REACTION, type AckForOptions } from './ack.js';
|
|
10
10
|
export { directCall, isProductAsk, type DirectCall, type DirectContext, type DirectReason } from './direct.js';
|
|
11
11
|
export { forModel, forModelError, replyAsWritten, withoutCardList, stripLinks, type BuyToolModelView, type BuyToolModelCart, type BuyToolModelProduct, type BuyToolModelStore, } from './model-view.js';
|
package/dist/model-view.d.ts
CHANGED
|
@@ -122,6 +122,13 @@ export interface BuyToolModelView {
|
|
|
122
122
|
* entirely, and a bare address reads as the site's name (siteName), so the store a reply names stays in it.
|
|
123
123
|
*/
|
|
124
124
|
export declare function stripLinks(text: string): string;
|
|
125
|
+
/**
|
|
126
|
+
* Agentcard's reply as the shopper and the model read it, with every link taken out (stripLinks) and no line left pointing at
|
|
127
|
+
* 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
|
|
128
|
+
* 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:
|
|
129
|
+
* "here's the stool on Wayfair:" reached the shopper with nothing after it.
|
|
130
|
+
*/
|
|
131
|
+
export declare function replyWithoutLinks(reply: string, sent: boolean): string;
|
|
125
132
|
/**
|
|
126
133
|
* An amount in its currency's minor unit, as the model reads it: the currency's own minor units place the
|
|
127
134
|
* decimal point, so 1331 is $13.31 in USD, CA$13.31 in CAD and ¥1,331 in JPY. An amount whose currency
|
|
@@ -162,10 +169,11 @@ export declare function pickId(product: Pick<BuyToolProduct, 'id' | 'ref'>): str
|
|
|
162
169
|
export declare function cardKey(product: Pick<BuyToolProduct, 'id' | 'ref' | 'url'>): string | null;
|
|
163
170
|
/**
|
|
164
171
|
* The key of the card a product gets, or null when it gets none: a product with a page at its store,
|
|
165
|
-
* or a flight option, which
|
|
166
|
-
* taps the flight they want (2026-10-20: three flights listed in text, and no card to tap)
|
|
172
|
+
* or a flight option or a place to book, which can have no page, with an id a pick takes. A flight's card
|
|
173
|
+
* is how the shopper taps the flight they want (2026-10-20: three flights listed in text, and no card to tap),
|
|
174
|
+
* and a place's card is where the shopper reads whether it has room.
|
|
167
175
|
*/
|
|
168
|
-
export declare function productCardKey(product: Pick<BuyToolProduct, 'id' | 'ref' | 'url' | 'flight'>): string | null;
|
|
176
|
+
export declare function productCardKey(product: Pick<BuyToolProduct, 'id' | 'ref' | 'url' | 'flight' | 'availability'>): string | null;
|
|
169
177
|
/** How long until `iso`, from `now`, as the shopper reads it: `about 24 hours`, `about 15 minutes`, `no longer`, or null. */
|
|
170
178
|
export declare function expiresIn(iso: string | null | undefined, now?: number): string | null;
|
|
171
179
|
/**
|
package/dist/model-view.js
CHANGED
|
@@ -59,6 +59,24 @@ export function stripLinks(text) {
|
|
|
59
59
|
.replace(/\n{3,}/g, '\n\n')
|
|
60
60
|
.trim();
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* A line that points at a link with a colon, the link after it on the same line or alone on the next: `here's the stool on
|
|
64
|
+
* Wayfair: https://www.wayfair.com/...`, one in angle brackets, or a bare address with a path or query (`vault.agentcard.sh?token=...`,
|
|
65
|
+
* never a bare `www.walmart.com`, which stripLinks keeps as the store's name). Every such
|
|
66
|
+
* link comes out of what the shopper reads (stripLinks), and the colon would then point at nothing. A Markdown link is not one:
|
|
67
|
+
* stripLinks keeps its label, which the shopper reads after the colon. A question mark that ends the sentence is not a query,
|
|
68
|
+
* as HOST_PATH reads it (`Buy from: glossier.com?`), and a page whose path ends in a bracket keeps it (`stool_(oak)`).
|
|
69
|
+
*/
|
|
70
|
+
const POINTS_AT_LINK = /^([^\n]*?\S)[ \t]*:[ \t]*(?:\n[ \t]*)?<?(?:https?:\/\/|(?:www\.)?(?:[\p{L}\p{N}-]+\.)+\p{L}{2,}(?:[/#]|\?(?![.,;:!?]*(?:[\s<>"'`)\]]|$))))[^\s<>"'`\]]*>?[ \t]*[.!]?[ \t]*$/gimu;
|
|
71
|
+
/**
|
|
72
|
+
* Agentcard's reply as the shopper and the model read it, with every link taken out (stripLinks) and no line left pointing at
|
|
73
|
+
* 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
|
|
74
|
+
* 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:
|
|
75
|
+
* "here's the stool on Wayfair:" reached the shopper with nothing after it.
|
|
76
|
+
*/
|
|
77
|
+
export function replyWithoutLinks(reply, sent) {
|
|
78
|
+
return stripLinks(String(reply ?? '').replace(POINTS_AT_LINK, (_line, lead) => (sent ? `${lead}.` : '')));
|
|
79
|
+
}
|
|
62
80
|
/** A currency's minor units as Intl knows them: 2 for USD and CAD, 0 for JPY. */
|
|
63
81
|
function minorUnits(format) {
|
|
64
82
|
return format.resolvedOptions().maximumFractionDigits ?? 0;
|
|
@@ -135,13 +153,14 @@ export function cardKey(product) {
|
|
|
135
153
|
}
|
|
136
154
|
/**
|
|
137
155
|
* The key of the card a product gets, or null when it gets none: a product with a page at its store,
|
|
138
|
-
* or a flight option, which
|
|
139
|
-
* taps the flight they want (2026-10-20: three flights listed in text, and no card to tap)
|
|
156
|
+
* or a flight option or a place to book, which can have no page, with an id a pick takes. A flight's card
|
|
157
|
+
* is how the shopper taps the flight they want (2026-10-20: three flights listed in text, and no card to tap),
|
|
158
|
+
* and a place's card is where the shopper reads whether it has room.
|
|
140
159
|
*/
|
|
141
160
|
export function productCardKey(product) {
|
|
142
161
|
if (typeof product.url === 'string' && product.url !== '')
|
|
143
162
|
return cardKey(product);
|
|
144
|
-
return product.flight ? pickId(product) : null;
|
|
163
|
+
return product.flight || product.availability ? pickId(product) : null;
|
|
145
164
|
}
|
|
146
165
|
function cartView(cart) {
|
|
147
166
|
return {
|
|
@@ -218,7 +237,7 @@ export function forModel(result) {
|
|
|
218
237
|
status,
|
|
219
238
|
next_action: result.next_action ?? null,
|
|
220
239
|
...(result.cart_building ? { cart_building: { store: stripLinks(result.cart_building.store) } } : {}),
|
|
221
|
-
reply:
|
|
240
|
+
reply: replyWithoutLinks(result.reply, products.some(held) || stores.some(heldStore) || result.link_sent === true),
|
|
222
241
|
cart: result.cart ? cartView(result.cart) : null,
|
|
223
242
|
carts: (result.carts ?? []).map(cartView),
|
|
224
243
|
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.15.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
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|