Skip to content
Alexander Holbreich
Go back

What's actually in your agent's context

An agent’s context contains more than the system prompt: tool definitions, instruction files, conversation history and tool results.

This post examines how harnesses assemble that context, how it becomes model input, and what fills it during real sessions.

Table of Contents

Open Table of Contents

From API request to model input

The harness sends messages and tool definitions as JSON, as shown in Agent loop anatomy. This is the API’s request format, not the model’s input format.

In the local setup used here, the server parses that request and applies a model-specific chat template. The template arranges the messages and tool definitions into text with role and turn markers. That text is then converted into token IDs, which are what the model processes.

The stateless API used by the loop requires the conversation history to be included with each request. The input grows as messages and tool results accumulate. Prefix caching can reuse earlier computation, so sending the history again does not necessarily mean evaluating every token again.

Context sources and lifecycle

The model input comes from five sources, not just the agent loop.

Fig. 1The writers' deskOne stack, five writers. Time runs down the page.
The writers' deskOne stack sits in the centre and grows downward over time. At session start, code writes tool schemas, the core prompt and the environment, and files on disk supply instruction files and the skills index. A double rule marks the end of this head. On turn 1 you write the task, the model writes thinking and a tool call, and a tool writes the result. Turn 2 appends another tool call and a larger result. Later a harness reminder and more calls are appended. Every write lands at the bottom of the stack. At compaction the old tail is swept away and replaced by a short summary, followed by the kept recent messages and your next message. The head above the double rule never changes.session startturn 1turn 2latercompactionCodeDiskYouModelToolstool schemascore promptinstruction filesskills indexenvironmentyour taskthinking, tool callread resulttool callbash resultlate remindertool callread resultsummarykept recent tailyour next messagehead abovetail belowold tail swept awaysummary left behindthe head, unchanged
  • Harness code
  • Instruction files + skills index
  • User messages
  • The model
  • Tool results

At session start, code and disk write the top of the string. Code supplies the tool schemas, the core prompt and whatever it knows about the environment. Disk supplies your instruction files and the list of available skills. Nothing from the conversation is in there yet.

Ordinary turns extend the tail. You write a message. The model responds, perhaps with thinking and a tool call. A tool writes its result. Harnesses can also append reminders when files change, rather than rewriting the earlier context.

Compaction interrupts that growth: the harness replaces older history in the next request with a summary, usually keeping recent messages intact. The full history can remain on disk even when it is no longer sent to the model.

Context structure: head and tail

That relatively stable setup is the head. The accumulated conversation below it is the tail.

Fig. 2The context barWire order, to scale. The double rule between environment and summary is the useful line.
The context barThe top strip shows the regions of one real session in wire order, to scale in a 272,000-token window. Tool schemas, core prompt, instruction files, skills index and environment are a thin slice at the left, the head. A double rule separates it from the tail: summary, conversation and late reminders, then free space and the output reserve. Tool results dominate the conversation. Under each region four facts are listed: who writes it, how long it lives, its format and its typical size. The lower half shows the head of seven harnesses at one scale: Mini-SWE-Agent about 0.9K, Pi 1.3K, Aider 2.3K, OpenCode 2.2K or more, Gemini CLI 10 to 12K, Codex 13K, and Claude Code somewhere between 18K and 65K, with a tick at 32K for one session of mine. Filled dots show where instruction files land: in the system region for Pi, OpenCode and Gemini global files, and in an early user-role message for Gemini project files, Codex and Claude Code.One session, in wire ordertool resultsfreereservethe headTool schemascodesessionJSON0 to 29K+Core promptcodesessionprose0 to 6KInstructionsdisksessionmarkdownyou decideSkills indexdisksessionnamesabout 1KEnvironmentcodesessiontagsunder 1KSummarymodel/codeto nextmarkdownabout 1.9KConversationall fiveto compactmessagesmost of itReminderscodeappendedtagssmallFree, reservenobodyuntil usedthe restwriter · lifetime · format · typical sizeThe head, per harness010K30K50K70Ksystemregionearlyuser turninstruction files land inMini-SWE-AgentPi 1.5.0Aideruser adds filesOpenCodeGemini CLICodex 0.167Claude Codemy session, 32K
  • Harness code
  • Instruction files + skills index
  • User messages
  • The model
  • Tool results
Top: one Pi session of mine at its last turn; the 272K window is inferred from where it compacted and Pi's 16,384 reserve; hatched thinking is what the session stored, what is resent varies by provider. Bottom: sizes from pinned source revisions in the Qwen tokenizer, not billing counts, November 2026. Codex measured with its own prompt dump on an empty directory. Hatching marks a range or an uncertain size; Claude Code captures disagree; the tick is one session of mine, from its own /context estimate.

The head is what people fight over. But look at the top strip, which is one of my real sessions at its last turn: the head is a sliver.

The table gives each region a writer, a lifetime and a format. The head usually persists across turns. A summary carries older context until the next compaction. A reminder becomes part of the history and can later be summarized too.

From session records to model tokens

Take a small conversation: the user asks which port the dev server uses, the assistant calls read_file on package.json, and the tool returns its contents. The figure shows four representations of that exchange, grouped by where they belong.

Fig. 3aFrom session record to model inputClient-side formats on the left; inference-side representations on the right.
From session record to model inputFour columns are grouped by location. A neutral background marks two client-side formats: an illustrative Claude Code session record and the separate JSON request used in a local test. A tinted background marks the inference side: prompt text constructed in llama.cpp server memory and the Qwen token IDs passed to the model. The dashed connection between the client columns is a comparison, not a captured conversion. The solid connections on the right follow the local request through rendering and tokenization. In this Qwen template the tool result becomes a user turn and the call ID is omitted. Tokenization encodes the text and role markers rather than removing their structure.Client-side formatsInference side · llama.cpp + QwenSession recordClaude Code keys; illustrative"uuid" "parentUuid""timestamp" "cwd""gitBranch" "version""toolInputStreamMs""message": { "role": "assistant", "content": [{ "type": "tool_use", "name": "read_file", "id": "toolu_1", "input": {"path": …} }]}API requestlocal test; JSON excerpt{ "role": "assistant", "tool_calls": [{ "id": "call_1", "type": "function", "function": { "name": "read_file", "arguments": "{\"path\":\"package.json\"}" }}]}{ "role": "tool", "tool_call_id": "call_1", "content": "{…}" }Prompt textassembled in server memory<|im_start|>assistant…<tool_call><function=read_file><parameter=path>package.json</parameter></function></tool_call><|im_end|><|im_start|>user<tool_response>{…}</tool_response><|im_end|>Token IDsQwen tokenizer; model input248045 74455 198…248058 19827 1628 86779 2378 29198 27 15704 79114 29198 1662 4182 198510 15704 29 198510 1628 29 198248059 248046 198248045 846 198248066 198…248067 248046format comparison,not a captured conversionQwen template:tool → user; call ID omittedtext and role markersencoded, not discarded
Composite example, not a captured Claude Code-to-Qwen pipeline. The first column uses Claude Code transcript keys with invented values; the dashed link compares formats. The local JSON request, Qwen3.6 prompt and token IDs were reproduced through llama.cpp b12110 on 2026-11-09. Excerpts omit reasoning and other prompt content.

The left pair compares client-side formats: an illustrative Claude Code session record and the JSON request sent to llama.cpp for a separate local test. They are not a captured Claude Code-to-Qwen pipeline. A harness can store transcript metadata without including it in the API request.

The right pair follows that local request through llama.cpp. The server parses the JSON and applies the model’s chat template to construct a prompt string in memory. The XML-like text means “call read_file with path set to package.json.” Here it represents an earlier assistant call reconstructed as part of the history. The tokenizer encodes the prompt, including its role and turn markers, as token IDs.

The next figure compares that same intended call across five model templates. These are alternative encodings, not further stages in the pipeline.

Fig. 3bTool-call syntax by model templateEach row represents the same call: read_file(path="package.json").
Tool-call syntax by model templateFive model templates represent the same request to read package.json using different tool-call syntax. Qwen wraps the function and its path parameter in XML-like tags. GPT-OSS uses a Harmony assistant message addressed to functions.read_file, with JSON arguments in the commentary channel. DeepSeek uses DSML tool_calls, invoke and parameter tags. GLM uses tool_call, arg_key and arg_value tags. Llama uses a JSON object with name and parameters. These are alternative serializations of one call, not successive stages or different harnesses.Model / templateSerialized tool-call excerptQwen3.6<tool_call><function=read_file><parameter=path>package.json</parameter></function></tool_call>GPT-OSS · Harmony<|start|>assistant to=functions.read_file<|channel|>commentary json<|message|>{"path":"package.json"}<|call|>DeepSeek V4 · DSML<|DSML|tool_calls><|DSML|invoke name="read_file"><|DSML|parameter name="path" string="true">package.json</|DSML|parameter></|DSML|invoke></|DSML|tool_calls>GLM-4.7-Flash<tool_call>read_file<arg_key>path</arg_key><arg_value>package.json</arg_value></tool_call>Llama 3.3{"name":"read_file","parameters":{"path":"package.json"}}
Excerpts of chat-template output, not API request formats or different harnesses. Qwen was rendered by local llama.cpp b12110; the other examples were rendered with sandboxed Jinja from templates pinned at cd959bae in llama.cpp. Line wrapping and whitespace are simplified. These are not captures of hosted API prompts.

