Exactly-once execution for AI agent tool calls
Project description
Ledger
My AI agent charged a customer 5 times.
retry storm → duplicate Stripe charges
Exactly-once execution for AI agent tool calls.
AI agents retry tools constantly.
timeouts
parallel workers
LLM loops
Sometimes those retries hit real side effects:
- duplicate Stripe charges
- duplicate refunds
- duplicate emails
- duplicate database writes
Ledger guarantees a tool executes once, even if the agent calls it 10 times.
agent
↓
ledger guard
↓
tool
Stripe added idempotency keys for APIs. Ledger brings the same guarantee to AI agents.
Quickstart
from ledger import guard
def charge_card(customer_id, amount):
print(f"charging {customer_id} ${amount}")
guard(charge_card, "cus_42", 49) # runs
guard(charge_card, "cus_42", 49) # blocked
guard(charge_card, "cus_42", 49) # blocked
guard.log()
# ✓ charge_card attempts 3 executed 1 blocked 2 ← retried 2×
The fix
from ledger import guard
guard(charge_card, customer_id="cus_42", amount=49) # runs
guard(charge_card, customer_id="cus_42", amount=49) # blocked
guard(charge_card, customer_id="cus_42", amount=49) # blocked
guard.log()
# ✓ charge_card attempts 3 executed 1 blocked 2 ← retried 2×
Without Ledger With Ledger
agent agent
↓ ↓
charge_card() ← executes guard()
↓ ↓
charge_card() ← executes charge_card() ← executes once
↓ ↓
charge_card() ← executes blocked
↓ ↓
charge_card() ← executes blocked
customer charged $245 customer charged $49
See the failure in 10 seconds
git clone https://github.com/yourusername/ledger
cd ledger
pip install -e .
python demos/demo_stripe_charge.py
💳 POST /v1/charges customer=cus_42 amount=$49 (total so far: $49)
💳 POST /v1/charges customer=cus_42 amount=$49 (total so far: $98)
💳 POST /v1/charges customer=cus_42 amount=$49 (total so far: $147)
💳 POST /v1/charges customer=cus_42 amount=$49 (total so far: $245)
💳 POST /v1/charges customer=cus_42 amount=$49 (total so far: $245)
❌ Customer charged $245 (should be $49)
With Ledger — same agent, same retries:
💳 POST /v1/charges customer=cus_42 amount=$49 (total so far: $49)
✅ Customer charged $49 (correct)
✓ stripe_charge attempts 5 executed 1 blocked 4 ← retried 4×
Try the other failure modes:
python demos/demo_concurrent.py # 3 workers race to charge the same customer
python demos/demo_agent_loop.py # runaway agent fires the tool repeatedly
Install
pip install ledger-once
Or copy ledger.py into your project — one file, zero dependencies.
Optional — clone and run locally:
git clone https://github.com/yourusername/ledger
cd ledger
pip install -e .
Fix it with one line
# Before — retries cause duplicate charges
charge_card(customer_id="cus_42", amount=49)
# After — retries are safe
guard(charge_card, customer_id="cus_42", amount=49)
Same agent. Same retries. No duplicate side effects.
Why this happens
LLM agents retry tool calls automatically.
If a tool times out, the agent can't tell whether it executed — so it retries.
If that tool has side effects (charges, refunds, emails), every retry executes again.
This is a classic distributed systems problem. Ledger restores exactly-once execution.
Real failures this prevents
- Duplicate Stripe charges — customer billed twice for one order
- Duplicate refunds — $500 refund becomes $1,500
- Duplicate emails — welcome email sent 5 times
- Duplicate database writes — record created multiple times
If your agent calls external APIs, it has this bug. You just haven't seen it yet.
The moment you see how bad it was
guard.log()
✓ charge_card attempts 5 executed 1 blocked 4 ← retried 4×
✓ send_email attempts 6 executed 1 blocked 5 ← retried 5×
✓ refund_order attempts 3 executed 1 blocked 2 ← retried 2×
Most teams have never seen these numbers. They're always higher than expected.
Async, decorators, any framework
# Async — same syntax, just await
result = await guard(post_webhook, url="https://...", payload=data)
# Decorator — protect every call at the source
@guard.once
def charge_card(card_id: str, amount: float):
return stripe.charge(card_id, amount)
# Drop into any agent loop — OpenAI, LangChain, AutoGen, custom
for tool_call in response.tool_calls:
result = guard(tools[tool_call.name], **tool_call.arguments)
Per-tool rules
guard.policy("search_web", unlimited=True) # reads: always run
guard.policy("charge_card", once=True, replay=True) # writes: once, return cached result on retry
guard.policy("send_sms", max=2) # cap at 2
guard.policy("daily_report", ttl=86400) # once per day
Custom idempotency key
# Like Stripe's Idempotency-Key header — caller controls identity
guard(charge_card, amount=49, key=f"order-{order_id}")
guard(charge_card, amount=49, key=f"order-{order_id}") # blocked — same key
Survives restarts, crashes, and parallel workers
guard.persist("ledger.db") # one line at startup
SQLite-backed. Atomic claims via INSERT OR IGNORE. If your process crashes mid-execution, Ledger detects the stale record and allows a safe retry.
CLI
ledger show ledger.db # print full history
ledger tail ledger.db # live-tail as your agent runs
ledger stats ledger.db # summary + duplicate rate
ledger clear ledger.db # wipe records
Production checklist
| Concern | How Ledger handles it |
|---|---|
| Agent retries after timeout | fingerprint + block |
| Process crashes mid-execution | stale RUNNING detection → safe retry |
| Two workers run in parallel | atomic INSERT OR IGNORE claim |
| Args passed positionally vs keyword | normalized to same fingerprint |
| Float args drift from JSON parsing | rounded to 8 decimal places |
| Need result back on retry | guard.policy("tool", replay=True) |
| Survive restart | guard.persist("ledger.db") |
| Multi-node / Redis | implement the 4-method Store protocol |
Repo structure
ledger/
├─ ledger.py ← the whole library, one file
├─ README.md
├─ LICENSE
├─ pyproject.toml
├─ .gitignore
├─ docs/
│ └─ demo.gif
└─ demos/
├─ demo_stripe_charge.py ← retry storm
├─ demo_concurrent.py ← parallel workers
└─ demo_agent_loop.py ← runaway agent
Design principles
- one file
- zero dependencies
- works with any agent framework
- safe across retries, crashes, and parallel workers
Roadmap
Ledger currently guarantees exactly-once execution.
Future layers:
- workflow budgets
- agent kill switches
- execution policies
- full action audit logs
One word change.
Exactly-once execution for AI agents.
pip install ledger-once
python demos/demo_stripe_charge.py
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ledger_once-0.1.1.tar.gz.
File metadata
- Download URL: ledger_once-0.1.1.tar.gz
- Upload date:
- Size: 16.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dde7aa5f384285d9a27b22c00be2282caa50d3feea4d7db55b273267dd55bf19
|
|
| MD5 |
4fcb8e154e293226cb5939f6e2152e12
|
|
| BLAKE2b-256 |
9fb30218ab8e1892b21ce1ed388b1f3898118a4b3c4ef30877b0c058873b3a92
|
File details
Details for the file ledger_once-0.1.1-py3-none-any.whl.
File metadata
- Download URL: ledger_once-0.1.1-py3-none-any.whl
- Upload date:
- Size: 14.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dfe4680490a1d869c1eb8f0904fd1b48a66c7dec74245fd18010f39c4030d44f
|
|
| MD5 |
0fd2847b64cd4853944dab7d90bae5b0
|
|
| BLAKE2b-256 |
79e4f6767bb7ba9dca26e9d702f4619c9333e8b70c312a08ac75166830359c85
|