|
1 | | -# Execution Guard — Exactly-Once Execution for AI Agents |
| 1 | +# SafeAgent / Execution Guard |
2 | 2 |
|
3 | | -A missing layer for safe side effects in agent systems. |
| 3 | +## Prevent duplicate or incorrect execution when retries happen |
4 | 4 |
|
5 | | -Retries can duplicate irreversible actions: |
6 | | -payments, emails, trades, and external API mutations. |
| 5 | +A missing execution boundary for AI agents, trading bots, automations, and workflow systems. |
7 | 6 |
|
8 | | -Execution Guard ensures a side effect runs exactly once — even under retries. |
| 7 | +When a system hits: |
| 8 | +- timeout |
| 9 | +- partial failure |
| 10 | +- retry |
| 11 | +- uncertain completion |
9 | 12 |
|
10 | | -> Even if your system runs it twice, it executes once. |
| 13 | +…it often does not know whether the action already happened. |
11 | 14 |
|
12 | | ---- |
13 | | - |
14 | | -## Quickstart (runs in ~10 seconds) |
15 | | - |
16 | | -Run the same request twice — it executes once. |
17 | | - |
18 | | -Install: |
19 | | - |
20 | | -```bash |
21 | | -pip install safeagent-exec-guard |
22 | | -``` |
23 | | - |
24 | | -Minimal local example (SQLite): |
25 | | - |
26 | | -```python |
27 | | -from safeagent_exec_guard.sqlite_store import SQLiteExecutionStore |
28 | | - |
29 | | -store = SQLiteExecutionStore("safeagent.db") |
30 | | -store.init_db() |
31 | | - |
32 | | -def send_payment(request_id: str): |
33 | | - action = "send_payment" |
34 | | - |
35 | | - if store.insert_if_not_exists(request_id, action): |
36 | | - print("executing side effect") |
37 | | - result = {"status": "sent", "receipt_id": "rcpt_12345"} |
38 | | - store.complete(request_id, result) |
39 | | - print("result:", result) |
40 | | - else: |
41 | | - print("duplicate blocked") |
| 15 | +That is how you get: |
| 16 | +- duplicate trades |
| 17 | +- duplicate payments |
| 18 | +- duplicate emails |
| 19 | +- duplicate API mutations |
42 | 20 |
|
43 | | -send_payment("req_123") |
44 | | -send_payment("req_123") |
45 | | -``` |
46 | | - |
47 | | -Expected output: |
| 21 | +SafeAgent adds a durable execution boundary around real side effects so retries can be reconciled instead of replayed. |
48 | 22 |
|
49 | | -```text |
50 | | -executing side effect |
51 | | -result: {'status': 'sent', 'receipt_id': 'rcpt_12345'} |
52 | | -duplicate blocked |
53 | | -``` |
| 23 | +> Most systems can retry. Very few can decide when a retry is still correct. |
54 | 24 |
|
55 | 25 | --- |
56 | 26 |
|
57 | | -## Demo |
58 | | - |
59 | | -A retry without protection can execute the same irreversible action twice. |
60 | | - |
61 | | -Execution Guard guarantees the side effect runs once — even if the agent retries. |
62 | | - |
63 | | -### Same request ID. Same retry. Different result. |
| 27 | +## Demo — duplicate trade prevented after uncertain completion |
64 | 28 |
|
65 | | -Without an execution boundary, retries can re-run irreversible side effects. |
66 | | -With Execution Guard, the second attempt returns the original receipt instead of executing again. |
| 29 | + |
67 | 30 |
|
68 | | - |
| 31 | +Without SafeAgent: |
| 32 | +- retry replays the action |
| 33 | +- duplicate trade executes |
69 | 34 |
|
70 | | -SafeAgent is a reference implementation of the Execution Guard pattern. |
| 35 | +With SafeAgent: |
| 36 | +- retry resolves against existing execution |
| 37 | +- duplicate is blocked |
71 | 38 |
|
72 | 39 | --- |
73 | 40 |
|
74 | | -## Install |
| 41 | +## Quickstart |
75 | 42 |
|
76 | | -```bash |
77 | 43 | pip install safeagent-exec-guard |
78 | | -``` |
79 | | - |
80 | | ---- |
81 | | - |
82 | | -## Quick start (SQLite) |
83 | | - |
84 | | -Use SQLite for local development, demos, and single-process workflows. |
85 | 44 |
|
86 | 45 | ```python |
87 | 46 | from safeagent_exec_guard.sqlite_store import SQLiteExecutionStore |
88 | 47 |
|
89 | 48 | store = SQLiteExecutionStore("safeagent.db") |
90 | 49 | store.init_db() |
91 | 50 |
|
92 | | -request_id = "payment_123" |
93 | | -action = "send_payment" |
94 | | - |
95 | | -def do_side_effect(): |
96 | | - print("Executing payment...") |
97 | | - return {"status": "sent", "receipt_id": "rcpt_12345"} |
98 | | - |
99 | | -if store.insert_if_not_exists(request_id, action): |
100 | | - result = do_side_effect() |
101 | | - store.complete(request_id, result) |
102 | | - print("Executed:", result) |
103 | | -else: |
104 | | - print("Duplicate request detected — execution blocked") |
105 | | - |
106 | | -``` |
107 | | -See also: Wrap a side effect |
108 | | - |
109 | | -## Postgres (production) |
110 | | - |
111 | | -Use Postgres for distributed workers, production services, and multi-instance agent systems. |
112 | | - |
113 | | -### Start Postgres with Docker |
114 | | - |
115 | | -```bash |
116 | | -docker run --name safeagent-postgres \ |
117 | | - -e POSTGRES_PASSWORD=postgres \ |
118 | | - -e POSTGRES_USER=postgres \ |
119 | | - -e POSTGRES_DB=postgres \ |
120 | | - -p 5432:5432 \ |
121 | | - -d postgres:16 |
122 | | -``` |
123 | | - |
124 | | -### Example |
125 | | - |
126 | | -```python |
127 | | -import os |
128 | | -from safeagent_exec_guard.postgres_store import PostgresExecutionStore |
129 | | - |
130 | | -dsn = os.getenv( |
131 | | - "POSTGRES_DSN", |
132 | | - "postgresql://postgres:postgres@localhost:5432/postgres" |
133 | | -) |
134 | | - |
135 | | -store = PostgresExecutionStore(dsn) |
136 | | -store.init_db() |
137 | | - |
138 | | -request_id = "payment_123" |
139 | | -action = "send_payment" |
140 | | - |
141 | | -def do_side_effect(): |
142 | | - print("Executing payment...") |
143 | | - return {"status": "sent", "receipt_id": "rcpt_12345"} |
144 | | - |
145 | | -if store.insert_if_not_exists(request_id, action): |
146 | | - result = do_side_effect() |
147 | | - store.complete(request_id, result) |
148 | | - print("Executed:", result) |
149 | | -else: |
150 | | - print("Duplicate request detected — execution blocked") |
151 | | -``` |
152 | | - |
153 | | -### Reset the demo table |
154 | | - |
155 | | -```bash |
156 | | -docker exec -it safeagent-postgres psql -U postgres -d postgres -c "TRUNCATE TABLE execution_requests;" |
157 | | -``` |
158 | | - |
159 | | ---- |
160 | | - |
161 | | -## How it works |
| 51 | +def send_payment(request_id: str): |
| 52 | + action = "send_payment" |
162 | 53 |
|
163 | | -```python |
164 | | -if store.insert_if_not_exists(request_id, action): |
165 | | - result = do_side_effect() |
166 | | - store.complete(request_id, result) |
167 | | -else: |
168 | | - print("Duplicate request detected — execution blocked") |
| 54 | + if store.insert_if_not_exists(request_id, action): |
| 55 | + result = {"status": "sent"} |
| 56 | + store.complete(request_id, result) |
| 57 | + print("executed") |
| 58 | + else: |
| 59 | + print("duplicate blocked") |
169 | 60 | ``` |
170 | 61 |
|
171 | | -A request is identified once. Every retry resolves against that same execution record. |
172 | | - |
173 | 62 | --- |
174 | 63 |
|
175 | 64 | ## Mental model |
176 | 65 |
|
177 | 66 | Without SafeAgent: |
178 | | - |
179 | | -1. Request is sent. |
180 | | -2. Timeout or uncertainty happens. |
181 | | -3. The system retries. |
182 | | -4. The side effect runs again. |
| 67 | +retry → replay → duplicate |
183 | 68 |
|
184 | 69 | With SafeAgent: |
185 | | - |
186 | | -1. Request is sent with a `request_id`. |
187 | | -2. Execution is recorded durably. |
188 | | -3. The side effect runs once. |
189 | | -4. Retries are blocked for the same request. |
| 70 | +retry → resolve → safe |
190 | 71 |
|
191 | 72 | --- |
192 | 73 |
|
193 | 74 | ## Where this matters |
194 | 75 |
|
195 | | -- Payments |
196 | | -- Trading systems |
197 | | -- Background jobs |
198 | | -- Webhooks |
199 | | -- External API mutations |
200 | | -- AI agent tool calls |
201 | | -- Ticketing and messaging workflows |
202 | | - |
203 | | -Any system where running the same side effect twice is unacceptable. |
204 | | - |
205 | | ---- |
206 | | - |
207 | | -## Why this exists |
208 | | - |
209 | | -Retries do not mean “nothing happened.” |
210 | | - |
211 | | -They mean “we do not know what happened.” |
212 | | - |
213 | | -Most systems retry anyway. |
214 | | - |
215 | | -That is fine for reads. |
216 | | - |
217 | | -It is dangerous for side effects. |
218 | | - |
219 | | -That is how you get duplicate payments, duplicate emails, duplicate trades, and repeated external mutations. |
220 | | - |
221 | | -Execution Guard adds a safety boundary at the side-effect layer: |
222 | | - |
223 | | -- record the execution attempt |
224 | | -- execute once |
225 | | -- resolve retries safely |
226 | | - |
227 | | ---- |
228 | | - |
229 | | -## Case studies |
230 | | - |
231 | | -- [Trading Bot Case Study](docs/TRADING_BOT_CASE_STUDY.md) |
232 | | - |
233 | | -## Backends |
234 | | - |
235 | | -- SQLite → local / single process |
236 | | -- Postgres → distributed / production |
| 76 | +- trading |
| 77 | +- payments |
| 78 | +- APIs |
| 79 | +- agents |
| 80 | +- workflows |
237 | 81 |
|
238 | 82 | --- |
239 | 83 |
|
|
0 commit comments