@agent-cards/commerce-sdk 0.18.4 → 0.18.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -2
- package/dist/action-links.js +1 -0
- package/dist/app-instructions.d.ts +17 -0
- package/dist/app-instructions.generated.d.ts +9 -0
- package/dist/app-instructions.generated.js +10 -0
- package/dist/app-instructions.js +22 -0
- package/dist/buy-tool.d.ts +21 -2
- package/dist/buy-tool.js +150 -67
- package/dist/client.d.ts +84 -11
- package/dist/client.js +101 -15
- package/dist/definition.js +2 -1
- package/dist/direct.d.ts +8 -0
- package/dist/direct.js +13 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -1
- package/dist/message.d.ts +2 -1
- package/dist/model-view.d.ts +29 -4
- package/dist/model-view.js +93 -13
- package/dist/plan.d.ts +41 -0
- package/dist/plan.js +34 -0
- package/dist/shopper-message.d.ts +12 -0
- package/dist/shopper-message.js +10 -0
- package/dist/store-wait.d.ts +27 -0
- package/dist/store-wait.js +41 -0
- package/dist/trip-ask.d.ts +1 -1
- package/dist/trip-ask.js +4 -4
- package/dist/turn-record.d.ts +2 -2
- package/dist/turn-record.js +3 -3
- package/examples/anthropic.mjs +3 -2
- package/package.json +4 -3
package/dist/client.d.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The client the buy tool calls Agentcard with. A company builds one from its
|
|
3
|
+
* client credentials, and every shopper's tool shares the one access token it
|
|
4
|
+
* exchanges them for; or a tool runs on one shopper's own access token. The
|
|
5
|
+
* global fetch is the only transport, so the package keeps no dependencies.
|
|
6
|
+
*
|
|
7
|
+
* The answer fields and the error codes follow the buy module of
|
|
8
|
+
* @agent-cards/sdk (packages/checkout/src/buy.ts: turnOf, buyError), and the
|
|
9
|
+
* token cache follows its client (packages/checkout/src/client.ts accessToken).
|
|
10
|
+
*/
|
|
11
|
+
import { type BuyPlan } from './plan.js';
|
|
1
12
|
import { type AgentcardListener, type ListenOptions } from './listen.js';
|
|
2
|
-
import type
|
|
13
|
+
import { type ShopperMessage } from './shopper-message.js';
|
|
3
14
|
/** The fetch the client calls: Node 18 and later have one built in. */
|
|
4
15
|
export type FetchLike = (url: string, init: {
|
|
5
16
|
method: string;
|
|
@@ -144,6 +155,13 @@ export interface BuyStreamHandlers {
|
|
|
144
155
|
onDelta?: (update: BuyDeltaUpdate) => void | Promise<void>;
|
|
145
156
|
/** Whether the places the turn's cards show have room, once Agentcard's live search says, after the cards it changes. */
|
|
146
157
|
onAvailability?: (update: BuyAvailabilityUpdate) => void | Promise<void>;
|
|
158
|
+
/**
|
|
159
|
+
* The reply is written, after its messages, and the turn stays open only for a slower source's cards, such as a second
|
|
160
|
+
* hotel supplier's, and the one message that says what they added. Comes once, or not at all when nothing is still out.
|
|
161
|
+
*/
|
|
162
|
+
onReplyWritten?: (update: {
|
|
163
|
+
conversationId: string | null;
|
|
164
|
+
}) => void | Promise<void>;
|
|
147
165
|
}
|
|
148
166
|
export declare const DEFAULT_BASE_URL = "https://api.agentcard.sh";
|
|
149
167
|
/** The wait for one purchase turn: past Agentcard's own 110-second budget, so a slow turn answers `turn_timeout` rather than aborting here. */
|
|
@@ -228,6 +246,22 @@ export interface BuyRequest {
|
|
|
228
246
|
resume?: true;
|
|
229
247
|
/** The conversation a confirm of a cart hash belongs to, in place of `resume`. */
|
|
230
248
|
conversation_id?: string;
|
|
249
|
+
/**
|
|
250
|
+
* Your own id for the chat or thread the shopper wrote in, such as a group chat's id (up to 200 characters). The shopper
|
|
251
|
+
* keeps one open purchase per thread: `resume` continues the newest one in this thread only, and a conversation_id from
|
|
252
|
+
* another thread is refused (409 `thread_mismatch`). Leave it out for an app with one chat per shopper.
|
|
253
|
+
*/
|
|
254
|
+
thread_id?: string;
|
|
255
|
+
/**
|
|
256
|
+
* How long, in milliseconds, the reply waits for store searches before Agentcard writes it. Left out, Agentcard keeps
|
|
257
|
+
* its own wait. `buyTool({ storeWaitMs })` sends it on every request.
|
|
258
|
+
*/
|
|
259
|
+
store_wait_ms?: number;
|
|
260
|
+
/**
|
|
261
|
+
* What happens to a store that answers after the reply: `stream` sends its cards after the reply in the same streamed
|
|
262
|
+
* turn, before the answer; `hold` keeps them out of the turn. Left out, Agentcard does as it does today.
|
|
263
|
+
*/
|
|
264
|
+
late_results?: 'stream' | 'hold';
|
|
231
265
|
/** Drop the cart on the table and start a new order. */
|
|
232
266
|
new_order?: true;
|
|
233
267
|
/**
|
|
@@ -529,6 +563,8 @@ export interface BuyTurn {
|
|
|
529
563
|
cart_building: BuyCartBuilding | null;
|
|
530
564
|
/** The choice the reply asks the shopper to make, each group with every value; null when it asks none, and on an older API. */
|
|
531
565
|
choice: BuyChoice | null;
|
|
566
|
+
/** The checkout plan this answer's yes started or moved, with each item's state; null when none, and on an older API. */
|
|
567
|
+
plan: BuyPlan | null;
|
|
532
568
|
/** The answer as Agentcard sent it. */
|
|
533
569
|
raw: Record<string, unknown>;
|
|
534
570
|
}
|
|
@@ -568,8 +604,22 @@ export interface BuyConversation {
|
|
|
568
604
|
last_checkout: BuyLastCheckout | null;
|
|
569
605
|
orders: BuyOrder[];
|
|
570
606
|
carts: BuyCart[];
|
|
607
|
+
/**
|
|
608
|
+
* The reply Agentcard wrote since the shopper's last message, once no turn runs: the answer a call that ended with
|
|
609
|
+
* `turn_timeout` never got. Null while a turn runs, when no reply was written since, and on an older API.
|
|
610
|
+
*/
|
|
611
|
+
latest_reply: BuyLatestReply | null;
|
|
571
612
|
raw: Record<string, unknown>;
|
|
572
613
|
}
|
|
614
|
+
/** A reply a conversation read hands back: its text, its messages, and the products it showed. */
|
|
615
|
+
export interface BuyLatestReply {
|
|
616
|
+
message_id: string;
|
|
617
|
+
text: string;
|
|
618
|
+
messages: string[];
|
|
619
|
+
/** The products the reply showed, when the call that asked ended before it was written; null otherwise. */
|
|
620
|
+
catalog: BuyCatalog | null;
|
|
621
|
+
created_at: string | null;
|
|
622
|
+
}
|
|
573
623
|
/** A Vault link: the page where a shopper connects and adds a card. */
|
|
574
624
|
export interface VaultLink {
|
|
575
625
|
id: string;
|
|
@@ -615,6 +665,10 @@ export declare class AgentcardClient {
|
|
|
615
665
|
private learnedSandbox;
|
|
616
666
|
/** Each shopper's messages that no history the model ran with holds yet, under your own id for them. */
|
|
617
667
|
private readonly inbox;
|
|
668
|
+
/** The chats (`threadId`, '' for a tool built without one) that need a kept message about the shopper alone no more: their history holds it, or their agent answered it (`forgetMessages`, `answered`). */
|
|
669
|
+
private readonly savedInChats;
|
|
670
|
+
/** The same marks by event id, for a copy of the message delivered later, such as a retried webhook's: `delivered` starts that copy with them. */
|
|
671
|
+
private readonly savedInChatsById;
|
|
618
672
|
private readonly deliveredIds;
|
|
619
673
|
private readonly trace;
|
|
620
674
|
constructor(options: AgentcardClientOptions);
|
|
@@ -659,22 +713,41 @@ export declare class AgentcardClient {
|
|
|
659
713
|
delivered(externalUserId: string, message: ShopperMessage): void;
|
|
660
714
|
/**
|
|
661
715
|
* Notes that your agent answers `message` with a turn of its own (`buy.nextStep`) instead of posting
|
|
662
|
-
* its `text`, so the shopper never reads that text
|
|
663
|
-
*
|
|
716
|
+
* its `text`, so the shopper never reads that text in that chat (`threadId`, or the tool built
|
|
717
|
+
* without one). A message about one purchase belongs to one chat, so it leaves the client:
|
|
718
|
+
* `delivered` skips it from now on, and no later `buy.turn` hands it to the model as words the
|
|
719
|
+
* shopper read. A message about the shopper alone, such as a saved card, is answered for that chat
|
|
720
|
+
* only: the shopper's other chats still keep it. `buy.nextStep` calls it.
|
|
664
721
|
*/
|
|
665
|
-
answered(externalUserId: string, message: ShopperMessage): void;
|
|
722
|
+
answered(externalUserId: string, message: ShopperMessage, threadId?: string): void;
|
|
666
723
|
/**
|
|
667
724
|
* Takes back `answered` for a message your agent's own turn could not answer (`BuyToolTurn.release` calls it): once you
|
|
668
|
-
* post its text after all, `delivered` keeps it, and a later `buy.turn` hands it to the model as words the
|
|
725
|
+
* post its text after all, `delivered` keeps it, and a later `buy.turn` in that chat hands it to the model as words the
|
|
726
|
+
* shopper read.
|
|
727
|
+
*/
|
|
728
|
+
unanswered(externalUserId: string, message: ShopperMessage, threadId?: string): void;
|
|
729
|
+
/**
|
|
730
|
+
* The messages kept for this shopper that this chat reads, oldest first: each one about a purchase
|
|
731
|
+
* opened in this chat or about the shopper alone, that reached their chat and that no history
|
|
732
|
+
* `turn.messages` read in this chat has held yet. The chat is `threadId`, or the tool built without
|
|
733
|
+
* one, which reads the purchases opened under no thread. `buy.turn` reads them when it opens.
|
|
734
|
+
*/
|
|
735
|
+
keptMessages(externalUserId: string, threadId?: string): ShopperMessage[];
|
|
736
|
+
/**
|
|
737
|
+
* Forgets these messages for this shopper in this chat (`threadId`, or the tool built without one): `turn.messages`
|
|
738
|
+
* calls it once the history it reads holds them. A message about one purchase leaves the client, since one chat reads
|
|
739
|
+
* it. A message about the shopper alone, such as a saved card, stays kept for the shopper's other chats until their
|
|
740
|
+
* own histories hold it.
|
|
669
741
|
*/
|
|
670
|
-
|
|
742
|
+
forgetMessages(externalUserId: string, messages: readonly ShopperMessage[], threadId?: string): void;
|
|
671
743
|
/**
|
|
672
|
-
*
|
|
673
|
-
*
|
|
744
|
+
* Marks a message about the shopper alone as one this chat needs no more, its history holding it or its agent having
|
|
745
|
+
* answered it, or takes the mark back. The kept copies with the same event id carry the mark too, and so does a copy
|
|
746
|
+
* delivered later (`delivered`), while the client still remembers the id.
|
|
674
747
|
*/
|
|
675
|
-
|
|
676
|
-
/**
|
|
677
|
-
|
|
748
|
+
private markChat;
|
|
749
|
+
/** Removes these messages from the shopper's kept messages, for every chat. */
|
|
750
|
+
private dropKept;
|
|
678
751
|
/** A company names the shopper exactly once; a shopper's own token already names them. */
|
|
679
752
|
private checkShopper;
|
|
680
753
|
private call;
|
package/dist/client.js
CHANGED
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
* @agent-cards/sdk (packages/checkout/src/buy.ts: turnOf, buyError), and the
|
|
9
9
|
* token cache follows its client (packages/checkout/src/client.ts accessToken).
|
|
10
10
|
*/
|
|
11
|
+
import { planOf } from './plan.js';
|
|
11
12
|
import { isRecord } from './verify.js';
|
|
12
13
|
import { listenToAgentcard } from './listen.js';
|
|
14
|
+
import { messageInChat } from './shopper-message.js';
|
|
13
15
|
/** The handlers that ask Agentcard to stream a turn. */
|
|
14
|
-
const STREAM_HANDLERS = ['onProgress', 'onStatus', 'onFound', 'onCatalog', 'onSegment', 'onAck', 'onCart', 'onDelta', 'onAvailability'];
|
|
16
|
+
const STREAM_HANDLERS = ['onProgress', 'onStatus', 'onFound', 'onCatalog', 'onSegment', 'onAck', 'onCart', 'onDelta', 'onAvailability', 'onReplyWritten'];
|
|
15
17
|
const PROGRESS_STAGES = new Set(['searching', 'building_cart', 'checking_out', 'working']);
|
|
16
18
|
export const DEFAULT_BASE_URL = 'https://api.agentcard.sh';
|
|
17
19
|
/** 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 +303,7 @@ export function turnOf(body) {
|
|
|
301
303
|
store_pick: isRecord(body.store_pick) && str(body.store_pick.store_id) ? { store_id: body.store_pick.store_id, name: str(body.store_pick.name), selected: body.store_pick.selected === true } : null,
|
|
302
304
|
cart_building: cartBuildingOf(body.cart_building),
|
|
303
305
|
choice: choiceOf(body.choice),
|
|
306
|
+
plan: planOf(body.plan),
|
|
304
307
|
raw: body,
|
|
305
308
|
};
|
|
306
309
|
}
|
|
@@ -357,9 +360,23 @@ function conversationOf(body) {
|
|
|
357
360
|
placed_at: str(o.placed_at),
|
|
358
361
|
})) : [],
|
|
359
362
|
carts: cartsOf(body.carts),
|
|
363
|
+
latest_reply: latestReplyOf(body.latest_reply),
|
|
360
364
|
raw: body,
|
|
361
365
|
};
|
|
362
366
|
}
|
|
367
|
+
/** The reply a conversation read hands back, or null when it names no text. */
|
|
368
|
+
function latestReplyOf(value) {
|
|
369
|
+
if (!isRecord(value) || !str(value.text)?.trim())
|
|
370
|
+
return null;
|
|
371
|
+
const messages = Array.isArray(value.messages) ? value.messages.filter((m) => typeof m === 'string' && m.trim() !== '') : [];
|
|
372
|
+
return {
|
|
373
|
+
message_id: str(value.message_id) ?? '',
|
|
374
|
+
text: value.text,
|
|
375
|
+
messages: messages.length > 0 ? messages : [value.text],
|
|
376
|
+
catalog: catalogOf(value.catalog),
|
|
377
|
+
created_at: str(value.created_at),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
363
380
|
/** An error code as the API spells one: lower-case words joined by underscores. */
|
|
364
381
|
const CODE = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)+$/;
|
|
365
382
|
/**
|
|
@@ -463,7 +480,7 @@ async function eachEvent(res, onEvent) {
|
|
|
463
480
|
}
|
|
464
481
|
/**
|
|
465
482
|
* A streamed purchase turn, read to its `envelope`: the same answer a JSON turn returns, with its
|
|
466
|
-
* status in `http_status`. Each `progress`, `status`, `delta`, `catalog`, `find`, `segment`, `cart` and `venue_availability` goes to its handler
|
|
483
|
+
* status in `http_status`. Each `progress`, `status`, `delta`, `catalog`, `find`, `segment`, `reply_written`, `cart` and `venue_availability` goes to its handler
|
|
467
484
|
* on the way, and any other event, such as a `ping` or a `tool`, is read past. A stream that breaks or
|
|
468
485
|
* ends before its envelope, once it named the conversation, reads as a turn past its budget
|
|
469
486
|
* (`turn_timeout`), so the tool reads the conversation for what the turn left instead of losing it.
|
|
@@ -542,6 +559,9 @@ async function readStreamedTurn(res, stream, timeoutMs) {
|
|
|
542
559
|
else if (event === 'segment' && typeof data.text === 'string' && data.text.trim() !== '' && stream.onSegment) {
|
|
543
560
|
deliver(() => stream.onSegment({ conversationId: named, text: data.text }));
|
|
544
561
|
}
|
|
562
|
+
else if (event === 'reply_written' && stream.onReplyWritten) {
|
|
563
|
+
deliver(() => stream.onReplyWritten({ conversationId: named }));
|
|
564
|
+
}
|
|
545
565
|
else if (event === 'ack' && typeof data.text === 'string' && data.text.trim() !== '' && stream.onAck) {
|
|
546
566
|
deliver(() => stream.onAck({ conversationId: named, text: data.text }));
|
|
547
567
|
}
|
|
@@ -586,6 +606,10 @@ export class AgentcardClient {
|
|
|
586
606
|
learnedSandbox = null;
|
|
587
607
|
/** Each shopper's messages that no history the model ran with holds yet, under your own id for them. */
|
|
588
608
|
inbox = new Map();
|
|
609
|
+
/** The chats (`threadId`, '' for a tool built without one) that need a kept message about the shopper alone no more: their history holds it, or their agent answered it (`forgetMessages`, `answered`). */
|
|
610
|
+
savedInChats = new WeakMap();
|
|
611
|
+
/** The same marks by event id, for a copy of the message delivered later, such as a retried webhook's: `delivered` starts that copy with them. */
|
|
612
|
+
savedInChatsById = new Map();
|
|
589
613
|
deliveredIds = new Set();
|
|
590
614
|
trace;
|
|
591
615
|
constructor(options) {
|
|
@@ -730,6 +754,10 @@ export class AgentcardClient {
|
|
|
730
754
|
this.deliveredIds.delete(this.deliveredIds.values().next().value);
|
|
731
755
|
}
|
|
732
756
|
const kept = this.inbox.get(externalUserId) ?? [];
|
|
757
|
+
// A copy delivered again, such as a retried webhook's, keeps the chats that already answered it.
|
|
758
|
+
const marks = str(message.eventId) ? this.savedInChatsById.get(message.eventId) : undefined;
|
|
759
|
+
if (marks)
|
|
760
|
+
this.savedInChats.set(message, new Set(marks));
|
|
733
761
|
// The shopper written to last moves to the end, so the one dropped first is the one written to longest ago.
|
|
734
762
|
this.inbox.delete(externalUserId);
|
|
735
763
|
this.inbox.set(externalUserId, [...kept, message].slice(-DELIVERED_PER_SHOPPER));
|
|
@@ -738,14 +766,21 @@ export class AgentcardClient {
|
|
|
738
766
|
}
|
|
739
767
|
/**
|
|
740
768
|
* Notes that your agent answers `message` with a turn of its own (`buy.nextStep`) instead of posting
|
|
741
|
-
* its `text`, so the shopper never reads that text
|
|
742
|
-
*
|
|
769
|
+
* its `text`, so the shopper never reads that text in that chat (`threadId`, or the tool built
|
|
770
|
+
* without one). A message about one purchase belongs to one chat, so it leaves the client:
|
|
771
|
+
* `delivered` skips it from now on, and no later `buy.turn` hands it to the model as words the
|
|
772
|
+
* shopper read. A message about the shopper alone, such as a saved card, is answered for that chat
|
|
773
|
+
* only: the shopper's other chats still keep it. `buy.nextStep` calls it.
|
|
743
774
|
*/
|
|
744
|
-
answered(externalUserId, message) {
|
|
775
|
+
answered(externalUserId, message, threadId) {
|
|
745
776
|
if (!str(externalUserId))
|
|
746
777
|
throw new TypeError('Pass externalUserId: your own id for the shopper the message is for.');
|
|
747
778
|
if (!isRecord(message))
|
|
748
779
|
throw new TypeError('Pass the message onMessage got.');
|
|
780
|
+
if (!message.threadId && !message.conversationId) {
|
|
781
|
+
this.markChat(externalUserId, message, threadId ?? '', true);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
749
784
|
if (str(message.eventId)) {
|
|
750
785
|
this.deliveredIds.add(message.eventId);
|
|
751
786
|
if (this.deliveredIds.size > DELIVERED_IDS_KEPT)
|
|
@@ -753,31 +788,82 @@ export class AgentcardClient {
|
|
|
753
788
|
}
|
|
754
789
|
const kept = this.inbox.get(externalUserId);
|
|
755
790
|
if (kept)
|
|
756
|
-
this.
|
|
791
|
+
this.dropKept(externalUserId, kept.filter((m) => m === message || (!!message.eventId && m.eventId === message.eventId)));
|
|
757
792
|
}
|
|
758
793
|
/**
|
|
759
794
|
* Takes back `answered` for a message your agent's own turn could not answer (`BuyToolTurn.release` calls it): once you
|
|
760
|
-
* post its text after all, `delivered` keeps it, and a later `buy.turn` hands it to the model as words the
|
|
795
|
+
* post its text after all, `delivered` keeps it, and a later `buy.turn` in that chat hands it to the model as words the
|
|
796
|
+
* shopper read.
|
|
761
797
|
*/
|
|
762
|
-
unanswered(externalUserId, message) {
|
|
798
|
+
unanswered(externalUserId, message, threadId) {
|
|
763
799
|
if (!str(externalUserId))
|
|
764
800
|
throw new TypeError('Pass externalUserId: your own id for the shopper the message is for.');
|
|
765
801
|
if (!isRecord(message))
|
|
766
802
|
throw new TypeError('Pass the message onMessage got.');
|
|
803
|
+
if (!message.threadId && !message.conversationId) {
|
|
804
|
+
this.markChat(externalUserId, message, threadId ?? '', false);
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
767
807
|
if (str(message.eventId))
|
|
768
808
|
this.deliveredIds.delete(message.eventId);
|
|
769
809
|
}
|
|
770
810
|
/**
|
|
771
|
-
* The messages kept for this shopper, oldest first: each one
|
|
772
|
-
*
|
|
811
|
+
* The messages kept for this shopper that this chat reads, oldest first: each one about a purchase
|
|
812
|
+
* opened in this chat or about the shopper alone, that reached their chat and that no history
|
|
813
|
+
* `turn.messages` read in this chat has held yet. The chat is `threadId`, or the tool built without
|
|
814
|
+
* one, which reads the purchases opened under no thread. `buy.turn` reads them when it opens.
|
|
773
815
|
*/
|
|
774
|
-
keptMessages(externalUserId) {
|
|
775
|
-
|
|
816
|
+
keptMessages(externalUserId, threadId) {
|
|
817
|
+
const chat = threadId ?? '';
|
|
818
|
+
return (this.inbox.get(externalUserId) ?? []).filter((m) => messageInChat(m, threadId) && !this.savedInChats.get(m)?.has(chat));
|
|
776
819
|
}
|
|
777
|
-
/**
|
|
778
|
-
|
|
820
|
+
/**
|
|
821
|
+
* Forgets these messages for this shopper in this chat (`threadId`, or the tool built without one): `turn.messages`
|
|
822
|
+
* calls it once the history it reads holds them. A message about one purchase leaves the client, since one chat reads
|
|
823
|
+
* it. A message about the shopper alone, such as a saved card, stays kept for the shopper's other chats until their
|
|
824
|
+
* own histories hold it.
|
|
825
|
+
*/
|
|
826
|
+
forgetMessages(externalUserId, messages, threadId) {
|
|
827
|
+
const chat = threadId ?? '';
|
|
828
|
+
this.dropKept(externalUserId, messages.filter((m) => {
|
|
829
|
+
if (m.threadId || m.conversationId)
|
|
830
|
+
return true;
|
|
831
|
+
this.markChat(externalUserId, m, chat, true);
|
|
832
|
+
return false;
|
|
833
|
+
}));
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Marks a message about the shopper alone as one this chat needs no more, its history holding it or its agent having
|
|
837
|
+
* answered it, or takes the mark back. The kept copies with the same event id carry the mark too, and so does a copy
|
|
838
|
+
* delivered later (`delivered`), while the client still remembers the id.
|
|
839
|
+
*/
|
|
840
|
+
markChat(externalUserId, message, chat, done) {
|
|
841
|
+
const id = str(message.eventId);
|
|
842
|
+
const mark = (chats) => {
|
|
843
|
+
if (done)
|
|
844
|
+
chats.add(chat);
|
|
845
|
+
else
|
|
846
|
+
chats.delete(chat);
|
|
847
|
+
return chats;
|
|
848
|
+
};
|
|
849
|
+
const same = (m) => m === message || (!!id && m.eventId === id);
|
|
850
|
+
for (const m of new Set([message, ...(this.inbox.get(externalUserId) ?? []).filter(same)])) {
|
|
851
|
+
this.savedInChats.set(m, mark(this.savedInChats.get(m) ?? new Set()));
|
|
852
|
+
}
|
|
853
|
+
if (!id)
|
|
854
|
+
return;
|
|
855
|
+
// The id marked last moves to the end, so the one dropped first is the one marked longest ago. A kept copy keeps its
|
|
856
|
+
// own marks after its id is dropped.
|
|
857
|
+
const chats = mark(this.savedInChatsById.get(id) ?? new Set());
|
|
858
|
+
this.savedInChatsById.delete(id);
|
|
859
|
+
this.savedInChatsById.set(id, chats);
|
|
860
|
+
if (this.savedInChatsById.size > DELIVERED_IDS_KEPT)
|
|
861
|
+
this.savedInChatsById.delete(this.savedInChatsById.keys().next().value);
|
|
862
|
+
}
|
|
863
|
+
/** Removes these messages from the shopper's kept messages, for every chat. */
|
|
864
|
+
dropKept(externalUserId, messages) {
|
|
779
865
|
const kept = this.inbox.get(externalUserId);
|
|
780
|
-
if (!kept)
|
|
866
|
+
if (!kept || messages.length === 0)
|
|
781
867
|
return;
|
|
782
868
|
const left = kept.filter((m) => !messages.includes(m));
|
|
783
869
|
if (left.length > 0)
|
package/dist/definition.js
CHANGED
|
@@ -52,7 +52,8 @@ export const BUY_TOOL_RULES = [
|
|
|
52
52
|
'Name each link by what it is for, from `next_action`: the link to add a card, to sign in to the store, or to approve the payment.',
|
|
53
53
|
'Within a step, ask for one detail per message, such as the delivery address before the name for the label.',
|
|
54
54
|
'Write every sentence in your own style, casing included, even where `reply` is written another way.',
|
|
55
|
-
"When the shopper asks what you can do or which stores you shop at, answer without calling this tool and with no line saying you are looking: one message that names at most four stores plus the kinds of things you can do, such as food delivery and restaurant reservations, then one that asks what they need. Every shopper can buy from Amazon, Walmart, Target and
|
|
55
|
+
"When the shopper asks what you can do or which stores you shop at, answer without calling this tool and with no line saying you are looking: one message that names at most four stores plus the kinds of things you can do, such as food delivery and restaurant reservations, then one that asks what they need. Every shopper can buy from Amazon, Walmart, Target, Best Buy and thousands more stores, so name those four and say \"and thousands more stores\" every time you name the stores you shop, in a reply about one kind of thing too, such as \"travel gear from Amazon, Target and thousands more stores\": never let the four read as the whole list, never say \"plenty more\", \"many more\" or \"most online stores\", and never give a number of stores. Name no other store unless a result showed it.",
|
|
56
|
+
"Never tell the shopper you can't render or show Markdown, a table or other formatting, and never say you keep to plain text: how your words look is your app's to decide.",
|
|
56
57
|
'Each paragraph you write reaches the shopper as its own message: give each one job and keep it under about 20 words. On a cart turn, write the confirmation and the one next step, such as "added a little cheeseburger from five guys, about $12.17 before delivery and tax" and then "next, sign in to doordash with the link so i can place it": one total, never a price breakdown your app\'s cart already shows (an app that shows no cart lists each line with its price) and never a fee by its name, rate or formula, such as an order fee, card processing or 2.9% + $0.30, even when `reply` or `fee_lines` names it, unless the shopper asks what the fees are, no promise to show the final total later, and nothing about passkeys, master passwords or Agentcard not seeing the card number unless the shopper asks.',
|
|
57
58
|
'Never listing products again is for offering choices: when you confirm what is in the cart, name each line by its item, such as "1 Dawn Ultra dish soap, 38 oz", never "the one you picked, shown in the card".',
|
|
58
59
|
'When you list options or choices for the shopper, such as sizes, breads, times, or stores no card shows, write one per line, each starting with "• ", in the same paragraph as the line that asks which one, so the question and its choices reach the shopper as one message. When the result has `choice`, list every value it holds that way, with its extra price, such as "• Gluten-free bread (+$2.46)", even when `reply` names them inside a sentence. The products and stores the cards show are still never listed again.',
|
package/dist/direct.d.ts
CHANGED
|
@@ -36,6 +36,14 @@ export interface DirectContext {
|
|
|
36
36
|
productId: string | null;
|
|
37
37
|
name: string;
|
|
38
38
|
}>;
|
|
39
|
+
/**
|
|
40
|
+
* Every product card an earlier answer of this conversation showed, each once, newest first. A card's tap whose name no card
|
|
41
|
+
* of `tappable` holds is read against these, so a tap on a card two answers back still sends its `product_id`.
|
|
42
|
+
*/
|
|
43
|
+
shownBefore?: ReadonlyArray<{
|
|
44
|
+
productId: string | null;
|
|
45
|
+
name: string;
|
|
46
|
+
}>;
|
|
39
47
|
/** The stores Agentcard's last answer showed, with the `store_id` a pick sends. */
|
|
40
48
|
stores?: ReadonlyArray<{
|
|
41
49
|
storeId: string;
|
package/dist/direct.js
CHANGED
|
@@ -185,14 +185,25 @@ function pickOf(raw, bare, context) {
|
|
|
185
185
|
if (named.length > 1)
|
|
186
186
|
return call({});
|
|
187
187
|
}
|
|
188
|
+
// A tap on a card an earlier answer showed, after an answer that showed no cards: it is read against every card shown before.
|
|
189
|
+
const tapNow = byName.length === 0 ? tappedName(raw) : null;
|
|
190
|
+
if (tapNow !== null) {
|
|
191
|
+
const before = (context.shownBefore ?? []).filter((product) => product.productId && [nameWords(tapNow), nameWords(`the ${tapNow}`)].includes(nameWords(product.name)));
|
|
192
|
+
if (before.length > 0)
|
|
193
|
+
return call(new Set(before.map((product) => product.productId)).size === 1 ? { product_id: before[0].productId } : {});
|
|
194
|
+
}
|
|
188
195
|
if (byName.length > 0) {
|
|
189
196
|
// A card's tap names the product whole, `I'll take the Burrito Bowl ($15.15)`: the product of exactly that name is the pick,
|
|
190
197
|
// never the Burrito whose one word it shares, and a tap that names no product here (a card this list does not hold), or two
|
|
191
198
|
// alike, sends none, so Agentcard reads the words against every card it showed.
|
|
192
199
|
const tap = tappedName(raw);
|
|
193
200
|
if (tap !== null) {
|
|
194
|
-
const
|
|
195
|
-
|
|
201
|
+
const named = (cards) => cards.filter((product) => [nameWords(tap), nameWords(`the ${tap}`)].includes(nameWords(product.name)));
|
|
202
|
+
const exact = named(byName);
|
|
203
|
+
// A tap on a card an earlier answer showed names a card of no newer list: it is read against every card shown before.
|
|
204
|
+
const before = exact.length === 0 ? named(context.shownBefore ?? []).filter((product) => product.productId) : [];
|
|
205
|
+
const card = exact.length === 1 ? exact[0] : exact.length === 0 && new Set(before.map((product) => product.productId)).size === 1 ? before[0] : null;
|
|
206
|
+
return call(card ? { product_id: card.productId } : {});
|
|
196
207
|
}
|
|
197
208
|
// A product's whole name inside the message: `I want the KODAK AA Alkaline Batteries (24 Pack)`.
|
|
198
209
|
const said = nameWords(raw);
|
package/dist/index.d.ts
CHANGED
|
@@ -3,17 +3,20 @@ export { readPurchaseMessage, type PurchaseMessage, type PurchaseMessageKind, ty
|
|
|
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 { ORDER_STEPS, updateOrder, arrivalText, type ShopperOrder, type OrderStep, type OrderPathStep, type OrderStepState, type ArrivalTextOptions, } from './order.js';
|
|
5
5
|
export { agentcardWebhook, purchaseAgentWebhook, type AgentcardWebhookOptions, type AgentcardWebhookHandler, type PurchaseAgentWebhookOptions, type WebhookFetchRequest, type WebhookRequest, type WebhookResponse, } from './handler.js';
|
|
6
|
-
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 BuyLastAnswer, 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, type BuyChoice, type BuyChoiceGroup, type BuyChoiceDelivery, type BuyChoiceOption, } from './client.js';
|
|
6
|
+
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 BuyLastAnswer, 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 BuyLatestReply, type BuyLastCheckout, type BuyOrder, type VaultLink, type BuyNextAction, type BuyPick, type BuyStoreChoice, type BuyStorePick, type BuyStores, type BuyChoice, type BuyChoiceGroup, type BuyChoiceDelivery, type BuyChoiceOption, } from './client.js';
|
|
7
7
|
export { BUY_TOOL_NAME, BUY_TOOL_DESCRIPTION, buyToolDefinition, parseBuyToolInput, type BuyToolDefinition, type BuyToolInput, type BuyToolInputSchema, type BuyToolInputCheck, type BuyToolStandardSchema, type OpenAIBuyTool, } from './definition.js';
|
|
8
8
|
export { toMessages, messageStream, typingDelay, type MessagePace, type MessageStream, type MessageStreamOptions, } from './messages.js';
|
|
9
9
|
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';
|
|
10
10
|
export { instantAck, instantReaction, ackFor, keepsYes, ACK_REACTION, type AckForOptions } from './ack.js';
|
|
11
11
|
export { directCall, isProductAsk, type DirectCall, type DirectContext, type DirectReason } from './direct.js';
|
|
12
|
-
export { forModel, forModelError, stepAsWritten, withoutCardList, stripLinks, type BuyToolModelView, type BuyToolModelCart, type BuyToolModelProduct, type BuyToolModelStore, type ShownCard, } from './model-view.js';
|
|
12
|
+
export { forModel, grandTotals, forModelError, stepAsWritten, withoutCardList, stripLinks, type BuyToolModelView, type BuyToolModelCart, type BuyToolGrandTotal, type BuyToolModelProduct, type BuyToolModelStore, type ShownCard, } from './model-view.js';
|
|
13
13
|
export { asBullets, choiceLines, replyAsWritten, textingStyle, withChoice, type ReplyAsWrittenOptions } from './texting.js';
|
|
14
|
+
export { appInstructions, APP_INSTRUCTIONS_VERSION, type AppInstructionsOptions } from './app-instructions.js';
|
|
14
15
|
export { toAnthropicTool, toOpenRouterTool, type AnthropicBuyTool, type OpenRouterToolkit, type ZodLike, } from './adapters.js';
|
|
15
16
|
export { listenToAgentcard, type AgentcardListener, type ListenOptions, type ListenCursorStore, type StreamFetch, } from './listen.js';
|
|
16
17
|
export { ACTION_LINK_ENDINGS, ACTION_LINK_KINDS, actionLinkId, actionLinkOutcome, isActionLink, type ActionLinkKind, type ActionLinkMessage, type ActionLinkOutcome, } from './action-links.js';
|
|
17
18
|
export { AGENTCARD_EVENT_TYPES, type AgentcardEventType, } from './events.js';
|
|
18
19
|
export { turnStream, readTurn, TURN_RECORD_TTL_SECONDS, type TurnStream, type TurnStreamOptions } from './turn-stream.js';
|
|
19
20
|
export { TURN_RECOVERY_ON, isTurnId, type TurnEvent, type TurnRecord, type TurnRecordStore } from './turn-record.js';
|
|
21
|
+
export type { BuyLateResults, BuyStoreWait } from './store-wait.js';
|
|
22
|
+
export { planOf, type BuyPlan, type BuyPlanItem, type BuyPlanItemState, type BuyPlanStatus } from './plan.js';
|
package/dist/index.js
CHANGED
|
@@ -9,11 +9,13 @@ export { toMessages, messageStream, typingDelay, } from './messages.js';
|
|
|
9
9
|
export { buyTool, nextStepPrompt, productAction, } from './buy-tool.js';
|
|
10
10
|
export { instantAck, instantReaction, ackFor, keepsYes, ACK_REACTION } from './ack.js';
|
|
11
11
|
export { directCall, isProductAsk } from './direct.js';
|
|
12
|
-
export { forModel, forModelError, stepAsWritten, withoutCardList, stripLinks, } from './model-view.js';
|
|
12
|
+
export { forModel, grandTotals, forModelError, stepAsWritten, withoutCardList, stripLinks, } from './model-view.js';
|
|
13
13
|
export { asBullets, choiceLines, replyAsWritten, textingStyle, withChoice } from './texting.js';
|
|
14
|
+
export { appInstructions, APP_INSTRUCTIONS_VERSION } from './app-instructions.js';
|
|
14
15
|
export { toAnthropicTool, toOpenRouterTool, } from './adapters.js';
|
|
15
16
|
export { listenToAgentcard, } from './listen.js';
|
|
16
17
|
export { ACTION_LINK_ENDINGS, ACTION_LINK_KINDS, actionLinkId, actionLinkOutcome, isActionLink, } from './action-links.js';
|
|
17
18
|
export { AGENTCARD_EVENT_TYPES, } from './events.js';
|
|
18
19
|
export { turnStream, readTurn, TURN_RECORD_TTL_SECONDS } from './turn-stream.js';
|
|
19
20
|
export { TURN_RECOVERY_ON, isTurnId } from './turn-record.js';
|
|
21
|
+
export { planOf } from './plan.js';
|
package/dist/message.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { type AgentcardEvent } from './verify.js';
|
|
|
10
10
|
* - `order_failed`: the order failed after the approval, and nothing more happens for that approval.
|
|
11
11
|
* - `order_not_placed_yet`: the shopper approved, and the order is not placed yet.
|
|
12
12
|
* - `order_unconfirmed`: the store's reply to the order was lost, so the order may exist; Agentcard is confirming it.
|
|
13
|
+
* - `order_placing`: the shopper approved, and Agentcard is placing the order now; one of the order kinds above follows.
|
|
13
14
|
* - `approval_expiring`: the approval link expires soon; `text` carries the link.
|
|
14
15
|
* - `approval_expired`: the approval expired before the shopper answered, and nothing was charged.
|
|
15
16
|
* - `approval_declined`: the shopper declined the approval, and nothing was charged.
|
|
@@ -25,7 +26,7 @@ import { type AgentcardEvent } from './verify.js';
|
|
|
25
26
|
* - `cart_needs_input`: that checkout asks the shopper something first, such as a size, a delivery address, a sign-in to the store or an approval of the most the order can cost.
|
|
26
27
|
* - `cart_failed`: that checkout built no cart, and nothing was ordered or charged.
|
|
27
28
|
*/
|
|
28
|
-
export type PurchaseMessageKind = 'order_placed' | 'order_failed' | 'order_not_placed_yet' | 'order_unconfirmed' | 'approval_expiring' | 'approval_expired' | 'approval_declined' | 'payment_declined' | 'card_link_unfinished' | 'card_link_expired' | 'cart_idle' | 'card_added' | 'store_connected' | 'store_connect_failed' | 'store_connect_expired' | 'cart_ready' | 'cart_needs_input' | 'cart_failed';
|
|
29
|
+
export type PurchaseMessageKind = 'order_placed' | 'order_failed' | 'order_not_placed_yet' | 'order_unconfirmed' | 'order_placing' | 'approval_expiring' | 'approval_expired' | 'approval_declined' | 'payment_declined' | 'card_link_unfinished' | 'card_link_expired' | 'cart_idle' | 'card_added' | 'store_connected' | 'store_connect_failed' | 'store_connect_expired' | 'cart_ready' | 'cart_needs_input' | 'cart_failed';
|
|
29
30
|
/** The message's values one by one, for a company that writes its own copy. A fact that does not apply is absent. */
|
|
30
31
|
export interface PurchaseFacts {
|
|
31
32
|
merchantName?: string;
|
package/dist/model-view.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `product_links_sent` for the products' store pages, and `link_sent` on each
|
|
8
8
|
* store and `store_links_sent` for the pages of the stores to pick from.
|
|
9
9
|
*/
|
|
10
|
-
import type { BuyNextAction } from './client.js';
|
|
10
|
+
import type { BuyCart, BuyNextAction } from './client.js';
|
|
11
11
|
import type { BuyToolProduct, BuyToolResult, BuyToolStatus, BuyToolStore } from './buy-tool.js';
|
|
12
12
|
export interface BuyToolModelCart {
|
|
13
13
|
store: string;
|
|
@@ -60,6 +60,22 @@ export interface BuyToolModelStore {
|
|
|
60
60
|
/** True when the shopper holds this store's page, which `sendLink` delivered beside the reply. */
|
|
61
61
|
link_sent: boolean;
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* What every store cart on the table adds up to, in one currency: the stores' totals summed in minor units, each store's
|
|
65
|
+
* tip, fees and tax counted once as its `total` counts them. `is_estimate` is true when any store's total is an estimate,
|
|
66
|
+
* and `leaves_out` names what the estimated totals leave out together; null when every store priced its cart.
|
|
67
|
+
*/
|
|
68
|
+
export interface BuyToolGrandTotal {
|
|
69
|
+
amount: string;
|
|
70
|
+
/** The sum in the currency's minor unit, such as cents. */
|
|
71
|
+
amount_cents: number;
|
|
72
|
+
/** The ISO 4217 code, upper case, or null when Agentcard named none for these carts. */
|
|
73
|
+
currency: string | null;
|
|
74
|
+
is_estimate: boolean;
|
|
75
|
+
leaves_out: string | null;
|
|
76
|
+
/** How many store carts this total adds up. */
|
|
77
|
+
stores: number;
|
|
78
|
+
}
|
|
63
79
|
/** The buy tool's answer as the model reads it. */
|
|
64
80
|
export interface BuyToolModelView {
|
|
65
81
|
status: BuyToolStatus;
|
|
@@ -78,6 +94,8 @@ export interface BuyToolModelView {
|
|
|
78
94
|
cart: BuyToolModelCart | null;
|
|
79
95
|
/** Every cart on the table, the newest last, when the shopper is buying from more than one store. */
|
|
80
96
|
carts: BuyToolModelCart[];
|
|
97
|
+
/** What the carts add up to, one entry per currency, in the order the carts first name it; empty when no cart is on the table. */
|
|
98
|
+
grand_totals: BuyToolGrandTotal[];
|
|
81
99
|
products: BuyToolModelProduct[];
|
|
82
100
|
approval_expires_at: string | null;
|
|
83
101
|
/**
|
|
@@ -197,6 +215,11 @@ export declare function cardKey(product: Pick<BuyToolProduct, 'id' | 'ref' | 'ur
|
|
|
197
215
|
* and a place's card is where the shopper reads whether it has room.
|
|
198
216
|
*/
|
|
199
217
|
export declare function productCardKey(product: Pick<BuyToolProduct, 'id' | 'ref' | 'url' | 'flight' | 'availability'>): string | null;
|
|
218
|
+
/**
|
|
219
|
+
* The carts' grand totals, one per currency, summed from each cart's `totalCents`, which already holds its tip, fees and
|
|
220
|
+
* tax: a display string is never parsed. Carts in two currencies are never added together.
|
|
221
|
+
*/
|
|
222
|
+
export declare function grandTotals(carts: readonly BuyCart[]): BuyToolGrandTotal[];
|
|
200
223
|
/** How long until `iso`, from `now`, as the shopper reads it: `about 24 hours`, `about 15 minutes`, `no longer`, or null. */
|
|
201
224
|
export declare function expiresIn(iso: string | null | undefined, now?: number): string | null;
|
|
202
225
|
/**
|
|
@@ -243,9 +266,11 @@ export interface ShownCard {
|
|
|
243
266
|
* points at a number the list gave (`the dishwand in #8`), a sentence that goes on to more of the list (`other 6-packs
|
|
244
267
|
* are ...`, `there's also ...`) and names a card, and, past the one or two products a reply recommends in words, each
|
|
245
268
|
* further sentence that names another card. A list written on one line goes the same way, item by item (inlineList),
|
|
246
|
-
* and so does a list line written without a bullet, whole, up to the question on it.
|
|
247
|
-
*
|
|
248
|
-
*
|
|
269
|
+
* and so does a list line written without a bullet, whole, up to the question on it. The lines set in under a list
|
|
270
|
+
* line that goes, its details such as ` - ships from London`, go with it, and once the list went, a sentence that only
|
|
271
|
+
* points back at it, `both are priced in foreign currency`, goes when the cards show another number than it counts.
|
|
272
|
+
* What is left is the reply around the list, such as `found these at amazon.` and `which one do you want?`. A reply
|
|
273
|
+
* whose cards went out names none of them twice over. A list that keeps an item for a product no card shows keeps its lead-in, and the two read as one
|
|
249
274
|
* message (toMessages), so the shopper never reads `two options came in under $20:` on its own.
|
|
250
275
|
*
|
|
251
276
|
* Each card is its name, or its name and its price as the model reads it (`{ name, price: '$22.89' }`): with the price,
|