Personalize this lesson
Adapt explanations and teaching visuals to your background and preferred voice.
Prompt engineering taught you to write a clear instruction. Function calling and the Model Context Protocol (MCP) added tool schemas, tool results, and external resources. Once an agent works through several steps, its request contains much more than a prompt: instructions, tool definitions, retrieved text, conversation history, intermediate results, and saved state.
A model's context window is the token capacity available for that request. Context engineering decides which tokens deserve a place in it at each step. Capacity asks whether the request fits; context engineering asks whether each included item helps the model make the next decision.
More tokens don't guarantee better answers. A request may sit far below its limit yet perform worse than a smaller curated packet because irrelevant tools, stale results, or wrong intermediate claims remain active. Compare curation policies on answer quality, latency, and cost instead of treating available capacity as permission to include everything.
How does context engineering differ from prompt engineering and context capacity?
Answer
Prompt engineering improves one instruction. Context capacity sets how many tokens a request can hold. Context engineering curates the instructions, tools, evidence, history, and state that enter each request so the model reasons on a small high-signal set.
A longer trace can make the agent worse
Picture an incident agent investigating why deploy RUN-842 failed its canary. It reads the deploy record, checks CI logs, searches rollback runbooks, and follows request traces. Every tool call dumps its raw output back into the conversation. After forty turns the context holds: the original alert, four full runbooks (most of which were a dead end), six multi-thousand-token trace exports, three issue-search results, and one early hallucinated guess that a database migration lock caused the outage.
The agent now performs worse than it did at turn five. It re-runs the dead-end issue search, cites a migration lock that doesn't exist, and picks a verbose, irrelevant rollback paragraph over the trace span that matters. The window is nowhere near full, yet the agent is failing.
This isn't a tool-protocol or prompt-wording problem. Every tool returned a valid payload, and the request fits. The active context has accumulated low-signal and even wrong tokens, and the model is attending to all of them. Context engineering is the set of techniques that would have prevented this failure. The failed-canary agent will anchor the rest of the chapter.
Context includes more than the instruction
For a few years the applied-AI conversation was dominated by prompt engineering: finding the right words and phrasing for a single instruction. Anthropic frames context engineering as the natural successor to that practice.[1] Prompt engineering is about writing one good instruction. Context engineering is the broader discipline of curating and maintaining the entire set of tokens present during inference: the system prompt, tool definitions, retrieved documents, conversation history, tool results, and any memory loaded back in.
Anthropic's framing is a useful operating objective because it gives you a single guiding principle:
Find the smallest set of high-signal tokens that maximize the likelihood of your desired outcome.[1]
Every technique below tests a way to approach that minimal, high-signal set. A 2025 survey organizes the same strategies into a formal taxonomy.[2]
Why is "context engineering" framed as the successor to "prompt engineering" rather than a replacement?
Answer
Prompt engineering optimizes the wording of one instruction. Context engineering keeps the prompt-writing skill but expands the scope to the entire token set entering the model at inference: tools, retrieved docs, history, tool results, and memory. The goal becomes curating the smallest high-signal set, rather than phrasing one message well.
Why curation needs evaluation: context rot
The reason you shouldn't assume "just add more" is empirical, not stylistic. Chroma's Context Rot report evaluated 18 models across increasing input lengths and reported non-uniform performance as input grew, including on simple retrieval and copying tasks.[3] The magnitude and shape depend on model, task, and distractors; additional tokens are a hypothesis to evaluate, not free signal.
Anthropic describes the same engineering concern with an "attention budget" mental model and recommends seeking the smallest high-signal token set that supports the desired outcome.[1] Padding a window with low-signal tokens always increases input cost and can lower workload quality; a paired evaluation should determine when.
Context rot is one reason behind the techniques below. Cost, latency, stale state, and contradictory evidence are others. Curation should be an explicit candidate policy with quality checks, not an article of faith.
Define context rot and explain why it makes curation worth evaluating.
Answer
Context rot is the reported pattern that reliability can degrade as input token count grows, including before the hard limit is hit. It means an engineer should measure whether adding context helps a workload, and compare it with a curated high-signal alternative.
Four failure modes of an overloaded context
Before fixing context, you need vocabulary for how it breaks. Drew Breunig describes four useful failure-mode labels for long contexts.[4] They are diagnostic categories, not a formal completeness proof. Each one can show up in our failed-canary agent.
| Failure mode | What it's | Symptom in the failed-canary agent |
|---|---|---|
| Poisoning | A hallucination or error enters the context and is then referenced repeatedly | The early wrong guess about a migration lock keeps getting cited |
| Distraction | The context grows so long the model over-focuses on its history and stops forming new plans | The agent re-runs the dead-end issue search instead of trying something new |
| Confusion | Superfluous content (often too many tools) drives a low-quality response | With dozens of tools loaded, the agent picks a cluster-admin tool |
| Clash | New information or instructions conflict with earlier ones in the context | A tool defined in XML contradicts the system rule to answer only in JSON |
Don't turn reported examples into universal thresholds. Breunig cites an agent anecdote where long history encouraged repeated actions and a tool-use experiment where reducing tool count improved one model's benchmark result.[4] Those observations justify testing history pruning and tool gating on your model, tools, and task distribution.
1def diagnose_context(signals: set[str]) -> tuple[str, str]:
2 routes = [
3 ("wrong_fact_repeated", "poisoning", "remove disproven spans and rebuild notes"),
4 ("old_trace_replayed", "distraction", "compact old history and retain decisions"),
5 ("irrelevant_tool_called", "confusion", "gate tools for the current phase"),
6 ("rules_disagree", "clash", "reconcile conflicting instructions"),
7 ]
8 for signal, mode, action in routes:
9 if signal in signals:
10 return mode, action
11 return "unknown", "inspect trace and add an evaluation case"
12
13mode, action = diagnose_context({"wrong_fact_repeated", "old_trace_replayed"})
14print(f"diagnosis={mode}; next_action={action}")1diagnosis=poisoning; next_action=remove disproven spans and rebuild notes
The write, select, compress, isolate taxonomy
Naming failure modes tells you what went wrong. LangChain organizes agent context strategies into four useful buckets: write, select, compress, and isolate.[5] The LLM acts like a CPU, and its context window acts like RAM: a working set to manage deliberately. The buckets classify many common tactics without claiming they exhaust every design.

Write: keep state outside the window
The cheapest token is the one you never put in the window. Write means persisting information outside the context so it doesn't consume the attention budget until it's needed.[5] The classic pattern is a scratchpad: the agent writes notes, plans, or intermediate findings to a file or a state field, then reloads only the relevant note later. Agentic memory works the same way, persisting durable facts across sessions.[6]
For the failed-canary agent, a write strategy means: instead of leaving four full runbooks in the conversation, the agent records "auth callback errors confirmed; migration lock ruled out" as a one-line note and drops the raw tool results. The finding survives; the tokens don't.
1def promote_findings(tool_results: list[dict]) -> tuple[list[str], list[str]]:
2 notes, discarded_raw = [], []
3 for result in tool_results:
4 if result["confirmed"]:
5 notes.append(f"{result['source']}: {result['finding']}")
6 discarded_raw.append(result["raw_output"])
7 return notes, discarded_raw
8
9notes, discarded = promote_findings([
10 {"source": "deploy_RUN_842", "finding": "auth callback errors confirmed", "confirmed": True, "raw_output": "..." * 600},
11 {"source": "db_lock_check", "finding": "migration lock ruled out", "confirmed": True, "raw_output": "..." * 900},
12])
13print("notes:", notes)
14print("raw_results_to_remove:", len(discarded))1notes: ['deploy_RUN_842: auth callback errors confirmed', 'db_lock_check: migration lock ruled out']
2raw_results_to_remove: 2Select: pull in only what this step needs
Select means retrieving only the tokens relevant to the current step.[5] Retrieval-augmented generation (RAG) applies that idea to documents: a retriever surfaces a small set of relevant chunks instead of dumping the whole corpus into one request. Selection also applies to tools. The confusion failure mode above appears when an agent loads all 50 tools instead of the 5 needed for its current phase.
1TOOLS_BY_PHASE = {
2 "investigate": {"deploy_lookup", "trace_lookup", "runbook_search"},
3 "resolve": {"runbook_search", "rollback_advisor", "page_oncall"},
4}
5
6def tools_for_phase(phase: str, available: set[str]) -> list[str]:
7 allowed = TOOLS_BY_PHASE.get(phase, set())
8 return sorted(allowed & available)
9
10available = {"deploy_lookup", "trace_lookup", "runbook_search", "rollback_advisor", "cluster_admin"}
11print("investigate tools:", tools_for_phase("investigate", available))1investigate tools: ['deploy_lookup', 'runbook_search', 'trace_lookup']Prompt caching is a separate efficiency tactic: when requests reuse a stable prefix, a provider may avoid repeating part of the work needed to process it.[7] Caching does not select better evidence or reduce the number of tokens the model reasons over. It improves eligible repeated-prefix economics only when the provider and cache policy support it.
1import hashlib
2
3def prefix_key(system_prompt: str, stable_docs: str) -> str:
4 payload = system_prompt + "\n" + stable_docs
5 return hashlib.sha256(payload.encode()).hexdigest()[:12]
6
7stable = prefix_key("Use cited policy only.", "Policy version: 7")
8same_prefix = prefix_key("Use cited policy only.", "Policy version: 7")
9changed_prefix = prefix_key("Use cited policy only.", "Policy version: 8")
10print("reuse eligible:", stable == same_prefix)
11print("changed source invalidates candidate:", stable != changed_prefix)1reuse eligible: True
2changed source invalidates candidate: TrueCompress: shrink what must stay
When information has to stay in the window, compress reduces it to the required tokens.[5] Two common candidate patterns follow.
The first is compaction: when a conversation approaches the budget, summarize it and start a fresh window seeded with that summary.[1] The agent keeps its working knowledge but sheds the verbose transcript that produced it.
The second is tool-result pruning, a low-risk candidate fix for our failed-canary agent. Anthropic describes clearing old tool results as a light-touch form of compaction: once the relevant finding has been captured, old raw results can often leave the active context.[1] Preserve evidence that the next decision still needs, and compare quality before adopting an aggressive pruning policy.
1def prune_results(results: list[dict], keep_recent: int) -> list[str]:
2 retained = []
3 cutoff = max(0, len(results) - keep_recent)
4 for index, result in enumerate(results):
5 if index < cutoff and result["finding_recorded"]:
6 retained.append(f"[pruned raw output] finding={result['finding']}")
7 else:
8 retained.append(result["raw"])
9 return retained
10
11history = [
12 {"raw": "old trace span" * 100, "finding": "auth errors confirmed", "finding_recorded": True},
13 {"raw": "latest canary trace", "finding": "rollback review needed", "finding_recorded": False},
14]
15pruned = prune_results(history, keep_recent=1)
16print(pruned[0])
17print(pruned[1])1[pruned raw output] finding=auth errors confirmed
2latest canary traceIsolate: split work across focused windows
Isolate means splitting context across focused workers so no single window has to hold every exploratory trace.[5] A lead agent delegates a focused subtask, such as "check linked issues and rollback notes for every failed-canary exception", to a worker with its own clean context window. The worker does noisy exploration in isolation and returns a bounded evidence summary to the lead.[1]
Isolation can reduce distraction and confusion because the lead doesn't need every intermediate search result. It also adds coordination overhead and creates clash risk when worker outputs disagree, so isolate is a candidate for separable exploration, not a default.
1def accept_handoff(handoff: dict, token_limit: int = 400) -> bool:
2 required = {"claim", "evidence", "next_check", "tokens"}
3 return required <= handoff.keys() and handoff["tokens"] <= token_limit
4
5handoff = {
6 "claim": "auth callback failure qualifies for staged rollback",
7 "evidence": "trace span 2026-05-28 and rollback runbook section 4",
8 "next_check": "quote rollback blast radius for RUN-842",
9 "tokens": 86,
10}
11print(f"bounded handoff accepted: {accept_handoff(handoff)}")1bounded handoff accepted: TrueMap each of the four context-engineering moves to a concrete technique, then to the failure mode it most directly fights.
Answer
Write (scratchpad or memory file) keeps tokens out of the window entirely. Select (RAG, tool gating) pulls in only relevant items and fights confusion. Compress (compaction, tool-result pruning) shrinks history and fights distraction and poisoning. Isolate (sub-agents) splits work into clean windows and fights distraction and confusion.
Package repeatable procedures as Agent Skills
A tool gives an agent an action such as reading a file or querying a trace. A prompt gives instructions for one conversation. An Agent Skill packages reusable procedural knowledge as a directory containing a SKILL.md file and optional scripts, references, and assets.[8] The agent can discover the Skill from its metadata, load its instructions when relevant, and fetch deeper resources only when the task calls for them.[9]
That loading pattern is progressive disclosure. Instead of placing every runbook in every prompt, the runtime first exposes a small name and description. A matching task activates the full SKILL.md; a specialized reference or script stays outside the window until the instructions point to it. This is context selection implemented as a reusable package.
Skills, tools, and harness state solve different problems:
| Component | Owns | It shouldn't own |
|---|---|---|
| Prompt | current objective and constraints | reusable procedures for every future run |
| Tool | an executable capability with a typed contract | policy for when the action is appropriate |
| Skill | reusable workflow, examples, scripts, and references | live task status or authorization decisions |
| Harness | durable progress, retries, budgets, and completion evidence | domain instructions duplicated across every run |
Build a small Skill package
Suppose the incident agent repeatedly performs the same triage procedure. Package that procedure instead of pasting a long runbook into every task. The Agent Skills specification requires name and description in SKILL.md frontmatter. It also defines conventional scripts/, references/, and assets/ directories whose contents can be loaded when needed.[8]
1incident-triage/
2├── SKILL.md
3├── references/
4│ └── handoff-contract.md
5└── scripts/
6 └── validate-handoff.pyThe description is part of the routing surface. It needs both capability and trigger language, while the body should keep only steps needed on most runs:
1---
2name: incident-triage
3description: Investigate failed deployments from alerts, traces, and approved runbooks. Use for canary failures, rollback analysis, or incident evidence handoffs.
4---
5
6# Incident triage
7
81. Read the current alert and deploy record.
92. Load only investigation-phase tools.
103. Record each confirmed or disproven finding in the task checkpoint.
114. Read `references/handoff-contract.md` before delegating broad searches.
125. Run `scripts/validate-handoff.py` before returning evidence.
13
14Never execute a rollback. Return a proposed action with supporting source IDs.The skill doesn't need a full rollback runbook in its main file. references/handoff-contract.md can define the worker response schema, while validate-handoff.py can check it deterministically. Only the validator's output needs to enter the model context when the script runs. Anthropic's Skills documentation describes this split between instructions, executable code, and resources as the mechanism behind progressive disclosure.[9]
Test routing, procedure, and trust separately
A well-formed directory can still produce poor behavior. Evaluate at least three layers: whether the Skill activates for relevant requests, whether it stays inactive for unrelated requests, and whether following it produces a valid artifact. Keep these cases beside the Skill so a description edit can't silently change routing.
1[
2 {
3 "request": "Investigate why canary RUN-842 failed and return trace evidence.",
4 "expected_activation": true,
5 "expected_artifact": "validated evidence handoff"
6 },
7 {
8 "request": "Summarize our vacation policy.",
9 "expected_activation": false,
10 "expected_artifact": null
11 },
12 {
13 "request": "Roll back RUN-842 now.",
14 "expected_activation": true,
15 "expected_artifact": "proposal only; no write executed"
16 }
17]Treat installed Skills as code. Review and version their instructions, scripts, dependencies, network access, and expected outputs. A Skill can recommend a write or run a bundled script, but trusted application code still owns identity, authorization, sandboxing, and confirmation. A Skill-bundled script inherits the same PreToolUse hooks, sandbox, rate limits, and confirmation stack as any ordinary tool: progressive disclosure loads instructions into context but can't bypass the tool runtime. Progressive loading reduces context use; it doesn't create a security boundary.
Why is an Agent Skill a context-engineering primitive rather than a larger system prompt?
Answer
A Skill exposes small discovery metadata first, loads its reusable workflow only when the request matches, and reads or runs deeper resources only when needed. A larger system prompt pays the full token and distraction cost on every call. The Skill still needs routing, behavior, and security evaluation.
Make long-running work resumable
Compaction can preserve a conversation, but a task that lasts hours or days needs durable execution state outside that conversation. When a fresh context starts, it should reconstruct scope, completed work, current evidence, and the next safe action from files or records. Guessing from a summary invites duplicate work and premature completion.
Anthropic's long-running-agent experiments used an initializer, a feature list, a progress file, repository history, and repeatable startup checks so later sessions could make incremental progress from a known state.[10] A later harness design added planner, generator, and evaluator roles plus a written sprint contract that defined acceptance evidence before implementation began.[11] These are reported designs, not universal requirements, but the durable-state principles transfer to research, incident response, and data work.
A resumable harness needs five artifacts:
| Artifact | Required content | Resume question it answers |
|---|---|---|
| Task manifest | stable task IDs, dependencies, status, acceptance criteria | What remains in scope? |
| Progress checkpoint | last completed action, evidence paths, known failures | What happened in prior sessions? |
| Bootstrap command | deterministic environment and smoke check | Is current state healthy before more work? |
| Work receipt | tests, outputs, hashes, or review result | Is a completed claim supported? |
| Recovery rule | retry, rollback, cancellation, and stale-lease behavior | What happens after interruption? |
The task manifest is machine-readable scope, while the progress log is a human-readable explanation. Neither should silently overwrite the other. A task marked complete needs a receipt, and a receipt needs a stable path or identifier that a fresh session can verify.
Mini-lab: resume from verified receipts
The script below simulates a new context choosing the next ready task. It refuses to treat a done item as complete unless the checkpoint names its evidence. Dependencies prevent the final report from starting before source verification finishes.
1from dataclasses import dataclass
2
3@dataclass(frozen=True)
4class WorkItem:
5 task_id: str
6 depends_on: tuple[str, ...]
7 status: str
8 receipt: str | None
9
10def verified_done(items: list[WorkItem]) -> set[str]:
11 invalid = [item.task_id for item in items if item.status == "done" and not item.receipt]
12 if invalid:
13 raise ValueError(f"done tasks missing receipts: {invalid}")
14 return {item.task_id for item in items if item.status == "done"}
15
16def next_ready(items: list[WorkItem]) -> WorkItem | None:
17 completed = verified_done(items)
18 for item in items:
19 if item.status == "pending" and set(item.depends_on) <= completed:
20 return item
21 return None
22
23checkpoint = [
24 WorkItem("collect-traces", (), "done", "artifacts/traces-run-842.json"),
25 WorkItem("verify-cause", ("collect-traces",), "pending", None),
26 WorkItem("write-report", ("verify-cause",), "pending", None),
27]
28
29selected = next_ready(checkpoint)
30assert selected is not None
31print(f"resume_task={selected.task_id}")
32print(f"verified_receipts={sorted(verified_done(checkpoint))}")1resume_task=verify-cause
2verified_receipts=['collect-traces']At session start, read manifest and checkpoint, inspect version history, run bootstrap smoke check, then select one ready task. At session end, run its acceptance check, write receipt, update status, and leave workspace recoverable. Cancellation should stop new work without deleting receipts; resume should verify them before continuing.
Separate generation from evaluation
For quality-critical work, define a small contract before generation. Producer proposes artifact and expected checks; evaluator reviews contract, then independently tests resulting artifact. The 2026 Anthropic harness used a comparable generator-evaluator agreement before each sprint.[11]
1{
2 "task_id": "verify-cause",
3 "input_receipts": ["artifacts/traces-run-842.json"],
4 "deliverable": "artifacts/cause-analysis.json",
5 "acceptance": [
6 "every causal claim cites a trace span",
7 "disproven migration-lock claim is absent",
8 "rollback remains a proposal"
9 ],
10 "evaluator": "incident-evidence-check"
11}The evaluator shouldn't rely on the producer's claim that checks passed. It should open artifact, run named validation, and write its own receipt. This split controls premature completion and keeps acceptance criteria stable across context resets.
| Failure | Symptom after resume | Guardrail |
|---|---|---|
| Narrative-only progress | new session can't tell which claims were verified | structured task IDs and evidence paths |
| Completion without receipt | harness skips unfinished work | reject done status without verifiable evidence |
| Non-idempotent restart | duplicate ticket, write, or deployment | operation keys plus read-before-write checks |
| Stale lease | two sessions work same task | owner and lease expiry in checkpoint store |
| Producer grades itself | plausible output passes without inspection | independent evaluator and separate receipt |
| Secret in checkpoint | durable context becomes data leak | store references or redacted facts, not credentials |
An Agent Skill can teach each session the procedure for incident triage. The harness owns which incident task is active, what already passed, and how to resume safely. Keeping those responsibilities separate lets instructions evolve without corrupting live progress.
What must survive a context reset for a long-running task to resume safely?
Answer
Stable scope and task IDs, dependency and status data, receipts for completed work, current failures, a repeatable bootstrap check, and retry or cancellation rules must survive outside model context. A narrative summary can explain progress, but it can't replace machine-checkable state and evidence.
Rebuild the failed-canary working set
With the taxonomy in hand, the running example fixes itself. Instead of letting the window grow monotonically, the agent runs a curation step before each model call:
- Select only the tools relevant to the current phase (an investigation needs deploy lookup and traces, not cluster administration), then evaluate whether tool-call accuracy improves.
- Write durable findings to a scratchpad ("auth callback errors confirmed; migration lock ruled out") and drop the raw tool results.
- Compress by pruning trace exports older than a few turns and compacting the transcript once it grows large.
- Isolate the noisy "check every linked issue" exploration into a sub-agent that returns a one-paragraph summary.
- Detect poisoning: when the early migration-lock guess is identified as wrong, remove it from history so it stops being cited.
The window now holds the alert, current evidence, a short notes block, and a clean tool set. In the concrete script below, it shrinks an illustrative 12.4K-token raw set to a 3.2K-token active window plus short external notes. Lower input cost is immediate; better task performance still requires evaluation.


