Skip to content
Structured Output & Tool CallingTool design — semantics

Senior · Core path · 7 chapters

Tool design — semantics

Idempotency and safe-retry for a non-deterministic caller; error messages written to be *actionable by a model*

If these contracts are vague, transient failures become duplicate side effects and recoverable input mistakes become operator incidents.

Read these first

What you will know

  • Make a tool safe to retry when the model calls it twice

    A request ID or idempotency key must survive timeout retries while side effects are committed at most once.

  • Write an error message that tells the model what to do differently

    The response should identify the violated precondition and expose permitted values or the correct next call shape, rather than merely report failure.

  • Replace a stack trace with a model-actionable tool error and verify the resulting retry path

    Raw frames consume context and often reveal server internals, while the model needs a stable error code plus corrected input or sequence guidance.

  • Design a destructive tool so a mistaken call is recoverable

    Soft deletion, preview, and explicit confirmation preserve an undo path when the model infers the wrong target or parameters.

  • Show a model recovering from a tool error without human help

    The test should include a plausible malformed call, a bounded retry, and a successful terminal result without hidden application code.


Chapters

  1. 1

    Idempotency and actionable errors

    A call that times out after the payment has committed and a call refused in 40 ms because the amount exceeds an account limit sit at opposite corners of one grid: did the attempt leave state behind, and could an identical repeat ever succeed. Only the first is an idempotency problem and only the second is an error-message problem, and the tempting single fix — make everything idempotent and let the agent retry freely — turns the second into a burned turn budget and a false report that the system is down.

  2. 2

    Deriving an idempotency key

    A key generated fresh on every retry protects nothing, because the retry is exactly where it needs to match. Derive it from the intent — the charge id, the amount in minor units, the currency — record it against the committed refund inside the same transaction, and return the original result when it reappears. The key is computed application-side from a chosen stable subset of the model's arguments, never asked of the model, and choosing that subset decides which two legitimately distinct requests can never both succeed.

  3. 3

    Model-actionable error responses

    Compare a bare failure string with a response carrying error invalid_status, field status, message 'must be one of: open, pending, closed', received 'OPEN'. The second names the violated precondition, the offending value and the permitted set, so the next call is a correction rather than a guess. What each field is for, what to do when the permitted set has 249 members, why every independent violation should arrive in one response, and why 'it eventually recovered' is not evidence that the bare version worked.

  4. 4

    Retryable and terminal error classes

    An agent retries book_appointment four times on a slot taken two seconds earlier, then reports an outage; another retries a transfer rejected for insufficient funds with the balance right there in the response. Neither error said that repetition was futile for these arguments or what to send instead, so retrying was the only move left. Three classes — transient, invalid_request, terminal — plus retry_after and adjacent alternatives, and why a machine-readable field beats an HTTP status code or a lookup table in the harness.

  5. 5

    Stack traces in a tool result

    Returning frames to a model is worse than merely expensive: they publish deployment paths, SQL and library versions to a component that paraphrases things for users, and the text changes whenever a dependency is upgraded, so there is no stable code to alert on or branch on. Catching the exception and returning str(exc) fixes tokens and most of the leakage while leaving recovery untouched — the mapped error object is what changes the model's next move.

  6. 6

    Recoverability on a destructive tool

    A confirm: true argument is the same model confirming its own belief in the same generation, and restating the target's name makes the error more confident rather than less. Soft deletion with a retention window, a result naming what was actually affected, and the restoring call returned alongside it give a real undo path — then the harder case, send_customer_email, where nothing can be un-sent and the window has to be manufactured.

  7. 7

    Testing unattended recovery

    Drive a realistic malformed call end to end and assert a correct terminal result within a bounded number of attempts, with no hidden application code doing the repair. Whether four attempts is a pass depends on what an attempt costs in tokens, seconds and commit opportunities. This is also the instrument that proves an error-path rewrite worked, by comparing the distribution of attempts to recovery before and after — and the thing to do when a new model version needs nine.

External resources