Metadata-Version: 2.4
Name: ledger-once
Version: 0.1.0
Summary: Exactly-once execution for AI agent tool calls
Author: Henry Miller
License: MIT License
        
        Copyright (c) 2026 Henry
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Keywords: ai,agents,idempotency,llm,tools
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# Ledger

My AI agent charged a customer 5 times.

retry storm → duplicate Stripe charges

![ledger demo](docs/demo.gif)

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

```python
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

```python
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

```bash
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:

```bash
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

```bash
pip install ledger-once
```

Or copy [`ledger.py`](ledger.py) into your project — one file, zero dependencies.

**Optional** — clone and run locally:

```bash
git clone https://github.com/yourusername/ledger
cd ledger
pip install -e .
```

---

## Fix it with one line

```python
# 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

```python
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

```python
# 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

```python
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

```python
# 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

```python
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

```bash
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.**

```bash
pip install ledger-once
python demos/demo_stripe_charge.py
```
