1. Context Engineering
Key Points
- Context engineering curates the smallest set of high-signal tokens for inference, managing the entire context state, not just one prompt.
- Context is the whole state: system prompt, tool and MCP definitions, injected data, and message history all compete for the same finite window.
- Bloat is low-value token buildup; drift is loss of task focus; rot is declining recall that begins before the hard limit.
- Defend against all three with progressive disclosure and just-in-time retrieval (keep identifiers, load content only when needed).
- Three composable levers: compaction (summarize, costs inference), clearing (prune re-fetchable tool output, free), and the memory tool (persist across sessions). Diagnose the bottleneck first.
Context engineering is the discipline of curating the optimal set of tokens the model sees at inference time — "the smallest possible set of high-signal tokens that maximize the likelihood of some desired outcome." Context is not just the current user message; it is the whole state the model sees. Think of the window as a workbench, not a warehouse: keep it clear of clutter so the tools that matter stay within reach.
Two failure modes threaten a long-running context. Context bloat is the accumulation of low-value tokens; context drift is the subtler wandering of focus from the original task. The root cause of degradation is context rot: as tokens grow, recall drops — and it drops before the hard limit is reached. The transformer's n² pairwise relationships stretch the model's "attention budget," so "more tokens makes agents worse." The defense is progressive disclosure with just-in-time retrieval: keep lightweight identifiers (paths, IDs, URLs) and load content only when needed.
| Problem | What it is | Symptom | Primary remedy |
| Context bloat | Accumulation of low-value tokens | Rising cost, slower turns, filled window | Tool-output pruning / clearing |
| Context drift | Focus wanders from the task | Off-topic answers, forgotten constraints | Compaction; progressive disclosure |
| Context rot | Recall degrades as tokens grow | Missed facts even below the hard limit | Keep the window small; isolate sub-tasks |
Anthropic's cookbook documents three complementary levers, applied diagnosis-first. Compaction (compact_20260112) summarizes a conversation nearing the limit and reinitializes the window; it costs inference but preserves decisions, key facts, and task state. You must append the full response.content (including the compaction block) each turn or the state is silently lost. Tool-result clearing (clear_tool_uses_20250919) mechanically replaces old tool_result bodies with a placeholder at no inference cost (a ~67% reduction in Anthropic's example) but invalidates the prompt cache on each firing. The memory tool (memory_20250818) stores notes in external files outside the window — the only lever that persists across sessions.
Figure 10.1: Diagnosis-first decision flow for choosing a context-management lever
flowchart TD
A["Window nearing the limit"] --> B{"What is the bottleneck?"}
B -->|"Accumulated dialogue and reasoning"| C["Compaction: summarize + reinitialize window"]
B -->|"Re-fetchable tool output"| D["Clearing: replace tool_result bodies with placeholder"]
B -->|"Cross-session knowledge"| E["Memory tool: store notes in external files"]
C --> F["Compose levers together on long-running agents"]
D --> F
E --> F
| Technique | Mechanism | Inference cost | Cross-session? | Default trigger | Cache impact |
Compaction (compact_20260112) | Summarize + reinitialize window | Yes (summarizer runs) | No | 150K input tokens | Rewrites history |
Clearing (clear_tool_uses_20250919) | Replace old tool_result bodies | No | No | 100K tokens | Invalidates cache on firing |
Memory (memory_20250818) | Store notes in external files | Reads only | Yes | N/A (agent-driven) | Neutral |
[Animation slot: token accumulation across turns, showing how clearing collapses a 128K window to ~43K while keeping the most recent read.]
2. Context Isolation
Key Points
- Context isolation gives separate parts of a workload separate windows via subagents, so no single window holds everything.
- The return contract is key: a subagent does deep work but returns only a 1,000–2,000-token summary to the orchestrator.
- Many isolated agents beat one monolithic agent (higher signal density, less rot, parallelism) — but disable unused MCP servers first, since subagents inherit every parent tool definition.
- Isolation is the structural defense for multi-step workflows; within-window (compaction, clearing) and cross-window (isolation, memory) techniques compose.
- Keep trusted context (your prompt/tools) separate from untrusted context (fetched pages, documents, third-party results) using the non-spoofable system-role channel; treat external content as data, never commands.
Context isolation gives separate parts of a workload separate context windows. The canonical mechanism is a multi-agent architecture: a lead (orchestrator) delegates sub-tasks to subagents, each with its own isolated window. The design property that makes it work is the return contract — a subagent reads many files or runs many tool calls in its own window but returns only a condensed summary, typically 1,000–2,000 tokens. The orchestrator accumulates distilled results, not raw exploration. Many isolated agents outperform one monolithic agent for three reasons: higher signal density, no single window holding everything (countering rot), and parallelism. One caution: subagents inherit every parent MCP tool definition, so disable unused MCP servers before spawning them.
Figure 10.2: Subagent context isolation — deep work in isolated windows, thin summaries returned
flowchart LR
O["Orchestrator (lead agent)
accumulates distilled results"]
S1["Subagent 1
isolated window"]
S2["Subagent 2
isolated window"]
S3["Subagent 3
isolated window"]
M["Shared memory file"]
O -->|"task assignment"| S1
O -->|"task assignment"| S2
O -->|"task assignment"| S3
S1 -->|"1K-2K token summary"| O
S2 -->|"1K-2K token summary"| O
S3 -->|"1K-2K token summary"| O
S1 -.->|"read / write"| M
S3 -.->|"read / write"| M
Isolation matters for security too. Trusted context is content you control — your system prompt, tool definitions, operator instructions. Untrusted context is anything from outside your control — a fetched page, an uploaded document, a third-party tool result — which may carry prompt-injection attempts. Deliver mid-conversation operator instructions as a {"role": "system", ...} message: that is a non-spoofable operator channel that injected text cannot forge. The practical rule: treat every tool result, fetched page, and uploaded document as data to reason about, never as instructions to obey — and route untrusted processing into a narrow-tool subagent to limit blast radius.
[Animation slot: orchestrator dispatching parallel subagents, each window filling then collapsing to a thin 1K–2K summary arrow back up.]
3. Output Handling
Key Points
- Structured output = JSON outputs (
output_config.format) plus strict tool use (strict: true), both driven by grammar-constrained sampling that restricts generation to schema-valid tokens.
- Schema rules: every object needs
additionalProperties: false and all properties in required; not supported are recursion, numeric/length constraints, and complex regex.
- Define schemas natively (Pydantic, Zod) and let the SDK's
parse() return a validated, typed object.
- Defensive parsing: always branch on
stop_reason before parsing — refusal and max_tokens legitimately break the schema, and enum casing may differ. Catch typed SDK exceptions, not error strings.
- A schema guarantees an answer's shape, never its truth; reduce hallucination (allow "I don't know," ground in quotes, cite, chain-of-thought, best-of-N) and cross-check facts.
Structured output constrains Claude's responses to a JSON schema through two features: JSON outputs (output_config.format) control what Claude says, and strict tool use (strict: true) validates how Claude calls your functions. Both rely on grammar-constrained sampling — the schema is compiled into a grammar that actively prevents emitting a schema-breaking token, unlike merely prompting "please return valid JSON." Every object must set "additionalProperties": false and list every property in required. Not supported: recursion, numeric constraints (minimum/maximum), string-length constraints, and complex regex. Note that strict: true is a sibling of name/description/input_schema on the tool — not on tool_choice.
Grammar-constrained sampling removes most parse failures — not all. Defensive parsing never assumes the schema held. Three cases matter: refusals (stop_reason: "refusal", content may be empty/partial), truncation (stop_reason: "max_tokens", output incomplete), and enum casing differences. The rule: always branch on stop_reason before touching content, and catch typed exceptions (e.g., anthropic.RateLimitError) rather than string-matching messages.
Figure 10.3: Defensive parsing — branch on stop_reason before touching content
flowchart TD
A["Response received"] --> B{"stop_reason?"}
B -->|"refusal"| C["handle_refusal(stop_details)
do not parse — content may be empty/partial"]
B -->|"max_tokens"| D["handle_truncation()
incomplete — retry with more tokens or stream"]
B -->|"other"| E["data = response.parsed_output
safe to consume"]
E --> F["Validate parsed object
fallback path if schema did not hold"]
Structured, well-formed output can still be wrong. A schema guarantees the shape of an answer, never its truth — a beautifully typeset invoice is not proof the amounts are correct. Five techniques reduce and audit hallucination: (1) allow uncertainty (permit "I don't know," which alone drastically reduces fabrication); (2) ground in direct quotes for long documents; (3) verify with citations (the Citations API ties responses to source chunks); (4) chain-of-thought verification; and (5) best-of-N verification. Cross-check load-bearing numbers, dates, and citations against trusted sources.
[Animation slot: token-by-token generation with a grammar mask graying out invalid next-tokens, then a stop_reason branch splitting into refusal / max_tokens / safe-to-parse paths.]
4. Debugging and Error Handling
Key Points
- Claude API errors follow a predictable HTTP taxonomy with a typed JSON
error object and a request_id; the retryable column drives recovery.
- Fix non-retryable 4xx errors yourself; let the SDK's automatic exponential backoff (twice by default, honoring
retry-after) handle transient 429/5xx/529.
- Trace analysis reconstructs a failure from artifacts — chiefly the
request-id header; log it on every failure. Catch typed exceptions most-specific-first.
- Isolate origin: integration-layer (yours: 4xx, 429), provider-side (Anthropic's: 5xx, 529), or model-output/state ("off but no error").
- 429 vs. 529 is the sharpest test: 429 is your responsibility (honor
retry-after, ramp traffic); 529 is Anthropic capacity (retry with jitter, circuit breaker, model fallback). Prefer /rewind over in-thread correction.
When a request fails, first identify what kind of failure it is. The API returns a JSON error object with a type and message, plus a request_id, mapped onto HTTP status codes. Recovery follows the "retryable?" column: non-retryable 4xx errors are yours to fix; retryable errors (429, 5xx, 529) call for exponential backoff, which the official SDKs do automatically (twice by default, honoring retry-after).
| Code | Error type | Retryable? | Common cause |
| 400 | invalid_request_error | No | Malformed/invalid request |
| 401 | authentication_error | No | API key problem |
| 402 | billing_error | No | Billing/payment issue |
| 403 | permission_error | No | Key lacks permission |
| 404 | not_found_error | No | Resource not found (often a typo'd model ID) |
| 409 | conflict_error | Yes | Conflicts with resource state — resolve and retry |
| 413 | request_too_large | No | Exceeds byte limits |
| 429 | rate_limit_error | Yes | Account hit a rate limit |
| 500 | api_error | Yes | Unexpected internal error |
| 504 | timeout_error | Yes | Request timed out — use streaming |
| 529 | overloaded_error | Yes | API temporarily overloaded across all users |
Figure 10.4: Error recovery decision tree driven by the retryable column
flowchart TD
A["Request fails with error type + status code"] --> B{"Retryable?"}
B -->|"No — 400/401/402/403/404/413"| C["Fix in your code or configuration
(correct request, valid key, model ID)"]
B -->|"Yes — 409"| D["Resolve resource-state conflict, then retry"]
B -->|"Yes — 429/500/504/529"| E["Exponential backoff, honor retry-after"]
E --> F{"Custom behavior needed?"}
F -->|"No"| G["SDK auto-retries (twice by default)"]
F -->|"Yes"| H["Configure max_retries / custom logic"]
Trace analysis reconstructs what happened from a request's artifacts. The most important is the request-id header (e.g., req_018EeWyXxfu5pfWkrYcMdjWG), mirrored as request_id in error bodies and exposed in Python/TypeScript as _request_id. Log it on every failure and include it in support tickets. Catch typed exceptions most-specific-first (e.g., NotFoundError before APIStatusError). Context-state diagnostics like /context tell you why a window is bloated so you can pick a remedy (/compact, /clear, /mcp disable).
The most important judgment is where a problem lives. The 429 vs. 529 distinction is the sharpest expression: a 429 is your responsibility (tier limit or a usage spike — honor retry-after, ramp traffic gradually); a 529 is not your fault (Anthropic-wide capacity, usually resolving in ~30–300 seconds — retry with jitter, a ~60s circuit breaker, and model fallback). A third origin — quality degradation with no error — is almost always conversation state; Claude Code does not silently change models.
| Signal | Origin | Whose fault | Recovery |
429 rate_limit_error | Integration layer | Yours (tier/spike) | Honor retry-after; ramp traffic gradually |
529 overloaded_error | Provider side | Anthropic capacity | Bounded retry + jitter, circuit breaker, model fallback |
| "Off but no error" | Model output / state | Usually conversation state | /context, /compact, /clear, /model, /effort, /rewind |
Figure 10.5: Isolating the origin of a failure — integration-layer vs. provider-side vs. model-output
flowchart TD
A["Something went wrong"] --> B{"Is there an error code?"}
B -->|"4xx (except 409)"| C["Integration-layer — yours to fix
429: honor retry-after, ramp traffic"]
B -->|"5xx or 529"| D["Provider-side — Anthropic's
retry w/ jitter, circuit breaker, /model fallback, check status page"]
B -->|"No error, output off"| E["Model-output / conversation state"]
E --> F["/context, /compact, /clear"]
E --> G["/model, /effort"]
E --> H["/rewind — remove bad turn instead of correcting in-thread"]
One recovery technique deserves emphasis: when a response goes wrong, rewinding (Esc twice or /rewind) beats correcting in-thread, because correcting keeps the wrong attempt in context and anchors later answers to it. Claude Code retries transient failures up to 10 times with exponential backoff, but deliberately does not retry TLS certificate-validation failures or server errors that arrive after visible output has streamed (a retry could double-execute tools).
[Animation slot: a failure flowing through the isolation checklist — error-code gate branching to integration-layer / provider-side / model-state, with 429 and 529 highlighted on opposite sides.]