Darkmind / Issue 016

Return a Failure Receipt, Not an Empty Tool Result

Make every failed tool call return a small, typed receipt that tells an agent what is safe to retry, repair, ask for, or stop doing.

Oct 23, 20264 min readAgent reliability

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:

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.

Sources

  1. OpenAI, “Function calling”
  2. Anthropic, “Handle tool calls”