<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Part 7: Capstone | Garage Education</title><link>https://www.garageeducation.org/docs/agent-harness-spring-ai/07-capstone/</link><atom:link href="https://www.garageeducation.org/docs/agent-harness-spring-ai/07-capstone/index.xml" rel="self" type="application/rss+xml"/><description>Part 7: Capstone</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 7: Capstone</title><link>https://www.garageeducation.org/docs/agent-harness-spring-ai/07-capstone/</link></image><item><title>Chapter 25: Assemble the Log Incident Watcher</title><link>https://www.garageeducation.org/docs/agent-harness-spring-ai/07-capstone/25-assemble-the-log-incident-watcher/</link><pubDate>Sat, 10 Oct 2026 00:00:00 +0000</pubDate><guid>https://www.garageeducation.org/docs/agent-harness-spring-ai/07-capstone/25-assemble-the-log-incident-watcher/</guid><description>&lt;p&gt;The capstone runs every part together. Each run is a Temporal workflow. Each model call and each tool call is an activity. Approval is a signal. Memory is Spring AI Session. Retrieval is hybrid search with a cross-encoder reranker.&lt;/p&gt;
&lt;figure class="arch"&gt;
&lt;script type="application/json"&gt;
{
"title": "The Log Incident Watcher, assembled.",
"nodes": [
{"id": "worker", "type": "boundary", "x": 0, "y": 110, "w": 880, "h": 270, "label": "Spring Boot app · worker \"watcher\""},
{"id": "person", "x": 0, "y": 0, "w": 160, "label": "Person", "detail": "starts runs, approves", "tone": "external"},
{"id": "rest", "x": 200, "y": 0, "w": 160, "label": "REST endpoint", "detail": "start, query, approve", "tone": "platform"},
{"id": "temporal", "x": 400, "y": 0, "w": 220, "label": "Temporal server", "detail": "history, timers, signals", "tone": "operator"},
{"id": "wf", "x": 400, "y": 150, "w": 220, "label": "WatcherWorkflowImpl", "detail": "the loop, stop rules, approval wait", "tone": "operator"},
{"id": "tool", "x": 80, "y": 280, "w": 180, "label": "ToolActivity", "detail": "allowlist, run, save result", "tone": "platform"},
{"id": "model", "x": 420, "y": 280, "w": 180, "label": "ModelActivity", "detail": "read session, call, save last", "tone": "platform"},
{"id": "alert", "x": 680, "y": 280, "w": 180, "label": "AlertActivity", "detail": "only after approval", "tone": "platform"},
{"id": "rerank", "x": 0, "y": 440, "w": 160, "label": "Reranker", "detail": "bge-reranker-v2-m3", "tone": "ai"},
{"id": "pg", "x": 210, "y": 440, "w": 200, "label": "PostgreSQL", "detail": "Session, corpus, audit", "tone": "data"},
{"id": "gemma", "x": 420, "y": 440, "w": 180, "label": "Gemma", "detail": "LM Studio", "tone": "ai"},
{"id": "channel", "x": 680, "y": 440, "w": 180, "label": "Alert channel", "tone": "external"}
],
"edges": [
{"from": "person", "to": "rest"},
{"from": "rest", "to": "temporal"},
{"from": "temporal", "to": "wf", "label": "workflow tasks", "both": true},
{"from": "wf", "to": "tool", "fromSide": "left", "toSide": "top"},
{"from": "wf", "to": "model", "label": "activities"},
{"from": "wf", "to": "alert", "fromSide": "right", "toSide": "top"},
{"from": "tool", "to": "rerank", "fromSide": "bottom", "toSide": "top"},
{"from": "tool", "to": "pg", "fromSide": "bottom", "toSide": "top"},
{"from": "model", "to": "pg", "fromSide": "left", "toSide": "top"},
{"from": "model", "to": "gemma", "label": "chat", "animated": true},
{"from": "alert", "to": "channel"}
]
}
&lt;/script&gt;
&lt;figcaption&gt;Figure 25.1. Every part of the course in one picture. The workflow only schedules; the three activities do all the I/O. The figure leaves out the step log from Chapter 19 and the triage cache from Chapter 22.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Temporal&amp;rsquo;s Spring AI integration does not yet support Spring AI 2.x. The integration is in public preview with Spring AI 1.1.0 listed. Upgrading it is tracked in temporalio/ai-integrations PR #40. This chapter uses plain activities that do the same job.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Activity&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ModelActivity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reads the session, calls the model, saves the reply last&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ToolActivity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Checks the allowlist, runs the tool, saves the result&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AlertActivity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Posts the alert, only after approval&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The workflow runs the loop. It stops on a final answer, a &lt;code&gt;submitFinding&lt;/code&gt; tool call, the step limit, loop detection, an approval timeout, or a rejection. It never calls a model or a tool itself.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stop rule&lt;/th&gt;
&lt;th&gt;Limit&lt;/th&gt;
&lt;th&gt;&lt;code&gt;StopReason&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Final answer&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FINAL_ANSWER&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;submitFinding&lt;/code&gt; tool call&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;span class="needs-input"&gt;NEEDS INPUT: which StopReason a submitFinding stop returns; the enum has no dedicated value&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Step limit&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MAX_STEPS = 12&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;STEP_LIMIT&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loop detection&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MAX_REPEATS = 3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LOOP_DETECTED&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approval timeout&lt;/td&gt;
&lt;td&gt;one hour (Chapter 9)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;APPROVAL_TIMEOUT&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rejection&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;REJECTED&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p class="caption"&gt;&lt;strong&gt;Table 25.1.&lt;/strong&gt; The capstone's stop rules mapped to the &lt;code&gt;StopReason&lt;/code&gt; enum from Chapter 6.&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;@WorkflowImpl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workers&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;watcher&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WatcherWorkflowImpl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;implements&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;WatcherWorkflow&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="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;static&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;MAX_STEPS&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="n"&gt;12&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="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;static&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;MAX_REPEATS&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="n"&gt;3&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;// activity stubs for model, tools, and alerts&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;// loop: model turn, then each tool call, then stop rules&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;figure class="seq"&gt;
&lt;script type="application/json"&gt;
{
"title": "One critical run, end to end.",
"lifelines": [
{"id": "rest", "label": "REST endpoint", "tone": "platform"},
{"id": "wf", "label": "WatcherWorkflowImpl", "tone": "operator"},
{"id": "model", "label": "ModelActivity", "tone": "platform"},
{"id": "tool", "label": "ToolActivity", "tone": "platform"},
{"id": "alert", "label": "AlertActivity", "tone": "platform"},
{"id": "person", "label": "Person", "tone": "external"}
],
"messages": [
{"from": "rest", "to": "wf", "label": "start run(WatchRequest)"},
{"from": "wf", "to": "model", "label": "model turn"},
{"from": "model", "to": "model", "label": "read session, call Gemma, save reply last"},
{"from": "model", "to": "wf", "label": "reply with tool calls", "kind": "response"},
{"from": "wf", "to": "tool", "label": "each tool call"},
{"from": "tool", "to": "tool", "label": "check allowlist, run tool, save result"},
{"from": "tool", "to": "wf", "label": "results", "kind": "response"},
{"from": "wf", "to": "wf", "label": "check stop rules: steps ≤ 12, repeats &lt; 3"},
{"from": "wf", "to": "tool", "label": "Repeat model turns until the model calls submitFinding", "kind": "note"},
{"from": "wf", "to": "wf", "label": "finding is critical: wait for approval"},
{"from": "person", "to": "wf", "label": "signal: decideApproval(true)"},
{"from": "wf", "to": "alert", "label": "post the alert"},
{"from": "alert", "to": "wf", "label": "posted", "kind": "response"},
{"from": "wf", "to": "rest", "label": "RunResult", "kind": "response"}
]
}
&lt;/script&gt;
&lt;figcaption&gt;Figure 25.2. A critical run, end to end. Step through it with Previous and Next. Placing the approval after &lt;code&gt;submitFinding&lt;/code&gt; combines this chapter with Chapter 9.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;Rule to teach.&lt;/strong&gt; A retried activity can save its message twice if it fails after saving. Save last. Verify whether Session accepts a message id for idempotent saves.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Switching later.&lt;/strong&gt; When &lt;code&gt;io.temporal:spring-ai&lt;/code&gt; ships a public release, replace the model activity with its chat model class. The stop rules and approval code stay the same.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Final exercise.&lt;/strong&gt; Run the full eval set. Break one drill from Chapter 21, fix it, and record the final score.&lt;/p&gt;</description></item></channel></rss>