Skip to main content

Exactly-once execution for AI agent tool calls

Project description

Ledger

My AI agent charged a customer 5 times.

retry storm → duplicate Stripe charges

ledger demo

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

ledger_once-0.1.0.tar.gz (16.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ledger_once-0.1.0-py3-none-any.whl (14.5 kB view details)

Uploaded Python 3

File details

Details for the file ledger_once-0.1.0.tar.gz.

File metadata

  • Download URL: ledger_once-0.1.0.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

Hashes for ledger_once-0.1.0.tar.gz
Algorithm Hash digest
SHA256 5c73736d9842cd4d0723c11f96f9fa2340ceaf60eb02fc5d08dd7bf3d4d5c1f2
MD5 b5e1837f8ad294f4ddb2ac2978bdc4f9
BLAKE2b-256 1eced28134f0a4c93343ff201a4d5af5ed5d38b9881c675c03621a04f5db3a4c

See more details on using hashes here.

File details

Details for the file ledger_once-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: ledger_once-0.1.0-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

Hashes for ledger_once-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e02608c0732b9d2c0128f5732af88b77a2e91523ee505667f66c47d3ac2bd7c8
MD5 527ea9d1a89f2f2c4db86f85c87a49ab
BLAKE2b-256 1affb81c8b8f89443110f97a61eff257ab1ab9b081d934e1e0543de8faf0e189

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page