<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Part 5: Observability, Evaluation, and Cost | Garage Education</title><link>https://www.garageeducation.org/docs/agent-harness-spring-ai/05-observability-evaluation-cost/</link><atom:link href="https://www.garageeducation.org/docs/agent-harness-spring-ai/05-observability-evaluation-cost/index.xml" rel="self" type="application/rss+xml"/><description>Part 5: Observability, Evaluation, and Cost</description><generator>HugoBlox Kit (https://hugoblox.com)</generator><language>en-US</language><copyright>©</copyright><lastBuildDate>Sat, 10 Oct 2026 00:00:00 +0000</lastBuildDate><image><url>https://www.garageeducation.org/media/logo.svg</url><title>Part 5: Observability, Evaluation, and Cost</title><link>https://www.garageeducation.org/docs/agent-harness-spring-ai/05-observability-evaluation-cost/</link></image><item><title>Chapter 19: Step logging</title><link>https://www.garageeducation.org/docs/agent-harness-spring-ai/05-observability-evaluation-cost/19-step-logging/</link><pubDate>Sat, 10 Oct 2026 00:00:00 +0000</pubDate><guid>https://www.garageeducation.org/docs/agent-harness-spring-ai/05-observability-evaluation-cost/19-step-logging/</guid><description>&lt;p&gt;Write one JSON line per model call and per tool call. Each line holds the run id, step number, kind, tool name, arguments, token counts, duration, and stop reason. A failure is easy to find when you search by run id.&lt;/p&gt;
&lt;figure class="arch"&gt;
&lt;script type="application/json"&gt;
{
"title": "Every call in the loop leaves one line.",
"nodes": [
{"id": "loop", "type": "boundary", "x": 0, "y": 0, "w": 420, "h": 150, "label": "One run · run id"},
{"id": "m", "x": 20, "y": 50, "w": 180, "label": "Model call", "detail": "step n", "tone": "ai"},
{"id": "t", "x": 220, "y": 50, "w": 180, "label": "Tool call", "detail": "step n", "tone": "platform"},
{"id": "log", "x": 500, "y": 50, "w": 200, "label": "Step log", "detail": "one JSON line per call", "tone": "data"},
{"id": "you", "x": 760, "y": 50, "w": 120, "label": "You", "detail": "search by run id", "tone": "external"}
],
"edges": [
{"from": "m", "to": "log", "fromSide": "bottom", "toSide": "bottom"},
{"from": "t", "to": "log", "label": "one line each"},
{"from": "you", "to": "log", "label": "search"}
]
}
&lt;/script&gt;
&lt;figcaption&gt;Figure 19.1. Both kinds of call write to the same log. Because every line carries the run id, one search returns the whole run in step order.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;An illustrative pair of lines, one per kind. The field names are an example; the chapter fixes the content, not the spelling.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;runId&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;run-42&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;step&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;kind&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;model&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;tool&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;args&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;tokensIn&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;812&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;tokensOut&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;durationMs&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;930&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;stopReason&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;runId&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;run-42&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;step&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;kind&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;tool&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;tool&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;countErrors&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;args&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;service&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;checkout&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;minutes&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;tokensIn&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;tokensOut&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;durationMs&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;stopReason&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Chapter 20: A small eval set</title><link>https://www.garageeducation.org/docs/agent-harness-spring-ai/05-observability-evaluation-cost/20-a-small-eval-set/</link><pubDate>Sat, 10 Oct 2026 00:00:00 +0000</pubDate><guid>https://www.garageeducation.org/docs/agent-harness-spring-ai/05-observability-evaluation-cost/20-a-small-eval-set/</guid><description>&lt;p&gt;An eval case has fixed input and expected properties. Check the stop reason, whether an alert posted, and required phrases. Do not check exact text, because model output varies.&lt;/p&gt;
&lt;figure class="flow"&gt;
&lt;script type="application/json"&gt;
{
"title": "One eval case.",
"steps": [
{"label": "Read the case JSON"},
{"label": "Run the watcher on its fixed input"},
{"label": "Is the stop reason the expected one?", "no": "Fail"},
{"label": "Did an alert post, or not, as expected?", "no": "Fail"},
{"label": "Are the required phrases present?", "no": "Fail"},
{"label": "Pass", "tone": "platform"}
]
}
&lt;/script&gt;
&lt;figcaption&gt;Figure 20.1. One eval case checks properties, never exact text. A case can pass with different wording on every run.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Write these cases first: a duplicate that should be suppressed, a critical error that needs approval, a flood that hits the rate limit, a novel error with no runbook, and an empty window that ends with no model call.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;What it proves&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;duplicate-suppressed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A duplicate is suppressed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;critical-needs-approval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A critical error waits for approval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;flood-hits-rate-limit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A flood hits the rate limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;novel error, no runbook&lt;/td&gt;
&lt;td&gt;The watcher copes with a novel error that has no runbook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;empty window&lt;/td&gt;
&lt;td&gt;An empty window ends with no model call&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p class="caption"&gt;&lt;strong&gt;Table 20.1.&lt;/strong&gt; The first five cases. The last two have no case name in the code yet.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@ParameterizedTest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@ValueSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;duplicate-suppressed&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;critical-needs-approval&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;flood-hits-rate-limit&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;evalCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;caseName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;throws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// read the case JSON, run the watcher, assert the expected properties&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Run the eval before every change. Use plain JUnit 5.&lt;/p&gt;</description></item><item><title>Chapter 21: Failure drills</title><link>https://www.garageeducation.org/docs/agent-harness-spring-ai/05-observability-evaluation-cost/21-failure-drills/</link><pubDate>Sat, 10 Oct 2026 00:00:00 +0000</pubDate><guid>https://www.garageeducation.org/docs/agent-harness-spring-ai/05-observability-evaluation-cost/21-failure-drills/</guid><description>&lt;p&gt;Break one part on purpose and watch the result. Remove a skill description. Return a large tool output. Remove the step cap. Loosen the fingerprint. Remove the citation check. Record the eval score before and after each drill, then revert.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Drill&lt;/th&gt;
&lt;th&gt;What it breaks&lt;/th&gt;
&lt;th&gt;Built in&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Remove a skill description&lt;/td&gt;
&lt;td&gt;Skill selection and the model&amp;rsquo;s choice&lt;/td&gt;
&lt;td&gt;
,
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Return a large tool output&lt;/td&gt;
&lt;td&gt;A small parent context&lt;/td&gt;
&lt;td&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remove the step cap&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;STEP_LIMIT&lt;/code&gt; stop&lt;/td&gt;
&lt;td&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loosen the fingerprint&lt;/td&gt;
&lt;td&gt;Duplicate suppression and the triage cache key&lt;/td&gt;
&lt;td&gt;
,
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remove the citation check&lt;/td&gt;
&lt;td&gt;Cited answers&lt;/td&gt;
&lt;td&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p class="caption"&gt;&lt;strong&gt;Table 21.1.&lt;/strong&gt; Each drill undoes one earlier chapter. The middle column is a teaching guide; the eval score is the real measure.&lt;/p&gt;
&lt;figure class="flow"&gt;
&lt;script type="application/json"&gt;
{
"title": "One drill.",
"steps": [
{"label": "Record the eval score"},
{"label": "Break one part"},
{"label": "Run the eval and record the score"},
{"label": "Revert the change", "tone": "platform"}
]
}
&lt;/script&gt;
&lt;figcaption&gt;Figure 21.1. One drill at a time, always reverted, so each score change has one cause.&lt;/figcaption&gt;
&lt;/figure&gt;</description></item><item><title>Chapter 22: Cost optimization</title><link>https://www.garageeducation.org/docs/agent-harness-spring-ai/05-observability-evaluation-cost/22-cost-optimization/</link><pubDate>Sat, 10 Oct 2026 00:00:00 +0000</pubDate><guid>https://www.garageeducation.org/docs/agent-harness-spring-ai/05-observability-evaluation-cost/22-cost-optimization/</guid><description>&lt;p&gt;Measure tokens per run first. Then add a cache with Spring Cache and Caffeine, keyed by error fingerprint, with a ten-minute expiry. Clear it when a runbook changes. Redis can replace Caffeine later with no code change.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@Cacheable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cacheNames&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;triage&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;#fingerprint&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;unless&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;#result == null&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TriageDecision&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;triage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stackTrace&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;figure class="arch"&gt;
&lt;script type="application/json"&gt;
{
"title": "The triage cache.",
"nodes": [
{"id": "call", "x": 0, "y": 60, "w": 190, "label": "triage(fingerprint, …)", "tone": "platform"},
{"id": "cache", "x": 260, "y": 60, "w": 220, "label": "Caffeine cache \"triage\"", "detail": "key = fingerprint · 10 min", "tone": "data"},
{"id": "model", "x": 560, "y": 60, "w": 180, "label": "Model", "detail": "only on a miss", "tone": "ai"},
{"id": "runbook", "x": 260, "y": 200, "w": 220, "label": "Runbook changes", "tone": "external"},
{"id": "redis", "x": 560, "y": 200, "w": 180, "label": "Redis", "detail": "later, no code change", "tone": "muted"}
],
"edges": [
{"from": "call", "to": "cache", "label": "lookup"},
{"from": "cache", "to": "model", "label": "miss", "animated": true},
{"from": "runbook", "to": "cache", "label": "clear", "dashed": true},
{"from": "redis", "to": "cache", "label": "can replace", "dashed": true, "fromSide": "left", "toSide": "right"}
]
}
&lt;/script&gt;
&lt;figcaption&gt;Figure 22.1. A repeat fingerprint within ten minutes costs no tokens. A null result is never cached, so a failed triage is retried.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Provider prompt caching does not apply to LM Studio, so skip that step. Each step must lower the token count in the eval run. Remove any step that does not.&lt;/p&gt;
&lt;figure class="flow"&gt;
&lt;script type="application/json"&gt;
{
"title": "Keep a cost step only if it pays.",
"steps": [
{"label": "Measure tokens per run"},
{"label": "Add one cost step"},
{"label": "Did the eval run use fewer tokens?", "no": "Remove the step"},
{"label": "Keep it", "tone": "platform"}
]
}
&lt;/script&gt;
&lt;figcaption&gt;Figure 22.2. Every cost step has to show its saving in the eval run.&lt;/figcaption&gt;
&lt;/figure&gt;</description></item></channel></rss>