The new model shipped on a Tuesday. By Wednesday the team had swapped the endpoint, re-run their suite, and watched the agent fail in the same four places it had been failing for a month. Someone said the thing everyone says in that meeting: maybe we wait for the next one.
They will wait. The next one will land. The agent will fail in the same four places, because none of those four failures were ever about the model.
An agent is two things: a model and a harness. The model is the part you rent, and it is roughly the same part your competitors rented, at the same price, on the same afternoon. The harness is everything else: the tools you exposed, the context you assembled, the errors you handed back, the loop you wrote around the call, and the environment you pointed the whole thing at. It is entirely yours, almost nobody writes about it, and it is where nearly all of the variance in whether your agent works actually lives.
Here is the tell. When a team says their agent got noticeably smarter this month, ask what changed. It is almost never the model. It is six tool descriptions, an error message, and a retry rule.
The harness is most of your agent
Five things sit between a model and a result, and you wrote all of them. Tools are the action surface: the only things the agent can actually do. Context is the visible surface: what you chose to put in front of it this turn, and what you left out. Feedback is what the world says back, including, especially, when the call fails. The loop is the control code that decides whether to retry, redirect, stop, or escalate. The environment is the system itself, and whether its state is legible enough to reason about at all.
Most teams treat these as plumbing around the interesting part. It is the reverse. The model is the commodity in your system. The harness is the product.
The cleanest way to hold this: the model supplies judgment, and the harness supplies everything the judgment operates on. Bad judgment on good information is a model problem, and those are real. Good judgment on bad information is a harness problem, and from the outside it looks identical to a model problem. That confusion is expensive, because it sends teams shopping when they should be building.
Your tool descriptions are prompts. You wrote them like API docs.
An agent selecting a tool is doing a reading-comprehension task against text you wrote, with no ability to experiment first and nobody to ask.
So consider what you gave it. Two tools, search_orders described as "Searches orders," and query_orders described as "Queries the orders table." An agent choosing between those is guessing. So is every human who reads that page. The difference is that the human walks over and asks someone, and the agent commits.
A tool description has to answer four questions that an API doc never bothers with: when to use this instead of the one next to it, what the arguments actually accept, what comes back, and what it costs. That makes it longer than a doc comment, and it should be. Every clause you add deletes a class of failure:
get_customer_orders(customer_id, since?) : "Returns up to 50 orders for one known customer, newest first. Use when you already have a customer_id. To find orders by product, status, or date across customers, use search_orders instead. Excludes cancelled orders unless include_cancelled is true."
Argument schemas are part of that prompt too, and they get even less attention. A field named dt is a guess waiting to happen; start_date is not. A free-text field that only really accepts four values is an open invitation to invent a fifth, so make it an enum. A required field with no example is a coin flip on the format.
None of this is prompt engineering in the sense people usually mean. You are not coaxing anything. You are writing onboarding documentation for a new hire who will read it exactly once, at speed, and then act without asking you a single question.
Errors are the highest-leverage prompt you never wrote
A long agent run spends a surprising share of its life in the failure branch, and that branch was written by whoever built your error handling, probably years ago, for a human with a debugger and a spare browser tab.
Error: 400 Bad Request tells an agent nothing. It will retry, usually identically, because nothing in that string suggests anything else to try. Now you have burned two steps and learned nothing. Compare:
{"error": "invalid_date_format", "message": "start_date must be YYYY-MM-DD. You sent '12 March 2026'. Retry as 2026-03-12."}
That is not a nicer error message. It is a correction, delivered at the precise moment the model can act on it, through the one channel guaranteed to be in context. It is the cheapest leverage in the entire system and almost nobody spends it.
Four rules that hold up in production. Say what to do, not what happened. Any error an agent can see should end with a next action. Distinguish retry from do-not-retry, explicitly. An agent that cannot tell a rate limit from a validation failure treats both as bad luck and burns its budget on the one that will never succeed. Never return a raw stack trace. It costs hundreds of tokens to carry a single bit of information: it broke. Return the near miss. An empty result is a failure with information in it, so spend it: "No orders for customer 88213 in the last 30 days. That customer has 4 orders, most recent 2026-02-11. Widen since to see them." The agent that receives that recovers on the next call. The one that receives [] concludes the customer does not exist and confidently tells someone so.
Context is a budget, not a container
Large context windows created a belief that context management is a solved problem. What actually happened is that the failure mode moved: from truncation, which is loud and obvious, to dilution, which is silent.
Forty steps into a run, the constraint you set at step three is still technically in the transcript and functionally gone. It is competing with thirty-nine tool results, most of them stale, several of them near-duplicates of each other. Nothing was dropped. The signal just got outvoted.
The fix is to stop treating the transcript as the agent's memory. Keep a durable working state next to it: the plan, the constraints, the findings so far, the open question. Update it explicitly as the run proceeds, and re-present it every turn. The transcript is what happened. The state is what matters now, and only the second one has earned a place in every prompt.
The same discipline applies to what tools return. A call that hands back a four-thousand-token JSON blob is not being generous, it is spending your agent's attention on your behalf, and it will spend it on the thirty fields nobody needed. Return what a reader needs, with a way to ask for the rest.
Treat the window the way you would treat a whiteboard in a long meeting. The value was never in how much it holds. It is in what somebody was willing to erase.
Every tool you add makes every other tool harder to use
Tool count is not free. Each new tool is a fresh opportunity to be selected wrongly, and it quietly degrades the selection accuracy of every tool beside it by adding one more near-neighbor to disambiguate against.
Teams get this wrong at exactly the moment the tooling makes it easy. You connect three servers, and now the model is choosing among ninety tools, sixty of which are irrelevant to anything your product actually does. Accuracy drops, and it drops fastest on the tools whose names sound most like their neighbors, which is to say the ones you care about.
Four moves, in rough order of how often they work. Expose the task surface, not the API surface: your agent does not need your API, it needs the eight operations your workflow performs. Consolidate near-neighbors behind one tool with a mode argument, so the model makes one easy choice and then a second one with better information. Scope by phase, because the tools available while researching are not the tools available while executing, and a sub-agent holding six tools beats a monolith holding sixty. And then, genuinely: delete things.
The highest-impact change to a struggling agent is, more often than not, a deletion. That is an uncomfortable line item to put on a roadmap, and it outperforms the upgrade.
The loop is where reliability actually lives
Between one model call and the next sits code that you wrote, and that code decides whether a wrong step becomes a wrong outcome.
The most valuable few lines in most agent loops detect that the last three tool calls were effectively identical and then change something. Not retry harder. Change something: a different tool, a reframed sub-goal, a handoff. An agent looping on the same failing call is not being persistent, it is stuck, and stuck is a state your code can see even when the model cannot.
Give every run a ceiling, in steps and in spend, and decide up front what happens when it hits. An agent with no stop condition is not an agent. It is a bill that occasionally produces an answer.
Watch for progress, not just activity. A run that has made ten tool calls and changed nothing about the world is not being thorough. It is lost, and the loop is the only component in a position to notice.
And build a third outcome. Most loops have two, success and failure, when what you want is stop and hand over, carrying the plan, the findings, and the one specific question that blocked it. A good escalation is worth more than a bad completion. Teams almost never build one, because escalation does not feel like the product, right up until the week it is the only reason anyone still trusts the thing.
The agent is only as good as the environment is legible
Humans operate opaque systems by accumulating folklore. You learn that the status field lies for about an hour after a migration, that the second dashboard is the honest one, that Tuesday exports land late. None of that is written down anywhere. It lives in people.
Agents have no folklore. They have whatever the environment tells them, and most enterprise systems tell you almost nothing without a person standing there to interpret it.
Which means the highest-leverage harness work is frequently not in the agent at all. It is in the system: adding a read model that exposes the state an agent needs to observe, making an operation idempotent so that a retry is safe to attempt, giving a long job a queryable status instead of a log line, returning an entity's history instead of its current row. This is unglamorous systems work, and it is what makes agents possible at all.
That is the same argument spec-driven development makes about legacy code, arriving from the other direction. There the problem is that behavior is implicit. Here the problem is that state is illegible. Both are the same underlying condition: a system built for operators who already knew how it worked.
So: when an agent keeps failing at one step, first ask whether that step is possible to do well with the information the environment actually provides. Surprisingly often the answer is no, and the fix is in the system rather than the prompt.
You cannot debug a harness with a benchmark
Model benchmarks measure the model. They will tell you the new release is better at competition math. They will tell you nothing about whether your agent can close a ticket.
Harness evaluation means running real tasks through the whole loop and grading the end state, not the individual calls. You are not marking text. You are asking whether the world ended up in the right shape: the record valid, the refund matching policy, the ten-call sequence leaving the system consistent. That is a conformance question, which means the evaluation problem is a prerequisite for this one, not a separate topic.
Then hold one side still. Change the harness with the model fixed, and change the model with the harness fixed. Teams that swap both in the same week learn nothing from the result in either direction, which is precisely how a company ends up believing a model release fixed something that a tool description fixed.
And keep the full trace of every run: every call, every argument, every result, every retry. The step you need to understand is almost never the last one. It is step four, which looked fine at the time, and which steps five through ten faithfully built on.
Nobody ships a model
The obvious objection is that this all sounds temporary. Give it two more generations and the model will handle the vague description, recover from the useless error, and manage its own context. Why invest in scaffolding that is being actively obsoleted?
Partly true, and still the wrong thing to plan around. Models get better at general competence. They do not get better at your permissions model, your half-migrated status field, or the six operations your workflow actually needs, because none of that is in anyone's training data. The frontier raises the floor. It does not learn your domain.
And the harness does not vanish as models improve. Its job changes. Today a large fraction of harness work is compensating for what the model cannot reliably do on its own. As that fraction shrinks, what is left is the part that was never about capability: deciding what the agent is permitted to touch, what it can take back, and what it has to prove. That is the trust boundary, and no model release moves it an inch.
The practical argument is simpler than the philosophical one anyway. A model advantage lasts until the next release, which is roughly a quarter. A harness advantage compounds, because every tool you sharpen and every error you make actionable keeps paying out on every model you ever run behind it. When a genuinely better model does land, the teams that get a step change on the Tuesday it ships are the ones whose harness was already good enough to let the improvement show.
Nobody ships a model. You ship the thing you built around it.