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.1.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.1-py3-none-any.whl (14.5 kB view details)

Uploaded Python 3

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

Hashes for ledger_once-0.1.1.tar.gz
Algorithm Hash digest
SHA256 dde7aa5f384285d9a27b22c00be2282caa50d3feea4d7db55b273267dd55bf19
MD5 4fcb8e154e293226cb5939f6e2152e12
BLAKE2b-256 9fb30218ab8e1892b21ce1ed388b1f3898118a4b3c4ef30877b0c058873b3a92

See more details on using hashes here.

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

Hashes for ledger_once-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 dfe4680490a1d869c1eb8f0904fd1b48a66c7dec74245fd18010f39c4030d44f
MD5 0fd2847b64cd4853944dab7d90bae5b0
BLAKE2b-256 79e4f6767bb7ba9dca26e9d702f4619c9333e8b70c312a08ac75166830359c85

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