Agent Tool Error Envelopes: Make Failures Safe to Recover From A developer has proposed a vendor-neutral typed error envelope for agent tool results, designed to replace ambiguous human-readable error strings with machine-readable fields that tell the runtime whether a state change occurred, whether retry is safe, and what action to take next. The design centers on a three-value commit_state (not_committed, committed, unknown), paired with retry directives, a next_action enum, and a durable operation_ref, with the goal of preventing duplicate side effects such as double charges when a write's outcome is uncertain. The author argues that HTTP status codes convey transport information rather than workflow information, so a 500 or even a 200 can mask an application-level failure an agent must handle deterministically. An agent calls create invoice . The network times out. Did the invoice fail, or did the provider create it just before the connection broke? If the tool returns only "request failed" , the model has to guess. Retrying may duplicate a charge. Stopping may leave the customer waiting. A human-readable error message is useful for a person, but it is a poor control plane for an autonomous workflow. The fix is not a more forceful prompt that says “do not retry.” Put a small, typed error envelope around every tool result. It should tell the runtime what is known, what is safe to do next, what must never be repeated automatically, and what evidence to preserve. This guide shows a vendor-neutral design that works for HTTP APIs, MCP tools, queues, and internal services. The practical payoff is simple: failures become explicit workflow states instead of ambiguous text in a chat transcript. HTTP status is transport information. An agent needs workflow information. A 400 can mean a field is missing and can be corrected. It can also mean the user is not allowed to make this change. A 500 can be a temporary dependency outage, or it can be returned after a side effect already committed. Even a 200 can contain an application-level failure if a legacy API always returns a successful transport response. This matters more for agents because the caller is probabilistic. If it sees a paragraph containing “try again,” it may resend an identical write, modify unrelated parameters, or continue as though the action worked. Those are three different recovery policies hiding behind one string. Treat tool results as a contract between two deterministic components: The model can help repair a request. It should not invent a failure policy after a payment, message, deployment, or record update becomes uncertain. An error envelope does not need dozens of fields. It must answer four questions reliably. | Question | Why the runtime needs it | Example answer | |---|---|---| | Did the requested state change? | Prevent duplicate side effects | unknown | | Is an automatic retry permitted? | Bound loops and unsafe repeats | never | | What action may happen next? | Keep recovery deterministic | verify operation | | What can the agent safely tell the user? | Avoid invented success or exposure | Your request is being checked | The critical value is not retryable . It is commit state . A timeout before a request leaves the process is different from a timeout after the remote system may have accepted it. Use three values: not committed : the downstream operation definitely did not run. committed : the tool has verified the desired state. unknown : the tool cannot prove either outcome. For a write, unknown means verify before retry . Never silently treat it as failure. Here is a TypeScript shape suitable for a JSON tool response. Keep stable machine fields separate from the explanation shown to the model. type CommitState = "not committed" | "committed" | "unknown"; type RetryDirective = "never" | "after delay" | "after input fix"; type NextAction = | "correct input" | "wait and retry" | "verify operation" | "request approval" | "escalate"; type ToolError = { ok: false; code: string; // stable, documented identifier category: "validation" | "authorization" | "conflict" | "dependency" | "internal"; commit state: CommitState; retry: { directive: RetryDirective; not before?: string; // ISO time, not a vague duration max attempts remaining: number; }; next action: NextAction; operation ref: string; // durable idempotency or operation key safe details: Record