Study guides / CCAR-F / Domain 5

Context Management & Reliability · Lesson 1 of 6

5.1 - Context Window Management

Manage conversation context to preserve critical information across long interactions, including the progressive summarisation trap, lost-in-the-middle effect, and tool result trimming.

Context window management is the foundation of reliable Claude-based systems. Every multi-turn conversation, every multi-agent pipeline, every long-document extraction task depends on what you let into the context window. Get it wrong and the failures are concrete: your support agent forgets refund amounts, your research pipeline drops citations, your extraction system loses precision on the fields that matter most.

The Progressive Summarisation Trap

When conversations grow long, a common strategy is to summarise earlier turns to free up token budget. This is a trap. Progressive summarisation systematically destroys the most critical information in customer-facing and data-processing systems: numerical values, dates, percentages, and customer-stated expectations.

Here is how it plays out. A customer contacts support about a refund:

Turn 3: "I'd like a refund of $247.83 for order #8891 placed on March 3rd"

After summarisation, this becomes:

Summary: "Customer wants a refund for a recent order"

The amount, order number, and date - the three facts the agent needs to process the refund - are gone. And that is not a fringe case. It is what summarisation does to transactional data by default.

The fix: persistent case facts blocks. Extract transactional facts (amounts, dates, order numbers, statuses) into a structured block that is included in every prompt, outside the summarised history. This block is never summarised. It persists across every turn regardless of what happens to the conversation history.

{
  "caseFactsBlock": {
    "customerId": "C-4421",
    "issues": [
      {
        "orderId": "#8891",
        "orderDate": "2024-03-03",
        "refundAmount": "$247.83",
        "status": "pending_refund",
        "itemDescription": "Wireless headphones - defective"
      }
    ]
  }
}

For multi-issue sessions where a customer raises several problems in one conversation, extract and persist structured issue data into a separate context layer. Each issue gets its own entry with order IDs, amounts, and statuses. This prevents cross-contamination between issues during summarisation.

The "Lost in the Middle" Effect

Models process information at the beginning and end of long inputs reliably. Findings buried in the middle of a long context may be missed or given less weight. This is a well-documented phenomenon in large language models and it directly affects how you structure aggregated inputs.

The fix is structural, not prompt-based. Place key findings summaries at the beginning of aggregated inputs. Organise detailed results with explicit section headers throughout. If you are feeding a synthesis agent the output of three research subagents, start with a "Key Findings Summary" section, then provide the detailed outputs with clear section boundaries.

## Key Findings Summary
- Source A: 12% market growth in renewable sector (2023)
- Source B: Patent filings increased 34% year-on-year
- Source C: Regulatory framework delayed until Q3 2025

## Detailed Findings

### Source A: Market Analysis Report
[Full details here...]

### Source B: Patent Database Analysis
[Full details here...]

### Source C: Regulatory Review
[Full details here...]

Tool Result Trimming

Tool results are a silent context budget killer. An order lookup might return 40+ fields: internal audit timestamps, warehouse codes, shipping carrier IDs, fulfilment centre identifiers, and dozens of other fields irrelevant to the customer's refund request. You need 5 fields. Those other 35 fields consume tokens in every subsequent turn as the conversation history grows.

Trim verbose tool outputs to only relevant fields before they accumulate in context. Skip it and multi-turn systems slowly drown in stale tool output. It is not a nice-to-have.

def trim_order_result(raw_result, relevant_fields=None):
    if relevant_fields is None:
        relevant_fields = [
            "order_id", "order_date", "total_amount",
            "return_eligible", "item_description"
        ]
    return {k: v for k, v in raw_result.items() if k in relevant_fields}

This trimming should happen in a PostToolUse hook or in the tool implementation itself, before the result enters the conversation history. Once verbose data is in the context, it stays there for every subsequent turn.

Full Conversation History

The Claude API is stateless. Each request must include the complete conversation history. Omit earlier messages and the model loses conversational coherence. There's no session state on the server side, so every turn has to carry everything the model needs to follow the conversation.

This creates a tension with context limits: you need the full history for coherence, but the history grows with every turn. The persistent case facts block resolves this by separating critical facts from summarisable narrative, letting you summarise the conversation flow while preserving every transactional detail.

Upstream Agent Optimisation

In multi-agent systems, upstream agents often return verbose reasoning chains and raw content that downstream agents do not need. When a research subagent sends its full thought process to a synthesis agent with a limited context budget, the synthesis agent wastes tokens on reasoning it cannot use.

Modify upstream agents to return structured data - key facts, citations, relevance scores - instead of verbose content and reasoning chains. Require subagents to include metadata (dates, source locations, methodological context) in structured outputs to support accurate downstream synthesis.

{
  "findings": [
    {
      "claim": "Renewable energy investment grew 12% in 2023",
      "source": "IEA World Energy Report 2024",
      "sourceUrl": "https://example.com/report",
      "relevanceScore": 0.92,
      "publicationDate": "2024-01-15"
    }
  ]
}

Tokens aren't the only win here. Structured outputs from upstream agents let downstream agents process findings without re-parsing verbose prose.

Key Concept

The persistent case facts block is the single most important pattern in context window management. Extract transactional facts (amounts, dates, order numbers) into a structured block that is included in every prompt and never summarised. This is the fix for progressive summarisation and the foundation for reliable multi-turn systems.

Prompt Caching

Prompt caching is the other half of context economics. Instead of trimming what the model sees, you avoid paying to reprocess the parts that don't change. Mark a stable prefix with a cache_control breakpoint and the API stores that processed prefix, then reuses it on the next request, charging a fraction of the input cost for the cached tokens.

Caching matches from the start of the prompt, prefix by prefix, so layout decides whether you get a hit. Put the content that stays constant first: system instructions, tool definitions, long reference documents. Place the cache_control breakpoint at the end of that static block. Put the volatile content, the user's latest message and anything that changes per request, after the breakpoint.

messages = [
    {
        "role": "system",
        "content": [
            {"type": "text", "text": LONG_STATIC_INSTRUCTIONS},
            {"type": "text", "text": REFERENCE_DOC,
             "cache_control": {"type": "ephemeral"}},
        ],
    },
    {"role": "user", "content": dynamic_user_message},
]

Get the order wrong and you lose the benefit entirely. If dynamic content sits before the static block, the prefix changes on every request, nothing matches, and every call pays full price. The cache is also short-lived: an ephemeral breakpoint lasts about five minutes since last use, so caching pays off for bursts of related requests, not for content reused hours apart.

Exam traps

Practice question

A customer support agent handles a multi-issue session. After several turns, the agent refers to 'your recent refund request' instead of the specific $247.83 refund for order #8891. The conversation history is being summarised between turns to manage context length. What is the most effective fix?

  • A Increase the context window size so the full conversation history fits and summarisation never needs to run

    This postpones the problem but does not solve it - eventually the context will fill, and summarisation will still destroy specifics.

  • B Store the full conversation history in an external database and retrieve the relevant turns on demand whenever the agent needs to recall an earlier detail

    This adds infrastructure complexity without addressing the core issue of which facts must persist in every prompt.

  • C Instruct the model to preserve all numerical values verbatim whenever it summarises the conversation history

    Prompt-based instructions for summarisation are unreliable - the model will still compress details probabilistically.

  • D Extract transactional facts (amounts, dates, order numbers) into a persistent case facts block included in every prompt, outside summarised history Correct

    This directly addresses the progressive summarisation trap by ensuring critical numerical and transactional data is never condensed.

Build exercise: Build a Persistent Case Facts Context Manager

Intermediate · 45 minutes

You'll practice:

  1. Create a case facts extractor that identifies transactional data (amounts, dates, order numbers, statuses) from tool results

    The persistent case facts block is the single most important pattern in context window management. Extracting transactional facts into a structured block that is never summarised prevents the progressive summarisation trap from destroying critical numerical values and identifiers.

    You should see: A function that takes raw tool output and returns a structured object containing only the transactional facts: customer ID, order numbers, amounts, dates, and statuses. Non-transactional narrative content should be excluded.

    Hints
    1. Focus on data types that summarisation destroys: numbers, dates, identifiers, and statuses. These are the fields a support agent needs to process a refund.
    2. Define a fixed set of transactional field types to extract: monetary amounts, dates, order/reference numbers, status values, and customer identifiers.
    3. function extractCaseFacts(toolResult) {
        return {
          customerId: toolResult.customer_id,
          issues: [{
            orderId: toolResult.order_id,
            orderDate: toolResult.order_date,
            refundAmount: toolResult.total_amount,
            status: toolResult.status,
            itemDescription: toolResult.item_description
          }]
        };
      }
      
      // Usage: extract facts from every tool result
      const caseFacts = extractCaseFacts(orderLookupResult);
  2. Implement a persistent case facts block that is prepended to every prompt, outside summarised history

    The case facts block must persist across every turn regardless of what happens to the conversation history. It sits outside the summarised portion of the context, ensuring amounts, dates, and order numbers survive even when earlier conversation turns are compressed.

    You should see: A prompt construction function that always includes the case facts block at the top of every message, followed by any summarised history, followed by the current turn. The case facts block should be clearly delimited with a section header.

    Hints
    1. Use a clear delimiter like a section header to separate the case facts block from the conversation history so the model treats them differently.
    2. The case facts block should be formatted as structured data (JSON or a clear key-value list) prepended to the system prompt or first user message of every API call.
    3. function buildPrompt(caseFacts, summarisedHistory, currentMessage) {
        const caseFactsBlock = `## Active Case Facts (DO NOT SUMMARISE)\n` +
          `${JSON.stringify(caseFacts, null, 2)}\n\n`;
        
        return [
          { role: "user", content: caseFactsBlock + summarisedHistory },
          { role: "assistant", content: "I have the case facts and history." },
          { role: "user", content: currentMessage }
        ];
      }
  3. Build a tool result trimmer that filters order lookup responses from 40+ fields to only the 5 relevant return-related fields

    Untrimmed tool results are a silent context budget killer. An order lookup returning 40+ fields consumes tokens in every subsequent turn as conversation history grows. Trimming to relevant fields before results enter context is essential, not optional.

    You should see: A trimming function that takes a raw tool result object and returns only the fields needed for the current task. The trimmed result should be 80-90% smaller than the original.

    Hints
    1. Implement this as a PostToolUse hook or within the tool implementation itself, before the result enters conversation history.
    2. Define the relevant fields based on the task context. For a refund workflow: order_id, order_date, total_amount, return_eligible, and item_description.
    3. function trimToolResult(rawResult, relevantFields = null) {
        if (!relevantFields) {
          relevantFields = [
            "order_id", "order_date", "total_amount",
            "return_eligible", "item_description"
          ];
        }
        return Object.fromEntries(
          Object.entries(rawResult).filter(([k]) => relevantFields.includes(k))
        );
      }
      
      // Before: 40+ fields, ~2000 tokens
      // After: 5 fields, ~200 tokens
      const trimmed = trimToolResult(orderLookupResult);
  4. Test with a multi-turn conversation where summarisation occurs and verify that transactional facts survive intact across all turns

    This validates that the persistent case facts pattern actually works. The exam tests whether you understand that progressive summarisation destroys specific amounts and dates, and the case facts block is the fix. You need to verify this empirically.

    You should see: A 6-8 turn conversation where summarisation occurs after turn 4. After summarisation, the agent should still reference the exact refund amount ($247.83), order number (#8891), and date (March 3rd) from the case facts block. Without the block, these values would be lost to summarisation.

    Hints
    1. Run two versions: one with the case facts block and one without. After summarisation, ask the agent to confirm the refund amount and order number. The version without the block will lose specifics.
    2. Simulate summarisation by replacing turns 1-4 with a compressed summary after turn 4, keeping the case facts block intact in both versions.
    3. // Simulate multi-turn conversation with summarisation
      const caseFacts = { orderId: "#8891", refundAmount: "$247.83", orderDate: "2024-03-03" };
      
      // Turn 5: after summarisation
      const withCaseFacts = buildPrompt(caseFacts, "Customer wants a refund for a recent order", "Please confirm the refund details.");
      // Agent should reference: $247.83, #8891, March 3rd
      
      const withoutCaseFacts = buildPrompt(null, "Customer wants a refund for a recent order", "Please confirm the refund details.");
      // Agent will say: your recent refund request (no specifics)
  5. Add key findings placement logic that positions summaries at the beginning of aggregated inputs to mitigate the lost-in-the-middle effect

    Models process information at the beginning and end of long inputs reliably, but findings buried in the middle may be missed. Placing key findings summaries at the start of aggregated inputs is a structural fix for this well-documented phenomenon.

    You should see: An aggregation function that places a Key Findings Summary section at the top of combined inputs, followed by detailed results with explicit section headers. The key findings should be concise bullet points drawn from the detailed content.

    Hints
    1. The fix is structural, not prompt-based. Do not tell the model to pay attention to everything. Instead, reorganise the input so critical information is at the beginning.
    2. Extract one-line summaries from each source and place them in a Key Findings Summary section at the top. Follow with detailed results using clear section headers.
    3. function aggregateWithKeyFindings(sources) {
        const keyFindings = sources.map(s => `- ${s.name}: ${s.keyClaim}`);
        
        return `## Key Findings Summary\n${keyFindings.join("\n")}\n\n` +
          `## Detailed Findings\n\n` +
          sources.map(s => `### ${s.name}\n${s.fullContent}`).join("\n\n");
      }
      
      // Key findings at top, detailed content with headers below
      // Mitigates lost-in-the-middle effect structurally

Sources