What an AI Agent Actually Is
Strip away the marketing and an agent is a loop with three parts. Understanding those parts tells you exactly why agents fail in production — and which of the failures are worth fixing.

"Agent" has become one of those words that means whatever the person saying it needs it to mean. A chatbot with a database lookup is an agent. A cron job with an LLM in the middle is an agent. A twelve-service orchestration platform is an agent. When a word covers all of that, it has stopped carrying information.
So here is the definition I actually use when building: an agent is a loop in which a model chooses the next action, an environment executes it, and the result is fed back in. That's it. Three parts — model, tools, loop.
Everything people find impressive about agents, and everything that makes them fail, comes from that loop existing.
The whole thing in thirty lines
Before any framework, it helps to see how little machinery is actually required. This is a complete agent:
async function runAgent(task: string, tools: ToolSet, maxSteps = 12) {
const messages: Message[] = [{ role: 'user', content: task }];
for (let step = 0; step < maxSteps; step++) {
const reply = await model.chat({ messages, tools: tools.schemas });
messages.push(reply);
// No tool call means the model considers the task finished.
if (!reply.toolCalls?.length) return reply.content;
for (const call of reply.toolCalls) {
const result = await tools.run(call.name, call.arguments);
messages.push({ role: 'tool', toolCallId: call.id, content: result });
}
}
throw new Error('Step budget exhausted');
}That is the entire idea. A framework may give you retries, tracing, streaming, and a nicer type system — real value, worth having — but it is not adding a concept. If you understand those thirty lines, you understand agents, and you are in a much better position to judge whether a given framework is helping.
Notice what the loop actually gives you: the model gets to see the result of its own action before deciding the next one. A single prompt has to plan everything up front and hope. An agent can try something, look at the error, and adjust. That feedback is the whole advantage. It is also, as we'll see, the whole problem.
Tools are an API, and you are designing it badly
The single biggest lever on whether an agent works is tool design, and it gets a fraction of the attention that prompt wording does.
A tool definition is an API contract written for a reader who has no documentation, no colleague to ask, and one chance to get the call right. The name and description are not metadata. They are the entire interface.
Consider two versions of the same capability:
// Version A — technically accurate, practically useless
{
name: 'query',
description: 'Runs a query against the data layer',
parameters: { sql: 'string' },
}
// Version B — describes the decision, not the mechanism
{
name: 'find_orders_by_customer',
description:
'Returns up to 50 recent orders for one customer, newest first. ' +
'Use when you have a customer ID and need their order history. ' +
'Does not include cancelled orders. Returns [] if the customer has none.',
parameters: {
customerId: 'string — the internal customer UUID, not their email',
limit: 'number — optional, defaults to 20, maximum 50',
},
}Version A will produce SQL injection attempts, malformed queries, and tables the model guessed the name of. Version B constrains the space so tightly that most failure modes become impossible to express.
The rules I keep coming back to:
Narrow beats general. find_orders_by_customer is better than query, even though query can do more. A tool that can do anything requires the model to know everything.
Say what it does not do. "Does not include cancelled orders" prevents an entire category of wrong answer. Negative space in a description is worth as much as positive space.
Return errors the model can act on. {"error": "customer_not_found", "hint": "Use search_customers with an email to get a UUID"} gets recovered from. A stack trace gets retried identically five times.
Cap the output. A tool that returns 8,000 rows has not helped the model; it has evicted everything else from the context window.
That last one deserves emphasis, because it is where naive agents die quietly.
Context is a budget, not a container
Every tool result stays in the conversation. Step three's output is still there at step eleven, competing for attention with everything else. An agent that reads five files, runs three searches, and inspects a database has spent most of its window on material that was relevant for one step and is now noise.
Two things go wrong at once. The obvious one is the hard limit — you run out of window and the request fails. The subtler and more damaging one is that model attention degrades well before the limit. Long contexts full of stale tool output produce worse decisions than short contexts, and they do it silently. You don't get an error. You get an agent that's mysteriously dumber on step nine than it was on step two.
So the loop needs to manage what it carries:
function compact(messages: Message[], keepRecent = 6): Message[] {
if (messages.length <= keepRecent + 1) return messages;
const [task, ...rest] = messages;
const older = rest.slice(0, -keepRecent);
const recent = rest.slice(-keepRecent);
// Replace older turns with a summary of what was learned, not what was said.
return [task, { role: 'system', content: summarise(older) }, ...recent];
}The distinction in that comment is the one that matters. A summary of what was said preserves transcript. A summary of what was learned preserves state: "the user's plan is pro, expired 2026-08-14; the billing table has no row for them" is three facts that cost thirty tokens and replace three thousand.
Where agents actually fail
Having run these in production, the failures cluster into four shapes.
The confident loop. The model calls a tool, gets an error it doesn't understand, and calls the same tool with the same arguments. Repeatedly. It has no memory that it just tried this, because from its perspective each turn is a fresh look at a growing transcript. A step budget catches this. Detecting a repeated identical call and injecting "this exact call already failed with X — try a different approach" catches it better.
Silent partial success. The agent completes six of eight steps, then produces a summary that reads like all eight succeeded. This is the failure mode I consider genuinely dangerous, because it looks exactly like success. The only real defence is verification outside the model: check the actual database row, the actual file, the actual API response. Never let the agent be the sole judge of whether it accomplished the task.
Plausible fabrication in tool arguments. Asked for orders by a customer whose ID it doesn't have, the model produces a well-formed UUID that does not exist. The parameter description ("the internal customer UUID, not their email") plus a tool that returns a clear customer_not_found handles most of this.
Cost that scales invisibly. Every step re-sends the whole conversation. A twelve-step agent doesn't cost twelve calls' worth of tokens; it costs something closer to the sum of a growing prefix, which is quadratic in the length of what you keep. This is why compaction is a cost control as much as a quality control, and why prompt caching is not a micro-optimisation for agents — it is the difference between viable and not.
When not to build one
Most tasks labelled "agent" are better served by a pipeline. If you know the sequence of steps in advance — fetch, classify, transform, store — write the sequence. Deterministic code is cheaper, faster, testable, and does not occasionally decide to do something else.
The loop earns its cost when the sequence genuinely depends on what is discovered along the way. Debugging a failing test is a real agent task: you don't know whether you'll need to read one file or seven until you've read the first. Summarising an article is not. It is one call with a good prompt, and wrapping it in a loop adds latency and failure modes in exchange for nothing.
A reasonable test: if you can draw the flowchart, build the flowchart. Reach for an agent when the flowchart has a box that says "it depends on what we find."
The part worth internalising
An agent is not a smarter model. It is the same model, given the ability to observe consequences before committing to the next move. Every strength follows from that — recovery from errors, adaptation to what's actually there rather than what was assumed. Every weakness does too: accumulating context, compounding mistakes, cost that grows with each step.
Design the loop, not the personality. Constrain the tools until the wrong action is hard to express. Verify outcomes outside the model. The prompt matters far less than any of these, which is inconvenient, because the prompt is the part that's fun to write.
Filed under


