Skip to main content

See exactly what context each LLM call consumed - local-first context lineage for LLM apps.

Project description

ctxlineage logo

ctxlineage

See exactly what context each LLM call consumed — and how it flowed into the next.

Two lines of code turn your app's LLM calls into a single static HTML report: the anatomy of every context window, and the lineage of every context element. No server, no database, no account. Your prompts never leave your machine.

Not another trace viewer: ctxlineage makes your runtime context an engineered artifact — decomposed, provenance-tracked, and (v0.2) testable in CI.

CI Coverage PyPI Python License

Call anatomy view — what filled the context window, segment by segment

Quickstart

pip install ctxlineage
import ctxlineage
ctxlineage.init()  # auto-instruments the openai and anthropic SDKs
# ... run your app: every LLM call is recorded to .ctxlineage/events.jsonl
ctxlineage report --open   # one self-contained HTML file

That's the whole integration. Streaming, async, tool calls — captured. Add .ctxlineage/ to your .gitignore.

What you get

View Question it answers
Overview Which calls are heaviest? How close to the window limit? Did my tags match?
Calls What actually filled this call's context window — and at what token cost?
Chain How did each output become the next call's input? Where do agent loops accumulate context?
Graph Where did each context element come from (vector DB, prompt file, memory) and which downstream calls did it influence?

Think dbt docs generate, for your context windows: calls are functions, context elements are typed inputs with provenance, outputs flow into downstream inputs.

Ten-second tour: Overview, call anatomy, session chain, lineage graph with click-to-trace, dark mode

Tagging (optional, unlocks lineage)

Everything above works with zero tagging. Label your context assembly and the report upgrades from role-based heuristics to real, provenance-carrying segments:

with ctxlineage.span("answer_user_query") as span:
    span.tag("rag_chunks", docs, source="qdrant:products_v2", transform="top_k(8)")
    span.tag("memory", user_profile, source="memory:user_prefs")
    resp = client.chat.completions.create(...)

Tagged content is matched back into the actual prompts (exact → partial → honestly-untagged, with the match rate displayed — never fabricated).

Testing context in CI (ctxlineage test)

The same recorded artifact is a substrate for deterministic assertions — no LLM judge, no eval dataset. Write a ctxlineage.toml:

[[assert.window_budget]]
max_pct = 80                 # no call may exceed 80% of the model's window

[[assert.window_budget]]
segment = "tool_defs"        # a segment kind, or a tag name
max_pct = 20                 # tool definitions may not eat >20% of the window

[[assert.grounded]]
tag = "rag_chunks"           # every rag_chunks tag must land in the window
warn_dead = true             # advisory: flag chunks nothing downstream consumed
ctxlineage test              # exits non-zero on a hard-gate failure → CI gate

A rule only gates where its evidence is exact, which decides what can fail your build:

Gates on Because
window_budget any call, no tagging needed token counts and the model window are deterministic from capture alone
grounded presence tagged content only the tag() is your declaration, so "it never reached the window" is exact
grounded dead-context nothing — advisory "nothing downstream used it" is read off inferred lineage edges

Gating on inference is how you get a flaky gate, so ctxlineage won't do it: an untagged run warns instead of failing, and anything that couldn't be evaluated (an unknown model window, a segment name that never appeared) is reported as skipped or warned — never silently passed.

segment selects on the kinds the pipeline really produces — system, user, assistant, tool, tool_defs — or any tag name.

Try it in 30 seconds (no API key)

python examples/rag_app.py --mock            # records a real run, fully offline
ctxlineage test -c examples/ctxlineage.toml  # → All 3 assertion(s) over 3 call(s) passed

examples/ctxlineage.toml is a commented reference config; CI runs exactly the two commands above, so it cannot drift from the code.

Wiring it into CI

ctxlineage test gates a recorded run, so CI needs one. It does not have to be a live one — assertions read the JSONL, not the network:

# .github/workflows/context.yml
- run: pip install ctxlineage
- run: pytest tests/            # your existing suite, with ctxlineage.init() in it
- run: ctxlineage test          # fails the build on a hard-gate breach

Where the events come from is your choice, and the trade is real:

Deterministic? Cost
Your existing tests, with mocked/replayed responses yes — start here none
A live run against the real API no (temperature, model drift) tokens
ctxlineage import --from claude-code yes (a recorded session) none

Prompt assembly is usually deterministic even when the model's replies are not — which is the point: a mocked run still proves what your code put in the window. The examples take this route (--mock replays canned responses), and so can your test suite. Live runs need a statistical gate (run N times, assert a pass rate) rather than a single hard gate — that is not built yet, so keep hard gates on recorded runs.

Inside pytest, per test (pytest --ctxlineage)

The two-step wiring above keeps the events and the assertions in separate places, and can only ever fail the whole run — "some call in this log blew the budget". If your suite already runs the app under ctxlineage.init(), the bundled pytest plugin evaluates the same ctxlineage.toml inside the suite and fails the test whose call breached it:

pytest --ctxlineage
FAILED tests/test_agent.py::test_long_loop
  FAIL  window_budget: ... is 91.4% of the 200,000-token window, over the 80% budget

