Skip to content
Skip to lesson
Back to Topics
LearnApplied LLM EngineeringContext Engineering
🤖MediumLLM Agents & Tool Use

Context Engineering

An agent can fit a 128k window and still repeat a disproven lock, so each request has to be curated and later work has to resume from checked artifacts.

26 min read
Learning path
Step 65 of 199 in the full curriculum

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

Imagine an incident response agent investigating failed deployment RUN-842. It keeps blaming a database migration lock that telemetry ruled out eight turns ago. The disproof sits inside the prompt, yet the agent keeps recommending database rollbacks. The request fits inside a hypothetical 128k-token window. Stale, contradictory context is one plausible contributor to this failure; the symptoms alone don't establish its cause.

The failed stays fixed for the rest of the lesson: a release test reports authentication errors, and the agent has to isolate the root cause from deploy records, runbooks, and request before it proposes a .

Compare two implementations. A reactive buffer trimmer waits until a token ceiling is reached, then slices history with a sliding window, FIFO eviction, or blunt summarization. If the application fails to protect its instructions and task definition, naive eviction can remove them while keeping repetitive failure traces. Context management also includes proactive techniques; these examples show how a reactive trimmer can discard instructions and task state.

is proactive working-set architecture. For every inference step, the application designs the payload from first principles, starting from the decision that step has to make, instead of waiting for a token ceiling.

Treat the as a scratchpad. Keep high-signal evidence, gate tools by phase, and leave exploratory subtasks behind a typed worker boundary. Stable prefixes stay aligned so the KV cache can reuse them. Checked state lives in a verified checkpoint outside the prompt.

Capacity asks whether a request fits inside the model's token limit. Curation asks whether the request contains sufficient, unpoisoned evidence for the model's next decision.

What long-context evaluations reveal

Anthropic frames context engineering as the successor to prompt engineering: curating every token present during inference, including tool definitions, retrieved documents, message history, and memory.[1]

Anthropic's practical objective is to minimize context while retaining the information that helps achieve the desired outcome.[1] Minimal isn't always shortest: removing a required evidence span defeats the objective.

A 2025 survey organizes the field into context retrieval and generation, processing, and management, then examines systems that combine them.[2] A model's input limit describes capacity; evaluate how effectively it uses evidence throughout that input.

Two historical findings and a benchmark distinction show that capacity and reliability depend on separate measurements. The results point to several mechanisms:

  1. Position sensitivity: Liu et al.'s TACL 2024 study tested multi-document question answering and key-value retrieval. Performance was often strongest when the relevant material appeared near the beginning or end, then fell when that material moved into the middle.[3] The study measured behavior across its tasks; test current models to identify their own failure patterns and mechanisms.
  2. Length and distractor sensitivity: Chroma's 2025 report evaluated 18 models while varying input length, needle similarity, distractors, and document structure. Its tested models became less reliable as inputs grew, and related distractors compounded errors.[4] Treat these historical results as motivation for fresh application evaluations; use current model scores for current decisions.
  3. Single-fact retrieval and evidence synthesis test different skills: A single-needle NIAH probe asks a model to retrieve one inserted fact. Kamradt's current repository also supports multi-needle and chained retrieval tasks, so name the variant when reporting results.[5] Our fictional RUN-842 requires linking an authentication callback timeout in trace-17 at 20% depth with an OAuth scope change in deploy-record at 60% depth while rejecting a database-lock distractor. Evaluate that multi-document synthesis directly, and gather causal evidence before attributing the incident to the scope change.

Adding tokens can therefore add useful evidence, but it also adds retrieval work and more opportunities for a plausible distractor to win. Measure the complete task on the context distribution your application actually sends.

Four failure modes in long prompts

Before picking a mitigation, inspect the prompt and its outputs. Drew Breunig's four-part taxonomy is a diagnostic lens for observable failures; pair it with model-specific evaluation to identify internal causes.[6] Here are illustrative RUN-842 symptoms:

Failure modeObservable patternSymptom in RUN-842
PoisoningAn erroneous claim or hallucination enters the prompt and gets cited repeatedlyAn early guess about a database migration lock keeps getting cited as root cause
DistractionLow-value history dominates later plansThe agent re-runs failed search queries instead of checking fresh trace spans
ConfusionIrrelevant content or overlapping tool descriptions influence an inappropriate choiceWith 40 tools loaded, the agent calls cluster_admin instead of trace_lookup
ClashContradictory instructions or policies compete within the active windowA retrieved runbook demands XML output while the system directive enforces JSON

An apology or corrective note can leave a poisoned claim influencing later output. Remove the poisoned span and any summary derived from it before the model generates its next plan, while preserving a verified note that records the negative finding.

Contradictory instructions create clashes between policies. An XML trace snippet inside a JSON-only system prompt remains data when the model treats it as passive evidence. A clash occurs when a tool description or retrieved runbook dictates an output schema that conflicts with the application's system prompt.

A poisoned lock, a distracting log, and a clashing schema do not call for the same edit. Should that span be stored outside the prompt, left out, shortened, or moved into another window?

The four context moves

LangChain groups those repairs as write, select, compress, and isolate.[7] Write keeps state outside the prompt. Select admits only what this step needs. Compress shortens what has to stay. Isolate gives exploratory work its own window. We call the tokens assembled for the immediate model step its working set.

In the fictional RUN-842 input, a depth rail places trace-17 at twenty percent, a callback timeout, and deploy-record at sixty percent, an OAuth scope change. Below, four routes run from what the agent has on the left to a single next-call box on the right. Selecting pulls the two source spans. Writing and reloading brings back the checked finding from findings.json that the lock was ruled out. Compressing turns raw history into a summary with source IDs. Isolating keeps the worker search trace in its own window and returns a handoff capped at 1,600 bytes. A note says the cause remains unresolved.
The depth rail locates two clues in the fictional input. Follow the source spans, checked negative finding, summary and bounded worker handoff into the next call. Reloaded notes consume input tokens, while the worker's search trace stays in its own window. Inspect the cited evidence and test the scope-change hypothesis before making a causal claim or proposing a rollback.

Write: persist state outside the prompt

Write means saving intermediate findings, structured plans, or raw records outside the active prompt, reloading only specific facts when needed.[7] Persisting data to an external store doesn't make it available to the model for free: reading the note back consumes input tokens on that subsequent call. The benefit comes from selective reloading.

Scratchpads, files, and structured memory stores preserve state across turns while the application selects what to reload.[8] For RUN-842, we retain checked findings in notes and remove bulky terminal output from the next prompt, preserving the source archive.

write-durable-findings.py
import json
from pathlib import Path
from tempfile import TemporaryDirectory

def promote_findings(tool_results: list[dict]) -> tuple[list[str], list[str]]:
    notes, discarded_raw = [], []
    for result in tool_results:
        if type(result["confirmed"]) is not bool:
            raise ValueError("confirmed must be a boolean set by the verifier")
        if result["confirmed"]:
            notes.append(f"{result['source']}: {result['finding']}")
        discarded_raw.append(result["raw_output"])
    return notes, discarded_raw

notes, discarded = promote_findings([
    {"source": "deploy_RUN_842", "finding": "auth callback errors confirmed", "confirmed": True, "raw_output": "..." * 600},
    {"source": "db_lock_check", "finding": "migration lock ruled out", "confirmed": True, "raw_output": "..." * 900},
])
with TemporaryDirectory() as directory:
    path = Path(directory) / "findings.json"
    path.write_text(json.dumps(notes), encoding="utf-8")
    reloaded = json.loads(path.read_text(encoding="utf-8"))
    assert reloaded == notes
    print("reloaded notes:", reloaded)
print("raw_results_to_remove:", len(discarded))
Output
reloaded notes: ['deploy_RUN_842: auth callback errors confirmed', 'db_lock_check: migration lock ruled out']
raw_results_to_remove: 2

Only the host's verification step sets confirmed; promote a finding after checking it against trusted evidence. Tool JSON cannot certify its own claims. This example queues raw outputs for a separate pruning step. A production store should retain source revisions, observation times, access controls, and rules for rechecking findings when their underlying state changes.

Select: pull in only what this step needs

Select means pulling into the active working set only the tokens necessary for the immediate decision.[7] applies this to documents: a retriever surfaces relevant chunks rather than stuffing an entire manual into the prompt.

Selection also applies to tool catalogs. A large catalog consumes schema tokens and can make selection harder when descriptions overlap. The effect depends on the model and task. Our release triage workflow needs read-only investigation tools during its first phase.

phase-specific-tool-gate.py
TOOLS_BY_PHASE = {
    "investigate": {"deploy_lookup", "trace_lookup", "runbook_search"},
    "propose": {"runbook_search", "rollback_advisor"},
}

def tools_for_phase(phase: str, available: set[str]) -> list[str]:
    allowed = TOOLS_BY_PHASE.get(phase, set())
    return sorted(allowed & available)

available = {"deploy_lookup", "trace_lookup", "runbook_search", "rollback_advisor", "cluster_admin"}
print("investigate tools:", tools_for_phase("investigate", available))
assert tools_for_phase("unknown", available) == []
Output
investigate tools: ['deploy_lookup', 'runbook_search', 'trace_lookup']

Gating tool schemas reduces prompt tokens, but visibility isn't security authorization. The execution backend must validate permissions on every attempted call regardless of what was exposed in the prompt.

Prompt caching and KV cache economics

Prompt caching is an efficiency mechanism that changes the latency and cost of repeated tokens, but it requires strict structural discipline.[9][10]

In transformer inference, the request runs through two stages: prefill and decode. Prefill processes the input to generate Key (KK) and Value (VV) states used by attention. Decode then generates output tokens autoregressively. As prompts grow, prefill contributes more work before the first output token appears.

Prompt caching preserves the computed KV states for a reusable prompt prefix. When a later request matches an eligible prefix, the provider can reuse those states instead of processing the matching tokens again. New suffix tokens still require processing, and normal decoding still follows.[10]

Provider controls and prices differ. Checked October 10, 2026: Anthropic supports top-level automatic caching and explicit cache_control breakpoints. OpenAI enables implicit caching on supported models, while explicit breakpoints and cache-write pricing depend on the model. Check the current provider documentation rather than applying one discount or breakpoint rule everywhere.[9][10]

Cache reuse requires the rendered prefix to match at an eligible boundary. The dynamic prefix trap places volatile fields before material that could otherwise be reused. This schematic prefix illustrates the ordering; provider message formats vary:

text
System: You are an on-call triage bot.
Current Time: 2026-09-02T16:55:01Z
Session UUID: 8a4b2c1d-9e8f
Tool Schemas: [ ... 40,000 tokens of runbooks and tools ... ]

Changing an early timestamp or UUID makes the following prefix differ. The unchanged part before it may still be reusable if it reaches an eligible boundary. The exact token position depends on the rendered request and tokenizer. A cache miss leaves older entries intact; a matching prefix can still miss after expiry or eviction.

To maximize cache hits, organize prompts into three stable tiers:

  1. Stable root: Invariant instructions and tools relevant to the current phase. A phase change can legitimately change this prefix; don't retain irrelevant tools just for caching.
  2. Semi-static context: Runbooks, architectural specs, and pinned background documents. Reuse here also requires all preceding context to match.
  3. Changing suffix: The new query, reloaded findings, and scratchpad notes. These may become reusable on later requests if retained unchanged; changing content still needs processing.

