<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Agent Loop Notes</title><link>https://agentloopnotes.com/</link><description>Hands-on guides to building AI agents: evals, tool design, memory, debugging and cost. Clear explanations, working code, cited sources.</description><language>en</language><lastBuildDate>Fri, 06 Nov 2026 23:05:23 +0000</lastBuildDate><atom:link href="https://agentloopnotes.com/feed.xml" rel="self" type="application/rss+xml"/><item><title>How to Write Evals for an AI Agent: A Practical Starter Guide</title><link>https://agentloopnotes.com/posts/agent-evals-starter-guide/</link><guid isPermaLink="true">https://agentloopnotes.com/posts/agent-evals-starter-guide/</guid><pubDate>Fri, 06 Nov 2026 16:00:00 +0000</pubDate><description>Build your first agent eval suite step by step: pick real tasks, grade outcomes instead of paths, measure consistency with pass^k and read the transcripts.</description><content:encoded><![CDATA[<p>Unit tests tell you whether a function returns the right value. They say very little about whether an agent will book the right meeting, file the right ticket or stop when it should. Agents make a chain of decisions, each one shaped by what came before, and the same input can lead to a different chain on the next run.</p>
<p>An eval suite is the test suite for that behavior. Most teams know they need one and put it off, because it sounds like a research project. It does not have to be. This guide walks through a small, useful first version you can build in an afternoon, then shows how to grow it.</p>
<h2 id="the-vocabulary-in-one-minute">The vocabulary, in one minute</h2>
<p>Anthropic's engineering post <a href="https://www.anthropic.com/engineering/demystifying-evals-for-ai-agents">Demystifying evals for AI agents</a> uses a set of terms that are worth adopting, because they keep conversations about evals precise:</p>
<ul>
<li><strong>Task:</strong> one test case, with defined inputs and a definition of success.</li>
<li><strong>Trial:</strong> one attempt at a task. You run several, because agent output varies.</li>
<li><strong>Grader:</strong> the logic that scores a trial. A task can have more than one.</li>
<li><strong>Transcript:</strong> the full record of a trial: messages, tool calls, intermediate results.</li>
<li><strong>Outcome:</strong> the state of the world when the trial ends.</li>
</ul>
<p>That last distinction matters more than it looks. The outcome is whether the reservation exists in the database, not whether the agent said "I booked it."</p>
<h2 id="step-1-collect-20-to-50-tasks-from-real-failures">Step 1: Collect 20 to 50 tasks from real failures</h2>
<p>You do not need hundreds of tasks to start. Early on, the differences between versions of your agent are large, and a small set catches them. The Anthropic post suggests 20 to 50 simple tasks drawn from real failures as a good starting point, and warns that evals get harder to build the longer you wait.</p>
<p>Good sources for your first tasks:</p>
<ul>
<li>Bugs you already fixed. Each one is a regression test waiting to be written.</li>
<li>Support tickets and user complaints.</li>
<li>The manual checks you run before every release. If you type the same three prompts into a chat window every time you ship, those are tasks.</li>
<li>Edge cases you argued about in code review.</li>
</ul>
<p>Write each task down with its input and a plain-language statement of what success looks like. If two people on your team would disagree about whether a given result passes, the task is not ready yet.</p>
<h2 id="step-2-define-success-as-an-outcome-you-can-check">Step 2: Define success as an outcome you can check</h2>
<p>For every task, ask what you could inspect after the run to know it worked. Some examples:</p>
<table>
<thead>
<tr>
<th>Agent</th>
<th>Weak success check</th>
<th>Strong success check</th>
</tr>
</thead>
<tbody>
<tr>
<td>Calendar assistant</td>
<td>Final message says the meeting was scheduled</td>
<td>An event exists at the right time with the right attendees</td>
</tr>
<tr>
<td>Coding agent</td>
<td>The agent says tests pass</td>
<td>You run the test suite yourself after the trial</td>
</tr>
<tr>
<td>Support agent</td>
<td>Reply sounds helpful</td>
<td>Refund issued for the correct order and amount, nothing else changed</td>
</tr>
<tr>
<td>Research agent</td>
<td>Answer is long and confident</td>
<td>Answer contains the specific facts in your reference answer</td>
</tr>
</tbody>
</table>
<p>Checking the environment instead of the agent's claims removes a whole class of false passes. Agents are good at sounding finished.</p>
<p>It also helps to keep each trial isolated. Reset the database, the file system or the mock services between trials, so one run cannot leak state into the next.</p>
<h2 id="step-3-pick-graders-cheapest-first">Step 3: Pick graders, cheapest first</h2>
<p>There are three kinds of graders, and each has a place:</p>
<table>
<thead>
<tr>
<th>Grader</th>
<th>Good for</th>
<th>Watch out for</th>
</tr>
</thead>
<tbody>
<tr>
<td>Code (string match, state checks, tests)</td>
<td>Anything with a checkable answer</td>
<td>Rejecting valid answers phrased differently</td>
</tr>
<tr>
<td>Model (an LLM judging against a rubric)</td>
<td>Open-ended output, tone, completeness</td>
<td>Cost, randomness, needs calibration</td>
</tr>
<tr>
<td>Human review</td>
<td>Calibrating the other two, subjective quality</td>
<td>Slow and expensive</td>
</tr>
</tbody>
</table>
<p>Start with code graders wherever the outcome is checkable. Add a model grader only for the parts of a task a script cannot judge, and give it a narrow rubric: one question per dimension, with an explicit "can't tell" option so it does not guess. Then spot-check a sample of its verdicts by hand. A model grader you have never compared against human judgment is a second untested system.</p>
<p>Grade what the agent produced, not the exact steps it took. A rule like "must call <code>search</code> then <code>fetch</code> then <code>summarize</code>" fails agents that find a better route. If you care about efficiency, measure it separately (tool calls, tokens, turns) instead of hard-coding a path.</p>
<h2 id="step-4-run-several-trials-and-measure-consistency">Step 4: Run several trials and measure consistency</h2>
<p>One run per task tells you what happened once. Run each task several times and you can measure two different things:</p>
<ul>
<li><strong>pass@k:</strong> the chance that at least one of k attempts succeeds. Useful when one good answer is enough, such as generating candidate code you will test anyway.</li>
<li><strong>pass^k:</strong> the chance that all k attempts succeed. Useful for anything customer-facing, where every run has to work.</li>
</ul>
<p>The gap between them is large. An agent that succeeds 75 percent of the time has a pass^3 of about 42 percent (0.75 × 0.75 × 0.75). Three customers in a row will all get a good result less than half the time. The pass^k metric was popularized by the <a href="https://arxiv.org/abs/2406.12045">τ-bench paper</a> for exactly this reason: reliability across repeated runs is what users feel.</p>
<p>With n trials per task and c successes, you can estimate both from your data. The pass@k estimator comes from the <a href="https://arxiv.org/abs/2107.03374">Codex paper</a>; the pass^k one uses the same counting idea.</p>
<pre><code class="language-python">from math import comb

