tracedrift
Lightweight tracing and divergence diagnosis for LLM agent chains.
When an agent chain does something wrong, the failure usually surfaces several
steps away from its cause — a retrieval step silently returns nothing, an LLM
call improvises around it, and by the time you see the bad final answer you're
staring at raw JSON logs trying to reconstruct what happened. tracedrift gives
you a decorator-based tracer, a queryable SQLite trace store, a diagnosis pass
that flags where a chain likely diverged from its normal behavior (not just
where it crashed), and a way to replay a trace with one step's output swapped
out to see if the rest of the chain recovers.
No mandatory cloud dependency, no framework lock-in — it works with raw API calls, LangChain, DSPy, or anything else that's just Python functions.
See docs/USAGE_GUIDE.md for instrumentation patterns,
how to get useful signal out of diagnose, async/replay caveats, and
performance notes — this README is the quickstart, that's the deep dive.
Install
pip install -e .
(PyPI release TBD — this is a fresh package.)
Quickstart
import tracedrift
def retrieve(query):
with tracedrift.span("retrieve", input={"query": query}) as s:
s.output = my_vector_db.search(query)
return s.output # read back so replay overrides work — see "Replay" below
def call_llm(docs):
with tracedrift.span("llm_call", input={"docs": docs}) as s:
s.output = my_llm.complete(docs)
return s.output
@tracedrift.trace_agent(name="qa_agent")
def run_agent(query):
docs = retrieve(query)
return call_llm(docs)
run_agent("what's our refund policy?")
Traces are written to tracedrift.db (SQLite) by default. Change it with
tracedrift.configure("path/to/db") before running.
Inspecting traces
tracedrift list # recent traces
tracedrift show <trace_id> # span tree with timing and status
tracedrift diagnose <trace_id> # where did this trace likely go wrong?
diagnose doesn't just look for exceptions. It compares each span's output
against that span's own history (by name) across prior traces and flags:
- Explicit errors — a span that raised. Highest confidence, reported first.
- Silent empty output — a span returned
None/[]/""when it historically almost never does. This catches the common case where nothing crashes but a step quietly failed. - Type drift — a span's output type differs from what it's always
returned before (e.g. usually a
list, this time astr).
These are intentionally simple, interpretable heuristics rather than an ML classifier — the goal is that you can trust why something got flagged, not just that it was.
Replay with intervention
Once you suspect a step, you can rerun the same call with that step's output forced to something else, to check whether the rest of the chain would have recovered:
result = tracedrift.replay_with_intervention(
run_agent,
args=("what's our refund policy?",),
overrides={"retrieve": [<docs you think it should have found>]},
)
Important caveat: this overrides what gets recorded as a span's output,
not the code that runs inside the with span(...) block. For an override to
actually change downstream behavior, your traced function needs to read
s.output back out after the block (as in the quickstart above) rather than
returning a value it computed before assigning s.output. This is a
deliberate simplicity tradeoff — true arbitrary step-skipping would require
capturing and replaying closures, which is out of scope for a library this
size.
Design notes
- Storage: SQLite by default, one file, no server.
Storageis a small wrapper around raw SQL — swap it out if you need Postgres for concurrent writers. - Nesting: spans nest automatically via
contextvars, so this is safe acrossasynciotasks (though not yet tested under heavy concurrent load — contributions welcome). - Serialization: inputs/outputs are stored as JSON via
json.dumps(..., default=str), so non-JSON-serializable objects get stringified rather than crashing the tracer.
Async
trace_agent and span both work with async def code — no separate API:
async def retrieve(query):
async with tracedrift.span("retrieve", input={"query": query}) as s:
s.output = await my_vector_db.search(query)
return s.output
@tracedrift.trace_agent(name="qa_agent")
async def run_agent(query):
docs = await retrieve(query)
...
trace_agent detects async def functions automatically and awaits them.
Nesting relies on contextvars, which asyncio copies into each Task at
creation time — spans nest correctly as long as they run within the same
Task as the enclosing trace_agent call; spans started in a separately
created Task (e.g. via asyncio.create_task) won't see updates made after
that task started.
Cost/token tracking
Record usage on a span with s.set_usage(...) — tracedrift doesn't know
anything about specific providers' response shapes, so pull the numbers out
yourself and pass them in:
with tracedrift.span("llm_call", input=docs) as s:
resp = my_llm.complete(docs)
s.output = resp.text
s.set_usage(
prompt_tokens=resp.usage.prompt_tokens,
completion_tokens=resp.usage.completion_tokens,
cost_usd=resp.usage.cost_usd,
)
tracedrift show <trace_id> prints per-span usage and a trace-level total.
Programmatically, tracedrift.trace_usage(trace_id) sums prompt/completion/
total tokens and cost across every span in a trace that called set_usage
(returns {} if none did).
Web UI
tracedrift serve # http://127.0.0.1:8765
tracedrift serve --port 9000
A minimal, read-only, stdlib-only local server: trace list, span tree per
trace, diagnosis and usage totals inline. No JS framework, no build step, no
auth — it's meant for browsing your own local tracedrift.db during
debugging, not for exposing over a network.
What's not here yet
- Framework-specific adapters (LangChain callback handler, DSPy module wrapper)
Contributing
See CONTRIBUTING.md for dev setup, project layout, and design principles the codebase holds itself to. Please also read the Code of Conduct. Found a security issue? See SECURITY.md instead of opening a public issue.
License
MIT
Metadata
Release files for tracedrift 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| tracedrift-0.1.0.tar.gz | 20.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tracedrift-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 39.4 kB
Release files / tracedrift-0.1.0.tar.gz
| Download URL | tracedrift-0.1.0.tar.gz |
|---|---|
| Size | 20.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3977235401a95955b4f720822de7e30477a449ca0845661b396ff050740c777d
|
|
BLAKE2b-256 checksum How to use checksums |
e1295d2f3cdf7a8a28f7058c0d113292e991df194d73cd9606d07cac36292b9b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Jul 30, 2026.
Transparency logRelease files / tracedrift-0.1.0-py3-none-any.whl
| Download URL | tracedrift-0.1.0-py3-none-any.whl |
|---|---|
| Size | 18.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f08bbae3389bf2df8aa8418e520bdc97a7c52c9bd0482a2ce0a7ae75626a39b4
|
|
BLAKE2b-256 checksum How to use checksums |
2d0b31e67bdb929617f45762af67fb9688e13982a860c5720c58878271a9862b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Jul 30, 2026.
Transparency log