Treat cache grouping and application authorization as separate concerns. Providers isolate caches at their own organization or workspace boundaries, but one provider organization may still serve many application users. OpenAI's optional prompt_cache_key can separate cache accounting and reduce cache-hit probing across those users.[10] Regardless of cache configuration, validate identity and rules before assembling the prefix. A cache hit must never decide which user's evidence or tools enter a request. The prompt-injection defense lesson works through that authority boundary in detail.

cacheable-prefix-check.py
import hashlib
import json

def prefix_fingerprint(system_prompt: str, tools_schema: str, dynamic_header: str = "") -> str:
    payload = json.dumps([dynamic_header, system_prompt, tools_schema], ensure_ascii=False)
    return hashlib.sha256(payload.encode("utf-8")).hexdigest()

system_prompt = "You are an incident responder for RUN-842."
tools_schema = "tools: [deploy_lookup, trace_lookup]"

cached_turn_1 = prefix_fingerprint(system_prompt, tools_schema)
cached_turn_2 = prefix_fingerprint(system_prompt, tools_schema)

broken_turn_1 = prefix_fingerprint(system_prompt, tools_schema, dynamic_header="timestamp: 16:55:01")
broken_turn_2 = prefix_fingerprint(system_prompt, tools_schema, dynamic_header="timestamp: 16:55:02")

print("static prefix matched:", cached_turn_1 == cached_turn_2)
print("dynamic header breaks prefix:", broken_turn_1 == broken_turn_2)
Output
static prefix matched: True
dynamic header breaks prefix: False

The hash example checks our serialized strings, not provider cache hits or token boundaries. Inspect the provider's cache-read/write usage fields to measure reuse. Cached tokens still occupy input capacity and participate in attention; caching doesn't remove long-context failure risks. Account for cache writes, reads, new suffix input, and output when comparing cost.

Compress: shrink what must stay

When information must remain in the active prompt, compress distills it into fewer tokens.[7] Two production techniques handle this:

  1. Tool output distillation and pruning: Raw API outputs often contain thousands of tokens of verbose headers, redundant metadata, and empty arrays. Once the agent identifies the critical finding, the application prunes the raw payload from active history, leaving a structured summary.[1]
  2. Token compression: LLMLingua uses a smaller language model, a budget controller, and token-level compression to shorten prompts without retraining the target model. Its reported compression and quality results apply to the evaluated datasets; measure quality on your prompt distribution before relying on them.[11]
tool-result-pruning-policy.py
def prune_results(results: list[dict], keep_recent: int) -> list[str]:
    if type(keep_recent) is not int or keep_recent < 0:
        raise ValueError("keep_recent must be a non-negative integer")
    if any(type(result["finding_recorded"]) is not bool for result in results):
        raise ValueError("finding_recorded must be a verifier-set boolean")
    retained = []
    cutoff = max(0, len(results) - keep_recent)
    for index, result in enumerate(results):
        if index < cutoff and result["finding_recorded"]:
            retained.append(f"[pruned raw output] {result['source']}: {result['finding']}")
        else:
            retained.append(result["raw"])
    return retained

history = [
    {"source": "trace-17", "raw": "old trace span" * 100, "finding": "auth errors confirmed", "finding_recorded": True},
    {"source": "trace-18", "raw": "latest canary trace", "finding": "rollback review needed", "finding_recorded": False},
]
pruned = prune_results(history, keep_recent=1)
print(pruned[0])
print(pruned[1])
Output
[pruned raw output] trace-17: auth errors confirmed
latest canary trace

These strings illustrate content replacement, not complete provider messages. Before pruning, preserve the source and its verified finding. Reconstruct transcripts with valid call/result pairs, identifiers, and provider message order.

Isolate: split work across focused windows

Isolate means delegating noisy exploration to sub-agents with dedicated, clean context windows.[7] If an agent must search 50 historical git commits or issue tickets, running that search inside the lead context dumps dozens of irrelevant diffs into the working set.

The lead delegates the search to a worker that returns a compact, structured handoff.[1] The application keeps the intermediate search trace out of the lead's prompt. Separate windows aren't an authorization boundary or an OS sandbox. They may reduce lead-context size while increasing total tokens and latency across both agents.

isolated-handoff-contract.py
import json

