Most teams write reason codes the way they write error messages: as prose. An agent declines a request and the log says something like "I can't do that" or "outside my scope." Those strings are readable and useless. Nothing downstream can branch on them.
The design fix is to invert the order. Decide what your system is allowed to do with a refusal before you name anything. In practice that set is small: retry the step (possibly with a repaired input), escalate to a human or a different agent, or terminate the task. Everything else is noise.
This matches how distributed systems already treat failures. Temporal's non-retryable error pattern exists exactly because retrying a permanent failure wastes time and burns backoff delays for no gain: an invalid input or a missing record fails the same way on attempt one, two, and three (Temporal docs). AWS's guidance on retries says the same thing from the network side: retries help with partial and transient failures, and hurt when the failure is structural (AWS Builder Center).
So the decision rule is: a refusal gets a reason code only if the code changes what happens next. If two codes always produce the same action with the same parameters, they are one code.
A worked comparison makes the cost concrete. Suppose your agent handles 10,000 tasks a day and declines 4% of steps, so 400 refusals. If 60% of those are labeled with a generic "unable to proceed," you have 240 cases a day that a human must read to decide the action. If the taxonomy splits those into retryable (say 70%), policy blocks (20%) and true unknowns (10%), only 40 cases a day reach a queue, and the other 200 resolve automatically.



