Most “AI agents” are a demo that breaks the moment a real user touches them. The difference between a toy and a system you can put in front of customers is architecture — the loop, the grounding, the guardrails, and the cost controls. Here’s the one that actually ships.

The core loop: observe → decide → act → verify

An agent is not a prompt. It’s a bounded loop where the model chooses a tool, you execute it, and you feed the result back — until the goal is met or a limit trips. The two failure modes that kill production agents are (1) infinite loops and (2) acting on hallucinated arguments. Bound both explicitly:

let turns = 0;
while (turns++ < MAX_TURNS) {
  const step = await model.decide(history);      // returns { tool, args } or { done, answer }
  if (step.done) return step.answer;
  const tool = TOOLS[step.tool];
  if (!tool) { history.push(err(`unknown tool ${step.tool}`)); continue; }
  const args = tool.schema.safeParse(step.args); // validate BEFORE executing
  if (!args.success) { history.push(err(args.error)); continue; }
  const result = await tool.run(args.data);      // the only place side effects happen
  history.push({ role: 'tool', name: step.tool, content: result });
}
throw new Error('agent exceeded MAX_TURNS');      // fail loud, never hang

Validate every tool argument against a schema before you execute. The model will occasionally emit a malformed or invented argument; a schema check turns that into a recoverable retry instead of a corrupted write.

Grounding: the model states nothing it can't cite

The single biggest trust-killer is confident fabrication. In production, the agent must answer from retrieved, real data — never its own memory of your business. Retrieve first, pass the facts into context, and instruct the model to say "I don't have that" when the retrieval is empty. If a number isn't in the grounded context, it does not appear in the answer. This one rule is the difference between a system a business trusts and one it quietly stops using.

Guardrails: irreversible and money actions are confirm-gated

Classify every tool by blast radius. Read-only tools run freely. Reversible writes run and log. Irreversible or money-moving actions never auto-execute — they return a preview the human approves. Encode this in the tool registry, not in prompt text (the model can be talked out of a prompt rule; it cannot be talked out of code that refuses to run):

const tool = {
  name: 'refund_customer',
  risk: 'money',                    // gate lives in code
  run: async (args) => requireApproval(args) ?? doRefund(args),
};

Cost and latency control

Route by difficulty: a cheap fast model for classification and simple turns, a stronger model only for genuine reasoning. Cache the stable system prefix so repeat calls are cheaper. Stream tokens so the user sees a reply in ~300ms instead of after a multi-second pause. And put a hard per-user daily and monthly spend cap in front of everything — a runaway loop or an abusive account should hit a wall measured in cents, not a surprise invoice.

An eval harness, or you're flying blind

The moment you have real users, every prompt tweak risks a silent regression. Keep a small golden set of inputs with expected properties (did it call the right tool? did it refuse the unsafe action? did it avoid fabricating a number?) and run it on every change. Ten good cases catch more production breakage than a hundred manual spot-checks.

What breaks at scale

Tools time out, get called twice, and fail mid-write. Make every side-effecting tool idempotent (safe to retry), wrap external calls in a timeout with a bounded retry, and make the loop resumable so a crash doesn't lose the turn. Frabo runs an autonomous operator on exactly this shape on Cloudflare Workers — the loop is boring on purpose; boring is what survives contact with real traffic.

The takeaway: a shippable agent is a small, bounded loop with validated tools, hard grounding, code-level guardrails on risky actions, cost caps, and an eval set. Get those five right and the "AI" part takes care of itself.