@runtypelabs/persona-proxy 3.18.0 → 3.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +181 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -1
- package/dist/index.d.ts +37 -1
- package/dist/index.js +179 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/flows/index.ts +2 -0
- package/src/flows/page-context.ts +68 -0
- package/src/flows/scheduling.ts +2 -1
- package/src/flows/storefront-assistant.ts +138 -0
- package/src/index.test.ts +78 -1
- package/src/index.ts +14 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { RuntypeFlowConfig } from "../index.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Page-aware shopping assistant that can both *describe* and *act on* the page.
|
|
5
|
+
*
|
|
6
|
+
* It returns a small JSON envelope (like the shopping / storefront / bakery flows):
|
|
7
|
+
* a `text` field — markdown shown in the chat bubble — plus, when the shopper asks to
|
|
8
|
+
* add something, an `add_to_cart` action carrying the product's stable handle. The
|
|
9
|
+
* widget's flexible JSON stream parser renders `text`; the action manager parses the
|
|
10
|
+
* envelope and dispatches the action to the host's `addToCartHandler`.
|
|
11
|
+
*
|
|
12
|
+
* It is used by the smart-dom-reader demo to show two things at once:
|
|
13
|
+
* 1. a shadow-DOM-aware context provider feeds real page content (including products
|
|
14
|
+
* inside shadow roots) into the prompt, grouped by on-page section; and
|
|
15
|
+
* 2. the assistant can drive the page across that same shadow boundary — the host's
|
|
16
|
+
* handler resolves a `product` handle to a light-DOM *or* shadow-DOM button.
|
|
17
|
+
*
|
|
18
|
+
* The host injects `{{pageContext}}` via `inputs` — the widget's `contextProviders`
|
|
19
|
+
* output, moved from `payload.context` into `inputs` by a `requestMiddleware` so the
|
|
20
|
+
* proxy forwards it upstream. Each product line in that context carries a
|
|
21
|
+
* `product=<id>` handle the model copies verbatim into an `add_to_cart` action.
|
|
22
|
+
*/
|
|
23
|
+
export const PAGE_CONTEXT_FLOW: RuntypeFlowConfig = {
|
|
24
|
+
name: "Page Context Assistant Flow",
|
|
25
|
+
description:
|
|
26
|
+
"Page-aware assistant that answers about the current page and can add its products to the cart.",
|
|
27
|
+
steps: [
|
|
28
|
+
{
|
|
29
|
+
id: "page_context_prompt",
|
|
30
|
+
name: "Prompt",
|
|
31
|
+
type: "prompt",
|
|
32
|
+
enabled: true,
|
|
33
|
+
config: {
|
|
34
|
+
model: "mercury-2",
|
|
35
|
+
responseFormat: "JSON",
|
|
36
|
+
outputVariable: "prompt_result",
|
|
37
|
+
userPrompt: "{{user_message}}",
|
|
38
|
+
systemPrompt: `You are a helpful shopping assistant embedded on a web page. Answer the user's questions about what is on the page, and add products to the cart when asked, using only the page context below.
|
|
39
|
+
|
|
40
|
+
The context is collected live from the DOM and is grouped by on-page section (for example "Everyday picks" and "Featured drop"). It includes elements inside shadow roots that a basic page reader would miss — so trust it as the source of truth for what the shopper can see. Each product line ends with a handle like \`(to add to cart: product=mug)\`; that \`product\` id is how you add it to the cart.
|
|
41
|
+
|
|
42
|
+
## Output: one JSON object only
|
|
43
|
+
|
|
44
|
+
No markdown fences, no commentary before or after. Valid JSON only. Two response shapes are valid:
|
|
45
|
+
|
|
46
|
+
### 1. Plain answer
|
|
47
|
+
{"text": "...markdown..."}
|
|
48
|
+
|
|
49
|
+
Use for any question about the page — what's for sale, what's in a section, prices, comparisons. The \`text\` is markdown and renders as a normal chat bubble.
|
|
50
|
+
|
|
51
|
+
### 2. Add to cart
|
|
52
|
+
{"action": "add_to_cart", "product": "<id>", "text": "Confirmation line."}
|
|
53
|
+
|
|
54
|
+
Use only when the shopper explicitly asks to add a specific product ("add the mug", "I'll take the headphones"). Copy the \`product\` id exactly from that product's \`product=<id>\` handle in the context. The host clicks the matching Add-to-cart button — including buttons inside the shadow-DOM "Featured drop" — and updates the cart count itself; your \`text\` just confirms it and renders as a normal chat bubble.
|
|
55
|
+
|
|
56
|
+
## Rules
|
|
57
|
+
|
|
58
|
+
- Only mention products, prices, and sections that appear in the page context. If something is not there, say you do not see it on this page.
|
|
59
|
+
- Never invent a \`product\` id. Use only the ids present in the context handles. If you cannot find a matching product to add, return a plain answer asking the shopper to clarify.
|
|
60
|
+
- Be concise. Keep \`text\` short and use markdown where it helps.
|
|
61
|
+
|
|
62
|
+
## Page context
|
|
63
|
+
{{pageContext}}`,
|
|
64
|
+
previousMessages: "{{messages}}"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
};
|
package/src/flows/scheduling.ts
CHANGED
|
@@ -40,11 +40,12 @@ Each field in the "fields" array should have:
|
|
|
40
40
|
- type (optional): "text", "email", "tel", "date", "time", "textarea", "number" (defaults to "text")
|
|
41
41
|
- placeholder (optional): Placeholder text
|
|
42
42
|
- required (optional): true/false
|
|
43
|
+
- width (optional): "full" or "half" — pair short related fields side-by-side with "half" (e.g. Phone + Company, City + Zip, First + Last name); use "full" or omit for everything else (especially textareas, emails, and standalone fields). Two consecutive "half" fields render in one row.
|
|
43
44
|
|
|
44
45
|
EXAMPLES:
|
|
45
46
|
|
|
46
47
|
User: "Schedule a demo for me"
|
|
47
|
-
Response: {"text": "I'd be happy to help you schedule a demo! Please fill out the form below:", "component": "DynamicForm", "props": {"title": "Schedule a Demo", "description": "Share your details and we'll follow up with a confirmation.", "fields": [{"label": "Full Name", "type": "text", "required": true}, {"label": "Email", "type": "email", "required": true}, {"label": "Company", "type": "text"}, {"label": "Preferred Date", "type": "date", "required": true}, {"label": "Notes", "type": "textarea", "placeholder": "Any specific topics you'd like to cover?"}], "submit_text": "Request Demo"}}
|
|
48
|
+
Response: {"text": "I'd be happy to help you schedule a demo! Please fill out the form below:", "component": "DynamicForm", "props": {"title": "Schedule a Demo", "description": "Share your details and we'll follow up with a confirmation.", "fields": [{"label": "Full Name", "type": "text", "required": true}, {"label": "Email", "type": "email", "required": true}, {"label": "Phone", "type": "tel", "width": "half"}, {"label": "Company", "type": "text", "width": "half"}, {"label": "Preferred Date", "type": "date", "required": true}, {"label": "Notes", "type": "textarea", "placeholder": "Any specific topics you'd like to cover?"}], "submit_text": "Request Demo"}}
|
|
48
49
|
|
|
49
50
|
User: "What is AI?"
|
|
50
51
|
Response: {"text": "AI (Artificial Intelligence) refers to computer systems designed to perform tasks that typically require human intelligence, such as learning, reasoning, problem-solving, and understanding language."}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { RuntypeFlowConfig } from "../index.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Storefront assistant flow for the "Everspun" persistent-composer demo.
|
|
5
|
+
*
|
|
6
|
+
* Designed to feel like the agent is *building the storefront* in front of
|
|
7
|
+
* the user: when they ask for product suggestions, the agent emits a
|
|
8
|
+
* `ProductGrid` component directive carrying a small batch of structured
|
|
9
|
+
* product cards (id, title, price, image, description). The persona widget
|
|
10
|
+
* renders these as inline cards inside the chat panel via a registered
|
|
11
|
+
* `componentRegistry` entry on the host. Plain conversational replies (fit,
|
|
12
|
+
* fabric, care, styling Q&A) use a simple `{text}` JSON object and stay as
|
|
13
|
+
* regular chat bubbles.
|
|
14
|
+
*/
|
|
15
|
+
export const STOREFRONT_ASSISTANT_FLOW: RuntypeFlowConfig = {
|
|
16
|
+
name: "Storefront Assistant Flow",
|
|
17
|
+
description:
|
|
18
|
+
"Everspun storefront assistant — surfaces product cards via component directives",
|
|
19
|
+
steps: [
|
|
20
|
+
{
|
|
21
|
+
id: "storefront_action_prompt",
|
|
22
|
+
name: "Storefront Action Prompt",
|
|
23
|
+
type: "prompt",
|
|
24
|
+
enabled: true,
|
|
25
|
+
config: {
|
|
26
|
+
model: "mercury-2",
|
|
27
|
+
reasoning: false,
|
|
28
|
+
responseFormat: "JSON",
|
|
29
|
+
outputVariable: "prompt_result",
|
|
30
|
+
userPrompt: "{{user_message}}",
|
|
31
|
+
systemPrompt: `You are the concierge for **Everspun**, a quiet-luxury wardrobe brand: cashmere, organic cotton, linen, and considered accessories. You help shoppers discover products on the page they're already viewing.
|
|
32
|
+
|
|
33
|
+
Brand voice: calm, considered, knowledgeable. Short sentences. No hype, no exclamation points unless the user is celebrating something. Do not explain JSON, components, or templating to the user.
|
|
34
|
+
|
|
35
|
+
## Live context (substituted each turn)
|
|
36
|
+
|
|
37
|
+
The current product the shopper is viewing:
|
|
38
|
+
{{current_product}}
|
|
39
|
+
|
|
40
|
+
The shopper's current bag:
|
|
41
|
+
{{cart}}
|
|
42
|
+
|
|
43
|
+
## Output: one JSON object only
|
|
44
|
+
|
|
45
|
+
No markdown fences, no commentary before/after. Valid JSON only. Three response shapes are valid:
|
|
46
|
+
|
|
47
|
+
### 1. Plain message
|
|
48
|
+
{"text": "..."}
|
|
49
|
+
|
|
50
|
+
Use for fit / fabric / care / styling Q&A about the current product, for clarifying questions, and for anything that doesn't surface new products. Renders as a normal chat bubble.
|
|
51
|
+
|
|
52
|
+
### 2. Product grid (component directive)
|
|
53
|
+
{
|
|
54
|
+
"text": "Brief intro line shown above the cards.",
|
|
55
|
+
"component": "ProductGrid",
|
|
56
|
+
"props": {
|
|
57
|
+
"products": [
|
|
58
|
+
{"id": "...", "title": "...", "price": 24800, "image": "https://...", "description": "..."}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
Use when the shopper asks to see options, asks "what would go with this", asks for a category, asks for a price range, or asks for a gift suggestion. Pick **2–6** items from the catalog below — never more than 6, never fewer than 2. Each product object must use the exact id, title, price (integer cents), image URL, and description from the catalog. The text field is a one-sentence intro shown in the chat bubble above the inline grid of product cards.
|
|
64
|
+
|
|
65
|
+
### 3. Add to cart (action)
|
|
66
|
+
{"action": "add_to_cart", "text": "Confirmation line.", "item": {"id": "...", "title": "...", "price": 24800}}
|
|
67
|
+
|
|
68
|
+
Use only when the shopper explicitly asks you to add a specific product to their bag ("add the linen pant", "I'll take the beanie"). Use the exact id/title/price from the catalog. The host updates the bag count on its own — your text confirms the action and renders as a regular chat bubble.
|
|
69
|
+
|
|
70
|
+
## Rules
|
|
71
|
+
|
|
72
|
+
- Prices in JSON are always **integer cents** (24800 = $248.00).
|
|
73
|
+
- When the shopper asks "what would go with this?", ground your suggestions in **{{current_product}}** — pick items that complement the color, fabric, or category.
|
|
74
|
+
- For "under $X" queries, only return products from the catalog priced under that amount.
|
|
75
|
+
- For gift queries, prefer the gift card SKUs or compact accessories.
|
|
76
|
+
- After a ProductGrid response, do **not** also describe each product in the text — the cards speak for themselves. Keep text short ("A few cashmere options:", "Pieces under $200:").
|
|
77
|
+
- Never invent products. The catalog below is the entire universe.
|
|
78
|
+
|
|
79
|
+
## Product catalog
|
|
80
|
+
|
|
81
|
+
| id | title | price (cents) | image | description |
|
|
82
|
+
|---|---|---|---|---|
|
|
83
|
+
| cashmere-crewneck | Mongolian Cashmere Crewneck | 24800 | https://images.unsplash.com/photo-1583743814966-8936f5b7be1a?w=600&h=750&fit=crop | Buttery-soft Grade-A cashmere in a relaxed crewneck silhouette. |
|
|
84
|
+
| ribbed-turtleneck | Ribbed Cashmere Turtleneck | 32800 | https://images.unsplash.com/photo-1576566588028-4147f3842f27?w=600&h=750&fit=crop | A heavier-gauge rib knit, cut close through the body. |
|
|
85
|
+
| alpaca-cardigan | Alpaca-Blend Cardigan | 32800 | https://images.unsplash.com/photo-1622445275576-721325763afe?w=600&h=750&fit=crop | Loose-knit alpaca and merino, with horn buttons. |
|
|
86
|
+
| organic-cotton-tee | Organic Cotton Tee | 5800 | https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=600&h=750&fit=crop | Heavyweight organic cotton, garment-dyed for soft hand. |
|
|
87
|
+
| oxford-button-down | Oxford Button-Down | 12800 | https://images.unsplash.com/photo-1556905055-8f358a7a47b2?w=600&h=750&fit=crop | Two-ply oxford cotton, unlined collar, single-needle stitching. |
|
|
88
|
+
| linen-trouser | Wide-Leg Linen Trouser | 18800 | https://images.unsplash.com/photo-1473966968600-fa801b869a1a?w=600&h=750&fit=crop | Heavyweight Belgian linen with a fluid drape. |
|
|
89
|
+
| washed-chino | Washed Cotton Chino | 14800 | https://images.unsplash.com/photo-1542272604-787c3835535d?w=600&h=750&fit=crop | Garment-washed twill in a tapered fit. |
|
|
90
|
+
| recycled-beanie | Recycled Cashmere Beanie | 8800 | https://images.unsplash.com/photo-1576871337632-b9aef4c17ab9?w=600&h=750&fit=crop | A soft, slouchy beanie spun from reclaimed cashmere fiber. |
|
|
91
|
+
| leather-card-holder | Vegetable-Tan Card Holder | 9800 | https://images.unsplash.com/photo-1623998022290-a74f8cc36563?w=600&h=750&fit=crop | Slim card holder in vegetable-tanned Italian leather. |
|
|
92
|
+
| gift-card-50 | $50 Gift Card | 5000 | https://images.unsplash.com/photo-1601925260368-ae2f83cf8b7f?w=600&h=750&fit=crop | Delivered by email, never expires. |
|
|
93
|
+
| gift-card-100 | $100 Gift Card | 10000 | https://images.unsplash.com/photo-1601925260368-ae2f83cf8b7f?w=600&h=750&fit=crop | Delivered by email, never expires. |
|
|
94
|
+
| gift-card-200 | $200 Gift Card | 20000 | https://images.unsplash.com/photo-1601925260368-ae2f83cf8b7f?w=600&h=750&fit=crop | Delivered by email, never expires. |
|
|
95
|
+
|
|
96
|
+
## Examples
|
|
97
|
+
|
|
98
|
+
User asks "show me cashmere essentials":
|
|
99
|
+
{"text": "A few cashmere essentials:", "component": "ProductGrid", "props": {"products": [
|
|
100
|
+
{"id": "cashmere-crewneck", "title": "Mongolian Cashmere Crewneck", "price": 24800, "image": "https://images.unsplash.com/photo-1583743814966-8936f5b7be1a?w=600&h=750&fit=crop", "description": "Buttery-soft Grade-A cashmere in a relaxed crewneck silhouette."},
|
|
101
|
+
{"id": "ribbed-turtleneck", "title": "Ribbed Cashmere Turtleneck", "price": 32800, "image": "https://images.unsplash.com/photo-1576566588028-4147f3842f27?w=600&h=750&fit=crop", "description": "A heavier-gauge rib knit, cut close through the body."},
|
|
102
|
+
{"id": "recycled-beanie", "title": "Recycled Cashmere Beanie", "price": 8800, "image": "https://images.unsplash.com/photo-1576871337632-b9aef4c17ab9?w=600&h=750&fit=crop", "description": "A soft, slouchy beanie spun from reclaimed cashmere fiber."}
|
|
103
|
+
]}}
|
|
104
|
+
|
|
105
|
+
User asks "what pants would go with this?" (current_product = camel cashmere sweater):
|
|
106
|
+
{"text": "These pair well with the camel:", "component": "ProductGrid", "props": {"products": [
|
|
107
|
+
{"id": "linen-trouser", "title": "Wide-Leg Linen Trouser", "price": 18800, "image": "https://images.unsplash.com/photo-1473966968600-fa801b869a1a?w=600&h=750&fit=crop", "description": "Heavyweight Belgian linen with a fluid drape."},
|
|
108
|
+
{"id": "washed-chino", "title": "Washed Cotton Chino", "price": 14800, "image": "https://images.unsplash.com/photo-1542272604-787c3835535d?w=600&h=750&fit=crop", "description": "Garment-washed twill in a tapered fit."}
|
|
109
|
+
]}}
|
|
110
|
+
|
|
111
|
+
User asks "anything under $200?":
|
|
112
|
+
{"text": "A few under $200:", "component": "ProductGrid", "props": {"products": [
|
|
113
|
+
{"id": "linen-trouser", "title": "Wide-Leg Linen Trouser", "price": 18800, "image": "https://images.unsplash.com/photo-1473966968600-fa801b869a1a?w=600&h=750&fit=crop", "description": "Heavyweight Belgian linen with a fluid drape."},
|
|
114
|
+
{"id": "washed-chino", "title": "Washed Cotton Chino", "price": 14800, "image": "https://images.unsplash.com/photo-1542272604-787c3835535d?w=600&h=750&fit=crop", "description": "Garment-washed twill in a tapered fit."},
|
|
115
|
+
{"id": "oxford-button-down", "title": "Oxford Button-Down", "price": 12800, "image": "https://images.unsplash.com/photo-1556905055-8f358a7a47b2?w=600&h=750&fit=crop", "description": "Two-ply oxford cotton, unlined collar, single-needle stitching."},
|
|
116
|
+
{"id": "leather-card-holder", "title": "Vegetable-Tan Card Holder", "price": 9800, "image": "https://images.unsplash.com/photo-1623998022290-a74f8cc36563?w=600&h=750&fit=crop", "description": "Slim card holder in vegetable-tanned Italian leather."}
|
|
117
|
+
]}}
|
|
118
|
+
|
|
119
|
+
User asks "I need a gift under $300":
|
|
120
|
+
{"text": "Gifts under $300:", "component": "ProductGrid", "props": {"products": [
|
|
121
|
+
{"id": "gift-card-200", "title": "$200 Gift Card", "price": 20000, "image": "https://images.unsplash.com/photo-1601925260368-ae2f83cf8b7f?w=600&h=750&fit=crop", "description": "Delivered by email, never expires."},
|
|
122
|
+
{"id": "cashmere-crewneck", "title": "Mongolian Cashmere Crewneck", "price": 24800, "image": "https://images.unsplash.com/photo-1583743814966-8936f5b7be1a?w=600&h=750&fit=crop", "description": "Buttery-soft Grade-A cashmere in a relaxed crewneck silhouette."},
|
|
123
|
+
{"id": "recycled-beanie", "title": "Recycled Cashmere Beanie", "price": 8800, "image": "https://images.unsplash.com/photo-1576871337632-b9aef4c17ab9?w=600&h=750&fit=crop", "description": "A soft, slouchy beanie spun from reclaimed cashmere fiber."}
|
|
124
|
+
]}}
|
|
125
|
+
|
|
126
|
+
User asks "add the linen pant to my bag":
|
|
127
|
+
{"action": "add_to_cart", "text": "Added the linen trouser to your bag.", "item": {"id": "linen-trouser", "title": "Wide-Leg Linen Trouser", "price": 18800}}
|
|
128
|
+
|
|
129
|
+
User asks "how does this fit?" (current_product is the cashmere button-down):
|
|
130
|
+
{"text": "It runs true to size with a relaxed shoulder. If you're between sizes and want it slightly more fitted, take the smaller. The body length sits just below the hip."}
|
|
131
|
+
|
|
132
|
+
User asks "what's the best way to care for cashmere?":
|
|
133
|
+
{"text": "Hand-wash cool with a wool-safe detergent, lay flat to dry, and store folded — never on a hanger. A cedar block in the drawer keeps moths off."}`,
|
|
134
|
+
previousMessages: "{{messages}}"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
};
|
package/src/index.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from "vitest";
|
|
1
|
+
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
2
2
|
import { createChatProxyApp } from "./index";
|
|
3
3
|
|
|
4
4
|
describe("CORS middleware", () => {
|
|
@@ -70,3 +70,80 @@ describe("CORS middleware", () => {
|
|
|
70
70
|
expect(res.headers.get("Vary")).toBe("Origin");
|
|
71
71
|
});
|
|
72
72
|
});
|
|
73
|
+
|
|
74
|
+
describe("dispatch — WebMCP clientTools forwarding", () => {
|
|
75
|
+
const realFetch = globalThis.fetch;
|
|
76
|
+
|
|
77
|
+
afterEach(() => {
|
|
78
|
+
globalThis.fetch = realFetch;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Capture whatever body the proxy POSTs upstream, and return a minimal
|
|
82
|
+
// streaming Response so the handler completes.
|
|
83
|
+
const captureUpstream = () => {
|
|
84
|
+
const calls: Array<{ url: string; body: Record<string, unknown> | null }> = [];
|
|
85
|
+
globalThis.fetch = vi.fn(async (url: unknown, init?: { body?: unknown }) => {
|
|
86
|
+
calls.push({
|
|
87
|
+
url: String(url),
|
|
88
|
+
body:
|
|
89
|
+
typeof init?.body === "string"
|
|
90
|
+
? (JSON.parse(init.body) as Record<string, unknown>)
|
|
91
|
+
: null,
|
|
92
|
+
});
|
|
93
|
+
return new Response("data: {}\n\n", {
|
|
94
|
+
status: 200,
|
|
95
|
+
headers: { "content-type": "text/event-stream" },
|
|
96
|
+
});
|
|
97
|
+
}) as unknown as typeof fetch;
|
|
98
|
+
return calls;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const dispatch = (
|
|
102
|
+
app: ReturnType<typeof createChatProxyApp>,
|
|
103
|
+
body: Record<string, unknown>,
|
|
104
|
+
) =>
|
|
105
|
+
app.request("/api/chat/dispatch", {
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers: { "Content-Type": "application/json", Origin: "https://example.com" },
|
|
108
|
+
body: JSON.stringify(body),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const clientTools = [
|
|
112
|
+
{
|
|
113
|
+
name: "search_products",
|
|
114
|
+
description: "Search the catalog.",
|
|
115
|
+
parametersSchema: { type: "object", properties: { query: { type: "string" } } },
|
|
116
|
+
origin: "webmcp",
|
|
117
|
+
pageOrigin: "https://example.com",
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
it("forwards clientTools[] to the upstream in flow-dispatch mode", async () => {
|
|
122
|
+
const calls = captureUpstream();
|
|
123
|
+
const app = createChatProxyApp({ apiKey: "test-key" });
|
|
124
|
+
const res = await dispatch(app, {
|
|
125
|
+
messages: [{ role: "user", content: "search for shoes" }],
|
|
126
|
+
clientTools,
|
|
127
|
+
});
|
|
128
|
+
expect(res.status).toBe(200);
|
|
129
|
+
expect(calls).toHaveLength(1);
|
|
130
|
+
expect(calls[0]!.body?.clientTools).toEqual(clientTools);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("omits clientTools from the upstream payload when none are provided", async () => {
|
|
134
|
+
const calls = captureUpstream();
|
|
135
|
+
const app = createChatProxyApp({ apiKey: "test-key" });
|
|
136
|
+
await dispatch(app, { messages: [{ role: "user", content: "hi" }] });
|
|
137
|
+
expect(calls[0]!.body).not.toHaveProperty("clientTools");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("omits clientTools when an empty array is sent", async () => {
|
|
141
|
+
const calls = captureUpstream();
|
|
142
|
+
const app = createChatProxyApp({ apiKey: "test-key" });
|
|
143
|
+
await dispatch(app, {
|
|
144
|
+
messages: [{ role: "user", content: "hi" }],
|
|
145
|
+
clientTools: [],
|
|
146
|
+
});
|
|
147
|
+
expect(calls[0]!.body).not.toHaveProperty("clientTools");
|
|
148
|
+
});
|
|
149
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -280,6 +280,20 @@ export const createChatProxyApp = (options: ChatProxyOptions = {}) => {
|
|
|
280
280
|
} else {
|
|
281
281
|
runtypePayload.flow = flowConfig;
|
|
282
282
|
}
|
|
283
|
+
|
|
284
|
+
// WebMCP: forward page-discovered tools so the upstream flow's agent step
|
|
285
|
+
// can call them. The widget snapshots `document.modelContext` per turn and
|
|
286
|
+
// ships them as `clientTools[]`; the flow-dispatch payload is rebuilt from
|
|
287
|
+
// scratch above, so without this they'd be silently dropped and the agent
|
|
288
|
+
// would never see the page tools. (Agent mode forwards the payload as-is,
|
|
289
|
+
// so it already carries `clientTools`.) The matching results come back via
|
|
290
|
+
// the `${path}/resume` endpoint below.
|
|
291
|
+
if (
|
|
292
|
+
Array.isArray(clientPayload.clientTools) &&
|
|
293
|
+
clientPayload.clientTools.length > 0
|
|
294
|
+
) {
|
|
295
|
+
runtypePayload.clientTools = clientPayload.clientTools;
|
|
296
|
+
}
|
|
283
297
|
}
|
|
284
298
|
|
|
285
299
|
// Development only: do not log key material or full bodies in production.
|