Lightweight tracing and divergence diagnosis for LLM agent chains.
Project description
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
Project details
Release history Release notifications | RSS feed
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 tracedrift-0.1.0.tar.gz.
File metadata
- Download URL: tracedrift-0.1.0.tar.gz
- Upload date:
- Size: 20.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 |
3977235401a95955b4f720822de7e30477a449ca0845661b396ff050740c777d
|
|
| MD5 |
3b77743e0fe236dfeb6ca522e10774dc
|
|
| BLAKE2b-256 |
e1295d2f3cdf7a8a28f7058c0d113292e991df194d73cd9606d07cac36292b9b
|
Provenance
The following attestation bundles were made for tracedrift-0.1.0.tar.gz:
Publisher:
release.yml on EtiSandeep/tracedrift
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tracedrift-0.1.0.tar.gz -
Subject digest:
3977235401a95955b4f720822de7e30477a449ca0845661b396ff050740c777d - Sigstore transparency entry: 2287310184
- Sigstore integration time:
-
Permalink:
EtiSandeep/tracedrift@cb13405573a9a80780ca5d597bc09088f594af95 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/EtiSandeep
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@cb13405573a9a80780ca5d597bc09088f594af95 -
Trigger Event:
push
-
Statement type:
File details
Details for the file tracedrift-0.1.0-py3-none-any.whl.
File metadata
- Download URL: tracedrift-0.1.0-py3-none-any.whl
- Upload date:
- Size: 18.7 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 |
f08bbae3389bf2df8aa8418e520bdc97a7c52c9bd0482a2ce0a7ae75626a39b4
|
|
| MD5 |
d4ef77965374730ee543502512b6ce02
|
|
| BLAKE2b-256 |
2d0b31e67bdb929617f45762af67fb9688e13982a860c5720c58878271a9862b
|
Provenance
The following attestation bundles were made for tracedrift-0.1.0-py3-none-any.whl:
Publisher:
release.yml on EtiSandeep/tracedrift
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tracedrift-0.1.0-py3-none-any.whl -
Subject digest:
f08bbae3389bf2df8aa8418e520bdc97a7c52c9bd0482a2ce0a7ae75626a39b4 - Sigstore transparency entry: 2287310194
- Sigstore integration time:
-
Permalink:
EtiSandeep/tracedrift@cb13405573a9a80780ca5d597bc09088f594af95 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/EtiSandeep
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@cb13405573a9a80780ca5d597bc09088f594af95 -
Trigger Event:
push
-
Statement type: