Skip to main content
Agent Engineering

Agent architectures that survive contact with users

The demo agent and the production agent share almost no code paths that matter. What breaks is rarely the model. It is state, context, tool interfaces, permissions and interruption — and every one of them is a design decision made before the first user arrives.

What real users do that a demo never does

Every agent demo runs the same way: a clean request, a happy path, a person watching who knows what the system can do. Production is the opposite of all three, and the gap is not about model quality. It is about the fifteen behaviours below, none of which the demo exercised, all of which arrive in the first week.

They change their mind halfway through a long task. They close the tab and come back tomorrow expecting the work to still be there. They paste four hundred pages. They ask two unrelated things in one message. They open the same task in two windows. They interrupt at step nine to correct step three. They ask for something the tools cannot do and take an apology as a yes. They approve everything without reading once the fourth confirmation dialog appears. And a few of them are actively trying to get the agent to do something it should not.

None of that is exotic. All of it is Tuesday. The architecture either has an answer for each behaviour or it produces an incident, and the incidents are more expensive than usual because an agent's mistakes are written into systems rather than displayed on a screen.

You are probably here because

  • A deploy killed in-flight agent runs and the users had to start over
  • An agent looped on the same failing tool for forty calls before anyone noticed
  • Long sessions get worse the longer they run and nobody can say exactly why
  • Something got written that should not have been, and the trace is not detailed enough to explain it

Those are four different symptoms of one architectural choice: the loop was written as a function call instead of as a durable state machine with budgets and permissions around it.

The loop is a state machine. Write it down.

The single highest-value structural decision is to stop treating the agent as a function that runs to completion and start treating it as a workflow with persisted state. Everything else in this article gets easier once that is true.

Persist after every step, not at the end. A step record holds the model's decision, the tool called, the arguments, the response, the tokens, the latency and the timestamp. Write it before executing the next step. Now a crashed process, a deploy, a scaled-down replica or a user closing their laptop all become resumable rather than fatal, and you get the debugging trail for free.

Give the run explicit states. Queued, running, waiting on a person, paused, succeeded, partial, failed, cancelled. The two people always forget are waiting on a person and partial, and both are load-bearing: an agent blocked on an approval is not running, and a task that completed nine of twelve items has produced something worth keeping.

Separate the conversation from the task. A user can have several tasks in one conversation and one task can outlive many conversations. Modelling them as one object is a refactor everybody eventually does, and doing it on day one costs nothing.

Make every step idempotent under replay. Resumption means re-running from a checkpoint, and a step with a side effect will happen twice unless it carries a key. This is the same discipline payments systems use, and it transfers exactly: a client-generated key per step, written before the effect, checked on replay.

If a deploy can destroy a user's in-flight work, the agent is not a product yet. It is a script with a chat interface.

Context is the scarcest resource, and it degrades before it fills

A long-running agent accumulates history: instructions, tool schemas, every result, every intermediate thought. Two things go wrong, and only one of them is obvious.

The obvious one is the hard limit. The subtler and more damaging one is that quality falls off well before the limit is reached. Material buried in the middle of a very long context gets used less reliably than material near the beginning or the end — a well-documented effect across model families — so an agent at step thirty is reasoning over a transcript in which the important instruction from step two is now the least reliable thing in the window. The failure is not an error. It is an agent quietly forgetting a constraint it was given.

Pin the invariants. The task definition, the hard constraints and the current plan should be re-rendered at a fixed position on every call rather than left to scroll away. This is cheap and it prevents the most common long-session failure.

Compact deliberately, on a rule. When history exceeds a threshold, summarise the older portion into a structured record — decisions taken, facts established, things ruled out and why — and keep recent turns verbatim. Structured beats prose here, because prose summaries lose the specific values the agent will need later.

Externalise state instead of carrying it. A scratchpad the agent reads and writes through tools is better than a transcript it must re-read every turn. Findings go in a store; the context holds a pointer. This is the difference between an agent that degrades linearly with task length and one that does not.

