Smith::Context
Keeps the conversation transcript from growing past the provider's context window.
The invariant everything here is built around: every tool_use block must
keep exactly one matching tool_result, and vice versa. Anthropic and
OpenAI reject a request outright if that pairing is broken, so compaction
may shorten content but must never orphan half a pair.
Constants
Fractions of the budget: compaction acts at the trigger and compacts down
to the target. Expressed as fractions rather than token counts so raising
max_tokens for a wider-window model scales both.
80 rather than 85 because a single repo-wide grep is tens of thousands of tokens, and at 85 such a result lands over the budget before compaction was ever consulted. 50 rather than 60 because each compaction costs a full prompt-cache creation charge on the surviving prefix — halving how often it runs is worth more than the marginal fidelity.
What an attachment is assumed to cost. Anthropic charges an image at
roughly (width x height) / 750 tokens and caps it around 1600, which is
the number taken here — smith does not parse image dimensions, and
guessing low is the expensive mistake. A PDF is page-count-dependent and
regularly worse, so it gets its own, larger figure.
Replaces an attachment once its turn is over. It names the file, so the model can ask for it again rather than being left with a hole it cannot see.
What a stale tool result gets shortened to: enough to keep a call's identity, not its content. The old floor was 2 KB, which sounds modest until a long session holds three hundred of them — 150k tokens, more than the whole budget. At 512 bytes the same tail is ~37k, and as stubs ~6k.
Bounds on the calibration ratio. Below 1.0 is implausible for a bytes // 4 estimate; above 2.0 is a measurement error, not a context window.
How fast the calibration ratio follows what the provider reports. Low enough that a single outlier cannot put a session into permanent compaction.
How many real turns are left alone. One turn is a user message and everything the agent did about it, so three of them cover "read the file, edit it, run the tests" whole. A constant rather than config: an extra knob here buys nothing.
Tools whose repeated call with identical arguments makes the earlier
result redundant. bash is deliberately absent: running make test
twice is not a duplicate, it is a before and an after, and collapsing the
earlier one deletes exactly the comparison the model is making. The same
goes for anything that observes changing state.
Both the writer of a shortened result and whatever later has to recognise one need to agree on these, so they are named rather than inlined.
Class methods
Brings messages down to budget's target if it can, in escalating
stages — and does nothing at all below the trigger.
The trigger and the target being different numbers is the whole design: compacting to the first point that fits means the next assistant turn pushes the history straight back over, and compaction runs again.
summarize receives the prefix being dropped and returns a replacement
text. It is a callback rather than a provider so this whole module stays
testable without a network.
The same heuristic applied to a plain string, so a breakdown of the context and the compaction decision cannot drift apart.
Rough heuristic, not an exact count. The point is to react before the
limit, not to spend the last token of it — and Budget#ratio corrects the
systematic part of the error against what the provider reports back.
signature is counted because Anthropic gets it back byte for byte
(see LLM::Anthropic), which makes it part of the request whether or not
anyone reads it.
The lowest index of a User message such that everything from there on
fits target. User messages only ever start a turn (Agent#send), so
cutting there can never orphan a tool pair. Returns 0 when no such cut
exists — the caller then leaves the history alone rather than breaking it.