Agent Tool Calls: Idempotent Retries
When an agent calls a write tool such as create-note, a timeout means the result did not arrive in time. It does not establish that the server did nothing. Reliable retries preserve one business operation key while the server checks parameters, records outcomes and prevents duplicate effects. Asking the model not to repeat itself is insufficient.
Contents
- The call most likely to duplicate may already have succeeded
- Idempotent does not mean identical response text
- Equal content can represent different intentions
- Commit the business effect and record together
- Concurrency and restart need separate design
- A remote effect is outside that transaction
- Key lifetime belongs in the contract
- Exercise the half of failure the caller cannot see
- Sources
The call most likely to duplicate may already have succeeded
Imagine a local demonstration: an assistant creates a note, the tool service inserts it, and the response connection breaks. The assistant sees a timeout and creates again. If the server treats that as a new operation, two notes appear. The problem is disagreement about completion between caller and executor, not language understanding.
Unknown outcome deserves its own state. Do not immediately turn timeout into failure and then generate a new operation. Preserve the original identity and query or replay its outcome. Distinguish confirmed nonexecution from possible execution rather than hiding both behind a generic error.
The example uses reversible notes without external effects. Idempotency does not replace authorization or parameter validation. The tool must still establish whether the authenticated caller may act. Deduplication handles repeated delivery of the same intent; it does not authorize that intent.
Idempotent does not mean identical response text
RFC 9110 section 9.2.2 defines idempotency in terms of the intended server effect of repeated identical requests compared with one request. Responses can differ, and additional logging can occur. Merely naming an HTTP method does not supply business deduplication.
A create-resource POST is safely replayable only if the service’s contract makes it so. A network error alone is not permission for automatic retries. Equally, a supposedly idempotent endpoint that appends a new business record on each call cannot be repaired unilaterally by the client.
Agent tools should define operation identity, outcome lookup, parameter conflicts and unknown-state handling. A model-generated tool-call identifier may not survive retries or workflow restoration. The execution layer should retain a stable identity instead of relying on the model to reproduce the same text.
Equal content can represent different intentions
A user may deliberately want two notes with the same title. Deduplicating only a hash of title incorrectly suppresses the second request. Identical parameters do not establish identical intent. A new user action generally receives a new operation key; a transport retry for the same action keeps the existing one.
Conversely, reusing a key with a different title must not silently return the old response as if the new title were stored. Detect mismatched parameters and report a conflict. The AWS Builders’ Library article discusses client identifiers, parameter matching and atomic recording. The teaching example applies those principles locally.
Keys also need scope. Different tenants using the same string must not obtain one another’s results; unrelated tool operations must not collide accidentally. The example makes tenant and key unique together and includes an operation version in the fingerprint. Real tenant identity must come from authentication, not a self-declared request string.
Commit the business effect and record together
Inserting a note, committing it, and only then recording deduplication leaves a crash window: the effect exists without its operation record. Marking completion before creating the note permits the reverse inconsistency. When both records fit in one database, commit them in one transaction.
SQLite’s atomic-commit documentation describes its transaction mechanism. This example uses one connection with sequential calls and keeps notes and completed responses in a local in-memory database. It implements no distributed lock, concurrent-conflict recovery, expiry cleanup or network transport.
import hashlib
import json
import sqlite3
db = sqlite3.connect(":memory:")
db.executescript("""
CREATE TABLE notes (
id INTEGER PRIMARY KEY, tenant TEXT NOT NULL, title TEXT NOT NULL
);
CREATE TABLE operations (
tenant TEXT NOT NULL, op_key TEXT NOT NULL,
fingerprint TEXT NOT NULL, response TEXT NOT NULL,
PRIMARY KEY (tenant, op_key)
);
""")
def create_note(tenant, op_key, title, fail_before_record=False):
payload = json.dumps({"action": "create-note-v1", "title": title},
sort_keys=True, separators=(",", ":"))
fingerprint = hashlib.sha256(payload.encode()).hexdigest()
with db:
previous = db.execute(
"SELECT fingerprint, response FROM operations "
"WHERE tenant=? AND op_key=?", (tenant, op_key)
).fetchone()
if previous:
if previous[0] != fingerprint:
raise ValueError("Key reused with different parameters")
return json.loads(previous[1])
cursor = db.execute("INSERT INTO notes(tenant,title) VALUES (?,?)",
(tenant, title))
result = {"id": cursor.lastrowid, "title": title}
if fail_before_record:
raise RuntimeError("Injected failure before operation record")
db.execute("INSERT INTO operations VALUES (?,?,?,?)",
(tenant, op_key, fingerprint, json.dumps(result)))
return result
# Local simulation: commit succeeded, but the caller lost the response.
first = create_note("demo-a", "operation-1", "Example note")
retry = create_note("demo-a", "operation-1", "Example note")
assert first == retry
assert db.execute("SELECT count(*) FROM notes").fetchone()[0] == 1
try:
create_note("demo-a", "operation-1", "Changed title")
except ValueError:
pass
else:
raise AssertionError("Conflicting parameters accepted")
try:
create_note("demo-a", "operation-fail", "Rollback", True)
except RuntimeError:
pass
assert db.execute("SELECT count(*) FROM notes").fetchone()[0] == 1
assert db.execute("SELECT count(*) FROM operations").fetchone()[0] == 1
create_note("demo-a", "operation-2", "Example note")
create_note("demo-b", "operation-1", "Example note")
assert db.execute("SELECT count(*) FROM notes").fetchone()[0] == 3
assert db.execute("SELECT count(*) FROM operations").fetchone()[0] == 3
print("Replay kept one note; conflict rejected; rollback passed; total notes: 3")
db.close()
The original Python example was executed. Replaying the first key leaves one note; changed parameters are rejected. An exception injected after the effect but before its operation record rolls both tables back. A new key allows the same title, and a second fictional tenant can reuse the first key. The final count is three.
These checks cover a local transaction boundary, not a proof that every service executes once forever. A fingerprint is not an authorization secret. All example parameters are ordinary strings. Production normalization must define numbers, defaults and version semantics rather than changing serialization unexpectedly and creating false conflicts.
Concurrency and restart need separate design
Identical operations can arrive concurrently rather than sequentially as in the example. Two callers can both observe that no record exists. That lookup alone does not guarantee deduplication. A uniqueness constraint, transaction or explicit state competition must decide who owns execution, with a defined outcome for the other caller: wait, query status, or receive a retryable in-progress response.
In progress must not masquerade as completed. Returning a placeholder as success can send a caller onward with a resource that does not exist. Conversely, deleting the placeholder merely because waiting took too long can overlap a fresh execution with old work still running. State transitions and recovery need explicit rules; timeout should not silently decide business facts.
The in-memory database disappears when this program ends. It demonstrates atomicity, not restart recovery. A deployed service needs durable records and tests covering backup, recovery and retention behavior. Restored workflows must also recover their original operation keys, rather than restoring prompt history alone and letting the model plan a duplicate action.
A remote effect is outside that transaction
If note creation also asks another service to process an attachment, rolling back local data cannot undo completed remote work. Calling the service inside the database transaction does not merge the network into an atomic commit domain; it can instead leave locks held while waiting.
A common direction is to store the business record and a pending event locally, then let a separate sender deliver it. The recipient still needs deduplication or an idempotent contract, and replay still needs stable event identity. This narrows inconsistency windows but does not establish end-to-end exactly-once merely because a queue exists.
For uncertain writes, prefer outcome lookup using stable identity. Replay only under a supported idempotency contract. If a side effect has neither lookup nor safe replay, preserve uncertainty for business handling rather than letting an agent guess and continue dependent actions.
Key lifetime belongs in the contract
Deduplication records are not a free permanent cache. Define retention, cleanup and late arrivals in relation to the longest workflow-recovery window. Once a record is removed, a late retry may reach a service that no longer has evidence identifying the earlier operation.
Retain operation key, parameter version, completion state and a recoverable result reference, with explicit post-expiry behavior. Private results still require authorization on lookup or replay. Knowing a key must not grant access. Logs need controlled correlation data and error categories, not copies of every sensitive input.
Idempotency also does not justify unlimited retrying. Unbounded attempts add load and delay. Attempt limits, a total deadline and backoff are separate resource controls. Establish whether replay is safe before deciding whether another attempt is worthwhile.
Exercise the half of failure the caller cannot see
Cover failure before commit, a lost post-commit response, identical replay, changed parameters with the same key, a new key with identical content, the same key in different scopes, and replay after recovery. Concurrent deployments also need tests of uniqueness, isolation and conflict handling. Sequential success here is not a substitute.
Separate request counts, business-effect counts and replayed-result counts in observations. More tool calls may reflect transport retries rather than more user-created content. Retain unknown outcomes in evaluation too, as with the failure denominator in confidence-interval evaluation, rather than deleting inconvenient records.
A useful acceptance question is whether one business action reaches an explainable final state after interruption, restart and replay. That is what lets an agent recover on an uncertain network instead of gambling on repeated execution.
Sources
- RFC 9110 — Idempotent Methods
- AWS Builders’ Library — Making retries safe with idempotent APIs
- SQLite — Atomic Commit
Backfilled on the evening of October 14, 2026; the article retains its planned 14:00 Beijing date. Examples are local teaching code, not production-service test results.
Support
If the explanations or examples helped you solve a problem or save time, you’re welcome to support this site with a voluntary tip.
Click a QR code to enlarge. More options: support page。