Keep tool results out of the transcript when they are large. A tool that returns forty thousand tokens of search results has just spent the session's budget. Return a summary plus a handle, and let the agent request detail on the specific item it needs.

Tools are the surface where things actually break

Most agent failures we are asked to diagnose are tool interface failures wearing a model costume. The model made a reasonable decision given a badly specified tool, or an unhelpful error, or a name that meant something different from what it did.

RuleWhyWhat it looks like when ignored
Few tools, not manySelection accuracy falls as the set grows; overlapping tools are worse than missing onesForty tools, two of which are almost the same, chosen at random between
Errors that say what to do nextThe error message is a prompt. It is the model's only instruction for recovery“Request failed” — retried identically eleven times
Idempotent, with a keyRetries and resumption are guaranteed, so double execution must be impossibleTwo refunds, one request, one very unhappy customer
Preview before destructive actionA dry run gives both the model and the person the diff before it is realAn update that matched more rows than anyone expected
Bounded, paginated resultsUnbounded output destroys the context window in one callOne search consuming the entire session budget
Descriptions written as promptsThe description is the only thing telling the model when not to use itA tool called for every request because nothing said otherwise

The error-message rule is worth dwelling on because it is the cheapest large improvement available. Compare "Error: invalid query" with "Error: field 'created' is not a valid column. Available columns: created_at, updated_at, closed_at. Did you mean created_at?" The first produces a retry loop. The second produces a corrected call. You are writing documentation for a reader who only ever sees one line at a time, and that one line is the most consequential prose in the system.

Where production agent failures originate — our observed distribution

Tool interface: naming, errors, unbounded output
28
Context management in long sessions
23
State and resumption: lost work, replayed effects
18
Permissions and unintended writes
14
Runaway loops and cost incidents
11
The model genuinely reasoning badly
6

Our attribution across the agent incidents we have been asked to diagnose. Not a survey. The ordering has been stable, and the last row is where teams look first.

Blast radius: decide what it can break before it can break it

An agent acts on behalf of somebody, and the permissions question is not optional plumbing. Get it wrong and the failure is unrecoverable rather than annoying.

Act as the user, never as the service. If the agent runs with a service account that can see everything, then a prompt injected into a document can reach everything. Scope every tool call to the initiating user's own authorisation, and let your existing access controls do the work they already do.

Two tiers, and keep the gated tier small. Reversible, low-consequence actions run freely: reads, drafts, anything trivially undone. Irreversible or externally-visible actions — sending, paying, deleting, publishing, anything touching another person — require confirmation. The discipline that matters is keeping the second tier short, because approval fatigue is real and a user who has clicked through nine confirmations is not reading the tenth. Gate few things and make each one count.

Confirmations show the diff, not the intention. "The agent would like to update customer records" is not a decision anyone can make. "Update 1,247 records: set status from active to archived" is. Show what will change, how many, and what is undone if it is wrong.

Assume anything the agent reads is hostile. Documents, web pages, tool responses, ticket bodies — any of it may contain text aimed at your agent. Treat all of it as data rather than instruction, keep the boundary explicit in the prompt structure, and put the real guarantee in the permission layer rather than in the wording. An agent that structurally cannot delete does not need to be persuaded not to.

Anything the agent reads may be written by someone who wants it to do something else. The permission layer is the only part of that sentence you control.

Interruption, handoff, and the stop button that actually stops

Any agent that runs longer than a few seconds needs to be interruptible, and interruptible means the work stops, not that a status field changes while the tokens keep being billed.

Cancel must propagate. Signal the worker, abort the in-flight model call, mark the run cancelled, and return what completed. A cancel endpoint that flips a flag while generation continues is a bug that costs money on every use.

