An AI agent asks a tool to create a DNS record. The provider accepts the change, but the HTTP response disappears before the worker receives it. Ten seconds later, the orchestration layer sees a timeout. That timeout proves only that the caller lacks a result. It does not prove the action failed.
Repeating the call may create a second ticket, send another message, trigger another deployment, or mutate a record that already changed. Voxfor’s guide to hop-relative nginx timeout evidence explains the same boundary at a proxy: the status recorded by one hop describes what that hop observed, not necessarily what every downstream system did.
Safe agent retries therefore need more than a prompt such as “try again if the tool fails.” The application must give each intended business action a stable identity, persist its state outside model context, bind approval to exact arguments, and reconcile an unknown result before another mutation. The goal is not a vague exactly-once promise. The goal is to prevent, detect, or reconcile repeated effects.
Model calls, tool calls, and business effects are different events. A model may select create_ticket; an executor may dispatch the request; the target may commit the ticket; and the response may fail on the return path. Collapsing those events into one success boolean hides the dangerous middle.
HTTP itself does not make every operation safe to repeat. RFC 9110 defines idempotency by the intended server effect and warns against automatically retrying non-idempotent requests unless the client knows their semantics are idempotent or can detect that the original request was not applied. An API’s POST endpoint can still provide idempotency through a documented key, but the client must use that contract correctly.
Separate two retry layers before changing policy:
OpenAI’s current Workspace Agent trigger documentation makes that distinction concrete. conversation_key continues a conversation, while Idempotency-Key safely retries the same trigger event. Neither value automatically deduplicates a separate external API call later in the agent run.
Start with an application-owned action_id, not a model-generated explanation. Derive it from the upstream event and intended effect, or create it once when the application accepts the request. Reusing a key is correct only for another attempt at the same logical action with the same protected arguments.
A minimal durable receipt can look like this:
{
"action_id": "act_01K2DNS7T6M3",
"business_key": "dns:example.com:A:203.0.113.42",
"effect": "upsert_dns_record",
"arguments_sha256": "sha256:REPLACE_WITH_REAL_DIGEST",
"approval_id": "apr_01K2DNS8B1Q9",
"policy_version": "dns-change-v3",
"attempt": 1,
"state": "prepared"
}
business_key should identify what the target considers the same object or operation. For email it might be a campaign recipient plus template version; for a deployment it might be service plus release digest; for DNS it may be zone, owner name, record type, and desired value. A random retry UUID defeats deduplication because every attempt appears new.
The argument digest protects approval integrity. If a resumed agent changes the hostname, recipient, amount, environment, or command, the prior approval no longer covers the action. Request new approval instead of carrying permission forward because the tool name stayed the same.
Keep credentials and authorization outside the receipt. Voxfor’s AI agent workspace permissions remain the separate least-privilege boundary: a stable action ID prevents duplicate intent; it does not justify broader file, shell, network, or account access.
Store the receipt in a durable database or workflow store shared by every worker able to execute the action. Chat history, a process-local dictionary, or a queue-delivery counter is not enough when another replica can take over after a crash.
Write state before crossing each irreversible boundary. Persist approved after approval verification. Persist sent in the same controlled handoff that authorizes dispatch, then store the target’s resource ID, request ID, or authoritative result as confirmed. Where the target supports an idempotency key, send the stable action_id or a documented derivative on every retry of that same action.
LangGraph persistence stores graph state as checkpoints, while its interrupt guidance notes that a resumed node restarts from the beginning and side effects before an interrupt should be idempotent. That is why approval UI and execution state must refer to one receipt instead of relying on where the model appears to pause.
Transport sessions solve another problem. Voxfor’s analysis of explicit MCP state handles shows how useful state can survive replica changes. An action receipt applies the same ownership principle to a mutation, but its identity must remain stable even when the model conversation or transport session changes.
Do not feed every exception into one exponential-backoff loop. Classify the receipt from evidence that exists outside the model’s prose.
| Observed outcome | What it proves | Safe next move |
|---|---|---|
| Never dispatched | No request crossed the executor boundary | Retry with the same action ID after the cause is corrected |
| Rejected before effect | Target returned an authoritative non-application result | Fix input or authorization; retry only under the same logical intent |
| Confirmed effect | Target resource ID, version, or idempotent replay receipt exists | Return the stored result; do not mutate again |
| Outcome unknown | Dispatch occurred but authoritative result is absent | Query by business key or idempotency key; reconcile before any retry |
Queue acknowledgment cannot replace this table. Voxfor’s JetStream redelivery guide separates pending work from delivered-but-unacknowledged work, yet even a correct redelivery policy still needs an idempotent business handler. A worker may complete the external effect and crash before acknowledging the message.
Temporal makes the same risk explicit: Activities may retry and should be idempotent, while its Python error-handling guidance warns that losing a permanent failure’s non-retryable classification can make the framework try again. Backoff reduces pressure; it does not change whether repetition is safe.
Prompts can help a model explain an error, but application code must own retry eligibility. The executor should accept only declared transitions, reject changed argument hashes, and return a stored result when the same confirmed action arrives again.
retry_policy:
never_dispatched: automatic_same_action_id
rejected_transient: bounded_same_action_id
rejected_permanent: stop_for_new_input_or_approval
confirmed: return_stored_receipt
outcome_unknown: reconcile_then_decide
Bound attempts by error class and time budget. Rate limits, connection refusal before dispatch, and target maintenance may be transient. Invalid arguments, revoked authorization, policy denial, and unsupported operations do not become safer because time passed. Preserve the original error class instead of wrapping every failure in ToolError.
OpenAI’s Programmatic Tool Calling guidance recommends direct calls for writes or approval-sensitive actions and explicitly separates bounded orchestration from side-effecting work that must not be repeated. The practical lesson is broader than one SDK: keep write authorization, attempt count, and terminal-state rules in deterministic application logic.
Reconciliation asks the target what exists now. Use an idempotency-key lookup when the API exposes one; otherwise query by stable business key and compare the observed resource with the approved arguments. A DNS worker can read the exact record set, a ticket worker can search an external reference field, and a deployment worker can compare release digest plus environment.
Follow a fail-closed order:
confirmed.manual_reconciliation; do not ask the model to guess.Evidence can also disappear. When an audit pipeline loses records, Voxfor’s Linux Audit evidence boundary requires operators to state honestly what can no longer be proven. Apply the same rule here: missing receipts do not become proof of failure. Escalate uncertainty rather than reconstructing a convenient success story from chat text.
Retention must exceed every realistic replay path: producer retries, queue redelivery, workflow resume, operator requeue, and the target’s idempotency window. Archive or compact receipts only after their business effects have an independent durable identifier and the retry path can no longer reach them.
Self-managed teams may host the executor, database, and workflow engine themselves; another team may operate some or all of that stack. The business-effect contract remains with the workflow owner.
The contract stays the same when a team uses autonomous AI workers on VPS or dedicated servers: the workflow owner still defines the business key, approval scope, replay rule, and reconciliation owner. Delegating operation changes who implements the controls, not who defines the meaning of a duplicate business effect.
Handoffs should name who can approve, who may release an unknown receipt, which target query is authoritative, how long receipts remain available, and what evidence closes the action. A managed operator can implement and monitor the mechanism, but cannot invent the client’s definition of “same order,” “same customer message,” or “same production change.”
Only under a proven retry contract. A failed or missing response does not prove the external effect failed. Retry automatically when durable state proves non-dispatch or the target guarantees that the same stable idempotency key returns the original outcome.
Conversation continuity and action identity solve different problems. A conversation ID groups model context; an idempotency key identifies one logical action. One conversation can contain many actions, and the same action may resume in another conversation or worker.
Store action receipts in a durable database or workflow store shared by every executor replica. Process memory and chat transcripts do not provide reliable cross-worker exclusion or retention.
Use a stable business key and reconcile through an authoritative read before writing again. If the target cannot identify the prior effect reliably, require manual resolution rather than automatic retry.
Approval must bind to normalized arguments, their digest, the effect, and policy version. Any material argument change creates a new approval decision even when the tool name is unchanged.
Keep receipts longer than every producer-retry, queue-redelivery, workflow-resume, operator-requeue, and target-deduplication window. Shorter retention can let an old event return after its duplicate evidence disappeared.
Test the contract on a harmless isolated target. Execute one effect with a stable business key, deliberately drop the success response after the target commits, and let the normal retry path wake up. The second worker should find outcome_unknown, reconcile the existing target object, and return the first result without creating another effect.
Record the trigger key, action ID, approved argument hash, state timestamps, target resource ID, reconciliation query, and final count of external effects. Then repeat the drill with a changed argument and prove prior approval is rejected.
An AI retry is safe when the application can explain why another attempt cannot create another business effect. A confident model response is not that explanation; a durable receipt and authoritative reconciliation are.