def unique_keys(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            raise ValueError("duplicate JSON key")
        result[key] = value
    return result

def accept_handoff(payload: bytes, known_sources: set[str], byte_limit: int = 1_600) -> bool:
    if type(byte_limit) is not int or byte_limit <= 0:
        raise ValueError("byte_limit must be a positive integer")
    if not isinstance(payload, bytes) or len(payload) > byte_limit:
        return False
    try:
        handoff = json.loads(payload.decode("utf-8"), object_pairs_hook=unique_keys)
    except (UnicodeDecodeError, ValueError, RecursionError):
        return False
    if not isinstance(handoff, dict) or set(handoff) != {"claim", "source_ids", "next_check"}:
        return False
    for field in ("claim", "next_check"):
        if not isinstance(handoff[field], str) or not handoff[field].strip():
            return False
    sources = handoff["source_ids"]
    if not isinstance(sources, list) or not sources:
        return False
    if any(not isinstance(source, str) or source not in known_sources for source in sources):
        return False
    return True

def wire(handoff: dict) -> bytes:
    return json.dumps(handoff, ensure_ascii=False).encode("utf-8")

handoff = {
    "claim": "trace-17 contains authentication callback errors; cause unresolved",
    "source_ids": ["trace-17", "runbook-v4-section-4"],
    "next_check": "check the failed dependency before proposing rollback",
}
sources = {"trace-17", "runbook-v4-section-4"}
print("bounded handoff accepted:", accept_handoff(wire(handoff), sources))
print("unknown source accepted:", accept_handoff(wire({**handoff, "source_ids": ["missing"]}), sources))
print("oversized claim accepted:", accept_handoff(wire({**handoff, "claim": "x" * 2_000}), sources))
Output
bounded handoff accepted: True
unknown source accepted: False
oversized claim accepted: False

The receiving boundary now measures incoming bytes before decoding or parsing, including whitespace and multibyte UTF-8 characters. Bound the transport read too: checking a fully buffered payload doesn't prevent oversized allocation upstream. The host supplies sources the caller may access. Schema and source-ID checks don't establish that those sources support the claim; verify their contents before accepting the finding.

Rebuilding the working set for RUN-842

Let's work through synthetic accounting for the failed-canary investigation. We assign item sizes and retention decisions in advance: 12,436 units labeled as tokens become 3,346 after dropping 9,090. This demonstrates the budget calculation; measure finalized prompts with the provider tokenizer and evaluate answer quality separately.

Two stacked bars share a scale from zero to 12,436 assigned token units for the made-up RUN-842 allocation. The raw bar contains alert, trace, and runbook at 2,400, scratchpad and active tools at 760, reloaded findings at 186, the old trace export at 8,200, unused tool schemas at 770, and the disproven lock claim at 120. The first three remain in the next call, totaling 3,346. The omitted items total 9,090, or 73.1 percent. The reloaded findings are 90 plus 96 units, which count again when reloaded.
The shared scale shows the 9,090-unit reduction. The next call contains 3,160 window units plus 186 reloaded findings, totaling 3,346. These assigned sizes demonstrate the retention policy; measure the finalized request with the provider's tokenizer and evaluate whether the retained evidence supports the task.

The example applies preassigned retention labels. It doesn't discover relevant facts or decide whether a claim is disproven:

incident-agent-context-curation.py
from dataclasses import dataclass

@dataclass
class ContextItem:
    name: str
    kind: str
    tokens: int
    keep: str

items = [
    ContextItem("alert_RUN_842", "task", 180, "window"),
    ContextItem("latest_trace_span", "evidence", 420, "window"),
    ContextItem("rollback_runbook", "evidence", 1800, "window"),
    ContextItem("scratchpad", "notes", 260, "window"),
    ContextItem("deploy_lookup_tool", "tool", 240, "window"),
    ContextItem("trace_lookup_tool", "tool", 260, "window"),
    ContextItem("old_trace_export", "log", 8200, "drop"),
    ContextItem("cluster_admin_tool", "tool", 360, "drop"),
    ContextItem("issue_search_tool", "tool", 410, "drop"),
    ContextItem("wrong_migration_lock_guess", "poison", 120, "drop"),
    ContextItem("auth_errors_confirmed", "finding", 90, "notes"),
    ContextItem("migration_lock_ruled_out", "finding", 96, "notes"),
]

def summarize(selection):
    return ", ".join(item.name for item in selection)

raw_total = sum(item.tokens for item in items)
window_items = [item for item in items if item.keep == "window"]
notes_items = [item for item in items if item.keep == "notes"]
dropped_items = [item for item in items if item.keep == "drop"]

window_total = sum(item.tokens for item in window_items)
notes_total = sum(item.tokens for item in notes_items)
next_call_total = window_total + notes_total
removed_total = sum(item.tokens for item in dropped_items)
assert raw_total == next_call_total + removed_total

print(f"raw_tokens={raw_total}")
print(f"active_window_tokens={window_total}")
print(f"reloaded_notes_tokens={notes_total}")
print(f"next_call_tokens={next_call_total}")
print(f"removed_tokens={removed_total}")
print("next_call:", summarize(window_items + notes_items))
print("notes:", summarize(notes_items))
print("dropped:", summarize(dropped_items))
Output
raw_tokens=12436
active_window_tokens=3160
reloaded_notes_tokens=186
next_call_tokens=3346
removed_tokens=9090
next_call: alert_RUN_842, latest_trace_span, rollback_runbook, scratchpad, deploy_lookup_tool, trace_lookup_tool, auth_errors_confirmed, migration_lock_ruled_out
notes: auth_errors_confirmed, migration_lock_ruled_out
dropped: old_trace_export, cluster_admin_tool, issue_search_tool, wrong_migration_lock_guess

Here is a greedy packer for an allocated context budget. It places required items first, then fits optional items in priority order:

working-set-budget-packer.py
from dataclasses import dataclass

@dataclass
class Candidate:
    name: str
    tokens: int
    priority: int
    required: bool = False

def pack_working_set(candidates: list[Candidate], budget: int) -> list[str]:
    if type(budget) is not int or budget < 0:
        raise ValueError("budget must be a non-negative integer")
    for item in candidates:
        if type(item.tokens) is not int or item.tokens < 0:
            raise ValueError("item sizes must be non-negative integers")
        if type(item.priority) is not int or type(item.required) is not bool:
            raise ValueError("priority must be an integer and required a boolean")
        if not isinstance(item.name, str) or not item.name.strip():
            raise ValueError("candidate names must be nonempty strings")
    if len({item.name for item in candidates}) != len(candidates):
        raise ValueError("candidate names must be unique")
    ordered = sorted(candidates, key=lambda item: (not item.required, -item.priority))
    selected, used = [], 0
    for item in ordered:
        if used + item.tokens <= budget:
            selected.append(item.name)
            used += item.tokens
        elif item.required:
            raise ValueError(f"required item does not fit: {item.name}")
    return selected

items = [
    Candidate("alert RUN-842", 180, 10, required=True),
    Candidate("latest trace span", 420, 10, required=True),
    Candidate("rollback runbook", 1_800, 9),
    Candidate("stale trace export", 8_200, 1),
]
print(pack_working_set(items, budget=3_000))
try:
    pack_working_set(
        [Candidate("full incident dump", 8_200, 10, required=True)],
        budget=3_000,
    )
except ValueError as exc:
    print(exc)
Output
['alert RUN-842', 'latest trace span', 'rollback runbook']
required item does not fit: full incident dump

This greedy rule can miss the optimal knapsack allocation and ignores dependencies among optional items. Bundle evidence that must travel together. Reserve space for protected instructions, message/tool framing, and the model's output and reasoning allowance before setting this allocation. Count the finalized request with the provider's tokenizer or token-count endpoint; cached and reloaded tokens still count. If required material can't fit, split the task or extract checked spans rather than silently truncating it.

Diagnosing context thrashing

An agent can over-manage its working set. Context thrashing occurs when an agent spends more tokens and tool calls retrieving, summarizing, evicting, and reloading context than completing concrete task steps.

A typical thrashing loop looks like this: turn one fetches a raw trace, turn two aggressively compacts it, turn three realizes the summary omitted a critical request ID, turn four refetches the same trace, and turn five compacts it again. The loop burns budget while producing zero new verified facts.

MetricThrashing signalHealthy signal
Context ops per task stepRepeated compaction and reloads without completed stepsEach operation addresses a named information need
Reload churn rateUnchanged spans are repeatedly refetched after evictionReuse adequate notes; refetch when detail or freshness requires it
Evidence yieldRepeated searches leave the same uncertainty unresolvedFindings, ruled-out paths, or an explicit next gap advance the investigation
Decision latencyRepeated preparation exhausts the task's latency budgetPreparation stays within measured task limits

Pin required evidence until it is no longer needed or must be refreshed. Track repeated queries and missing details; cap retries and context operations when progress stalls. A legitimate search can return no result, so requiring a confirmed fact after every retrieval would block valid investigations or encourage invented findings. After exhausting the budget, report the unresolved gap and request the needed evidence or escalate.

Progressive disclosure and Agent Skills

As workflows mature, procedures shouldn't be copy-pasted into every prompt. An Agent Skill packages reusable procedural knowledge as a directory containing a SKILL.md file along with optional reference docs, scripts, and evaluation assets.[12][13]

Skills implement progressive disclosure across three distinct tiers:

Diagram showing Tier 1: Discovery metadata (~100 tokens per skill) Name + trigger description, Tier 2: Skill activation (<5,000 tokens recommended) Load the SKILL.md body, and Tier 3: Targeted resource Read one reference or run one script.
Tier 1: Discovery metadata (~100 tokens per skill) Name + trigger description, Tier 2: Skill activation (<5,000 tokens recommended) Load the SKILL.md body, and Tier 3: Targeted resource Read one reference or run one script.
  1. Tier 1: Discovery metadata (lightweight prompt layer): The runtime exposes each skill's name and trigger description before loading the full procedure. The documentation's roughly 100 tokens per skill is an estimate; measure the provider's rendered token count for each skill and message role.
  2. Tier 2: Skill activation (playbook layer): When user intent matches the trigger, the runtime loads the full SKILL.md body. The Agent Skills specification recommends keeping those instructions below 5,000 tokens.
  3. Tier 3: Targeted resource (on-demand layer): Specialized references (such as references/handoff-contract.md) or deterministic validation scripts stay on disk until a specific step demands them. A reference consumes context when read; in Anthropic's documented runtime, a script can run without loading its source, returning only its output to the model.[12][13]
incident-triage-skill.txt
incident-triage/
├── SKILL.md
├── references/
│   └── handoff-contract.md
├── scripts/
│   └── validate-handoff.py
└── evals/
    └── routing.json

The frontmatter of SKILL.md defines the routing surface:

incident-triage/SKILL.md
---
name: incident-triage
description: Investigate failed deployments from alerts, traces, and approved runbooks. Use for canary failures, rollback analysis, or incident evidence handoffs.
---

# Incident triage procedure

1. Read the active alert and canary deploy record.
2. Load only investigation-phase tools (`deploy_lookup`, `trace_lookup`).
3. Record each confirmed or disproven finding in the task checkpoint.
4. Read `references/handoff-contract.md` before delegating sub-agent searches.
5. Execute `scripts/validate-handoff.py` before returning evidence.

Never execute a rollback directly. Return a verified proposal with supporting source IDs.

Executing scripts/validate-handoff.py in a runtime with that script boundary returns deterministic exit codes and outputs without copying the validator's source into the model context.

Test routing, execution, and boundary safety separately:

incident-triage/evals/routing.json
[
  {
    "request": "Investigate why canary RUN-842 failed and return trace evidence.",
    "expected_activation": true,
    "expected_artifact": "validated evidence handoff"
  },
  {
    "request": "Summarize our internal vacation policy.",
    "expected_activation": false,
    "expected_artifact": null
  },
  {
    "request": "Roll back RUN-842 now.",
    "expected_activation": true,
    "expected_artifact": "proposal only; no write executed"
  }
]

Skills package instructions and sometimes executable code. Review their publisher, instructions, and scripts before trusting them; use bounded permissions when executing code. Progressive disclosure controls which instructions load. Authorization and execution isolation still come from the runtime, and returned script output consumes context.

Resumable harnesses and durable checkpoints

Long-running sessions need to recover from compaction, network drops, and restarts. A conversational summary alone can omit unfinished work or leave an external action's outcome uncertain.

Anthropic's harness experiments use persistent progress and explicit evaluation to support work across sessions.[14][15] The following five-part resume design builds on that idea. Adapt its artifacts to the framework and task:

ArtifactContentsPurpose
Task manifestUnique task IDs, dependency graphs, acceptance criteriaDefines scope and outstanding dependencies
Progress logLast completed task ID, error traces, verified findingsInforms the next session of prior attempts
Bootstrap commandEnvironment validation script and health checksConfirms clean runtime state before starting work
Work receiptArtifact hash, acceptance version, results, evaluator identity, input revisionsRecords what was checked, against which inputs and criteria
Recovery ruleTimeout limits, retry counts, rollback triggersDictates behavior after an interruption or crash

Treat a model's completion claim as a claim to verify. This simulation checks graph validity and binds a receipt to artifact and input bytes with SHA-256. It assumes a trusted manifest and authentic evaluator receipts; creating the fixture below doesn't perform a real incident evaluation. Predict both failure cases: should a receipt remain valid if the report stays unchanged but its deployment evidence changes?

resume-from-checkpoint.py
from dataclasses import dataclass
import hashlib
import json
from pathlib import Path
from tempfile import TemporaryDirectory

@dataclass(frozen=True)
class WorkItem:
    task_id: str
    depends_on: tuple[str, ...]
    status: str
    receipt: str | None
    contract_version: str = "incident-v1"
    inputs: tuple[str, ...] = ()

def inside(root: Path, relative: str) -> Path:
    if not isinstance(relative, str) or not relative or Path(relative).is_absolute():
        raise ValueError("path must be a nonempty relative string")
    path = (root / relative).resolve()
    if not path.is_relative_to(root.resolve()):
        raise ValueError("path escapes checkpoint root")
    return path

def verified_done(items: list[WorkItem], root: Path) -> set[str]:
    for item in items:
        if not isinstance(item.task_id, str) or not item.task_id.strip():
            raise ValueError("task IDs must be nonempty strings")
        if not isinstance(item.depends_on, tuple) or any(
            not isinstance(dep, str) or not dep.strip() for dep in item.depends_on
        ):
            raise ValueError("dependencies must be task-ID tuples")
    ids = {item.task_id for item in items}
    if len(ids) != len(items):
        raise ValueError("duplicate task IDs")
    for item in items:
        if (
            item.status not in {"pending", "done"}
            or not set(item.depends_on) <= ids
            or len(set(item.depends_on)) != len(item.depends_on)
        ):
            raise ValueError(f"invalid task state: {item.task_id}")
    dependencies = {item.task_id: item.depends_on for item in items}
    visiting, visited = set(), set()

    def visit(task_id):
        if task_id in visiting:
            raise ValueError("dependency cycle")
        if task_id in visited:
            return
        visiting.add(task_id)
        for dependency in dependencies[task_id]:
            visit(dependency)
        visiting.remove(task_id)
        visited.add(task_id)

    for task_id in dependencies:
        visit(task_id)  # Reject cycles even if every involved task says "done".
    completed = set()
    for item in items:
        if item.status != "done":
            continue
        if not item.receipt:
            raise ValueError(f"done task missing receipt: {item.task_id}")
        receipt = json.loads(inside(root, item.receipt).read_text(encoding="utf-8"))
        if not isinstance(receipt, dict) or (
            receipt.get("task_id") != item.task_id
            or receipt.get("passed") is not True
            or receipt.get("evaluator") != "incident-evidence-check"
            or receipt.get("contract_version") != item.contract_version
            or not isinstance(receipt.get("sha256"), str)
            or not isinstance(receipt.get("artifact"), str)
        ):
            raise ValueError(f"receipt does not certify task: {item.task_id}")
        data = inside(root, receipt["artifact"]).read_bytes()
        if hashlib.sha256(data).hexdigest() != receipt["sha256"]:
            raise ValueError(f"artifact changed: {item.task_id}")
        input_hashes = receipt.get("input_sha256")
        if not isinstance(input_hashes, dict) or set(input_hashes) != set(item.inputs):
            raise ValueError(f"receipt input set differs: {item.task_id}")
        for relative, expected in input_hashes.items():
            current = hashlib.sha256(inside(root, relative).read_bytes()).hexdigest()
            if current != expected:
                raise ValueError(f"input changed: {item.task_id}: {relative}")
        completed.add(item.task_id)
    for item in items:
        if item.task_id in completed and not set(item.depends_on) <= completed:
            raise ValueError(f"done task has unfinished dependencies: {item.task_id}")
    return completed

def next_ready(items: list[WorkItem], root: Path) -> WorkItem | None:
    completed = verified_done(items, root)
    for item in items:
        if item.status == "pending" and set(item.depends_on) <= completed:
            return item
    if any(item.status == "pending" for item in items):
        raise ValueError("pending work has no ready task; inspect dependency cycle")
    return None

with TemporaryDirectory() as directory:
    root = Path(directory)
    (root / "artifacts").mkdir()
    (root / "receipts").mkdir()
    artifact = root / "artifacts" / "traces.json"
    artifact.write_text('{"run_id":"RUN-842","spans":["trace-17"]}', encoding="utf-8")
    original_artifact = artifact.read_bytes()
    source = root / "artifacts" / "deploy-record.json"
    source.write_text('{"run_id":"RUN-842","revision":1}', encoding="utf-8")
    receipt = {
        "task_id": "collect-traces",
        "passed": True,
        "evaluator": "incident-evidence-check",
        "contract_version": "incident-v1",
        "artifact": "artifacts/traces.json",
        "sha256": hashlib.sha256(artifact.read_bytes()).hexdigest(),
        "input_sha256": {"artifacts/deploy-record.json": hashlib.sha256(source.read_bytes()).hexdigest()},
    }
    (root / "receipts" / "collect-traces.json").write_text(json.dumps(receipt), encoding="utf-8")
    checkpoint = [
        WorkItem("collect-traces", (), "done", "receipts/collect-traces.json",
                 inputs=("artifacts/deploy-record.json",)),
        WorkItem("verify-cause", ("collect-traces",), "pending", None),
        WorkItem("write-report", ("verify-cause",), "pending", None),
    ]
    selected = next_ready(checkpoint, root)
    assert selected is not None
    print(f"resume_task={selected.task_id}")
    print(f"verified_receipts={sorted(verified_done(checkpoint, root))}")
    artifact.write_text("changed after verification", encoding="utf-8")
    try:
        next_ready(checkpoint, root)
    except ValueError as exc:
        print(exc)
    artifact.write_bytes(original_artifact)
    source.write_text('{"run_id":"RUN-842","revision":2}', encoding="utf-8")
    try:
        next_ready(checkpoint, root)
    except ValueError as exc:
        print(exc)
Output
resume_task=verify-cause
verified_receipts=['collect-traces']
artifact changed: collect-traces
input changed: collect-traces: artifacts/deploy-record.json

This example rejects invalid graphs, unfinished dependencies, changed artifact bytes, and changed declared inputs. The second failure leaves the trace artifact intact but changes the deployment record. Its old receipt no longer certifies work against the current input revision, so the harness stops before selecting the next task. Try declaring an additional required input without adding its hash: the exact input-set check must also fail. A matching hash establishes byte consistency, not factual correctness, evaluator authenticity, or authorization for external actions. The evaluator name and contract version are meaningful only when the host trusts the receipt's issuer and manifest.

In production, restrict receipt issuance and task-state transitions to authorized components. Append-only storage alone doesn't prevent forged new records. Bind receipts to input revisions and acceptance versions, and recheck live state when freshness matters. The path check assumes a stable local filesystem; it isn't race-resistant confinement against another process replacing symlinks or files.

For high-stakes tasks, separate generation from evaluation with a formal contract:[15]

sprint-contract.json
{
  "task_id": "verify-cause",
  "input_receipts": ["receipts/collect-traces.json"],
  "deliverable": "artifacts/cause-analysis.json",
  "acceptance": [
    "causal claims cite checked evidence and address plausible alternatives",
    "disproven migration lock claim is absent",
    "rollback remains a proposal without execution"
  ],
  "evaluator": "incident-evidence-check"
}

The generator drafts the deliverable, and a separate evaluator checks the agreed criteria. Deterministic checks can enforce schemas and forbidden actions; semantic review must examine evidence quality. An independent model can still make mistakes. A citation establishes a reference, not causality.

Failure modeResume symptomArchitectural guardrail
Narrative-only progressSession can't tell which steps passedStructured task IDs and machine-checkable receipts
Premature completionIncomplete work gets marked doneEnforce receipt validation before status transition
Non-idempotent actionDuplicate incident tickets or deployments createdIdempotency keys enforced at the tool boundary
Stale lease collisionAn expired worker continues writingLease fencing tokens enforced by the protected resource
Self-grading biasModel approves its own invalid outputIndependent evaluator role with separate validation logic

Evaluating context curation policies

Curation can remove critical evidence as easily as repetitive logs. Evaluate the decisions, including regressions on protected cases.

First, exclude claims the host's verification process has marked disproven. Preserve the audit archive and a checked negative finding. The following filter applies trusted status labels; it doesn't determine truth:

remove-disproven-facts.py
def rebuild_notes(notes: list[dict]) -> list[str]:
    if any(note["status"] not in {"confirmed", "hypothesis", "disproven"} for note in notes):
        raise ValueError("unknown finding status")
    return [
        f"{note['status']}: {note['text']}"
        for note in notes
        if note["status"] != "disproven"
    ]

notes = [
    {"text": "auth callback errors confirmed", "status": "confirmed"},
    {"text": "migration lock caused RUN-842", "status": "disproven"},
    {"text": "database lock ruled out", "status": "confirmed"},
]
print("rebuilt notes:", rebuild_notes(notes))
Output
rebuilt notes: ['confirmed: auth callback errors confirmed', 'confirmed: database lock ruled out']

Next, compare raw and curated contexts on paired cases with the same task and evidence. Here is an illustrative gate requiring fewer allocated tokens, no aggregate score loss, and all critical cases to pass. Other policies may accept more tokens for a quality gain. The outcomes below are invented to demonstrate the rule:

curation-release-gate.py
from dataclasses import dataclass

@dataclass(frozen=True)
class Outcome:
    raw_ok: bool
    curated_ok: bool
    critical: bool = False
    poisoned_references: int = 0

def approve_curation(cases: list[Outcome], raw_tokens: int, curated_tokens: int) -> bool:
    if any(type(count) is not int or count <= 0 for count in (raw_tokens, curated_tokens)):
        raise ValueError("token counts must be positive integers")
    for case in cases:
        if any(type(flag) is not bool for flag in (case.raw_ok, case.curated_ok, case.critical)):
            raise ValueError("outcome flags must be booleans")
        if type(case.poisoned_references) is not int or case.poisoned_references < 0:
            raise ValueError("poisoned_references must be a non-negative integer")
    if not cases or not 0 < curated_tokens < raw_tokens:
        return False
    no_critical_failures = all(case.curated_ok for case in cases if case.critical)
    no_poisoning = all(case.poisoned_references == 0 for case in cases)
    score_holds = sum(case.curated_ok for case in cases) >= sum(case.raw_ok for case in cases)
    return no_critical_failures and no_poisoning and score_holds

cases = [
    Outcome(True, True), Outcome(True, False, critical=True),
    Outcome(False, True), Outcome(True, True), Outcome(False, True),
    Outcome(True, True), Outcome(True, True), Outcome(False, False),
    Outcome(True, True), Outcome(True, True),
]
gains = sum(not case.raw_ok and case.curated_ok for case in cases)
regressions = sum(case.raw_ok and not case.curated_ok for case in cases)
print(f"raw_success={sum(case.raw_ok for case in cases)}/{len(cases)}")
print(f"curated_success={sum(case.curated_ok for case in cases)}/{len(cases)}")
print(f"gains={gains}; regressions={regressions}")
print("candidate_approved:", approve_curation(cases, raw_tokens=12_436, curated_tokens=3_346))
Output
raw_success=7/10
curated_success=8/10
gains=2; regressions=1
candidate_approved: False

The mock candidate scores 8/10 versus 7/10 and has a 73.1% smaller synthetic allocation, yet the gate rejects its critical failure. These numbers aren't a measured curation benchmark. For a real release, repeat paired runs, record model and policy versions, inspect evidence support and authorization behavior, and measure actual input/output tokens, latency, and cost.

persists thread-scoped state through checkpoints and provides stores for memory across threads.[16] Persistence doesn't choose which stored information belongs in the next request. That selection, its trust boundaries, and its evaluation remain application responsibilities.

A full window can still be the wrong working set. Budget what the decision needs, and reject a curation change that breaks a protected case. Curation still assumes the tokens you kept are allowed to be there. Untrusted instructions inside that working set are a different failure.

Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.A new packet has two observed defects: the plan repeatedly treats a “DNS outage” guess that the host verifier ruled out as fact, and a retrieved procedure tells the assistant to emit CSV despite the application’s JSON contract. Which paired repair matches those defects?
2.A controlled application probe keeps the same question, evidence and answer key. Moving one decisive span from the beginning to the middle changes success from 18/20 to 12/20; a separate two-source synthesis probe scores 9/20. Which report is supported?
3.After reservations, a packer has 3,000 units. Required R=400 and S=700 come first. Optional A=1,400, B=600 and C=400 have descending priorities 9, 8 and 7. Apply the taught greedy rule. What is selected, and what happens in a separate run whose only required item has size 3,100?
4.Use a toy tokenizer with every token boundary eligible for reuse. A fixed root has 80 tokens, a changing request ID has 16, and an unchanged document has 240. Compare root–ID–document with root–document–ID across two ID values whose first token differs while the ID length stays 16. What are the longest common prefixes and complete logical input lengths?
5.There are five ordered tool results, indexed 0 through 4. Keep the latest two raw results. Their exact Boolean finding-recorded flags are [false, true, true, true, false]. Which payloads does the article pruning rule replace, and which transcript property must be preserved?
6.A worker sends a valid UTF-8 JSON object of 1,590 bytes with exactly claim, source_ids and next_check, all required types, and known authorized source S9. Its claim says S9 reports HTTP 200, but S9 actually reports 503. A second otherwise valid object is 1,610 bytes. Under the 1,600-byte contract, which paired result follows?
7.A declared skills runtime exposes eight metadata entries of 75 assigned units each. It activates one 1,800-unit body, reads a 350-unit reference, and runs a script whose 90-unit output enters the prompt while its 900-unit source stays on disk. What input allocation is added, and does activation authorize a prohibited write?
8.A trusted done receipt still matches artifact A and declared input I1. The current manifest now requires both I1 and I2, but the receipt contains only I1. All task IDs and graph edges are valid. Which state transition follows?
9.On 20 paired cases, raw succeeds on 13 and curated on 16. Input falls from 6,000 to 4,200 units, and poisoning counts are zero. One protected curated case fails. Repairing only that case raises curated success to 17. Under the article’s gate, which before/after decision and token reduction follow?
10.An agent performs six retrieve/compact/reload operations with zero completed task steps because each summary loses the same request ID. A previous external write also has an unknown outcome after a lost response. Which next plan respects both contracts?

10 questions remaining.

References

Effective context engineering for AI agents

Anthropic · 2025

https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents

A Survey of Context Engineering for Large Language Models.

Multiple authors · 2025 · arXiv preprint

https://arxiv.org/abs/2507.13334

Lost in the Middle: How Language Models Use Long Contexts

Liu, N.F., et al. · 2024 · TACL 2024

https://aclanthology.org/2024.tacl-1.9/

Context Rot: How Increasing Input Tokens Impacts LLM Performance

Hong, K., Troynikov, A., & Huber, J. · 2025

https://www.trychroma.com/research/context-rot

Needle In A Haystack: Pressure Testing LLMs

Kamradt, G. · 2023

https://github.com/gkamradt/needle-in-a-haystack

How Long Contexts Fail

Breunig, D. · 2025

https://www.dbreunig.com/2025/06/22/how-contexts-fail-and-how-to-fix-them.html

Context Engineering

LangChain · 2025

https://www.langchain.com/blog/context-engineering-for-agents

Agent Memory: How to Build Agents that Learn and Remember

Letta · 2026

https://www.letta.com/blog/agent-memory/

Prompt caching (Claude API documentation)

Anthropic · 2026 · Official documentation

https://platform.claude.com/docs/en/build-with-claude/prompt-caching

Prompt caching

OpenAI · 2026

https://developers.openai.com/api/docs/guides/prompt-caching

LLMLingua: Compressing Prompts for Accelerated Inference of Large Language Models

Jiang, H., et al. · 2023 · EMNLP

https://arxiv.org/abs/2310.05736

Agent Skills Specification

Agent Skills · 2026

https://agentskills.io/specification

Agent Skills

Anthropic · 2026

https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview

Effective harnesses for long-running agents

Anthropic · 2025

https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents

Harness design for long-running application development

Anthropic · 2026

https://www.anthropic.com/engineering/harness-design-long-running-apps

LangGraph Memory Overview

LangChain · 2026

https://docs.langchain.com/oss/python/concepts/memory

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.