AI agents have moved from research demos to production systems in barely two years. But most teams shipping “agents” today are really shipping a prompt, a loop, and a great deal of hope. This article looks at what actually makes an agent work, and where the current generation still falls apart under load.
What we mean by “agent”
The word has been stretched to breaking point. For the purposes of this article, an agent is a system that uses a language model to decide which actions to take, executes those actions against tools or environments, and feeds the results back into its own reasoning loop.
That definition has three moving parts, and each one is a source of failure:
Perception
The model turns raw context (a user request, tool output, retrieved documents) into an internal representation it can reason over. Garbage in, confidently-wrong out.
Decision
Given that representation, the model chooses the next action. This is where planning, tool selection and self-correction live.
Action
The chosen action is executed against the real world. This is the only part that has consequences, and the only part the model does not control.
A reference architecture
Most robust agent systems converge on a similar shape, regardless of framework. There is a model at the center, a set of tools it can call, a memory layer, and, critically, a deterministic orchestrator that the model does not get to override.
The orchestrator is the part teams under-invest in. It owns retries, timeouts, budget limits, and the hard stops that keep a confused model from calling delete_production_database seventeen times in a row.
The control loop, in code
Here is the loop stripped to its essentials. Note that the model never touches the tool registry directly; it only ever emits a request that the orchestrator validates.
type ToolCall = { name: string; args: Record<string, unknown> };
async function runAgent(goal: string, budget = 8): Promise<string> {
const history: Message[] = [{ role: 'user', content: goal }];
for (let step = 0; step < budget; step++) {
const decision = await model.decide(history);
if (decision.type === 'final') {
return decision.answer;
}
// The orchestrator, not the model, decides if a call is allowed.
const call = decision.call as ToolCall;
if (!registry.isAllowed(call.name)) {
history.push(toolError(call, 'tool not permitted'));
continue;
}
const result = await registry.run(call).catch(toObservableError);
history.push(toolResult(call, result));
}
throw new BudgetExceededError(goal);
} The budget parameter is doing more work than it looks. Unbounded loops are the single most common way agents burn money and trust. A hard ceiling turns an infinite failure into a bounded, observable one.
Where agents break
After building and reviewing a number of these systems, the failure modes cluster into a small number of categories.
| Failure mode | What it looks like | Usual root cause |
|---|---|---|
| Context rot | Quality degrades as the loop grows | Unbounded history in the prompt |
| Tool thrashing | Same tool called repeatedly, no progress | No memory of prior results |
| Confident errors | Wrong answer stated with total certainty | No grounding / verification |
| Silent truncation | Half a plan executed, then forgotten | Token limits hit mid-task |
| Runaway cost | A simple task costs dollars | Missing budget ceiling |
Memory: the part everyone gets wrong
The naive approach, appending everything to the prompt, works until it doesn’t, usually right around the point a demo becomes a product. Effective memory is layered:
- Working memory: the current task’s recent steps, kept verbatim.
- Episodic memory: summaries of past tasks, retrieved on demand.
- Semantic memory: durable facts about the user or domain, curated deliberately.
Treating these as one undifferentiated context window is the fastest route to context rot. The discipline is knowing what to forget.
Where this is heading
Three shifts look durable rather than hype-driven:
- Smaller, specialized models doing tool selection while a larger model handles synthesis, which is cheaper and more reliable than one model doing everything.
- Verification-first designs, where a second pass checks the first rather than trusting it.
- Standardized tool protocols so agents stop reinventing function calling for every integration.
The agents that survive contact with production are not the cleverest ones. They are the ones wrapped in enough deterministic scaffolding that the model’s mistakes stay small, observable, and cheap. That is not a glamorous conclusion, but it is the one the next few years will keep proving right.