In an agent, tools get retried. A lot. Reasons:
If the tool has side effects (creates a record, charges money, sends a mail), retrying without protection means: two records, double charge, two mails. For the user, that's a production bug that costs trust and money.
The solution is idempotency: design the tool so that calling it N times with the same parameters is equivalent to calling it once.
Naturally idempotent. Some operations already are. set_status("active") called 5 times leaves the same state. No extra work needed.
By idempotency_key. The client (or the agent) generates a unique key per intent and passes it. The server remembers that key and the result. If the key repeats, it returns the previous result without re-executing. This is the canonical pattern for creating things (payments, orders, records).
By state check. Before acting, the handler checks if the effect already happened. assign_shift can check whether the crewmate is already on that shift and, if so, return { ok: true, already_assigned: true }.
idempotency_key contractWhen you support idempotency_key:
req-{uuid} or similar.replayed flag or equivalent. The agent needs to know whether the operation created something new or returned an existing result.async function handle({ ..., idempotency_key }) {
if (idempotency_key) {
const prev = await db.payments.findByIdempotencyKey(idempotency_key);
if (prev) {
return { ok: true, payment_id: prev.payment_id, replayed: true };
}
}
const payment = await db.payments.create({ ..., idempotency_key });
return { ok: true, payment_id: payment.payment_id, replayed: false };
}On the right you have register_payment. The starter charges every time it's called. Make it idempotent.
In agents that touch money, external state, or humans, idempotency is not optional. It's the difference between a system you can operate and a system you support every week because "it duplicated".