def pass_at_k(n: int, c: int, k: int) -&gt; float:
    &quot;&quot;&quot;Chance that at least one of k sampled trials passes.&quot;&quot;&quot;
    if n - c &lt; k:
        return 1.0
    return 1.0 - comb(n - c, k) / comb(n, k)

def pass_hat_k(n: int, c: int, k: int) -&gt; float:
    &quot;&quot;&quot;Chance that all k sampled trials pass.&quot;&quot;&quot;
    if c &lt; k:
        return 0.0
    return comb(c, k) / comb(n, k)

# 8 trials, 6 passed
print(round(pass_at_k(8, 6, 3), 3))   # 1.0
print(round(pass_hat_k(8, 6, 3), 3))  # 0.357
</code></pre>
<p>Report both. A change that raises pass@k while lowering pass^k made your agent more capable and less dependable at the same time.</p>
<h2 id="step-5-a-minimal-harness">Step 5: A minimal harness</h2>
<p>You do not need a framework for the first version. Here is the whole loop in plain Python. Replace <code>run_agent</code> with a call into your own agent, and write graders that inspect whatever your agent changes.</p>
<pre><code class="language-python">import json
from dataclasses import dataclass, field
from typing import Callable

@dataclass
class Task:
    id: str
    prompt: str
    graders: list[Callable[[dict], bool]]
    setup: Callable[[], dict] = field(default=lambda: {})

def run_agent(prompt: str, env: dict) -&gt; dict:
    &quot;&quot;&quot;Call your agent here. Return the transcript plus anything you need to grade.&quot;&quot;&quot;
    raise NotImplementedError

