Agents and the difference between refusing and failing silently
Five outcomes an agent-facing tool can produce, and what an agent should do with each. The dangerous one is not failure; it is a response that describes a decision rather than the work.

A refusal tells an agent that nothing happened and why; a silent failure tells it that something happened when nothing did. Both leave the world unchanged, and only one of them leaves the agent able to act. That is the whole distinction, and it is a property of the response rather than of the error handling behind it: if the outcome of the work is not in what the caller receives, the caller will assume the work was done.
Most tool responses are treated as a binary, worked or errored, and that is too coarse for anything an agent drives unsupervised. There are at least five distinct outcomes, and an agent needs a different move for each one. Collapsing them is what produces the two classic failures: retrying something that will never succeed, and moving on from something that never ran.
| Outcome | What the caller receives | What the agent should do next |
|---|---|---|
| Refusal | An error naming the missing precondition and the fix | Do the named thing, then call once more |
| Declined on purpose | A success carrying a false flag and a reason | Accept it; the state is already what was wanted |
| Partial, and says so | The result plus a flag naming what was skipped | Use what came back, then narrow the request |
| Failure | An error naming what broke | Stop and report. Never retry anything that spends |
| Silence | A success describing the decision rather than the work | Go and read wherever the outcome was actually recorded |
Refusing early is how a tool keeps an agent from spending money by accident. Market4's diagnosis of an unindexed URL refuses when no sweep has ever inspected that URL, rather than quietly buying an inspection to answer with, on the principle that a read path which can spend is a read path that eventually does. The refusal names the fix in the same sentence, so the agent loses one round trip instead of a day's quota.
The same reasoning applies to work that would be queued rather than run. A keyword sweep whose plan fails its quota check is refused when it is requested, not parked for an administrator, because an approval queue full of requests that cannot run is a queue nobody reads. Refusing at the point of asking keeps the queue meaningful and puts the error in front of whoever can still change the plan.
Some tools should decline to act and still report success, and the response has to make that legible. Drawing a social card for a post that already has a cover image returns a successful result with a flag saying nothing was generated and a reason naming why: the picture somebody chose is still there. An agent reading that does not need to retry, escalate or apologise. Overwriting it is a separate, explicit request.

When an assistant works through dozens of your tools in a single turn, there is no transaction around them. Everything before the failure has already happened, and the turn can still end by reporting success.

Market4 turns one release note into a changelog page, a blog post, a mail-out and a week of social posts — and then tells you which of them brought anyone back.
The rule that produces this shape is to answer the question the caller actually asked. The caller asked for the post to have a card. It has one. Returning an error would be technically defensible and practically wrong, because it would send an agent looking for a problem that does not exist.
Silence is what happens when the outcome of the work is written somewhere other than the response. It is rarely a bug in the ordinary sense; it is usually the residue of a deliberate split between deciding and doing. When the decision is committed first and the work happens afterwards, a failure in the work has nowhere to go: the decision succeeded, the response describes the decision, and the failure is recorded elsewhere.
There is a further subtlety worth knowing about that arrangement. An emergency stop is re-checked at the moment of execution, not only when the request was made, so a request parked before somebody pulled the brake does not escape merely because it was approved afterwards. That check happens inside the same error handling as everything else, which means a deliberate refusal and an unexpected crash land in the same place. Reading the recorded error is what tells them apart.
None of this is about being generous with error messages. It is about the fact that an agent has no way to check the world independently: what it receives is the entire evidence available to it, and any part of the outcome that is missing from the response is a part it will fill in with an assumption. Assumptions are cheap to make and expensive to discover, which is why the response is the right place to spend design effort.
A refusal means the tool declined before doing anything, usually because a precondition was missing, and it names the precondition. A failure means the tool tried and something broke. The distinction matters to an agent because a refusal has a next step — satisfy the condition and call again — while a failure usually means stop and report, particularly when the call spends money or quota.
Because the caller carries on. An error stops an agent and surfaces the problem while somebody can still act on it. A response that reports success on the step before the work leaves the agent believing the task is done, so it moves to the next task, reports completion and leaves the real failure sitting in a record nobody has been told to read.
No, when the caller's request has already been satisfied. Returning success with an explicit flag and a reason — nothing was generated, because a cover image already exists — tells the agent the state is correct without inventing a problem. Reserve errors for cases where the request was not satisfied, and make the reason readable enough to act on.
Use what came back and treat the missing part as unknown rather than absent. That only works when the response says it was partial: a flag naming what was skipped, and how many items were fully processed. Without that flag, an empty section means "nothing here" and "we did not look" at the same time, and the agent has no way to choose between them.
A connection is a transport detail a client can reset by reconnecting. The unit worth counting is the tool, scoped to the account whose resources the call spends.

A result too large to return has three cheaper fixes before persistence: send the delta instead of the state, a count instead of a list, and a budgeted answer that says it was budgeted.