Public
Claude shopping-agent demo: skills, cart safety, memory
agentsclaudedemo
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
A small, runnable demo of the architecture in Anthropic's blog post "A guide to the anatomy of effective commerce agents", built on Val Town instead of the post's own Python/Node reference repo, anthropics/commerce-agents.
It's a single Claude model in a standard agent loop, equipped with tools and on-demand skills, shopping a small fictional outdoor-gear catalog. Nothing here is real — no real products, no real payment, no real customer data.
- Add your key: open this val's environment variables page and add
ANTHROPIC_API_KEY:https://www.val.town/x/amotivv/commerce-agent-demo/environment-variables?key=ANTHROPIC_API_KEY - Open the val's HTTP endpoint (the
main.tsxlink) and chat with Ridge, the ACME Outdoors shopping assistant. Try:- "I need a 2-person tent under $250"
- "Outfit me for a 3-day backpacking trip for 2 people"
- "Compare the Traverse Mid boot and the Fast Trail Runner"
- "What's your return policy?"
- Tell it a preference ("I'm usually a size 10"), then click New chat and ask something size-related again — it remembers across chats, forgets nothing until you click Forget me.
Optional env vars (both have working defaults):
ANTHROPIC_MODEL— defaults toclaude-sonnet-5(the blog's own pick for latency-sensitive consumer agents; a merchant/analysis agent would default to Opus instead).ANTHROPIC_MEMORY_MODEL— defaults toclaude-haiku-4-5-20251001, a small fast model for the post-turn memory extraction pass.
| Blog concept | This demo |
|---|---|
| One agent loop, not a router + subagents | agent.ts — a single runTurn() loop that calls the Messages API, executes any tool_use blocks, and loops until a final answer. No intent router, no domain subagents. |
| Skills, not subagents | skills.ts has five skill bodies (search-discovery, purchase-research, planning-goals, customer-care, memory-personalization), loaded on demand via a load_skill tool — same five names the blog's own shopping agent uses. |
| System prompt vs. skill, decided by frequency | prompt.ts holds only grounding, cart/checkout semantics, and presentation rules — the stuff nearly every turn touches. Everything else is a skill. |
| Tools call systems you already run; results are shaped for the model | tools.ts / catalog.ts — search_products returns already-ranked, trimmed results; there's no reasoning logic duplicated inside a tool. |
| The UI components are tools | present_products is a real tool call with typed args (product_ids, layout, message); the server validates and renders it as an event, never parsed from prose. Same for view_cart / checkout. |
| Prompt caching layout (global → session → volatile) | agent.ts sends system as two blocks: the byte-identical prompt with an ephemeral cache breakpoint, then per-session memory facts appended after it (uncached, since they change). Tools carry their own trailing cache breakpoint. The chat UI's debug line shows cache_read_input_tokens / cache_creation_input_tokens per turn so you can watch the cache warm up on a repeat visit. |
| Parallel tool calls | agent.ts executes all tool_use blocks in one model response with Promise.all, and returns their results as a single user message — matching the blog's "fewer turns" guidance. |
| Writes and renders accept only server-issued IDs | db.ts's commerce_known_ids table + tools.ts's checks: present_products/add_to_cart/update_cart_item refuse any product ID the session hasn't actually seen from search_products/get_product. |
| Caps enforced on resulting state | add_to_cart computes min(current_qty + requested, stock) — a repeated "add 5 more" can't stack past stock, the way the blog describes for ticket/promo caps. |
| The model stages, a person/policy applies | checkout renders a summary only; there is no charge tool, no payment backend, matching the reference repo's own note that nothing in it "places an order, charges a card, or changes a live listing." |
| Third-party content is fenced | prompt.ts#catalogFence wraps every product description in <catalog_data> tags with an instruction to treat it as data, never instructions — the blog's sanitizer pattern, simplified to fit a demo catalog with no real third-party text. |
| Memory: store / write / read | db.ts's commerce_memory table (typed key/value/category facts, not a markdown blob) + agent.ts#extractMemory (a separate, smaller-model pass that reads only the user's and assistant's text, never tool results) + a small always-in-context block injected into the system prompt each turn. |
| Evals: snapshots, not conversations | Not implemented here — this is a single runnable demo, not a test suite. See "What's different" below. |
This is a from-scratch reimplementation on Val Town's Deno/serverless runtime, not a port of the Python/Node repo. Some things the blog covers are deliberately out of scope for a single demo val:
- No streaming / eager tool dispatch. The Messages API call here is
request/response, not streamed, so there's no token-level UI streaming or
eager-dispatch-on-first-argument. The blog's biggest perceived-latency
techniques (
eager_input_streaming, progressive rendering) aren't represented. - Memory extraction isn't truly asynchronous. It runs after the reply is computed but inside the same HTTP request/response cycle, because a Val Town HTTP isolate doesn't outlive its response for a true background process. The blog's "separate thread, zero latency added to the conversation" claim doesn't fully hold here — this demo's memory pass adds a second small model call to each turn's total time.
- No eval suite, no CI, no multi-team ownership model. Part 3's production-readiness material (SME-authored eval cases, CI gating, canary rollout) has no equivalent in a single demo val.
- One vertical, one skill body each. The reference repo ships four verticals (retail, travel, telecom, entertainment) and both a shopping and a merchant agent. This demo is retail/shopping only, with fictional data invented for this val (ACME Outdoors), not copied from the repo.
- Memory has no deletion/retention UI beyond "Forget me." The blog's data-handling checklist (types of memory allowed, retention period, per-deployment on/off switch) isn't fully built out; "Forget me" covers the "give users a way to delete" requirement only.
catalog.ts— fictional ACME Outdoors product data + a small ranking function standing in for a real search backend.db.ts— sqlite-backed session state: conversation history, known/ server-issued product IDs, cart, and long-term memory facts.skills.ts— the five skill bodies, loaded on demand.prompt.ts— the always-loaded system prompt + the<catalog_data>fencing helper.tools.ts— tool schemas (Anthropic tool-use format) and the executor that enforces every safety rule in one place.agent.ts— the turn loop: calls the Messages API, executes tools, loops ontool_use, builds the cached system-prompt layout, and runs memory extraction after the reply.main.tsx— the Hono HTTP app: serves the chat UI and the/api/chat//api/reset//api/forget//api/catalogroutes.index.html/frontend.ts— the static chat UI (vanilla TS, no framework): message bubbles plus rendered cards forpresent_products/view_cart/checkoutevents, and a debug line showing per-turn token/cache usage.