afterstate
Deterministic external-state invariant testing for AI agents.
Your agent said "done." What did it actually do to your database, your
payments, your inventory? afterstate doesn't grade the agent's answers or its
tool-call trajectory — it attacks the systems your agent touches with
duplicate requests, client retries, conflicting idempotency keys and duplicate
webhooks, then verifies hard state invariants: money moved exactly once, a
decline left no trace, a compensation actually restored the balance.
No LLM judge anywhere. Same inputs, same verdict, every run.
[PASS] retry-after-timeout-grants-once
[FAIL] duplicate-request-grants-once
one_effect_per_key: duplicate effects in credit_ledger: dup-k1×2
balance_delta: delta is 500 (before=0, after=500), expected 250
Why
Eval tools compare messages and tool-call trajectories. But the failures that cost real money happen one layer down, in the state your agent leaves behind:
- a timeout retry charges the customer twice
- a declined payment still writes a ledger row
- a refund exceeds the original charge
- a webhook is emitted twice and the consumer double-fulfills
Every one of these can look like a 200 OK with a perfectly reasonable
transcript. You only catch them by diffing external state under fault
injection — which is what afterstate does, in CI, deterministically.
Install
Requires Python 3.11+.
pip install afterstate # once v0.1.0 lands on PyPI
pip install git+https://github.com/Createyouracccount/afterstate # until then
The demo service and its contracts live in the repo, not the wheel — clone for the full experience.
Ninety-second demo, one command
The repo ships a deliberately realistic demo: a SaaS credit-issuance service whose buggy mode contains four production-grade bug classes (ignored idempotency keys, declines with side effects, overdrawable balances, double-emitted webhooks).
git clone https://github.com/Createyouracccount/afterstate && cd afterstate
./demo.sh # or: docker compose run --rm demo
demo.sh deploys the buggy service, attacks it with 7 money-safety contracts
(6 fail, each with the exact duplicated ledger rows or wrong deltas), then
deploys the fixed service and shows the same contracts go green. A daily
sandbox-loop workflow re-runs this
red→green proof in CI and publishes the dated reports — the demo cannot
silently rot. See a sample failure report.
Your own system
afterstate init # scaffolds contracts/starter.yaml
afterstate run contracts/starter.yaml
Point target at a sandbox deployment of your service and its database,
list the tables your money flows through, pick faults and invariants. Guides:
How it works
A contract is a YAML file describing one scenario:
name: retry-after-timeout-grants-once
target:
base_url: http://127.0.0.1:8123
db: sqlite:///credits.db # or postgresql://...
setup:
- http: { method: POST, path: /admin/reset }
capture:
tables: [credit_ledger, balances]
invoke:
- http:
method: POST
path: /credits/grant
json: { user_id: u1, amount: 100 }
headers: { Idempotency-Key: retry-k1 }
faults: [retry_after_failure] # timeout, then client retry
expect_status: [200]
assert:
- one_effect_per_key: { table: credit_ledger, key: idempotency_key }
- balance_delta:
query: "SELECT COALESCE(SUM(amount),0) FROM balances WHERE user_id='u1'"
delta: 100
The runner executes setup → snapshot → invoke (with faults) → snapshot → assert → cleanup and reports per-invariant verdicts.
Fault injection
Request faults go under invoke[].faults:
| fault | what it simulates |
|---|---|
duplicate_request |
the same request delivered twice (double click, queue redelivery) |
response_lost |
server processed, client never saw the response (timeout / reset) |
retry_after_failure |
the classic timeout-then-retry under one idempotency key |
conflicting_payload_same_key |
an idempotency key reused with a different payload |
Webhook delivery faults go under the contract's webhooks.faults block (which
also runs the inbox that records what your service emits — see the
contract reference):
| fault | what it simulates |
|---|---|
duplicate_webhook |
each webhook delivered twice to the consumer |
out_of_order_webhooks |
webhook delivery order reversed |
Built-in invariants
| invariant | asserts |
|---|---|
one_effect_per_key |
at most one effect row per idempotency key |
no_effect_on_failure |
failed/declined calls leave zero state changes |
sums_equal |
two SQL aggregates agree (ledger vs balances, order vs payment) |
balance_delta |
a quantity changed by exactly the expected amount |
nonnegative |
a column never goes below zero (inventory, balances) |
state_restored |
a compensating flow returned tables to their prior state |
webhook_exactly_once |
each event key delivered exactly once |
CI
afterstate run exits nonzero on any failure and emits JUnit XML:
# .github/workflows/afterstate.yml
name: afterstate
on: [push]
jobs:
invariants:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install afterstate
- run: ./scripts/start-sandbox.sh # your sandbox SUT
- run: afterstate run 'contracts/*.yaml' --junit afterstate.xml --report report.md
- uses: actions/upload-artifact@v4
if: always()
with: { name: afterstate-report, path: report.md }
Scope (v0.1)
- HTTP systems under test; SQLite and PostgreSQL state capture; webhook inbox.
afterstateacts as the client and injects faults client-side — which is deterministic and covers the client-visible half of timeouts, resets and redeliveries. A transparent proxy that intercepts a live agent's own traffic is the v0.2 extension point.- Not an agent framework, not an LLM evaluator, no dashboard. It is a testing engine you point at a sandbox.
License
MIT
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 afterstate-0.1.0.tar.gz.
File metadata
- Download URL: afterstate-0.1.0.tar.gz
- Upload date:
- Size: 31.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
06617f6ec6fb09af1189a3a92e844a33b0c8825302e8edfb905f532ec30a2e96
|
|
| MD5 |
cdcab999c239d968c55ef41949085aec
|
|
| BLAKE2b-256 |
824031997ffd2a4609f69d44cd0ecad270e5b562024eecf241db207ea79de72b
|
Provenance
The following attestation bundles were made for afterstate-0.1.0.tar.gz:
Publisher:
publish.yml on Createyouracccount/afterstate
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
afterstate-0.1.0.tar.gz -
Subject digest:
06617f6ec6fb09af1189a3a92e844a33b0c8825302e8edfb905f532ec30a2e96 - Sigstore transparency entry: 2497104800
- Sigstore integration time:
-
Permalink:
Createyouracccount/afterstate@a83cb0b220efcb77450ac5088f40c86b73359e62 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Createyouracccount
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a83cb0b220efcb77450ac5088f40c86b73359e62 -
Trigger Event:
push
-
Statement type:
File details
Details for the file afterstate-0.1.0-py3-none-any.whl.
File metadata
- Download URL: afterstate-0.1.0-py3-none-any.whl
- Upload date:
- Size: 21.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9fd8403558ed29c719176408f500374c11cc58ea59947f6ea95000bb9183faca
|
|
| MD5 |
b4183645c488aa3c2293f889c2dd9ce2
|
|
| BLAKE2b-256 |
5c399c22c24844ef4b7bc5e730e20cdd43b7747bf657c1e5562f70f24b7828d5
|
Provenance
The following attestation bundles were made for afterstate-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on Createyouracccount/afterstate
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
afterstate-0.1.0-py3-none-any.whl -
Subject digest:
9fd8403558ed29c719176408f500374c11cc58ea59947f6ea95000bb9183faca - Sigstore transparency entry: 2497104828
- Sigstore integration time:
-
Permalink:
Createyouracccount/afterstate@a83cb0b220efcb77450ac5088f40c86b73359e62 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Createyouracccount
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a83cb0b220efcb77450ac5088f40c86b73359e62 -
Trigger Event:
push
-
Statement type: