<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Part 2: The Loop | Garage Education</title><link>https://www.garageeducation.org/docs/agent-harness-spring-ai/02-the-loop/</link><atom:link href="https://www.garageeducation.org/docs/agent-harness-spring-ai/02-the-loop/index.xml" rel="self" type="application/rss+xml"/><description>Part 2: The Loop</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 2: The Loop</title><link>https://www.garageeducation.org/docs/agent-harness-spring-ai/02-the-loop/</link></image><item><title>Chapter 5: Writing the agent loop by hand</title><link>https://www.garageeducation.org/docs/agent-harness-spring-ai/02-the-loop/05-writing-the-agent-loop-by-hand/</link><pubDate>Sat, 10 Oct 2026 00:00:00 +0000</pubDate><guid>https://www.garageeducation.org/docs/agent-harness-spring-ai/02-the-loop/05-writing-the-agent-loop-by-hand/</guid><description>&lt;p&gt;The loop has four steps. Send the messages to the model. If the reply has no tool calls, return its text. Otherwise run each tool, add the results to the messages, and repeat.&lt;/p&gt;
&lt;figure class="flow"&gt;
&lt;script type="application/json"&gt;
{
"title": "The agent loop.",
"steps": [
{"label": "1. Send the messages to the model"},
{"label": "2. Does the reply have tool calls?", "no": "Return its text: FINAL_ANSWER"},
{"label": "3. Run each tool"},
{"label": "4. Add the results to the messages"},
{"label": "Is step still ≤ MAX_STEPS?", "no": "Stop: STEP_LIMIT (Chapter 6)"},
{"label": "Repeat from step 1", "tone": "platform"}
]
}
&lt;/script&gt;
&lt;figcaption&gt;Figure 5.1. The four steps, with the step cap from the code below. The other stop rules come in Chapter 6.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Turn off automatic tool execution, so the code runs each tool itself.&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="n"&gt;ToolCallingChatOptions&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;options&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;ToolCallingChatOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&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="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toolCallbacks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ToolCallbacks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logTools&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="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;internalToolExecutionEnabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&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;step&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;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;MAX_STEPS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&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;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ChatResponse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;response&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;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;options&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="n"&gt;AssistantMessage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reply&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getResult&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getOutput&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="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reply&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="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasToolCalls&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;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RunResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getText&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;StopReason&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;FINAL_ANSWER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;step&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="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;// run each tool call, add ToolResponseMessage to messages&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": "A two-step run through the loop.",
"lifelines": [
{"id": "loop", "label": "Your loop", "tone": "platform"},
{"id": "model", "label": "ChatModel", "tone": "subscriber"},
{"id": "tools", "label": "LogTools", "tone": "platform"}
],
"messages": [
{"from": "loop", "to": "model", "label": "step 1: call(new Prompt(messages, options))"},
{"from": "model", "to": "loop", "label": "AssistantMessage with a countErrors call", "kind": "response"},
{"from": "loop", "to": "tools", "label": "run countErrors"},
{"from": "tools", "to": "loop", "label": "412", "kind": "response"},
{"from": "loop", "to": "loop", "label": "add ToolResponseMessage to messages"},
{"from": "loop", "to": "model", "label": "step 2: call(new Prompt(messages, options))"},
{"from": "model", "to": "loop", "label": "AssistantMessage, no tool calls", "kind": "response"},
{"from": "loop", "to": "loop", "label": "return RunResult(text, FINAL_ANSWER, 2)"}
]
}
&lt;/script&gt;
&lt;figcaption&gt;Figure 5.2. A two-step run. Because &lt;code&gt;internalToolExecutionEnabled(false)&lt;/code&gt; is set, your loop runs the tool, not Spring AI. Each step resends the full message list.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;blockquote class="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic text-neutral-600 dark:text-neutral-400 my-6"&gt;
&lt;p&gt;&lt;strong&gt;Verify:&lt;/strong&gt; Confirm the class names and builder methods for your Spring AI version. They change between releases.&lt;/p&gt;
&lt;/blockquote&gt;</description></item><item><title>Chapter 6: Termination</title><link>https://www.garageeducation.org/docs/agent-harness-spring-ai/02-the-loop/06-termination/</link><pubDate>Sat, 10 Oct 2026 00:00:00 +0000</pubDate><guid>https://www.garageeducation.org/docs/agent-harness-spring-ai/02-the-loop/06-termination/</guid><description>&lt;p&gt;A run ends in one of these ways: a final answer, a step limit, a token budget, a timeout, or loop detection. Every run returns a &lt;code&gt;RunResult&lt;/code&gt; with the reason, so failures are easy to read.&lt;/p&gt;
&lt;figure class="flow"&gt;
&lt;script type="application/json"&gt;
{
"title": "Stop checks after each model reply.",
"steps": [
{"label": "A model reply arrives"},
{"label": "Does it have tool calls?", "no": "FINAL_ANSWER"},
{"label": "Below the step limit?", "no": "STEP_LIMIT"},
{"label": "Within the token budget?", "no": "TOKEN_BUDGET"},
{"label": "Within the timeout?", "no": "TIMEOUT"},
{"label": "No repeated tool call?", "no": "LOOP_DETECTED"},
{"label": "Run the tools and continue", "tone": "platform"}
]
}
&lt;/script&gt;
&lt;figcaption&gt;Figure 6.1. Each way a run ends maps to one &lt;code&gt;StopReason&lt;/code&gt;. The chapter does not fix an order for the checks; this order is one reasonable choice. &lt;code&gt;APPROVAL_TIMEOUT&lt;/code&gt; and &lt;code&gt;REJECTED&lt;/code&gt; come from the approval path in Chapter 9.&lt;/figcaption&gt;
&lt;/figure&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="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;RunResult&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;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;StopReason&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;,&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;steps&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;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;enum&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;StopReason&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="n"&gt;FINAL_ANSWER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;STEP_LIMIT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TOKEN_BUDGET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TIMEOUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LOOP_DETECTED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;APPROVAL_TIMEOUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;REJECTED&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="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;</description></item><item><title>Chapter 7: Guardrails</title><link>https://www.garageeducation.org/docs/agent-harness-spring-ai/02-the-loop/07-guardrails/</link><pubDate>Sat, 10 Oct 2026 00:00:00 +0000</pubDate><guid>https://www.garageeducation.org/docs/agent-harness-spring-ai/02-the-loop/07-guardrails/</guid><description>&lt;p&gt;Put guardrails in code, not in the prompt.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Argument checks.&lt;/strong&gt; Reject a &lt;code&gt;minutes&lt;/code&gt; value outside 1 to 1440 before the tool runs. Return the error text to the model.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Allowlist.&lt;/strong&gt; Each agent has a fixed set of tool names. Reject any other name.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Loop detection.&lt;/strong&gt; Count each tool name plus arguments. Stop when one key repeats three times.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class="flow"&gt;
&lt;script type="application/json"&gt;
{
"title": "Three guardrails between a tool call and the tool.",
"steps": [
{"label": "The model asks for a tool call"},
{"label": "Is the tool name in this agent's allowlist?", "no": "Reject the name"},
{"label": "Is minutes between 1 and 1440?", "no": "Return the error text to the model"},
{"label": "Has this name plus arguments repeated fewer than three times?", "no": "Stop: LOOP_DETECTED"},
{"label": "Run the tool", "tone": "platform"}
]
}
&lt;/script&gt;
&lt;figcaption&gt;Figure 7.1. The three guardrails, in front of every tool. Each one is a line of Java, so the model cannot talk its way past it.&lt;/figcaption&gt;
&lt;/figure&gt;</description></item><item><title>Chapter 8: Graphs</title><link>https://www.garageeducation.org/docs/agent-harness-spring-ai/02-the-loop/08-graphs/</link><pubDate>Sat, 10 Oct 2026 00:00:00 +0000</pubDate><guid>https://www.garageeducation.org/docs/agent-harness-spring-ai/02-the-loop/08-graphs/</guid><description>&lt;p&gt;A graph makes the flow explicit. Each stage is a node. Each move between stages is an edge. Keep the control flow in plain Java, so every edge has a unit test. A graph database is a different tool, used for relationships between services. Chapter 8b covers it.&lt;/p&gt;
&lt;figure class="arch"&gt;
&lt;script type="application/json"&gt;
{
"title": "The watcher as a graph of stages.",
"nodes": [
{"id": "search", "x": 570, "y": 0, "w": 160, "label": "Runbook search", "detail": "hybrid + reranker", "tone": "data"},
{"id": "window", "x": 0, "y": 120, "w": 160, "label": "Read error window", "tone": "platform"},
{"id": "fp", "x": 190, "y": 120, "w": 160, "label": "Fingerprint", "detail": "group errors", "tone": "platform"},
{"id": "rate", "x": 380, "y": 120, "w": 160, "label": "Rate limit", "tone": "platform"},
{"id": "triage", "x": 570, "y": 120, "w": 160, "label": "Triage", "detail": "triage-error-group skill", "tone": "ai"},
{"id": "lowmed", "x": 760, "y": 120, "w": 150, "label": "Low or medium", "detail": "not defined yet", "tone": "muted"},
{"id": "empty", "x": 0, "y": 270, "w": 160, "label": "Stop", "detail": "no model call", "tone": "muted"},
{"id": "dup", "x": 190, "y": 270, "w": 160, "label": "Suppress", "detail": "duplicate", "tone": "muted"},
{"id": "flood", "x": 380, "y": 270, "w": 160, "label": "Stop", "detail": "flood hits the limit", "tone": "muted"},
{"id": "approval", "x": 570, "y": 270, "w": 160, "label": "Await approval", "detail": "signal or 1-hour timer", "tone": "operator"},
{"id": "alert", "x": 760, "y": 270, "w": 150, "label": "Post alert", "tone": "external"},
{"id": "reject", "x": 570, "y": 400, "w": 160, "label": "Stop", "detail": "REJECTED or APPROVAL_TIMEOUT", "tone": "muted"}
],
"edges": [
{"from": "window", "to": "fp", "label": "errors"},
{"from": "window", "to": "empty", "label": "empty"},
{"from": "fp", "to": "rate", "label": "new"},
{"from": "fp", "to": "dup", "label": "duplicate"},
{"from": "rate", "to": "triage", "label": "ok"},
{"from": "rate", "to": "flood", "label": "flood"},
{"from": "triage", "to": "search", "label": "runbooks", "both": true},
{"from": "triage", "to": "lowmed", "label": "low, medium"},
{"from": "triage", "to": "approval", "label": "critical"},
{"from": "approval", "to": "alert", "label": "approved"},
{"from": "approval", "to": "reject", "label": "rejected, timeout"}
]
}
&lt;/script&gt;
&lt;figcaption&gt;Figure 8.1. An illustrative watcher graph, assembled from the eval cases in Chapter 20, the triage skill in Chapter 14, and the approval in Chapter 9. Each arrow is one edge to unit-test. The course does not fix the stage order, and it does not define the low and medium path.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The graph above is a teaching sketch, not the course&amp;rsquo;s specification. What the watcher does with low and medium severity is &lt;span class="needs-input"&gt;NEEDS INPUT: the low and medium severity path, for example post without approval or record only&lt;/span&gt;.&lt;/p&gt;</description></item><item><title>Chapter 9: Human-in-the-loop</title><link>https://www.garageeducation.org/docs/agent-harness-spring-ai/02-the-loop/09-human-in-the-loop/</link><pubDate>Sat, 10 Oct 2026 00:00:00 +0000</pubDate><guid>https://www.garageeducation.org/docs/agent-harness-spring-ai/02-the-loop/09-human-in-the-loop/</guid><description>&lt;p&gt;A critical alert waits for a person. The workflow sets its state to awaiting approval and waits for an approval signal or a one-hour timer. Temporal stores the wait, so a restart does not lose it. Chapter 24 shows the code. Keep an audit row in PostgreSQL for each decision.&lt;/p&gt;
&lt;figure class="seq"&gt;
&lt;script type="application/json"&gt;
{
"title": "Approval: wait, decide, and record.",
"lifelines": [
{"id": "wf", "label": "Watcher workflow", "tone": "operator"},
{"id": "person", "label": "Person", "tone": "external"},
{"id": "pg", "label": "PostgreSQL audit", "tone": "platform"},
{"id": "alert", "label": "Alert channel", "tone": "external"}
],
"messages": [
{"from": "wf", "to": "wf", "label": "critical: state = awaiting approval"},
{"from": "wf", "to": "wf", "label": "wait for a signal or a one-hour timer"},
{"from": "wf", "to": "pg", "label": "Temporal stores the wait. A worker restart does not lose it.", "kind": "note"},
{"from": "person", "to": "wf", "label": "approval signal"},
{"from": "wf", "to": "pg", "label": "audit row for the decision"},
{"from": "wf", "to": "alert", "label": "post the alert"},
{"from": "wf", "to": "alert", "label": "No signal in one hour: stop with APPROVAL_TIMEOUT", "kind": "note"}
]
}
&lt;/script&gt;
&lt;figcaption&gt;Figure 9.1. The approval path for a critical alert. A rejection writes the audit row and stops with &lt;code&gt;REJECTED&lt;/code&gt;. How the person is notified is not covered here.&lt;/figcaption&gt;
&lt;/figure&gt;</description></item></channel></rss>