DEV Community

Royal Simpson Pinto
Royal Simpson Pinto

Posted on

Profiling an AI agent's context window: where the tokens actually go

Token dashboards tell you the bill. They do not tell you why the bill is that size. When a coding agent gets slow, expensive, and a little dumb, it is usually because its context window has quietly filled with junk: the same file read six times, a 12k-token tool result that mattered for exactly one turn, tool schemas re-sent on every single step. You can see the total go up. You cannot see where the tokens went, so you cannot delete anything with confidence.

I wanted a profiler for that. Not a chat UI, not a live proxy, just a tool I could point at a session transcript and ask: what is in this context window, and how much of it is avoidable? That is ctxlens.

The core idea

ctxlens treats an agent session the way a CPU profiler treats a program. A profiler does not judge whether your code is good; it tells you where the time went so you know where to look. ctxlens does the same for tokens. It parses a session transcript, attributes every message to a segment, counts tokens per segment and per turn, and then runs rule-based checks to flag the parts that are genuinely wasted.

Every message lands in one of these buckets: system, tool_definitions, user, assistant, thinking, tool_call, tool_result. Once every token has a home, the interesting questions become answerable. Which segment dominates? When did the context spike? What is being paid for on every turn versus once?

It reads Claude Code JSONL sessions (the ones under ~/.claude/projects/*/*.jsonl), OpenAI/Codex rollout sessions, and generic OpenAI chat arrays. The format is auto-detected by sniffing the file, and you can force it with --format if you need to.

How it works

The basic run is one command:

pip install ctxlens-cli
ctxlens analyze session.jsonl
Enter fullscreen mode Exit fullscreen mode

You get a summary panel, a breakdown of context composition by segment, a couple of sparklines for how context grew over the run, and a list of recommendations. The composition view is the part I reach for first, because it immediately answers "what is this window made of":

Context composition by segment
 Segment       Tokens     %  Msgs  Share
 tool result    6,204  49.7    22  ██████████████·······
 assistant      2,110  16.9    14  ██████···············
 system         1,540  12.3     1  ████·················
Enter fullscreen mode Exit fullscreen mode

Tool results eating half the window is the single most common thing I see. Which leads to the second half of the tool: the waste report.

waste_ratio = total_waste / total_tokens, and total waste is the sum of four disjoint sources:

  • Duplicate tokens. The same file or tool result appearing more than once, matched either by reference (for example Read:file_path=config.py) or by exact body. Every copy after the first is counted as wasted.
  • Tool-result bloat. Tokens in a tool result above a per-result cap (--tool-result-cap, default 400). Only the overage counts, and each unique body is charged once so a repeated giant result is not double-counted here and again as a duplicate.
  • Stale tool outputs. When the same reference is read more than once and a later read supersedes an earlier one, the older superseded copies are dead weight still sitting in context.
  • Tool-definition overage. Tool schema tokens above a budget (--tool-def-budget, default 800). This one stings because you pay it on every turn.

Each finding carries a severity and an estimated token saving, so recommendations read like "'Read:file_path=config.py' appears 6 times, ~2,410 tokens" rather than generic advice to "manage your context better." The estimate is exactly the arithmetic above, not a guess.

Because it is all deterministic, it slots into CI. You can fail a build when a captured session wastes too much:

ctxlens analyze session.jsonl --fail-over-ratio 0.30
Enter fullscreen mode Exit fullscreen mode

Exit code 0 is fine, 2 means the threshold was exceeded, 1 is an error. Add --json for machine-readable output, or diff a baseline against a candidate with ctxlens diff before.jsonl after.jsonl to catch regressions when you change a prompt or a tool. There is also an HTML reporter via ctxlens report session.jsonl --html -o report.html for when you want to actually look at it.

On counting: by default ctxlens uses a deterministic heuristic tokenizer with no network calls and no heavy dependencies, which is deliberate. For relative profiling and CI thresholds you mostly care about proportions and trends, and a stable heuristic gives you reproducible numbers everywhere. If you install tiktoken, --tokenizer auto picks it up and you get exact BPE counts. The whole thing has 55 tests covering the parsers, analysis, tokenizers, reporters, and CLI.

One honest limitation

The heuristic tokenizer is an approximation, and it should be treated as one. Its token counts will not match your provider's billing exactly, so the absolute numbers in the summary panel are estimates unless you install tiktoken. What stays reliable without tiktoken is the shape of the picture: which segment dominates, which references repeat, where the spikes are. If you need the reported token figures to line up with an actual invoice, install the extra and use exact counts. I would rather ship a tool that is honest about being a fast approximation by default than one that implies billing-grade precision it does not have.

Closing

ctxlens started because I was tired of guessing which part of a bloated agent session was safe to trim. Having the window broken down by segment, with the duplicates and stale reads called out by name and token count, turned that from a hunch into an edit. If you run agents and your context windows feel heavier than they should, point it at a real session and see what falls out.

Top comments (0)