Cart
Review the cart before checkout, then hand off to the host's own checkout.
Experience · Shares patterns with Change Preview, Result Receipt
≈ 1,335 tokens per use (tool 835 · guidance 313 · typical payload 187)How token costs are estimated
Example: Cart
OK, I'm ready to check out.
Your cart
When to use
- The user is ready to check out, or asks to see or change their cart.
- They should confirm quantities and pick shipping before the store's checkout.
When not to use
- The user is still choosing a product or its options. Use Product Options or Entity Cards.
- The user asks about one item's price or whether it is a good deal. Use Price Details.
- A sentence answers it ("how much is my cart?") and nothing needs changing.
- You don't have the cart from the store. Never invent items, prices, shipping or discounts.
- The order is already placed. Show a Result Receipt instead.
How the agent should use it
- Pass items, prices, shipping and
promoexactly as the store's cart returned them, in one currency. - Only set
promofor a discount the store has already applied. - On
checkout, update the store's cart to the returned items and shipping, then open the host's checkout. The host recomputes the total. - Never collect payment, card or bank details in chat. Payment happens only in the host's checkout.
- On
keep_shopping, leave the cart as it was and help the user find more.
Checkout is a handoff
The Cart never takes payment and has no payment fields. Pressing checkout sends the final lines and shipping choice back, and your app opens its own checkout (a hosted payment page, your checkout route, a native sheet) where the real amount is calculated and charged. total in the result is the figure the user saw, for the agent's reply and your logs; never charge it.
Editing the cart
- Quantities go from 1 to
maxQuantity(default 10, or the current quantity if higher). Remove a line with its remove button; "Undo" brings it back. - Shipping shows as one row with the chosen method, its arrival and its price. Change expands the other methods; picking one collapses the list again. With a single method there is nothing to change.
- Every line is always visible: the item list never scrolls inside the card.
- Removing the last line shows "Cart is empty" and disables checkout.
- Totals update as the user edits: subtotal, shipping, the discount (never more than the subtotal) and the total, all in integer minor units so
3 × 2.49is exactly7.47. Each change is announced to screen readers with the new total.
The math lives in cart-totals.ts as a pure computeTotals(items, shippingPrice, promoAmount) you can reuse on the server.
Installation
npx shadcn@latest add https://your-site.com/r/cart.json
Usage
import { tool } from "ai"
import { cartTool } from "@/components/genui/cart.schema"
const tools = {
// No execute: the user's decision is the tool result.
[cartTool.name]: tool({
description: cartTool.description,
inputSchema: cartTool.inputSchema,
}),
}Render the tool call, return the user's decision as the tool result, and open your own checkout:
import { Cart } from "@/components/genui/cart"
function ToolPart({ part, addToolResult }) {
if (part.type !== "tool-review_cart") return null
return (
<Cart
{...part.input}
status={part.state === "input-streaming" ? "streaming" : undefined}
result={part.state === "output-available" ? part.output : undefined}
onResult={async (result) => {
addToolResult({
tool: "review_cart",
toolCallId: part.toolCallId,
output: result,
})
if (result.action === "checkout") {
// Your store's cart and checkout recompute prices and take payment.
await updateCart(result.items, result.shippingId)
router.push("/checkout")
}
}}
/>
)
}The agent receives one of:
{ action: "checkout", items: [{ id: "sku_4471-black-42", quantity: 1 }, { id: "sku_8810-m", quantity: 2 }], shippingId: "standard", total: 165 }
{ action: "keep_shopping" }Lines the user removed are left out of items.
With LangGraph
Push it from a graph node with ui.push({ name: "review_cart", props: { toolCallId, ...args } }, { message }), and render the thread's UI messages with the Renderer.
Restoring from history
When a conversation is reloaded, pass the stored result back as result. After a checkout it shows a compact read-only summary and "Sent to checkout"; after keep shopping it shows the cart locked. It never calls onResult again, so the cart can't be sent twice. With the AI SDK, that's the tool call's output:
<Cart
{...part.input}
result={part.state === "output-available" ? part.output : undefined}
/>Schema
| Prop | Type | Notes |
|---|---|---|
title | string? | Default "Your cart" |
currency | string | ISO 4217 code for every amount |
items | CartItem[] | 1–20. Data-bound |
shipping | { options: ShippingOption[], selected? }? | 1–4 options; selected defaults to the first. Data-bound |
promo | { code, amount }? | A discount already applied. Data-bound |
taxNote | string? | e.g. "Taxes calculated at checkout" |
checkoutLabel | string? | Default "Continue to checkout" |
CartItem
| Field | Type | Notes |
|---|---|---|
id | string | Returned in the result |
name | string | |
variant | string? | e.g. "Size 42 · Black". Shown on one quiet line with the unit price (unit price only when quantity is above 1) |
image | { url, alt }? | http(s) only. The row is text-only when missing or broken |
unitPrice | number | |
quantity | number | Whole number, at least 1 |
maxQuantity | number? | Caps the stepper. Default 10 |
ShippingOption
| Field | Type | Notes |
|---|---|---|
id | string | Returned as shippingId |
label | string | e.g. "Express" |
price | number | 0 shows as Free |
eta | string? | e.g. "Arrives Tue 12 Nov" |
Host props: status (streaming, ready, checked_out, kept_shopping), onResult, result and className.

