<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Agents on WebDevStation</title>
    <link>https://webdevstation.com/tags/agents/</link>
    <description>2 articles tagged Agents — tutorials, code examples and notes from building real systems, newest first.</description>
    <generator>Hugo</generator>
    <language>en</language>
    <managingEditor>Alex</managingEditor>
    <webMaster>Alex</webMaster>
    <copyright>© 2026 WebDevStation</copyright>
    <lastBuildDate>Tue, 01 Sep 2026 09:20:00 +0200</lastBuildDate>
    <atom:link href="https://webdevstation.com/tags/agents/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>How to Test Go Code That Calls an LLM</title>
      <link>https://webdevstation.com/posts/testing-go-code-that-calls-an-llm/</link>
      <pubDate>Tue, 01 Sep 2026 09:20:00 +0200</pubDate>
      <author>Alex</author>
      <guid isPermaLink="true">https://webdevstation.com/posts/testing-go-code-that-calls-an-llm/</guid>
      <description>Non-determinism does not have to mean untestable. Wrapping the model behind an interface, faking it in unit tests, golden files for prompt assembly, and the small…</description>
      <content:encoded><![CDATA[<p>The first question everyone asks about a feature backed by a language model is &ldquo;how do you even test that?&rdquo; — usually with a shrug, as though non-determinism were a get-out-of-jail card for the whole test suite. It is not. Almost all of the code you write around a model is perfectly deterministic, and the small part that is not can be pinned down with a different kind of test. Here is how I split it.</p>
<h2 id="three-things-tested-three-ways">Three Things, Tested Three Ways</h2>
<p>The confusion comes from treating &ldquo;the LLM feature&rdquo; as one indivisible thing. It is three:</p>
<table>
	<thead>
			<tr>
					<th>What</th>
					<th>Deterministic?</th>
					<th>How to test it</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Prompt assembly, parsing, tool handlers, the loop</td>
					<td>Yes, completely</td>
					<td>Ordinary unit tests</td>
			</tr>
			<tr>
					<td>Wiring to the real API — auth, streaming, errors</td>
					<td>Yes enough</td>
					<td>A few integration tests, run on demand</td>
			</tr>
			<tr>
					<td>Output quality — is the answer any good?</td>
					<td>No</td>
					<td>Evals, scored not asserted</td>
			</tr>
	</tbody>
</table>
<p>The first row is 90% of your code and needs no model at all. Get that boundary right and the rest is manageable.</p>
<h2 id="put-an-interface-in-front-of-the-model">Put an Interface in Front of the Model</h2>
<p>Do not scatter <code>client.Messages.New</code> through your handlers. Define the narrowest interface your code actually needs and depend on that:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// Completer turns a prompt into text. It is deliberately narrow — narrower</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// than the SDK — so tests can implement it in five lines.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Completer</span> <span style="color:#66d9ef">interface</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Complete</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">req</span> <span style="color:#a6e22e">Request</span>) (<span style="color:#a6e22e">Response</span>, <span style="color:#66d9ef">error</span>)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Request</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">System</span>   <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Messages</span> []<span style="color:#a6e22e">Message</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Tools</span>    []<span style="color:#a6e22e">Tool</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Response</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Text</span>       <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">ToolCalls</span>  []<span style="color:#a6e22e">ToolCall</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">StopReason</span> <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Usage</span>      <span style="color:#a6e22e">Usage</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This is just the dependency-inversion habit that makes any external service testable, and it earns its keep faster here than almost anywhere else. Two things fall out of it:</p>
<p><strong>Your business logic never imports the SDK.</strong> It imports your <code>Request</code> and <code>Response</code> types. When the SDK&rsquo;s union types change shape, one adapter file changes.</p>
<p><strong>The fake is trivial.</strong> No HTTP, no fixtures, no mocking library:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">fakeCompleter</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">responses</span> []<span style="color:#a6e22e">Response</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">err</span>       <span style="color:#66d9ef">error</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">calls</span>     []<span style="color:#a6e22e">Request</span> <span style="color:#75715e">// recorded for assertions</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">f</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">fakeCompleter</span>) <span style="color:#a6e22e">Complete</span>(<span style="color:#a6e22e">_</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">req</span> <span style="color:#a6e22e">Request</span>) (<span style="color:#a6e22e">Response</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">calls</span> = append(<span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">calls</span>, <span style="color:#a6e22e">req</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">Response</span>{}, <span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> len(<span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">responses</span>) <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">Response</span>{}, <span style="color:#a6e22e">errors</span>.<span style="color:#a6e22e">New</span>(<span style="color:#e6db74">&#34;fakeCompleter: no responses left&#34;</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">resp</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">responses</span>[<span style="color:#ae81ff">0</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">responses</span> = <span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">responses</span>[<span style="color:#ae81ff">1</span>:]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">resp</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Returning a queue rather than a single value is what lets you test multi-turn loops: the first call returns a tool request, the second returns the final answer.</p>
<h2 id="test-the-loop-not-the-model">Test the Loop, Not the Model</h2>
<p>An agent loop has plenty of logic worth testing, none of which needs a model. Does it stop when it should? Does it return a result for every tool call? Does it hand a tool failure back rather than aborting?</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestLoopReturnsResultForFailedTool</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">fake</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">fakeCompleter</span>{<span style="color:#a6e22e">responses</span>: []<span style="color:#a6e22e">Response</span>{
</span></span><span style="display:flex;"><span>        {<span style="color:#a6e22e">StopReason</span>: <span style="color:#e6db74">&#34;tool_use&#34;</span>, <span style="color:#a6e22e">ToolCalls</span>: []<span style="color:#a6e22e">ToolCall</span>{
</span></span><span style="display:flex;"><span>            {<span style="color:#a6e22e">ID</span>: <span style="color:#e6db74">&#34;t1&#34;</span>, <span style="color:#a6e22e">Name</span>: <span style="color:#e6db74">&#34;lookup&#34;</span>, <span style="color:#a6e22e">Input</span>: []byte(<span style="color:#e6db74">`{&#34;id&#34;:&#34;missing&#34;}`</span>)},
</span></span><span style="display:flex;"><span>        }},
</span></span><span style="display:flex;"><span>        {<span style="color:#a6e22e">StopReason</span>: <span style="color:#e6db74">&#34;end_turn&#34;</span>, <span style="color:#a6e22e">Text</span>: <span style="color:#e6db74">&#34;I couldn&#39;t find that record.&#34;</span>},
</span></span><span style="display:flex;"><span>    }}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">agent</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">fake</span>, <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#a6e22e">ToolFunc</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;lookup&#34;</span>: <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, []<span style="color:#66d9ef">byte</span>) (<span style="color:#66d9ef">string</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">errors</span>.<span style="color:#a6e22e">New</span>(<span style="color:#e6db74">&#34;not found&#34;</span>)
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">got</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">agent</span>.<span style="color:#a6e22e">Run</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#e6db74">&#34;look up record missing&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;Run() error = %v, want nil&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">got</span> <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#34;I couldn&#39;t find that record.&#34;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;got %q&#34;</span>, <span style="color:#a6e22e">got</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// The failure must reach the model as a tool result, not abort the loop.</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">second</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fake</span>.<span style="color:#a6e22e">calls</span>[<span style="color:#ae81ff">1</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">result</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">findToolResult</span>(<span style="color:#a6e22e">t</span>, <span style="color:#a6e22e">second</span>, <span style="color:#e6db74">&#34;t1&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">result</span>.<span style="color:#a6e22e">IsError</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Error</span>(<span style="color:#e6db74">&#34;tool failure was not marked as an error result&#34;</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That test catches a real bug — a loop that drops the failed call instead of reporting it — and runs in microseconds with no API key.</p>
<p>The same approach covers the rest of the loop&rsquo;s contract — <a href="/posts/tool-use-in-go-agent-loop/">the invariants from the agent-loop article</a>: that it stops at <code>MaxIterations</code>, that every <code>tool_use</code> block gets exactly one result, that results come back in one message. Table-driven tests fit this beautifully, since each case is just a different queue of canned responses.</p>
<h2 id="golden-files-for-prompt-assembly">Golden Files for Prompt Assembly</h2>
<p>Prompt building is string manipulation, and it drifts. Someone adds a field, reorders a section, changes a heading — and unlike code, a prompt regression produces no compile error and no failing assertion, just slightly worse output that nobody attributes to the change.</p>
<p>Golden files make the diff visible in review:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">update</span> = <span style="color:#a6e22e">flag</span>.<span style="color:#a6e22e">Bool</span>(<span style="color:#e6db74">&#34;update&#34;</span>, <span style="color:#66d9ef">false</span>, <span style="color:#e6db74">&#34;update golden files&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestBuildSystemPrompt</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">tests</span> <span style="color:#f92672">:=</span> []<span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">name</span>   <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">cfg</span>    <span style="color:#a6e22e">Config</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">golden</span> <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>    }{
</span></span><span style="display:flex;"><span>        {<span style="color:#e6db74">&#34;default&#34;</span>, <span style="color:#a6e22e">Config</span>{}, <span style="color:#e6db74">&#34;system_default.txt&#34;</span>},
</span></span><span style="display:flex;"><span>        {<span style="color:#e6db74">&#34;with_tools&#34;</span>, <span style="color:#a6e22e">Config</span>{<span style="color:#a6e22e">Tools</span>: <span style="color:#a6e22e">allTools</span>}, <span style="color:#e6db74">&#34;system_with_tools.txt&#34;</span>},
</span></span><span style="display:flex;"><span>        {<span style="color:#e6db74">&#34;terse_mode&#34;</span>, <span style="color:#a6e22e">Config</span>{<span style="color:#a6e22e">Terse</span>: <span style="color:#66d9ef">true</span>}, <span style="color:#e6db74">&#34;system_terse.txt&#34;</span>},
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">tt</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">tests</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Run</span>(<span style="color:#a6e22e">tt</span>.<span style="color:#a6e22e">name</span>, <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">got</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">BuildSystemPrompt</span>(<span style="color:#a6e22e">tt</span>.<span style="color:#a6e22e">cfg</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">path</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">filepath</span>.<span style="color:#a6e22e">Join</span>(<span style="color:#e6db74">&#34;testdata&#34;</span>, <span style="color:#a6e22e">tt</span>.<span style="color:#a6e22e">golden</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">update</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">WriteFile</span>(<span style="color:#a6e22e">path</span>, []byte(<span style="color:#a6e22e">got</span>), <span style="color:#ae81ff">0</span><span style="color:#a6e22e">o644</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">return</span>
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">want</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#a6e22e">path</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">diff</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">cmp</span>.<span style="color:#a6e22e">Diff</span>(string(<span style="color:#a6e22e">want</span>), <span style="color:#a6e22e">got</span>); <span style="color:#a6e22e">diff</span> <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#34;&#34;</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;prompt changed (-want +got):\n%s&#34;</span>, <span style="color:#a6e22e">diff</span>)
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        })
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Run <code>go test ./... -update</code> to accept a change deliberately. The point is not that the golden file is correct — it is that changing it requires saying so out loud, in a diff a reviewer can read.</p>
<p>This also catches the caching bugs from the <a href="/posts/prompt-caching-llm-cost/">prompt caching article</a> before they cost you anything. A golden test on the system prompt fails the moment somebody interpolates <code>time.Now()</code> into it, because the output differs on every run.</p>
<p>Which suggests a second, blunter test worth having:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestPromptAssemblyIsDeterministic</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Go randomises map iteration order, so a prompt built by ranging over a</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// map differs run to run — and silently never caches.</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">first</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">BuildSystemPrompt</span>(<span style="color:#a6e22e">cfg</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">i</span> &lt; <span style="color:#ae81ff">20</span>; <span style="color:#a6e22e">i</span><span style="color:#f92672">++</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">got</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">BuildSystemPrompt</span>(<span style="color:#a6e22e">cfg</span>); <span style="color:#a6e22e">got</span> <span style="color:#f92672">!=</span> <span style="color:#a6e22e">first</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;prompt is not deterministic on run %d&#34;</span>, <span style="color:#a6e22e">i</span>)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Twenty iterations is enough to make a randomised map order fail essentially every time.</p>
<h2 id="integration-tests-behind-a-build-tag">Integration Tests, Behind a Build Tag</h2>
<p>You do need a handful of tests that touch the real API — enough to catch an SDK upgrade that changed a union type, a model id that no longer resolves, or streaming that broke. But they cost money and need a key, so they must not run on every <code>go test ./...</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">//go:build integration</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">llm_test</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestStreamingReturnsCompleteMessage</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Getenv</span>(<span style="color:#e6db74">&#34;ANTHROPIC_API_KEY&#34;</span>) <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;&#34;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Skip</span>(<span style="color:#e6db74">&#34;no API key; skipping integration test&#34;</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// ... real call, assert on shape rather than content</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>go test ./...                    <span style="color:#75715e"># fast, free, no key</span>
</span></span><span style="display:flex;"><span>go test -tags<span style="color:#f92672">=</span>integration ./...  <span style="color:#75715e"># the real thing, on demand</span>
</span></span></code></pre></div><p>Assert on <strong>shape</strong>, never on wording. <code>resp.Text</code> being non-empty, <code>StopReason</code> being <code>end_turn</code>, <code>Usage.OutputTokens</code> being greater than zero, a streamed message accumulating to the same content as a non-streamed one. Those hold across model versions. &ldquo;The answer contains the word Paris&rdquo; does not, and a flaky test that fails once a month teaches your team to ignore failures.</p>
<p>There is a middle option that gets you a long way for free: point the SDK at a local <code>httptest.Server</code> via the <code>ANTHROPIC_BASE_URL</code> environment variable and serve canned JSON. That exercises the real SDK — its parsing, its retry behaviour, its streaming decoder — without a key or a bill. It is the closest analogue to what <a href="/posts/how-to-test-database-interactions-go/">go-sqlmock</a> does for the database layer: a real driver, a fake server.</p>
<p>It is also the only sane way to test the paths you cannot easily provoke on purpose:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// Rate limiting: does the caller back off, or hammer?</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">mux</span>.<span style="color:#a6e22e">HandleFunc</span>(<span style="color:#e6db74">&#34;/v1/messages&#34;</span>, <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">w</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ResponseWriter</span>, <span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">Header</span>().<span style="color:#a6e22e">Set</span>(<span style="color:#e6db74">&#34;Retry-After&#34;</span>, <span style="color:#e6db74">&#34;1&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">WriteHeader</span>(<span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">StatusTooManyRequests</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Fprint</span>(<span style="color:#a6e22e">w</span>, <span style="color:#e6db74">`{&#34;type&#34;:&#34;error&#34;,&#34;error&#34;:{&#34;type&#34;:&#34;rate_limit_error&#34;,&#34;message&#34;:&#34;...&#34;}}`</span>)
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><p>Do the same for a 500, a connection dropped mid-stream, and a malformed body. Those are the failures that actually page you, and they are trivial to simulate and nearly impossible to trigger on demand against the real API.</p>
<h2 id="the-refusal-case-nobody-tests">The Refusal Case Nobody Tests</h2>
<p>Worth its own paragraph because it is so easy to miss: a safety refusal comes back as <strong>HTTP 200</strong> with <code>StopReason</code> set to <code>refusal</code>. Your error handling never fires. Code that goes from <code>if err != nil</code> straight to reading the first content block treats it as an empty answer.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestRefusalIsNotTreatedAsEmptyAnswer</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">fake</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">fakeCompleter</span>{<span style="color:#a6e22e">responses</span>: []<span style="color:#a6e22e">Response</span>{
</span></span><span style="display:flex;"><span>        {<span style="color:#a6e22e">StopReason</span>: <span style="color:#e6db74">&#34;refusal&#34;</span>, <span style="color:#a6e22e">Text</span>: <span style="color:#e6db74">&#34;&#34;</span>},
</span></span><span style="display:flex;"><span>    }}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">fake</span>, <span style="color:#66d9ef">nil</span>).<span style="color:#a6e22e">Run</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#e6db74">&#34;...&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">errors</span>.<span style="color:#a6e22e">Is</span>(<span style="color:#a6e22e">err</span>, <span style="color:#a6e22e">ErrDeclined</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;got %v, want ErrDeclined&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>One line in the fake, and you have covered a branch most production code does not have at all. The sentinel-error pattern behind <code>ErrDeclined</code> is the one from <a href="/posts/error-handling-in-go/">error handling in Go</a>.</p>
<h2 id="evals-for-quality-not-correctness">Evals: For Quality, Not Correctness</h2>
<p>Everything above tests whether your <em>code</em> is right. None of it tells you whether the answers are any good. That needs a different instrument, and the mistake is trying to force it into <code>go test</code>.</p>
<p>An eval is a fixed set of inputs, run against the real model, scored rather than asserted:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">//go:build eval</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestClassificationAccuracy</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">cases</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">loadEvalCases</span>(<span style="color:#a6e22e">t</span>, <span style="color:#e6db74">&#34;testdata/eval/classification.jsonl&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">correct</span> <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">cases</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">got</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">classify</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#a6e22e">realClient</span>, <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Input</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">got</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Want</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">correct</span><span style="color:#f92672">++</span>
</span></span><span style="display:flex;"><span>        } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Logf</span>(<span style="color:#e6db74">&#34;MISS: input=%q got=%q want=%q&#34;</span>, <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Input</span>, <span style="color:#a6e22e">got</span>, <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Want</span>)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">accuracy</span> <span style="color:#f92672">:=</span> float64(<span style="color:#a6e22e">correct</span>) <span style="color:#f92672">/</span> float64(len(<span style="color:#a6e22e">cases</span>))
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Logf</span>(<span style="color:#e6db74">&#34;accuracy: %.1f%% (%d/%d)&#34;</span>, <span style="color:#a6e22e">accuracy</span><span style="color:#f92672">*</span><span style="color:#ae81ff">100</span>, <span style="color:#a6e22e">correct</span>, len(<span style="color:#a6e22e">cases</span>))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// A floor, not an equality check. Below this, something regressed.</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">accuracy</span> &lt; <span style="color:#ae81ff">0.90</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;accuracy %.1f%% below the 90%% floor&#34;</span>, <span style="color:#a6e22e">accuracy</span><span style="color:#f92672">*</span><span style="color:#ae81ff">100</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Two things make this useful rather than annoying. <strong>The threshold is a floor</strong>, not a target — you are detecting regression, not demanding perfection. And <strong>the misses get logged</strong>, because the list of what it got wrong is the actual output; the pass/fail is almost incidental.</p>
<p>Run evals when you change a prompt, a model, or an effort setting — not on every commit. They cost money and take minutes.</p>
<h2 id="what-not-to-do">What Not to Do</h2>
<p><strong>Do not assert on model wording.</strong> <code>strings.Contains(resp, &quot;Paris&quot;)</code> passes today and fails after a model update that phrases it differently. It is not testing your code.</p>
<p><strong>Do not set temperature to zero and call it deterministic.</strong> Sampling parameters are not even accepted on current models, and identical output was never guaranteed regardless.</p>
<p><strong>Do not mock the SDK&rsquo;s types.</strong> Mocking <code>anthropic.Message</code> and its union blocks is a lot of work to test the adapter you wrote to avoid exactly that. Fake your own interface instead.</p>
<p><strong>Do not let integration tests run by default.</strong> A test suite that needs an API key is a test suite that new contributors cannot run, and CI cost that grows with every push.</p>
<p><strong>Do not skip testing the error paths</strong> because they are &ldquo;just the SDK&rsquo;s job&rdquo;. Rate limits, refusals and mid-stream disconnects are the failures you will actually see in production, and they are the cheapest things in this article to cover.</p>
<h2 id="checklist">Checklist</h2>
<ul>
<li>A narrow interface between your logic and the SDK; business code never imports the SDK.</li>
<li>A fake with a queue of canned responses for multi-turn loops.</li>
<li>Unit tests for the loop&rsquo;s contract: one result per tool call, errors handed back, iteration cap honoured.</li>
<li>Golden files for prompt assembly, plus a determinism test.</li>
<li>Integration tests behind <code>//go:build integration</code>, asserting on shape not wording.</li>
<li>An <code>httptest.Server</code> for 429s, 500s and truncated streams.</li>
<li>A test for the refusal path.</li>
<li>Evals behind their own tag, scored against a floor, misses logged.</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>&ldquo;You cannot test LLM code&rdquo; conflates the model with the code around it. The code around it — the prompt builder, the parser, the loop, the tool handlers, the error branches — is ordinary Go, and it becomes easy to test the moment there is an interface between it and the SDK. Push non-determinism out to the edges, cover the edges with evals scored against a floor, and the rest of your suite stays fast, free and green.</p>]]></content:encoded>
      <category>AI Engineering</category>
      <category>Go Programming</category>
      <category>Testing</category>
    </item>
    <item>
      <title>Tool Use in Go: Building an Agent Loop You Can Actually Debug</title>
      <link>https://webdevstation.com/posts/tool-use-in-go-agent-loop/</link>
      <pubDate>Sat, 29 Aug 2026 10:10:00 +0200</pubDate>
      <author>Alex</author>
      <guid isPermaLink="true">https://webdevstation.com/posts/tool-use-in-go-agent-loop/</guid>
      <description>How LLM tool use really works in Go: the agentic loop, the SDK&#39;s tool runner, parallel tool calls, bounded concurrency, returning errors as tool results, and the…</description>
      <content:encoded><![CDATA[<p>&ldquo;Agent&rdquo; is doing a lot of work as a word right now. Strip the marketing off and what is underneath is a <code>for</code> loop: you send a message, the model asks you to run something, you run it, you send the result back, repeat until it stops asking. That is genuinely all it is — and once you have written the loop yourself, most of the mystique evaporates and what is left is a set of very ordinary Go problems.</p>
<h2 id="the-loop-in-full">The Loop, In Full</h2>
<p>Here is a complete manual loop. It is worth reading once even if you end up using the SDK&rsquo;s runner, because everything that goes wrong later is easier to diagnose when you know this shape. It assumes you already have a client and know how to read a response — if not, start with <a href="/posts/calling-claude-from-go/">calling an LLM from Go</a>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> (
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;context&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;encoding/json&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;fmt&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;log&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;github.com/anthropics/anthropic-sdk-go&#34;</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">client</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewClient</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">addTool</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">ToolParam</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Name</span>:        <span style="color:#e6db74">&#34;add&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Description</span>: <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;Add two integers&#34;</span>),
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">InputSchema</span>: <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">ToolInputSchemaParam</span>{
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Properties</span>: <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">any</span>{
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;a&#34;</span>: <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">any</span>{<span style="color:#e6db74">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;integer&#34;</span>},
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;b&#34;</span>: <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">any</span>{<span style="color:#e6db74">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;integer&#34;</span>},
</span></span><span style="display:flex;"><span>            },
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">tools</span> <span style="color:#f92672">:=</span> []<span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">ToolUnionParam</span>{{<span style="color:#a6e22e">OfTool</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">addTool</span>}}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">messages</span> <span style="color:#f92672">:=</span> []<span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">MessageParam</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewUserMessage</span>(<span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewTextBlock</span>(<span style="color:#e6db74">&#34;What is 2 + 3?&#34;</span>)),
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Messages</span>.<span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">MessageNewParams</span>{
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Model</span>:     <span style="color:#e6db74">&#34;claude-opus-5&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">MaxTokens</span>: <span style="color:#ae81ff">16000</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Messages</span>:  <span style="color:#a6e22e">messages</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Tools</span>:     <span style="color:#a6e22e">tools</span>,
</span></span><span style="display:flex;"><span>        })
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Append the assistant turn BEFORE handling the tool calls.</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">messages</span> = append(<span style="color:#a6e22e">messages</span>, <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">ToParam</span>())
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">toolResults</span> []<span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">ContentBlockParamUnion</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">block</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Content</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">variant</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">block</span>.<span style="color:#a6e22e">AsAny</span>().(<span style="color:#66d9ef">type</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">TextBlock</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">variant</span>.<span style="color:#a6e22e">Text</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">ToolUseBlock</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">in</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">A</span> <span style="color:#66d9ef">int</span> <span style="color:#e6db74">`json:&#34;a&#34;`</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">B</span> <span style="color:#66d9ef">int</span> <span style="color:#e6db74">`json:&#34;b&#34;`</span>
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// block.Input is raw JSON — parse it, never string-match it.</span>
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">Unmarshal</span>([]byte(<span style="color:#a6e22e">variant</span>.<span style="color:#a6e22e">JSON</span>.<span style="color:#a6e22e">Input</span>.<span style="color:#a6e22e">Raw</span>()), <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">in</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">result</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%d&#34;</span>, <span style="color:#a6e22e">in</span>.<span style="color:#a6e22e">A</span><span style="color:#f92672">+</span><span style="color:#a6e22e">in</span>.<span style="color:#a6e22e">B</span>)
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">toolResults</span> = append(<span style="color:#a6e22e">toolResults</span>,
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewToolResultBlock</span>(<span style="color:#a6e22e">block</span>.<span style="color:#a6e22e">ID</span>, <span style="color:#a6e22e">result</span>, <span style="color:#66d9ef">false</span>))
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">StopReason</span> <span style="color:#f92672">!=</span> <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">StopReasonToolUse</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">break</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// All results from this turn go back in ONE user message.</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">messages</span> = append(<span style="color:#a6e22e">messages</span>, <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewUserMessage</span>(<span style="color:#a6e22e">toolResults</span><span style="color:#f92672">...</span>))
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Five things in there are load-bearing, and four of them are easy to get subtly wrong.</p>
<p><strong><code>resp.ToParam()</code> converts the response into a history entry.</strong> You must append the assistant&rsquo;s turn — including its tool-call blocks — before you send the results, or the next request has results referring to a call that does not exist in the conversation.</p>
<p><strong>Parse the tool input; never pattern-match the raw string.</strong> <code>variant.JSON.Input.Raw()</code> gives you the JSON to unmarshal. Current models vary their JSON string escaping (Unicode escapes, escaped forward slashes), so anything doing <code>strings.Contains</code> on the serialised input is a bug waiting for a release.</p>
<p><strong>All tool results go back in a single user message.</strong> <code>anthropic.NewUserMessage</code> is variadic for exactly this reason. Splitting results across several messages technically works, and it quietly teaches the model to stop issuing parallel calls — which halves your throughput for no visible reason.</p>
<p><strong><code>StopReason</code> is the exit condition</strong>, not &ldquo;did I see any tool blocks&rdquo;. Check it after you have appended the results, not before.</p>
<p><strong>Every tool call needs a result.</strong> If the model asked for three tools and you return two results, the next request is malformed. Including for the one that failed — which brings us to the most useful trick in this whole article.</p>
<h2 id="errors-are-results-not-exceptions">Errors Are Results, Not Exceptions</h2>
<p>The instinct when a tool fails is to abort the loop. Usually that is wrong. Hand the failure back to the model as a tool result flagged as an error, and it will very often recover on its own — retry with a corrected argument, try a different tool, or tell the user what went wrong:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">out</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">run</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">variant</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// isError = true. The model sees the failure and can adapt.</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">toolResults</span> = append(<span style="color:#a6e22e">toolResults</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewToolResultBlock</span>(<span style="color:#a6e22e">block</span>.<span style="color:#a6e22e">ID</span>, <span style="color:#a6e22e">err</span>.<span style="color:#a6e22e">Error</span>(), <span style="color:#66d9ef">true</span>))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">continue</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">toolResults</span> = append(<span style="color:#a6e22e">toolResults</span>, <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewToolResultBlock</span>(<span style="color:#a6e22e">block</span>.<span style="color:#a6e22e">ID</span>, <span style="color:#a6e22e">out</span>, <span style="color:#66d9ef">false</span>))
</span></span></code></pre></div><p>That third parameter is <code>isError</code>. Getting this right turns a class of hard failures into self-correcting ones.</p>
<p>One caveat worth stating plainly: the error text goes into the model&rsquo;s context, so do not put a raw database error with connection strings and internal hostnames in there. Return the error you would show a careful external user. This is the same discipline as deciding what a sentinel error exposes at your HTTP boundary, which I covered in <a href="/posts/error-handling-in-go/">error handling in Go</a>.</p>
<h2 id="let-the-sdk-drive">Let the SDK Drive</h2>
<p>Once you understand the loop, you mostly do not want to maintain it. The Go SDK&rsquo;s tool runner handles the iteration, and generates the JSON schema from your struct tags:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;github.com/anthropics/anthropic-sdk-go/toolrunner&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">GetWeatherInput</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">City</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;city&#34; jsonschema:&#34;required,description=The city name&#34;`</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">weatherTool</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">toolrunner</span>.<span style="color:#a6e22e">NewBetaToolFromJSONSchema</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;get_weather&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;Get current weather for a city&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">in</span> <span style="color:#a6e22e">GetWeatherInput</span>) (<span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">BetaToolResultBlockParamContentUnion</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">BetaToolResultBlockParamContentUnion</span>{
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">OfText</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">BetaTextBlockParam</span>{
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">Text</span>: <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;The weather in %s is sunny, 22°C&#34;</span>, <span style="color:#a6e22e">in</span>.<span style="color:#a6e22e">City</span>),
</span></span><span style="display:flex;"><span>            },
</span></span><span style="display:flex;"><span>        }, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">runner</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Beta</span>.<span style="color:#a6e22e">Messages</span>.<span style="color:#a6e22e">NewToolRunner</span>(
</span></span><span style="display:flex;"><span>    []<span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">BetaTool</span>{<span style="color:#a6e22e">weatherTool</span>},
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">BetaToolRunnerParams</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">BetaMessageNewParams</span>: <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">BetaMessageNewParams</span>{
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Model</span>:     <span style="color:#e6db74">&#34;claude-opus-5&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">MaxTokens</span>: <span style="color:#ae81ff">16000</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Messages</span>: []<span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">BetaMessageParam</span>{
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewBetaUserMessage</span>(<span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewBetaTextBlock</span>(<span style="color:#e6db74">&#34;What&#39;s the weather in Kyiv?&#34;</span>)),
</span></span><span style="display:flex;"><span>            },
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">MaxIterations</span>: <span style="color:#ae81ff">5</span>,
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">message</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">runner</span>.<span style="color:#a6e22e">RunToCompletion</span>(<span style="color:#a6e22e">ctx</span>)
</span></span></code></pre></div><p>Note the namespace: this lives under <code>client.Beta.Messages</code>, and the types are the <code>Beta*</code> variants — <code>BetaTextBlock</code>, not <code>TextBlock</code>. Mixing the two is the most common compile error here.</p>
<p><code>MaxIterations</code> is not optional decoration. Without a ceiling, a model that gets into a retry rut can loop until your context deadline, and you pay for every turn. Set it to the smallest number that lets legitimate work finish.</p>
<p>If you need to inspect or gate each step — approvals, audit logging, a check before a destructive tool runs — you do not have to drop back to a manual loop. The runner exposes <code>NextMessage()</code> and an <code>All()</code> iterator so you can step it and look at each message, and its <code>Params</code> field lets you adjust the next request. Reach for the manual loop only when you want control the runner genuinely does not expose.</p>
<h2 id="running-tools-concurrently--with-a-limit">Running Tools Concurrently — With a Limit</h2>
<p>When the model asks for four tools in one turn, running them sequentially wastes the whole point. But the naive concurrent version is the same mistake Go developers make everywhere else:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// Don&#39;t. One turn can ask for many tools; this has no ceiling.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">call</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">calls</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">go</span> <span style="color:#a6e22e">run</span>(<span style="color:#a6e22e">call</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Use a bounded group. The results still have to come back in one message, in a fixed order, so index into a preallocated slice rather than appending from goroutines:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;golang.org/x/sync/errgroup&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">results</span> <span style="color:#f92672">:=</span> make([]<span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">ContentBlockParamUnion</span>, len(<span style="color:#a6e22e">calls</span>))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">g</span>, <span style="color:#a6e22e">gctx</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">errgroup</span>.<span style="color:#a6e22e">WithContext</span>(<span style="color:#a6e22e">ctx</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">g</span>.<span style="color:#a6e22e">SetLimit</span>(<span style="color:#ae81ff">4</span>) <span style="color:#75715e">// whatever your slowest downstream can absorb</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span>, <span style="color:#a6e22e">call</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">calls</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">g</span>.<span style="color:#a6e22e">Go</span>(<span style="color:#66d9ef">func</span>() <span style="color:#66d9ef">error</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">out</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">run</span>(<span style="color:#a6e22e">gctx</span>, <span style="color:#a6e22e">call</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Not a group error: hand it to the model instead.</span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">results</span>[<span style="color:#a6e22e">i</span>] = <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewToolResultBlock</span>(<span style="color:#a6e22e">call</span>.<span style="color:#a6e22e">ID</span>, <span style="color:#a6e22e">err</span>.<span style="color:#a6e22e">Error</span>(), <span style="color:#66d9ef">true</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">results</span>[<span style="color:#a6e22e">i</span>] = <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewToolResultBlock</span>(<span style="color:#a6e22e">call</span>.<span style="color:#a6e22e">ID</span>, <span style="color:#a6e22e">out</span>, <span style="color:#66d9ef">false</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">g</span>.<span style="color:#a6e22e">Wait</span>(); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">messages</span> = append(<span style="color:#a6e22e">messages</span>, <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewUserMessage</span>(<span style="color:#a6e22e">results</span><span style="color:#f92672">...</span>))
</span></span></code></pre></div><p>Each goroutine writes one distinct slice element, so no mutex is needed — different elements are different memory. Appending to a shared slice from several goroutines is a different story, and so is writing to a shared map; <a href="/posts/concurrent-map-writing-and-reading-in-go/">concurrent map writing and reading in Go</a> has the failure mode in detail.</p>
<p>Notice that a tool failure returns <code>nil</code> from <code>g.Go</code>. Returning the error would cancel <code>gctx</code> and kill the sibling tool calls, when what you actually want is to report that one failure to the model and let the others finish. The full set of tradeoffs around <code>SetLimit</code> and error propagation is in <a href="/posts/worker-pools-in-go-with-errgroup/">worker pools in Go with errgroup</a>.</p>
<h2 id="designing-the-tools-themselves">Designing the Tools Themselves</h2>
<p>The loop is the easy part. Tool <em>design</em> is where agents get good or stay bad.</p>
<p><strong>The description is the API documentation, and its reader is the model.</strong> A tool called <code>search</code> described as &ldquo;searches&rdquo; will be called wrongly and often. Say what it searches, what it returns, and when <em>not</em> to use it. Most &ldquo;the agent keeps doing the wrong thing&rdquo; problems are description problems.</p>
<p><strong>Fewer, broader tools beat many narrow ones.</strong> Twenty tools that each wrap one endpoint force the model to plan a long chain and give it twenty chances to pick wrong. One <code>query_orders</code> tool with a few well-named parameters usually outperforms <code>get_order</code>, <code>list_orders_by_user</code>, <code>list_orders_by_date</code> and <code>count_orders</code>.</p>
<p><strong>Constrain the schema.</strong> Enums, required fields and explicit types are enforced before your handler runs. Every constraint you express in the schema is a class of invalid call you never have to validate by hand.</p>
<p><strong>Make results terse.</strong> Tool results occupy context on every subsequent turn of the loop. Returning a 400-row JSON dump costs you tokens on turn two, turn three and turn four. Return the fields the model needs to decide what to do next, and nothing else.</p>
<p><strong>Be deliberate about side effects.</strong> The model will call your tools in orders you did not anticipate. Anything that writes, sends, charges or deletes wants an approval gate — step the runner and confirm — or, at minimum, idempotency so a double call is harmless.</p>
<h2 id="guardrails-that-actually-matter-in-production">Guardrails That Actually Matter in Production</h2>
<p>A tool loop has a cost profile unlike a normal handler: every iteration resends the whole conversation. The bill grows quadratically with the number of turns if you are not careful, and three things keep it honest.</p>
<p><strong>Cap the iterations.</strong> <code>MaxIterations</code> on the runner, or a counter in your manual loop. Non-negotiable.</p>
<p><strong>Bound the wall clock.</strong> A deadline on the context that covers the whole loop, not each request:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">cancel</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">WithTimeout</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#ae81ff">5</span><span style="color:#f92672">*</span><span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Minute</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">cancel</span>()
</span></span></code></pre></div><p><strong>Cache the prefix.</strong> Every turn resends the system prompt and the full history. Without prompt caching you pay full price for all of it, every iteration — this is where agent loops get expensive, and it is the one lever with no quality tradeoff at all. It gets <a href="/posts/prompt-caching-llm-cost/">its own article</a>.</p>
<p>Then there is the interaction between concurrency and rate limits. A pool of four tool calls, times however many concurrent user requests, times however many turns each — an agent loop is a very effective way to discover your own rate limits. The token-bucket approach from <a href="/posts/rate-limiting-go-apis/">rate limiting Go APIs</a> works just as well pointed at your own outbound calls as at inbound traffic.</p>
<h2 id="observability-or-you-are-flying-blind">Observability, Or You Are Flying Blind</h2>
<p>When a loop misbehaves, you need to see what the model actually saw. Log per iteration:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">slog</span>.<span style="color:#a6e22e">Info</span>(<span style="color:#e6db74">&#34;agent turn&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;iteration&#34;</span>, <span style="color:#a6e22e">i</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;stop_reason&#34;</span>, <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">StopReason</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;tools_called&#34;</span>, <span style="color:#a6e22e">toolNames</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;input_tokens&#34;</span>, <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Usage</span>.<span style="color:#a6e22e">InputTokens</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;output_tokens&#34;</span>, <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Usage</span>.<span style="color:#a6e22e">OutputTokens</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;cache_read&#34;</span>, <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Usage</span>.<span style="color:#a6e22e">CacheReadInputTokens</span>,
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p>With a request-scoped logger carrying the conversation id (<a href="/posts/structured-logging-in-go-with-slog/">the pattern from the slog post</a>), you can pull the entire trajectory of one run out of your logs — which is the difference between &ldquo;the agent is flaky&rdquo; and &ldquo;on turn three it called <code>search</code> with an empty query because the description was ambiguous&rdquo;.</p>
<h2 id="pitfalls">Pitfalls</h2>
<table>
	<thead>
			<tr>
					<th>Pitfall</th>
					<th>Fix</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Results returned for only some tool calls</td>
					<td>Return one result per <code>tool_use</code> block, failures included</td>
			</tr>
			<tr>
					<td>Tool results split across several user messages</td>
					<td>One user message, all results, variadic <code>NewUserMessage</code></td>
			</tr>
			<tr>
					<td>Assistant turn not appended before results</td>
					<td><code>messages = append(messages, resp.ToParam())</code> first</td>
			</tr>
			<tr>
					<td>String-matching the raw tool input</td>
					<td><code>json.Unmarshal(variant.JSON.Input.Raw())</code></td>
			</tr>
			<tr>
					<td>Mixing <code>TextBlock</code> and <code>BetaTextBlock</code></td>
					<td>Pick a namespace; the runner is <code>Beta.*</code> throughout</td>
			</tr>
			<tr>
					<td>Loop runs until the deadline</td>
					<td><code>MaxIterations</code>, plus a context timeout for the whole loop</td>
			</tr>
			<tr>
					<td>Tool error cancels its siblings</td>
					<td>Return <code>nil</code> from <code>g.Go</code>; hand the error to the model</td>
			</tr>
			<tr>
					<td>Cost grows faster than expected</td>
					<td>Cache the prefix; keep tool results terse</td>
			</tr>
	</tbody>
</table>
<h2 id="conclusion">Conclusion</h2>
<p>The loop is twenty lines and you should write it once by hand, then let the runner own it. After that, the work that actually improves an agent is not loop code at all: sharper tool descriptions, tighter schemas, terser results, a hard iteration cap, and enough logging to reconstruct a bad run. The interesting engineering is in the tools, not the loop around them.</p>]]></content:encoded>
      <category>AI Engineering</category>
      <category>Go Programming</category>
      <category>Backend Development</category>
    </item>
  </channel>
</rss>
