I Built an AI Agent That Handles Orders, Refunds & Support Without LangChain A developer built an ecommerce AI agent from scratch without LangChain, revealing the core logic is a simple while loop that calls an LLM and executes tools until no tool calls are needed. The agent handles orders, refunds, and support by using Anthropic's tool-calling API with a clean folder structure and design patterns like Strategy and Factory. Every AI agent tutorial I found did one of two things: Either it used LangChain, which abstracts away the exact thing you need to understand or it was so simple it was basically a chatbot with if/else routing and called itself an "agent." I wanted to understand what an agent actually is at the code level. So I built one from scratch. Turns out the whole thing is a while loop. js typescriptwhile true { const response = await llm messages ; if noToolCalls break; // Claude answered — we're done await runTools toolCalls ; // Claude needs data — run the tools messages.push toolResults ; // feed results back, loop again } That's the agent. Everything else is just well-written tools hanging off it. What we built An ecommerce support agent that can: 🔍 Search products by natural language query 📦 Check order status by order ID 📋 Answer return policy questions 🎫 Create support tickets write action, foreshadows the Command pattern The agent figures out on its own which tool to call — sometimes multiple tools in a single message. You never write if message.includes "order" anywhere. How tool-calling actually works This is the part most tutorials gloss over. When you pass tools to anthropic.messages.create , Claude's response isn't just text but it's an array of content blocks, each tagged with a type: { "content": { "type": "text", "text": "Let me check that for you." }, { "type": "tool use", "id": "toolu 01A2bC...", "name": "search products", "input": { "query": "wireless earbuds" } } , "stop reason": "tool use" } Your code filters on type: typescriptconst toolCalls = response.content.filter block : block is Anthropic.ToolUseBlock = block.type === "tool use" ; Notice the TypeScript type guard — block : block is Anthropic.ToolUseBlock. This isn't just a runtime check. It tells TypeScript to narrow the type so call.name and call.input are properly typed, not any. The SDK exports ContentBlock = TextBlock | ToolUseBlock as a proper discriminated union — use it. If toolCalls.length === 0, Claude responded with plain text. That's your exit condition. Loop ends. Folder structure — organized to scale src/ ├── agent/ │ └── EcommerceAgent.ts the while true loop as a class ├── tools/ │ ├── types.ts shared Tool type │ ├── index.ts registry — the only file that knows all tools │ ├── searchProducts.ts │ ├── getOrderStatus.ts │ └── getReturnPolicy.ts ├── data/ │ ├── products.ts swap this for Postgres later │ ├── orders.ts │ └── policy.ts └── cli.ts The key decision: each tool in its own file. Adding a new tool means one new file + one line in tools/index.ts. The agent loop never changes. js typescript// tools/index.ts — the registry export const tools: Tool = searchProductsTool, getOrderStatusTool, getReturnPolicyTool, ; js typescript// agent loop — never touches individual tools directly const tool = this.toolset.find t = t.name === call.name ; This is the Factory pattern in practice — the registry decides which tool to instantiate, the agent just calls it by name. The design patterns hiding in plain sight I didn't set out to implement design patterns. But when you structure this properly they appear naturally: Strategy — swappable LLM provider typescriptclass EcommerceAgent { constructor private readonly client: Anthropic = new Anthropic , private readonly toolset: Tool = tools, private readonly model: string = "claude-sonnet-4-6" {} } Want to swap Anthropic for OpenAI? Change one constructor argument. Nothing else moves. Factory, tool registry The tools/index.ts file is your factory. The agent never does new SearchProductsTool — it looks up by name. Adding a tool is additive, not a modification. Repository sort of — data isolation typescript// src/data/products.ts export const products: Product = ... ; Today it's an array. In production it becomes a Postgres query. The tool files that import from data/ don't change — only the data file itself changes. That's the Repository pattern's whole point. Command — write actions need special treatment getOrderStatus is a read. getReturnPolicy is a read. But createSupportTicket mutates something. In production that needs: Audit logging Confirmation before execution Idempotency don't create two tickets for one click That's the Command pattern — wrap write actions as objects with their own validation and logging, not just another function in the tool registry. CQRS, already naturally split Your read tools search, status, policy hit one data source. Your write tools create ticket hit another path entirely. The split is already there — CQRS just makes it intentional and explicit. The one question everyone gets wrong "Does this need WebSockets or SSE?" No. Here's why. The agent loop runs entirely server-side. Multiple Anthropic API calls, tool executions, result feeding — all of it happens inside one async function. From the client's perspective: Client sends ONE request → server does the whole while true loop internally → server sends ONE response back SSE is a UX upgrade stream tokens as they arrive so users don't stare at a blank screen for 3 seconds , not a technical requirement. The agent works perfectly fine as a standard request/response without it. What "static data" means for production In the video we use hardcoded arrays: js typescript// today const products = { id: "p1", title: "Wireless Earbuds Pro", ... } ; // production class PostgresProductRepository implements ProductRepository { async search query: string { const embedding = await embedText query ; return vectorDb.query { vector: embedding, topK: 5 } ; } } The tool file doesn't change. The import changes. That's it. That's what "basic but scalable" actually means — not that the simple version is production-ready, but that the seam where you'd upgrade is visible and clean. Full agent loop, the complete picture typescriptexport class EcommerceAgent { constructor private readonly client: Anthropic = new Anthropic , private readonly toolset: Tool = tools, private readonly model: string = "claude-sonnet-4-6" {} async chat userMessage: string, history: ChatMessage = : Promise