def run_suite(tasks: list[Task], trials: int = 5) -&gt; dict:
    results = {}
    for task in tasks:
        passes = 0
        transcripts = []
        for _ in range(trials):
            env = task.setup()                 # fresh state for every trial
            transcript = run_agent(task.prompt, env)
            ok = all(grade(env) for grade in task.graders)
            passes += ok
            transcripts.append({&quot;passed&quot;: ok, &quot;transcript&quot;: transcript})
        results[task.id] = {&quot;passes&quot;: passes, &quot;trials&quot;: trials, &quot;runs&quot;: transcripts}
    return results

if __name__ == &quot;__main__&quot;:
    results = run_suite(TASKS)
    with open(&quot;eval_results.json&quot;, &quot;w&quot;) as f:
        json.dump(results, f, indent=2, default=str)
    for task_id, r in results.items():
        print(f&quot;{task_id}: {r['passes']}/{r['trials']}&quot;)
</code></pre>
<p>A grader in this setup is just a function that looks at the environment after the run:</p>
<pre><code class="language-python">def meeting_was_booked(env: dict) -&gt; bool:
    events = env[&quot;calendar&quot;].events_on(&quot;2026-11-12&quot;)
    return any(
        e.start.hour == 14 and {&quot;dana@example.com&quot;} &lt;= set(e.attendees)
        for e in events
    )
</code></pre>
<p>Save every transcript. You will need them in the next step.</p>
<h2 id="step-6-read-the-transcripts">Step 6: Read the transcripts</h2>
<p>This is the step teams skip, and it is the one that makes the numbers trustworthy. Before you believe a score, open a handful of passing and failing transcripts and check two things:</p>
<ol>
<li><strong>Did failures fail for real?</strong> Sometimes the agent did the right thing and the grader rejected it, for example because a date was formatted differently. That is a grader bug.</li>
<li><strong>Did passes pass for real?</strong> Sometimes the agent got lucky or the grader was too loose.</li>
</ol>
<p>A task with a 0 percent pass rate across many trials is usually a broken task, not a hopeless agent. A good habit is to write a reference solution for each task, a run you know is correct, and confirm your graders accept it.</p>
<h2 id="step-7-split-capability-and-regression-suites">Step 7: Split capability and regression suites</h2>
<p>As the suite grows, keep two lists:</p>
<ul>
<li><strong>Capability evals</strong> ask what the agent can do. They should start with a low pass rate, so you have room to improve.</li>
<li><strong>Regression evals</strong> ask whether the agent still does what it used to. They should sit near 100 percent, and any drop blocks a release.</li>
</ul>
<p>When a capability task passes reliably, move it to the regression suite. Over time, your regression suite becomes a record of every problem you have solved, and your capability suite stays pointed at what you have not.</p>
<h2 id="common-mistakes">Common mistakes</h2>
<ul>
<li><strong>Waiting for a big suite.</strong> Twenty real tasks this week beat three hundred synthetic ones next quarter.</li>
<li><strong>Grading the agent's words.</strong> Check the world, not the claim.</li>
<li><strong>One trial per task.</strong> You will mistake luck for progress.</li>
<li><strong>Only positive cases.</strong> Include tasks where the right move is to refuse, ask a question or do nothing.</li>
<li><strong>Never reading transcripts.</strong> A score you have not inspected is a guess.</li>
</ul>
<h2 id="where-to-go-next">Where to go next</h2>
<p>Once the basic loop works, the most useful additions are cost and latency tracking per task, a model grader for open-ended answers, and running the suite in CI on every prompt or tool change. If your agent's failures are mostly about picking the wrong tool, the next post covers that directly: <a href="/posts/agent-calls-wrong-tool/">why your agent calls the wrong tool</a>.</p>
<h2 id="sources">Sources</h2>
<ul>
<li>Anthropic, <a href="https://www.anthropic.com/engineering/demystifying-evals-for-ai-agents">Demystifying evals for AI agents</a> (2026)</li>
<li>Yao et al., <a href="https://arxiv.org/abs/2406.12045">τ-bench: A Benchmark for Tool-Agent-User Interaction in Real-World Domains</a> (2024)</li>
<li>Chen et al., <a href="https://arxiv.org/abs/2107.03374">Evaluating Large Language Models Trained on Code</a> (2021)</li>
</ul>]]></content:encoded></item><item><title>Why Your Agent Calls the Wrong Tool (and How to Fix It)</title><link>https://agentloopnotes.com/posts/agent-calls-wrong-tool/</link><guid isPermaLink="true">https://agentloopnotes.com/posts/agent-calls-wrong-tool/</guid><pubDate>Fri, 06 Nov 2026 16:00:00 +0000</pubDate><description>A debugging checklist for tool-calling agents: overlapping tools, vague descriptions, ambiguous parameters, noisy responses and unhelpful errors, with a fix for each.</description><content:encoded><![CDATA[<p>Most tool-calling failures look like model failures. The agent searched when it should have fetched, passed a name where an ID belonged, or looped on the same call until it ran out of turns. The natural reaction is to rewrite the system prompt or try a bigger model.</p>
<p>Usually the problem is the tools. A model only knows what a tool does from its name, its description, its parameters and what it returns. If those are unclear to someone who has never seen your codebase, they are unclear to the model too. Anthropic's engineers made the same point in <a href="https://www.anthropic.com/engineering/building-effective-agents">Building effective agents</a>: on one of their coding agents, they spent more time improving the tools than the overall prompt.</p>
<p>Here is a checklist of the causes that come up most often, roughly in the order worth checking them.</p>
<h2 id="first-classify-the-failure">First, classify the failure</h2>
<p>Pull a few failing transcripts and sort each failure into one of these buckets:</p>
<ol>
<li><strong>Wrong tool:</strong> the agent picked a tool that cannot do the job.</li>
<li><strong>Right tool, wrong arguments:</strong> the call was sensible but the inputs were malformed or mismatched.</li>
<li><strong>Right call, misread result:</strong> the tool worked but the agent drew the wrong conclusion from its output.</li>
<li><strong>Stuck:</strong> the agent repeated a call, gave up, or stopped before finishing.</li>
</ol>
<p>The bucket points to the fix. Wrong-tool failures usually mean the tool set or descriptions are confusing. Wrong-argument failures point at parameter design. Misread results point at response format. Getting stuck often traces back to error messages.</p>
<h2 id="cause-1-too-many-overlapping-tools">Cause 1: Too many overlapping tools</h2>
<p>A common pattern is to wrap every API endpoint as its own tool. The agent then sees <code>list_users</code>, <code>get_user</code>, <code>search_users</code> and <code>find_user_by_email</code>, and has to guess which one you meant.</p>
<p>Anthropic's post <a href="https://www.anthropic.com/engineering/writing-tools-for-agents">Writing effective tools for AI agents</a> recommends building fewer tools around whole workflows instead. Their examples:</p>
<ul>
<li>Replace <code>list_users</code>, <code>list_events</code> and <code>create_event</code> with one <code>schedule_event</code> tool that finds availability and books the slot.</li>
<li>Replace <code>read_logs</code> with <code>search_logs</code>, which returns only the relevant lines.</li>
<li>Replace <code>get_customer_by_id</code>, <code>list_transactions</code> and <code>list_notes</code> with <code>get_customer_context</code>.</li>
</ul>
<p><strong>Fix:</strong> list your tools and, for each pair, ask whether a new teammate could tell from the names alone which one to use. Merge any pair where the answer is no. Each tool should have one clear purpose that no other tool shares.</p>
<h2 id="cause-2-names-that-do-not-separate">Cause 2: Names that do not separate</h2>
<p>Once an agent has tools from several services, names collide. Is <code>search</code> for the wiki, the ticket tracker or the codebase?</p>
<p><strong>Fix:</strong> namespace by service and resource, for example <code>jira_issues_search</code> and <code>confluence_pages_search</code>. The Anthropic tools post notes that prefix versus suffix naming had measurable effects in their evals and that the best choice varied by model, so if you have an eval suite, test both.</p>
<h2 id="cause-3-descriptions-written-for-people-who-already-know-the-system">Cause 3: Descriptions written for people who already know the system</h2>
<p>A description like "Gets account data" makes sense to the person who wrote the endpoint. It tells the model nothing about what an account is, what data comes back or when to prefer another tool.</p>
<p><strong>Fix:</strong> write the description the way you would explain the tool to a new hire. Include:</p>
<ul>
<li>What it does and what it returns, in one or two sentences.</li>
<li>When to use it, and when to use a different tool instead.</li>
<li>Any terms of art, formats or limits ("dates are ISO 8601", "returns at most 50 results").</li>
<li>One short example call.</li>
</ul>
<p>Before:</p>
<pre><code class="language-json">{
  &quot;name&quot;: &quot;get_account&quot;,
  &quot;description&quot;: &quot;Gets account data.&quot;,
  &quot;input_schema&quot;: {
    &quot;type&quot;: &quot;object&quot;,
    &quot;properties&quot;: { &quot;account&quot;: { &quot;type&quot;: &quot;string&quot; } }
  }
}
</code></pre>
<p>After:</p>
<pre><code class="language-json">{
  &quot;name&quot;: &quot;billing_account_get&quot;,
  &quot;description&quot;: &quot;Look up one billing account and return its plan, status, renewal date and the last 5 invoices. Use this when you already know the account ID. To find an account from a company name or email, call billing_accounts_search first.&quot;,
  &quot;input_schema&quot;: {
    &quot;type&quot;: &quot;object&quot;,
    &quot;properties&quot;: {
      &quot;account_id&quot;: {
        &quot;type&quot;: &quot;string&quot;,
        &quot;description&quot;: &quot;Billing account ID, formatted like 'acct_1234'. Not the company name.&quot;
      }
    },
    &quot;required&quot;: [&quot;account_id&quot;]
  }
}
</code></pre>
<p>The second version answers the questions the model would otherwise guess at. The Anthropic post reports that small refinements to tool descriptions produced large gains on their own benchmarks.</p>
<h2 id="cause-4-parameters-that-invite-mistakes">Cause 4: Parameters that invite mistakes</h2>
<p><code>user</code> could be a name, an email or an ID. <code>date</code> could be a day, a timestamp or "next Tuesday". Every ambiguous parameter is a coin flip.</p>
<p><strong>Fix:</strong> make the wrong input hard to send.</p>
<ul>
<li>Name parameters by what they hold: <code>user_id</code>, not <code>user</code>.</li>
<li>Use enums for any field with a fixed set of values.</li>
<li>Mark required fields as required.</li>
<li>Choose formats that leave no room for error.</li>
</ul>
<p>The Building effective agents post has a good example of that last point. Their coding agent made mistakes with relative file paths after it moved out of the root directory. They changed the tool to require absolute paths, and the errors went away. They call this poka-yoke, a term from manufacturing for designs that prevent errors instead of detecting them.</p>
<h2 id="cause-5-responses-full-of-noise">Cause 5: Responses full of noise</h2>
<p>If a tool returns a 3,000-token JSON blob of internal fields, the agent has to dig through all of it to find the one value it needs, and that blob sits in context for the rest of the conversation.</p>
<p><strong>Fix:</strong> return what the agent needs to make its next decision, in a form it can use.</p>
<ul>
<li>Prefer human-readable identifiers. The Anthropic tools post found that resolving UUIDs to names or short numeric IDs improved retrieval precision and reduced hallucinations.</li>
<li>Drop low-level fields the agent will never act on, like MIME types or thumbnail URLs.</li>
<li>Offer a <code>response_format</code> parameter with <code>concise</code> and <code>detailed</code> options. In their example, the concise response used about a third of the tokens of the detailed one.</li>
<li>Paginate or truncate large results, and say so in the response: "Showing 20 of 312 results. Narrow the query or request the next page." Claude Code, for reference, caps tool responses at 25,000 tokens by default.</li>
</ul>
<h2 id="cause-6-errors-the-agent-cannot-act-on">Cause 6: Errors the agent cannot act on</h2>
<p>When a call fails with <code>Error 400</code> or a stack trace, the agent has nothing to work with. It retries the same call, or it gives up.</p>
<p><strong>Fix:</strong> write error messages that tell the agent what to do next.</p>
<pre><code class="language-text">Bad:   ValidationError: invalid value
Good:  start_date must be YYYY-MM-DD, for example 2026-11-12. You sent &quot;next Tuesday&quot;.
       Resolve relative dates before calling this tool.
</code></pre>
<p>A good error message turns a dead end into a one-step correction.</p>
<h2 id="cause-7-the-tool-set-changes-between-turns">Cause 7: The tool set changes between turns</h2>
<p>Some agents add and remove tools as the conversation moves, to keep the list short. This can confuse the model when earlier turns refer to tools that no longer exist, and it also defeats prompt caching, since tool definitions sit at the very start of the prompt. If you need to restrict which tools can run at a given step, both the Claude and OpenAI APIs offer a <code>tool_choice</code> setting, and OpenAI's caching guide suggests restricting tools that way instead of editing the list. More on that in <a href="/posts/prompt-caching-agent-costs/">prompt caching for AI agents</a>.</p>
<h2 id="how-to-know-you-fixed-it">How to know you fixed it</h2>
<p>Guessing is how tool problems come back. Build a small set of tasks that each require a specific tool or sequence, run them a few times, and track:</p>
<ul>
<li>Pass rate on the tasks.</li>
<li>Number of tool calls per task.</li>
<li>Number of tool errors per task.</li>
<li>Tokens per task.</li>
</ul>
<p>Then change one thing at a time: a description, a name, a merged tool. If the numbers move the right way, keep the change. If you have not set up evals yet, <a href="/posts/agent-evals-starter-guide/">this starter guide</a> covers the basics.</p>
<h2 id="quick-checklist">Quick checklist</h2>
<ul>
<li>Can a new teammate tell every tool apart by name and description alone?</li>
<li>Does each description say when not to use the tool?</li>
<li>Is every parameter named for what it holds, with enums and required fields set?</li>
<li>Do responses use readable identifiers and skip internal fields?</li>
<li>Are large results paginated, with a note saying so?</li>
<li>Do errors explain how to fix the call?</li>
<li>Is the tool list stable across the conversation?</li>
</ul>
<h2 id="sources">Sources</h2>
<ul>
<li>Anthropic, <a href="https://www.anthropic.com/engineering/writing-tools-for-agents">Writing effective tools for AI agents</a> (2025)</li>
<li>Anthropic, <a href="https://www.anthropic.com/engineering/building-effective-agents">Building effective agents</a> (2024)</li>
<li>OpenAI, <a href="https://developers.openai.com/api/docs/guides/prompt-caching">Prompt caching guide</a></li>
</ul>]]></content:encoded></item><item><title>Prompt Caching for AI Agents: How to Cut Input Costs</title><link>https://agentloopnotes.com/posts/prompt-caching-agent-costs/</link><guid isPermaLink="true">https://agentloopnotes.com/posts/prompt-caching-agent-costs/</guid><pubDate>Fri, 06 Nov 2026 16:00:00 +0000</pubDate><description>How prompt caching works on Claude and OpenAI, how to order an agent&#x27;s prompt so the cache actually hits, and how to measure your hit rate from the usage fields.</description><content:encoded><![CDATA[<p>An agent resends almost everything on every turn: the system prompt, every tool definition and the whole conversation so far. On a long task, the same few thousand tokens of instructions get billed dozens of times. Input tokens, not output, often end up as the biggest line on the bill.</p>
<p>Prompt caching fixes most of that. Both Anthropic and OpenAI let you reuse the processed form of a prompt prefix you sent recently, at a fraction of the normal input price. The catch is that the cache only hits if your prompt is built the right way, and agents are very good at accidentally breaking it.</p>
<p>This post covers how caching works, how to structure an agent so it hits, and how to check that it is working.</p>
<h2 id="how-prompt-caching-works">How prompt caching works</h2>
<p>The idea is the same on both platforms. When a request starts with exactly the same tokens as a recent request, the provider can skip reprocessing that shared beginning and charge you less for it.</p>
<p>Two rules follow from that:</p>
<ol>
<li><strong>Only the prefix counts.</strong> The cache matches from the start of the prompt and stops at the first difference. A changed character near the top makes everything after it a miss.</li>
<li><strong>Matches are exact.</strong> Anthropic's documentation says cache hits require 100 percent identical prompt segments. "Almost the same" is a miss.</li>
</ol>
<p>So the whole game is ordering: put what never changes first, and what changes every turn last.</p>
<h2 id="the-order-that-works">The order that works</h2>
<p>For an agent, that usually means:</p>
<ol>
<li><strong>Tool definitions.</strong> Same on every turn.</li>
<li><strong>System prompt.</strong> Same on every turn, with no timestamps, user names or request IDs in it.</li>
<li><strong>Stable reference material.</strong> Documentation, examples, a codebase summary.</li>
<li><strong>Conversation history.</strong> Grows, but earlier turns do not change.</li>
<li><strong>The newest message.</strong> Different every time.</li>
</ol>
<p>On Claude, the API processes the prompt in the order tools, then system, then messages, and a change at one level invalidates the cache for that level and everything after it. Changing a tool definition invalidates everything. Changing the newest message invalidates nothing before it.</p>
<h2 id="prompt-caching-on-claude">Prompt caching on Claude</h2>
<p>On the Claude API you mark where cacheable content ends with a <code>cache_control</code> field. Everything up to and including that block becomes a cache entry. Per the <a href="https://platform.claude.com/docs/en/build-with-claude/prompt-caching">prompt caching docs</a>:</p>
<ul>
<li>You can set up to 4 cache breakpoints per request.</li>
<li>There is also an automatic mode: a single top-level <code>cache_control</code> puts the breakpoint on the last cacheable block and moves it forward as the conversation grows.</li>
<li>The default cache lifetime is 5 minutes, refreshed for free each time the entry is used. A 1-hour lifetime is available with <code>"ttl": "1h"</code>.</li>
<li>Writing to the cache costs 1.25 times the base input price for the 5-minute lifetime and 2 times for the 1-hour lifetime. Reading from it costs 0.1 times the base price.</li>
<li>There is a minimum cacheable length, which varies by model (from 1,024 to 4,096 tokens at the time of writing). Shorter prompts are simply not cached, with no error.</li>
</ul>
<p>Here is an agent turn with two breakpoints: one at the end of the system prompt and one on the latest message, so each turn reuses everything before it.</p>
<pre><code class="language-python">import anthropic

