Undoom
Turn a production agent failure into a regression test that runs on every change.
Observability shows that a tool-calling agent failed. An eval platform scores a dataset. Undoom does the step after that: replay the failed run with one change, mock the tools from the recording, and promote a fixed replay into a test your CI can run. It is not a monitor, and it is not a general benchmark suite.
Undoom sits next to Keywarden (what an agent is allowed to do) and Boxwren (where untrusted code runs). Use those when the question is permission or isolation. Use Undoom when the question is whether a known failure can come back.
The loop
- Record a run: prompts, model calls, tool calls, and tool results.
- Mark it failed, and name the tool calls that should not have happened.
- Replay with exactly one change: a prompt, a model id, or one tool implementation.
- Every other tool returns the recorded result. Undoom does not call it.
- Promote a fixed replay.
undoom testfails if that failure shows up again.
Live model output is not bit-identical. Deterministic here means the tool I/O is frozen. Replay calls the model again at temperature 0. A cassette hit is the pair (tool name, canonical JSON arguments). Different key order does not count as a new call. A miss stops the replay. Undoom will not fall through to the live tool.
A tool override runs your function, with your privileges, the same way your test suite does. Do not point it at a tool you would not run locally.
Install
Python 3.11 or newer.
pip install undoom
pip install 'undoom[openai]' # live replay against the OpenAI API
pip install 'undoom[anthropic]' # Anthropic Messages capture helper
pip install 'undoom[postgres]' # shared trace log
pip install 'undoom[ui]' # local diff UI
Traces default to .undoom/traces.sqlite. Case files live in .undoom/cases/ and are meant to be committed. Keep the database out of git. A useful .gitignore:
.undoom/*
!.undoom/cases/
!.undoom/cases/**
Traces can contain customer text. Undoom redacts common API-key and Authorization shapes before writing. It cannot know every secret. Treat the sqlite file, and any case you commit, as sensitive. See SECURITY.md.
Record a run
import undoom
from openai import OpenAI
undoom.configure(agent="support") # sqlite at .undoom/traces.sqlite
def lookup_order(order_id: str) -> dict:
return {"order_id": order_id, "status": "shipped"}
def refund_order(order_id: str) -> dict:
return {"order_id": order_id, "status": "refunded"}
with undoom.run(name="ticket-1042") as recorder:
client = undoom.wrap_openai(
OpenAI(),
recorder,
tools={"lookup_order": lookup_order, "refund_order": refund_order},
)
client.chat.completions.create(
model="gpt-4.1",
temperature=0,
messages=[
{"role": "system", "content": open("prompts/support.txt").read()},
{"role": "user", "content": "Where is order 1042?"},
],
tools=[...], # the same schema you pass to the API
)
if any(step.kind == "tool" and step.name == "refund_order" for step in recorder.run.steps):
recorder.fail(
"refunded without an explicit request",
unexpected_tools=["refund_order"],
)
unexpected_tools is the failure signature. Promote turns those names into the check undoom test enforces. Without them, Undoom treats tools that disappeared in the fixed replay as the signature.
undoom.wrap_anthropic(client, recorder, tools=...) does the same for Anthropic's messages.create API.
You can record a model call and a tool yourself when you are not on those SDKs. recorder.record_model(...) and with recorder.tool(name, arguments) as handle: handle.record(result) are the contract. Adapters are thin callers of that API.
undoom runs list
undoom runs show <run-id>
undoom runs fail <run-id> --reason "refunded without an explicit request" --unexpected-tool refund_order
Import an OpenTelemetry trace
When a stack already exports GenAI spans, you can turn one OTLP/JSON document into a stored run:
undoom import trace.json --agent support --name ticket-1042
undoom import trace.json --trace-id <id> # required when the file holds more than one trace
The importer only accepts tool spans that include both gen_ai.tool.call.arguments and a result or error. GenAI conventions mark those fields as opt-in; a span that omits them cannot fill a cassette, so Undoom refuses the import instead of inventing values. Mark the failure afterward with undoom runs fail the same way you would for a native capture.
Replay one change
undoom replay <run-id> --prompt-file prompts/support.txt
undoom replay <run-id> --model gpt-4.1-mini
undoom replay <run-id> --tool lookup_order=myapp.tools:lookup_order
A command that sets more than one of prompt, model, and tool is rejected. The replay id and verdict are printed. Verdicts:
| Verdict | Meaning |
|---|---|
fixed |
The failure signature did not recur |
still_failing |
The same unexpected tool or tool error came back |
diverged |
The model called a tool the recording cannot answer |
error |
The provider or the override function could not be loaded |
Final assistant text is shown for comparison. It is not the pass/fail check.
Promote and test
undoom promote <replay-id> --name no-unsolicited-refund
undoom promote <replay-id> --name no-unsolicited-refund --expect-tools
undoom test
undoom test --format junit
--expect-tools also locks the successful tool sequence. The default check is only that the original failure does not recur.
Prompt and tool changes point at files in your repo. undoom test reads those paths again, so editing the prompt is what CI evaluates. The case file embeds the tool cassette, which means CI does not need the sqlite history.
# .github/workflows/undoom.yml
name: undoom
on: [push, pull_request]
jobs:
regressions:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install undoom
- run: undoom test --format junit
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
Set UNDOOM_MODEL_PROVIDER=openai (the default) for live replay. A provider can also be module:factory, a zero-argument function that returns an object with complete(...).
Offline demo
The order-lookup story ships in examples/order_lookup/. A buggy prompt refunds order 1042 after looking it up. The fixed prompt refunds only when the user asks. No API key is required.
mkdir -p /tmp/undoom-demo
UNDOOM_DEMO_ROOT=/tmp/undoom-demo python examples/order_lookup/demo.py
cd /tmp/undoom-demo
PYTHONPATH=/path/to/undoom \
UNDOOM_MODEL_PROVIDER=examples.order_lookup.fake:provider \
undoom test
Put the refund instruction back into prompts/fixed.txt and undoom test fails on refund_order.
Docker runs that story and leaves the case file on a volume:
docker compose -f docker/compose.yml up --build demo
Postgres, for a shared trace log rather than for CI, is the postgres profile in the same file. Point the CLI at it with UNDOOM_STORE=postgresql://undoom:undoom@localhost:5432/undoom.
Local diff UI
pip install 'undoom[ui]'
undoom serve
The API listens on http://127.0.0.1:8787. From apps/ui:
npm install
NEXT_PUBLIC_UNDOOM_API=http://127.0.0.1:8787 npm run dev
The hosted demo at undoom.rejisterjack.com is the same UI built with NEXT_PUBLIC_DEMO=1. It reads committed synthetic fixtures only. There is no write API and no customer data on that deployment. On Vercel, set the project root to apps/ui and NEXT_PUBLIC_DEMO to 1.
Store
UNDOOM_STORE |
Store |
|---|---|
| unset | .undoom/traces.sqlite |
| a filesystem path | SQLite at that path |
sqlite:///relative.sqlite |
SQLite |
postgresql://... |
Postgres (undoom[postgres]) |
Documents are JSON with schema_version. This release reads version 1 and rejects anything else.
Development
uv sync --all-extras --group dev
uv run ruff check src tests examples
uv run ruff format --check src tests examples
uv run mypy src
uv run pytest
UNDOOM_TEST_POSTGRES opts into the Postgres integration test. The default suite never calls the network.
What this is not
Undoom does not replace production monitoring. It does not decide which tools an agent may call, and it does not sandbox code. Those are Keywarden and Boxwren. It is not a place to host a general LLM benchmark. The architecture note in ARCHITECTURE.md is the longer version of that boundary.
Metadata
Release files for undoom 1.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| undoom-1.0.1.tar.gz | 29.9 kB | With provenance | Details for undoom-1.0.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| undoom-1.0.1-py3-none-any.whl | Python 3 | none | any | Details for undoom-1.0.1-py3-none-any.whl |
Total release size: 70.6 kB
Release files / undoom-1.0.1.tar.gz
| Download URL | undoom-1.0.1.tar.gz |
|---|---|
| Size | 29.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
08e5abfa0e601fbc8bbd1d9715b77f0f3e189f34054e7a25c92a016463f6d428
|
|
BLAKE2b-256 checksum How to use checksums |
92f793f6b9fc28a5a4ae5a2793e656e7caaa0ae76f91e545958ec8b1400c04db
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 7, 2026.
Transparency logRelease files / undoom-1.0.1-py3-none-any.whl
| Download URL | undoom-1.0.1-py3-none-any.whl |
|---|---|
| Size | 40.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3532a0fc9c495e2b30cc824ce14e48ad902a7d2866c1287029c841fd357b8dea
|
|
BLAKE2b-256 checksum How to use checksums |
d6c1c3194ca727350087a9cda52fbc47e8f8e9844b864c307536ccade3cf0cd7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 7, 2026.
Transparency logSecurity for undoom 1.0.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for undoom-1.0.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 7, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.