Per-test attribution is the whole reason to prefer it over ctxlineage test; that is the one thing the CLI cannot do. Everything else is identical — same config, same tier rule (a warning never gates, an unevaluated call is reported as skipped, never a green test). It is inert until you pass --ctxlineage (installing ctxlineage never changes a suite's behaviour), or you can commit the choice:

# pyproject.toml
[tool.pytest.ini_options]
addopts = "--ctxlineage"

The plugin owns ctxlineage.init() into a throwaway temp dir for the run, unless your app already called init() — then it uses your app's directory and leaves capture alone. ctxlineage test is still the tool for a recorded run produced outside pytest (a script, a --mock example, ctxlineage import), and for a grounded tag declared once for the whole run rather than inside one test.

Gate granularity Recorded run comes from
ctxlineage test the whole run anywhere — a script, an example, an import
pytest --ctxlineage the individual test the suite itself, as it runs

Importing coding-agent sessions (ctxlineage import)

Claude Code and claude -p are separate, non-Python processes, so ctxlineage.init() can't patch them. But they already write a session transcript to disk — so ctxlineage reads that local file. Nothing is proxied, injected, or sent anywhere; it is the same local-first bargain as capture.

ctxlineage import --from claude-code       # newest session for this directory
ctxlineage import --from claude-code --session <id>
ctxlineage import --from claude-code path/to/session.jsonl
ctxlineage import --from claude-code --dry-run   # show what it would do, write nothing
ctxlineage report --open                   # renders like any captured session

The transcript becomes ordinary events.jsonl, so the four views, ctxlineage test, and the MCP server all work on it unchanged.

What a transcript cannot tell you. This is reconstruction, not capture, and the gaps are real — the report is honest about them rather than inventing numbers:

Token counts The API's own usage, reconstructed from the transcript — not estimated
Segment sizes Estimated (the prompt text is rebuilt from the conversation tree)
System prompt, tool definitions Not preserved — they were sent and cost tokens, but the transcript never records them
Reasoning text Not preserved — kept as a signature with the text stripped
Request params, duration, stream flag Not preserved

So an imported call's segments account for only part of its real prompt: the system prompt and tool definitions alone can be tens of thousands of tokens. ctxlineage import prints the coverage it achieved, and the unaccounted remainder is recorded per call. Live capture via ctxlineage.init() remains the complete picture; import is the bridge to processes you cannot patch.

Principles

  • Local-first, zero-server. The artifact is one HTML file that opens offline. Capture is an append-only local JSONL. Nothing is ever transmitted.
  • Non-intrusive by default. init() and you're done; tags are progressive enhancement. The capture layer never breaks your app — failures degrade to warnings, not exceptions.
  • Honest data. Real usage counts preferred over estimates, estimates labeled, unmatched tags shown as unmatched, inference caps disclosed.

Scope & limits

Designed as a per-run, dev-time artifact. Comfortable up to ~5,000 calls (~15 MB report); usable to ~20,000 calls (~65 MB). Reports contain full prompt text — treat them like logs with sensitive data (see SECURITY.md). Before sharing, mask secrets with ctxlineage report --redact "sk-[A-Za-z0-9]+" (repeatable regex; the report discloses what was redacted), or keep them out of the log entirely with ctxlineage.init(redact_fields=["request.messages.content"]).

Status

v0.2.0 — the capture layer (openai + anthropic), the four-view report, the span/tag lineage pipeline, the read-only MCP server, and runnable examples, plus:

  • ctxlineage test — deterministic context contracts (window_budget, grounded) that gate a recorded run in CI, and a pytest plugin (pytest --ctxlineage) that attributes a breach to the test that caused it.
  • ctxlineage import --from claude-code — bring a Claude Code / claude -p session into the same report, honest about what a transcript cannot preserve.

See the CHANGELOG and the issues.

Contributing

See CONTRIBUTING.md — DCO sign-off, hermetic tests, deliberately small maintenance surface (off-roadmap issues may be closed).

License

Apache-2.0

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

ctxlineage-0.2.0.tar.gz (991.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ctxlineage-0.2.0-py3-none-any.whl (77.1 kB view details)

Uploaded Python 3

File details

Details for the file ctxlineage-0.2.0.tar.gz.

File metadata

  • Download URL: ctxlineage-0.2.0.tar.gz
  • Upload date:
  • Size: 991.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for ctxlineage-0.2.0.tar.gz
Algorithm Hash digest
SHA256 e90d703d0a2172bf330e204f5a431a3beb9457d5fabf77a129381e92f93debe5
MD5 fa194472ca1dab753554dcba840cda82
BLAKE2b-256 e5e0f6e773198f63e902cba9240658a261fe7fe1ab41d642b985147bbdd88ad5

See more details on using hashes here.

Provenance

The following attestation bundles were made for ctxlineage-0.2.0.tar.gz:

Publisher: release.yml on ctxlineage/ctxlineage

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file ctxlineage-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: ctxlineage-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 77.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for ctxlineage-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d8c20ee4cf0f393bda83c860ffff2c6279eca4325c19356ea60adda7d546e5e6
MD5 b687508995c13f36d3c5f788a357b176
BLAKE2b-256 f513044c9304d5ba45b21727a3e20c0a5efb20d083f6a49a5099340f26181100

See more details on using hashes here.

Provenance

The following attestation bundles were made for ctxlineage-0.2.0-py3-none-any.whl:

Publisher: release.yml on ctxlineage/ctxlineage

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page