@agent-cards/commerce-sdk 0.2.0 → 0.8.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 +157 -4
- package/dist/ack.d.ts +51 -0
- package/dist/ack.js +166 -0
- package/dist/adapters.d.ts +4 -1
- package/dist/adapters.js +1 -0
- package/dist/buy-tool.d.ts +63 -3
- package/dist/buy-tool.js +227 -21
- package/dist/client.d.ts +78 -7
- package/dist/client.js +147 -13
- package/dist/definition.d.ts +15 -4
- package/dist/definition.js +20 -7
- package/dist/events.d.ts +12 -0
- package/dist/events.js +79 -0
- package/dist/handler.d.ts +58 -5
- package/dist/handler.js +207 -46
- package/dist/index.d.ts +7 -3
- package/dist/index.js +6 -2
- package/dist/listen.d.ts +93 -0
- package/dist/listen.js +496 -0
- package/dist/message.d.ts +3 -1
- package/dist/model-view.d.ts +25 -5
- package/dist/model-view.js +48 -10
- package/dist/shopper-message.d.ts +86 -0
- package/dist/shopper-message.js +175 -0
- package/package.json +2 -2
package/dist/client.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* token cache follows its client (packages/checkout/src/client.ts accessToken).
|
|
10
10
|
*/
|
|
11
11
|
import { isRecord } from './verify.js';
|
|
12
|
+
import { listenToAgentcard } from './listen.js';
|
|
13
|
+
const PROGRESS_STAGES = new Set(['searching', 'building_cart', 'checking_out', 'working']);
|
|
12
14
|
export const DEFAULT_BASE_URL = 'https://api.agentcard.sh';
|
|
13
15
|
/** The wait for one purchase turn: past Agentcard's own 110-second budget, so a slow turn answers `turn_timeout` rather than aborting here. */
|
|
14
16
|
export const BUY_TURN_TIMEOUT_MS = 130_000;
|
|
@@ -20,7 +22,7 @@ const NEXT_ACTIONS = new Set(['shipping_details', 'guest_details', 'add_card', '
|
|
|
20
22
|
/**
|
|
21
23
|
* A refusal, or a failure to reach Agentcard. `code` is the API's own
|
|
22
24
|
* (`user_not_connected`, `external_user_id_ambiguous`, `turn_in_progress`,
|
|
23
|
-
* `user_id_mismatch`), one read from the shape of the answer
|
|
25
|
+
* `user_id_mismatch`, `pick_not_found`), one read from the shape of the answer
|
|
24
26
|
* (`cart_changed`, `nothing_to_confirm`, `conversation_closed`,
|
|
25
27
|
* `conversation_not_found`), or the client's (`unreachable`, `timeout`,
|
|
26
28
|
* `oauth_failed`, `unauthorized`, `http_<status>`). `status` is 0 when
|
|
@@ -118,6 +120,7 @@ function catalogOf(v) {
|
|
|
118
120
|
const flight = flightOf(i.flight);
|
|
119
121
|
return {
|
|
120
122
|
id: str(i.id) ?? '',
|
|
123
|
+
ref: str(i.ref),
|
|
121
124
|
name: str(i.name),
|
|
122
125
|
priceCents: num(i.priceCents),
|
|
123
126
|
currency: str(i.currency),
|
|
@@ -127,7 +130,7 @@ function catalogOf(v) {
|
|
|
127
130
|
url: pageUrl(i.url),
|
|
128
131
|
};
|
|
129
132
|
}) : [];
|
|
130
|
-
return { merchant: str(v.merchant) ?? '', merchant_name: str(v.merchant_name) ?? str(v.merchant) ?? '', items };
|
|
133
|
+
return { merchant: str(v.merchant) ?? '', merchant_name: str(v.merchant_name) ?? str(v.merchant) ?? '', items, presented: v.presented === true };
|
|
131
134
|
}
|
|
132
135
|
function paymentSourceOf(v) {
|
|
133
136
|
return isRecord(v) && str(v.source) ? { source: v.source, brand: str(v.brand), last4: str(v.last4) } : null;
|
|
@@ -161,6 +164,7 @@ export function turnOf(body) {
|
|
|
161
164
|
store_login_expires_at: str(body.store_login_expires_at),
|
|
162
165
|
store_login_merchant_name: str(body.store_login_merchant_name),
|
|
163
166
|
next_action: typeof body.next_action === 'string' && NEXT_ACTIONS.has(body.next_action) ? body.next_action : null,
|
|
167
|
+
pick: isRecord(body.pick) && str(body.pick.product_id) ? { product_id: body.pick.product_id, ref: str(body.pick.ref), in_cart: body.pick.in_cart === true } : null,
|
|
164
168
|
raw: body,
|
|
165
169
|
};
|
|
166
170
|
}
|
|
@@ -253,6 +257,106 @@ function parseJson(text) {
|
|
|
253
257
|
return null;
|
|
254
258
|
}
|
|
255
259
|
}
|
|
260
|
+
/** Each event of a server-sent stream, in order, read as it arrives, or from the whole text when the fetch hands no stream. */
|
|
261
|
+
async function eachEvent(res, onEvent) {
|
|
262
|
+
let buffer = '';
|
|
263
|
+
// True once onEvent says the stream is over.
|
|
264
|
+
let over = false;
|
|
265
|
+
const drain = (final) => {
|
|
266
|
+
buffer = buffer.replace(/\r\n?/g, '\n');
|
|
267
|
+
for (let end = buffer.indexOf('\n\n'); end >= 0 || (final && buffer.trim() !== ''); end = buffer.indexOf('\n\n')) {
|
|
268
|
+
const block = end >= 0 ? buffer.slice(0, end) : buffer;
|
|
269
|
+
buffer = end >= 0 ? buffer.slice(end + 2) : '';
|
|
270
|
+
let event = 'message';
|
|
271
|
+
const data = [];
|
|
272
|
+
for (const line of block.split('\n')) {
|
|
273
|
+
if (line.startsWith('event:'))
|
|
274
|
+
event = line.slice(6).trim();
|
|
275
|
+
else if (line.startsWith('data:'))
|
|
276
|
+
data.push(line.slice(5).replace(/^ /, ''));
|
|
277
|
+
}
|
|
278
|
+
if (data.length > 0 && onEvent(event, data.join('\n')) === true) {
|
|
279
|
+
over = true;
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
const reader = res.body?.getReader?.();
|
|
285
|
+
if (!reader) {
|
|
286
|
+
buffer = await res.text();
|
|
287
|
+
drain(true);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const decoder = new TextDecoder();
|
|
291
|
+
try {
|
|
292
|
+
while (!over) {
|
|
293
|
+
const chunk = await reader.read();
|
|
294
|
+
if (chunk.done)
|
|
295
|
+
break;
|
|
296
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
297
|
+
drain(false);
|
|
298
|
+
}
|
|
299
|
+
if (!over)
|
|
300
|
+
drain(true);
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
if (over)
|
|
304
|
+
reader.cancel?.().catch(() => { });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* A streamed purchase turn, read to its `envelope`: the same answer a JSON turn returns, with its
|
|
309
|
+
* status in `http_status`. Each `progress` and `catalog` goes to its handler on the way. A stream that
|
|
310
|
+
* breaks or ends before its envelope, once it named the conversation, reads as a turn past its budget
|
|
311
|
+
* (`turn_timeout`), so the tool reads the conversation for what the turn left instead of losing it.
|
|
312
|
+
*/
|
|
313
|
+
async function readStreamedTurn(res, stream, timeoutMs) {
|
|
314
|
+
let envelope = null;
|
|
315
|
+
let conversationId = null;
|
|
316
|
+
const delivering = [];
|
|
317
|
+
let failure = null;
|
|
318
|
+
try {
|
|
319
|
+
await eachEvent(res, (event, text) => {
|
|
320
|
+
const data = parseJson(text);
|
|
321
|
+
if (!data)
|
|
322
|
+
return;
|
|
323
|
+
conversationId = str(data.conversation_id) ?? conversationId;
|
|
324
|
+
if (event === 'envelope') {
|
|
325
|
+
envelope = data;
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
if (event === 'progress' && typeof data.stage === 'string' && PROGRESS_STAGES.has(data.stage) && stream.onProgress) {
|
|
329
|
+
try {
|
|
330
|
+
stream.onProgress({ stage: data.stage, conversationId: str(data.conversation_id) });
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
// The app's display failing never fails the turn.
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
else if (event === 'catalog' && stream.onCatalog && str(data.conversation_id)) {
|
|
337
|
+
const catalog = catalogOf(data.catalog);
|
|
338
|
+
if (!catalog || catalog.items.length === 0)
|
|
339
|
+
return;
|
|
340
|
+
const asOf = isRecord(data.catalog) ? str(data.catalog.as_of) : null;
|
|
341
|
+
delivering.push(Promise.resolve().then(() => stream.onCatalog({ conversationId: data.conversation_id, catalog, asOf })).catch(() => { }));
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
failure = error;
|
|
347
|
+
}
|
|
348
|
+
await Promise.all(delivering);
|
|
349
|
+
const answered = envelope;
|
|
350
|
+
if (answered) {
|
|
351
|
+
const { http_status: httpStatus, ...body } = answered;
|
|
352
|
+
const status = typeof httpStatus === 'number' ? httpStatus : res.status;
|
|
353
|
+
return { status, ok: status >= 200 && status < 300, readable: true, body };
|
|
354
|
+
}
|
|
355
|
+
if (conversationId) {
|
|
356
|
+
return { status: 200, ok: true, readable: true, body: { conversation_id: conversationId, status: 'needs_input', reply: '', error_code: 'turn_timeout' } };
|
|
357
|
+
}
|
|
358
|
+
throw failure ? failureOf(failure, timeoutMs) : new AgentcardApiError(0, 'unreachable', 'Agentcard closed the purchase answer before it ended.');
|
|
359
|
+
}
|
|
256
360
|
/** The client every shopper's buy tool shares. Build one per set of credentials and keep it. */
|
|
257
361
|
export class AgentcardClient {
|
|
258
362
|
baseUrl;
|
|
@@ -289,10 +393,15 @@ export class AgentcardClient {
|
|
|
289
393
|
get sandbox() {
|
|
290
394
|
return this.learnedSandbox;
|
|
291
395
|
}
|
|
292
|
-
/**
|
|
293
|
-
|
|
396
|
+
/**
|
|
397
|
+
* The access token the client sends: the shopper's own, or the company's, exchanged once and
|
|
398
|
+
* renewed a minute before it expires. Pass a company token Agentcard refused to get a new one.
|
|
399
|
+
*/
|
|
400
|
+
async bearer(refused) {
|
|
294
401
|
if (this.shopperToken !== null)
|
|
295
402
|
return this.shopperToken;
|
|
403
|
+
if (refused !== undefined)
|
|
404
|
+
return this.renewAfter(refused);
|
|
296
405
|
if (this.token && Date.now() < this.token.expiresAt)
|
|
297
406
|
return this.token.value;
|
|
298
407
|
if (!this.inflight)
|
|
@@ -305,11 +414,14 @@ export class AgentcardClient {
|
|
|
305
414
|
* conversation instead. Only a 401 is sent again, once, with a fresh token,
|
|
306
415
|
* because Agentcard refused it before anything ran.
|
|
307
416
|
*/
|
|
308
|
-
async buy(body) {
|
|
417
|
+
async buy(body, stream) {
|
|
309
418
|
if (!isRecord(body))
|
|
310
419
|
throw new TypeError('Pass the purchase request as an object, such as { ask: "two AA battery packs" }.');
|
|
311
420
|
this.checkShopper({ externalUserId: body.external_user_id, userId: body.user_id });
|
|
312
|
-
|
|
421
|
+
const streamed = !!stream && (typeof stream.onProgress === 'function' || typeof stream.onCatalog === 'function');
|
|
422
|
+
// A streamed turn keeps the soft budget a JSON turn has, so it answers within BUY_TURN_TIMEOUT_MS too.
|
|
423
|
+
const path = streamed ? '/buy?stream=1&soft_timeout=1' : '/buy';
|
|
424
|
+
return turnOf(await this.call('POST', path, body, BUY_TURN_TIMEOUT_MS, streamed ? stream : undefined));
|
|
313
425
|
}
|
|
314
426
|
/** One read of a conversation: whether a turn is running, the last checkout, the orders it placed, the carts on the table. */
|
|
315
427
|
async conversation(conversationId, who = {}) {
|
|
@@ -337,6 +449,13 @@ export class AgentcardClient {
|
|
|
337
449
|
throw new AgentcardApiError(200, 'invalid_answer', 'Agentcard answered the Vault link request without a link.', answer);
|
|
338
450
|
return { id: str(answer.id) ?? '', url, expiresAt: str(answer.expires_at), reused: answer.reused === true };
|
|
339
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* Hear every event Agentcard sends your company over one connection your server opens, with no
|
|
454
|
+
* webhook endpoint: `listenToAgentcard(this, opts)`. Takes your company credentials.
|
|
455
|
+
*/
|
|
456
|
+
listen(opts) {
|
|
457
|
+
return listenToAgentcard(this, opts);
|
|
458
|
+
}
|
|
340
459
|
/** A company names the shopper exactly once; a shopper's own token already names them. */
|
|
341
460
|
checkShopper(who) {
|
|
342
461
|
const named = [who.externalUserId, who.userId].filter((v) => v !== undefined);
|
|
@@ -349,10 +468,10 @@ export class AgentcardClient {
|
|
|
349
468
|
throw new TypeError('Name the shopper once: externalUserId, your own id for them, or userId, the Agentcard user id.');
|
|
350
469
|
}
|
|
351
470
|
}
|
|
352
|
-
async call(method, path, body, timeoutMs) {
|
|
471
|
+
async call(method, path, body, timeoutMs, stream) {
|
|
353
472
|
let token = await this.bearer();
|
|
354
473
|
for (let attempt = 0;; attempt++) {
|
|
355
|
-
const answer = await this.send(method, path, token, body, timeoutMs);
|
|
474
|
+
const answer = await this.send(method, path, token, body, timeoutMs, stream);
|
|
356
475
|
if (answer.status === 401 && attempt === 0 && this.credentials) {
|
|
357
476
|
token = await this.renewAfter(token);
|
|
358
477
|
continue;
|
|
@@ -370,24 +489,39 @@ export class AgentcardClient {
|
|
|
370
489
|
throw errorOf(answer.status, answer.body);
|
|
371
490
|
}
|
|
372
491
|
}
|
|
373
|
-
async send(method, path, token, body, timeoutMs) {
|
|
492
|
+
async send(method, path, token, body, timeoutMs, stream) {
|
|
493
|
+
let res;
|
|
374
494
|
try {
|
|
375
|
-
|
|
495
|
+
res = await this.fetchFn(`${this.baseUrl}${path}`, {
|
|
376
496
|
method,
|
|
377
497
|
headers: {
|
|
378
498
|
authorization: `Bearer ${token}`,
|
|
379
|
-
accept: 'application/json',
|
|
499
|
+
accept: stream ? 'text/event-stream, application/json' : 'application/json',
|
|
380
500
|
...(body !== undefined ? { 'content-type': 'application/json' } : {}),
|
|
381
501
|
},
|
|
382
502
|
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
383
503
|
signal: AbortSignal.timeout(timeoutMs),
|
|
384
504
|
});
|
|
385
|
-
const parsed = parseJson(await res.text());
|
|
386
|
-
return { status: res.status, ok: res.ok, readable: parsed !== null, body: parsed ?? {} };
|
|
387
505
|
}
|
|
388
506
|
catch (error) {
|
|
389
507
|
throw failureOf(error, timeoutMs);
|
|
390
508
|
}
|
|
509
|
+
// Agentcard streams only once the turn starts: a refusal before it is plain JSON.
|
|
510
|
+
if (stream && /text\/event-stream/i.test(res.headers?.get?.('content-type') ?? ''))
|
|
511
|
+
return readStreamedTurn(res, stream, timeoutMs);
|
|
512
|
+
let text;
|
|
513
|
+
try {
|
|
514
|
+
text = await res.text();
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
throw failureOf(error, timeoutMs);
|
|
518
|
+
}
|
|
519
|
+
const parsed = parseJson(text);
|
|
520
|
+
// A fetch that hands no headers still carries a streamed answer in its text: read it whole.
|
|
521
|
+
if (stream && parsed === null && /^(?:event|data):/m.test(text)) {
|
|
522
|
+
return readStreamedTurn({ status: res.status, ok: res.ok, text: async () => text }, stream, timeoutMs);
|
|
523
|
+
}
|
|
524
|
+
return { status: res.status, ok: res.ok, readable: parsed !== null, body: parsed ?? {} };
|
|
391
525
|
}
|
|
392
526
|
/** After a 401: the token another call already renewed, or one new exchange, however many calls were refused at once. */
|
|
393
527
|
async renewAfter(refused) {
|
package/dist/definition.d.ts
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The buy tool as a model sees it: one name, one description, and a strict
|
|
3
|
-
* JSON Schema with
|
|
3
|
+
* JSON Schema with four required fields, `product_id` null unless the shopper
|
|
4
|
+
* picks a product they were shown. The delivery address and the
|
|
4
5
|
* shopper's id are construction options, never model inputs, so a model can
|
|
5
6
|
* neither change where an order goes nor buy for someone else.
|
|
6
7
|
*/
|
|
7
8
|
export declare const BUY_TOOL_NAME = "buy";
|
|
8
9
|
/** What the model reads before it calls the tool. Every sentence is a rule the model follows. */
|
|
9
10
|
export declare const BUY_TOOL_DESCRIPTION: string;
|
|
10
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* What the model sends: `ask`, `confirm` and `new_order` every time, and `product_id`, null unless the
|
|
13
|
+
* shopper picks a product from the last result. Your own code may leave `product_id` out.
|
|
14
|
+
*/
|
|
11
15
|
export interface BuyToolInput {
|
|
12
16
|
ask: string;
|
|
13
17
|
confirm: boolean;
|
|
14
18
|
new_order: boolean;
|
|
19
|
+
/** The `product_id` of the product the shopper picked from the last result's `products`, or null. */
|
|
20
|
+
product_id?: string | null;
|
|
15
21
|
}
|
|
16
22
|
/** A type, not an interface, so it fits the SDKs' own input schema types, which take any key. */
|
|
17
23
|
export type BuyToolInputSchema = {
|
|
@@ -29,6 +35,10 @@ export type BuyToolInputSchema = {
|
|
|
29
35
|
type: 'boolean';
|
|
30
36
|
description: string;
|
|
31
37
|
};
|
|
38
|
+
product_id: {
|
|
39
|
+
type: ['string', 'null'];
|
|
40
|
+
description: string;
|
|
41
|
+
};
|
|
32
42
|
};
|
|
33
43
|
required: Array<keyof BuyToolInput>;
|
|
34
44
|
additionalProperties: false;
|
|
@@ -42,8 +52,9 @@ export interface BuyToolDefinition {
|
|
|
42
52
|
export declare function buyToolDefinition(): BuyToolDefinition;
|
|
43
53
|
/**
|
|
44
54
|
* The model's input, checked against the schema: an object, or its JSON text,
|
|
45
|
-
* with `ask` a string
|
|
46
|
-
* else. Throws a TypeError whose message
|
|
55
|
+
* with `ask` a string, `confirm` and `new_order` booleans, `product_id` a
|
|
56
|
+
* string, null or left out, and nothing else. Throws a TypeError whose message
|
|
57
|
+
* tells the model what to send.
|
|
47
58
|
*/
|
|
48
59
|
export declare function parseBuyToolInput(value: unknown): BuyToolInput;
|
|
49
60
|
/** The buy tool in OpenAI's function-calling format, which the OpenAI SDK, OpenRouter's chat API and most other model APIs take. */
|
package/dist/definition.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The buy tool as a model sees it: one name, one description, and a strict
|
|
3
|
-
* JSON Schema with
|
|
3
|
+
* JSON Schema with four required fields, `product_id` null unless the shopper
|
|
4
|
+
* picks a product they were shown. The delivery address and the
|
|
4
5
|
* shopper's id are construction options, never model inputs, so a model can
|
|
5
6
|
* neither change where an order goes nor buy for someone else.
|
|
6
7
|
*/
|
|
@@ -10,6 +11,7 @@ export const BUY_TOOL_DESCRIPTION = [
|
|
|
10
11
|
'Buy what this shopper asks for at real stores.',
|
|
11
12
|
"Put the shopper's own words in `ask`.",
|
|
12
13
|
'Tell the shopper what `reply` says, in your own voice.',
|
|
14
|
+
"When `shopper_already_told_looking` is true, the shopper was already told you are working on their request, so never acknowledge it again: open with the answer or the question.",
|
|
13
15
|
'Show the shopper the cart before you confirm it.',
|
|
14
16
|
'Ask the shopper for exactly one thing per message: the step `next_action` names, even when `reply` lists more. Never mention a later step, and never ask for the next one until the shopper has done this one.',
|
|
15
17
|
'Set `confirm` to true only when the shopper just said yes to the cart in your last result, says they added the card for a cart that waits, says they approved, or asks for the approval link again.',
|
|
@@ -22,6 +24,8 @@ export const BUY_TOOL_DESCRIPTION = [
|
|
|
22
24
|
'After `needs_card` with a cart, Agentcard keeps the cart, so set `confirm` to true once the shopper says they added the card.',
|
|
23
25
|
"After `needs_card` with no cart, call this tool again with the shopper's request once they say they added a card.",
|
|
24
26
|
'Set `new_order` to true only when the shopper drops the cart.',
|
|
27
|
+
"When the shopper picks one of the `products` in your last result, by its name, its place in the list or a tap that names it, set `product_id` to that product's `product_id`, so Agentcard adds exactly that product; otherwise set it to null.",
|
|
28
|
+
'When `pick.in_cart` is false, the product the shopper picked is not in the cart: tell the shopper what `reply` says, and never ask them to confirm the cart.',
|
|
25
29
|
].join(' ');
|
|
26
30
|
/** A fresh copy of the tool's definition, so a caller that edits one never changes another. */
|
|
27
31
|
export function buyToolDefinition() {
|
|
@@ -37,17 +41,22 @@ export function buyToolDefinition() {
|
|
|
37
41
|
description: 'True only when the shopper just said yes to the cart in your last result, says they added the card for a cart that waits, says they approved, or asks for the approval link again. False otherwise.',
|
|
38
42
|
},
|
|
39
43
|
new_order: { type: 'boolean', description: 'True only when the shopper drops the cart to start a new order. False otherwise.' },
|
|
44
|
+
product_id: {
|
|
45
|
+
type: ['string', 'null'],
|
|
46
|
+
description: "The `product_id` of the product the shopper picked from the `products` in your last result. Null when they picked none.",
|
|
47
|
+
},
|
|
40
48
|
},
|
|
41
|
-
required: ['ask', 'confirm', 'new_order'],
|
|
49
|
+
required: ['ask', 'confirm', 'new_order', 'product_id'],
|
|
42
50
|
additionalProperties: false,
|
|
43
51
|
},
|
|
44
52
|
};
|
|
45
53
|
}
|
|
46
|
-
const FIELDS = new Set(['ask', 'confirm', 'new_order']);
|
|
54
|
+
const FIELDS = new Set(['ask', 'confirm', 'new_order', 'product_id']);
|
|
47
55
|
/**
|
|
48
56
|
* The model's input, checked against the schema: an object, or its JSON text,
|
|
49
|
-
* with `ask` a string
|
|
50
|
-
* else. Throws a TypeError whose message
|
|
57
|
+
* with `ask` a string, `confirm` and `new_order` booleans, `product_id` a
|
|
58
|
+
* string, null or left out, and nothing else. Throws a TypeError whose message
|
|
59
|
+
* tells the model what to send.
|
|
51
60
|
*/
|
|
52
61
|
export function parseBuyToolInput(value) {
|
|
53
62
|
let input = value;
|
|
@@ -65,14 +74,18 @@ export function parseBuyToolInput(value) {
|
|
|
65
74
|
const fields = input;
|
|
66
75
|
const extra = Object.keys(fields).find((key) => !FIELDS.has(key));
|
|
67
76
|
if (extra !== undefined)
|
|
68
|
-
throw new TypeError(`The buy tool takes only ask, confirm and
|
|
77
|
+
throw new TypeError(`The buy tool takes only ask, confirm, new_order and product_id, and ${extra} is not one of them.`);
|
|
69
78
|
if (typeof fields.ask !== 'string')
|
|
70
79
|
throw new TypeError("Set ask to the shopper's words, as a string.");
|
|
71
80
|
if (typeof fields.confirm !== 'boolean')
|
|
72
81
|
throw new TypeError('Set confirm to true or false.');
|
|
73
82
|
if (typeof fields.new_order !== 'boolean')
|
|
74
83
|
throw new TypeError('Set new_order to true or false.');
|
|
75
|
-
|
|
84
|
+
const productId = fields.product_id;
|
|
85
|
+
if (productId !== undefined && productId !== null && typeof productId !== 'string') {
|
|
86
|
+
throw new TypeError("Set product_id to the picked product's product_id, as a string, or to null.");
|
|
87
|
+
}
|
|
88
|
+
return { ask: fields.ask, confirm: fields.confirm, new_order: fields.new_order, product_id: typeof productId === 'string' && productId.trim() !== '' ? productId.trim() : null };
|
|
76
89
|
}
|
|
77
90
|
/** A fresh copy of the tool in OpenAI's function-calling format: plain JSON, so an SDK that sends it as is sends exactly this. */
|
|
78
91
|
export function openAIBuyTool() {
|
package/dist/events.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every event type Agentcard sends a company, the same list a webhook endpoint subscribes to by
|
|
3
|
+
* name. The stream (`agentcard.listen`) and a webhook endpoint carry the same types, with the same
|
|
4
|
+
* envelope and the same ids. A test holds this list to the backend's catalog.
|
|
5
|
+
*/
|
|
6
|
+
export declare const AGENTCARD_EVENT_TYPES: readonly ["card.created", "card.updated", "card.closed", "cardholder.created", "cardholder.updated", "transaction.authorized", "transaction.declined", "transaction.cleared", "transaction.voided", "balance.low", "cardholder_onboarding_session.completed", "platform_connect_session.completed", "merchant.connected", "order.placed", "order.confirmed", "order.failed", "order.updated", "purchase.message", "connection.created", "connected_card.updated", "vault.card_stored", "vault.session_linked", "vault.session_card_picked", "vault.payment_permission.updated", "action.confirmed", "action.done", "checkout_authorization.approved", "checkout_authorization.submitted", "checkout_authorization.declined", "checkout_authorization.cancelled", "checkout_authorization.expired", "checkout_authorization.duplicate_charge", "checkout_authorization.refused", "checkout_authorization.watched", "checkout_authorization.amount_mismatch", "checkout_authorization.settled", "checkout_authorization.settlement_unconfirmed", "checkout_authorization.outcome_reported", "card_flow.started", "card_flow.failed", "transfer.approved", "transfer.initiated", "transfer.completed", "transfer.failed", "transfer.released", "wallet.funded", "user_wallet.funding_detected", "user_wallet.funded", "wallet.balance.low", "recovery.requested", "recovery.completed", "recovery.rejected", "wallet.withdrawal.initiated", "wallet.withdrawal.completed", "wallet.withdrawal.failed", "identity.verification.updated", "approval.requested", "reward.earned", "reward.reversed", "wallet_link.opened"];
|
|
7
|
+
/** One event type Agentcard sends. A type Agentcard adds later still reads as its own name. */
|
|
8
|
+
export type AgentcardEventType = (typeof AGENTCARD_EVENT_TYPES)[number];
|
|
9
|
+
/** Whether a filter entry names something Agentcard sends: an event type, a family such as `order.*`, or `*`. */
|
|
10
|
+
export declare function isEventPattern(pattern: string): boolean;
|
|
11
|
+
/** Whether an event type matches a filter entry, as a webhook subscription matches it. */
|
|
12
|
+
export declare function matchesEventPattern(type: string, pattern: string): boolean;
|
package/dist/events.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every event type Agentcard sends a company, the same list a webhook endpoint subscribes to by
|
|
3
|
+
* name. The stream (`agentcard.listen`) and a webhook endpoint carry the same types, with the same
|
|
4
|
+
* envelope and the same ids. A test holds this list to the backend's catalog.
|
|
5
|
+
*/
|
|
6
|
+
export const AGENTCARD_EVENT_TYPES = [
|
|
7
|
+
'card.created',
|
|
8
|
+
'card.updated',
|
|
9
|
+
'card.closed',
|
|
10
|
+
'cardholder.created',
|
|
11
|
+
'cardholder.updated',
|
|
12
|
+
'transaction.authorized',
|
|
13
|
+
'transaction.declined',
|
|
14
|
+
'transaction.cleared',
|
|
15
|
+
'transaction.voided',
|
|
16
|
+
'balance.low',
|
|
17
|
+
'cardholder_onboarding_session.completed',
|
|
18
|
+
'platform_connect_session.completed',
|
|
19
|
+
'merchant.connected',
|
|
20
|
+
'order.placed',
|
|
21
|
+
'order.confirmed',
|
|
22
|
+
'order.failed',
|
|
23
|
+
'order.updated',
|
|
24
|
+
'purchase.message',
|
|
25
|
+
'connection.created',
|
|
26
|
+
'connected_card.updated',
|
|
27
|
+
'vault.card_stored',
|
|
28
|
+
'vault.session_linked',
|
|
29
|
+
'vault.session_card_picked',
|
|
30
|
+
'vault.payment_permission.updated',
|
|
31
|
+
'action.confirmed',
|
|
32
|
+
'action.done',
|
|
33
|
+
'checkout_authorization.approved',
|
|
34
|
+
'checkout_authorization.submitted',
|
|
35
|
+
'checkout_authorization.declined',
|
|
36
|
+
'checkout_authorization.cancelled',
|
|
37
|
+
'checkout_authorization.expired',
|
|
38
|
+
'checkout_authorization.duplicate_charge',
|
|
39
|
+
'checkout_authorization.refused',
|
|
40
|
+
'checkout_authorization.watched',
|
|
41
|
+
'checkout_authorization.amount_mismatch',
|
|
42
|
+
'checkout_authorization.settled',
|
|
43
|
+
'checkout_authorization.settlement_unconfirmed',
|
|
44
|
+
'checkout_authorization.outcome_reported',
|
|
45
|
+
'card_flow.started',
|
|
46
|
+
'card_flow.failed',
|
|
47
|
+
'transfer.approved',
|
|
48
|
+
'transfer.initiated',
|
|
49
|
+
'transfer.completed',
|
|
50
|
+
'transfer.failed',
|
|
51
|
+
'transfer.released',
|
|
52
|
+
'wallet.funded',
|
|
53
|
+
'user_wallet.funding_detected',
|
|
54
|
+
'user_wallet.funded',
|
|
55
|
+
'wallet.balance.low',
|
|
56
|
+
'recovery.requested',
|
|
57
|
+
'recovery.completed',
|
|
58
|
+
'recovery.rejected',
|
|
59
|
+
'wallet.withdrawal.initiated',
|
|
60
|
+
'wallet.withdrawal.completed',
|
|
61
|
+
'wallet.withdrawal.failed',
|
|
62
|
+
'identity.verification.updated',
|
|
63
|
+
'approval.requested',
|
|
64
|
+
'reward.earned',
|
|
65
|
+
'reward.reversed',
|
|
66
|
+
'wallet_link.opened',
|
|
67
|
+
];
|
|
68
|
+
const KNOWN = new Set(AGENTCARD_EVENT_TYPES);
|
|
69
|
+
const FAMILIES = new Set(AGENTCARD_EVENT_TYPES.map((t) => `${t.split('.')[0]}.*`));
|
|
70
|
+
/** Whether a filter entry names something Agentcard sends: an event type, a family such as `order.*`, or `*`. */
|
|
71
|
+
export function isEventPattern(pattern) {
|
|
72
|
+
return pattern === '*' || KNOWN.has(pattern) || FAMILIES.has(pattern);
|
|
73
|
+
}
|
|
74
|
+
/** Whether an event type matches a filter entry, as a webhook subscription matches it. */
|
|
75
|
+
export function matchesEventPattern(type, pattern) {
|
|
76
|
+
if (pattern === '*' || pattern === type)
|
|
77
|
+
return true;
|
|
78
|
+
return pattern.endsWith('.*') && type.startsWith(pattern.slice(0, -1));
|
|
79
|
+
}
|
package/dist/handler.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Handlers for an Agentcard webhook endpoint, for a Node or Express `(req, res)`
|
|
3
|
+
* route and for a route that takes a web `Request`, such as a Next.js route
|
|
4
|
+
* handler. Each reads the raw bytes, checks the signature, hands the event to
|
|
5
|
+
* your callback, and answers once the callback is done: 200 when it returns,
|
|
6
|
+
* 500 when it throws, so Agentcard delivers the event again instead of losing it.
|
|
6
7
|
*/
|
|
7
8
|
import { type AgentcardEvent, type VerifyOptions } from './verify.js';
|
|
8
9
|
import { type PurchaseMessage } from './message.js';
|
|
10
|
+
import { type ShopperMessage } from './shopper-message.js';
|
|
9
11
|
/** The parts of a Node or Express request the handler reads. */
|
|
10
12
|
export interface WebhookRequest {
|
|
11
13
|
headers: Record<string, string | string[] | undefined>;
|
|
@@ -35,6 +37,23 @@ export interface PurchaseAgentWebhookOptions extends Pick<VerifyOptions, 'tolera
|
|
|
35
37
|
*/
|
|
36
38
|
onError?: (error: unknown, event: AgentcardEvent | undefined) => void | Promise<void>;
|
|
37
39
|
}
|
|
40
|
+
/** The parts of a web `Request` the fetch handler reads. */
|
|
41
|
+
export interface WebhookFetchRequest {
|
|
42
|
+
headers: {
|
|
43
|
+
get(name: string): string | null;
|
|
44
|
+
};
|
|
45
|
+
/** The body as a stream, read chunk by chunk so a body past the size cap is never held whole. */
|
|
46
|
+
body?: {
|
|
47
|
+
getReader(): {
|
|
48
|
+
read(): Promise<{
|
|
49
|
+
done: boolean;
|
|
50
|
+
value?: Uint8Array;
|
|
51
|
+
}>;
|
|
52
|
+
cancel(reason?: unknown): Promise<void>;
|
|
53
|
+
};
|
|
54
|
+
} | null;
|
|
55
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
56
|
+
}
|
|
38
57
|
/**
|
|
39
58
|
* A `(req, res)` handler for your Agentcard webhook endpoint. A delivery with a
|
|
40
59
|
* missing, malformed, stale or wrong signature gets a 400 and reaches no
|
|
@@ -43,6 +62,40 @@ export interface PurchaseAgentWebhookOptions extends Pick<VerifyOptions, 'tolera
|
|
|
43
62
|
* callback is done: 200 when it returns, and 500 when it throws, so Agentcard
|
|
44
63
|
* delivers the event again. Agentcard waits 10 seconds for an answer, so keep
|
|
45
64
|
* the callback short. Agentcard delivers at least once, and again after a 500,
|
|
46
|
-
* so handle each `message.eventId` once.
|
|
65
|
+
* so handle each `message.eventId` once. `agentcardWebhook` does that for you.
|
|
47
66
|
*/
|
|
48
67
|
export declare function purchaseAgentWebhook(secret: string, onMessage: (message: PurchaseMessage, event: AgentcardEvent) => void | Promise<void>, opts?: PurchaseAgentWebhookOptions): (req: WebhookRequest, res: WebhookResponse) => Promise<void>;
|
|
68
|
+
export interface AgentcardWebhookOptions extends PurchaseAgentWebhookOptions {
|
|
69
|
+
/** The secret Agentcard returned once, when you created the endpoint. */
|
|
70
|
+
secret: string;
|
|
71
|
+
/**
|
|
72
|
+
* Receives each message for one of your shoppers, under your own id for them: post
|
|
73
|
+
* `message.text` into that shopper's chat, or hand `message.note` to your agent. Runs once per
|
|
74
|
+
* event, never for a stale message, and the delivery answers 500 when it throws, so Agentcard
|
|
75
|
+
* delivers the event again.
|
|
76
|
+
*/
|
|
77
|
+
onMessage: (externalUserId: string, message: ShopperMessage, event: AgentcardEvent) => void | Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Receives every verified event that is no message for one of your shoppers: another event type,
|
|
80
|
+
* such as `vault.card_stored`, and a shopper message for a shopper you gave Agentcard no id for.
|
|
81
|
+
*/
|
|
82
|
+
onEvent?: (event: AgentcardEvent) => void | Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
/** The handler `agentcardWebhook` returns: a Node or Express `(req, res)` handler, with `fetch` for a web `Request`. */
|
|
85
|
+
export interface AgentcardWebhookHandler {
|
|
86
|
+
(req: WebhookRequest, res: WebhookResponse): Promise<void>;
|
|
87
|
+
/** The same handler for a web `Request`, such as a Next.js route handler's: `export const POST = webhook.fetch`. */
|
|
88
|
+
fetch(request: WebhookFetchRequest): Promise<Response>;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* One handler for every Agentcard event your shoppers should hear about. It checks each delivery's
|
|
92
|
+
* signature, skips an event this process already handled, reads the message the event carries for
|
|
93
|
+
* a shopper
|
|
94
|
+
* (`readShopperMessage`), drops a message whose link or window has closed (`isStale`), and calls
|
|
95
|
+
* `onMessage` with your own id for the shopper and the message, Agentcard's own text included.
|
|
96
|
+
* Every other verified event goes to `onEvent`. The answer waits for the callback: 200 when it
|
|
97
|
+
* returns, 500 when it throws, and an event whose callback threw runs again on the next delivery.
|
|
98
|
+
* The handled events live in this process's memory: with more than one server, deduplicate on
|
|
99
|
+
* `message.eventId` in your own storage too.
|
|
100
|
+
*/
|
|
101
|
+
export declare function agentcardWebhook(opts: AgentcardWebhookOptions): AgentcardWebhookHandler;
|