Commerce Agent Demo — ACME Outdoors

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.

Try it

  1. 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
  2. Open the val's HTTP endpoint (the main.tsx link) 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 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.

What maps to what

Blog conceptThis demo
One agent loop, not a router + subagentsagent.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 subagentsskills.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 frequencyprompt.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 modeltools.ts / catalog.tssearch_products returns already-ranked, trimmed results; there's no reasoning logic duplicated inside a tool.
The UI components are toolspresent_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 callsagent.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 IDsdb.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 stateadd_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 appliescheckout 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 fencedprompt.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 / readdb.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 conversationsNot implemented here — this is a single runnable demo, not a test suite. See "What's different" below.

What's different from the reference repo (be aware of this)

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.

Files

  • 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.