Chat templates usually ship with an open-weight model’s tokenizer files or model package; in this local GGUF, the template is stored as tokenizer.chat_template metadata and applied by llama.cpp. Hosted services generally keep this formatting server-side, so clients see structured messages rather than the rendered prompt.

Fig. 4 shows input prepared for the next model call, not a generated response. The conversation is constructed: the earlier assistant reasoning and tool call are included as history.

Tokenization happens behind the API. Here llama.cpp tokenizes the rendered prompt using the vocabulary and tokenizer metadata bundled in the Qwen GGUF model file. The tokenizer itself is not part of the prompt. The harness normally sends messages and tools, not token IDs, although it may count tokens to manage its context budget. On the return path, the server decodes generated token IDs and parses tool calls into the API response.

Fig. 4Tokenized input to QwenConstructed conversation history, prepared for the next model call.

377 input tokens, in prompt order.

Show 272 setup tokensHide 272 setup tokensTool definition, tool-use instructions and opening markers
<|im_start|>system⏎#·Tools⏎⏎You·have·access·to·the·following·functions:⏎⏎<tools>⏎{"type":·"function",·"function":·{"name":·"read_file",·"description":·"Read·a·UTF-8·text·file·from·the·workspace.",·"parameters":·{"type":·"object",·"properties":·{"path":·{"type":·"string",·"description":·"Path·relative·to·the·repository·root."}},·"required":·["path"]}}}⏎</tools>⏎⏎If·you·choose·to·call·a·function·ONLY·reply·in·the·following·format·with·NO·suffix:⏎⏎<tool_call>⏎<function=example_function_name>⏎<parameter=example_parameter_1>⏎value_1⏎</parameter>⏎<parameter=example_parameter_2>⏎This·is·the·value·for·the·second·parameter⏎that·can·span⏎multiple·lines⏎</parameter>⏎</function>⏎</tool_call>⏎⏎<IMPORTANT>⏎Reminder:⏎-·Function·calls·MUST·follow·the·specified·format:·an·inner·<function=...></function>·block·must·be·nested·within·<tool_call></tool_call>·XML·tags⏎-·Required·parameters·MUST·be·specified⏎-·You·may·provide·optional·reasoning·for·your·function·call·in·natural·language·BEFORE·the·function·call,·but·NOT·after⏎-·If·there·is·no·function·call·available,·answer·the·question·like·normal·with·your·current·knowledge·and·do·not·tell·the·user·about·function·calls⏎</IMPORTANT>⏎⏎
You·are·a·coding·agent.·Be·brief.<|im_end|>⏎<|im_start|>user⏎Which·port·does·the·dev·server·use?<|im_end|>⏎<|im_start|>assistant⏎<think>⏎The·port·is·probably·set·in·package.json,·so·I·will·read·it.⏎</think>⏎⏎<tool_call>⏎<function=read_file>⏎<parameter=path>⏎package.json⏎</parameter>⏎</function>⏎</tool_call><|im_end|>⏎<|im_start|>user⏎<tool_response>⏎{"scripts":·{"dev":·"vite·--port·5173"}}⏎</tool_response><|im_end|>⏎<|im_start|>assistant⏎<think>⏎

Hover or focus a tile to see its token ID and position in the input.

The same input, as regions
  1. Tool setup272 tokens
  2. System prompt11 tokens
  3. User message13 tokens
  4. Assistant history50 tokens
  5. Tool result26 tokens
  6. Next assistant prefix5 tokens

Top to bottom: prompt order. Counts include markers; bands are not to scale.

  • Harness code
  • User messages
  • The model
  • Tool results
Qwen3.6-35B-A3B tokenizer from the UD-Q4_K_XL GGUF, run by llama.cpp b12110; measured 2026-11-09. ⏎ is a newline, · is a space; an outline marks a reserved token.

Tool definitions and tool-use instructions are prompt content too, not overhead introduced by the tokenizer. The collapsed setup preserves those tokens without making you read through all of them.

Context assembly across harnesses

Harnesses agree on the regions and disagree on the details. The lower half of Fig. 2 shows the two details that matter most.

The first is how much is in the head.

  • Pi starts with 1.3K tokens and four tools.
  • Gemini CLI starts around 10K to 12K.
  • Codex measured 13K on my machine, with its own prompt dump, in an empty directory.
  • For Claude Code I can’t give you one number: published captures range from 18K to 65K depending on version and MCP setup.

