For a few days in September, three quite different failures kept arriving with almost the same explanation.
Internal error.
Codex had hit a usage limit. Claude had an OAuth refresh problem. Agy was missing a runtime component it needed to start.
Those are not the same failure. They do not have the same remedy. One asks you to wait or change capacity. One asks you to repair authentication. One asks you to fix the installation.
Agent Bridge was managing to make them look alike.
That turned out to matter far beyond the quality of the error message.
The useful information was there
ACP errors are structured.
A provider can return a top-level request error and put the useful explanation in nested fields such as data.message or data.details. That is what we were seeing.
A Codex failure preserved the provider's usage-limit explanation inside the diagnostic. Claude exposed OAuth-refresh contention. Agy's nested detail said that its expected localharness binary could not be found.
But different parts of Agent Bridge were reading different parts of the error.
The classification code could sometimes see enough to recognise an authentication or capacity problem. The durable diagnostic path retained some nested fields but not others. The user-facing path often stopped at the top-level Internal error.
So we had produced a strange system in which one component knew why a Run had failed, another component had lost the detail, and the person who could decide what to do next saw the least useful version.
The fix was not to invent better generic error prose. It was to stop discarding the provider's bounded, sanitised explanation.
Recovery depends on classification
I had thought of error reporting mainly as an interface concern.
A good error should tell the user what happened. A bad error is frustrating.
That is true, but it understates the role errors play in an agent runtime.
Agent Bridge makes decisions from failures. It may retry the same provider. It may start a fresh session. It may ask for reauthentication. It may fall back to another provider. It may decide that retrying would be pointless.
Those actions are only sensible if the failure has a useful identity.
A capacity limit is not a broken credential. A broken credential is not a transport closure. A transport closure is not a missing executable.
Once those all become Internal error, the runtime has two bad choices: do nothing useful, or guess.
Autonomy makes guessing more expensive. A person staring at a terminal can inspect logs and reinterpret a vague failure. An unattended system needs the evidence to survive the whole path from provider to policy.
We had treated diagnostics as exhaust
The more interesting mistake was architectural.
We were persisting diagnostics, but I still thought of them as something emitted after the real work had failed. Useful for debugging later. Secondary to the Run itself.
In practice, the diagnostic is part of the Run's state.
If a provider says "your quota is exhausted", that fact may determine whether another attempt should happen. If it says "authentication failed", that fact may change whether the provider remains selectable. If a runtime component is missing, retrying the same invocation without repair is mostly theatre.
The error is therefore not exhaust from the execution path. It is an input to the next decision.
That changed the standard I now apply to failure handling.
A diagnostic is not preserved merely because a log line exists somewhere. It is preserved when the actionable cause remains available, in bounded and safe form, to every layer that needs to make a decision from it.
More detail is not always better
There is an obvious danger here.
Provider errors can contain enormous payloads, internal protocol state or sensitive values. Passing raw exception objects through to chat would solve one problem by creating several worse ones.
So the answer was not "show everything".
We added a provider-neutral extraction boundary that keeps useful nested messages and details while applying the existing redaction and size limits. Durable diagnostics and user presentation can use the same underlying cause without having to render identical text.
That distinction matters.
The user needs a useful explanation. The runtime needs a stable classification. The diagnostic store needs enough technical evidence to investigate. None of them needs an unbounded dump of the provider protocol.
This keeps turning up in production systems
The same pattern exists outside AI.
A database driver wraps a network error. An HTTP client wraps the database driver. An application layer catches that exception and replaces it with "operation failed". By the time an operator or retry policy sees the result, all of the information needed to choose the next action has been normalised away.
Agents make the cost easier to see because we actively expect the software to continue after a failure.
Continuation requires meaning.
If the system cannot distinguish "try again", "authenticate again", "use something else" and "repair the runtime", then it does not have a recovery strategy. It has a retry button.
What I am watching next
We have since become much more deliberate about provider failure classes, retry boundaries and durable Run diagnostics.
I do not expect the taxonomy to stop changing. New runtimes produce new failure modes, and providers are not obliged to describe the same problem in the same way.
The invariant I care about is smaller.
When the provider gives us a useful reason, we should not turn it into a less useful one on the way through.
That sounds like an error-message rule.
I now think it is an autonomy rule.
You cannot recover intelligently from an error you can no longer name.