Topic 26

The Context Budget

Budgeting

Give the context a budget in tokens and enforce it in code, the same way you would a memory limit on a service that must not be killed by the kernel. Without one, a ticket that happens to call three tools with large payloads triples in cost and degrades in quality, and nobody notices until the invoice arrives or the eval numbers move.

A budget is not the context window. The window is the provider's ceiling and it is much larger than the number you should be sending; the budget is the number you have decided is worth spending on a turn of this kind of work. Sundry's is 8,000 tokens per request against a window many times that, and the gap between those two numbers is deliberate.

Setting the Budget

Start from the tenants and allocate. Sundry's 8,000 splits into 1,100 fixed — the 420-token system prompt and 680 tokens of tool schemas — plus 800 reserved for the answer, leaving 6,100 that history and tool results have to share. The fixed part is fixed because changing it is a deploy; the reserve is untouchable; the 6,100 is where every decision in this topic actually happens.

The number itself comes from measurement rather than from the window size. Sundry set 8,000 because the 120-ticket eval set showed resolution flat between 6,000 and 12,000 tokens per request and falling below 5,000, and because a request budget of 8,000 keeps a typical run — a prefix that grows toward the cap over the eight or nine turns most resolved tickets take — inside the 60,000-token per-ticket ceiling the loop already enforced in Chapter 2. It does not keep the worst case inside: twelve turns sitting at the cap would be 96,000, and on that ticket the ceiling stops the run first, which is what it is for. Two constraints meeting at a round number is a better justification than "the window is big".

Write the allocation down as constants next to the loop, not as a comment in a design document. The reserve especially: a budget without an explicit reserve is a budget that spends the answer's room on one more tool result, and the failure shows up as a reply that stops mid-sentence.

Enforcement Points

There are three places to enforce, and they are not interchangeable. Cap each tool result at the tool boundary, which is the cheapest cut because a field trimmed there is trimmed on every remaining turn (Chapter 3). Cap the history, which is compaction and is Topic 27's subject. And check the total immediately before sending, which is the backstop that catches the combination nobody predicted — two medium results and a long thread, each individually within its own limit.

A budget with a defined action, checked before every call
BUDGET     = 8_000   # tokens in a single request
RESERVE    =   800   # room the answer must have
CAP_RESULT = 1_200   # per tool result, at the tool boundary

def fit(messages, tools):
    while count(messages, tools) + RESERVE > BUDGET:
        if not compactable(messages):
            return escalate_to_human(ticket_id, summary_so_far())
        messages = compact(messages)   # middle of the history first
    return messages

In words: before each call, count what you are about to send plus the room the answer needs. While that exceeds the budget, compact the history. If there is nothing left that may be compacted — the pinned material alone has overflowed the budget — hand the ticket to a person with the work so far attached, rather than sending a request you know is too big and letting the API decide what to lose.

That last clause is the whole point of enforcing in code. If you do not define the behaviour at the cap, the provider defines it for you, and it defines it by truncating at exactly the wrong moment: mid-decision, on the turn where the agent had finally assembled everything it needed. A defined action — compact, drop, escalate — is a designed outcome. Truncation is an outcome that arrives.

Over 8,000 with the answer's 800 reserved: what goes, in what order, and what never goes
A single tool result is over the 1,200-token capTrim at the boundary
Still over the budget once the results are capped, and the history has a middleCut the middle turns
The buyer's ticket text · the hard constraints · the structured state · the newest resultsPinned, never eligible
Dropping the oldest message, because it is the cheapest thing to reachThat is the ticket text
Nothing compactable is left and the pinned material alone has overflowedEscalate, work attached
No cap at all, so the provider decides what is lostTruncation, mid-decision

Priority Order When Cutting

Some material is pinned and never eligible for removal: the buyer's original ticket text, the hard constraints, the structured state recording what the agent has already done, and the results of the most recent tool calls. Those four are what the current decision is actually made of. Everything else is negotiable, and the first thing to go is the middle of a long history — the turns from the middle of the thread, which are also the region the model attends to least (Topic 28). Cutting where attention is already lowest is the one place where the cheap fix and the correct fix coincide.

The naive alternative is to drop the oldest messages, and it is wrong for a specific reason: the oldest message is the buyer's original ticket, and it is the most important thing in the buffer. Sundry shipped that version for a week. The symptom was an agent that, twenty turns into a damage claim, started answering a question about delivery timing — because the sentence saying the shelving unit arrived cracked had been evicted as the oldest item, and everything still in context was about the parcel.

Measuring the Composition

You cannot set a sensible budget without knowing what fills it. Here are two real Sundry contexts side by side: a typical order-status ticket, which is 55% of the queue, and a disputed damage claim that ran to eleven turns.

TenantTypical ticketBad ticketWhat moved
System prompt420420Nothing — it is fixed
Nine tool schemas680680Nothing — also fixed
History4501,150Nine more turns of conversation: +700
Tool results1,4508,000Two oversized payloads: +6,550
Total3,00010,250Over the 8,000 budget by 2,250

The bad ticket is not bad because the conversation was long. Nine extra turns of dialogue added 700 tokens; the whole overflow came from two payloads — a get_order response on an eleven-item order at 4,200 tokens and a track_parcel response carrying the carrier's entire scan history at 3,800. Tool results were 59% of the canonical ticket in Chapter 2 and are 78% here, and they are the tenant nobody was watching. Capping those two results at 1,200 each brings the same ticket to 4,650 tokens without removing a single turn of the conversation the buyer actually had.

The Budget as a Product Decision

A higher budget buys quality on hard tickets and costs money on every easy one. That makes it a routing decision rather than a global setting: the 55% of the queue that asks where a parcel is does not need 8,000 tokens, and the disputed damage claim with a seller in the middle may deserve 16,000. Sundry runs three budgets keyed on the triage classification — 4,000 for status, 8,000 for returns and refunds, 16,000 for disputes — and the blended cost per ticket came down because the largest class got the smallest number.

Set the budget per task type, and revisit it when Chapter 13 puts real cost figures next to real resolution figures. A single global budget is always wrong in both directions at once: generous enough to waste money on the easy majority, and tight enough to lose the hard minority that was worth spending on.

Common Mistakes
  • Having no cap and relying on the context window — the failure then arrives as truncation chosen by the provider, mid-decision, on the most expensive turn of the ticket.
  • Cutting the oldest messages blindly — the oldest message is the buyer's original ticket text, and evicting it produces an agent that confidently answers a question nobody asked.
  • Capping history while tool results stay unbounded — results were 59% of the canonical Sundry context and 78% of a bad one, so trimming the conversation attacks the smaller half.
  • Setting one budget for every task type — the order-status majority needs a fraction of what a disputed damage claim does, and one number overspends on 55% of the queue to underspend on 3%.
Best Practices
  • Enforce the budget in the loop with a defined action when it is exceeded — compact, then drop, then escalate with the work so far.
  • Pin the ticket text, the hard constraints and the structured state outside anything the pruner is allowed to touch.
  • Cap tool results at the tool boundary first, because a field trimmed there stays trimmed on every remaining turn.
  • Set the budget per task type from the triage classification, and revisit the numbers against the cost figures in Chapter 13.
Comparable toolscgroup memory limits a hard cap with a defined action on breachAdmission control refuse work rather than degrade everythingConnection pool sizing the same bounded-resource disciplineFramework context managers a fixed trimming policy you cannot inspect

Knowledge Check

A Sundry ticket assembles a request of 10,250 tokens against an 8,000-token budget. Where should the enforcement have happened first?

  • At the tool boundary, capping the two oversized results before they ever entered the buffer
  • In the history, compacting the nine turns of conversation that accumulated during the dispute
  • In the system prompt and tool schemas, which are sent unchanged on every one of those turns
  • In the 800-token answer reserve, which is the easiest allocation to reclaim under pressure

When a thread must be pruned, which material should go first and which must be pinned?

  • Cut the middle of the history; pin the ticket text, hard constraints and the most recent results
  • Cut the oldest messages first; pin whatever arrived on the last two turns of the conversation
  • Cut the most recent tool results; pin the earlier ones, which have already proved themselves useful
  • Trim every message proportionally; pin nothing, so no single part of the thread dominates the buffer

Why is "trim the system prompt" the wrong first move when a Sundry context overflows?

  • It is 420 fixed tokens, while the tenant that actually overflowed reached 8,000 and grows with the work
  • Shortening the prompt weakens instruction adherence, which costs more resolution than the tokens are worth
  • Providers enforce a minimum system prompt length, so trimming it below that threshold breaks the request
  • It invalidates the prompt cache, which raises the cost of the ticket by more than the overflow would have

Sundry runs 4,000 tokens for status tickets, 8,000 for returns and 16,000 for disputes. What does that buy over a single 8,000 budget?

  • The 55% of tickets that ask about status stop overpaying, and disputes stop being cut short
  • Lower latency on every ticket, because a larger allowance lets the model answer in fewer turns
  • A higher effective turn limit on disputes, since the loop can run longer before hitting its ceiling
  • Compaction becomes unnecessary, because each class of ticket now fits inside its own allocation

You got correct