Overview
"Agent" is the most oversold word in AI right now. It gets attached to anything with a model in it, which makes it useless as a description and dangerous as a design decision.
A real agent is narrow and specific: a loop that decides its own next step, calls a tool, reads the result, and decides again — until it is done or you stop it. That autonomy is the entire value and the entire risk. This lesson covers the loop, the two guards that keep it from running forever or doing something irreversible, and the more useful question of when you should have written a plain workflow instead.
Theory
The loop. An agent is four steps on repeat: perceive the goal and what it has seen so far, decide the next action, act by calling one tool, observe the result. That is the whole idea. Everything else — planning, reflection, multi-agent choreography — is a variation on this loop.
Workflow vs. agent. If you can draw the steps in advance, write a workflow. You get determinism, cheap debugging, and a bill you can predict. Reach for an agent only when the number of steps genuinely depends on what it finds along the way. Most "agents" in production are workflows with one model call in the middle, and they are better for it.
The step budget is not optional. A loop that decides its own exit condition will eventually fail to exit. Cap the iterations, cap the wall-clock time, and treat hitting either cap as a normal outcome you handle — not an error you log and forget.
Irreversible actions need a human. Reading is safe to retry. Sending an email, moving money, or deleting a row is not. Split your tools into reversible and irreversible, and put an approval gate in front of the second group before you ship, not after the first incident.
Visual Diagram
Read it as a circle with two exits. The circle is perceive → decide → act → observe → back to perceive. One exit is the happy path: the model decides there's nothing left to do and returns text. The other exit is the guard rail: a step counter and a cost counter sit outside the circle, incrementing on every lap, and either one crossing its threshold forces an exit regardless of what the model wants next. A third element sits between "act" and any tool tagged as irreversible: a human-approval gate that pauses the loop until a person confirms, rather than letting "act" execute directly.
Examples
Agent-shaped problems, and the workflow-shaped problems that are frequently mislabeled as agents:
- Genuinely agent-shaped: a coding assistant that reads a failing test, edits a file, reruns the test, and repeats until it passes or gives up — the number of edit-test cycles can't be known up front.
- Genuinely agent-shaped: a research assistant that searches, decides the results are insufficient, searches again with a refined query, and keeps going until it has enough sources — same reasoning.
- Mislabeled as an agent: "classify this support ticket, then look up the customer, then draft a reply" — the steps and their order are fixed. This is a three-step workflow. Calling it an agent adds a loop, a stopping condition to get wrong, and non-determinism, for zero benefit.
- Mislabeled as an agent: a form-filling assistant that always does extract → validate → confirm. Same story — the sequence is known, so hardcode it.
Code
A minimal support agent with the loop, a hard step cap as a runaway-cost guard, and a tool that drafts (never executes) a refund — using the Vercel AI SDK's built-in tool-calling loop.
import { generateText, tool, stepCountIs } from "ai";
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { z } from "zod";
const provider = createOpenAICompatible({
name: "example-gateway",
baseURL: "https://ai.gateway.lovable.dev/v1",
headers: { "Lovable-API-Key": process.env.LOVABLE_API_KEY ?? "" },
});
const MAX_STEPS = 6; // runaway-cost guard: hard ceiling on loop iterations
const MAX_REFUND_CENTS = 5_000; // guardrail baked into the tool, not just the prompt
const lookupOrder = tool({
description: "Look up an order by ID and return its status and refund eligibility.",
inputSchema: z.object({ orderId: z.string().min(1) }),
execute: async ({ orderId }) => {
// Stand-in for a real database call.
return { orderId, status: "delivered", refundEligibleCents: 4200 };
},
});
const draftRefund = tool({
description:
"Draft (never send) a refund for an order. A human must approve it before it is issued.",
inputSchema: z.object({
orderId: z.string().min(1),
amountCents: z.number().int().positive(),
}),
execute: async ({ orderId, amountCents }) => {
// A model-chosen number is untrusted input — reject before it reaches billing logic.
if (amountCents > MAX_REFUND_CENTS) {
return { status: "rejected", reason: `exceeds cap of ${MAX_REFUND_CENTS} cents` };
}
return { status: "drafted", orderId, amountCents, requiresHumanApproval: true };
},
});
export async function runSupportAgent(userMessage: string) {
const { text, steps } = await generateText({
model: provider("gpt-4o-mini"),
system:
"You help with order support. Look up an order before answering questions about it. " +
"Never claim a refund was issued — drafting one requires human approval before it is sent.",
prompt: userMessage,
tools: { lookupOrder, draftRefund },
stopWhen: stepCountIs(MAX_STEPS), // perceive -> decide -> act -> observe, capped
});
return { text, stepsTaken: steps.length };
}Three things make this an agent rather than a scripted refund flow: the model decides whether to call
lookupOrder at all and how many times, stopWhen: stepCountIs(MAX_STEPS) is the runaway-cost guard that
exists independent of what the model wants, and draftRefund never moves money — it returns a draft that a
human still has to approve downstream, which is the human-in-the-loop boundary described above made concrete
in code rather than left as a policy document.
Real Product Example
Klarna's AI customer service assistant is a widely reported, concrete example of this loop in production. Announced by Klarna as doing the work of roughly 700 full-time agents, it handles a majority of customer service chats by taking real actions — checking order status, initiating refunds, handling returns — through tool calls into Klarna's existing backend systems, not by generating plausible-sounding text about what it would do. The mechanism is exactly this lesson's loop: perceive the customer's message and account state, decide which backend action (if any) applies, act by calling that system, observe the result, and either respond or take another action. Critically, Klarna scoped the agent to a bounded set of well-defined actions (refunds, returns, order lookups) rather than giving it open-ended access to every internal system, and anything outside that playbook — a dispute, an unusual request, visible frustration — escalates to a human agent rather than the model improvising. That scoping is the human-in-the-loop and termination-condition discipline from this lesson, not an afterthought bolted on for compliance.
Common Mistakes
Building an agent when a workflow would do
The most common mistake in this space isn't a bug — it's an architecture decision made backwards. Teams reach for a looping, tool-calling agent because it sounds impressive, then spend weeks fighting non-determinism (the same input produces a different sequence of tool calls on different runs) to solve a problem that a fixed three-step workflow would have solved deterministically, more cheaply, and with far easier debugging. If you can enumerate the steps in advance, do that — save the loop for when you genuinely can't.
No hard ceiling on steps or cost
A model that gets into a "call tool, get an unhelpful result, call the same tool again" cycle will keep doing that until something stops it. Without an explicit step cap and cost cap that live outside the model's control, "something stops it" ends up being your API bill or a timeout, discovered after the fact rather than prevented. Every agent loop needs a hard, code-enforced ceiling from the first version, not added after an incident.
Letting the agent execute irreversible actions directly
It's tempting to let a working, well-tested agent "just send the email" or "just issue the refund" once it's
been reliable in testing — testing conditions are never a complete sample of production inputs. The fix
isn't more testing; it's an architectural boundary: irreversible or costly actions get a draft-and-approve
tool, never an execute-directly tool, regardless of how good the agent has proven itself to be.
Trusting a step count as a proxy for cost
Ten loop iterations sounds bounded, but if one of those steps hands the model a 50,000-token document retrieved by a tool, that single step can cost more than the other nine combined. A step cap protects against infinite loops; it does not protect against expensive loops. You need a token or dollar budget tracked across the whole run, independent of how many steps it took to get there.
Senior Tips
Senior tip
Default to "no agent" and make the team argue you into one. The bias in most orgs is the opposite — someone proposes an agent because it's the exciting option, and the burden of proof falls on whoever wants the boring workflow. Flip it: require an explicit answer to "why can't this be a fixed sequence of steps?" before a loop gets built. Most of the time the honest answer is "it can."
Senior tip
Log every step of the loop, not just the final answer. When an agent produces a wrong or bizarre result, the final text rarely tells you why. The sequence of tool calls, their arguments, and their results is where the actual failure lives — a tool that returned stale data, a step that misread a previous result, a decision that made sense given bad information three steps back. Structured per-step logging turns "the agent did something weird" into a five-minute diagnosis instead of an unreproducible mystery.
Senior tip
Give the agent a way to say "I'm stuck," and make that a first-class successful outcome, not a failure. An agent that always produces a confident final answer, even when it's actually uncertain or ran out of useful moves, will occasionally hand a user a wrong answer with total conviction. A tool or instruction that lets it explicitly bail — "hand this to a human, here's what I tried and why it didn't work" — is safer and cheaper than pushing it to keep looping until the step cap forces an unsatisfying answer anyway.
Senior tip
Treat the system prompt's tool-use instructions as part of your termination condition, not just your step
cap. "Call lookupOrder at most once per order ID" or "stop and ask the user if you've made two failed
attempts at the same tool" costs nothing to add and meaningfully cuts down on loops that are technically
under your step cap but are still wasting calls repeating a mistake.
Exercises
Exercise
Take three "AI agent" products or features you've used or read about. For each, decide honestly whether it's agent-shaped (the model genuinely decides the sequence of steps at runtime) or workflow-shaped (a fixed sequence marketed as an agent). Write one sentence justifying each classification.
Exercise
In the runSupportAgent code above, MAX_STEPS is set to 6. Describe a realistic user request that would
hit that cap before the agent finishes helping the user, and write one sentence on what the code should do
when that happens (hint: it should not be silence).
Exercise
Design a step cap, a cost cap, and a human-approval boundary for an agent that manages a company's social
media posting — it can draft posts and check analytics freely, but should never publish without review.
Write the three rules as plain English tool-level constraints, the way draftRefund's cap was described in
the Code section.
Mini Project
Runaway guard audit. If you've built any kind of tool-calling loop earlier in this path (or in outside work), spend 20 minutes hardening it against exactly the failure modes this lesson describes.
- Add (or verify you already have) a hard
stopWhen-style step cap, set low enough to bite during testing so you actually see what happens when it trips. - Add a running cost tracker across the loop — even a rough token-count sum is enough — and log a warning once it crosses a threshold you pick.
- Identify any tool in the loop that takes an irreversible or costly action. If one executes directly, change it to draft-and-return instead, and write one sentence describing what the (currently nonexistent) approval step would look like.
- Deliberately trigger the step cap with a prompt designed to confuse the model into looping, and confirm the loop terminates cleanly instead of erroring out or hanging.
If you haven't built a loop yet, do this against the runSupportAgent example from the Code section instead
— lower MAX_STEPS to 2, write a prompt that needs 3 tool calls to answer properly, and observe exactly what
the agent does when it runs out of room.
Resources
- Anthropic Engineering — Building Effective AI Agents: the field report this lesson leans on most heavily — when agent patterns earn their complexity and when a simpler workflow is the better call.
- Vercel AI SDK — Agents Overview: the
ToolLoopAgentAPI and loop-control primitives (includingstepCountIs) used in this lesson's code sample. - Anthropic Engineering — Effective Context Engineering for AI Agents: how to manage what stays in an agent's context across a long-running loop — the foundation for tomorrow's Memory lesson.