The logic is simple enough to simulate with a tiny script. This version turns the failed-canary agent's messy transcript into a compact working set by keeping the current alert and evidence, writing durable findings to notes, and dropping poisoned or irrelevant tokens.
1from dataclasses import dataclass
2
3@dataclass
4class ContextItem:
5 name: str
6 kind: str
7 tokens: int
8 signal: int
9 keep: str
10
11items = [
12 ContextItem("alert_RUN_842", "task", 180, 10, "window"),
13 ContextItem("latest_trace_span", "evidence", 420, 10, "window"),
14 ContextItem("rollback_runbook", "evidence", 1800, 9, "window"),
15 ContextItem("scratchpad", "notes", 260, 8, "window"),
16 ContextItem("deploy_lookup_tool", "tool", 240, 8, "window"),
17 ContextItem("trace_lookup_tool", "tool", 260, 8, "window"),
18 ContextItem("old_trace_export", "log", 8200, 2, "drop"),
19 ContextItem("cluster_admin_tool", "tool", 360, 1, "drop"),
20 ContextItem("issue_search_tool", "tool", 410, 1, "drop"),
21 ContextItem("wrong_migration_lock_guess", "poison", 120, 0, "drop"),
22 ContextItem("auth_errors_confirmed", "finding", 90, 7, "notes"),
23 ContextItem("migration_lock_ruled_out", "finding", 96, 7, "notes"),
24]
25
26def summarize(selection):
27 return ", ".join(item.name for item in selection)
28
29raw_total = sum(item.tokens for item in items)
30window_items = [item for item in items if item.keep == "window"]
31notes_items = [item for item in items if item.keep == "notes"]
32dropped_items = [item for item in items if item.keep == "drop"]
33
34curated_total = sum(item.tokens for item in window_items)
35notes_total = sum(item.tokens for item in notes_items)
36
37print(f"raw_tokens={raw_total}")
38print(f"curated_window_tokens={curated_total}")
39print(f"external_notes_tokens={notes_total}")
40print(f"removed_tokens={raw_total - curated_total - notes_total}")
41print("window:", summarize(window_items))
42print("notes:", summarize(notes_items))
43print("dropped:", summarize(dropped_items))1raw_tokens=12436
2curated_window_tokens=3160
3external_notes_tokens=186
4removed_tokens=9090
5window: alert_RUN_842, latest_trace_span, rollback_runbook, scratchpad, deploy_lookup_tool, trace_lookup_tool
6notes: auth_errors_confirmed, migration_lock_ruled_out
7dropped: old_trace_export, cluster_admin_tool, issue_search_tool, wrong_migration_lock_guess1from dataclasses import dataclass
2
3@dataclass
4class Candidate:
5 name: str
6 tokens: int
7 priority: int
8 required: bool = False
9
10def pack_working_set(candidates: list[Candidate], budget: int) -> list[str]:
11 ordered = sorted(candidates, key=lambda item: (not item.required, -item.priority))
12 selected, used = [], 0
13 for item in ordered:
14 if used + item.tokens <= budget:
15 selected.append(item.name)
16 used += item.tokens
17 elif item.required:
18 raise ValueError(f"required item does not fit: {item.name}")
19 return selected
20
21items = [
22 Candidate("alert RUN-842", 180, 10, required=True),
23 Candidate("latest trace span", 420, 10, required=True),
24 Candidate("rollback runbook", 1_800, 9),
25 Candidate("stale trace export", 8_200, 1),
26]
27print(pack_working_set(items, budget=3_000))1['alert RUN-842', 'latest trace span', 'rollback runbook']Suppose a production incident trace reaches 120K tokens of mostly stale logs and one referenced hallucination, while the selected model advertises a much larger window. Why is "we have headroom, leave it" the wrong call?
Answer
Headroom is a capacity fact, not a quality result. Stale logs and a referenced hallucination create plausible distraction and poisoning failures. Compare a pruned or compacted candidate against the raw trace rather than relying on spare capacity.
Context thrashing: management work exceeds useful work
An agent can also over-manage a bounded window. Context thrashing is the operating pattern where it spends more tokens and tool calls retrieving, summarizing, evicting, and reloading context than advancing the task. One turn fetches a trace, the next compacts it, the next retrieves the same trace because the summary omitted a field, and the loop repeats without a new verified finding.
Don't diagnose thrashing from one large retrieval. Look for repeated churn with little progress:
| Signal | Evidence of thrashing |
|---|---|
| Context operations per completed task step | retrieval, compaction, or reload count rises while completed steps stay flat |
| Reload rate | recently evicted evidence is fetched again without a changed question |
| New-evidence yield | retrieved tokens grow while accepted facts or receipts remain flat |
| Decision latency | most wall time occurs before the agent takes or verifies a task action |
Use a stable working-set contract instead of a bigger window. Pin the task, current evidence, and next acceptance check for the phase. Cap retrieval and compaction cycles, require each cycle to add a named fact or resolve a decision, and isolate broad exploration behind a bounded handoff. If required evidence still doesn't fit, split the task or persist a structured checkpoint instead of paging the same material repeatedly.
How does context thrashing differ from ordinary context bloat?
Answer
Bloat means too much low-signal material remains active. Thrashing means the agent repeatedly spends retrieval and compaction work moving context in and out without producing new evidence or completing task steps. Bound the working set and require measurable progress per context operation.
Larger windows don't remove curation
Large-window model offerings make it tempting to treat curation as obsolete.[12] Bigger windows raise the capacity ceiling; they don't establish quality for an overloaded agent trace. Frameworks such as LangGraph expose short- and long-term memory plus summarization or deletion patterns because state management remains an application responsibility.[13]
When an agent underperforms, inspect the transmitted context alongside model and window choices. Name a suspected failure mode, apply a bounded candidate change, and measure whether it improves the task.
When context curation breaks down
| Symptom | Likely cause | Fix |
|---|---|---|
| Agent gets worse over a long session despite spare window | Distraction from accumulated stale history | Compact the transcript; prune old tool results |
| Agent keeps citing a wrong fact | Context poisoning: an early error is being re-referenced | Remove the bad tokens from history; don't just add a correction |
| Agent picks irrelevant tools or ignores the right one | Context confusion from overlapping active tools | Gate tools per phase; evaluate a smaller active set |
| Model violates a format rule when a Model Context Protocol (MCP) tool is attached | Context clash between tool instructions and system rules | Reconcile instructions or isolate the tool behind a sub-agent |
| Costs balloon and latency rises with no quality gain | Stuffing the window instead of curating it | Apply the smallest-high-signal-set principle: select and compress |
| Agent repeatedly reloads evidence it just summarized or evicted | Context thrashing | Pin phase evidence; cap context operations; require new evidence or a completed step |
| Sub-agent answers conflict with each other | Isolation without reconciliation | Have the lead agent resolve clashes before acting |
Use this checklist before shipping an agent that handles long sessions:
- Does the context shrink or stay bounded across turns, or does it only grow? Unbounded growth invites distraction and rot.
- Are tool results pruned or compacted once they are no longer needed for the next step?
- Does a phase-gated active tool set outperform loading every available tool on your evaluation cases?
- When a sub-agent is used, does it return a distilled summary rather than its full transcript?
- Do repeated retrieval or compaction calls produce a new accepted fact, receipt, or completed task step?
- Do you have an eval that catches poisoning: a wrong fact persisting and being re-cited across turns?
An agent that worked in short demos degrades in long production sessions. What is the systematic first step before changing models?
Answer
Inspect the actual context being sent. Name a suspected failure mode (poisoning, distraction, confusion, or clash), then test a matching move: prune or compact for distraction and poisoning, gate tools for confusion, reconcile or isolate for clash. Compare the candidate on quality and cost before escalating architecture.
Diagnostic playbook
When a long-running agent underperforms, use this sequence:
- Inspect the actual context, not the window size or cost alone.
- Name the failure mode: poisoning, distraction, confusion, or clash.
- Choose the matching move: write, select, compress, or isolate.
- Rebuild the next call around the smallest high-signal working set.
- Compare baseline and curated calls on accuracy, latency, token cost, and failure recurrence.
- Only after that ask whether you still need a different model, window, or architecture.
1def rebuild_notes(notes: list[dict]) -> list[str]:
2 return [
3 note["text"]
4 for note in notes
5 if note["status"] != "disproven"
6 ]
7
8notes = [
9 {"text": "auth callback errors confirmed", "status": "confirmed"},
10 {"text": "migration lock caused RUN-842", "status": "disproven"},
11 {"text": "database lock ruled out", "status": "confirmed"},
12]
13print("rebuilt notes:", rebuild_notes(notes))1rebuilt notes: ['auth callback errors confirmed', 'database lock ruled out']1def approve_curation(
2 baseline_accuracy: float,
3 curated_accuracy: float,
4 baseline_tokens: int,
5 curated_tokens: int,
6 poisoned_references_after: int,
7) -> bool:
8 quality_ok = curated_accuracy >= baseline_accuracy
9 cost_ok = curated_tokens < baseline_tokens
10 poisoning_removed = poisoned_references_after == 0
11 return quality_ok and cost_ok and poisoning_removed
12
13approved = approve_curation(
14 baseline_accuracy=0.82,
15 curated_accuracy=0.87,
16 baseline_tokens=12_436,
17 curated_tokens=3_346,
18 poisoned_references_after=0,
19)
20print(f"curated context approved: {approved}")1curated context approved: TrueContext engineering gives an agent a bounded, inspectable working set even as tools and history accumulate. The next chapter studies prompt injection, where untrusted content tries to turn retrieved evidence or tool output into instructions. Curation and source labels provide the context boundary that those defenses need.
State the one-sentence guiding principle of context engineering and explain why it subsumes the four-move taxonomy.
Answer
Find the smallest set of high-signal tokens that maximizes the chance of the desired outcome. Write, select, compress, and isolate are simply the four mechanisms for pushing toward that minimal high-signal set, each removing or avoiding low-value tokens in a different way.