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.
ANTHROPIC_API_KEY:
https://www.val.town/x/amotivv/commerce-agent-demo/environment-variables?key=ANTHROPIC_API_KEYmain.tsx link) and chat with Ridge,
the ACME Outdoors shopping assistant. Try:
Optional env vars (both have working defaults):
ANTHROPIC_MODEL — defaults to claude-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 to claude-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:
eager_input_streaming, progressive rendering) aren't
represented.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 on tool_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/catalog routes.index.html / frontend.ts — the static chat UI (vanilla TS, no
framework): message bubbles plus rendered cards for
present_products / view_cart / checkout events, and a debug line
showing per-turn token/cache usage.