Support redirection, not just stopping. The most common interruption is not "stop", it is "no, not that one — the other account." That means accepting new input mid-run, folding it into the plan, and continuing. Designing for it turns a frustrating restart into the feature people like most.

The handoff to a person has to be readable. When an agent escalates, a human needs the goal, what has been done, what is left, what it is blocked on and what it recommends — in that order, on one screen. Handing over a raw transcript of forty tool calls is not a handoff, it is a transfer of confusion. And after a person intervenes, the resumed agent needs to know what they did, or it will redo it.

Design Note

Store the exact prompt, not the template

When something goes wrong at two in the morning, the question is always what the model actually saw. Templates plus variables reconstructed later are close and not identical, and the difference is usually where the bug is. Persist the fully rendered prompt for every step, along with the model snapshot, the sampling parameters and the tool schemas as sent. It is the difference between reproducing a failure in five minutes and arguing about it for a day.

Send the architecture and we will tell you what will break first.

Email your tool list, your loop structure and a couple of real trajectories to contact@precisionfederal.com. You get back a written note naming the three failure modes we would expect to see first in production and what we would change to prevent each. One business day. No charge, no meeting, no deck.

contact@precisionfederal.com

Circuit breakers, because the loop will not stop itself

An agent that cannot complete a task does not fail cleanly. It keeps trying, and every attempt costs money and capacity. Four budgets, enforced in code rather than requested in the prompt, and each returns a partial result rather than an exception:

  • Step budget — a hard maximum per run, sized from the observed distribution rather than guessed, typically two or three times the p95 step count for that task type.
  • Token budget — cumulative across the run, because a small number of very large steps costs the same as many small ones.
  • Wall-clock budget — the user gave up long before this fires; it exists to stop the work, not to serve them.
  • Progress detector — the same tool called with the same arguments three times, or N steps with no change to the task state, is a loop. Stop and escalate.

The progress detector catches what the counters do not: an agent burning its full budget productively-looking and achieving nothing. It is simple to implement — hash the tool name and arguments, keep a small window — and it catches the most expensive failure mode in the category.

Every breaker returns a partial result with an explanation. "I got through eight of the twelve accounts and stopped at the ninth because the lookup kept timing out" is useful. A generic failure after fifteen minutes is not, and it throws away the eight that worked.

How many agents, honestly

Multi-agent architectures are the most over-adopted pattern in this field, so it is worth being direct about when the split earns its cost.

Start with one agent and a good tool set. It is easier to debug, cheaper to run, and its trajectory is a single readable sequence. A surprising share of what gets built as three coordinating agents works better as one agent with three well-designed tools.

Split into planner and executor when the steps are genuinely heterogeneous — when planning wants a strong model and one long look at the whole problem, while execution is many cheap, narrow, parallelisable calls. The saving is real and the coordination cost is modest because the interface is one plan object.

Use separate agents when there is real parallelism or a real isolation requirement. Ten documents processed independently is genuine parallelism. A sub-agent that must not see the parent's context is a genuine isolation need. Both are good reasons.

Do not split because the diagram looks better. Every additional agent adds a context boundary where information is lost in translation, a failure mode where one agent waits on another that has already failed, and a cost multiplier from the coordination messages. The debugging difficulty rises faster than the number of agents. If you cannot name the specific parallelism or isolation you are buying, you are paying for a diagram.

The mistakes we get called in to fix

  • Agent state in process memory, so every deploy destroys in-flight user work
  • Tool errors that say “request failed”, producing identical retries until a budget runs out
  • A cancel button that sets a flag while the model keeps generating and billing
  • A service account with broad access, reachable by any text the agent happens to read
  • Confirmation dialogs on everything, so users approve without reading by the fourth one
  • Unbounded tool output consuming the context window in a single call
  • No progress detector, so a loop burns the full budget looking busy
  • Three agents where one would do, with the coordination layer as the top source of bugs

A four-week hardening pass on an agent that already works

Agent Hardening

