git diff for AI agent runs — record golden trajectories, find the first divergent step, gate CI on real regressions
Project description
offtrack
git diff for AI agent runs.
Record golden trajectories. Re-run after any change. See the first step where your agent went off track — and gate CI on it.
You bumped a model. Or tweaked a prompt. Or upgraded your agent framework.
Every eval score still looks fine. But your agent now refunds $842 without checking the refund policy — cheaper, faster, and catastrophically wrong:
$ offtrack check
✗ refund/over-limit 0/5 aligned
= step 0 llm_call (both runs identical)
= step 1 tool_call lookup_order({"order_id": "TEST-1"})
▲ first divergence — expected check_refund_policy, got issue_refund
baseline check_refund_policy({"amount_usd": 842.0})
this run issue_refund({"order_id": "TEST-1", "amount_usd": 842.0})
divergence rate rose 0% → 100% (Fisher exact p=0.004, effect +100%)
Δtokens -46% Δlatency -30%
FAIL — exit code 1
Eval scores tell you whether behavior changed. offtrack shows you where: the first divergent step, with cost and latency deltas, from traces stored locally in SQLite. Statistical verdicts (PASS / FAIL / INCONCLUSIVE) gate CI so stochastic LLM variance doesn't cause false alarms — and honest uncertainty doesn't hide behind a green check.
Quickstart
pip install offtrack
offtrack init # scaffold offtrack.yaml + baselines/
offtrack record # capture golden trajectories (N runs per task)
# ...change your model / prompt / framework...
offtrack check # first divergent step + verdict + exit code
Capturing traces — three ways
Zero-code (OpenAI / Anthropic SDKs) — two lines at your agent's entrypoint:
import offtrack.capture
offtrack.capture.install() # patches whichever SDKs are importable
Every LLM call is recorded, and tool calls are reconstructed by message delta — real names, args, and results, no changes to your agent loop.
LangGraph / LangChain — pass the callback:
from offtrack.integrations.langgraph import OfftrackCallbackHandler
handler = OfftrackCallbackHandler()
graph.invoke(inputs, config={"callbacks": [handler]})
handler.finish()
Anything else — write capture events (JSONL) to $OFFTRACK_TRACE_DIR, or export OpenTelemetry GenAI traces there: offtrack ingests both semconv generations and the OpenInference flavor. Claude Code sessions import directly with offtrack ingest claude-code <session.jsonl>.
How it works
- Record —
offtrack recordruns each task N times (default 5) and stores the trajectories: every LLM call, tool call, argument, token count, cost, latency. Baselines auto-export tobaselines/*.json— committed to git, so changing golden behavior is a reviewed act in PRs. - Align — new runs are aligned against baselines with Needleman-Wunsch sequence alignment over steps (tool-name gating + structural argument similarity). Reordered parallel calls, retries, and volatile fields (UUIDs, timestamps — masked by default) don't cause false alarms.
- Localize — the report points at the first divergent step: missing, extra, or changed — with the argument-level diff and whether the trajectories resynced afterward.
- Verdict — baselines are recorded N times so run-to-run variance is measured, not assumed. FAIL requires an exact Fisher test plus a minimum effect size; PASS requires an exact upper confidence bound. Anything else is INCONCLUSIVE with a prescription: "run 2 more repetitions."
CI
- uses: abhi13-tech/offtrack@v0
with:
suite: offtrack.yaml
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
FAIL fails the check and posts a PR comment with the divergence table. INCONCLUSIVE warns without blocking (strictness is opt-in via inconclusive-as: fail). Exit codes: 0 pass, 1 fail, 3 inconclusive, 4 setup error — infra problems never masquerade as regressions.
This repo gates itself: every PR runs offtrack against its own demo agent (dogfood.yml), and the gate's failure path is itself under test.
pytest
@pytest.mark.offtrack(task="refund/over-limit")
def test_refund_flow(offtrack):
with offtrack.record():
run_my_agent("refund order TEST-1")
offtrack.assert_matches_baseline()
offtrack.assert_cost_under(usd=0.05)
offtrack.assert_max_steps(30)
pytest -n auto (xdist) is fully supported — workers spill trajectories to append-only files; the controller merges at session end.
What offtrack is not
These are complements, not competitors — offtrack is the trajectory-diff layer:
| offtrack | promptfoo | LangSmith | EvalView | |
|---|---|---|---|---|
| Unit of comparison | step-level trajectory | prompt/output pairs | hosted traces + evals | eval-run snapshots |
| First-divergence localization | ✓ | — | — | — |
| Semantic alignment (retries, reordering, masks) | ✓ | — | — | exact-match only |
| Statistical verdict (variance-aware) | ✓ | — | — | pass@k |
| Local-first, no account | ✓ SQLite | ✓ | hosted | ✓ |
| Vendor-neutral ingest (OTel GenAI, both semconv generations) | ✓ | — | — | — |
offtrack doesn't score answer quality — pair it with an eval framework for that. It catches the thing eval scores structurally miss: procedural regressions in how the agent got there.
Demo
examples/refund-agent/ is a complete offline demo — a small support agent with a written refund policy and two scripted personas: fake-careful (follows policy) and fake-sloppy (a stand-in for a cheaper model that skips the policy check). No API key, no network, $0:
cd examples/refund-agent
AGENT_MODEL=fake-careful offtrack record # golden: lookup → policy check → escalate
AGENT_MODEL=fake-sloppy offtrack check # FAIL: skips policy, refunds $842 directly
The fake mode is a scripted LLM, not a mocked test — the entire real pipeline (capture, ingest, alignment, stats) executes identically in fake and real mode.
Roadmap
LangGraph callback + OpenAI/Anthropic SDK capture shims— shipped in 0.2.0 (offtrack.capture.install())Claude Code session ingest— shipped in 0.2.0 (offtrack ingest claude-code)- Semantic matchers (embedding / LLM-judge) via the
Matcherprotocol — v1 is deterministic-only by design offtrack bisect— find the commit that introduced a divergence- CrewAI / Pydantic-AI adapters · HTML report viewer
Development
git clone https://github.com/abhi13-tech/offtrack && cd offtrack
uv sync --all-extras --group dev
uv run pytest # 200 tests, all offline
uv run mypy # strict
just dogfood # run the gate against the demo locally
MIT licensed.
Project details
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 offtrack-0.2.0.tar.gz.
File metadata
- Download URL: offtrack-0.2.0.tar.gz
- Upload date:
- Size: 596.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 |
f787c4e1da8dd65c4359d7dd29b4a54d5c527be7e40f5dca9f9ecbe7f6f41f95
|
|
| MD5 |
6e6d215e8e58b07a61763f33b68dfda3
|
|
| BLAKE2b-256 |
2d69ff0d084759345a9f9b098de59e1f71ea6ac2723481aef39fa0e1d0151384
|
Provenance
The following attestation bundles were made for offtrack-0.2.0.tar.gz:
Publisher:
release.yml on abhi13-tech/offtrack
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
offtrack-0.2.0.tar.gz -
Subject digest:
f787c4e1da8dd65c4359d7dd29b4a54d5c527be7e40f5dca9f9ecbe7f6f41f95 - Sigstore transparency entry: 2341014961
- Sigstore integration time:
-
Permalink:
abhi13-tech/offtrack@3d59af17d8df8b32b3075c2497374199db33dd6c -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/abhi13-tech
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3d59af17d8df8b32b3075c2497374199db33dd6c -
Trigger Event:
push
-
Statement type:
File details
Details for the file offtrack-0.2.0-py3-none-any.whl.
File metadata
- Download URL: offtrack-0.2.0-py3-none-any.whl
- Upload date:
- Size: 66.2 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 |
38d393f680974ed058a3a0f8c654df129f2e29d04664e1d7c1056a36f0c9b991
|
|
| MD5 |
d8f83d477d6d0e3c641d856a20036992
|
|
| BLAKE2b-256 |
4da3653480482b3b9812e4732624b3807a18bc1d4f7c3db17845592bc06919f5
|
Provenance
The following attestation bundles were made for offtrack-0.2.0-py3-none-any.whl:
Publisher:
release.yml on abhi13-tech/offtrack
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
offtrack-0.2.0-py3-none-any.whl -
Subject digest:
38d393f680974ed058a3a0f8c654df129f2e29d04664e1d7c1056a36f0c9b991 - Sigstore transparency entry: 2341014968
- Sigstore integration time:
-
Permalink:
abhi13-tech/offtrack@3d59af17d8df8b32b3075c2497374199db33dd6c -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/abhi13-tech
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3d59af17d8df8b32b3075c2497374199db33dd6c -
Trigger Event:
push
-
Statement type: