A tool call that fails is still part of the conversation. Yet many agent integrations turn a useful failure into an empty string, a stack trace, or a vague failed flag. The model then has to guess what happened. It may repeat a request that cannot work, invent a missing value, tell the user that an action completed, or abandon a task that only needed one correction.
The practical move is to make every tool failure return a failure receipt: a small structured result that names the operation, classifies the failure, preserves the correlation ID, and states the next safe action. The receipt is not an apology for the error. It is the bounded handoff that lets the model and the surrounding application decide whether to retry, repair the request, ask a person, or stop.
OpenAI’s function-calling guide describes tool calling as a multi-step loop: the application receives one or more calls, executes them, and sends a function_call_output back to the model. It also says that the returned output format is controlled by the application and can be JSON, error codes, or plain text.[1] Anthropic’s tool-call guidance similarly specifies a tool_result block matched to the original tool-use ID and provides an is_error field for execution errors.[2] Both interfaces make the same operational point: the tool result is an explicit message in the workflow, so its failure shape should be designed instead of improvised.
Classify the failure before the model reacts
Do not collapse every problem into “retry later.” A tool failure usually belongs to one of a few action classes:
- Repairable input: a required date, identifier, or enum value is missing or invalid.
- Authorization or approval: the request is valid, but the caller lacks permission or a confirmation is required.
- Not found or conflict: the named record does not exist, changed since it was read, or fails a precondition.
- Transient infrastructure: a timeout, rate limit, or temporary upstream error may justify a bounded retry.
- Permanent policy or capability limit: the requested operation is forbidden, unsupported, or outside the tool’s scope.
The classification matters more than a provider-specific exception name. HTTP 403 alone does not tell an agent whether it should obtain approval, select a different target, or stop. A stable needs_approval or forbidden_scope code does. Keep detailed diagnostics in protected logs if needed, but give the model a concise category it can follow safely.
Copy this failure-receipt contract
Use this as the result schema for every tool wrapper. It works whether a provider expects JSON in a function-call output or a tool-result block with an error flag.
TOOL FAILURE RECEIPT
{
"ok": false,
"operation": "[stable tool/action name]",
"call_id": "[original tool-call ID]",
"error_code": "invalid_input | needs_approval | not_found | conflict |
rate_limited | timeout | unavailable | forbidden_scope",
"retryable": true | false,
"retry_after_seconds": 0 | null,
"safe_detail": "[short user-safe explanation; no secrets or raw stack trace]",
"field_errors": [
{"field":"[name]", "rule":"[required/format/allowed value]"}
],
"current_state_ref": "[record/version/request ID or null]",
"next_safe_action": "repair_input | request_approval | reread_state |
retry_once | choose_allowed_operation | stop_and_escalate"
}
WRAPPER RULES
- Preserve call_id and an operation/request ID on every outcome.
- Validate arguments and authorization before the external write.
- Return only allowlisted, non-sensitive detail to the model.
- Mark retryable only when repeating the same operation is safe.
- Never turn a timeout into a claimed success.
Suppose an agent tries to update an account with an obsolete version number. A useful receipt says conflict, includes the current record reference, and sets next_safe_action to reread_state. That directs the next step toward a fresh read, not a blind retry. By contrast, a malformed postal code can return invalid_input with a field error and invite a clarification. The same tool wrapper can keep a database exception private while giving the agent enough information to recover honestly.
Make retries narrow and bounded
A receipt should never create an endless repair loop. For a transient failure, record the attempt count and let the executor—not the model—enforce a limit, backoff, and deadline. Reuse the original operation ID for a retry of the same intended write. If arguments, recipient, amount, or target change, treat that as a new proposed action rather than silently reusing a prior attempt.
OpenAI notes that a response may contain multiple function calls, so wrappers should associate each result with its own call ID instead of assuming a single sequential action.[1] Anthropic requires tool-result blocks to immediately follow the corresponding tool-use blocks in the conversation history, which is another reason to keep each result self-contained and unambiguous.[2] A receipt with a stable ID, code, and next action is easier to route correctly when several calls finish at once.
Failure mode: leaking the debugging console
More error detail is not always more useful. Raw stack traces can expose service names, paths, account data, tokens, or implementation clues; passing them into a model also creates noisy, untrusted context. The opposite extreme—hiding every detail—forces the agent to guess.
Keep two layers. Store full diagnostics in restricted observability systems keyed by a request or call ID. Return the model only an allowlisted code, a short safe explanation, and the next permitted move. Test the wrapper with invalid input, expired approval, conflict, timeout, and forbidden-scope fixtures. For each case, verify that the model could take the safe next action without being given a credential, an internal stack trace, or permission to broaden scope.
The habit to keep
a failed tool call is not a dead end or a prompt for improvisation. Return a small failure receipt that says what happened, what is safe next, and whether repeating the same operation is allowed.