1
Move run state to durable storage; persist a step record before each step executes
Week 1
2
Make every side-effecting tool idempotent with a key, and add previews to destructive ones
Week 1–2
3
Rewrite tool errors as recovery instructions; bound and paginate every result
Week 2
4
Add the four budgets and the progress detector; make every breaker return partial work
Week 3
5
Scope tools to the user's own permissions; define the small gated tier and its diff view
Week 3
6
Real cancel and redirect; the handoff summary; context pinning and structured compaction
Week 4

Then break it deliberately, which is the part that gets cut and the part that finds things. Kill the process mid-run and check the work resumes. Make a tool time out and see whether the loop recovers or thrashes. Feed it a document containing instructions aimed at the agent. Cancel a run and confirm the tokens stopped. Every one of those is an afternoon, and every one of them is a defect a user would otherwise find for you.

Before you put an agent in front of customers

  • Run state is durable and a run survives a deploy mid-task
  • Every side-effecting tool is idempotent under replay
  • Every tool error tells the model what to do differently
  • Tool output is bounded, paginated and summarised before it enters context
  • Task definition and constraints are pinned, not left to scroll away
  • Tools run with the initiating user's permissions, never a broad service account
  • The gated action tier is short, and confirmations show the actual diff
  • Step, token, wall-clock and no-progress budgets are enforced in code
  • Cancel stops upstream work and returns what completed
  • The fully rendered prompt and the full trajectory are persisted for every step

Bottom line

Agents fail in production for boring, fixable reasons that have almost nothing to do with model quality. Make the loop a durable state machine and resumption, debugging and interruption all become tractable at once. Treat the tool layer as the real interface and spend your care on error messages and output bounds, because that is where the trajectory goes wrong. Decide the blast radius before shipping rather than after an incident. Put the budgets in code, since a prompt cannot enforce anything. And be honest about how many agents the problem actually needs, which is usually one.

Frequently asked questions

Why do agents get worse the longer a session runs?

Mostly context. History accumulates, and material buried in the middle of a long window is used less reliably than material at either end, so a constraint given early quietly stops being followed. Pin the task definition and hard constraints at a fixed position on every call, compact older history into a structured record, and move working state out of the transcript into a store the agent reads through tools.

How do you stop an agent from looping?

Budgets in code, not instructions in a prompt: a step cap, a cumulative token cap, a wall-clock cap, and a no-progress detector that trips when the same tool is called with the same arguments repeatedly or the task state stops changing. Each breaker should return partial work with an explanation of where it stopped, because a generic failure discards everything that succeeded.

Should an agent ask permission before every action?

No — that produces approval fatigue and users clicking through without reading, which is worse than no gate at all. Split actions into reversible and irreversible, let the reversible tier run freely, and gate a deliberately short list of consequential actions with a confirmation that shows the actual change and its scope rather than a statement of intent.

When is a multi-agent architecture worth the complexity?

When there is genuine parallelism, a genuine isolation requirement, or a planning step that wants a stronger model than execution does. Otherwise one agent with a well-designed tool set is easier to debug, cheaper, and usually more reliable. Every boundary between agents loses information and adds a coordination failure mode, so name what you are buying before you add one.

What should we log to debug an agent in production?

The full trajectory: for each step, the fully rendered prompt as sent, the model snapshot and sampling parameters, the tool call with arguments, the response, tokens, latency and timestamp, all under one trace id. Storage is cheap and reconstructing a prompt from a template afterwards produces something close but not identical — which is usually exactly where the bug is hiding.

1 business day response

Want to know what will break first when real users arrive?

Send your tool list, your loop structure and a few real trajectories. Our engineers will come back with the failure modes we would expect first, ranked, and what we would change to prevent each — or do the hardening pass with your team as a scoped piece of work. Email bo@precisionfederal.com.

Email an engineerCapabilitiesMore insights →
Agent SystemsBackend EngineeringReliabilityApplied ML