client = anthropic.Anthropic()
MODEL = &quot;your-claude-model-id&quot;

TOOLS = [...]          # keep this list identical on every turn
SYSTEM_PROMPT = &quot;...&quot;  # no timestamps or per-user text in here

def agent_turn(history: list[dict]) -&gt; anthropic.types.Message:
    # Mark the newest message so the next turn can reuse this whole prefix.
    last = history[-1]
    if isinstance(last[&quot;content&quot;], str):
        last[&quot;content&quot;] = [{&quot;type&quot;: &quot;text&quot;, &quot;text&quot;: last[&quot;content&quot;]}]
    last[&quot;content&quot;][-1][&quot;cache_control&quot;] = {&quot;type&quot;: &quot;ephemeral&quot;}

    return client.messages.create(
        model=MODEL,
        max_tokens=2048,
        tools=TOOLS,
        system=[
            {
                &quot;type&quot;: &quot;text&quot;,
                &quot;text&quot;: SYSTEM_PROMPT,
                &quot;cache_control&quot;: {&quot;type&quot;: &quot;ephemeral&quot;},
            }
        ],
        messages=history,
    )
</code></pre>
<p>In a real loop, remove the marker from the previous message before adding a new one, so you stay under the 4-breakpoint limit. The docs also note that when a request comes in, the system looks back up to 20 blocks from each breakpoint for an earlier cache entry. In very long conversations, that is a reason to keep a breakpoint near the end rather than only at the top.</p>
<h3 id="does-it-pay-off">Does it pay off?</h3>
<p>Treat a normal, uncached prompt as costing 1.0 per use. With the 5-minute cache:</p>
<ul>
<li>Two uncached uses cost 2.0.</li>
<li>One cache write plus one cache read costs 1.25 + 0.1 = 1.35.</li>
</ul>
<p>You come out ahead the first time the prefix is reused. With the 1-hour cache, the write costs 2.0, so you need two reuses to come out ahead (2.0 + 0.1 + 0.1 = 2.2, against 3.0 uncached). For an agent that takes ten turns on one task, the shared prefix costs 1.25 + 9 × 0.1 = 2.15 instead of 10, a saving of almost 80 percent.</p>
<h2 id="prompt-caching-on-openai">Prompt caching on OpenAI</h2>
<p>OpenAI's <a href="https://developers.openai.com/api/docs/guides/prompt-caching">prompt caching guide</a> says caching is on by default for supported models, so there is nothing to mark. A few things to know:</p>
<ul>
<li>Prompts need at least 1,024 tokens to be cached.</li>
<li>The whole prefix has to match, so the same ordering rules apply.</li>
<li>You can pass a <code>prompt_cache_key</code> to group related requests, such as all requests for one user or one task.</li>
<li>Retention options, write pricing and discounts vary by model generation, and newer models add more explicit controls. Check the guide for the model you use rather than relying on a number from a blog post, including this one.</li>
</ul>
<p>The guide's own advice lines up with everything above: put stable instructions and reference material first, move dynamic content to the end, append to the conversation instead of rewriting earlier turns, and keep tool definitions stable.</p>
<h2 id="seven-ways-agents-break-their-own-cache">Seven ways agents break their own cache</h2>
<p>These are the common culprits when the hit rate is lower than it should be:</p>
<ol>
<li><strong>A timestamp in the system prompt.</strong> "Today is November 6, 2026, 3:42 PM" changes every minute. Put the date in the latest user message instead, or round it to the day.</li>
<li><strong>Per-user text near the top.</strong> Move names, preferences and account details below the shared instructions.</li>
<li><strong>Changing the tool list between turns.</strong> Adding or removing a tool invalidates everything on Claude. Keep the list fixed and restrict tools with <code>tool_choice</code> instead.</li>
<li><strong>Non-deterministic serialization.</strong> If tool schemas or context are built from a dictionary with unstable key order, the bytes change even when the content does not. Serialize with sorted keys.</li>
<li><strong>Rewriting history.</strong> Summarizing or trimming earlier turns changes the prefix. When you must compact, do it rarely and in large steps, so you pay for one cache write instead of many.</li>
<li><strong>Toggling settings.</strong> On Claude, changing <code>tool_choice</code>, extended thinking settings, or adding or removing images invalidates the cached messages.</li>
<li><strong>Gaps longer than the lifetime.</strong> If a user pauses for ten minutes, a 5-minute entry is gone. For agents that wait on humans, the 1-hour lifetime can be worth its higher write price.</li>
</ol>
<h2 id="measure-your-hit-rate">Measure your hit rate</h2>
<p>Do not assume caching works. Read it from the response.</p>
<p>On Claude, the <code>usage</code> object reports three numbers: <code>cache_creation_input_tokens</code> (written to the cache), <code>cache_read_input_tokens</code> (served from it) and <code>input_tokens</code> (processed normally, after the last breakpoint). Their sum is your total input. If both cache fields are zero, nothing was cached, often because the prompt was under the minimum length.</p>
<p>On OpenAI, cached tokens appear in the usage details as <code>cached_tokens</code>.</p>
<p>A simple hit rate for Claude:</p>
<pre><code class="language-python">def cache_hit_rate(usage) -&gt; float:
    read = usage.cache_read_input_tokens or 0
    written = usage.cache_creation_input_tokens or 0
    fresh = usage.input_tokens or 0
    total = read + written + fresh
    return read / total if total else 0.0
</code></pre>
<p>Log it per turn. On a healthy multi-turn agent, the first turn shows mostly writes and later turns should be mostly reads. If later turns keep showing large writes, something near the top of your prompt is changing. Diff two consecutive requests and look for the first byte that differs.</p>
<h2 id="checklist">Checklist</h2>
<ul>
<li>Tools first, then system prompt, then history, then the new message.</li>
<li>No timestamps, IDs or user details in the system prompt.</li>
<li>Tool list and schemas byte-for-byte identical across turns.</li>
<li>On Claude: a breakpoint on the system prompt and one on the latest message.</li>
<li>Hit rate logged on every call.</li>
</ul>
<p>If you are cutting costs, it is also worth checking how much context your tools return on each call. Large tool responses sit in the history and get billed on every later turn. The post on <a href="/posts/agent-calls-wrong-tool/">why agents call the wrong tool</a> covers how to keep them short.</p>
<h2 id="sources">Sources</h2>
<ul>
<li>Anthropic, <a href="https://platform.claude.com/docs/en/build-with-claude/prompt-caching">Prompt caching</a> (Claude API documentation)</li>
<li>OpenAI, <a href="https://developers.openai.com/api/docs/guides/prompt-caching">Prompt caching</a> (API documentation)</li>
</ul>]]></content:encoded></item></channel></rss>
