What is Context Isolation?
Context Isolation is an AI design pattern for LLM apps and agents, in the Context and knowledge group. Run subtasks in separate contexts and bring back only the result.
Isolamento de Contexto
Context Isolation is an AI design pattern for LLM apps and agents, in the Context and knowledge group. Run subtasks in separate contexts and bring back only the result.
Run subtasks in separate contexts and bring back only the result. It is for systems where the material read, the intermediate reasoning, or one step’s instructions would get in the way of the others if they shared a context.
Separate rooms in a newsroom: the reporters do not work at the editor’s desk, and only the finished story crosses the door.
Sofia maintains an automated pull request reviewer. To review well, it needs to read the diff, the touched files, related tests, and the file’s commit history. All of that went into the same context as the review.
Two effects appeared. The first was predictable: the context passed 100,000 tokens on medium PRs, and reviews became expensive and slow. The second was less obvious: the reviewer began commenting on code that was not in the diff. It had read the whole file and pointed out old problems that were not the PR’s concern. Useful comments got lost among the out-of-scope ones.
Summarizing the material did not help. The reviewer did need to look at files and tests, but only to answer specific questions (“does this function have tests?”, “who else calls this method?”), not to carry everything while writing the review.
This is a sibling of Context Rot, with a difference: the heavy material should never have entered the writing context. Compacting afterwards cleans what already leaked in. Isolating keeps it out.
The question this pattern answers: how does one step use a lot of material without that material contaminating the rest of the work?
Give each subtask its own context, created from scratch, with only what it needs: a self-contained instruction, the read tools, and, when needed, a piece of the material. When the subtask ends, the context is thrown away. Only a short result in an agreed format comes back.
The main context never sees the raw material. It sees questions and answers: “does apply_discount have tests?” and “yes, in tests/test_pricing.py, lines 40 to 88, covering zero discount and discount larger than the total”.
Isolation is the principle; Subagent Delegation is the most common way to apply it, but it also applies to simple calls with no loop and to the steps of a workflow.
Sofia’s PR changes only pricing.py. The reviewer needs to know whether apply_discount has a test, not the whole test file on the desk.
The main context sends a self-contained question:
Question: does apply_discount in src/pricing.py, changed in this PR, have tests?
Scope: answer only about coverage of this function. Ignore old failures in other functions.
Format: { "answer": string, "evidence": [{ "path": string, "lines": string }] }
The isolated context reads what it needs and returns:
{
"answer": "Yes. tests/test_pricing.py covers zero discount and discount larger than the total. I did not see a negative-discount case.",
"evidence": [{ "path": "tests/test_pricing.py", "lines": "40-88" }]
}
The main context gets about 80 words and cites the lines. The thousands of tokens in the test file died in the isolated context.
An illustrative account. Without isolation, a medium PR carries a diff (3,000 tokens) plus three files (45,000) on each of 8 turns: about 384,000 input tokens. With isolation, the main context keeps the diff and the short answers (say 8,000 per turn, 64,000 total) and the isolated contexts pay for the read once. Total cost may even rise if there are many questions. What drops, and that is the point, is the context that writes the review, and with it the out-of-scope comments.
The instruction has to carry the constraints. The isolated context did not see the diff. If the question omits “ignore old failures”, it reports the test that has been missing for two years.
flowchart TD
M(["1. Main context: review"]):::llm --> Q1["2. Self-contained question"]:::tool
Q1 --> C1(["3. Isolated context A"]):::llm
C1 --> F[("4. Files, tests, history")]:::store
C1 -->|"5. short answer"| M
M --> Q2["2. Another question"]:::tool
Q2 --> C2(["3. Isolated context B"]):::llm
C2 --> F
C2 -->|"5. short answer"| M
M --> OUT(["6. Focused result"]):::io
schema Answer { answer: string, evidence: list[{ path: string, lines: string }] }
function isolated(question, tools, budget):
# 3. fresh context: nothing from the main history enters here
ctx = new_context(system=READER_PROMPT, messages=[question])
result = run_agent(ctx, tools=tools, budget=budget, output=Answer) # 4
trace.span("isolated", question, tokens=ctx.tokens)
return result # 5. ctx is discarded
function review(diff):
state.messages = [diff]
while budget.check():
reply = llm_with_tools(REVIEW_PROMPT, [ask_codebase], state) # 1
if reply.is_final:
return reply.text # 6
for call in reply.tool_calls: # 2
answer = isolated(call.args.question, READ_TOOLS, budget.share(10))
state.add(tool_result(call, answer))
evidence in code.while True is Unbounded Loop in the next room. The cap applies to the tree.| Aspect | What happens | How it shows up in the trace |
|---|---|---|
| Calls | One or more per isolated question | child isolated spans |
| Total cost | May rise, because isolated contexts reread material | sum of tokens across isolated spans |
| Main cost | Drops sharply, because the context stays small | stable input tokens on the main context |
| Latency | Independent questions can run in parallel | overlapping isolated spans |
| Failure: incomplete question | The isolated context misses a constraint the main context knew | a correct answer to the wrong question |
| Failure: answer without evidence | The main context trusts an invented claim | empty evidence field |
{ "diff": "fixtures/pr-512.diff", "expected_comments": 2, "max_out_of_scope_comments": 0 }
Measure both costs. A team that watches only the total bill sometimes undoes the pattern, without seeing that the missing out-of-scope comments were worth more.
Isolated contexts are the right place to read untrusted content. Give them read tools only, and treat their answer as data in the main context.
Greshake et al. (2023) documented indirect prompt injection: hidden instructions in a page or an email the model reads. If that page shares a context with side-effect tools, hostile text becomes an order. Isolating the read does not replace the gate on the tools, but it cuts the most common channel.
| Pros | Cons |
|---|---|
| A small, focused main context | Total cost may rise |
| Material read does not contaminate the reasoning | The question must be self-contained |
| Parts get their own instructions and tools | More spans to debug |
| Hostile content is read far from side-effect tools | A short answer may lose useful detail |
The tabs expose an ask_codebase tool that answers questions in a fresh context, without the review’s history. Python and TypeScript clear the message list by hand. LangChain does the same with a create_agent that receives only the question: the reviewer’s history never enters the dict.
Python. Use this when the review runs in Python and isolated reading is a regular function.
import anthropic
from pydantic import BaseModel
client = anthropic.Anthropic()
class Evidence(BaseModel):
path: str
lines: str
class Answer(BaseModel):
answer: str
evidence: list[Evidence]
READER = "You answer one question about the repository using only the read tools. Be brief and cite file and lines."
def ask_codebase(question: str, max_steps: int = 10) -> Answer:
messages = [{"role": "user", "content": question}] # 3. nothing from the review enters here
for _ in range(max_steps):
reply = client.messages.create(
model="claude-haiku-4-5", max_tokens=4000, system=READER,
tools=READ_TOOLS, messages=messages,
)
if reply.stop_reason != "tool_use":
break
messages.append({"role": "assistant", "content": reply.content})
messages.append({"role": "user", "content": run_tools(reply)}) # 4
messages.append({"role": "user", "content": "Answer now in the requested format."})
return client.messages.parse( # 5. only the short answer leaves
model="claude-haiku-4-5", max_tokens=1000, system=READER,
messages=messages, output_format=Answer,
).parsed_output
ASK_TOOL = {
"name": "ask_codebase",
"description": "Answers a question about code outside the diff (tests, callers, history). "
"The question must be self-contained. Returns a short answer with file and lines.",
"input_schema": {"type": "object", "properties": {"question": {"type": "string"}}, "required": ["question"]},
}What you gained: the review never carries whole files, and reading uses a cheap model. What you lost: each question rereads what it needs; for many questions about the same files, consider caching.
TypeScript. Use this when the review runs on the SDK runner and isolated reading is a typed tool.
import Anthropic from "@anthropic-ai/sdk";
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { z } from "zod";
const client = new Anthropic();
const READER =
"You answer one question about the repository using only the read tools. " +
'Finish with JSON {"answer": string, "evidence": [{"path": string, "lines": string}]}.';
export const askCodebase = betaZodTool({
name: "ask_codebase",
description:
"Answers a question about code outside the diff (tests, callers, history). " +
"The question must be self-contained. Returns a short answer with file and lines.",
inputSchema: z.object({ question: z.string() }),
run: async ({ question }) => {
// 3. a new runner = a new context, without the review's history
const final = await client.beta.messages.toolRunner({
model: "claude-haiku-4-5",
max_tokens: 4000,
system: READER,
tools: READ_TOOLS,
messages: [{ role: "user", content: question }],
max_iterations: 10,
});
return final.content.find((b) => b.type === "text")?.text ?? "{}"; // 5
},
});
// review: tools: [askCodebase]; the diff is the only large thing in the main contextWhat you gained: isolation is structural, since each tool call creates its own runner. What you lost: the answer arrives as text; validate the JSON before trusting it.
Python + LangChain. Use this when the isolated reader is a create_agent and its call receives only the question.
from langchain.agents import create_agent
from langchain.tools import tool
from pydantic import BaseModel
class Evidence(BaseModel):
path: str
lines: str
class Answer(BaseModel):
answer: str
evidence: list[Evidence]
# 3. this agent does not receive the review's messages
reader = create_agent(
"anthropic:claude-haiku-4-5",
tools=READ_TOOLS,
system_prompt="You answer one question about the repository using only the read tools. "
"Be brief and cite file and lines.",
response_format=Answer,
)
@tool
def ask_codebase(question: str) -> str:
"""Answer a question about code outside the diff. The question must stand alone."""
out = reader.invoke(
{"messages": [{"role": "user", "content": question}]}, # only the question
{"recursion_limit": 20},
)
return out["structured_response"].model_dump_json() # 5READ_TOOLS are the read tools. The main reviewer receives ask_codebase and does not receive READ_TOOLS. The other ways to write that loop are on ReAct Loop.
What you gained: the review history has nowhere to enter, because the call does not receive it. What you lost: recursion_limit counts steps, not money.
Context Isolation is an AI design pattern for LLM apps and agents, in the Context and knowledge group. Run subtasks in separate contexts and bring back only the result.
In the real-world scenarios Coding agent, Deep research with citations, and Automated PR reviewer.