The source-derived estimates use one tokenizer as a ruler, not as a billing count. Claude Code’s published captures use its own accounting, so the comparison is approximate.

The second is where your instruction files land. The dots in the figure show it. Pi and OpenCode put them in the system region. Codex and Claude Code put them in an early user-role message. Gemini CLI does both: global memory in the system instruction, project memory in the first user message.

Claude Code will show you this split itself. Type /context:

Fig. 5/context, read as regionsClaude Code's own estimate of one session of mine.
/context, read as regionsA Claude Code /context screen for one session: 278.1 thousand of 1 million tokens used. The system prompt, system tools, MCP server instructions, memory files and skills together are the head, 32.3 thousand tokens. Messages are the tail, 245.8 thousand tokens. Free space is 688.9 thousand and the autocompact buffer 33 thousand, the reserve.the head, 32.3Kwritten by code and diskthe tail, 245.8Kfree space and reserve
Claude Code with Opus 5.5, November 2026, the tool's own token estimate. Head is everything above Messages: 3.6K + 19.8K + 0.7K + 1.7K + 6.5K = 32.3K. Deferred tool definitions (about 27K if all were loaded) are not part of the 278.1K total.

That’s the session that helped write this post, so its tail is large. Its head is 32.3K tokens. Look at the line it doesn’t count, too: another 27K tokens of tool definitions are deferred, and load only when needed.

Lazy loading reduces the initial context size. Skills keep only an index in the head: a name and a description for each. In one repository of mine, 19 skills cost 1,136 tokens as an index, against 29,257 tokens if the bodies were loaded. When the model decides it needs one, it reads the file. So a loaded skill is literally a tool result, sitting in the tail like any other.

Context composition in 264 Pi sessions

These measurements reflect my own workflow. Alongside coding, I work extensively with file-based knowledge bases, or “second brains”.

Here are three of my own Pi sessions as drill cores, to one scale. Time runs upward. Each message lays down one layer in the colour of whoever wrote it.

Fig. 6The core sampleThree real sessions, to one scale. Each message lays down a layer.
The core sampleThree real sessions drawn as drill cores to one scale, newest layers on top. A ruler runs up the left in tokens. The short core is 6 turns and 27K tokens. The median core is 23 turns and 56K tokens, and is repeated six times larger so that single messages can be seen. The long core is 238 turns with two compactions, at turns 107 and 211: below each jagged line the erased layers are ghosted and a thin summary seam sits above them. Only 149K tokens are still live at the end. In all three cores the thick bands are tool results and the thin bands are the model's own text. Tool results are 46, 59 and 85 percent of the context at the end of the short, median and long sessions.tokens laid down100K200K300K400K500K0median session, ×6turn 1turn 5turn 10turn 15turn 20file written, 5.7Klargest thinking, 1.6Klargest read, 1.9K tokenslargest bash, 11Kmy task, 0.1Kbedrock: head, 4.5Kshort6 turns27K, tools 46%median23 turns56K, tools 59%long, compacted twice238 turns149K live at the end, tools 85%still in context149K livecompaction, turn 107at 259K, summary seamwritten by codecompaction, turn 211at 229K, summary seamwritten by codeeroded: sentbefore, gone nowbedrock: head, 5.5K
  • Harness code
  • Instruction files + skills index
  • User messages
  • The model
  • Tool results
Pi sessions of mine, chosen because they are typical for their length, not at random. Layer sizes: characters converted with calibrated per-type ratios, then scaled so the live stack matches the provider-reported total at the last turn (scale factors 0.903, 0.805, 0.846). Numbers only, measured 2026-11-09.

The median session is mostly read and bash output in ochre. Violet marks the model’s thinking and tool calls; pink marks my messages. Green is the instruction files and skills index loaded at session start. Reading a note from a second brain is a tool result, so it appears in ochre, not green.

Across my 264 sessions up to 2026-11-08 (15,468 assistant turns, mixed providers), read and bash produced 92% of the tool result characters.

Who wrote the end-of-session context, on average:

Fig. 7Who wrote it, and where the head wentLeft: share of context at session end. Right: median context by turn.
Who wrote it, and where the head wentA single bar shows who wrote the context at session end: tool results 65.5 percent, the model 17.7 percent, my messages 9.6 percent, and code plus disk 7.2 percent. Seven columns show the median context size at turns 1, 2, 5, 10, 20, 50 and 100: 7.6K, 12K, 27K, 45K, 77K, 133K and 188K tokens. A thin line across them marks vanilla Pi's head of 1.3K and a dashed line the 7.6K first-turn median. Fewer sessions reach later turns: 264 at turn 1 and 40 at turn 100.Who wrote it65.5%tool results17.7%the model9.6%my messages7.2%code + diskContext by turn, p507.6Kturn 126412Kturn 226127Kturn 524045Kturn 1018377Kturn 20143133Kturn 5078188Kturn 10040sessions alivedashed: my first-turn median, 7.6Ksolid: vanilla Pihead, 1.3K
My Pi sessions, 2026-08-28 to 2026-11-08, mixed providers: composition is the mean over 185 sessions with 5+ turns; columns are p50 of provider-reported context over 264 sessions. Later columns only cover sessions that lived that long, and compaction resets are included.

On average, tool results make up 65.5% of my end-of-session context. My own messages account for under 10%.

On the right, the head becomes a smaller share of a growing context. Vanilla Pi’s 1.3K head is the thin line. My real first-turn median, with extensions and skills loaded, is 7.6K. By turn 10 the median context is 45K, so even that bigger head is a sixth of it. By turn 100 it’s 4%, and only 40 of my 264 sessions get there.

So if you want a smaller context, the system prompt is rarely where to look. Look at what your tools return.

A larger context is not necessarily a more useful one. Long-context evaluations show that models can become less reliable as input grows, and that the position of relevant information matters. But these session measurements describe what occupies the context, not how well the model uses it. Tool output dominating the history is a reason to examine its relevance, not proof that it makes the agent less capable.

Compaction and information loss

Compaction makes room for new work by replacing older history with a shorter account of it. The growing tail forces a choice: what must the next request still contain?

In Pi, the harness triggers this when its context estimate crosses a configured threshold below the model’s window limit, or when the user requests it. This manages capacity; it does not detect declining model quality.

By default, Pi keeps recent messages unchanged and makes a separate model call to summarize older history. The next request contains the head, the summary and the retained recent history, rather than the full conversation.

In my 264 sessions up to 2026-11-08, compaction happened 62 times across 36 sessions. Most ended without it. The long core in Fig. 6 shows older layers ghosted below each compaction boundary, with a thin summary seam above. That session used pi-vcc to extract its summaries without a model call.

Missing from context does not mean deleted from storage. Pi keeps the original session entries on disk, but omitted details must be retrieved to reach the model again. A summary can preserve a decision while dropping its rationale, or omit a constraint entirely. These measurements do not tell us how often that caused mistakes.

Tool results fill most of my history, but volume alone does not tell us what to preserve. Code and notes can often be read again. Requirements stated only in conversation need to survive in the retained context or a record the agent knows to retrieve.

Context processing costs

What remains in context also has a processing cost. The following comparison estimates that cost for a median session: 23 turns on a cloud model, priced at list.

Fig. 8The receiptA median 23-turn session, and the same work on my own GPU.
The receiptTwo till receipts. A median 23-turn session on cloud list prices: written once, tool results 27 cents, the model 7 cents, you 4 cents, code and disk 3 cents; re-reading everything on every turn 21 cents; model output 30 cents; total 93 cents. Hypothetical repricing with Opus 5.5 list prices of 8 November 2026. The second receipt, for the local model: 960 MiB of memory reserved for a 48K context, about zero milliseconds for a token already in the slot, about 4 milliseconds for a new token and about 36 milliseconds for an output token.SESSION RECEIPT23 turns · median session · cloudWRITTEN ONCEtool results 65.5%$0.27the model 17.7%$0.07you 9.6%$0.04code + disk 7.2%$0.03RE-READ, EVERY TURN$0.21MODEL OUTPUT$0.30TOTAL$0.93Hypothetical. List prices of 2026-11-08.Every cache hit. Not what was paid.Items rounded.LOCAL RECEIPTQwen3.6-35B · RTX 3060 · no invoiceRESERVED, FIXEDmemory for 48K ctx960 MiBPER TOKENalready in slot~0 msnew~4 msoutput~36 msllama.cpp b12110, 144 requests.Cold starts are where the waiting lives.
Left: hypothetical repricing of a median 23-turn session at Opus 5.5 list prices of 2026-11-08 with every cache hit, not what was paid. Region shares from the session-end composition; items are rounded so they don't sum exactly. Right: Qwen3.6-35B-A3B on an RTX 3060, llama.cpp b12110, 144 requests. The next post is about why these two receipts disagree.

In the cloud estimate, the four written-once lines together cost more than re-reading the history. That depends on the assumed cache hits; it is hypothetical repricing, not my actual bill. The local measurements express the cost in reserved memory and processing time instead.

Cloud pricing and local inference reflect different costs. The next post examines the role of caching.

Sources